From 5eea9f2fef1a840f9c9e6ec6d471fe4f87f1d1fd Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 9 Sep 2026 12:50:56 +0200 Subject: make /grajcuj w\ compress finally work as compressing a file and then /grajcuj'ing Signed-off-by: salmonik --- src/commands/utilities.rs | 72 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) (limited to 'src/commands/utilities.rs') diff --git a/src/commands/utilities.rs b/src/commands/utilities.rs index 3f7e812..337979e 100644 --- a/src/commands/utilities.rs +++ b/src/commands/utilities.rs @@ -17,7 +17,7 @@ type Context<'a> = poise::Context<'a, Data, Error>; const LARGE_FILE_THRESHOLD: usize = 10 * 1024 * 1024; #[derive(Clone, Copy, PartialEq, Eq)] -enum FileType { +pub(crate) enum FileType { Video, Audio, Image, @@ -29,7 +29,7 @@ enum FileType { /// Mirrors Python's `mimetypes.guess_type`-based `filetype()` helper without /// pulling in a mime crate: the set of extensions below covers everything the /// original bot handled. -fn file_type(filename: &str) -> FileType { +pub(crate) fn file_type(filename: &str) -> FileType { let Some((_, ext)) = filename.rsplit_once('.') else { return FileType::Unknown; }; @@ -58,7 +58,7 @@ fn ext_of(filename: &str) -> Option { } /// Filename stem: everything before the final dot of the basename. -fn stem_of(filename: &str) -> &str { +pub(crate) fn stem_of(filename: &str) -> &str { let base = filename.rsplit('/').next().unwrap_or(filename); match base.rsplit_once('.') { Some((stem, _)) if !stem.is_empty() => stem, @@ -134,7 +134,7 @@ async fn resolve_input( /// - Video keeps its container extension (ffmpeg re-muxes in place). /// - Audio is always re-encoded to Opus in an `.ogg` container. /// - JPEG/GIF keep their extension; other images are rewritten to `.jpg`. -fn compress_output_ext(ft: FileType, original: &str) -> String { +pub(crate) fn compress_output_ext(ft: FileType, original: &str) -> String { let orig = ext_of(original); match ft { FileType::Video => orig.unwrap_or_else(|| "mp4".to_string()), @@ -149,7 +149,7 @@ fn compress_output_ext(ft: FileType, original: &str) -> String { /// Build the ffmpeg/magick command for extreme-low-quality compression. /// Returns `None` for unsupported (`Unknown`) file types. -fn build_compress_command( +pub(crate) fn build_compress_command( ft: FileType, input: &str, output: &str, @@ -266,6 +266,68 @@ async fn compress_attachment_bytes( res.map(|b| (b, out_filename)) } +/// Compress audio bytes for playback using Vorbis (symphonia-decodable). +/// Uses Vorbis instead of Opus because symphonia 0.6 lacks an Opus decoder. +/// Returns the compressed .ogg bytes on success. +pub(crate) async fn compress_audio_bytes(input_bytes: &[u8], original_name: &str) -> Option> { + let safe_name = sanitize_filename(original_name); + let safe_name = if safe_name.is_empty() { "audio".to_string() } else { safe_name }; + let stem = stem_of(&safe_name).to_string(); + let out_filename = format!("compressed_{stem}.ogg"); + let input_path = temp_path_for(&safe_name); + let output_path = temp_path_for(&out_filename); + let cleanup_paths = [input_path.as_str(), output_path.as_str()]; + + let _ = tokio::fs::create_dir_all(crate::config::TEMP_PATH).await; + if let Err(e) = tokio::fs::write(&input_path, input_bytes).await { + tracing::warn!("compress_audio_bytes: failed to write input: {e}"); + return None; + } + tracing::debug!( + input = %input_path, + output = %output_path, + bytes = input_bytes.len(), + "compress_audio_bytes: running ffmpeg" + ); + + // Opus at 500bps, 12 kHz, mono — same as /compress. + // Decoded via ffmpeg in the caller (symphonia lacks Opus support). + let mut cmd = tokio::process::Command::new("ffmpeg"); + cmd.arg("-y") + .arg("-i") + .arg(&input_path) + .arg("-c:a") + .arg("libopus") + .arg("-b:a") + .arg("500") + .arg("-ar") + .arg("12000") + .arg("-ac") + .arg("1") + .arg(&output_path); + + let res = match cmd.output().await { + Ok(out) if out.status.success() => tokio::fs::read(&output_path).await.ok(), + Ok(out) => { + let stderr = String::from_utf8_lossy(&out.stderr); + tracing::warn!( + input = %input_path, + output = %output_path, + exit = %out.status, + "compress_audio_bytes ffmpeg failed:\n{}", + stderr.chars().take(500).collect::() + ); + None + } + Err(e) => { + tracing::warn!("compress_audio_bytes ffmpeg spawn error: {e}"); + None + } + }; + cleanup(&cleanup_paths).await; + res +} + /// Strip characters that Discord/coping filenames can't safely carry. Mirrors /// Python's `Downloader._sanitize_filename`. fn sanitize_filename(name: &str) -> String { -- cgit v1.2.3