Skip to main content

yt_dlp/executor/
ffmpeg.rs

1//! FFmpeg command builder and execution utilities.
2//!
3//! Provides a builder pattern for constructing FFmpeg arguments
4//! and a helper for the common temp-file + rename execution pattern.
5
6use std::path::Path;
7use std::time::Duration;
8
9use super::Executor;
10use crate::error::{Error, Result};
11use crate::utils::fs::remove_temp_file;
12
13/// Builder for constructing FFmpeg command arguments.
14///
15/// # Example
16///
17/// ```rust,no_run
18/// use yt_dlp::executor::FfmpegArgs;
19///
20/// let args = FfmpegArgs::new()
21///     .input("/tmp/input.mp4")
22///     .input("/tmp/audio.mp3")
23///     .args(["-map", "0:v", "-map", "1:a"])
24///     .codec_copy()
25///     .output("/tmp/output.mkv")
26///     .build();
27/// ```
28pub struct FfmpegArgs {
29    parts: Vec<String>,
30    output: Option<String>,
31    overwrite: bool,
32}
33
34impl FfmpegArgs {
35    /// Creates a new empty FFmpeg argument builder.
36    ///
37    /// # Returns
38    ///
39    /// A new `FfmpegArgs` with no arguments or output set.
40    pub fn new() -> Self {
41        Self {
42            parts: Vec::new(),
43            output: None,
44            overwrite: false,
45        }
46    }
47
48    /// Adds an input file (`-i <path>`).
49    ///
50    /// # Arguments
51    ///
52    /// * `path` - Path to the input file
53    ///
54    /// # Returns
55    ///
56    /// Self for method chaining.
57    pub fn input(mut self, path: impl AsRef<str>) -> Self {
58        self.parts.push("-i".to_string());
59        self.parts.push(path.as_ref().to_string());
60        self
61    }
62
63    /// Adds global codec copy (`-c copy`).
64    ///
65    /// # Returns
66    ///
67    /// Self for method chaining.
68    pub fn codec_copy(mut self) -> Self {
69        self.parts.push("-c".to_string());
70        self.parts.push("copy".to_string());
71        self
72    }
73
74    /// Adds the overwrite flag (`-y`).
75    ///
76    /// # Returns
77    ///
78    /// Self for method chaining.
79    pub fn overwrite(mut self) -> Self {
80        self.overwrite = true;
81        self
82    }
83
84    /// Sets the output path (always placed last).
85    ///
86    /// # Arguments
87    ///
88    /// * `path` - Path to the output file
89    ///
90    /// # Returns
91    ///
92    /// Self for method chaining.
93    pub fn output(mut self, path: impl AsRef<str>) -> Self {
94        self.output = Some(path.as_ref().to_string());
95        self
96    }
97
98    /// Adds arbitrary arguments.
99    ///
100    /// # Arguments
101    ///
102    /// * `args` - Iterator of arguments to append
103    ///
104    /// # Returns
105    ///
106    /// Self for method chaining.
107    pub fn args<I, S>(mut self, args: I) -> Self
108    where
109        I: IntoIterator<Item = S>,
110        S: Into<String>,
111    {
112        self.parts.extend(args.into_iter().map(Into::into));
113        self
114    }
115
116    /// Adds a single argument.
117    ///
118    /// # Arguments
119    ///
120    /// * `arg` - The argument to append
121    ///
122    /// # Returns
123    ///
124    /// Self for method chaining.
125    pub fn arg(mut self, arg: impl Into<String>) -> Self {
126        self.parts.push(arg.into());
127        self
128    }
129
130    /// Builds the final argument list.
131    ///
132    /// # Returns
133    ///
134    /// A `Vec<String>` with all arguments in the correct order:
135    /// inputs and flags first, then `-y` if set, then the output path.
136    pub fn build(mut self) -> Vec<String> {
137        if self.overwrite {
138            self.parts.push("-y".to_string());
139        }
140
141        if let Some(output) = self.output {
142            self.parts.push(output);
143        }
144
145        self.parts
146    }
147}
148
149impl Default for FfmpegArgs {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155/// Executes an FFmpeg command writing to a temporary file, then renames over the original.
156///
157/// This is the common pattern used by metadata operations: write to a temp file,
158/// verify success, then atomically replace the original.
159///
160/// # Arguments
161///
162/// * `ffmpeg_path` - Path to the FFmpeg executable
163/// * `base_path` - The original file path (will be overwritten on success)
164/// * `extension` - File extension for the temp file
165/// * `args` - FFmpeg arguments (output path will be appended automatically)
166/// * `timeout` - Execution timeout
167///
168/// # Errors
169///
170/// Returns an error if FFmpeg fails or the rename operation fails
171pub async fn run_ffmpeg_with_tempfile(
172    ffmpeg_path: &Path,
173    base_path: &Path,
174    extension: &str,
175    args: FfmpegArgs,
176    timeout: Duration,
177) -> Result<()> {
178    let temp_output_path = crate::utils::fs::create_temp_path(base_path, extension);
179    let temp_output_str = temp_output_path
180        .to_str()
181        .ok_or_else(|| Error::path_validation(&temp_output_path, "Invalid output path"))?;
182
183    tracing::debug!(
184        base_path = ?base_path,
185        temp_path = ?temp_output_path,
186        timeout_secs = timeout.as_secs(),
187        "✂️ Running ffmpeg with temp file"
188    );
189
190    let final_args = args.overwrite().output(temp_output_str).build();
191
192    let executor = Executor::new(ffmpeg_path.to_path_buf(), final_args, timeout);
193    if let Err(e) = executor.execute().await {
194        // Clean up temp file on execution failure (timeout, process error, etc.)
195        if temp_output_path.exists() {
196            remove_temp_file(&temp_output_path).await;
197        }
198        return Err(e);
199    }
200
201    tokio::fs::rename(&temp_output_path, base_path).await?;
202    tracing::debug!(base_path = ?base_path, "✅ ffmpeg temp file renamed to final path");
203    Ok(())
204}