├── src ├── error.rs └── lib.rs ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE └── README.md /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum YoutubeDLError { 5 | #[error("failed to execute youtube-dl")] 6 | IOError(#[from] std::io::Error), 7 | #[error("failed to convert path")] 8 | UTF8Error(#[from] std::string::FromUtf8Error), 9 | #[error("youtube-dl exited with: {0}")] 10 | Failure(String), 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | # 15 | # already existing elements were commented out 16 | 17 | /target 18 | #Cargo.lock 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ytd-rs" 3 | version = "0.1.7" 4 | authors = ["Nils Pukropp "] 5 | edition = "2018" 6 | license-file ="LICENSE" 7 | description = "A simple wrapper for youtube-dl. Youtube-dl has to be installed on the system" 8 | homepage = "https://github.com/Nirusu99/ytd-rs" 9 | repository = "https://github.com/Nirusu99/ytd-rs" 10 | readme = "README.md" 11 | 12 | [features] 13 | yt-dlp = [] 14 | youtube-dlc = [] 15 | 16 | 17 | [dependencies] 18 | regex = "1.5.5" 19 | thiserror = "1.0.30" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nils 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ytd-rs 2 | [![Build status](https://github.com/nirusu99/ytd-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/nirusu99/ytd-rs/actions) 3 | [![crates.io](https://img.shields.io/crates/v/ytd-rs.svg)](https://crates.io/crates/ytd-rs) 4 | [![docs.rs](https://docs.rs/ytd-rs/badge.svg)](https://docs.rs/ytd-rs) 5 | [![dependency status](https://deps.rs/repo/github/nirusu99/ytd-rs/status.svg)](https://deps.rs/repo/github/nirusu99/ytd-rs) 6 | 7 | This is a simple wrapper for [youtube-dl](https://youtube-dl.org/) in rust. 8 | 9 | ```rust 10 | use ytd_rs::{YoutubeDL, Arg}; 11 | use std::path::PathBuf; 12 | use std::error::Error; 13 | fn main() -> Result<(), Box> { 14 | // youtube-dl arguments quietly run process and to format the output 15 | // one doesn't take any input and is an option, the other takes the desired output format as input 16 | let args = vec![Arg::new("--quiet"), Arg::new_with_arg("--output", "%(title).90s.%(ext)s")]; 17 | let link = "https://www.youtube.com/watch?v=uTO0KnDsVH0"; 18 | let path = PathBuf::from("./path/to/download/directory"); 19 | let ytd = YoutubeDL::new(&path, args, link)?; 20 | 21 | // start download 22 | let download = ytd.download()?; 23 | 24 | // print out the download path 25 | println!("Your download: {}", download.output_dir().to_string_lossy()); 26 | Ok(()) 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /src/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> { 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 | 26 | use error::YoutubeDLError; 27 | use std::{ 28 | fmt, 29 | process::{Output, Stdio}, 30 | }; 31 | use std::{ 32 | fmt::{Display, Formatter}, 33 | fs::{canonicalize, create_dir_all}, 34 | path::PathBuf, 35 | }; 36 | use std::{path::Path, process::Command}; 37 | 38 | pub mod error; 39 | type Result = std::result::Result; 40 | 41 | const 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)] 68 | pub struct Arg { 69 | arg: String, 70 | input: Option, 71 | } 72 | 73 | impl 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 | 89 | impl 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)] 103 | pub struct YoutubeDL { 104 | path: PathBuf, 105 | links: Vec, 106 | args: Vec, 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)] 116 | pub struct YoutubeDLResult { 117 | path: PathBuf, 118 | output: String, 119 | } 120 | 121 | impl 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 | 141 | impl 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, 150 | links: Vec, 151 | ) -> Result { 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, link: &str) -> Result { 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 { 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 { 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)] 215 | mod tests { 216 | use crate::{Arg, YoutubeDL}; 217 | use regex::Regex; 218 | use std::{env, error::Error}; 219 | 220 | #[test] 221 | fn version() -> Result<(), Box> { 222 | let current_dir = env::current_dir()?; 223 | let ytd = YoutubeDL::new( 224 | ¤t_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 | } 240 | --------------------------------------------------------------------------------