1use 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#[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#[derive(Clone, Debug)]
103pub struct YoutubeDL {
104 path: PathBuf,
105 links: Vec<String>,
106 args: Vec<Arg>,
107}
108
109#[derive(Debug, Clone)]
116pub struct YoutubeDLResult {
117 path: PathBuf,
118 output: String,
119}
120
121impl YoutubeDLResult {
122 fn new(path: &PathBuf) -> YoutubeDLResult {
124 YoutubeDLResult {
125 path: path.clone(),
126 output: String::new(),
127 }
128 }
129
130 pub fn output(&self) -> &str {
132 &self.output
133 }
134
135 pub fn output_dir(&self) -> &PathBuf {
137 &self.path
138 }
139}
140
141impl YoutubeDL {
142 pub fn new_multiple_links(
148 dl_path: &PathBuf,
149 args: Vec<Arg>,
150 links: Vec<String>,
151 ) -> Result<YoutubeDL> {
152 let path = Path::new(dl_path);
154
155 if !path.exists() {
157 create_dir_all(&path)?;
159 }
160
161 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 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 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 ¤t_dir,
225 vec![Arg::new("--version")],
227 "",
229 )?;
230
231 let regex = Regex::new(r"\d{4}\.\d{2}\.\d{2}")?;
232 let output = ytd.download()?;
233
234 assert!(regex.is_match(output.output()));
237 Ok(())
238 }
239}