extern crate hyper;
extern crate hyper_native_tls;
extern crate pbr;
extern crate regex;
extern crate json;
use pbr::ProgressBar;
use std::str;
use std::collections::HashMap;
use hyper::client::response::Response;
use hyper::Client;
use hyper::net::HttpsConnector;
use hyper_native_tls::NativeTlsClient;
use hyper::header::{ContentLength, Headers, ByteRangeSpec, Range};
use std::io::Read;
use std::io::prelude::*;
use std::fs::File;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct Rafy {
pub videoid: String,
pub title: String,
pub rating: String,
pub viewcount: u32,
pub author: String,
pub length: u32,
pub thumbdefault: String,
pub likes: u32,
pub dislikes: u32,
pub commentcount: u32,
pub description: String,
pub streams: Vec<Stream>,
pub videostreams: Vec<Stream>,
pub audiostreams: Vec<Stream>,
pub thumbmedium: String,
pub thumbhigh: String,
pub thumbstandard: String,
pub thumbmaxres: String,
pub published: String,
pub category: u32,
}
#[derive(Debug, Clone)]
pub struct Stream {
pub extension: String,
pub quality: String,
pub url: String,
}
impl Stream {
pub fn download(&self, title: &str) -> hyper::Result<()> {
let response = Rafy::send_request(&self.url)?;
let file_size = Rafy::get_file_size(&response);
let file_name = format!("{}.{}", title, &self.extension);
Self::write_file(response, &file_name, file_size);
Ok(())
}
fn write_file(mut response: Response, title: &str, file_size: u64) {
let mut pb = ProgressBar::new(file_size);
pb.format("╢▌▌░╟");
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),
};
}
}
}
#[derive(Debug)]
pub enum Error {
VideoNotFound,
NetworkRequestFailed(Box<std::error::Error>),
}
impl Rafy {
pub fn new(url: &str) -> Result<Rafy, Error> {
let key = "AIzaSyDHTKjtUchUxUOzCtYW4V_h1zzcyd0P6c0";
let url_regex = Regex::new(r"^.*(?:(?:youtu\.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*").unwrap();
let mut vid = url;
if url_regex.is_match(vid) {
let vid_split = url_regex.captures(vid).unwrap();
vid = vid_split.get(1)
.unwrap()
.as_str();
}
let url_info = format!("https://youtube.com/get_video_info?video_id={}", vid);
let api_info = format!("https://www.googleapis.com/youtube/v3/videos?id={}&part=snippet,statistics&key={}", vid, key);
let mut url_response = match Self::send_request(&url_info) {
Ok(response) => response,
Err(e) => return Err(Error::NetworkRequestFailed(Box::new(e))),
};
let mut url_response_str = String::new();
url_response.read_to_string(&mut url_response_str).unwrap();
let basic = Self::parse_url(&url_response_str);
let mut api_response = match Self::send_request(&api_info) {
Ok(response) => response,
Err(e) => return Err(Error::NetworkRequestFailed(Box::new(e))),
};
let mut api_response_str = String::new();
api_response.read_to_string(&mut api_response_str).unwrap();
let parsed_json = json::parse(&api_response_str).unwrap();
if basic["status"] != "ok" {
return Err(Error::VideoNotFound)
}
let videoid = &basic["video_id"];
let title = &basic["title"];
let rating = &basic["avg_rating"];
let viewcount = &basic["view_count"];
let author = &basic["author"];
let length = &basic["length_seconds"];
let thumbdefault = &basic["thumbnail_url"];
let likes = &parsed_json["items"][0]["statistics"]["likeCount"];
let dislikes = &parsed_json["items"][0]["statistics"]["dislikeCount"];
let commentcount = &parsed_json["items"][0]["statistics"]["commentCount"];
let description = &parsed_json["items"][0]["snippet"]["description"];
let thumbmedium = &parsed_json["items"][0]["snippet"]["thumbnails"]["medium"]["url"];
let thumbhigh = &parsed_json["items"][0]["snippet"]["thumbnails"]["high"]["url"];
let thumbstandard = &parsed_json["items"][0]["snippet"]["thumbnails"]["standard"]["url"];
let thumbmaxres = &parsed_json["items"][0]["snippet"]["thumbnails"]["maxres"]["url"];
let published = &parsed_json["items"][0]["snippet"]["publishedAt"];
let category = &parsed_json["items"][0]["snippet"]["categoryId"];
let (streams, videostreams, audiostreams) = Self::get_streams(&basic);
Ok(Rafy { videoid: videoid.to_string(),
title: title.to_string(),
rating: rating.to_string(),
viewcount: viewcount.parse::<u32>().unwrap(),
author: author.to_string(),
length: length.parse::<u32>().unwrap(),
thumbdefault: thumbdefault.to_string(),
likes: likes.to_string().parse::<u32>().unwrap(),
dislikes: dislikes.to_string().parse::<u32>().unwrap(),
commentcount: commentcount.to_string().parse::<u32>().unwrap(),
description: description.to_string(),
thumbmedium: thumbmedium.to_string(),
thumbhigh: thumbhigh.to_string(),
thumbstandard: thumbstandard.to_string(),
thumbmaxres: thumbmaxres.to_string(),
published: published.to_string(),
category: category.to_string().parse::<u32>().unwrap(),
streams: streams,
videostreams: videostreams,
audiostreams: audiostreams,
})
}
fn get_streams(basic: &HashMap<String, String>) -> (Vec<Stream>, Vec<Stream>, Vec<Stream>) {
let mut parsed_streams: Vec<Stream> = Vec::new();
let streams: Vec<&str> = basic["url_encoded_fmt_stream_map"]
.split(',')
.collect();
for url in streams.iter() {
let parsed = Self::parse_url(url);
let extension = &parsed["type"]
.split('/')
.nth(1)
.unwrap()
.split(';')
.next()
.unwrap();
let quality = &parsed["quality"];
let stream_url = &parsed["url"];
let parsed_stream = Stream {
extension: extension.to_string(),
quality: quality.to_string(),
url: stream_url.to_string(),
};
parsed_streams.push(parsed_stream);
}
let mut parsed_videostreams: Vec<Stream> = Vec::new();
let mut parsed_audiostreams: Vec<Stream> = Vec::new();
if basic.contains_key("adaptive_fmts") {
let streams: Vec<&str> = basic["adaptive_fmts"]
.split(',')
.collect();
for url in streams.iter() {
let parsed = Self::parse_url(url);
let extension = &parsed["type"]
.split('/')
.nth(1)
.unwrap()
.split(';')
.next()
.unwrap();
let stream_url = &parsed["url"];
if parsed.contains_key("quality_label") {
let quality = &parsed["quality_label"];
let parsed_videostream = Stream {
extension: extension.to_string(),
quality: quality.to_string(),
url: stream_url.to_string(),
};
parsed_videostreams.push(parsed_videostream);
} else {
let audio_extension = if extension == &"mp4" {"m4a"} else {extension};
let quality = &parsed["bitrate"];
let parsed_audiostream = Stream {
extension: audio_extension.to_string(),
quality: quality.to_string(),
url: stream_url.to_string(),
};
parsed_audiostreams.push(parsed_audiostream);
}
}
}
(parsed_streams, parsed_videostreams, parsed_audiostreams)
}
fn send_request(url: &str) -> hyper::Result<Response> {
let ssl = NativeTlsClient::new().unwrap();
let connector = HttpsConnector::new(ssl);
let client = Client::with_connector(connector);
let mut header = Headers::new();
header.set(Range::Bytes(vec![ByteRangeSpec::AllFrom(0)]));
client.get(url).headers(header).send()
}
fn parse_url(query: &str) -> HashMap<String, String> {
let url = format!("{}{}", "http://e.com?", query);
let parsed_url = hyper::Url::parse(&url).unwrap();
parsed_url.query_pairs()
.into_owned()
.collect()
}
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
}
}