feat: add ChannelRss

- add documentation
- small model refactor: rename player VideoPlayerDetails.thumbnails to thumbnail
This commit is contained in:
ThetaDev 2022-09-27 15:23:09 +02:00
parent 6ac5bc3782
commit 305c3ee70e
29 changed files with 2222 additions and 118 deletions

View file

@ -1,3 +1,5 @@
//! Persistent cache storage
use std::{
fs,
path::{Path, PathBuf},

View file

@ -1,5 +1,4 @@
use anyhow::{anyhow, bail, Result};
use reqwest::Method;
use serde::Serialize;
use url::Url;
@ -68,7 +67,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"channel_videos",
channel_id,
Method::POST,
"browse",
&request_body,
)
@ -89,7 +87,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"channel_videos_continuation",
ctoken,
Method::POST,
"browse",
&request_body,
)
@ -111,7 +108,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"channel_playlists",
channel_id,
Method::POST,
"browse",
&request_body,
)
@ -132,7 +128,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"channel_videos_continuation",
ctoken,
Method::POST,
"browse",
&request_body,
)
@ -151,7 +146,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"channel_info",
channel_id,
Method::POST,
"browse",
&request_body,
)

92
src/client/channel_rss.rs Normal file
View file

@ -0,0 +1,92 @@
use std::collections::BTreeMap;
use anyhow::Result;
use crate::{model::ChannelRss, report::Report};
use super::{response, RustyPipeQuery};
impl RustyPipeQuery {
pub async fn channel_rss(&self, channel_id: &str) -> Result<ChannelRss> {
let url = format!(
"https://www.youtube.com/feeds/videos.xml?channel_id={}",
channel_id
);
let xml = self
.client
.http_request_txt(self.client.inner.http.get(&url).build()?)
.await?;
match quick_xml::de::from_str::<response::ChannelRss>(&xml) {
Ok(feed) => Ok(feed.into()),
Err(e) => {
if let Some(reporter) = &self.client.inner.reporter {
let report = Report {
info: Default::default(),
level: crate::report::Level::ERR,
operation: "channel_rss".to_owned(),
error: Some(e.to_string()),
msgs: Vec::new(),
deobf_data: None,
http_request: crate::report::HTTPRequest {
url: url,
method: "GET".to_owned(),
req_header: BTreeMap::new(),
req_body: String::new(),
status: 200,
resp_body: xml,
},
};
reporter.report(&report);
}
Err(e.into())
}
}
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use chrono::{Datelike, Timelike};
use crate::{
client::{response, RustyPipe},
model::ChannelRss,
};
#[tokio::test]
async fn get_channel_rss() {
let rp = RustyPipe::builder().strict().build();
let channel = rp
.query()
.channel_rss("UCHnyfMqiRRG1u-2MsSQLbXA")
.await
.unwrap();
assert_eq!(channel.id, "UCHnyfMqiRRG1u-2MsSQLbXA");
assert_eq!(channel.name, "Veritasium");
assert_eq!(channel.create_date.year(), 2010);
assert_eq!(channel.create_date.month(), 7);
assert_eq!(channel.create_date.day(), 21);
assert_eq!(channel.create_date.hour(), 7);
assert_eq!(channel.create_date.minute(), 18);
assert!(!channel.videos.is_empty());
}
#[test]
fn map_channel_rss() {
let xml_path = Path::new("testfiles/channel_rss/base.xml");
let xml_file = File::open(xml_path).unwrap();
let feed: response::ChannelRss =
quick_xml::de::from_reader(BufReader::new(xml_file)).unwrap();
let map_res: ChannelRss = feed.into();
insta::assert_ron_snapshot!("map_channel_rss", map_res);
}
}

View file

@ -1,3 +1,5 @@
//! YouTube API Client
mod channel;
mod pagination;
mod player;
@ -5,6 +7,9 @@ mod playlist;
mod response;
mod video_details;
#[cfg(feature = "rss")]
mod channel_rss;
use std::fmt::Debug;
use std::sync::Arc;
@ -14,7 +19,7 @@ use fancy_regex::Regex;
use log::{error, warn};
use once_cell::sync::Lazy;
use rand::Rng;
use reqwest::{header, Client, ClientBuilder, Method, Request, RequestBuilder, Response};
use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::sync::Mutex;
@ -787,24 +792,16 @@ impl RustyPipeQuery {
/// - `ctype`: Client type (`Desktop`, `DesktopMusic`, `Android`, ...)
/// - `method`: HTTP method
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
async fn request_builder(
&self,
ctype: ClientType,
method: Method,
endpoint: &str,
) -> RequestBuilder {
async fn request_builder(&self, ctype: ClientType, endpoint: &str) -> RequestBuilder {
match ctype {
ClientType::Desktop => self
.client
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
),
)
.post(format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
))
.header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com")
.header(header::COOKIE, self.client.inner.consent_cookie.to_owned())
@ -817,16 +814,13 @@ impl RustyPipeQuery {
.client
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBE_MUSIC_V1_URL,
endpoint,
DESKTOP_MUSIC_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
),
)
.post(format!(
"{}{}?key={}{}",
YOUTUBE_MUSIC_V1_URL,
endpoint,
DESKTOP_MUSIC_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
))
.header(header::ORIGIN, "https://music.youtube.com")
.header(header::REFERER, "https://music.youtube.com")
.header(header::COOKIE, self.client.inner.consent_cookie.to_owned())
@ -839,13 +833,10 @@ impl RustyPipeQuery {
.client
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
),
)
.post(format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
))
.header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com")
.header("X-YouTube-Client-Name", "1")
@ -854,16 +845,13 @@ impl RustyPipeQuery {
.client
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL,
endpoint,
ANDROID_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
),
)
.post(format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL,
endpoint,
ANDROID_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
))
.header(
header::USER_AGENT,
format!(
@ -876,16 +864,10 @@ impl RustyPipeQuery {
.client
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL,
endpoint,
IOS_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
),
)
.post(format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL, endpoint, IOS_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
))
.header(
header::USER_AGENT,
format!(
@ -911,7 +893,6 @@ impl RustyPipeQuery {
/// - `endpoint`: YouTube API endpoint (`https://www.youtube.com/youtubei/v1/<XYZ>?key=...`)
/// - `body`: Serializable request body to be sent in json format
/// - `deobf`: Deobfuscator (is passed to the mapper to deobfuscate stream URLs).
#[allow(clippy::too_many_arguments)]
async fn execute_request_deobf<
R: DeserializeOwned + MapResponse<M> + Debug,
M,
@ -921,13 +902,12 @@ impl RustyPipeQuery {
ctype: ClientType,
operation: &str,
id: &str,
method: Method,
endpoint: &str,
body: &B,
deobf: Option<&Deobfuscator>,
) -> Result<M> {
let request = self
.request_builder(ctype, method.clone(), endpoint)
.request_builder(ctype, endpoint)
.await
.json(body)
.build()?;
@ -943,9 +923,7 @@ impl RustyPipeQuery {
let create_report = |level: Level, error: Option<String>, msgs: Vec<String>| {
if let Some(reporter) = &self.client.inner.reporter {
let report = Report {
package: "rustypipe".to_owned(),
version: "0.1.0".to_owned(),
date: chrono::Local::now(),
info: Default::default(),
level,
operation: format!("{}({})", operation, id),
error,
@ -953,7 +931,7 @@ impl RustyPipeQuery {
deobf_data: deobf.map(Deobfuscator::get_data),
http_request: crate::report::HTTPRequest {
url: request_url,
method: method.to_string(),
method: "POST".to_string(),
req_header: request_headers
.iter()
.map(|(k, v)| {
@ -1030,11 +1008,10 @@ impl RustyPipeQuery {
ctype: ClientType,
operation: &str,
id: &str,
method: Method,
endpoint: &str,
body: &B,
) -> Result<M> {
self.execute_request_deobf::<R, M, B>(ctype, operation, id, method, endpoint, body, None)
self.execute_request_deobf::<R, M, B>(ctype, operation, id, endpoint, body, None)
.await
}
}

View file

@ -7,7 +7,6 @@ use anyhow::{anyhow, bail, Result};
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
use fancy_regex::Regex;
use once_cell::sync::Lazy;
use reqwest::Method;
use serde::Serialize;
use url::Url;
@ -98,7 +97,6 @@ impl RustyPipeQuery {
client_type,
"player",
video_id,
Method::POST,
"player",
&request_body,
Some(&deobf),
@ -175,7 +173,7 @@ impl MapResponse<VideoPlayer> for response::Player {
title: video_details.title,
description: video_details.short_description,
length: video_details.length_seconds,
thumbnails: video_details.thumbnail.into(),
thumbnail: video_details.thumbnail.into(),
channel: ChannelId {
id: video_details.channel_id,
name: video_details.author,
@ -439,7 +437,7 @@ fn map_audio_stream(
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<AudioStream>> {
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2})\."#).unwrap());
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2,3})\."#).unwrap());
let (mtype, codecs) = some_or_bail!(
parse_mime(&f.mime_type),
@ -631,7 +629,7 @@ mod tests {
#[case::android(ClientType::Android)]
#[case::ios(ClientType::Ios)]
#[test_log::test(tokio::test)]
async fn t_get_player(#[case] client_type: ClientType) {
async fn get_player(#[case] client_type: ClientType) {
let rp = RustyPipe::builder().strict().build();
let player_data = rp.query().player("n4tK7LYFxI0", client_type).await.unwrap();
@ -647,7 +645,7 @@ mod tests {
));
}
assert_eq!(player_data.details.length, 259);
assert!(!player_data.details.thumbnails.is_empty());
assert!(!player_data.details.thumbnail.is_empty());
assert_eq!(player_data.details.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
assert_eq!(player_data.details.channel.name, "NoCopyrightSounds");
assert!(player_data.details.view_count > 146818808);
@ -733,6 +731,18 @@ mod tests {
assert!(player_data.expires_in_seconds > 10000);
}
#[tokio::test]
async fn tmp() {
let rp = RustyPipe::builder().strict().build();
let player_data = rp
.query()
.player("tVWWp1PqDus", ClientType::Desktop)
.await
.unwrap();
dbg!(&player_data);
}
#[test]
fn t_cipher_to_url() {
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";

View file

@ -1,7 +1,6 @@
use std::convert::TryFrom;
use anyhow::{anyhow, bail, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{
@ -34,7 +33,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"playlist",
playlist_id,
Method::POST,
"browse",
&request_body,
)
@ -52,7 +50,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"get_playlist_continuation",
ctoken,
Method::POST,
"browse",
&request_body,
)

View file

@ -0,0 +1,81 @@
use chrono::{DateTime, Utc};
use serde::Deserialize;
use super::Thumbnail;
#[derive(Clone, Debug, Deserialize)]
pub struct ChannelRss {
#[serde(rename = "$unflatten=yt:channelId")]
pub channel_id: String,
#[serde(rename = "$unflatten=title")]
pub title: String,
#[serde(rename = "$unflatten=published")]
pub create_date: DateTime<Utc>,
pub entry: Vec<Entry>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Entry {
#[serde(rename = "$unflatten=yt:videoId")]
pub video_id: String,
#[serde(rename = "$unflatten=title")]
pub title: String,
#[serde(rename = "$unflatten=published")]
pub published: DateTime<Utc>,
#[serde(rename = "$unflatten=updated")]
pub updated: DateTime<Utc>,
#[serde(rename = "$unflatten=media:group")]
pub media_group: MediaGroup,
}
#[derive(Clone, Debug, Deserialize)]
pub struct MediaGroup {
#[serde(rename = "$unflatten=media:thumbnail")]
pub thumbnail: Thumbnail,
#[serde(rename = "$unflatten=media:description")]
pub description: String,
#[serde(rename = "$unflatten=media:community")]
pub community: Community,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Community {
#[serde(rename = "$unflatten=media:starRating")]
pub rating: Rating,
#[serde(rename = "$unflatten=media:statistics")]
pub statistics: Statistics,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Rating {
pub count: u32,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Statistics {
pub views: u64,
}
impl From<ChannelRss> for crate::model::ChannelRss {
fn from(feed: ChannelRss) -> Self {
Self {
id: feed.channel_id,
name: feed.title,
videos: feed
.entry
.into_iter()
.map(|item| crate::model::ChannelRssVideo {
id: item.video_id,
title: item.title,
description: item.media_group.description,
thumbnail: item.media_group.thumbnail.into(),
publish_date: item.published,
update_date: item.updated,
view_count: item.media_group.community.statistics.views,
like_count: item.media_group.community.rating.count,
})
.collect(),
create_date: feed.create_date,
}
}
}

View file

@ -14,6 +14,11 @@ pub use video_details::VideoComments;
pub use video_details::VideoDetails;
pub use video_details::VideoRecommendations;
#[cfg(feature = "rss")]
pub mod channel_rss;
#[cfg(feature = "rss")]
pub use channel_rss::ChannelRss;
use serde::Deserialize;
use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError};

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@ VideoPlayer(
title: "Inspiring Cinematic Uplifting (Creative Commons)",
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
length: 163,
thumbnails: [
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/default.webp",
width: 120,

View file

@ -8,7 +8,7 @@ VideoPlayer(
title: "Inspiring Cinematic Uplifting (Creative Commons)",
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
length: 163,
thumbnails: [
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBSNHImLtGal2a95M5oyTT_uuTZlw",
width: 168,

View file

@ -8,7 +8,7 @@ VideoPlayer(
title: "Inspiring Cinematic Uplifting",
description: None,
length: 163,
thumbnails: [
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AOn4CLC-0nIQMyPuy8CtzqTMl6z1rmG_XQ",
width: 400,

View file

@ -8,7 +8,7 @@ VideoPlayer(
title: "Inspiring Cinematic Uplifting (Creative Commons)",
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
length: 163,
thumbnails: [
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/mqdefault.jpg",
width: 320,

View file

@ -8,7 +8,7 @@ VideoPlayer(
title: "Inspiring Cinematic Uplifting (Creative Commons)",
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
length: 163,
thumbnails: [
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/default.jpg",
width: 120,

View file

@ -1,7 +1,6 @@
use std::convert::TryFrom;
use anyhow::{anyhow, bail, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{
@ -44,7 +43,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"video_details",
video_id,
Method::POST,
"next",
&request_body,
)
@ -62,7 +60,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"video_recommendations",
ctoken,
Method::POST,
"next",
&request_body,
)
@ -80,7 +77,6 @@ impl RustyPipeQuery {
ClientType::Desktop,
"video_comments",
ctoken,
Method::POST,
"next",
&request_body,
)

View file

@ -7,7 +7,7 @@ use crate::{
/// The dictionary contains the information required to parse dates and numbers
/// in all supported languages.
pub struct Entry {
pub(crate) struct Entry {
/// Should the language be parsed by character instead of by word?
/// (e.g. Chinese/Japanese)
pub by_char: bool,
@ -43,7 +43,7 @@ pub struct Entry {
}
#[rustfmt::skip]
pub fn entry(lang: Language) -> Entry {
pub(crate) fn entry(lang: Language) -> Entry {
match lang {
Language::Af => Entry {
by_char: false,

View file

@ -1,3 +1,5 @@
//! YouTube audio/video downloader
use std::{cmp::Ordering, ffi::OsString, ops::Range, path::PathBuf};
use anyhow::{anyhow, bail, Result};

View file

@ -1,3 +1,8 @@
//! # RustyPipe
//!
//! Client for the public YouTube / YouTube Music API (Innertube),
//! inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
#![allow(dead_code)]
#![warn(clippy::todo)]

View file

@ -1,3 +1,5 @@
//! YouTube API request and response models
pub mod locale;
mod ordering;
mod paginator;
@ -11,85 +13,148 @@ pub use param::ChannelOrder;
use std::ops::Range;
use chrono::{DateTime, Local};
use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize};
use self::richtext::RichText;
/*
#COMMON
*/
/// Video thumbnail or other image
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Thumbnail {
pub url: String,
pub width: u32,
pub height: u32,
}
/*
#PLAYER
*/
pub trait FileFormat {
/// Get the file extension (".xyz") of the file format
fn extension(&self) -> &str;
}
/// Video player data
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VideoPlayer {
/// Video metadata
pub details: VideoPlayerDetails,
/// List of streams containing both audio and video
pub video_streams: Vec<VideoStream>,
/// List of streams containing video only
pub video_only_streams: Vec<VideoStream>,
/// List of streams containing audio only
pub audio_streams: Vec<AudioStream>,
/// List of subtitles
pub subtitles: Vec<Subtitle>,
/// Lifetime of the stream URLs in seconds
pub expires_in_seconds: u32,
}
/// Video metadata from the player
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoPlayerDetails {
/// Unique YouTube video ID
pub id: String,
/// Video title
pub title: String,
/// Video description in plaintext format
pub description: Option<String>,
/// Video length in seconds
pub length: u32,
pub thumbnails: Vec<Thumbnail>,
/// Video thumbnail
pub thumbnail: Vec<Thumbnail>,
/// Channel of the video
pub channel: ChannelId,
/// Video publishing date. Start date in case of a livestream.
pub publish_date: Option<DateTime<Local>>,
/// Number of views / current viewers in case of a livestream.
pub view_count: u64,
/// List of words that describe the topic of the video
pub keywords: Vec<String>,
pub category: Option<String>,
/// True if the video is/was livestreamed
pub is_live_content: bool,
/// True if the video is not age-restricted
pub is_family_safe: Option<bool>,
}
/// Video stream
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoStream {
/// Video stream URL
pub url: String,
/// YouTube stream format identifier
pub itag: u32,
pub bitrate: u32,
pub average_bitrate: u32,
/// Video file size in bytes
pub size: Option<u64>,
pub index_range: Option<Range<u32>>,
pub init_range: Option<Range<u32>>,
/// Video width in pixels
pub width: u32,
/// Video height in pixels
pub height: u32,
/// Video frames per second
pub fps: u8,
/// Quality text (e.g. "1080p60")
pub quality: String,
/// True if the video is HDR
pub hdr: bool,
/// MIME file type
pub mime: String,
/// Video file format
pub format: VideoFormat,
/// Video codec
pub codec: VideoCodec,
/// True if the deobfuscation of the nsig url parameter failed
/// and the stream will be throttled
pub throttled: bool,
}
/// Audio stream
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct AudioStream {
/// Audio stream URL
pub url: String,
/// YouTube stream format identifier
pub itag: u32,
pub bitrate: u32,
pub average_bitrate: u32,
/// Audio file size in bytes
pub size: u64,
pub index_range: Option<Range<u32>>,
pub init_range: Option<Range<u32>>,
/// MIME file type
pub mime: String,
/// Audio file format
pub format: AudioFormat,
/// Audio codec
pub codec: AudioCodec,
/// True if the deobfuscation of the nsig url parameter failed
/// and the stream will be throttled
pub throttled: bool,
/// Audio track information
///
/// Videos can have multiple audio tracks (different languages).
/// In this case, this object shows to which track the stream belongs to.
///
/// This is None if the video contains only 1 audio track.
pub track: Option<AudioTrack>,
}
/// Video codec
#[derive(
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
@ -108,6 +173,7 @@ pub enum VideoCodec {
Av01,
}
/// Audio codec
#[derive(
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
@ -122,23 +188,36 @@ pub enum AudioCodec {
Opus,
}
/// The video file format
/// Video file type
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum VideoFormat {
/// `*.3gp`
#[serde(rename = "3gp")]
ThreeGp,
/// `*.mp4`
Mp4,
/// `*.webm`
Webm,
}
/// Audio track information
///
/// Videos can have multiple audio tracks (different languages).
/// In this case, this object shows to which track the stream belongs to.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct AudioTrack {
/// Track ID (e.g. `en.0`)
pub id: String,
/// 2/3 letter language code (e.g. `en`)
///
/// Extracted from the track ID
pub lang: Option<String>,
/// Language name (e.g. "English")
pub lang_name: String,
/// True if this is the default audio track
pub is_default: bool,
}
@ -152,11 +231,14 @@ impl FileFormat for VideoFormat {
}
}
/// Audio file type
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum AudioFormat {
/// `*.m4a`
M4a,
/// `*.webm`
Webm,
}
@ -169,20 +251,128 @@ impl FileFormat for AudioFormat {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Thumbnail {
pub url: String,
pub width: u32,
pub height: u32,
}
/// YouTube provides subtitles in different formats.
///
/// srv1 (XML) is the default format, to request a different format you have
/// to append `&fmt=<Format>` to the URL.
///
/// # Subtitle formats
///
/// ### `srv1` (default)
///
/// ```xml
/// <?xml version="1.0" encoding="utf-8"?>
/// <transcript>
/// <text start="0.12" dur="1.59">- [Mr Beast] I built two massive circles</text>
/// <text start="1.71" dur="3.39">and put 100 boys in one
/// and 100 girls in the other</text>
/// </transcript>
/// ```
///
/// ### `srv2`
///
/// ```xml
/// <?xml version="1.0" encoding="utf-8"?>
/// <timedtext>
/// <text t="120" d="1590">- [Mr Beast] I built two massive circles</text>
/// <text t="1710" d="3390">and put 100 boys in one
/// and 100 girls in the other</text>
/// </timedtext>
/// ```
///
/// ### `srv3`
///
/// ```xml
/// <?xml version="1.0" encoding="utf-8"?>
/// <timedtext format="3">
/// <body>
/// <p t="120" d="1590">- [Mr Beast] I built two massive circles</p>
/// <p t="1710" d="3390">and put 100 boys in one
/// and 100 girls in the other</p>
/// </body>
/// </timedtext>
/// ```
///
/// ### `json3`
///
/// ```json
/// {
/// "wireMagic": "pb3",
/// "pens": [{}],
/// "wsWinStyles": [{}],
/// "wpWinPositions": [{}],
/// "events": [
/// {
/// "tStartMs": 120,
/// "dDurationMs": 1590,
/// "segs": [
/// {
/// "utf8": "- [Mr Beast] I built two massive circles"
/// }
/// ]
/// },
/// {
/// "tStartMs": 1710,
/// "dDurationMs": 3390,
/// "segs": [
/// {
/// "utf8": "and put 100 boys in one\nand 100 girls in the other"
/// }
/// ]
/// }
/// ]
/// }
/// ```
///
/// ### Timed Text Markup Language (`ttml`)
///
/// ```xml
/// <?xml version="1.0" encoding="utf-8" ?>
/// <tt xml:lang="en-US" xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:ttp="http://www.w3.org/ns/ttml#parameter" ttp:profile="http://www.w3.org/TR/profile/sdp-us" >
/// <head>
/// <styling>
/// <style xml:id="s1" tts:textAlign="center" tts:extent="90% 90%" tts:origin="5% 5%" tts:displayAlign="after"/>
/// <style xml:id="s2" tts:fontSize=".72c" tts:backgroundColor="black" tts:color="white"/>
/// </styling>
/// <layout>
/// <region xml:id="r1" style="s1"/>
/// </layout>
/// </head>
/// <body region="r1">
/// <div>
/// <p begin="00:00:00.120" end="00:00:01.710" style="s2">- [Mr Beast] I built two massive circles</p>
/// <p begin="00:00:01.710" end="00:00:05.100" style="s2">and put 100 boys in one<br />and 100 girls in the other</p>
/// </div>
/// </body>
/// </tt>
/// ```
///
/// ### WebVTT (`vtt`)
///
/// ```txt
/// WEBVTT
/// Kind: captions
/// Language: en-US
///
/// 00:00:00.120 --> 00:00:01.710
/// - [Mr Beast] I built two massive circles
///
/// 00:00:01.710 --> 00:00:05.100
/// and put 100 boys in one
/// and 100 girls in the other
/// ```
///
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Subtitle {
/// URL of the subtitle file
pub url: String,
/// Subtitle language code (e.g. "en")
pub lang: String,
/// Subtitle language name (e.g. "English")
pub lang_name: String,
/// True if the subtitle was automatically generated
/// by YouTube's speech recognition
pub auto_generated: bool,
}
@ -190,34 +380,53 @@ pub struct Subtitle {
#PLAYLIST
*/
/// YouTube playlist
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Playlist {
/// Unique YouTube playlist ID
pub id: String,
/// Playlist name
pub name: String,
/// Playlist videos
pub videos: Paginator<PlaylistVideo>,
/// Number of videos in the playlist
pub video_count: u32,
/// Playlist thumbnail
pub thumbnail: Vec<Thumbnail>,
/// Playlist description in plaintext format
pub description: Option<String>,
/// Channel of the playlist
pub channel: Option<ChannelId>,
/// Last update date
pub last_update: Option<DateTime<Local>>,
/// Textual last update date
pub last_update_txt: Option<String>,
}
/// YouTube video extracted from a playlist
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct PlaylistVideo {
/// Unique YouTube video ID
pub id: String,
/// Video title
pub title: String,
/// Video length in seconds
pub length: u32,
/// Video thumbnail
pub thumbnail: Vec<Thumbnail>,
/// Channel of the video
pub channel: ChannelId,
}
/// Channel identifier
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelId {
/// Unique YouTube channel ID
pub id: String,
/// Channel name
pub name: String,
}
@ -225,6 +434,8 @@ pub struct ChannelId {
#VIDEO DETAILS
*/
/// VideoDetails contains additional information that YouTube shows next
/// to the video player.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoDetails {
@ -232,7 +443,7 @@ pub struct VideoDetails {
pub id: String,
/// Video title
pub title: String,
/// Video description
/// Video description in rich text format
pub description: RichText,
/// Channel of the video
pub channel: ChannelTag,
@ -265,11 +476,17 @@ pub struct VideoDetails {
/// Note: Recommendations are not available for age-restricted videos
pub recommended: Paginator<RecommendedVideo>,
/// Paginator to fetch comments (most liked first)
///
/// Is initially empty.
pub top_comments: Paginator<Comment>,
/// Paginator to fetch comments (latest first)
///
/// Is initially empty.
pub latest_comments: Paginator<Comment>,
}
/// Chapter of a video
///
/// Videos can consist of different chapters, which YouTube shows
/// on the seek bar and below the description text.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -287,6 +504,7 @@ pub struct Chapter {
@RECOMMENDATIONS
*/
/// YouTube video fetched from the recommendations next to a video
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct RecommendedVideo {
@ -318,6 +536,7 @@ pub struct RecommendedVideo {
pub is_live: bool,
}
/// Channel information attached to a video or comment
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelTag {
@ -341,6 +560,7 @@ pub struct ChannelTag {
@COMMENTS
*/
/// Verification status of a channel
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
@ -355,11 +575,14 @@ pub enum Verification {
}
impl Verification {
/// Returns true if the verification status is not None
/// (either Verified or Artist).
pub fn verified(&self) -> bool {
self != &Self::None
}
}
/// Comment under a YouTube video
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Comment {
@ -395,6 +618,10 @@ pub struct Comment {
#CHANNEL
*/
/// YouTube channel object.
///
/// Contains channel metadata as well as additional content
/// depending on which channel tab is fetched.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Channel<T> {
@ -426,6 +653,7 @@ pub struct Channel<T> {
pub content: T,
}
/// Video fetched from a YouTube channel
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelVideo {
@ -457,6 +685,7 @@ pub struct ChannelVideo {
pub is_short: bool,
}
/// Playlist fetched from a YouTube channel
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelPlaylist {
@ -470,6 +699,7 @@ pub struct ChannelPlaylist {
pub video_count: Option<u32>,
}
/// Additional channel metadata fetched from the "About" tab.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelInfo {
@ -480,3 +710,41 @@ pub struct ChannelInfo {
/// Links to other websites or social media profiles
pub links: Vec<(String, String)>,
}
/// YouTube channel RSS feed
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelRss {
/// Unique YouTube Channel-ID (e.g. `UC-lHJZR3Gqxm24_Vd_AJ5Yw`)
pub id: String,
/// Channel name
pub name: String,
/// List of the latest channel videos
pub videos: Vec<ChannelRssVideo>,
/// Channel creation date (second-accurate).
pub create_date: DateTime<Utc>,
}
/// YouTube video fetched from a channel's RSS feed
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ChannelRssVideo {
/// Unique YouTube video ID
pub id: String,
/// Video title
pub title: String,
/// Video description in plaintext format
pub description: String,
/// Video thumbnail
pub thumbnail: Thumbnail,
/// Video publishing date (second-accurate).
pub publish_date: DateTime<Utc>,
/// Date and time when the RSS feed entry was last updated.
pub update_date: DateTime<Utc>,
/// View count
pub view_count: u64,
/// Number of likes
///
/// Zero if the like count was hidden by the creator.
pub like_count: u32,
}

View file

@ -2,6 +2,8 @@ use std::convert::TryInto;
use serde::{Deserialize, Serialize};
/// Wrapper around progressively fetched items
///
/// The paginator is a wrapper around a list of items that are fetched
/// in pages from the YouTube API (e.g. playlist items,
/// video recommendations or comments).

View file

@ -1,3 +1,5 @@
//! Error reporting
use std::{
collections::BTreeMap,
fs::File,
@ -12,13 +14,9 @@ use serde::{Deserialize, Serialize};
use crate::deobfuscate::DeobfData;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Report {
/// Rust package name (`rustypipe`)
pub package: String,
/// Package version (`0.1.0`)
pub version: String,
/// Date/Time when the event occurred
pub date: DateTime<Local>,
pub info: Info,
/// Report level
pub level: Level,
/// RustyPipe operation (e.g. `get_player`)
@ -35,6 +33,18 @@ pub struct Report {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Info {
/// Rust package name (`rustypipe`)
pub package: String,
/// Package version (`0.1.0`)
pub version: String,
/// Date/Time when the event occurred
pub date: DateTime<Local>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HTTPRequest {
/// Request URL
pub url: String,
@ -61,6 +71,16 @@ pub enum Level {
ERR,
}
impl Default for Info {
fn default() -> Self {
Self {
package: "rustypipe".to_owned(),
version: "0.1.0".to_owned(),
date: chrono::Local::now(),
}
}
}
pub trait Reporter {
fn report(&self, report: &Report);
}
@ -103,7 +123,11 @@ fn get_report_path(root: &Path, report: &Report, ext: &str) -> Result<PathBuf> {
std::fs::create_dir_all(root)?;
}
let filename_prefix = format!("{}_{:?}", report.date.format("%F_%H-%M-%S"), report.level);
let filename_prefix = format!(
"{}_{:?}",
report.info.date.format("%F_%H-%M-%S"),
report.level
);
let mut report_path = root.to_path_buf();
report_path.push(format!("{}.{}", filename_prefix, ext));

View file

@ -1,3 +1,20 @@
//! Parser for textual dates and times.
//!
//! The YouTube API mostly outputs pre-formatted dates and times
//! like "18 minutes ago" or "Jul 2, 2014" instead of standardized
//! machine-readable date and time formats.
//!
//! Additionally these formats are localized, meaning they depend
//! on the configured language.
//!
//! This module can parse these dates using an embedded dictionary which
//! contains date/time unit tokens for all supported languages.
//!
//! Note that this module is public so it can be tested from outside
//! the crate, which is important for including new languages, too.
//!
//! It is not intended to be used to parse textual dates that are not from YouTube.
use std::ops::Mul;
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
@ -5,18 +22,21 @@ use serde::{Deserialize, Serialize};
use crate::{dictionary, model::Language, util};
/// Parsed TimeAgo string, contains amount and time unit.
///
/// Example: "14 hours ago" => `TimeAgo {n: 14, unit: TimeUnit::Hour}`
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TimeAgo {
pub n: u8,
pub unit: TimeUnit,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TaToken {
pub n: u8,
pub unit: Option<TimeUnit>,
}
/// Parsed date string that may be relative or absolute.
///
/// Examples:
///
/// - "Jul 2, 2014" => `ParsedDate::Absolute("2014-07-02")`
/// - "2 months ago" => `ParsedDate::Relative(TimeAgo {n: 2, unit: TimeUnit::Month})`
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ParsedDate {
Absolute(NaiveDate),
@ -35,7 +55,14 @@ pub enum TimeUnit {
Year,
}
pub enum DateCmp {
/// Value of a parsed TimeAgo token, used in the dictionary
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) struct TaToken {
pub n: u8,
pub unit: Option<TimeUnit>,
}
pub(crate) enum DateCmp {
Y,
M,
D,
@ -135,6 +162,9 @@ fn parse_textual_month(entry: &dictionary::Entry, filtered_str: &str) -> Option<
}
}
/// Parse a TimeAgo string (e.g. "29 minutes ago") into a TimeAgo object.
///
/// Returns None if the date could not be parsed.
pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
let entry = dictionary::entry(lang);
let filtered_str = filter_str(textual_date);
@ -144,6 +174,9 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
parse_ta_token(&entry, false, &filtered_str).map(|ta| ta * qu)
}
/// Parse a TimeAgo string (e.g. "29 minutes ago") into a Chrono DateTime object.
///
/// Returns None if the date could not be parsed.
pub fn parse_timeago_to_dt(lang: Language, textual_date: &str) -> Option<DateTime<Local>> {
parse_timeago(lang, textual_date).map(|ta| ta.into())
}
@ -160,6 +193,9 @@ pub(crate) fn parse_timeago_or_warn(
res
}
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
///
/// Returns None if the date could not be parsed.
pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDate> {
let entry = dictionary::entry(lang);
let filtered_str = filter_str(textual_date);
@ -206,6 +242,9 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
}
}
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a Chrono DateTime object.
///
/// Returns None if the date could not be parsed.
pub fn parse_textual_date_to_dt(lang: Language, textual_date: &str) -> Option<DateTime<Local>> {
parse_textual_date(lang, textual_date).map(|ta| ta.into())
}