Skip to content

Commit

Permalink
detect file metadata with ffprobe
Browse files Browse the repository at this point in the history
  • Loading branch information
jelni committed Dec 30, 2024
1 parent d510fee commit f1913b9
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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
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
Expand All @@ -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/
Expand Down
7 changes: 7 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub enum CommandError {
Telegram(TdError),
Server(StatusCode),
Reqwest(reqwest::Error),
SerdeJson(serde_json::Error),
Download(DownloadError),
}

Expand Down Expand Up @@ -117,6 +118,12 @@ impl From<reqwest::Error> for CommandError {
}
}

impl From<serde_json::Error> for CommandError {
fn from(value: serde_json::Error) -> Self {
Self::SerdeJson(value)
}
}

impl From<DownloadError> for CommandError {
fn from(value: DownloadError) -> Self {
Self::Download(value)
Expand Down
61 changes: 45 additions & 16 deletions src/commands/cobalt_download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<InputMessageContent, CommandError> {
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::<f32>().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::<f32>().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,
})
}))
}
1 change: 1 addition & 0 deletions src/utilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/utilities/command_dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
45 changes: 45 additions & 0 deletions src/utilities/ffprobe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use std::path::Path;

use serde::Deserialize;
use tokio::process::Command;

#[derive(Deserialize)]
pub struct Ffprobe {
pub streams: Option<Vec<Streams>>,
pub format: Option<Format>,
}

#[derive(Deserialize)]
pub struct Streams {
pub codec_type: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
pub tags: Option<Tags>,
}

#[derive(Deserialize)]
pub struct Tags {
pub title: Option<String>,
pub artist: Option<String>,
}

#[derive(Deserialize)]
pub struct Format {
pub duration: String,
}

pub async fn ffprobe(path: &Path) -> serde_json::Result<Ffprobe> {
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)
}

0 comments on commit f1913b9

Please sign in to comment.