Skip to main content

yt_dlp/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use crate::client::deps::{Libraries, LibraryInstaller};
4use crate::download::manager::ManagerConfig;
5use crate::error::{Error, Result};
6use crate::executor::Executor;
7use crate::utils::fs;
8#[cfg(feature = "cache")]
9use cache::{DownloadCache, PlaylistCache, VideoCache};
10use std::fmt::{self, Display};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use std::time::Duration;
14
15// Core modules
16#[cfg(feature = "cache")]
17pub mod cache;
18pub mod error;
19pub mod executor;
20pub mod metadata;
21pub use metadata::PlaylistMetadata;
22pub mod model;
23pub mod utils;
24
25// Architecture modules
26pub mod client;
27pub mod download;
28
29// Convenience modules
30pub mod macros;
31pub mod prelude;
32
33// Re-export of common traits to facilitate their use
34pub use model::utils::{AllTraits, CommonTraits};
35
36// Re-export main types for easy access
37pub use client::{DownloadBuilder, YoutubeBuilder};
38pub use download::{DownloadManager, DownloadPriority, DownloadStatus};
39
40/// A YouTube video fetcher that uses yt-dlp to fetch video information and download it.
41///
42/// The 'yt-dlp' executable and 'ffmpeg' build can be installed with this fetcher.
43///
44/// The video can be downloaded with or without its audio, and the audio and video can be combined.
45/// The video thumbnail can also be downloaded.
46///
47/// The major implementations of this struct are located in the 'fetcher' module.
48///
49/// # Examples
50///
51/// ```rust, no_run
52/// # use yt_dlp::Youtube;
53/// # use std::path::PathBuf;
54/// # use yt_dlp::client::deps::Libraries;
55/// # #[tokio::main]
56/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
57/// let libraries_dir = PathBuf::from("libs");
58/// let output_dir = PathBuf::from("output");
59///
60/// let youtube = libraries_dir.join("yt-dlp");
61/// let ffmpeg = libraries_dir.join("ffmpeg");
62///
63/// let libraries = Libraries::new(youtube, ffmpeg);
64/// let mut fetcher = Youtube::new(libraries, output_dir)?;
65///
66/// let url = String::from("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
67/// let video = fetcher.fetch_video_infos(url).await?;
68/// println!("Video title: {}", video.title);
69///
70/// fetcher.download_video(&video, "video.mp4").await?;
71/// # Ok(())
72/// # }
73/// ```
74#[derive(Clone, Debug)]
75pub struct Youtube {
76    /// The required libraries.
77    pub libraries: Libraries,
78
79    /// The directory where the video (or formats) will be downloaded.
80    pub output_dir: PathBuf,
81    /// The arguments to pass to 'yt-dlp'.
82    pub args: Vec<String>,
83    /// The timeout for command execution.
84    pub timeout: Duration,
85    /// Optional proxy configuration for HTTP requests and yt-dlp.
86    pub proxy: Option<client::proxy::ProxyConfig>,
87    /// The cache for video metadata.
88    #[cfg(feature = "cache")]
89    pub cache: Option<Arc<cache::VideoCache>>,
90    /// The cache for downloaded files.
91    #[cfg(feature = "cache")]
92    pub download_cache: Option<Arc<cache::DownloadCache>>,
93    /// The cache for playlist metadata.
94    #[cfg(feature = "cache")]
95    pub playlist_cache: Option<Arc<cache::PlaylistCache>>,
96    /// The download manager for managing parallel downloads.
97    pub download_manager: Arc<DownloadManager>,
98    /// Cancellation token for graceful shutdown.
99    pub(crate) cancellation_token: tokio_util::sync::CancellationToken,
100}
101
102impl fmt::Display for Youtube {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(
105            f,
106            "Youtube: output_dir={:?}, args={:?}, proxy={}",
107            self.output_dir,
108            self.args,
109            self.proxy.is_some()
110        )
111    }
112}
113
114impl Youtube {
115    /// Creates a new builder for constructing a Youtube instance with a fluent API.
116    ///
117    /// This is the recommended way to create a Youtube instance as it provides
118    /// a clean and intuitive interface for configuration.
119    ///
120    /// # Arguments
121    ///
122    /// * `libraries` - The required libraries (yt-dlp and ffmpeg paths)
123    /// * `output_dir` - The directory where videos will be downloaded
124    ///
125    /// # Examples
126    ///
127    /// ```rust,no_run
128    /// # use yt_dlp::Youtube;
129    /// # use yt_dlp::client::deps::Libraries;
130    /// # use std::path::PathBuf;
131    /// # #[tokio::main]
132    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
133    /// let libraries = Libraries::new("libs/yt-dlp", "libs/ffmpeg");
134    ///
135    /// let youtube = Youtube::builder(libraries, "output")
136    ///     .with_timeout(std::time::Duration::from_secs(120))
137    ///     .with_max_concurrent_downloads(4)
138    ///     .build()
139    ///     .await?;
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub fn builder(libraries: Libraries, output_dir: impl Into<PathBuf>) -> YoutubeBuilder {
144        YoutubeBuilder::new(libraries, output_dir)
145    }
146
147    /// Creates a new download builder for downloading a video with custom quality and codec preferences.
148    ///
149    /// This provides a fluent API for configuring and executing downloads with
150    /// custom quality, codec preferences, priority, and progress tracking.
151    ///
152    /// # Arguments
153    ///
154    /// * `url` - The YouTube video URL to download
155    /// * `output` - The output filename for the downloaded video
156    ///
157    /// # Returns
158    ///
159    /// A `DownloadBuilder` instance that can be configured with various options
160    /// before calling `execute()` to start the download.
161    ///
162    /// # Examples
163    ///
164    /// ```rust,no_run
165    /// # use yt_dlp::Youtube;
166    /// # use yt_dlp::client::deps::Libraries;
167    /// # use yt_dlp::model::selector::{VideoQuality, AudioQuality, VideoCodecPreference};
168    /// # use std::path::PathBuf;
169    /// # #[tokio::main]
170    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
171    /// # let libraries = Libraries::new("libs/yt-dlp", "libs/ffmpeg");
172    /// # let fetcher = Youtube::new(libraries, "output")?;
173    /// let url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
174    ///
175    /// let video_path = fetcher.download(url, "my-video.mp4")
176    ///     .video_quality(VideoQuality::Q1080p)
177    ///     .video_codec(VideoCodecPreference::H264)
178    ///     .audio_quality(AudioQuality::Best)
179    ///     .execute()
180    ///     .await?;
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn download(
185        &self,
186        url: impl Into<String>,
187        output: impl Into<PathBuf>,
188    ) -> client::DownloadBuilder<'_> {
189        client::DownloadBuilder::new(self, url, output)
190    }
191
192    /// Creates a new YouTube fetcher with the given yt-dlp executable, ffmpeg executable and video URL.
193    /// The output directory can be void if you only want to fetch the video information.
194    ///
195    /// # Arguments
196    ///
197    /// * `libraries` - The required libraries.
198    /// * `output_dir` - The directory where the video will be downloaded.
199    ///
200    /// # Errors
201    ///
202    /// This function will return an error if the parent directories of the executables and output directory could not be created.
203    ///
204    /// # Examples
205    ///
206    /// ```rust, no_run
207    /// # use yt_dlp::Youtube;
208    /// # use std::path::PathBuf;
209    /// # use yt_dlp::client::deps::Libraries;
210    /// # #[tokio::main]
211    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
212    /// let libraries_dir = PathBuf::from("libs");
213    /// let output_dir = PathBuf::from("output");
214    ///
215    /// let youtube = libraries_dir.join("yt-dlp");
216    /// let ffmpeg = libraries_dir.join("ffmpeg");
217    ///
218    /// let libraries = Libraries::new(youtube, ffmpeg);
219    /// let fetcher = Youtube::new(libraries, output_dir)?;
220    /// # Ok(())
221    /// # }
222    /// ```
223    pub async fn new(
224        libraries: Libraries,
225        output_dir: impl AsRef<Path> + std::fmt::Debug,
226    ) -> Result<Self> {
227        #[cfg(feature = "tracing")]
228        tracing::debug!("Creating a new video fetcher");
229
230        fs::create_parent_dir(&output_dir)?;
231
232        // Initialize cache in the output directory
233        let cache_dir = output_dir.as_ref().join("cache");
234        fs::create_parent_dir(&cache_dir)?;
235        #[cfg(feature = "cache")]
236        let cache = VideoCache::new(cache_dir.clone(), None).await?;
237        #[cfg(feature = "cache")]
238        let download_cache = DownloadCache::new(cache_dir.clone(), None).await?;
239        #[cfg(feature = "cache")]
240        let playlist_cache = PlaylistCache::new(cache_dir.join("playlists.db")).await?;
241
242        // Initialize download manager with default configuration
243        let download_manager = DownloadManager::new();
244
245        Ok(Self {
246            libraries,
247            output_dir: output_dir.as_ref().to_path_buf(),
248            args: Vec::new(),
249            timeout: Duration::from_secs(30),
250            proxy: None,
251            #[cfg(feature = "cache")]
252            cache: Some(Arc::new(cache)),
253            #[cfg(feature = "cache")]
254            download_cache: Some(Arc::new(download_cache)),
255            #[cfg(feature = "cache")]
256            playlist_cache: Some(Arc::new(playlist_cache)),
257            download_manager: Arc::new(download_manager),
258            cancellation_token: tokio_util::sync::CancellationToken::new(),
259        })
260    }
261
262    /// Creates a new YouTube fetcher with a custom download manager configuration.
263    ///
264    /// # Arguments
265    ///
266    /// * `libraries` - The required libraries.
267    /// * `output_dir` - The directory where the video will be downloaded.
268    /// * `download_manager_config` - The configuration for the download manager.
269    ///
270    /// # Errors
271    ///
272    /// This function will return an error if the parent directories of the executables and output directory could not be created.
273    pub async fn with_download_manager_config(
274        libraries: Libraries,
275        output_dir: impl AsRef<Path> + std::fmt::Debug,
276        download_manager_config: ManagerConfig,
277    ) -> Result<Self> {
278        #[cfg(feature = "tracing")]
279        tracing::debug!("Creating a new video fetcher with custom download manager config");
280
281        fs::create_parent_dir(&output_dir)?;
282
283        // Initialize cache in the output directory
284        let cache_dir = output_dir.as_ref().join("cache");
285        fs::create_parent_dir(&cache_dir)?;
286        #[cfg(feature = "cache")]
287        let cache = VideoCache::new(cache_dir.clone(), None).await?;
288        #[cfg(feature = "cache")]
289        let download_cache = DownloadCache::new(cache_dir.clone(), None).await?;
290        #[cfg(feature = "cache")]
291        let playlist_cache = PlaylistCache::new(cache_dir.join("playlists.db")).await?;
292
293        // Initialize download manager with custom configuration
294        let download_manager = DownloadManager::with_config(download_manager_config);
295
296        Ok(Self {
297            libraries,
298            output_dir: output_dir.as_ref().to_path_buf(),
299            args: Vec::new(),
300            timeout: Duration::from_secs(30),
301            proxy: None,
302            #[cfg(feature = "cache")]
303            cache: Some(Arc::new(cache)),
304            #[cfg(feature = "cache")]
305            download_cache: Some(Arc::new(download_cache)),
306            #[cfg(feature = "cache")]
307            playlist_cache: Some(Arc::new(playlist_cache)),
308            download_manager: Arc::new(download_manager),
309            cancellation_token: tokio_util::sync::CancellationToken::new(),
310        })
311    }
312
313    /// Creates a new YouTube fetcher, and installs the yt-dlp and ffmpeg binaries.
314    /// The output directory can be void if you only want to fetch the video information.
315    /// Be careful, this function may take a while to execute.
316    ///
317    /// # Arguments
318    ///
319    /// * `executables_dir` - The directory where the binaries will be installed.
320    /// * `output_dir` - The directory where the video will be downloaded.
321    ///
322    /// # Errors
323    ///
324    /// This function will return an error if the executables could not be installed.
325    ///
326    /// # Examples
327    ///
328    /// ```rust, no_run
329    /// # use yt_dlp::Youtube;
330    /// # use std::path::PathBuf;
331    /// # #[tokio::main]
332    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
333    /// let executables_dir = PathBuf::from("libs");
334    /// let output_dir = PathBuf::from("output");
335    ///
336    /// let fetcher = Youtube::with_new_binaries(executables_dir, output_dir).await?;
337    /// # Ok(())
338    /// # }
339    /// ```
340    pub async fn with_new_binaries(
341        executables_dir: impl AsRef<Path> + std::fmt::Debug + Send + Sync,
342        output_dir: impl AsRef<Path> + std::fmt::Debug + Send + Sync,
343    ) -> Result<Self> {
344        #[cfg(feature = "tracing")]
345        tracing::debug!("Creating a new video fetcher with binaries installation");
346
347        let installer = LibraryInstaller::new(executables_dir.as_ref().to_path_buf());
348
349        // Check if binaries already exist
350        let youtube_path = executables_dir
351            .as_ref()
352            .join(utils::find_executable("yt-dlp"));
353        let ffmpeg_path = executables_dir
354            .as_ref()
355            .join(utils::find_executable("ffmpeg"));
356
357        let youtube = if youtube_path.exists() {
358            youtube_path
359        } else {
360            installer.install_youtube(None).await?
361        };
362
363        let ffmpeg = if ffmpeg_path.exists() {
364            ffmpeg_path
365        } else {
366            installer.install_ffmpeg(None).await?
367        };
368
369        let libraries = Libraries::new(youtube, ffmpeg);
370        Self::new(libraries, output_dir).await
371    }
372
373    /// Sets the arguments to pass to yt-dlp.
374    ///
375    /// # Arguments
376    ///
377    /// * `args` - The arguments to pass to yt-dlp.
378    ///
379    /// # Examples
380    ///
381    /// ```rust, no_run
382    /// # use yt_dlp::Youtube;
383    /// # use std::path::PathBuf;
384    /// # use yt_dlp::client::deps::Libraries;
385    /// # #[tokio::main]
386    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
387    /// # let libraries_dir = PathBuf::from("libs");
388    /// # let output_dir = PathBuf::from("output");
389    /// # let youtube = libraries_dir.join("yt-dlp");
390    /// # let ffmpeg = libraries_dir.join("ffmpeg");
391    /// # let libraries = Libraries::new(youtube, ffmpeg);
392    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
393    ///
394    /// let args = vec!["--no-progress".to_string()];
395    /// fetcher.with_args(args);
396    /// # Ok(())
397    /// # }
398    /// ```
399    pub fn with_args(&mut self, mut args: Vec<String>) -> &mut Self {
400        self.args.append(&mut args);
401        self
402    }
403
404    /// Sets the timeout for command execution.
405    ///
406    /// # Arguments
407    ///
408    /// * `timeout` - The timeout duration for command execution.
409    ///
410    /// # Examples
411    ///
412    /// ```rust, no_run
413    /// # use yt_dlp::Youtube;
414    /// # use std::path::PathBuf;
415    /// # use yt_dlp::client::deps::Libraries;
416    /// # use std::time::Duration;
417    /// # #[tokio::main]
418    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
419    /// # let libraries_dir = PathBuf::from("libs");
420    /// # let output_dir = PathBuf::from("output");
421    /// # let youtube = libraries_dir.join("yt-dlp");
422    /// # let ffmpeg = libraries_dir.join("ffmpeg");
423    /// # let libraries = Libraries::new(youtube, ffmpeg);
424    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
425    ///
426    /// // Set a longer timeout for large videos
427    /// fetcher.with_timeout(Duration::from_secs(300));
428    /// # Ok(())
429    /// # }
430    /// ```
431    pub fn with_timeout(&mut self, timeout: Duration) -> &mut Self {
432        self.timeout = timeout;
433        self
434    }
435
436    /// Adds an argument to pass to yt-dlp.
437    ///
438    /// # Arguments
439    ///
440    /// * `arg` - The argument to pass to yt-dlp.
441    ///
442    /// # Examples
443    ///
444    /// ```rust, no_run
445    /// # use yt_dlp::Youtube;
446    /// # use std::path::PathBuf;
447    /// # use yt_dlp::client::deps::Libraries;
448    /// # #[tokio::main]
449    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
450    /// # let libraries_dir = PathBuf::from("libs");
451    /// # let output_dir = PathBuf::from("output");
452    /// # let youtube = libraries_dir.join("yt-dlp");
453    /// # let ffmpeg = libraries_dir.join("ffmpeg");
454    /// # let libraries = Libraries::new(youtube, ffmpeg);
455    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
456    ///
457    /// fetcher.with_arg("--no-progress");
458    /// # Ok(())
459    /// # }
460    /// ```
461    pub fn with_arg(&mut self, arg: impl AsRef<str>) -> &mut Self {
462        self.args.push(arg.as_ref().to_string());
463        self
464    }
465
466    /// Updates the yt-dlp executable.
467    /// Be careful, this function may take a while to execute.
468    ///
469    /// # Errors
470    ///
471    /// This function will return an error if the yt-dlp executable could not be updated.
472    ///
473    /// # Examples
474    ///
475    /// ```rust, no_run
476    /// # use yt_dlp::Youtube;
477    /// # use std::path::PathBuf;
478    /// # use yt_dlp::client::deps::Libraries;
479    /// # #[tokio::main]
480    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
481    /// # let libraries_dir = PathBuf::from("libs");
482    /// # let output_dir = PathBuf::from("output");
483    /// # let youtube = libraries_dir.join("yt-dlp");
484    /// # let ffmpeg = libraries_dir.join("ffmpeg");
485    /// # let libraries = Libraries::new(youtube, ffmpeg);
486    /// let fetcher = Youtube::new(libraries, output_dir)?;
487    ///
488    /// fetcher.update_downloader().await?;
489    /// # Ok(())
490    /// # }
491    /// ```
492    pub async fn update_downloader(&self) -> Result<()> {
493        #[cfg(feature = "tracing")]
494        tracing::debug!("Updating the downloader");
495
496        let args = vec!["--update"];
497
498        let executor = Executor {
499            executable_path: self.libraries.youtube.clone(),
500            timeout: self.timeout,
501            args: utils::to_owned(args),
502        };
503
504        executor.execute().await?;
505        Ok(())
506    }
507
508    /// Combines the audio and video files into a single file.
509    /// Be careful, this function may take a while to execute.
510    ///
511    /// # Arguments
512    ///
513    /// * `audio_file` - The name of the audio file to combine.
514    /// * `video_file` - The name of the video file to combine.
515    /// * `output_file` - The name of the output file.
516    ///
517    /// # Errors
518    ///
519    /// This function will return an error if the audio and video files could not be combined.
520    ///
521    /// # Examples
522    ///
523    /// ```rust, no_run
524    /// # use yt_dlp::Youtube;
525    /// # use std::path::PathBuf;
526    /// # use yt_dlp::client::deps::Libraries;
527    /// # #[tokio::main]
528    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
529    /// # let libraries_dir = PathBuf::from("libs");
530    /// # let output_dir = PathBuf::from("output");
531    /// # let youtube = libraries_dir.join("yt-dlp");
532    /// # let ffmpeg = libraries_dir.join("ffmpeg");
533    /// # let libraries = Libraries::new(youtube, ffmpeg);
534    /// let fetcher = Youtube::new(libraries, output_dir)?;
535    ///
536    /// let url = String::from("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
537    /// let video = fetcher.fetch_video_infos(url).await?;
538    ///
539    /// let audio_format = video.best_audio_format().unwrap();
540    /// let audio_path = fetcher.download_format(&audio_format, "audio-stream.mp3").await?;
541    ///
542    /// let video_format = video.worst_video_format().unwrap();
543    /// let format_path = fetcher.download_format(&video_format, "video-stream.mp4").await?;
544    ///
545    /// let output_path = fetcher.combine_audio_and_video("audio-stream.mp3", "video-stream.mp4", "my-output.mp4").await?;
546    /// # Ok(())
547    /// # }
548    /// ```
549    pub async fn combine_audio_and_video(
550        &self,
551        audio_file: impl AsRef<str> + std::fmt::Debug + Display,
552        video_file: impl AsRef<str> + std::fmt::Debug + Display,
553        output_file: impl AsRef<str> + std::fmt::Debug + Display,
554    ) -> Result<PathBuf> {
555        #[cfg(feature = "tracing")]
556        tracing::debug!(
557            "Combining audio and video files {} and {}, into {}",
558            audio_file,
559            video_file,
560            output_file
561        );
562
563        let audio_path = self.output_dir.join(audio_file.as_ref());
564        let video_path = self.output_dir.join(video_file.as_ref());
565        let output_path = self.output_dir.join(output_file.as_ref());
566
567        // Perform the combination with FFmpeg
568        self.execute_ffmpeg_combine(&audio_path, &video_path, &output_path)
569            .await?;
570
571        // Add metadata to the combined file, propagating potential errors
572        self.add_metadata_to_combined_file(&audio_path, &video_path, &output_path)
573            .await?;
574
575        Ok(output_path)
576    }
577
578    /// Executes the FFmpeg command to combine audio and video files
579    async fn execute_ffmpeg_combine(
580        &self,
581        audio_path: impl AsRef<Path>,
582        video_path: impl AsRef<Path>,
583        output_path: impl AsRef<Path>,
584    ) -> Result<()> {
585        let audio = audio_path
586            .as_ref()
587            .to_str()
588            .ok_or(Error::Unknown("Invalid audio path".to_string()))?;
589        let video = video_path
590            .as_ref()
591            .to_str()
592            .ok_or(Error::Unknown("Invalid video path".to_string()))?;
593        let output = output_path
594            .as_ref()
595            .to_str()
596            .ok_or(Error::Unknown("Invalid output path".to_string()))?;
597
598        let args = vec![
599            "-i", audio, "-i", video, "-c:v", "copy", "-c:a", "aac", output,
600        ];
601
602        let executor = Executor {
603            executable_path: self.libraries.ffmpeg.clone(),
604            timeout: self.timeout,
605            args: utils::to_owned(args),
606        };
607
608        executor.execute().await?;
609        Ok(())
610    }
611
612    /// Adds metadata to the combined file by extracting the video ID and
613    /// retrieving information from the original audio and video formats
614    async fn add_metadata_to_combined_file(
615        &self,
616        audio_path: impl AsRef<Path>,
617        video_path: impl AsRef<Path>,
618        output_path: impl AsRef<Path>,
619    ) -> Result<()> {
620        let video_id =
621            self.extract_video_id_from_file_paths(video_path.as_ref(), audio_path.as_ref());
622
623        if let Some(video_id) = video_id
624            && let Some(video) = self.get_video_by_id(&video_id).await
625        {
626            #[cfg(feature = "tracing")]
627            tracing::debug!("Adding metadata to combined file");
628
629            cfg_if::cfg_if! {
630                if #[cfg(feature = "cache")] {
631                    let video_format = self.find_cached_format(video_path.as_ref()).await;
632                    let audio_format = self.find_cached_format(audio_path.as_ref()).await;
633
634                    // Add metadata (including chapters) to the combined file with full format information
635                    if let Err(_e) = metadata::MetadataManager::add_metadata_with_chapters(
636                        output_path.as_ref(),
637                        &video,
638                        video_format.as_ref(),
639                        audio_format.as_ref(),
640                    )
641                    .await
642                    {
643                        #[cfg(feature = "tracing")]
644                        tracing::warn!("Failed to add metadata to combined file: {}", _e);
645                    } else {
646                        #[cfg(feature = "tracing")]
647                        tracing::debug!("Successfully added metadata (including chapters) to combined file");
648                    }
649                } else {
650                    // Without cache, we don't have format details, add basic metadata only
651                    if let Err(e) = metadata::MetadataManager::add_metadata(
652                        output_path.as_ref(),
653                        &video,
654                    )
655                    .await
656                    {
657                        #[cfg(feature = "tracing")]
658                        tracing::warn!("Failed to add basic metadata to combined file: {}", e);
659                    } else {
660                        #[cfg(feature = "tracing")]
661                        tracing::debug!("Successfully added basic metadata to combined file");
662                    }
663                }
664            }
665        }
666
667        Ok(())
668    }
669
670    /// Extracts the video ID from audio and video file paths
671    fn extract_video_id_from_file_paths(
672        &self,
673        video_path: impl AsRef<Path>,
674        audio_path: impl AsRef<Path>,
675    ) -> Option<String> {
676        let video_filename = video_path.as_ref().file_name()?.to_str()?;
677
678        if let Some(id) = utils::fs::extract_video_id(video_filename) {
679            return Some(id);
680        }
681
682        let audio_filename = audio_path.as_ref().file_name()?.to_str()?;
683        utils::fs::extract_video_id(audio_filename)
684    }
685
686    /// Finds the format of a file in the cache if it exists
687    #[cfg(feature = "cache")]
688    async fn find_cached_format(
689        &self,
690        file_path: impl AsRef<Path>,
691    ) -> Option<model::format::Format> {
692        if let Some(download_cache) = &self.download_cache {
693            let file_hash = match DownloadCache::calculate_file_hash(file_path.as_ref()).await {
694                Ok(hash) => hash,
695                Err(_) => return None,
696            };
697
698            if let Some((cached_file, _)) = download_cache.get_by_hash(&file_hash).await
699                && let Some(ref format_json) = cached_file.format_json
700                && let Ok(format) = serde_json::from_str(format_json)
701            {
702                return Some(format);
703            }
704        }
705
706        None
707    }
708
709    /// Enables caching of video metadata.
710    ///
711    /// # Arguments
712    ///
713    /// * `cache_dir` - The directory where to store the cache.
714    /// * `ttl` - The time-to-live for cache entries in seconds (default: 24 hours).
715    ///
716    /// # Errors
717    ///
718    /// This function will return an error if the cache directory could not be created.
719    ///
720    /// # Examples
721    ///
722    /// ```rust, no_run
723    /// # use yt_dlp::Youtube;
724    /// # use std::path::PathBuf;
725    /// # use yt_dlp::client::deps::Libraries;
726    /// # #[tokio::main]
727    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
728    /// # let libraries_dir = PathBuf::from("libs");
729    /// # let output_dir = PathBuf::from("output");
730    /// # let youtube = libraries_dir.join("yt-dlp");
731    /// # let ffmpeg = libraries_dir.join("ffmpeg");
732    /// # let libraries = Libraries::new(youtube, ffmpeg);
733    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
734    ///
735    /// // Enable video metadata caching
736    /// fetcher.with_cache(PathBuf::from("cache"), None)?;
737    /// # Ok(())
738    /// # }
739    /// ```
740    #[cfg(feature = "cache")]
741    pub async fn with_cache(
742        &mut self,
743        cache_dir: impl AsRef<Path> + std::fmt::Debug,
744        ttl: Option<u64>,
745    ) -> Result<&mut Self> {
746        #[cfg(feature = "tracing")]
747        tracing::debug!("Enabling video metadata cache");
748
749        let cache = VideoCache::new(cache_dir.as_ref(), ttl).await?;
750        self.cache = Some(Arc::new(cache));
751        Ok(self)
752    }
753
754    /// Enables caching of downloaded files.
755    ///
756    /// # Arguments
757    ///
758    /// * `cache_dir` - The directory where to store the cache.
759    /// * `ttl` - The time-to-live for cache entries in seconds (default: 7 days).
760    ///
761    /// # Errors
762    ///
763    /// This function will return an error if the cache directory could not be created.
764    ///
765    /// # Examples
766    ///
767    /// ```rust, no_run
768    /// # use yt_dlp::Youtube;
769    /// # use std::path::PathBuf;
770    /// # use yt_dlp::client::deps::Libraries;
771    /// # #[tokio::main]
772    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
773    /// # let libraries_dir = PathBuf::from("libs");
774    /// # let output_dir = PathBuf::from("output");
775    /// # let youtube = libraries_dir.join("yt-dlp");
776    /// # let ffmpeg = libraries_dir.join("ffmpeg");
777    /// # let libraries = Libraries::new(youtube, ffmpeg);
778    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
779    ///
780    /// // Enable downloaded files caching
781    /// fetcher.with_download_cache(PathBuf::from("cache"), None)?;
782    /// # Ok(())
783    /// # }
784    /// ```
785    #[cfg(feature = "cache")]
786    pub async fn with_download_cache(
787        &mut self,
788        cache_dir: impl AsRef<Path> + std::fmt::Debug,
789        ttl: Option<u64>,
790    ) -> Result<&mut Self> {
791        #[cfg(feature = "tracing")]
792        tracing::debug!("Enabling downloaded files cache");
793
794        let download_cache = DownloadCache::new(cache_dir.as_ref(), ttl).await?;
795        self.download_cache = Some(Arc::new(download_cache));
796        Ok(self)
797    }
798
799    /// Enables caching of playlist metadata.
800    ///
801    /// # Arguments
802    ///
803    /// * `cache_dir` - The directory where to store the cache.
804    /// * `ttl` - The time-to-live for cache entries in seconds (default: 6 hours).
805    ///
806    /// # Errors
807    ///
808    /// This function will return an error if the cache directory could not be created.
809    ///
810    /// # Examples
811    ///
812    /// ```rust, no_run
813    /// # use yt_dlp::Youtube;
814    /// # use std::path::PathBuf;
815    /// # use yt_dlp::client::deps::Libraries;
816    /// # #[tokio::main]
817    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
818    /// # let libraries_dir = PathBuf::from("libs");
819    /// # let output_dir = PathBuf::from("output");
820    /// # let youtube = libraries_dir.join("yt-dlp");
821    /// # let ffmpeg = libraries_dir.join("ffmpeg");
822    /// # let libraries = Libraries::new(youtube, ffmpeg);
823    /// let mut fetcher = Youtube::new(libraries, output_dir)?;
824    ///
825    /// // Enable playlist metadata caching
826    /// fetcher.with_playlist_cache(PathBuf::from("cache"), None)?;
827    /// # Ok(())
828    /// # }
829    /// ```
830    #[cfg(feature = "cache")]
831    pub async fn with_playlist_cache(
832        &mut self,
833        cache_dir: impl AsRef<Path> + std::fmt::Debug,
834        ttl: Option<i64>,
835    ) -> Result<&mut Self> {
836        #[cfg(feature = "tracing")]
837        tracing::debug!("Enabling playlist metadata cache");
838
839        let db_path = cache_dir.as_ref().join("playlists.db");
840        let playlist_cache = if let Some(ttl_seconds) = ttl {
841            PlaylistCache::with_ttl(db_path, ttl_seconds).await?
842        } else {
843            PlaylistCache::new(db_path).await?
844        };
845        self.playlist_cache = Some(Arc::new(playlist_cache));
846        Ok(self)
847    }
848
849    /// Download a video using the download manager with priority.
850    ///
851    /// This method adds the video download to the download queue with the specified priority.
852    /// The download will be processed according to its priority and the current load.
853    ///
854    /// # Arguments
855    ///
856    /// * `video` - The video to download.
857    /// * `output` - The name of the file to save the video to.
858    /// * `priority` - The download priority (optional).
859    ///
860    /// # Returns
861    ///
862    /// The download ID that can be used to track the download status.
863    ///
864    /// # Errors
865    ///
866    /// This function will return an error if the video information could not be retrieved.
867    pub async fn download_video_with_priority(
868        &self,
869        video: &model::Video,
870        output: impl AsRef<str> + std::fmt::Debug,
871        priority: Option<download::manager::DownloadPriority>,
872    ) -> Result<u64> {
873        #[cfg(feature = "tracing")]
874        tracing::debug!("Downloading video with priority: {}", video.id);
875
876        // Get the best format with video and audio
877        let format = video
878            .formats
879            .iter()
880            .find(|f| f.format_type().is_audio_and_video())
881            .ok_or_else(|| Error::FormatNotAvailable {
882                video_id: video.id.clone(),
883                format_type: "audio+video".to_string(),
884                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
885            })?;
886
887        // Get the URL
888        let url = format
889            .download_info
890            .url
891            .as_ref()
892            .ok_or_else(|| Error::FormatNoUrl {
893                video_id: video.id.clone(),
894                format_id: format.format_id.clone(),
895            })?;
896
897        // Create the output path
898        let output_path = self.output_dir.join(output.as_ref());
899
900        // Add to download queue
901        let download_id = self
902            .download_manager
903            .enqueue(url, output_path, priority)
904            .await;
905
906        Ok(download_id)
907    }
908
909    /// Download a video using the download manager with progress tracking.
910    ///
911    /// This method adds the video download to the download queue and provides progress updates.
912    ///
913    /// # Arguments
914    ///
915    /// * `video` - The video to download.
916    /// * `output` - The name of the file to save the video to.
917    /// * `progress_callback` - A function that will be called with progress updates.
918    ///
919    /// # Returns
920    ///
921    /// The download ID that can be used to track the download status.
922    ///
923    /// # Errors
924    ///
925    /// This function will return an error if the video information could not be retrieved.
926    pub async fn download_video_with_progress<F>(
927        &self,
928        video: &model::Video,
929        output: impl AsRef<str> + std::fmt::Debug,
930        progress_callback: F,
931    ) -> Result<u64>
932    where
933        F: Fn(u64, u64) + Send + Sync + 'static,
934    {
935        #[cfg(feature = "tracing")]
936        tracing::debug!("Downloading video with progress tracking: {}", video.id);
937
938        // Get the best format with video and audio
939        let format = video
940            .formats
941            .iter()
942            .find(|f| f.format_type().is_audio_and_video())
943            .ok_or_else(|| Error::FormatNotAvailable {
944                video_id: video.id.clone(),
945                format_type: "audio+video".to_string(),
946                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
947            })?;
948
949        // Get the URL
950        let url = format
951            .download_info
952            .url
953            .as_ref()
954            .ok_or_else(|| Error::FormatNoUrl {
955                video_id: video.id.clone(),
956                format_id: format.format_id.clone(),
957            })?;
958
959        // Create the output path
960        let output_path = self.output_dir.join(output.as_ref());
961
962        // Add to download queue with progress callback
963        let download_id = self
964            .download_manager
965            .enqueue_with_progress(
966                url,
967                output_path,
968                Some(download::manager::DownloadPriority::Normal),
969                progress_callback,
970            )
971            .await;
972
973        Ok(download_id)
974    }
975
976    /// Get the status of a download.
977    ///
978    /// # Arguments
979    ///
980    /// * `download_id` - The ID of the download to check.
981    ///
982    /// # Returns
983    ///
984    /// The download status, or None if the download ID is not found.
985    pub async fn get_download_status(
986        &self,
987        download_id: u64,
988    ) -> Option<download::manager::DownloadStatus> {
989        self.download_manager.get_status(download_id).await
990    }
991
992    /// Cancel a download.
993    ///
994    /// # Arguments
995    ///
996    /// * `download_id` - The ID of the download to cancel.
997    ///
998    /// # Returns
999    ///
1000    /// true if the download was canceled, false if it was not found or already completed.
1001    pub async fn cancel_download(&self, download_id: u64) -> bool {
1002        self.download_manager.cancel(download_id).await
1003    }
1004
1005    /// Wait for a download to complete.
1006    ///
1007    /// # Arguments
1008    ///
1009    /// * `download_id` - The ID of the download to wait for.
1010    ///
1011    /// # Returns
1012    ///
1013    /// The final download status, or None if the download ID is not found.
1014    pub async fn wait_for_download(
1015        &self,
1016        download_id: u64,
1017    ) -> Option<download::manager::DownloadStatus> {
1018        self.download_manager.wait_for_completion(download_id).await
1019    }
1020
1021    /// Downloads a video with the specified video and audio quality preferences.
1022    ///
1023    /// # Arguments
1024    ///
1025    /// * `url` - The URL of the video to download
1026    /// * `output` - The name of the output file
1027    /// * `video_quality` - The desired video quality
1028    /// * `video_codec` - The preferred video codec
1029    /// * `audio_quality` - The desired audio quality
1030    /// * `audio_codec` - The preferred audio codec
1031    ///
1032    /// # Returns
1033    ///
1034    /// The path to the downloaded video file
1035    ///
1036    /// # Example
1037    ///
1038    /// ```rust, no_run
1039    /// # use yt_dlp::Youtube;
1040    /// # use std::path::PathBuf;
1041    /// # use yt_dlp::client::deps::Libraries;
1042    /// # use yt_dlp::model::{VideoQuality, VideoCodecPreference, AudioQuality, AudioCodecPreference};
1043    /// # #[tokio::main]
1044    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1045    /// # let libraries_dir = PathBuf::from("libs");
1046    /// # let output_dir = PathBuf::from("output");
1047    /// # let youtube = libraries_dir.join("yt-dlp");
1048    /// # let ffmpeg = libraries_dir.join("ffmpeg");
1049    /// # let libraries = Libraries::new(youtube, ffmpeg);
1050    /// # let fetcher = Youtube::new(libraries, output_dir)?;
1051    /// let url = String::from("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
1052    ///
1053    /// // Download a high quality video with VP9 codec and high quality audio with Opus codec
1054    /// let video_path = fetcher.download_video_with_quality(
1055    ///     url,
1056    ///     "my-video.mp4",
1057    ///     VideoQuality::High,
1058    ///     VideoCodecPreference::VP9,
1059    ///     AudioQuality::High,
1060    ///     AudioCodecPreference::Opus
1061    /// ).await?;
1062    /// # Ok(())
1063    /// # }
1064    /// ```
1065    pub async fn download_video_with_quality(
1066        &self,
1067        url: impl AsRef<str> + std::fmt::Debug + Display,
1068        output: impl AsRef<str> + std::fmt::Debug + Display,
1069        video_quality: model::selector::VideoQuality,
1070        video_codec: model::selector::VideoCodecPreference,
1071        audio_quality: model::selector::AudioQuality,
1072        audio_codec: model::selector::AudioCodecPreference,
1073    ) -> Result<PathBuf> {
1074        let video = self.fetch_video_infos(url.to_string()).await?;
1075
1076        // Select video format based on quality and codec preferences
1077        let video_format = video
1078            .select_video_format(video_quality, video_codec.clone())
1079            .ok_or_else(|| Error::FormatNotAvailable {
1080                video_id: video.id.clone(),
1081                format_type: "video".to_string(),
1082                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
1083            })?;
1084
1085        // Select audio format based on quality and codec preferences
1086        let audio_format = video
1087            .select_audio_format(audio_quality, audio_codec.clone())
1088            .ok_or_else(|| Error::FormatNotAvailable {
1089                video_id: video.id.clone(),
1090                format_type: "audio".to_string(),
1091                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
1092            })?;
1093
1094        // Download video format with preferences
1095        let video_ext = format!("{:?}", video_format.download_info.ext);
1096        let video_filename = format!("temp_video_{}.{}", utils::fs::random_filename(8), video_ext);
1097
1098        cfg_if::cfg_if! {
1099            if #[cfg(feature = "cache")] {
1100                let video_path = self
1101                    .download_format_with_preferences(
1102                        video_format,
1103                        &video_filename,
1104                        Some(video_quality),
1105                        None,
1106                        Some(video_codec),
1107                        None,
1108                    )
1109                    .await?;
1110            } else {
1111                let video_path = self
1112                    .download_format(video_format, &video_filename)
1113                    .await?;
1114            }
1115        }
1116
1117        // Download audio format with preferences
1118        let audio_ext = format!("{:?}", audio_format.download_info.ext);
1119        let audio_filename = format!("temp_audio_{}.{}", utils::fs::random_filename(8), audio_ext);
1120        cfg_if::cfg_if! {
1121            if #[cfg(feature = "cache")] {
1122                let audio_path = self
1123                    .download_format_with_preferences(
1124                        audio_format,
1125                        &audio_filename,
1126                        None,
1127                        Some(audio_quality),
1128                        None,
1129                        Some(audio_codec),
1130                    )
1131                    .await?;
1132            } else {
1133                let audio_path = self
1134                    .download_format(audio_format, &audio_filename)
1135                    .await?;
1136            }
1137        }
1138
1139        // Combine audio and video
1140        let output_path = self
1141            .combine_audio_and_video(&audio_filename, &video_filename, output)
1142            .await?;
1143
1144        // Clean up temporary files
1145        if let Err(_e) = tokio::fs::remove_file(&video_path).await {
1146            #[cfg(feature = "tracing")]
1147            tracing::warn!("Failed to remove temporary video file: {}", _e);
1148        }
1149
1150        if let Err(_e) = tokio::fs::remove_file(&audio_path).await {
1151            #[cfg(feature = "tracing")]
1152            tracing::warn!("Failed to remove temporary audio file: {}", _e);
1153        }
1154
1155        Ok(output_path)
1156    }
1157
1158    /// Downloads a video stream with the specified quality preferences.
1159    ///
1160    /// # Arguments
1161    ///
1162    /// * `url` - The URL of the video to download
1163    /// * `output` - The name of the output file
1164    /// * `quality` - The desired video quality
1165    /// * `codec` - The preferred video codec
1166    ///
1167    /// # Returns
1168    ///
1169    /// The path to the downloaded video file
1170    ///
1171    /// # Example
1172    ///
1173    /// ```rust, no_run
1174    /// # use yt_dlp::Youtube;
1175    /// # use std::path::PathBuf;
1176    /// # use yt_dlp::client::deps::Libraries;
1177    /// # use yt_dlp::model::{VideoQuality, VideoCodecPreference};
1178    /// # #[tokio::main]
1179    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1180    /// # let libraries_dir = PathBuf::from("libs");
1181    /// # let output_dir = PathBuf::from("output");
1182    /// # let youtube = libraries_dir.join("yt-dlp");
1183    /// # let ffmpeg = libraries_dir.join("ffmpeg");
1184    /// # let libraries = Libraries::new(youtube, ffmpeg);
1185    /// # let fetcher = Youtube::new(libraries, output_dir)?;
1186    /// let url = String::from("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
1187    ///
1188    /// // Download a medium quality video with AVC1 codec
1189    /// let video_path = fetcher.download_video_stream_with_quality(
1190    ///     url,
1191    ///     "video-only.mp4",
1192    ///     VideoQuality::Medium,
1193    ///     VideoCodecPreference::AVC1
1194    /// ).await?;
1195    /// # Ok(())
1196    /// # }
1197    /// ```
1198    pub async fn download_video_stream_with_quality(
1199        &self,
1200        url: impl AsRef<str> + std::fmt::Debug + Display,
1201        output: impl AsRef<str> + std::fmt::Debug + Display,
1202        quality: model::selector::VideoQuality,
1203        codec: model::selector::VideoCodecPreference,
1204    ) -> Result<PathBuf> {
1205        let video = self.fetch_video_infos(url.to_string()).await?;
1206
1207        // Select video format based on quality and codec preferences
1208        let video_format = video
1209            .select_video_format(quality, codec.clone())
1210            .ok_or_else(|| Error::FormatNotAvailable {
1211                video_id: video.id.clone(),
1212                format_type: "video".to_string(),
1213                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
1214            })?;
1215
1216        // Download video format with preferences
1217        cfg_if::cfg_if! {
1218            if #[cfg(feature = "cache")] {
1219                self.download_format_with_preferences(
1220                    video_format,
1221                    output,
1222                    Some(quality),
1223                    None,
1224                    Some(codec),
1225                    None,
1226                )
1227                .await
1228            } else {
1229                self.download_format(video_format, output)
1230                    .await
1231            }
1232        }
1233    }
1234
1235    /// Downloads an audio stream with the specified quality preferences.
1236    ///
1237    /// # Arguments
1238    ///
1239    /// * `url` - The URL of the video to download
1240    /// * `output` - The name of the output file
1241    /// * `quality` - The desired audio quality
1242    /// * `codec` - The preferred audio codec
1243    ///
1244    /// # Returns
1245    ///
1246    /// The path to the downloaded audio file
1247    ///
1248    /// # Example
1249    ///
1250    /// ```rust, no_run
1251    /// # use yt_dlp::Youtube;
1252    /// # use std::path::PathBuf;
1253    /// # use yt_dlp::client::deps::Libraries;
1254    /// # use yt_dlp::model::{AudioQuality, AudioCodecPreference};
1255    /// # #[tokio::main]
1256    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1257    /// # let libraries_dir = PathBuf::from("libs");
1258    /// # let output_dir = PathBuf::from("output");
1259    /// # let youtube = libraries_dir.join("yt-dlp");
1260    /// # let ffmpeg = libraries_dir.join("ffmpeg");
1261    /// # let libraries = Libraries::new(youtube, ffmpeg);
1262    /// # let fetcher = Youtube::new(libraries, output_dir)?;
1263    /// let url = String::from("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
1264    ///
1265    /// // Download a high quality audio with Opus codec
1266    /// let audio_path = fetcher.download_audio_stream_with_quality(
1267    ///     url,
1268    ///     "audio-only.mp3",
1269    ///     AudioQuality::High,
1270    ///     AudioCodecPreference::Opus
1271    /// ).await?;
1272    /// # Ok(())
1273    /// # }
1274    /// ```
1275    pub async fn download_audio_stream_with_quality(
1276        &self,
1277        url: impl AsRef<str> + std::fmt::Debug + Display,
1278        output: impl AsRef<str> + std::fmt::Debug + Display,
1279        quality: model::selector::AudioQuality,
1280        codec: model::selector::AudioCodecPreference,
1281    ) -> Result<PathBuf> {
1282        let video = self.fetch_video_infos(url.to_string()).await?;
1283
1284        // Select audio format based on quality and codec preferences
1285        let audio_format = video
1286            .select_audio_format(quality, codec.clone())
1287            .ok_or_else(|| Error::FormatNotAvailable {
1288                video_id: video.id.clone(),
1289                format_type: "audio".to_string(),
1290                available_formats: video.formats.iter().map(|f| f.format_id.clone()).collect(),
1291            })?;
1292
1293        // Download audio format with preferences
1294        cfg_if::cfg_if! {
1295            if #[cfg(feature = "cache")] {
1296                self.download_format_with_preferences(
1297                    audio_format,
1298                    output,
1299                    None,
1300                    Some(quality),
1301                    None,
1302                    Some(codec),
1303                )
1304                .await
1305            } else {
1306                self.download_format(audio_format, output)
1307                    .await
1308            }
1309        }
1310    }
1311
1312    /// Initiates a graceful shutdown of all ongoing operations.
1313    ///
1314    /// This method triggers the cancellation token, signaling all ongoing
1315    /// downloads and operations to stop gracefully. It does not wait for
1316    /// operations to complete.
1317    ///
1318    /// # Examples
1319    ///
1320    /// ```rust,no_run
1321    /// # use yt_dlp::Youtube;
1322    /// # use yt_dlp::client::deps::Libraries;
1323    /// # use std::path::PathBuf;
1324    /// # #[tokio::main]
1325    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1326    /// # let libs = Libraries::new(PathBuf::from("yt-dlp"), PathBuf::from("ffmpeg"));
1327    /// let youtube = Youtube::new(libs, "output").await?;
1328    ///
1329    /// // Start some downloads...
1330    ///
1331    /// // Initiate graceful shutdown
1332    /// youtube.shutdown();
1333    /// # Ok(())
1334    /// # }
1335    /// ```
1336    pub fn shutdown(&self) {
1337        #[cfg(feature = "tracing")]
1338        tracing::info!("Initiating graceful shutdown");
1339
1340        self.cancellation_token.cancel();
1341    }
1342
1343    /// Checks if a shutdown has been requested.
1344    ///
1345    /// # Returns
1346    ///
1347    /// Returns `true` if shutdown has been initiated, `false` otherwise.
1348    pub fn is_shutdown_requested(&self) -> bool {
1349        self.cancellation_token.is_cancelled()
1350    }
1351
1352    // ==================== Fluent API Methods ====================
1353
1354    /// Fluent method to fetch video info and return self for chaining.
1355    ///
1356    /// This is useful for building operation pipelines.
1357    ///
1358    /// # Arguments
1359    ///
1360    /// * `url` - The YouTube video URL
1361    ///
1362    /// # Returns
1363    ///
1364    /// A tuple of (self, video) for method chaining
1365    ///
1366    /// # Examples
1367    ///
1368    /// ```rust,no_run
1369    /// # use yt_dlp::Youtube;
1370    /// # use yt_dlp::client::deps::Libraries;
1371    /// # #[tokio::main]
1372    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1373    /// # let libs = Libraries::new("yt-dlp", "ffmpeg");
1374    /// let (youtube, video) = Youtube::builder(libs, "output")
1375    ///     .build()
1376    ///     .await?
1377    ///     .fetch("https://youtube.com/watch?v=dQw4w9WgXcQ")
1378    ///     .await?;
1379    ///
1380    /// println!("Title: {}", video.title);
1381    /// # Ok(())
1382    /// # }
1383    /// ```
1384    pub async fn fetch(self, url: impl Into<String>) -> Result<(Self, model::Video)> {
1385        let video = self.fetch_video_infos(url.into()).await?;
1386        Ok((self, video))
1387    }
1388
1389    /// Fluent method to download a video and return self for chaining.
1390    ///
1391    /// # Arguments
1392    ///
1393    /// * `video` - The video to download
1394    /// * `output` - The output filename
1395    ///
1396    /// # Examples
1397    ///
1398    /// ```rust,no_run
1399    /// # use yt_dlp::Youtube;
1400    /// # use yt_dlp::client::deps::Libraries;
1401    /// # #[tokio::main]
1402    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1403    /// # let libs = Libraries::new("yt-dlp", "ffmpeg");
1404    /// let youtube = Youtube::builder(libs, "output")
1405    ///     .build()
1406    ///     .await?
1407    ///     .fetch("https://youtube.com/watch?v=dQw4w9WgXcQ")
1408    ///     .await?
1409    ///     .0
1410    ///     .download_and_continue(&video, "output.mp4")
1411    ///     .await?;
1412    /// # Ok(())
1413    /// # }
1414    /// ```
1415    pub async fn download_and_continue(
1416        self,
1417        video: &model::Video,
1418        output: impl AsRef<str> + std::fmt::Debug + Display,
1419    ) -> Result<Self> {
1420        self.download_video(video, output).await?;
1421        Ok(self)
1422    }
1423
1424    /// Chain multiple operations in a pipeline.
1425    ///
1426    /// This method allows you to chain fetch -> download -> metadata operations.
1427    ///
1428    /// # Examples
1429    ///
1430    /// ```rust,no_run
1431    /// # use yt_dlp::Youtube;
1432    /// # use yt_dlp::client::deps::Libraries;
1433    /// # #[tokio::main]
1434    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1435    /// # let libs = Libraries::new("yt-dlp", "ffmpeg");
1436    /// Youtube::builder(libs, "output")
1437    ///     .build()
1438    ///     .await?
1439    ///     .pipeline("https://youtube.com/watch?v=dQw4w9WgXcQ", |yt, video| async move {
1440    ///         yt.download_video(&video, "video.mp4").await?;
1441    ///         Ok(yt)
1442    ///     })
1443    ///     .await?;
1444    /// # Ok(())
1445    /// # }
1446    /// ```
1447    pub async fn pipeline<F, Fut>(self, url: impl Into<String>, operation: F) -> Result<Self>
1448    where
1449        F: FnOnce(Self, model::Video) -> Fut,
1450        Fut: std::future::Future<Output = Result<Self>>,
1451    {
1452        let video = self.fetch_video_infos(url.into()).await?;
1453        operation(self, video).await
1454    }
1455
1456    /// Applies post-processing to a video file using FFmpeg.
1457    ///
1458    /// This method allows you to apply various post-processing operations such as:
1459    /// - Codec conversion (H.264, H.265, VP9, AV1)
1460    /// - Bitrate adjustment
1461    /// - Resolution scaling
1462    /// - Video filters (crop, rotate, brightness, contrast, etc.)
1463    ///
1464    /// # Arguments
1465    ///
1466    /// * `input_path` - Path to the input video file
1467    /// * `output` - The output filename
1468    /// * `config` - Post-processing configuration
1469    ///
1470    /// # Errors
1471    ///
1472    /// Returns an error if FFmpeg execution fails
1473    ///
1474    /// # Returns
1475    ///
1476    /// The path to the processed video file
1477    ///
1478    /// # Examples
1479    ///
1480    /// ```rust,no_run
1481    /// # use yt_dlp::Youtube;
1482    /// # use yt_dlp::download::postprocess::{PostProcessConfig, VideoCodec, AudioCodec, Resolution};
1483    /// # use std::path::PathBuf;
1484    /// # use yt_dlp::client::deps::Libraries;
1485    /// # #[tokio::main]
1486    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1487    /// # let libraries = Libraries::new("libs/yt-dlp", "libs/ffmpeg");
1488    /// # let youtube = Youtube::builder(libraries, "output").build().await?;
1489    /// let config = PostProcessConfig::new()
1490    ///     .with_video_codec(VideoCodec::H264)
1491    ///     .with_audio_codec(AudioCodec::AAC)
1492    ///     .with_video_bitrate("2M")
1493    ///     .with_resolution(Resolution::HD);
1494    ///
1495    /// let processed = youtube.postprocess_video("input.mp4", "output.mp4", config).await?;
1496    /// # Ok(())
1497    /// # }
1498    /// ```
1499    pub async fn postprocess_video(
1500        &self,
1501        input_path: impl AsRef<std::path::Path>,
1502        output: impl AsRef<str>,
1503        config: download::postprocess::PostProcessConfig,
1504    ) -> Result<PathBuf> {
1505        let output_path = self.output_dir.join(output.as_ref());
1506
1507        metadata::postprocess::apply_postprocess(
1508            input_path,
1509            &output_path,
1510            &config,
1511            &self.libraries,
1512            self.timeout,
1513        )
1514        .await
1515    }
1516}