Repository: smoqadam/rust-youtube-downloader Branch: master Commit: a90f38f38dce Files: 8 Total size: 12.1 KB Directory structure: gitextract_648uvzwv/ ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── src/ │ ├── lib.rs │ └── main.rs └── tests/ └── lib.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ target ================================================ FILE: .travis.yml ================================================ language: rust rust: - stable - beta - nightly matrix: allow_failures: - rust: nightly ================================================ FILE: Cargo.toml ================================================ [package] name = "youtube-downloader" version = "0.1.0" authors = ["smoqadam "] [dependencies] hyper="0.10.8" hyper-native-tls="0.2.2" pbr = "1.0.0-alpha.3" clap="2.23.2" regex="0.2.1" log = "0.3" stderrlog = "0.2" serde_derive = "1.0" serde = "1.0" serde_urlencoded = "0.5.1" url = "1.0" [dev-dependencies] reqwest = "0.8" [[bin]] name = "youtube-downloader" path = "src/main.rs" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Saeed Moqadam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ## Rust YouTube Downloader YouTube video downloader written in Rust. [![Build Status](https://travis-ci.org/smoqadam/rust-youtube-downloader.svg?branch=master)](https://travis-ci.org/smoqadam/rust-youtube-downloader) ## Installation & Usage ```bash $ cargo install youtube-downloader $ youtube-downloader [youtube video id] ``` for example: `youtube-downloader l6zpi90IT1g` ## Development ```bash $ git clone https://github.com/smoqadam/rust-youtube-downloader $ cd rust-youtube-downloader $ cargo run -- [youtube video id or youtube URL] ``` For example: `$ cargo run -- l6zpi90IT1g` ## Contributing This project is for learning purposes and may contain bugs. Please let me know if you find any by opening an issue or send a PR. ================================================ FILE: src/lib.rs ================================================ //! Parser for youtube video information as returned by //! https://youtube.com/get_video_info?video_id={} #[macro_use] extern crate serde_derive; extern crate serde; extern crate serde_urlencoded; extern crate url; #[derive(Deserialize, Debug)] pub struct Stream { pub url: String, #[serde(default = "String::new")] pub quality: String, #[serde(rename = "type")] pub stream_type: String, } impl Stream { pub fn extension(&self) -> Option<&str> { self.stream_type.split(';') .next() .and_then(|mimetype| mimetype.split('/').nth(1)) } } #[derive(Deserialize, Debug)] struct VideoInfoResponse { author: String, video_id: String, status: String, title: String, thumbnail_url: String, url_encoded_fmt_stream_map: String, view_count: usize, adaptive_fmts: Option, hlsvp: Option, } impl VideoInfoResponse { pub fn fmt_streams(&self) -> Result, serde_urlencoded::de::Error> { let mut result = Vec::new(); // this field may be empty if self.url_encoded_fmt_stream_map.is_empty() { return Ok(result); } // This field has a list of encoded stream dicts separated by commas for input in self.url_encoded_fmt_stream_map.split(',') { result.push(serde_urlencoded::from_str(input)?); } Ok(result) } pub fn adaptive_streams(&self) -> Result, serde_urlencoded::de::Error> { let mut result = Vec::new(); if let Some(ref fmts) = self.adaptive_fmts { // This field has a list of encoded stream dicts separated by commas for input in fmts.split(',') { result.push(serde_urlencoded::from_str(input)?); } } Ok(result) } } #[derive(Debug)] pub struct VideoInfo { pub author: String, pub video_id: String, pub title: String, pub thumbnail_url: String, pub streams: Vec, pub view_count: usize, pub adaptive_streams: Vec, /// Video URL for videos with HLS streams pub hlsvp: Option, } impl VideoInfo { pub fn parse(inp: &str) -> Result { let resp: VideoInfoResponse = match serde_urlencoded::from_str(inp) { Ok(r) => r, Err(original_err) => { // attempt to decode error info let error_info: ErrorInfo = match serde_urlencoded::from_str(inp) { Ok(error_info) => error_info, Err(_) => return Err(Error::from(original_err)), }; return Err(Error::from(error_info)); } }; let streams = resp.fmt_streams()?; let adaptive_streams = resp.adaptive_streams()?; Ok(VideoInfo { author: resp.author, video_id: resp.video_id, title: resp.title, thumbnail_url: resp.thumbnail_url, streams: streams, view_count: resp.view_count, adaptive_streams: adaptive_streams, hlsvp: resp.hlsvp, }) } } #[derive(Deserialize, Debug)] pub struct ErrorInfo { pub reason: String, } #[derive(Debug)] pub enum Error { JsonError(serde_urlencoded::de::Error), Youtube(ErrorInfo), Url(url::ParseError), UrlMissingVAttr, } impl From for Error { fn from(e: serde_urlencoded::de::Error) -> Self { Error::JsonError(e) } } impl From for Error { fn from(e: ErrorInfo) -> Self { Error::Youtube(e) } } impl From for Error { fn from(e: url::ParseError) -> Self { Error::Url(e) } } /// The URL to grab video information, the video_id is passed in as a query argument. /// /// See 'video_info_url()'. pub const GET_VIDEO_INFO_URL: &str = "https://youtube.com/get_video_info"; /// Build the URL to retrieve the video information from a video id pub fn video_info_url(vid: &str) -> String { let vid = url::percent_encoding::utf8_percent_encode(vid, url::percent_encoding::DEFAULT_ENCODE_SET).to_string(); format!("{}?video_id={}", GET_VIDEO_INFO_URL, vid) } /// Build the URL to retrieve the video information from a video url pub fn video_info_url_from_url(video_url: &str) -> Result { let url = url::Url::parse(video_url)?; let mut vid = None; for (name, value) in url.query_pairs() { if name == "v" { vid = Some(value); } } let vid = vid.ok_or(Error::UrlMissingVAttr)?; let vid = url::percent_encoding::utf8_percent_encode(&vid, url::percent_encoding::DEFAULT_ENCODE_SET).to_string(); Ok(format!("{}?video_id={}", GET_VIDEO_INFO_URL, vid)) } ================================================ FILE: src/main.rs ================================================ extern crate hyper; extern crate hyper_native_tls; extern crate pbr; extern crate clap; extern crate regex; extern crate stderrlog; #[macro_use] extern crate log; extern crate youtube_downloader; use pbr::ProgressBar; use std::{process,str}; use hyper::client::response::Response; use hyper::Client; use hyper::net::HttpsConnector; use hyper_native_tls::NativeTlsClient; use hyper::header::ContentLength; use std::io::Read; use std::io::prelude::*; use std::fs::File; use clap::{Arg, App}; use regex::Regex; use youtube_downloader::VideoInfo; fn main() { //Regex for youtube URLs. let url_regex = Regex::new(r"^.*(?:(?:youtu\.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*").unwrap(); let args = App::new("youtube-downloader") .version("0.1.0") .arg(Arg::with_name("verbose") .help("Increase verbosity") .short("v") .multiple(true) .long("verbose")) .arg(Arg::with_name("adaptive") .help("List adaptive streams, instead of video streams") .short("A") .long("adaptive")) .arg(Arg::with_name("video-id") .help("The ID of the video to download.") .required(true) .index(1)) .get_matches(); stderrlog::new() .module(module_path!()) .verbosity(args.occurrences_of("verbose") as usize) .init() .expect("Unable to initialize stderr output"); let mut vid = args.value_of("video-id").unwrap(); if url_regex.is_match(vid) { let vid_split = url_regex.captures(vid).unwrap(); vid = vid_split.get(1).unwrap().as_str(); } let url = format!("https://youtube.com/get_video_info?video_id={}", vid); download(&url, args.is_present("adaptive")); } fn download(url: &str, adaptive: bool) { debug!("Fetching video info from {}", url); let mut response = send_request(url); let mut response_str = String::new(); response.read_to_string(&mut response_str).unwrap(); trace!("Response {}", response_str); let info = VideoInfo::parse(&response_str).unwrap(); debug!("Video info {:#?}", info); let streams = if adaptive { info.adaptive_streams } else { info.streams }; for (i, stream) in streams.iter().enumerate() { println!("{}- {} {}", i, stream.quality, stream.stream_type); } println!("Choose quality (0): "); let input = read_line().trim().parse().unwrap_or(0); println!("Please wait..."); if let Some(ref stream) = streams.get(input) { // get response from selected quality debug!("Downloading {}", url); let response = send_request(&stream.url); println!("Download is starting..."); // get file size from Content-Length header let file_size = get_file_size(&response); let filename = match stream.extension() { Some(ext) => format!("{}.{}", info.title, ext), None => info.title, }; // write file to disk write_file(response, &filename, file_size); } else { error!("Invalid stream index"); } } // get file size from Content-Length header fn get_file_size(response: &Response) -> u64 { let mut file_size = 0; match response.headers.get::(){ Some(length) => file_size = length.0, None => println!("Content-Length header missing"), }; file_size } fn write_file(mut response: Response, title: &str, file_size: u64) { // initialize progressbar let mut pb = ProgressBar::new(file_size); pb.format("╢▌▌░╟"); // Download and write to file let mut buf = [0; 128 * 1024]; let mut file = File::create(title).unwrap(); loop { match response.read(&mut buf) { Ok(len) => { file.write_all(&buf[..len]).unwrap(); pb.add(len as u64); if len == 0 { break; } len } Err(why) => panic!("{}", why), }; } } fn send_request(url: &str) -> Response { let ssl = NativeTlsClient::new().unwrap(); let connector = HttpsConnector::new(ssl); let client = Client::with_connector(connector); client.get(url).send().unwrap_or_else(|e| { error!("Network request failed: {}", e); process::exit(1); }) } fn read_line() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("Could not read stdin!"); input } ================================================ FILE: tests/lib.rs ================================================ extern crate youtube_downloader; extern crate reqwest; use std::io::Read; use youtube_downloader::{video_info_url_from_url, VideoInfo}; fn get_video_info(url: &str) -> VideoInfo { let info_url = youtube_downloader::video_info_url_from_url(url).unwrap(); let mut resp = reqwest::get(&info_url).unwrap(); let mut data = String::new(); resp.read_to_string(&mut data).unwrap(); youtube_downloader::VideoInfo::parse(&data).unwrap() } #[test] fn live_video() { get_video_info("https://www.youtube.com/watch?v=XOacA3RYrXk"); } #[test] fn video() { get_video_info("https://www.youtube.com/watch?v=aqz-KE-bpKQ"); }