chientrm_youtube_dl/
lib.rs

1//! # youtube_dl
2//! A crate for running and parsing the JSON output of `youtube-dl`.
3//! Example usage:
4//! ```rust
5//! use youtube_dl::YoutubeDl;
6//! let output = YoutubeDl::new("https://www.youtube.com/watch?v=VFbhKZFzbzk")
7//!   .socket_timeout("15")
8//!   .run()
9//!   .unwrap();
10//! ```
11
12#![deny(
13    missing_debug_implementations,
14    missing_copy_implementations,
15    trivial_casts,
16    trivial_numeric_casts,
17    unsafe_code,
18    unstable_features,
19    unused_import_braces,
20    unused_qualifications,
21    rust_2018_idioms
22)]
23#![warn(missing_docs)]
24
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27use std::error::Error as StdError;
28use std::fmt;
29#[cfg(target_os = "windows")]
30use std::os::windows::process::CommandExt;
31use std::path::{Path, PathBuf};
32use std::process::ExitStatus;
33use std::time::Duration;
34
35#[cfg(target_os = "windows")]
36const CREATE_NO_WINDOW: u32 = 0x08000000;
37
38/// Exposes a function to download the latest version of youtube-dl/yt-dlp.
39#[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
40pub mod downloader;
41pub mod model;
42
43pub use crate::model::*;
44
45#[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
46pub use crate::downloader::download_yt_dlp;
47
48/// Data returned by `YoutubeDl::run`. Output can either be a single video or a playlist of videos.
49#[derive(Clone, Serialize, Deserialize, Debug)]
50pub enum YoutubeDlOutput {
51    /// Playlist result
52    Playlist(Box<Playlist>),
53    /// Single video result
54    SingleVideo(Box<SingleVideo>),
55}
56
57impl YoutubeDlOutput {
58    /// Get the inner content as a single video.
59    pub fn into_single_video(self) -> Option<SingleVideo> {
60        match self {
61            YoutubeDlOutput::SingleVideo(video) => Some(*video),
62            _ => None,
63        }
64    }
65
66    /// Get the inner content as a playlist.
67    pub fn into_playlist(self) -> Option<Playlist> {
68        match self {
69            YoutubeDlOutput::Playlist(playlist) => Some(*playlist),
70            _ => None,
71        }
72    }
73}
74
75/// Errors that can occur during executing `youtube-dl` or during parsing the output.
76#[derive(Debug)]
77pub enum Error {
78    /// I/O error
79    Io(std::io::Error),
80
81    /// Error parsing JSON
82    Json(serde_json::Error),
83
84    /// `youtube-dl` returned a non-zero exit code
85    ExitCode {
86        /// Exit code
87        code: i32,
88        /// Standard error of youtube-dl
89        stderr: String,
90    },
91
92    /// Process-level timeout expired.
93    ProcessTimeout,
94
95    /// HTTP error (when fetching youtube-dl/yt-dlp)
96    #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
97    Http(reqwest::Error),
98
99    /// When no GitHub release could be found to download the youtube-dl/yt-dlp executable.
100    #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
101    NoReleaseFound,
102}
103
104impl From<std::io::Error> for Error {
105    fn from(err: std::io::Error) -> Self {
106        Error::Io(err)
107    }
108}
109
110impl From<serde_json::Error> for Error {
111    fn from(err: serde_json::Error) -> Self {
112        Error::Json(err)
113    }
114}
115
116#[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
117impl From<reqwest::Error> for Error {
118    fn from(err: reqwest::Error) -> Self {
119        Error::Http(err)
120    }
121}
122
123impl fmt::Display for Error {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            Self::Io(err) => write!(f, "io error: {}", err),
127            Self::Json(err) => write!(f, "json error: {}", err),
128            Self::ExitCode { code, stderr } => {
129                write!(f, "non-zero exit code: {}, stderr: {}", code, stderr)
130            }
131            Self::ProcessTimeout => write!(f, "process timed out"),
132            #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
133            Self::Http(err) => write!(f, "http error: {}", err),
134            #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
135            Self::NoReleaseFound => write!(f, "no github release found for specified binary"),
136        }
137    }
138}
139
140impl StdError for Error {
141    fn source(&self) -> Option<&(dyn StdError + 'static)> {
142        match self {
143            Self::Io(err) => Some(err),
144            Self::Json(err) => Some(err),
145            Self::ExitCode { .. } => None,
146            Self::ProcessTimeout => None,
147            #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
148            Self::Http(err) => Some(err),
149            #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
150            Self::NoReleaseFound => None,
151        }
152    }
153}
154
155/// The search options currently supported by youtube-dl, and a custom option to allow
156/// specifying custom options, in case this library is outdated.
157#[derive(Clone, Debug)]
158pub enum SearchType {
159    /// Search on youtube.com
160    Youtube,
161    /// Search with yahoo.com's video search
162    Yahoo,
163    /// Search with Google's video search
164    Google,
165    /// Search on SoundCloud
166    SoundCloud,
167    /// Allows to specify a custom search type, for forwards compatibility purposes.
168    Custom(String),
169}
170
171impl fmt::Display for SearchType {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            SearchType::Yahoo => write!(f, "yvsearch"),
175            SearchType::Youtube => write!(f, "ytsearch"),
176            SearchType::Google => write!(f, "gvsearch"),
177            SearchType::SoundCloud => write!(f, "scsearch"),
178            SearchType::Custom(name) => write!(f, "{}", name),
179        }
180    }
181}
182
183/// Specifies where to search, how many results to fetch and the query. The count
184/// defaults to 1, but can be changed with the `with_count` method.
185#[derive(Clone, Debug)]
186pub struct SearchOptions {
187    search_type: SearchType,
188    count: usize,
189    query: String,
190}
191
192impl SearchOptions {
193    /// Search on youtube.com
194    pub fn youtube(query: impl Into<String>) -> Self {
195        Self {
196            query: query.into(),
197            search_type: SearchType::Youtube,
198            count: 1,
199        }
200    }
201    /// Search with Google's video search
202    pub fn google(query: impl Into<String>) -> Self {
203        Self {
204            query: query.into(),
205            search_type: SearchType::Google,
206            count: 1,
207        }
208    }
209    /// Search with yahoo.com's video search
210    pub fn yahoo(query: impl Into<String>) -> Self {
211        Self {
212            query: query.into(),
213            search_type: SearchType::Yahoo,
214            count: 1,
215        }
216    }
217    /// Search on SoundCloud
218    pub fn soundcloud(query: impl Into<String>) -> Self {
219        Self {
220            query: query.into(),
221            search_type: SearchType::SoundCloud,
222            count: 1,
223        }
224    }
225    /// Search with a custom search provider (in case this library falls behind the feature set of youtube-dl)
226    pub fn custom(search_type: impl Into<String>, query: impl Into<String>) -> Self {
227        Self {
228            query: query.into(),
229            search_type: SearchType::Custom(search_type.into()),
230            count: 1,
231        }
232    }
233    /// Set the count for how many videos at most to retrieve from the search.
234    pub fn with_count(self, count: usize) -> Self {
235        Self {
236            search_type: self.search_type,
237            query: self.query,
238            count,
239        }
240    }
241}
242
243impl fmt::Display for SearchOptions {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "{}{}:{}", self.search_type, self.count, self.query)
246    }
247}
248
249/// A builder to create a `youtube-dl` command to execute.
250#[derive(Clone, Debug)]
251pub struct YoutubeDl {
252    youtube_dl_path: Option<PathBuf>,
253    format: Option<String>,
254    flat_playlist: bool,
255    socket_timeout: Option<String>,
256    all_formats: bool,
257    auth: Option<(String, String)>,
258    cookies: Option<String>,
259    user_agent: Option<String>,
260    referer: Option<String>,
261    url: String,
262    process_timeout: Option<Duration>,
263    playlist_reverse: bool,
264    date_before: Option<String>,
265    date_after: Option<String>,
266    date: Option<String>,
267    extract_audio: bool,
268    playlist_items: Option<String>,
269    extra_args: Vec<String>,
270    output_template: Option<String>,
271    output_directory: Option<String>,
272    #[cfg(test)]
273    debug: bool,
274    ignore_errors: bool,
275}
276
277impl YoutubeDl {
278    /// Create a new builder.
279    pub fn new(url: impl Into<String>) -> Self {
280        Self {
281            url: url.into(),
282            youtube_dl_path: None,
283            format: None,
284            flat_playlist: false,
285            socket_timeout: None,
286            all_formats: false,
287            auth: None,
288            cookies: None,
289            user_agent: None,
290            referer: None,
291            process_timeout: None,
292            date: None,
293            date_after: None,
294            date_before: None,
295            playlist_reverse: false,
296            extract_audio: false,
297            playlist_items: None,
298            extra_args: Vec::new(),
299            output_template: None,
300            output_directory: None,
301            #[cfg(test)]
302            debug: false,
303            ignore_errors: false,
304        }
305    }
306
307    /// Performs a search with the given search options.
308    pub fn search_for(options: &SearchOptions) -> Self {
309        Self::new(options.to_string())
310    }
311
312    /// Set the path to the `youtube-dl` or `yt-dlp executable.
313    pub fn youtube_dl_path<P: AsRef<Path>>(&mut self, youtube_dl_path: P) -> &mut Self {
314        self.youtube_dl_path = Some(youtube_dl_path.as_ref().to_owned());
315        self
316    }
317
318    /// Set the `-f` command line option.
319    pub fn format<S: Into<String>>(&mut self, format: S) -> &mut Self {
320        self.format = Some(format.into());
321        self
322    }
323
324    /// Set the `--flat-playlist` command line flag.
325    pub fn flat_playlist(&mut self, flat_playlist: bool) -> &mut Self {
326        self.flat_playlist = flat_playlist;
327        self
328    }
329
330    /// Set the `--socket-timeout` command line flag.
331    pub fn socket_timeout<S: Into<String>>(&mut self, socket_timeout: S) -> &mut Self {
332        self.socket_timeout = Some(socket_timeout.into());
333        self
334    }
335
336    /// Set the `--user-agent` command line flag.
337    pub fn user_agent<S: Into<String>>(&mut self, user_agent: S) -> &mut Self {
338        self.user_agent = Some(user_agent.into());
339        self
340    }
341
342    /// Set the `--playlist-reverse` flag. Useful with break-on-reject and date_before
343    /// for faster queries.
344    pub fn playlist_reverse(&mut self, playlist_reverse: bool) -> &mut Self {
345        self.playlist_reverse = playlist_reverse;
346        self
347    }
348
349    /// Sets the `--date` command line flag only downloading/viewing videos on this date
350    pub fn date<S: Into<String>>(&mut self, date_string: S) -> &mut Self {
351        self.date = Some(date_string.into());
352        self
353    }
354
355    /// Set the `--datebefore` flag only downloading/viewing videos on or before this date
356    pub fn date_before<S: Into<String>>(&mut self, date_string: S) -> &mut Self {
357        self.date_before = Some(date_string.into());
358        self
359    }
360
361    /// Set the `--dateafter` flag only downloading/viewing vidieos on or after this date
362    pub fn date_after<S: Into<String>>(&mut self, date_string: S) -> &mut Self {
363        self.date_after = Some(date_string.into());
364        self
365    }
366
367    /// Set the `--referer` command line flag.
368    pub fn referer<S: Into<String>>(&mut self, referer: S) -> &mut Self {
369        self.referer = Some(referer.into());
370        self
371    }
372
373    /// Set the `--all-formats` command line flag.
374    pub fn all_formats(&mut self, all_formats: bool) -> &mut Self {
375        self.all_formats = all_formats;
376        self
377    }
378
379    /// Set the `-u` and `-p` command line flags.
380    pub fn auth<S: Into<String>>(&mut self, username: S, password: S) -> &mut Self {
381        self.auth = Some((username.into(), password.into()));
382        self
383    }
384
385    /// Specify a file with cookies in Netscape cookie format.
386    pub fn cookies<S: Into<String>>(&mut self, cookie_path: S) -> &mut Self {
387        self.cookies = Some(cookie_path.into());
388        self
389    }
390
391    /// Set a process-level timeout for youtube-dl. (this controls the maximum overall duration
392    /// the process may take, when it times out, `Error::ProcessTimeout` is returned)
393    pub fn process_timeout(&mut self, timeout: Duration) -> &mut Self {
394        self.process_timeout = Some(timeout);
395        self
396    }
397
398    /// Set the `--extract-audio` command line flag.
399    pub fn extract_audio(&mut self, extract_audio: bool) -> &mut Self {
400        self.extract_audio = extract_audio;
401        self
402    }
403
404    /// Set the `--playlist-items` command line flag.
405    pub fn playlist_items(&mut self, index: u32) -> &mut Self {
406        self.playlist_items = Some(index.to_string());
407        self
408    }
409
410    /// Add an additional custom CLI argument.
411    ///
412    /// This allows specifying arguments that are not covered by other
413    /// configuration methods.
414    pub fn extra_arg<S: Into<String>>(&mut self, arg: S) -> &mut Self {
415        self.extra_args.push(arg.into());
416        self
417    }
418
419    /// Specify the filename template. Only relevant for downloading.
420    /// (referred to as "output template" by [youtube-dl docs](https://github.com/ytdl-org/youtube-dl#output-template))
421    pub fn output_template<S: Into<String>>(&mut self, arg: S) -> &mut Self {
422        self.output_template = Some(arg.into());
423        self
424    }
425
426    /// Specify the output directory. Only relevant for downloading.
427    /// (the `-P` command line switch)
428    pub fn output_directory<S: Into<String>>(&mut self, arg: S) -> &mut Self {
429        self.output_directory = Some(arg.into());
430        self
431    }
432
433    #[cfg(test)]
434    pub fn debug(&mut self, arg: bool) -> &mut Self {
435        self.debug = arg;
436        self
437    }
438
439    /// Specify whether to ignore errors (exit code & flag)
440    pub fn ignore_errors(&mut self, arg: bool) -> &mut Self {
441        self.ignore_errors = arg;
442        self
443    }
444
445    fn path(&self) -> &Path {
446        match &self.youtube_dl_path {
447            Some(path) => path,
448            None => Path::new("yt-dlp"),
449        }
450    }
451
452    fn common_args(&self) -> Vec<&str> {
453        let mut args = vec![];
454        if let Some(format) = &self.format {
455            args.push("-f");
456            args.push(format);
457        }
458
459        if self.flat_playlist {
460            args.push("--flat-playlist");
461        }
462
463        if let Some(timeout) = &self.socket_timeout {
464            args.push("--socket-timeout");
465            args.push(timeout);
466        }
467
468        if self.all_formats {
469            args.push("--all-formats");
470        }
471
472        if let Some((user, password)) = &self.auth {
473            args.push("-u");
474            args.push(user);
475            args.push("-p");
476            args.push(password);
477        }
478
479        if let Some(cookie_path) = &self.cookies {
480            args.push("--cookies");
481            args.push(cookie_path);
482        }
483
484        if let Some(user_agent) = &self.user_agent {
485            args.push("--user-agent");
486            args.push(user_agent);
487        }
488
489        if let Some(referer) = &self.referer {
490            args.push("--referer");
491            args.push(referer);
492        }
493
494        if self.extract_audio {
495            args.push("--extract-audio");
496        }
497
498        if let Some(playlist_items) = &self.playlist_items {
499            args.push("--playlist-items");
500            args.push(playlist_items);
501        }
502
503        if let Some(output_template) = &self.output_template {
504            args.push("-o");
505            args.push(output_template);
506        }
507
508        if let Some(output_dir) = &self.output_directory {
509            args.push("-P");
510            args.push(output_dir);
511        }
512
513        if let Some(date) = &self.date {
514            args.push("--date");
515            args.push(date);
516        }
517
518        if let Some(date_after) = &self.date_after {
519            args.push("--dateafter");
520            args.push(date_after);
521        }
522
523        if let Some(date_before) = &self.date_before {
524            args.push("--datebefore");
525            args.push(date_before);
526        }
527
528        if self.ignore_errors {
529            args.push("--ignore-errors");
530        }
531
532        for extra_arg in &self.extra_args {
533            args.push(extra_arg);
534        }
535
536        args
537    }
538
539    fn process_args(&self) -> Vec<&str> {
540        let mut args = self.common_args();
541
542        if let Some(output_dir) = &self.output_directory {
543            args.push("-P");
544            args.push(output_dir);
545        }
546
547        args.push("-J");
548        args.push(&self.url);
549        log::debug!("youtube-dl arguments: {:?}", args);
550
551        args
552    }
553
554    fn process_download_args<'a>(&'a self, folder: &'a str) -> Vec<&'a str> {
555        let mut args = self.common_args();
556
557        args.push("-P");
558        args.push(folder);
559        args.push("--no-simulate");
560        args.push("--no-progress");
561        args.push(&self.url);
562        log::debug!("youtube-dl arguments: {:?}", args);
563
564        args
565    }
566
567    fn run_process(&self, args: Vec<&str>) -> Result<ProcessResult, Error> {
568        use std::io::Read;
569        use std::process::{Command, Stdio};
570        use wait_timeout::ChildExt;
571
572        let path = self.path();
573        #[cfg(not(target_os = "windows"))]
574        let mut child = Command::new(path)
575            .stdout(Stdio::piped())
576            .stderr(Stdio::piped())
577            .args(args)
578            .spawn()?;
579        #[cfg(target_os = "windows")]
580        let mut child = Command::new(path)
581            .creation_flags(CREATE_NO_WINDOW)
582            .stdout(Stdio::piped())
583            .stderr(Stdio::piped())
584            .args(args)
585            .spawn()?;
586
587        // Continually read from stdout so that it does not fill up with large output and hang forever.
588        // We don't need to do this for stderr since only stdout has potentially giant JSON.
589        let mut stdout = Vec::new();
590        let child_stdout = child.stdout.take();
591        std::io::copy(&mut child_stdout.unwrap(), &mut stdout)?;
592
593        let exit_code = if let Some(timeout) = self.process_timeout {
594            match child.wait_timeout(timeout)? {
595                Some(status) => status,
596                None => {
597                    child.kill()?;
598                    return Err(Error::ProcessTimeout);
599                }
600            }
601        } else {
602            child.wait()?
603        };
604
605        let mut stderr = vec![];
606        if let Some(mut reader) = child.stderr {
607            reader.read_to_end(&mut stderr)?;
608        }
609
610        Ok(ProcessResult {
611            stdout,
612            stderr,
613            exit_code,
614        })
615    }
616
617    #[cfg(feature = "tokio")]
618    async fn run_process_async(&self, args: Vec<&str>) -> Result<ProcessResult, Error> {
619        use std::process::Stdio;
620        use tokio::io::AsyncReadExt;
621        use tokio::process::Command;
622        use tokio::time::timeout;
623
624        let path = self.path();
625        #[cfg(not(target_os = "windows"))]
626        let mut child = Command::new(path)
627            .stdout(Stdio::piped())
628            .stderr(Stdio::piped())
629            .args(args)
630            .spawn()?;
631        #[cfg(target_os = "windows")]
632        let mut child = Command::new(path)
633            .creation_flags(CREATE_NO_WINDOW)
634            .stdout(Stdio::piped())
635            .stderr(Stdio::piped())
636            .args(args)
637            .spawn()?;
638
639        // Continually read from stdout so that it does not fill up with large output and hang forever.
640        // We don't need to do this for stderr since only stdout has potentially giant JSON.
641        let mut stdout = Vec::new();
642        let child_stdout = child.stdout.take();
643        tokio::io::copy(&mut child_stdout.unwrap(), &mut stdout).await?;
644
645        let exit_code = if let Some(dur) = self.process_timeout {
646            match timeout(dur, child.wait()).await {
647                Ok(n) => n?,
648                Err(_) => {
649                    child.kill().await?;
650                    return Err(Error::ProcessTimeout);
651                }
652            }
653        } else {
654            child.wait().await?
655        };
656        let mut stderr = vec![];
657        if let Some(mut reader) = child.stderr {
658            reader.read_to_end(&mut stderr).await?;
659        }
660
661        Ok(ProcessResult {
662            stdout,
663            stderr,
664            exit_code,
665        })
666    }
667
668    fn process_json_output(&self, stdout: Vec<u8>) -> Result<YoutubeDlOutput, Error> {
669        use serde_json::json;
670
671        #[cfg(test)]
672        if self.debug {
673            let string = std::str::from_utf8(&stdout).expect("invalid utf-8 output");
674            eprintln!("{}", string);
675        }
676
677        let value: Value = serde_json::from_reader(stdout.as_slice())?;
678
679        let is_playlist = value["_type"] == json!("playlist");
680        if is_playlist {
681            let playlist: Playlist = serde_json::from_value(value)?;
682            Ok(YoutubeDlOutput::Playlist(Box::new(playlist)))
683        } else {
684            let video: SingleVideo = serde_json::from_value(value)?;
685            Ok(YoutubeDlOutput::SingleVideo(Box::new(video)))
686        }
687    }
688
689    /// Run yt-dlp with the arguments specified through the builder and parse its
690    /// JSON ouput into `YoutubeDlOutput`. Note: This can fail when the JSON output
691    /// is not compatible with the struct definitions in this crate.
692    pub fn run(&self) -> Result<YoutubeDlOutput, Error> {
693        let args = self.process_args();
694        let ProcessResult {
695            stderr,
696            stdout,
697            exit_code,
698        } = self.run_process(args)?;
699
700        if exit_code.success() || self.ignore_errors {
701            self.process_json_output(stdout)
702        } else {
703            let stderr = String::from_utf8(stderr).unwrap_or_default();
704            Err(Error::ExitCode {
705                code: exit_code.code().unwrap_or(1),
706                stderr,
707            })
708        }
709    }
710
711    /// Run yt-dlp with the arguments through the builder and parse its JSON output
712    /// into a `serde_json::Value`. This is meant as a fallback for when the JSON
713    /// output is not compatible with the struct definitions in this crate.
714    pub fn run_raw(&self) -> Result<Value, Error> {
715        let args = self.process_args();
716        let ProcessResult {
717            stderr,
718            stdout,
719            exit_code,
720        } = self.run_process(args)?;
721
722        if exit_code.success() || self.ignore_errors {
723            let value: Value = serde_json::from_reader(stdout.as_slice())?;
724            Ok(value)
725        } else {
726            let stderr = String::from_utf8(stderr).unwrap_or_default();
727            Err(Error::ExitCode {
728                code: exit_code.code().unwrap_or(1),
729                stderr,
730            })
731        }
732    }
733
734    /// Run yt-dlp asynchronously with the arguments specified through the builder.
735    #[cfg(feature = "tokio")]
736    pub async fn run_async(&self) -> Result<YoutubeDlOutput, Error> {
737        let args = self.process_args();
738        let ProcessResult {
739            stderr,
740            stdout,
741            exit_code,
742        } = self.run_process_async(args).await?;
743
744        if exit_code.success() || self.ignore_errors {
745            self.process_json_output(stdout)
746        } else {
747            let stderr = String::from_utf8(stderr).unwrap_or_default();
748            Err(Error::ExitCode {
749                code: exit_code.code().unwrap_or(1),
750                stderr,
751            })
752        }
753    }
754
755    /// Run yt-dlp asynchronously with the arguments through the builder and parse its JSON output
756    /// into a `serde_json::Value`. This is meant as a fallback for when the JSON
757    /// output is not compatible with the struct definitions in this crate.
758    #[cfg(feature = "tokio")]
759    pub async fn run_raw_async(&self) -> Result<Value, Error> {
760        let args = self.process_args();
761        let ProcessResult {
762            stderr,
763            stdout,
764            exit_code,
765        } = self.run_process_async(args).await?;
766
767        if exit_code.success() || self.ignore_errors {
768            let value: Value = serde_json::from_reader(stdout.as_slice())?;
769            Ok(value)
770        } else {
771            let stderr = String::from_utf8(stderr).unwrap_or_default();
772            Err(Error::ExitCode {
773                code: exit_code.code().unwrap_or(1),
774                stderr,
775            })
776        }
777    }
778
779    /// Download the file to the specified destination folder.
780    pub fn download_to(&self, folder: impl AsRef<Path>) -> Result<(), Error> {
781        let folder_str = folder.as_ref().to_string_lossy();
782        let args = self.process_download_args(&folder_str);
783        self.run_process(args)?;
784
785        Ok(())
786    }
787
788    /// Download the file to the specified destination folder asynchronously.
789    #[cfg(feature = "tokio")]
790    pub async fn download_to_async(&self, folder: impl AsRef<Path>) -> Result<(), Error> {
791        let folder_str = folder.as_ref().to_string_lossy();
792        let args = self.process_download_args(&folder_str);
793        self.run_process_async(args).await?;
794
795        Ok(())
796    }
797}
798
799struct ProcessResult {
800    stdout: Vec<u8>,
801    stderr: Vec<u8>,
802    exit_code: ExitStatus,
803}
804
805#[cfg(test)]
806mod tests {
807    use crate::{Protocol, SearchOptions, YoutubeDl};
808
809    use std::path::Path;
810    use std::time::Duration;
811
812    #[test]
813    fn test_youtube_url() {
814        let output = YoutubeDl::new("https://www.youtube.com/watch?v=7XGyWcuYVrg")
815            .socket_timeout("15")
816            .run()
817            .unwrap()
818            .into_single_video()
819            .unwrap();
820        assert_eq!(output.id, "7XGyWcuYVrg");
821    }
822
823    #[test]
824    fn test_with_timeout() {
825        let output = YoutubeDl::new("https://www.youtube.com/watch?v=7XGyWcuYVrg")
826            .socket_timeout("15")
827            .process_timeout(Duration::from_secs(15))
828            .run()
829            .unwrap()
830            .into_single_video()
831            .unwrap();
832        assert_eq!(output.id, "7XGyWcuYVrg");
833    }
834
835    #[test]
836    fn test_unknown_url() {
837        YoutubeDl::new("https://www.rust-lang.org")
838            .socket_timeout("15")
839            .process_timeout(Duration::from_secs(15))
840            .run()
841            .unwrap_err();
842    }
843
844    #[test]
845    fn test_search() {
846        let output = YoutubeDl::search_for(&SearchOptions::youtube("Never Gonna Give You Up"))
847            .socket_timeout("15")
848            .process_timeout(Duration::from_secs(15))
849            .run()
850            .unwrap()
851            .into_playlist()
852            .unwrap();
853        assert_eq!(output.entries.unwrap().first().unwrap().id, "dQw4w9WgXcQ");
854    }
855
856    #[test]
857    fn correct_format_codec_parsing() {
858        let output = YoutubeDl::new("https://www.youtube.com/watch?v=WhWc3b3KhnY")
859            .run()
860            .unwrap()
861            .into_single_video()
862            .unwrap();
863
864        let mut none_counter = 0;
865        for format in output.formats.unwrap() {
866            assert_ne!(Some("none".to_string()), format.acodec);
867            assert_ne!(Some("none".to_string()), format.vcodec);
868            if format.acodec.is_none() || format.vcodec.is_none() {
869                none_counter += 1;
870            }
871        }
872        assert!(none_counter > 0);
873    }
874
875    #[cfg(feature = "tokio")]
876    #[test]
877    fn test_async() {
878        use tokio::runtime::Runtime;
879        let runtime = Runtime::new().unwrap();
880        let output = runtime.block_on(async move {
881            YoutubeDl::new("https://www.youtube.com/watch?v=7XGyWcuYVrg")
882                .socket_timeout("15")
883                .run_async()
884                .await
885                .unwrap()
886                .into_single_video()
887                .unwrap()
888        });
889        assert_eq!(output.id, "7XGyWcuYVrg");
890    }
891
892    #[test]
893    fn test_with_yt_dlp() {
894        let output = YoutubeDl::new("https://www.youtube.com/watch?v=7XGyWcuYVrg")
895            .run()
896            .unwrap()
897            .into_single_video()
898            .unwrap();
899        assert_eq!(output.id, "7XGyWcuYVrg");
900    }
901
902    #[test]
903
904    fn test_download_with_yt_dlp() {
905        // yee
906        YoutubeDl::new("https://www.youtube.com/watch?v=q6EoRBvdVPQ")
907            .debug(true)
908            .output_template("yee")
909            .download_to(".")
910            .unwrap();
911        assert!(Path::new("yee.webm").is_file() || Path::new("yee").is_file());
912        let _ = std::fs::remove_file("yee.webm");
913        let _ = std::fs::remove_file("yee");
914    }
915
916    #[test]
917
918    fn test_timestamp_parse_error() {
919        let output = YoutubeDl::new("https://www.reddit.com/r/loopdaddy/comments/baguqq/first_time_poster_here_couldnt_resist_sharing_my")
920            .output_template("video")
921            .run()
922            .unwrap();
923        assert_eq!(output.into_single_video().unwrap().width, Some(608.0));
924    }
925
926    #[test]
927    fn test_protocol_fallback() {
928        let parsed_protocol: Protocol = serde_json::from_str("\"http\"").unwrap();
929        assert!(matches!(parsed_protocol, Protocol::Http));
930
931        let unknown_protocol: Protocol = serde_json::from_str("\"some_unknown_protocol\"").unwrap();
932        assert!(matches!(unknown_protocol, Protocol::Unknown));
933    }
934
935    #[test]
936    fn test_download_to_destination() {
937        let dir = tempfile::tempdir().unwrap();
938
939        YoutubeDl::new("https://www.youtube.com/watch?v=q6EoRBvdVPQ")
940            .download_to(&dir)
941            .unwrap();
942
943        let files: Vec<_> = std::fs::read_dir(&dir).unwrap().collect();
944        assert_eq!(1, files.len());
945        assert!(files[0].as_ref().unwrap().path().is_file());
946    }
947}