ytd_rs/
lib.rs

1//! Rust-Wrapper for youtube-dl
2//!
3//! # Example
4//!
5//! ```no_run
6//! use ytd_rs::{YoutubeDL, Arg};
7//! use std::path::PathBuf;
8//! use std::error::Error;
9//! fn main() -> Result<(), Box<dyn Error>> {
10//!     // youtube-dl arguments quietly run process and to format the output
11//!     // one doesn't take any input and is an option, the other takes the desired output format as input
12//!     let args = vec![Arg::new("--quiet"), Arg::new_with_arg("--output", "%(title).90s.%(ext)s")];
13//!     let link = "https://www.youtube.com/watch?v=uTO0KnDsVH0";
14//!     let path = PathBuf::from("./path/to/download/directory");
15//!     let ytd = YoutubeDL::new(&path, args, link)?;
16//!
17//!     // start download
18//!     let download = ytd.download()?;
19//!
20//!     // check what the result is and print out the path to the download or the error
21//!     println!("Your download: {}", download.output_dir().to_string_lossy());
22//!     Ok(())
23//! }
24//! ```
25
26use error::YoutubeDLError;
27use std::{
28    fmt,
29    process::{Output, Stdio},
30};
31use std::{
32    fmt::{Display, Formatter},
33    fs::{canonicalize, create_dir_all},
34    path::PathBuf,
35};
36use std::{path::Path, process::Command};
37
38pub mod error;
39type Result<T> = std::result::Result<T, YoutubeDLError>;
40
41const YOUTUBE_DL_COMMAND: &str = if cfg!(feature = "youtube-dlc") {
42    "youtube-dlc"
43} else if cfg!(feature = "yt-dlp") {
44    "yt-dlp"
45} else {
46    "youtube-dl"
47};
48
49/// A structure that represents an argument of a youtube-dl command.
50///
51/// There are two different kinds of Arg:
52/// - Option with no other input
53/// - Argument with input
54///
55/// # Example
56///
57/// ```
58/// use ytd_rs::Arg;
59/// // youtube-dl option to embed metadata into the file
60/// // doesn't take any input
61/// let simple_arg = Arg::new("--add-metadata");
62///
63/// // youtube-dl cookies argument that takes a path to
64/// // cookie file
65/// let input_arg = Arg::new_with_arg("--cookie", "/path/to/cookie");
66/// ```
67#[derive(Clone, Debug)]
68pub struct Arg {
69    arg: String,
70    input: Option<String>,
71}
72
73impl Arg {
74    pub fn new(argument: &str) -> Arg {
75        Arg {
76            arg: argument.to_string(),
77            input: None,
78        }
79    }
80
81    pub fn new_with_arg(argument: &str, input: &str) -> Arg {
82        Arg {
83            arg: argument.to_string(),
84            input: Option::from(input.to_string()),
85        }
86    }
87}
88
89impl Display for Arg {
90    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
91        match &self.input {
92            Some(input) => write!(fmt, "{} {}", self.arg, input),
93            None => write!(fmt, "{}", self.arg),
94        }
95    }
96}
97
98/// Structure that represents a youtube-dl task.
99///
100/// Every task needs a download location, a list of ['Arg'] that can be empty
101/// and a ['link'] to the desired source.
102#[derive(Clone, Debug)]
103pub struct YoutubeDL {
104    path: PathBuf,
105    links: Vec<String>,
106    args: Vec<Arg>,
107}
108
109///
110/// This is the result of a [`YoutubeDL`].
111///
112/// It contains the information about the exit status, the output and the directory it was executed
113/// in.
114///
115#[derive(Debug, Clone)]
116pub struct YoutubeDLResult {
117    path: PathBuf,
118    output: String,
119}
120
121impl YoutubeDLResult {
122    /// creates a new YoutubeDLResult
123    fn new(path: &PathBuf) -> YoutubeDLResult {
124        YoutubeDLResult {
125            path: path.clone(),
126            output: String::new(),
127        }
128    }
129
130    /// get the output of the youtube-dl process
131    pub fn output(&self) -> &str {
132        &self.output
133    }
134
135    /// get the directory where youtube-dl was executed
136    pub fn output_dir(&self) -> &PathBuf {
137        &self.path
138    }
139}
140
141impl YoutubeDL {
142    /// Creates a new YoutubeDL job to be executed.
143    /// It takes a path where youtube-dl should be executed, a vec! of [`Arg`] that can be empty
144    /// and finally a link that can be `""` if no video should be downloaded
145    ///
146    /// The path gets canonicalized and the directory gets created by the constructor
147    pub fn new_multiple_links(
148        dl_path: &PathBuf,
149        args: Vec<Arg>,
150        links: Vec<String>,
151    ) -> Result<YoutubeDL> {
152        // create path
153        let path = Path::new(dl_path);
154
155        // check if it already exists
156        if !path.exists() {
157            // if not create
158            create_dir_all(&path)?;
159        }
160
161        // return error if no directory
162        if !path.is_dir() {
163            return Err(YoutubeDLError::IOError(std::io::Error::new(
164                std::io::ErrorKind::Other,
165                "path is not a directory",
166            )));
167        }
168
169        // absolute path
170        let path = canonicalize(dl_path)?;
171        Ok(YoutubeDL { path, links, args })
172    }
173
174    pub fn new(dl_path: &PathBuf, args: Vec<Arg>, link: &str) -> Result<YoutubeDL> {
175        YoutubeDL::new_multiple_links(dl_path, args, vec![link.to_string()])
176    }
177
178    /// Starts the download and returns when finished the result as [`YoutubeDLResult`].
179    pub fn download(&self) -> Result<YoutubeDLResult> {
180        let output = self.spawn_youtube_dl()?;
181        let mut result = YoutubeDLResult::new(&self.path);
182
183        if !output.status.success() {
184            return Err(YoutubeDLError::Failure(String::from_utf8(output.stderr)?));
185        }
186        result.output = String::from_utf8(output.stdout)?;
187
188        Ok(result)
189    }
190
191    fn spawn_youtube_dl(&self) -> Result<Output> {
192        let mut cmd = Command::new(YOUTUBE_DL_COMMAND);
193        cmd.current_dir(&self.path)
194            .env("LC_ALL", "en_US.UTF-8")
195            .stdout(Stdio::piped())
196            .stderr(Stdio::piped());
197
198        for arg in self.args.iter() {
199            match &arg.input {
200                Some(input) => cmd.arg(&arg.arg).arg(input),
201                None => cmd.arg(&arg.arg),
202            };
203        }
204
205        for link in self.links.iter() {
206            cmd.arg(&link);
207        }
208
209        let pr = cmd.spawn()?;
210        Ok(pr.wait_with_output()?)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use crate::{Arg, YoutubeDL};
217    use regex::Regex;
218    use std::{env, error::Error};
219
220    #[test]
221    fn version() -> Result<(), Box<dyn Error>> {
222        let current_dir = env::current_dir()?;
223        let ytd = YoutubeDL::new(
224            &current_dir,
225            // get youtube-dl version
226            vec![Arg::new("--version")],
227            // we don't need a link to print version
228            "",
229        )?;
230
231        let regex = Regex::new(r"\d{4}\.\d{2}\.\d{2}")?;
232        let output = ytd.download()?;
233
234        // check output
235        // fails if youtube-dl is not installed
236        assert!(regex.is_match(output.output()));
237        Ok(())
238    }
239}