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 <phpro.ir@gmail.com>"]
[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.
[](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<String>,
hlsvp: Option<String>,
}
impl VideoInfoResponse {
pub fn fmt_streams(&self) -> Result<Vec<Stream>, 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<Vec<Stream>, 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<Stream>,
pub view_count: usize,
pub adaptive_streams: Vec<Stream>,
/// Video URL for videos with HLS streams
pub hlsvp: Option<String>,
}
impl VideoInfo {
pub fn parse(inp: &str) -> Result<VideoInfo, Error> {
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<serde_urlencoded::de::Error> for Error {
fn from(e: serde_urlencoded::de::Error) -> Self {
Error::JsonError(e)
}
}
impl From<ErrorInfo> for Error {
fn from(e: ErrorInfo) -> Self {
Error::Youtube(e)
}
}
impl From<url::ParseError> 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<String, Error> {
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::<ContentLength>(){
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");
}
gitextract_648uvzwv/
├── .gitignore
├── .travis.yml
├── Cargo.toml
├── LICENSE
├── README.md
├── src/
│ ├── lib.rs
│ └── main.rs
└── tests/
└── lib.rs
SYMBOL INDEX (24 symbols across 3 files)
FILE: src/lib.rs
type Stream (line 11) | pub struct Stream {
method extension (line 20) | pub fn extension(&self) -> Option<&str> {
type VideoInfoResponse (line 28) | struct VideoInfoResponse {
method fmt_streams (line 41) | pub fn fmt_streams(&self) -> Result<Vec<Stream>, serde_urlencoded::de:...
method adaptive_streams (line 56) | pub fn adaptive_streams(&self) -> Result<Vec<Stream>, serde_urlencoded...
type VideoInfo (line 69) | pub struct VideoInfo {
method parse (line 82) | pub fn parse(inp: &str) -> Result<VideoInfo, Error> {
type ErrorInfo (line 110) | pub struct ErrorInfo {
type Error (line 115) | pub enum Error {
method from (line 123) | fn from(e: serde_urlencoded::de::Error) -> Self {
method from (line 129) | fn from(e: ErrorInfo) -> Self {
method from (line 135) | fn from(e: url::ParseError) -> Self {
constant GET_VIDEO_INFO_URL (line 143) | pub const GET_VIDEO_INFO_URL: &str = "https://youtube.com/get_video_info";
function video_info_url (line 146) | pub fn video_info_url(vid: &str) -> String {
function video_info_url_from_url (line 152) | pub fn video_info_url_from_url(video_url: &str) -> Result<String, Error> {
FILE: src/main.rs
function main (line 25) | fn main() {
function download (line 60) | fn download(url: &str, adaptive: bool) {
function get_file_size (line 109) | fn get_file_size(response: &Response) -> u64 {
function write_file (line 118) | fn write_file(mut response: Response, title: &str, file_size: u64) {
function send_request (line 141) | fn send_request(url: &str) -> Response {
function read_line (line 151) | fn read_line() -> String {
FILE: tests/lib.rs
function get_video_info (line 8) | fn get_video_info(url: &str) -> VideoInfo {
function live_video (line 17) | fn live_video() {
function video (line 22) | fn video() {
Condensed preview — 8 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (13K chars).
[
{
"path": ".gitignore",
"chars": 7,
"preview": "target\n"
},
{
"path": ".travis.yml",
"chars": 99,
"preview": "language: rust\nrust:\n - stable\n - beta\n - nightly\nmatrix:\n allow_failures:\n - rust: nightly\n"
},
{
"path": "Cargo.toml",
"chars": 404,
"preview": "[package]\nname = \"youtube-downloader\"\nversion = \"0.1.0\"\nauthors = [\"smoqadam <phpro.ir@gmail.com>\"]\n\n[dependencies]\nhype"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2018 Saeed Moqadam\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 745,
"preview": "## Rust YouTube Downloader\n\nYouTube video downloader written in Rust.\n\n[. The extraction includes 8 files (12.1 KB), approximately 3.3k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.