1#![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#[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#[derive(Clone, Serialize, Deserialize, Debug)]
50pub enum YoutubeDlOutput {
51 Playlist(Box<Playlist>),
53 SingleVideo(Box<SingleVideo>),
55}
56
57impl YoutubeDlOutput {
58 pub fn into_single_video(self) -> Option<SingleVideo> {
60 match self {
61 YoutubeDlOutput::SingleVideo(video) => Some(*video),
62 _ => None,
63 }
64 }
65
66 pub fn into_playlist(self) -> Option<Playlist> {
68 match self {
69 YoutubeDlOutput::Playlist(playlist) => Some(*playlist),
70 _ => None,
71 }
72 }
73}
74
75#[derive(Debug)]
77pub enum Error {
78 Io(std::io::Error),
80
81 Json(serde_json::Error),
83
84 ExitCode {
86 code: i32,
88 stderr: String,
90 },
91
92 ProcessTimeout,
94
95 #[cfg(any(feature = "downloader-rustls-tls", feature = "downloader-native-tls"))]
97 Http(reqwest::Error),
98
99 #[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#[derive(Clone, Debug)]
158pub enum SearchType {
159 Youtube,
161 Yahoo,
163 Google,
165 SoundCloud,
167 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#[derive(Clone, Debug)]
186pub struct SearchOptions {
187 search_type: SearchType,
188 count: usize,
189 query: String,
190}
191
192impl SearchOptions {
193 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 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 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 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 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 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#[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 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 pub fn search_for(options: &SearchOptions) -> Self {
309 Self::new(options.to_string())
310 }
311
312 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 pub fn format<S: Into<String>>(&mut self, format: S) -> &mut Self {
320 self.format = Some(format.into());
321 self
322 }
323
324 pub fn flat_playlist(&mut self, flat_playlist: bool) -> &mut Self {
326 self.flat_playlist = flat_playlist;
327 self
328 }
329
330 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 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 pub fn playlist_reverse(&mut self, playlist_reverse: bool) -> &mut Self {
345 self.playlist_reverse = playlist_reverse;
346 self
347 }
348
349 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 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 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 pub fn referer<S: Into<String>>(&mut self, referer: S) -> &mut Self {
369 self.referer = Some(referer.into());
370 self
371 }
372
373 pub fn all_formats(&mut self, all_formats: bool) -> &mut Self {
375 self.all_formats = all_formats;
376 self
377 }
378
379 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 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 pub fn process_timeout(&mut self, timeout: Duration) -> &mut Self {
394 self.process_timeout = Some(timeout);
395 self
396 }
397
398 pub fn extract_audio(&mut self, extract_audio: bool) -> &mut Self {
400 self.extract_audio = extract_audio;
401 self
402 }
403
404 pub fn playlist_items(&mut self, index: u32) -> &mut Self {
406 self.playlist_items = Some(index.to_string());
407 self
408 }
409
410 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 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 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 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 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 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 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 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 #[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 #[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 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 #[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 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}