From f1913b9499cd6462e6d8498bc5de6e71f09b55cd Mon Sep 17 00:00:00 2001 From: jel <25802745+jelni@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:13:18 +0100 Subject: [PATCH] detect file metadata with ffprobe --- Cargo.toml | 2 +- Dockerfile | 7 ++-- src/commands.rs | 7 ++++ src/commands/cobalt_download.rs | 61 +++++++++++++++++++++-------- src/utilities.rs | 1 + src/utilities/command_dispatcher.rs | 1 + src/utilities/ffprobe.rs | 45 +++++++++++++++++++++ 7 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 src/utilities/ffprobe.rs diff --git a/Cargo.toml b/Cargo.toml index ce39941..fc20997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,5 +31,5 @@ serde_json = "1.0" tdlib = { git = "https://github.com/jelni/tdlib-rs-latest" } tempfile = "3.14" time = { version = "0.3", features = ["macros", "serde", "serde-well-known"] } -tokio = { version = "1.42", features = ["macros", "rt-multi-thread", "signal", "time"] } +tokio = { version = "1.42", features = ["macros", "process", "rt-multi-thread", "signal", "time"] } url = "2.5" diff --git a/Dockerfile b/Dockerfile index 8ce7ba5..c5d3741 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian as tdlib-builder +FROM debian AS tdlib-builder RUN apt update && apt install git make cmake g++ libssl-dev zlib1g-dev gperf -y RUN git clone https://github.com/tdlib/td WORKDIR /td/build @@ -6,7 +6,7 @@ RUN git checkout $TDLIB_COMMIT_HASH RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. RUN cmake --build . --target install -FROM rust as bot-builder +FROM rust AS bot-builder COPY --from=tdlib-builder /td/tdlib/lib /usr/local/lib RUN ldconfig WORKDIR /app @@ -19,7 +19,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ cargo build --release \ && cp target/release/craiyon-bot craiyon-bot -FROM rust +FROM debian:testing-slim +RUN apt update && apt install ffmpeg -y COPY --from=tdlib-builder /td/tdlib/lib /usr/local/lib RUN ldconfig COPY --from=bot-builder /app/craiyon-bot /app/ diff --git a/src/commands.rs b/src/commands.rs index c33b117..7776046 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -67,6 +67,7 @@ pub enum CommandError { Telegram(TdError), Server(StatusCode), Reqwest(reqwest::Error), + SerdeJson(serde_json::Error), Download(DownloadError), } @@ -117,6 +118,12 @@ impl From for CommandError { } } +impl From for CommandError { + fn from(value: serde_json::Error) -> Self { + Self::SerdeJson(value) + } +} + impl From for CommandError { fn from(value: DownloadError) -> Self { Self::Download(value) diff --git a/src/commands/cobalt_download.rs b/src/commands/cobalt_download.rs index 5df7bb4..48fe473 100644 --- a/src/commands/cobalt_download.rs +++ b/src/commands/cobalt_download.rs @@ -11,13 +11,13 @@ use tdlib::types::{ InputMessageReplyToMessage, InputMessageVideo, InputThumbnail, }; -use super::{CommandResult, CommandTrait}; +use super::{CommandError, CommandResult, CommandTrait}; use crate::apis::cobalt::{self, Error, Response}; use crate::utilities::command_context::CommandContext; use crate::utilities::convert_argument::{ConvertArgument, StringGreedyOrReply}; use crate::utilities::file_download::NetworkFile; use crate::utilities::message_entities::{self, ToEntity}; -use crate::utilities::telegram_utils; +use crate::utilities::{ffprobe, telegram_utils}; const TWITTER_REPLACEMENTS: [&str; 5] = ["fxtwitter.com", "fixupx.com", "twittpr.com", "vxtwitter.com", "fixvx.com"]; @@ -145,7 +145,7 @@ async fn send_files(ctx: &CommandContext, instance: &str, result: Response) -> C .message_queue .wait_for_message( ctx.reply_custom( - get_message_content(&file.filename, &network_file), + get_message_content(&file.filename, &network_file).await?, Some(telegram_utils::donate_markup( "≫ cobalt", "https://cobalt.tools/donate", @@ -304,44 +304,73 @@ async fn send_files(ctx: &CommandContext, instance: &str, result: Response) -> C Ok(()) } -fn get_message_content(filename: &str, file: &NetworkFile) -> InputMessageContent { +async fn get_message_content( + filename: &str, + file: &NetworkFile, +) -> Result { let input_file = InputFile::Local(InputFileLocal { path: file.file_path.to_str().unwrap().into() }); if let Some(file_extension) = Path::new(filename).extension() { if file_extension.eq_ignore_ascii_case("mp4") { - return InputMessageContent::InputMessageVideo(InputMessageVideo { + let ffprobe = ffprobe::ffprobe(&file.file_path).await?; + + let video_stream = ffprobe.streams.and_then(|streams| { + streams.into_iter().find(|stream| { + stream.codec_type.as_ref().is_some_and(|codec_type| codec_type == "video") + }) + }); + + return Ok(InputMessageContent::InputMessageVideo(InputMessageVideo { video: input_file, thumbnail: None, added_sticker_file_ids: Vec::new(), - duration: 0, - width: 0, - height: 0, + #[expect(clippy::cast_possible_truncation)] + duration: ffprobe + .format + .map(|format| format.duration.parse::().unwrap() as i32) + .unwrap_or_default(), + width: video_stream.as_ref().and_then(|stream| stream.width).unwrap_or_default(), + height: video_stream.and_then(|stream| stream.height).unwrap_or_default(), supports_streaming: true, caption: None, show_caption_above_media: false, self_destruct_type: None, has_spoiler: false, - }); + })); } else if ["mp3", "opus", "weba"] .into_iter() .any(|extension| file_extension.eq_ignore_ascii_case(extension)) { - return InputMessageContent::InputMessageAudio(InputMessageAudio { + let ffprobe = ffprobe::ffprobe(&file.file_path).await?; + + let audio_stream = ffprobe.streams.and_then(|streams| { + streams.into_iter().find(|stream| { + stream.codec_type.as_ref().is_some_and(|codec_type| codec_type == "audio") + }) + }); + + let tags = audio_stream.and_then(|stream| stream.tags); + + return Ok(InputMessageContent::InputMessageAudio(InputMessageAudio { audio: input_file, album_cover_thumbnail: None, - duration: 0, - title: String::new(), - performer: String::new(), + #[expect(clippy::cast_possible_truncation)] + duration: ffprobe + .format + .map(|format| format.duration.parse::().unwrap() as i32) + .unwrap_or_default(), + title: tags.as_ref().and_then(|tags| tags.title.clone()).unwrap_or_default(), + performer: tags.and_then(|tags| tags.artist).unwrap_or_default(), caption: None, - }); + })); } } - InputMessageContent::InputMessageDocument(InputMessageDocument { + Ok(InputMessageContent::InputMessageDocument(InputMessageDocument { document: input_file, thumbnail: None, disable_content_type_detection: true, caption: None, - }) + })) } diff --git a/src/utilities.rs b/src/utilities.rs index 1e6f2cb..7d0f951 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -6,6 +6,7 @@ pub mod command_dispatcher; pub mod command_manager; pub mod config; pub mod convert_argument; +pub mod ffprobe; pub mod file_download; pub mod google_translate; pub mod image_utils; diff --git a/src/utilities/command_dispatcher.rs b/src/utilities/command_dispatcher.rs index 4fc8f68..7a065b3 100644 --- a/src/utilities/command_dispatcher.rs +++ b/src/utilities/command_dispatcher.rs @@ -121,6 +121,7 @@ async fn report_command_error( log::error!("HTTP error in the {command} command: {text}"); context.reply(text).await? } + CommandError::SerdeJson(err) => context.reply(format!("JSON parse error: {err}")).await?, CommandError::Download(err) => match err { DownloadError::RequestError(err) => { log::warn!("cobalt download failed: {err}"); diff --git a/src/utilities/ffprobe.rs b/src/utilities/ffprobe.rs new file mode 100644 index 0000000..defaa9e --- /dev/null +++ b/src/utilities/ffprobe.rs @@ -0,0 +1,45 @@ +use std::path::Path; + +use serde::Deserialize; +use tokio::process::Command; + +#[derive(Deserialize)] +pub struct Ffprobe { + pub streams: Option>, + pub format: Option, +} + +#[derive(Deserialize)] +pub struct Streams { + pub codec_type: Option, + pub width: Option, + pub height: Option, + pub tags: Option, +} + +#[derive(Deserialize)] +pub struct Tags { + pub title: Option, + pub artist: Option, +} + +#[derive(Deserialize)] +pub struct Format { + pub duration: String, +} + +pub async fn ffprobe(path: &Path) -> serde_json::Result { + let output = Command::new("ffprobe") + .arg("-v") + .arg("quiet") + .arg("-output_format") + .arg("json") + .arg("-show_format") + .arg("-show_streams") + .arg(path) + .output() + .await + .unwrap(); + + serde_json::from_slice(&output.stdout) +}