use std::path::PathBuf;
use std::time::Duration;
use crate::client::Libraries;
use crate::download::config::postprocess::PostProcessConfig;
use crate::error::{Error, Result};
use crate::executor::Executor;
pub async fn apply_postprocess(
input_path: impl Into<PathBuf>,
output_path: impl Into<PathBuf>,
config: &PostProcessConfig,
libraries: &Libraries,
timeout: Duration,
) -> Result<PathBuf> {
let input_path: PathBuf = input_path.into();
let output_path: PathBuf = output_path.into();
tracing::debug!(
input_path = ?input_path,
output_path = ?output_path,
is_empty = config.is_empty(),
timeout_secs = timeout.as_secs(),
"✂️ Applying post-processing to video file"
);
if config.is_empty() {
tracing::debug!(
input_path = ?input_path,
"✂️ No post-processing needed, returning input path"
);
return Ok(input_path);
}
let input_str = input_path.to_str().ok_or_else(|| Error::PathValidation {
path: input_path.clone(),
reason: "Invalid UTF-8 in path".to_string(),
})?;
let output_str = output_path.to_str().ok_or_else(|| Error::PathValidation {
path: output_path.clone(),
reason: "Invalid UTF-8 in path".to_string(),
})?;
let args = build_ffmpeg_command(input_str, output_str, config)?;
tracing::debug!(
input_path = ?input_path,
output_path = ?output_path,
arg_count = args.len(),
"✂️ Executing FFmpeg post-processing command"
);
let executor = Executor::new(libraries.ffmpeg.clone(), args, timeout);
let result = executor.execute().await;
match &result {
Ok(_) => tracing::debug!(
output_path = ?output_path,
"✅ Post-processing completed successfully"
),
Err(e) => tracing::warn!(
input_path = ?input_path,
output_path = ?output_path,
error = %e,
"Post-processing failed"
),
}
result?;
Ok(output_path)
}
pub fn build_ffmpeg_command(input: &str, output: &str, config: &PostProcessConfig) -> Result<Vec<String>> {
tracing::debug!(
input = input,
output = output,
has_video_codec = config.video_codec.is_some(),
has_audio_codec = config.audio_codec.is_some(),
has_video_bitrate = config.video_bitrate.is_some(),
has_audio_bitrate = config.audio_bitrate.is_some(),
has_framerate = config.framerate.is_some(),
has_preset = config.preset.is_some(),
has_resolution = config.resolution.is_some(),
filter_count = config.filters.len(),
"✂️ Building FFmpeg command for post-processing"
);
let mut builder = crate::executor::FfmpegArgs::new().input(input);
if let Some(ref video_codec) = config.video_codec {
tracing::trace!(
video_codec = %video_codec.to_ffmpeg_name(),
"⚙️ Adding video codec"
);
builder = builder.args(["-c:v", video_codec.to_ffmpeg_name()]);
}
if let Some(ref audio_codec) = config.audio_codec {
tracing::trace!(
audio_codec = %audio_codec.to_ffmpeg_name(),
"⚙️ Adding audio codec"
);
builder = builder.args(["-c:a", audio_codec.to_ffmpeg_name()]);
}
if let Some(ref bitrate) = config.video_bitrate {
builder = builder.args(["-b:v", bitrate]);
}
if let Some(ref bitrate) = config.audio_bitrate {
builder = builder.args(["-b:a", bitrate]);
}
if let Some(fps) = config.framerate {
builder = builder.args(["-r", &fps.to_string()]);
}
if let Some(ref preset) = config.preset {
builder = builder.args(["-preset", preset.to_ffmpeg_name()]);
}
let mut filter_chain = Vec::new();
if let Some(ref resolution) = config.resolution {
let scale_filter = format!("scale={}", resolution.to_ffmpeg_scale());
tracing::trace!(
resolution = ?resolution,
scale_filter = %scale_filter,
"⚙️ Adding resolution filter"
);
filter_chain.push(scale_filter);
}
for filter in &config.filters {
let filter_str = filter.to_ffmpeg_string();
tracing::trace!(
filter = %filter_str,
"⚙️ Adding custom filter"
);
filter_chain.push(filter_str);
}
if !filter_chain.is_empty() {
let joined = filter_chain.join(",");
tracing::trace!(
filter_count = filter_chain.len(),
filter_chain = %joined,
"⚙️ Adding video filter chain"
);
builder = builder.args(["-vf".to_string(), joined]);
}
let args = builder.output(output).build();
tracing::debug!(arg_count = args.len(), "✅ FFmpeg command built");
Ok(args)
}