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}