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