// Copyright (C) 2026 Elias Wendland // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 exclusively. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::plugin::Plugin; use rust_ffmpeg::FFmpegBuilder; use std::io::Write; use std::path::Path; pub struct PluginImpl; impl Plugin for PluginImpl { fn name(&self) -> &'static str { "ffmpeg_audio" } fn from_formats(&self) -> Vec<&'static str> { vec![ "mp3", "ogg", "aac", "m4a", "opus", "wma", "mka", "mp2", "ra", "wav", "flac", "alac", "aiff", "au", "caf", "w64", "ac3", "eac3", "dts", "truehd", "thd", "ape", "wv", "tta", "amr", "spx", "gsm", "voc", ] } fn to_formats(&self) -> Vec<&'static str> { vec![ "mp3", "ogg", "aac", "m4a", "opus", "wma", "mka", "mp2", "ra", "wav", "flac", "alac", "aiff", "au", "caf", "w64", "ac3", "eac3", "dts", "truehd", "thd", "ape", "wv", "tta", "amr", "spx", "gsm", "voc", ] } #[tracing::instrument(skip(self))] fn is_available(&self) -> bool { tracing::trace!("Checking availability of ffmpeg for ffmpeg_audio plugin"); let available = std::process::Command::new("ffmpeg") .arg("-version") .output() .is_ok(); tracing::debug!("ffmpeg_audio plugin available: {}", available); available } fn familiarity(&self, _from: &str, to: &str) -> u8 { match to { "mp3" | "wav" => 255, "m4a" | "aac" | "flac" => 240, "ogg" | "opus" | "alac" => 220, "ac3" | "eac3" | "dts" => 180, "wma" | "amr" | "mka" | "mp2" => 150, "aiff" | "au" | "caf" | "w64" => 120, "ape" | "wv" | "tta" | "truehd" | "thd" => 100, "spx" | "gsm" | "voc" | "ra" => 80, _ => 128, } } fn quality(&self, _from: &str, to: &str) -> u8 { match to { "flac" | "wav" | "aiff" | "au" | "alac" | "ape" | "wv" | "tta" | "caf" | "w64" | "truehd" | "thd" => 255, "opus" | "ogg" | "aac" | "m4a" | "eac3" | "dts" => 220, "mp3" | "ac3" | "mka" | "mp2" | "ra" => 200, "wma" | "amr" | "spx" | "gsm" | "voc" => 150, _ => 128, } } fn speed(&self, _from: &str, to: &str) -> u8 { match to { "wav" | "aiff" | "au" | "caf" | "w64" | "voc" => 255, "flac" | "alac" | "wv" | "mp2" | "ac3" | "dts" => 200, "mp3" | "aac" | "m4a" | "ogg" | "mka" | "eac3" => 180, "opus" | "wma" | "amr" | "ape" | "tta" | "truehd" | "thd" | "spx" | "gsm" | "ra" => 150, _ => 128, } } #[tracing::instrument(skip(self, input, temp_dir))] fn convert( &self, input: &[u8], _from: &str, to: &str, temp_dir: &Path, ) -> Result, String> { tracing::debug!("ffmpeg_audio starting conversion: {} -> {}", _from, to); let mut temp_in = tempfile::Builder::new() .suffix(&format!(".{}", _from)) .tempfile_in(temp_dir) .map_err(|e| { tracing::error!("Failed to create temp input file: {}", e); e.to_string() })?; temp_in.write_all(input).map_err(|e| { tracing::error!("Failed to write to temp input file: {}", e); e.to_string() })?; let temp_out = tempfile::Builder::new() .suffix(&format!(".{}", to)) .tempfile_in(temp_dir) .map_err(|e| { tracing::error!("Failed to create temp output file: {}", e); e.to_string() })?; let temp_out_path = temp_out.into_temp_path(); let in_path = temp_in.path().to_path_buf(); tracing::trace!("Temp files created. In: {:?}, Out: {:?}", in_path, temp_out_path); let mut raw_args = vec![]; match to { "mp3" => raw_args.extend(vec!["-c:a", "libmp3lame", "-q:a", "2"]), "ogg" => raw_args.extend(vec!["-c:a", "libvorbis", "-q:a", "4"]), "aac" | "m4a" | "mka" => raw_args.extend(vec!["-c:a", "aac", "-b:a", "192k"]), "opus" => raw_args.extend(vec!["-c:a", "libopus", "-b:a", "128k"]), "mp2" => raw_args.extend(vec!["-c:a", "mp2", "-b:a", "192k"]), "wma" => raw_args.extend(vec!["-c:a", "wmav2", "-b:a", "192k"]), "flac" => raw_args.extend(vec!["-c:a", "flac"]), "alac" => raw_args.extend(vec!["-c:a", "alac"]), "ape" => raw_args.extend(vec!["-c:a", "ape"]), "wv" => raw_args.extend(vec!["-c:a", "wavpack"]), "tta" => raw_args.extend(vec!["-c:a", "tta"]), "ac3" => raw_args.extend(vec!["-c:a", "ac3", "-b:a", "384k"]), "eac3" => raw_args.extend(vec!["-c:a", "eac3", "-b:a", "640k"]), "dts" => raw_args.extend(vec!["-c:a", "dca", "-strict", "-2", "-b:a", "1536k"]), "truehd" | "thd" => raw_args.extend(vec!["-c:a", "truehd", "-strict", "-2"]), "wav" | "aiff" | "au" | "caf" | "w64" => raw_args.extend(vec!["-c:a", "pcm_s16le"]), "amr" => raw_args.extend(vec![ "-ar", "8000", "-c:a", "libopencore_amrnb", "-b:a", "12.2k", ]), "spx" => raw_args.extend(vec!["-c:a", "libspeex"]), "gsm" => raw_args.extend(vec!["-ar", "8000", "-c:a", "libgsm"]), _ => raw_args.extend(vec!["-c:a", "copy"]), } tracing::debug!("Built ffmpeg arguments: {:?}", raw_args); let rt = tokio::runtime::Runtime::new().map_err(|e| { tracing::error!("Failed to create Tokio runtime: {}", e); e.to_string() })?; rt.block_on(async { tracing::trace!("Executing FFmpegBuilder..."); FFmpegBuilder::new() .map_err(|e| { tracing::error!("Failed to init FFmpegBuilder: {}", e); e.to_string() })? .input_path(in_path) .output_path(temp_out_path.to_path_buf()) .raw_args(raw_args) .overwrite() .run() .await .map_err(|e| { tracing::error!("FFmpeg execution failed: {}", e); e.to_string() }) })?; tracing::debug!("FFmpeg execution succeeded. Reading output file..."); std::fs::read(&temp_out_path).map_err(|e| { tracing::error!("Failed to read output file {:?}: {}", temp_out_path, e); e.to_string() }) } }