Compare commits
2 commits
1f6a9a5aaa
...
1fb4a2664e
Author | SHA1 | Date | |
---|---|---|---|
1fb4a2664e | |||
4dfdb47cae |
14 changed files with 1005 additions and 871 deletions
|
@ -494,149 +494,15 @@ fn map_channel_content(
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use chrono::Datelike;
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapResponse, RustyPipe},
|
client::{response, MapResponse},
|
||||||
model::{Channel, ChannelInfo, ChannelPlaylist, ChannelVideo, Paginator},
|
model::{Channel, ChannelInfo, ChannelPlaylist, ChannelVideo, Paginator},
|
||||||
param::{ChannelOrder, Language},
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case::latest(ChannelOrder::Latest)]
|
|
||||||
#[case::oldest(ChannelOrder::Oldest)]
|
|
||||||
#[case::popular(ChannelOrder::Popular)]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_videos(#[case] order: ChannelOrder) {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let channel = rp
|
|
||||||
.query()
|
|
||||||
.channel_videos_ordered("UC2DjFE7Xf11URZqWBigcVOQ", order)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&channel);
|
|
||||||
check_channel(&channel);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
|
||||||
"got no videos"
|
|
||||||
);
|
|
||||||
|
|
||||||
let first_video = &channel.content.items[0];
|
|
||||||
let first_video_date = first_video.publish_date.unwrap();
|
|
||||||
let age_days = (chrono::Local::now() - first_video_date).num_days();
|
|
||||||
|
|
||||||
match order {
|
|
||||||
ChannelOrder::Latest => {
|
|
||||||
assert!(age_days < 60, "latest video older than 60 days")
|
|
||||||
}
|
|
||||||
ChannelOrder::Oldest => {
|
|
||||||
assert!(age_days > 4700, "oldest video newer than 4700 days")
|
|
||||||
}
|
|
||||||
ChannelOrder::Popular => {
|
|
||||||
assert!(
|
|
||||||
first_video.view_count > 2300000,
|
|
||||||
"most popular video < 2.3M views"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let next = channel.content.next(rp.query()).await.unwrap().unwrap();
|
|
||||||
assert!(
|
|
||||||
!next.is_exhausted() && !next.items.is_empty(),
|
|
||||||
"no more videos"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_playlists() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let channel = rp
|
|
||||||
.query()
|
|
||||||
.channel_playlists("UC2DjFE7Xf11URZqWBigcVOQ")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&channel);
|
|
||||||
check_channel(&channel);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
|
||||||
"got no playlists"
|
|
||||||
);
|
|
||||||
|
|
||||||
let next = channel.content.next(rp.query()).await.unwrap().unwrap();
|
|
||||||
assert!(
|
|
||||||
!next.is_exhausted() && !next.items.is_empty(),
|
|
||||||
"no more playlists"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_info() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let channel = rp
|
|
||||||
.query()
|
|
||||||
.channel_info("UC2DjFE7Xf11URZqWBigcVOQ")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
dbg!(&channel);
|
|
||||||
check_channel(&channel);
|
|
||||||
|
|
||||||
let created = channel.content.create_date.unwrap();
|
|
||||||
assert_eq!(created.year(), 2009);
|
|
||||||
assert_eq!(created.month(), 4);
|
|
||||||
assert_eq!(created.day(), 4);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
channel.content.view_count.unwrap() > 186854340,
|
|
||||||
"exp >186M views, got {}",
|
|
||||||
channel.content.view_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
insta::assert_ron_snapshot!(channel.content.links, @r###"
|
|
||||||
[
|
|
||||||
("EEVblog Web Site", "http://www.eevblog.com/"),
|
|
||||||
("Twitter", "http://www.twitter.com/eevblog"),
|
|
||||||
("Facebook", "http://www.facebook.com/EEVblog"),
|
|
||||||
("EEVdiscover", "https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ"),
|
|
||||||
("The EEVblog Forum", "http://www.eevblog.com/forum"),
|
|
||||||
("EEVblog Merchandise (T-Shirts)", "http://www.eevblog.com/merch"),
|
|
||||||
("EEVblog Donations", "http://www.eevblog.com/donations/"),
|
|
||||||
("Patreon", "https://www.patreon.com/eevblog"),
|
|
||||||
("SubscribeStar", "https://www.subscribestar.com/eevblog"),
|
|
||||||
("The AmpHour Radio Show", "http://www.theamphour.com/"),
|
|
||||||
("Flickr", "http://www.flickr.com/photos/eevblog"),
|
|
||||||
("EEVblog AMAZON Store", "http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2F&tag=ee04-20&linkCode=ur2&camp=1789&creative=390957"),
|
|
||||||
("2nd EEVblog Channel", "http://www.youtube.com/EEVblog2"),
|
|
||||||
]
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_channel<T>(channel: &Channel<T>) {
|
|
||||||
assert_eq!(channel.id, "UC2DjFE7Xf11URZqWBigcVOQ");
|
|
||||||
assert_eq!(channel.name, "EEVblog");
|
|
||||||
assert!(
|
|
||||||
channel.subscriber_count.unwrap() > 880000,
|
|
||||||
"exp >880K subscribers, got {}",
|
|
||||||
channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
assert!(!channel.avatar.is_empty(), "got no thumbnails");
|
|
||||||
assert_eq!(channel.description, "NO SCRIPT, NO FEAR, ALL OPINION\nAn off-the-cuff Video Blog about Electronics Engineering, for engineers, hobbyists, enthusiasts, hackers and Makers\nHosted by Dave Jones from Sydney Australia\n\nDONATIONS:\nBitcoin: 3KqyH1U3qrMPnkLufM2oHDU7YB4zVZeFyZ\nEthereum: 0x99ccc4d2654ba40744a1f678d9868ecb15e91206\nPayPal: david@alternatezone.com\n\nPatreon: https://www.patreon.com/eevblog\n\nEEVblog2: http://www.youtube.com/EEVblog2\nEEVdiscover: https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ\n\nEMAIL:\nAdvertising/Commercial: eevblog+business@gmail.com\nFan mail: eevblog+fan@gmail.com\nHate Mail: eevblog+hate@gmail.com\n\nI DON'T DO PAID VIDEO SPONSORSHIPS, DON'T ASK!\n\nPLEASE:\nDo NOT ask for personal advice on something, post it in the EEVblog forum.\nI read ALL email, but please don't be offended if I don't have time to reply, I get a LOT of email.\n\nMailbag\nPO Box 7949\nBaulkham Hills NSW 2153\nAUSTRALIA");
|
|
||||||
assert!(!channel.tags.is_empty(), "got no tags");
|
|
||||||
assert_eq!(
|
|
||||||
channel.vanity_url.as_ref().unwrap(),
|
|
||||||
"https://www.youtube.com/c/EevblogDave"
|
|
||||||
);
|
|
||||||
assert!(!channel.banner.is_empty(), "got no banners");
|
|
||||||
assert!(!channel.mobile_banner.is_empty(), "got no mobile banners");
|
|
||||||
assert!(!channel.tv_banner.is_empty(), "got no tv banners");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::base("base", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
#[case::base("base", "UC2DjFE7Xf11URZqWBigcVOQ")]
|
||||||
#[case::music("music", "UC_vmjW5e1xEHhYjY2a0kK1A")]
|
#[case::music("music", "UC_vmjW5e1xEHhYjY2a0kK1A")]
|
||||||
|
@ -644,7 +510,7 @@ mod tests {
|
||||||
#[case::live("live", "UChs0pSaEoNLV4mevBFGaoKA")]
|
#[case::live("live", "UChs0pSaEoNLV4mevBFGaoKA")]
|
||||||
#[case::empty("empty", "UCxBa895m48H5idw5li7h-0g")]
|
#[case::empty("empty", "UCxBa895m48H5idw5li7h-0g")]
|
||||||
#[case::upcoming("upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
#[case::upcoming("upcoming", "UCcvfHa-GHSOHFAjU0-Ie57A")]
|
||||||
fn t_map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
fn map_channel_videos(#[case] name: &str, #[case] id: &str) {
|
||||||
let filename = format!("testfiles/channel/channel_videos_{}.json", name);
|
let filename = format!("testfiles/channel/channel_videos_{}.json", name);
|
||||||
let json_path = Path::new(&filename);
|
let json_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
@ -672,7 +538,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_map_channel_videos_cont() {
|
fn map_channel_videos_cont() {
|
||||||
let json_path = Path::new("testfiles/channel/channel_videos_cont.json");
|
let json_path = Path::new("testfiles/channel/channel_videos_cont.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -693,7 +559,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_map_channel_playlists() {
|
fn map_channel_playlists() {
|
||||||
let json_path = Path::new("testfiles/channel/channel_playlists.json");
|
let json_path = Path::new("testfiles/channel/channel_playlists.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -712,7 +578,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_map_channel_playlists_cont() {
|
fn map_channel_playlists_cont() {
|
||||||
let json_path = Path::new("testfiles/channel/channel_playlists_cont.json");
|
let json_path = Path::new("testfiles/channel/channel_playlists_cont.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -731,7 +597,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_map_channel_info() {
|
fn map_channel_info() {
|
||||||
let json_path = Path::new("testfiles/channel/channel_info.json");
|
let json_path = Path::new("testfiles/channel/channel_info.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -56,32 +56,7 @@ impl RustyPipeQuery {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use chrono::{Datelike, Timelike};
|
use crate::{client::response, model::ChannelRss};
|
||||||
|
|
||||||
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]
|
#[test]
|
||||||
fn map_channel_rss() {
|
fn map_channel_rss() {
|
||||||
|
|
|
@ -559,7 +559,7 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use crate::{client::RustyPipe, deobfuscate::DeobfData};
|
use crate::deobfuscate::DeobfData;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
@ -579,7 +579,7 @@ mod tests {
|
||||||
#[case::tv_html5_embed("tvhtml5embed")]
|
#[case::tv_html5_embed("tvhtml5embed")]
|
||||||
#[case::android("android")]
|
#[case::android("android")]
|
||||||
#[case::ios("ios")]
|
#[case::ios("ios")]
|
||||||
fn t_map_player_data(#[case] name: &str) {
|
fn map_player_data(#[case] name: &str) {
|
||||||
let filename = format!("testfiles/player/{}_video.json", name);
|
let filename = format!("testfiles/player/{}_video.json", name);
|
||||||
let json_path = Path::new(&filename);
|
let json_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
@ -608,133 +608,8 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert equality within 10% margin
|
|
||||||
fn assert_approx(left: f64, right: f64) {
|
|
||||||
if left != right {
|
|
||||||
let f = left / right;
|
|
||||||
assert!(
|
|
||||||
0.9 < f && f < 1.1,
|
|
||||||
"{} not within 10% margin of {}",
|
|
||||||
left,
|
|
||||||
right
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case::desktop(ClientType::Desktop)]
|
|
||||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
|
||||||
#[case::android(ClientType::Android)]
|
|
||||||
#[case::ios(ClientType::Ios)]
|
|
||||||
#[test_log::test(tokio::test)]
|
|
||||||
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();
|
|
||||||
|
|
||||||
// dbg!(&player_data);
|
|
||||||
|
|
||||||
assert_eq!(player_data.details.id, "n4tK7LYFxI0");
|
|
||||||
assert_eq!(player_data.details.title, "Spektrem - Shine [NCS Release]");
|
|
||||||
if client_type == ClientType::DesktopMusic {
|
|
||||||
assert!(player_data.details.description.is_none());
|
|
||||||
} else {
|
|
||||||
assert!(player_data.details.description.unwrap().starts_with(
|
|
||||||
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
assert_eq!(player_data.details.length, 259);
|
|
||||||
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);
|
|
||||||
assert_eq!(player_data.details.keywords[0], "spektrem");
|
|
||||||
assert_eq!(player_data.details.is_live_content, false);
|
|
||||||
|
|
||||||
if client_type == ClientType::Ios {
|
|
||||||
let video = player_data
|
|
||||||
.video_only_streams
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.itag == 247)
|
|
||||||
.unwrap();
|
|
||||||
let audio = player_data
|
|
||||||
.audio_streams
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.itag == 140)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Bitrates may change between requests
|
|
||||||
assert_approx(video.bitrate as f64, 1507068.0);
|
|
||||||
assert_eq!(video.average_bitrate, 1345149);
|
|
||||||
assert_eq!(video.size.unwrap(), 43553412);
|
|
||||||
assert_eq!(video.width, 1280);
|
|
||||||
assert_eq!(video.height, 720);
|
|
||||||
assert_eq!(video.fps, 30);
|
|
||||||
assert_eq!(video.quality, "720p");
|
|
||||||
assert_eq!(video.hdr, false);
|
|
||||||
assert_eq!(video.mime, "video/webm; codecs=\"vp09.00.31.08\"");
|
|
||||||
assert_eq!(video.format, VideoFormat::Webm);
|
|
||||||
assert_eq!(video.codec, VideoCodec::Vp9);
|
|
||||||
|
|
||||||
assert_approx(audio.bitrate as f64, 130685.0);
|
|
||||||
assert_eq!(audio.average_bitrate, 129496);
|
|
||||||
assert_eq!(audio.size, 4193863);
|
|
||||||
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
|
||||||
assert_eq!(audio.format, AudioFormat::M4a);
|
|
||||||
assert_eq!(audio.codec, AudioCodec::Mp4a);
|
|
||||||
} else {
|
|
||||||
let video = player_data
|
|
||||||
.video_only_streams
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.itag == 398)
|
|
||||||
.unwrap();
|
|
||||||
let audio = player_data
|
|
||||||
.audio_streams
|
|
||||||
.iter()
|
|
||||||
.find(|s| s.itag == 251)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_approx(video.bitrate as f64, 1340829.0);
|
|
||||||
assert_approx(video.average_bitrate as f64, 1233444.0);
|
|
||||||
assert_approx(video.size.unwrap() as f64, 39936630.0);
|
|
||||||
assert_eq!(video.width, 1280);
|
|
||||||
assert_eq!(video.height, 720);
|
|
||||||
assert_eq!(video.fps, 30);
|
|
||||||
assert_eq!(video.quality, "720p");
|
|
||||||
assert_eq!(video.hdr, false);
|
|
||||||
assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
|
|
||||||
assert_eq!(video.format, VideoFormat::Mp4);
|
|
||||||
assert_eq!(video.codec, VideoCodec::Av01);
|
|
||||||
assert_eq!(video.throttled, false);
|
|
||||||
|
|
||||||
assert_approx(audio.bitrate as f64, 142718.0);
|
|
||||||
assert_approx(audio.average_bitrate as f64, 130708.0);
|
|
||||||
assert_approx(audio.size as f64, 4232344.0);
|
|
||||||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
|
||||||
assert_eq!(audio.format, AudioFormat::Webm);
|
|
||||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
|
||||||
assert_eq!(audio.throttled, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(player_data.expires_in_seconds > 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[rstest]
|
|
||||||
#[case::desktop(ClientType::Desktop)]
|
|
||||||
// #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
|
||||||
// #[case::android(ClientType::Android)]
|
|
||||||
// #[case::ios(ClientType::Ios)]
|
|
||||||
#[test_log::test(tokio::test)]
|
|
||||||
async fn get_player_live(#[case] client_type: ClientType) {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let player_data = rp.query().player("86YLFOog4GM", client_type).await.unwrap();
|
|
||||||
|
|
||||||
dbg!(&player_data);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_cipher_to_url() {
|
fn 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";
|
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";
|
||||||
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
|
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
|
||||||
let map_res = map_url(
|
let map_res = map_url(
|
||||||
|
|
|
@ -248,67 +248,13 @@ mod tests {
|
||||||
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::client::RustyPipe;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[rstest]
|
|
||||||
#[case::long(
|
|
||||||
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
|
|
||||||
"Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022",
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
Some(ChannelId {
|
|
||||||
id: "UCIekuFeMaV78xYfvpmoCnPg".to_owned(),
|
|
||||||
name: "Best Music".to_owned(),
|
|
||||||
})
|
|
||||||
)]
|
|
||||||
#[case::short(
|
|
||||||
"RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk",
|
|
||||||
"Easy Pop",
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
None
|
|
||||||
)]
|
|
||||||
#[case::nomusic(
|
|
||||||
"PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe",
|
|
||||||
"Minecraft SHINE",
|
|
||||||
false,
|
|
||||||
Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...".to_owned()),
|
|
||||||
Some(ChannelId {
|
|
||||||
id: "UCQM0bS4_04-Y4JuYrgmnpZQ".to_owned(),
|
|
||||||
name: "Chaosflo44".to_owned(),
|
|
||||||
})
|
|
||||||
)]
|
|
||||||
#[test_log::test(tokio::test)]
|
|
||||||
async fn t_get_playlist(
|
|
||||||
#[case] id: &str,
|
|
||||||
#[case] name: &str,
|
|
||||||
#[case] is_long: bool,
|
|
||||||
#[case] description: Option<String>,
|
|
||||||
#[case] channel: Option<ChannelId>,
|
|
||||||
) {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let playlist = rp.query().playlist(id).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(playlist.id, id);
|
|
||||||
assert_eq!(playlist.name, name);
|
|
||||||
assert!(!playlist.videos.is_empty());
|
|
||||||
assert_eq!(!playlist.videos.is_exhausted(), is_long);
|
|
||||||
assert!(playlist.video_count > 10);
|
|
||||||
assert_eq!(playlist.video_count > 100, is_long);
|
|
||||||
assert_eq!(playlist.description, description);
|
|
||||||
if channel.is_some() {
|
|
||||||
assert_eq!(playlist.channel, channel);
|
|
||||||
}
|
|
||||||
assert!(!playlist.thumbnail.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||||
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
||||||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||||
fn t_map_playlist_data(#[case] name: &str, #[case] id: &str) {
|
fn map_playlist_data(#[case] name: &str, #[case] id: &str) {
|
||||||
let filename = format!("testfiles/playlist/playlist_{}.json", name);
|
let filename = format!("testfiles/playlist/playlist_{}.json", name);
|
||||||
let json_path = Path::new(&filename);
|
let json_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
@ -326,36 +272,4 @@ mod tests {
|
||||||
".last_update" => "[date]"
|
".last_update" => "[date]"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(tokio::test)]
|
|
||||||
async fn t_playlist_cont() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let mut playlist = rp
|
|
||||||
.query()
|
|
||||||
.playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
playlist
|
|
||||||
.videos
|
|
||||||
.extend_pages(rp.query(), usize::MAX)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(playlist.videos.items.len() > 100);
|
|
||||||
assert!(playlist.videos.count.unwrap() > 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test_log::test(tokio::test)]
|
|
||||||
async fn t_playlist_cont2() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let mut playlist = rp
|
|
||||||
.query()
|
|
||||||
.playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
playlist.videos.extend_limit(rp.query(), 101).await.unwrap();
|
|
||||||
assert!(playlist.videos.items.len() > 100);
|
|
||||||
assert!(playlist.videos.count.unwrap() > 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,6 +148,7 @@ pub struct ChannelFullMetadata {
|
||||||
pub joined_date_text: String,
|
pub joined_date_text: String,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "Text")]
|
||||||
pub view_count_text: String,
|
pub view_count_text: String,
|
||||||
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub primary_links: Vec<PrimaryLink>,
|
pub primary_links: Vec<PrimaryLink>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,10 +165,14 @@ pub struct ChannelRenderer {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
/// Abbreviated channel description
|
/// Abbreviated channel description
|
||||||
|
///
|
||||||
|
/// Not present if the channel has no description
|
||||||
|
#[serde(default)]
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "Text")]
|
||||||
pub description_snippet: String,
|
pub description_snippet: String,
|
||||||
#[serde_as(as = "Text")]
|
/// Not present if the channel has no videos
|
||||||
pub video_count_text: String,
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub video_count_text: Option<String>,
|
||||||
#[serde_as(as = "Option<Text>")]
|
#[serde_as(as = "Option<Text>")]
|
||||||
pub subscriber_count_text: Option<String>,
|
pub subscriber_count_text: Option<String>,
|
||||||
/// Channel verification badge
|
/// Channel verification badge
|
||||||
|
|
|
@ -257,6 +257,10 @@ fn map_search_items(
|
||||||
subscriber_count: channel
|
subscriber_count: channel
|
||||||
.subscriber_count_text
|
.subscriber_count_text
|
||||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||||
|
video_count: channel
|
||||||
|
.video_count_text
|
||||||
|
.and_then(|txt| util::parse_numeric(&txt).ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
short_description: channel.description_snippet,
|
short_description: channel.description_snippet,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -281,7 +285,7 @@ mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapResponse, RustyPipe},
|
client::{response, MapResponse},
|
||||||
model::{Paginator, SearchItem, SearchResult},
|
model::{Paginator, SearchItem, SearchResult},
|
||||||
param::Language,
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
|
@ -289,17 +293,6 @@ mod tests {
|
||||||
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn t1() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let result = rp
|
|
||||||
.query()
|
|
||||||
.search("grewhbtrjlrbnerwhlbvuwrkeghurzueg")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
dbg!(&result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::default("default")]
|
#[case::default("default")]
|
||||||
#[case::playlists("playlists")]
|
#[case::playlists("playlists")]
|
||||||
|
|
|
@ -23,6 +23,7 @@ SearchResult(
|
||||||
],
|
],
|
||||||
verification: verified,
|
verification: verified,
|
||||||
subscriber_count: Some(292),
|
subscriber_count: Some(292),
|
||||||
|
video_count: 219,
|
||||||
short_description: "Hi, I\'m Tina, aka Doobydobap! Food is the medium I use to tell stories and connect with people who share the same passion as I\u{a0}...",
|
short_description: "Hi, I\'m Tina, aka Doobydobap! Food is the medium I use to tell stories and connect with people who share the same passion as I\u{a0}...",
|
||||||
)),
|
)),
|
||||||
Video(SearchVideo(
|
Video(SearchVideo(
|
||||||
|
|
|
@ -570,478 +570,13 @@ fn map_comment(
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use chrono::Datelike;
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapResponse, RustyPipe},
|
client::{response, MapResponse},
|
||||||
model::{richtext::ToPlaintext, Verification},
|
|
||||||
param::Language,
|
param::Language,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "ZeerrnuLi5E");
|
|
||||||
assert_eq!(details.title, "aespa 에스파 'Black Mamba' MV");
|
|
||||||
let desc = details.description.to_plaintext();
|
|
||||||
assert!(
|
|
||||||
desc.contains("Listen and download aespa's debut single \"Black Mamba\""),
|
|
||||||
"bad description: {}",
|
|
||||||
desc
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCEf_Bc-KVd7onSeifS3py9g");
|
|
||||||
assert_eq!(details.channel.name, "SMTOWN");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::Verified);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 30000000,
|
|
||||||
"expected >30M subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 232000000,
|
|
||||||
"expected > 232M views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
details.like_count.unwrap() > 4000000,
|
|
||||||
"expected > 4M likes, got {}",
|
|
||||||
details.like_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2020);
|
|
||||||
assert_eq!(date.month(), 11);
|
|
||||||
assert_eq!(date.day(), 17);
|
|
||||||
|
|
||||||
assert!(!details.is_live);
|
|
||||||
assert!(!details.is_ccommons);
|
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.top_comments.count.unwrap() > 700000,
|
|
||||||
"expected > 700K comments, got {}",
|
|
||||||
details.top_comments.count.unwrap()
|
|
||||||
);
|
|
||||||
assert!(!details.top_comments.is_exhausted());
|
|
||||||
assert!(!details.latest_comments.is_exhausted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details_music() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("XuM2onMGvTI").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "XuM2onMGvTI");
|
|
||||||
assert_eq!(details.title, "Gäa");
|
|
||||||
let desc = details.description.to_plaintext();
|
|
||||||
assert!(desc.contains("Gäa · Oonagh"), "bad description: {}", desc);
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCVGvnqB-5znqPSbMGlhF4Pw");
|
|
||||||
assert_eq!(details.channel.name, "Sentamusic");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::Artist);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 33000,
|
|
||||||
"expected >33K subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 20309,
|
|
||||||
"expected > 20309 views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
details.like_count.unwrap() > 145,
|
|
||||||
"expected > 145 likes, got {}",
|
|
||||||
details.like_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2020);
|
|
||||||
assert_eq!(date.month(), 8);
|
|
||||||
assert_eq!(date.day(), 6);
|
|
||||||
|
|
||||||
assert!(!details.is_live);
|
|
||||||
assert!(!details.is_ccommons);
|
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
|
||||||
|
|
||||||
// Comments are disabled for this video
|
|
||||||
assert_eq!(details.top_comments.count, Some(0));
|
|
||||||
assert_eq!(details.latest_comments.count, Some(0));
|
|
||||||
assert!(details.top_comments.is_empty());
|
|
||||||
assert!(details.latest_comments.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details_ccommons() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("0rb9CfOvojk").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "0rb9CfOvojk");
|
|
||||||
assert_eq!(
|
|
||||||
details.title,
|
|
||||||
"BahnMining - Pünktlichkeit ist eine Zier (David Kriesel)"
|
|
||||||
);
|
|
||||||
let desc = details.description.to_plaintext();
|
|
||||||
assert!(
|
|
||||||
desc.contains("Seit Anfang 2019 hat David jeden einzelnen Halt jeder einzelnen Zugfahrt auf jedem einzelnen Fernbahnhof in ganz Deutschland"),
|
|
||||||
"bad description: {}",
|
|
||||||
desc
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UC2TXq_t06Hjdr2g_KdKpHQg");
|
|
||||||
assert_eq!(details.channel.name, "media.ccc.de");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::None);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 170000,
|
|
||||||
"expected >170K subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 2517358,
|
|
||||||
"expected > 2517358 views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
details.like_count.unwrap() > 52330,
|
|
||||||
"expected > 52330 likes, got {}",
|
|
||||||
details.like_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2019);
|
|
||||||
assert_eq!(date.month(), 12);
|
|
||||||
assert_eq!(date.day(), 29);
|
|
||||||
|
|
||||||
assert!(!details.is_live);
|
|
||||||
assert!(details.is_ccommons);
|
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.top_comments.count.unwrap() > 2199,
|
|
||||||
"expected > 2199 comments, got {}",
|
|
||||||
details.top_comments.count.unwrap()
|
|
||||||
);
|
|
||||||
assert!(!details.top_comments.is_exhausted());
|
|
||||||
assert!(!details.latest_comments.is_exhausted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details_chapters() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("nFDBxBUfE74").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "nFDBxBUfE74");
|
|
||||||
assert_eq!(details.title, "The Prepper PC");
|
|
||||||
let desc = details.description.to_plaintext();
|
|
||||||
assert!(
|
|
||||||
desc.contains("These days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse"),
|
|
||||||
"bad description: {}",
|
|
||||||
desc
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCXuqSBlHAE6Xw-yeJA0Tunw");
|
|
||||||
assert_eq!(details.channel.name, "Linus Tech Tips");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::Verified);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 14700000,
|
|
||||||
"expected >14.7M subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 1157262,
|
|
||||||
"expected > 1157262 views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
details.like_count.unwrap() > 54670,
|
|
||||||
"expected > 54670 likes, got {}",
|
|
||||||
details.like_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2022);
|
|
||||||
assert_eq!(date.month(), 9);
|
|
||||||
assert_eq!(date.day(), 15);
|
|
||||||
|
|
||||||
assert!(!details.is_live);
|
|
||||||
assert!(!details.is_ccommons);
|
|
||||||
|
|
||||||
insta::assert_ron_snapshot!(details.chapters, {
|
|
||||||
"[].thumbnail" => insta::dynamic_redaction(move |value, _path| {
|
|
||||||
assert!(!value.as_slice().unwrap().is_empty());
|
|
||||||
"[ok]"
|
|
||||||
}),
|
|
||||||
}, @r###"
|
|
||||||
[
|
|
||||||
Chapter(
|
|
||||||
title: "Intro",
|
|
||||||
position: 0,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "The PC Built for Super Efficiency",
|
|
||||||
position: 42,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Our BURIAL ENCLOSURE?!",
|
|
||||||
position: 161,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Our Power Solution (Thanks Jackery!)",
|
|
||||||
position: 211,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Diggin\' Holes",
|
|
||||||
position: 287,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Colonoscopy?",
|
|
||||||
position: 330,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Diggin\' like a man",
|
|
||||||
position: 424,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "The world\'s worst woodsman",
|
|
||||||
position: 509,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Backyard cable management",
|
|
||||||
position: 543,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Time to bury this boy",
|
|
||||||
position: 602,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Solar Power Generation",
|
|
||||||
position: 646,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Issues",
|
|
||||||
position: 697,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "First Play Test",
|
|
||||||
position: 728,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
Chapter(
|
|
||||||
title: "Conclusion",
|
|
||||||
position: 800,
|
|
||||||
thumbnail: "[ok]",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
"###);
|
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.top_comments.count.unwrap() > 3199,
|
|
||||||
"expected > 3199 comments, got {}",
|
|
||||||
details.top_comments.count.unwrap()
|
|
||||||
);
|
|
||||||
assert!(!details.top_comments.is_exhausted());
|
|
||||||
assert!(!details.latest_comments.is_exhausted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details_live() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("86YLFOog4GM").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "86YLFOog4GM");
|
|
||||||
assert_eq!(
|
|
||||||
details.title,
|
|
||||||
"🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS"
|
|
||||||
);
|
|
||||||
let desc = details.description.to_plaintext();
|
|
||||||
assert!(
|
|
||||||
desc.contains("Live NASA - Views Of Earth from Space"),
|
|
||||||
"bad description: {}",
|
|
||||||
desc
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCakgsb0w7QB0VHdnCc-OVEA");
|
|
||||||
assert_eq!(details.channel.name, "Space Videos");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::Verified);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 5500000,
|
|
||||||
"expected >5.5M subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 10,
|
|
||||||
"expected > 10 views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
details.like_count.unwrap() > 872290,
|
|
||||||
"expected > 872290 likes, got {}",
|
|
||||||
details.like_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2021);
|
|
||||||
assert_eq!(date.month(), 9);
|
|
||||||
assert_eq!(date.day(), 23);
|
|
||||||
|
|
||||||
assert!(details.is_live);
|
|
||||||
assert!(!details.is_ccommons);
|
|
||||||
|
|
||||||
assert!(!details.recommended.items.is_empty());
|
|
||||||
assert!(!details.recommended.is_exhausted());
|
|
||||||
|
|
||||||
// No comments because livestream
|
|
||||||
assert_eq!(details.top_comments.count, Some(0));
|
|
||||||
assert_eq!(details.latest_comments.count, Some(0));
|
|
||||||
assert!(details.top_comments.is_empty());
|
|
||||||
assert!(details.latest_comments.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_details_agegate() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("HRKu0cvrr_o").await.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&details);
|
|
||||||
|
|
||||||
assert_eq!(details.id, "HRKu0cvrr_o");
|
|
||||||
assert_eq!(
|
|
||||||
details.title,
|
|
||||||
"AlphaOmegaSin Fanboy Logic: Likes/Dislikes Disabled = Point Invalid Lol wtf?"
|
|
||||||
);
|
|
||||||
insta::assert_ron_snapshot!(details.description, @"RichText([])");
|
|
||||||
|
|
||||||
assert_eq!(details.channel.id, "UCQT2yul0lr6Ie9qNQNmw-sg");
|
|
||||||
assert_eq!(details.channel.name, "PrinceOfFALLEN");
|
|
||||||
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
|
||||||
assert_eq!(details.channel.verification, Verification::None);
|
|
||||||
assert!(
|
|
||||||
details.channel.subscriber_count.unwrap() > 1400,
|
|
||||||
"expected >1400 subs, got {}",
|
|
||||||
details.channel.subscriber_count.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
details.view_count > 200,
|
|
||||||
"expected > 200 views, got {}",
|
|
||||||
details.view_count
|
|
||||||
);
|
|
||||||
assert!(details.like_count.is_none(), "like count not hidden");
|
|
||||||
|
|
||||||
let date = details.publish_date.unwrap();
|
|
||||||
assert_eq!(date.year(), 2019);
|
|
||||||
assert_eq!(date.month(), 1);
|
|
||||||
assert_eq!(date.day(), 2);
|
|
||||||
|
|
||||||
assert!(!details.is_live);
|
|
||||||
assert!(!details.is_ccommons);
|
|
||||||
|
|
||||||
// No recommendations because agegate
|
|
||||||
assert_eq!(details.recommended.count, Some(0));
|
|
||||||
assert!(details.recommended.items.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_recommendations() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
|
||||||
let next_recommendations = details.recommended.next(rp.query()).await.unwrap().unwrap();
|
|
||||||
dbg!(&next_recommendations);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
next_recommendations.items.len() > 10,
|
|
||||||
"expected > 10 next recommendations, got {}",
|
|
||||||
next_recommendations.items.len()
|
|
||||||
);
|
|
||||||
assert!(!next_recommendations.is_exhausted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_video_comments() {
|
|
||||||
let rp = RustyPipe::builder().strict().build();
|
|
||||||
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
|
||||||
|
|
||||||
let top_comments = details
|
|
||||||
.top_comments
|
|
||||||
.next(rp.query())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
top_comments.items.len() > 10,
|
|
||||||
"expected > 10 next comments, got {}",
|
|
||||||
top_comments.items.len()
|
|
||||||
);
|
|
||||||
assert!(!top_comments.is_exhausted());
|
|
||||||
|
|
||||||
let n_comments = top_comments.count.unwrap();
|
|
||||||
assert!(
|
|
||||||
n_comments > 700000,
|
|
||||||
"expected > 700k comments, got {}",
|
|
||||||
n_comments
|
|
||||||
);
|
|
||||||
// Comment count should be exact after fetching first page
|
|
||||||
assert!(n_comments % 1000 != 0);
|
|
||||||
|
|
||||||
let latest_comments = details
|
|
||||||
.latest_comments
|
|
||||||
.next(rp.query())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
latest_comments.items.len() > 10,
|
|
||||||
"expected > 10 next comments, got {}",
|
|
||||||
latest_comments.items.len()
|
|
||||||
);
|
|
||||||
assert!(!latest_comments.is_exhausted());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::mv("mv", "ZeerrnuLi5E")]
|
#[case::mv("mv", "ZeerrnuLi5E")]
|
||||||
#[case::music("music", "XuM2onMGvTI")]
|
#[case::music("music", "XuM2onMGvTI")]
|
||||||
|
|
|
@ -68,7 +68,7 @@ impl From<DeobfData> for Deobfuscator {
|
||||||
const DEOBFUSCATION_FUNC_NAME: &str = "deobfuscate";
|
const DEOBFUSCATION_FUNC_NAME: &str = "deobfuscate";
|
||||||
|
|
||||||
fn get_sig_fn_name(player_js: &str) -> Result<String> {
|
fn get_sig_fn_name(player_js: &str) -> Result<String> {
|
||||||
static FUNCTION_PATTERNS: Lazy<[Regex; 6]> = Lazy::new(|| {
|
static FUNCTION_REGEXES: Lazy<[Regex; 6]> = Lazy::new(|| {
|
||||||
[
|
[
|
||||||
Regex::new("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)").unwrap(),
|
Regex::new("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)").unwrap(),
|
||||||
Regex::new("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)").unwrap(),
|
Regex::new("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)").unwrap(),
|
||||||
|
@ -79,7 +79,7 @@ fn get_sig_fn_name(player_js: &str) -> Result<String> {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
util::get_cg_from_regexes(FUNCTION_PATTERNS.iter(), player_js, 1)
|
util::get_cg_from_regexes(FUNCTION_REGEXES.iter(), player_js, 1)
|
||||||
.ok_or(DeobfError::Extraction("deobf function name"))
|
.ok_or(DeobfError::Extraction("deobf function name"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,10 +107,10 @@ fn get_sig_fn(player_js: &str) -> Result<String> {
|
||||||
.as_str()
|
.as_str()
|
||||||
+ ";";
|
+ ";";
|
||||||
|
|
||||||
static HELPER_OBJECT_NAME_PATTERN: Lazy<Regex> =
|
static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap());
|
Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap());
|
||||||
let helper_object_name = some_or_bail!(
|
let helper_object_name = some_or_bail!(
|
||||||
HELPER_OBJECT_NAME_PATTERN
|
HELPER_OBJECT_NAME_REGEX
|
||||||
.captures(&deobfuscate_function)
|
.captures(&deobfuscate_function)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
@ -151,13 +151,13 @@ fn deobfuscate_sig(sig: &str, sig_fn: &str) -> Result<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_nsig_fn_name(player_js: &str) -> Result<String> {
|
fn get_nsig_fn_name(player_js: &str) -> Result<String> {
|
||||||
static FUNCTION_NAME_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9]\\)")
|
Regex::new("\\.get\\(\"n\"\\)\\)&&\\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9$_]\\)")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
let fname_match = some_or_bail!(
|
let fname_match = some_or_bail!(
|
||||||
FUNCTION_NAME_PATTERN.captures(player_js).ok().flatten(),
|
FUNCTION_NAME_REGEX.captures(player_js).ok().flatten(),
|
||||||
Err(DeobfError::Extraction("n_deobf function"))
|
Err(DeobfError::Extraction("n_deobf function"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ pub enum ExtractionError {
|
||||||
InvalidData(Cow<'static, str>),
|
InvalidData(Cow<'static, str>),
|
||||||
#[error("got wrong result from YT: {0}")]
|
#[error("got wrong result from YT: {0}")]
|
||||||
WrongResult(String),
|
WrongResult(String),
|
||||||
#[error("Warnings during deserialization/mapping in strict mode")]
|
#[error("Warnings during deserialization/mapping")]
|
||||||
DeserializationWarnings,
|
DeserializationWarnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
//! Client for the public YouTube / YouTube Music API (Innertube),
|
//! Client for the public YouTube / YouTube Music API (Innertube),
|
||||||
//! inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
//! inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
|
|
||||||
#![warn(clippy::todo)]
|
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
|
@ -835,6 +835,8 @@ pub struct SearchChannel {
|
||||||
///
|
///
|
||||||
/// [`None`] if hidden by the owner or not present.
|
/// [`None`] if hidden by the owner or not present.
|
||||||
pub subscriber_count: Option<u64>,
|
pub subscriber_count: Option<u64>,
|
||||||
|
/// Number of videos from the channel
|
||||||
|
pub video_count: u64,
|
||||||
/// Abbreviated channel description
|
/// Abbreviated channel description
|
||||||
pub short_description: String,
|
pub short_description: String,
|
||||||
}
|
}
|
||||||
|
|
968
tests/youtube.rs
Normal file
968
tests/youtube.rs
Normal file
|
@ -0,0 +1,968 @@
|
||||||
|
use chrono::{Datelike, Timelike};
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use rustypipe::client::{ClientType, RustyPipe};
|
||||||
|
use rustypipe::model::richtext::ToPlaintext;
|
||||||
|
use rustypipe::model::{
|
||||||
|
AudioCodec, AudioFormat, Channel, SearchItem, Verification, VideoCodec, VideoFormat,
|
||||||
|
};
|
||||||
|
use rustypipe::param::{
|
||||||
|
search_filter::{self, SearchFilter},
|
||||||
|
ChannelOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
//#PLAYER
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::desktop(ClientType::Desktop)]
|
||||||
|
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||||
|
#[case::android(ClientType::Android)]
|
||||||
|
#[case::ios(ClientType::Ios)]
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
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();
|
||||||
|
|
||||||
|
// dbg!(&player_data);
|
||||||
|
|
||||||
|
assert_eq!(player_data.details.id, "n4tK7LYFxI0");
|
||||||
|
assert_eq!(player_data.details.title, "Spektrem - Shine [NCS Release]");
|
||||||
|
if client_type == ClientType::DesktopMusic {
|
||||||
|
assert!(player_data.details.description.is_none());
|
||||||
|
} else {
|
||||||
|
assert!(player_data.details.description.unwrap().starts_with(
|
||||||
|
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
assert_eq!(player_data.details.length, 259);
|
||||||
|
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);
|
||||||
|
assert_eq!(player_data.details.keywords[0], "spektrem");
|
||||||
|
assert_eq!(player_data.details.is_live_content, false);
|
||||||
|
|
||||||
|
if client_type == ClientType::Ios {
|
||||||
|
let video = player_data
|
||||||
|
.video_only_streams
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.itag == 247)
|
||||||
|
.unwrap();
|
||||||
|
let audio = player_data
|
||||||
|
.audio_streams
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.itag == 140)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Bitrates may change between requests
|
||||||
|
assert_approx(video.bitrate as f64, 1507068.0);
|
||||||
|
assert_eq!(video.average_bitrate, 1345149);
|
||||||
|
assert_eq!(video.size.unwrap(), 43553412);
|
||||||
|
assert_eq!(video.width, 1280);
|
||||||
|
assert_eq!(video.height, 720);
|
||||||
|
assert_eq!(video.fps, 30);
|
||||||
|
assert_eq!(video.quality, "720p");
|
||||||
|
assert_eq!(video.hdr, false);
|
||||||
|
assert_eq!(video.mime, "video/webm; codecs=\"vp09.00.31.08\"");
|
||||||
|
assert_eq!(video.format, VideoFormat::Webm);
|
||||||
|
assert_eq!(video.codec, VideoCodec::Vp9);
|
||||||
|
|
||||||
|
assert_approx(audio.bitrate as f64, 130685.0);
|
||||||
|
assert_eq!(audio.average_bitrate, 129496);
|
||||||
|
assert_eq!(audio.size, 4193863);
|
||||||
|
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
||||||
|
assert_eq!(audio.format, AudioFormat::M4a);
|
||||||
|
assert_eq!(audio.codec, AudioCodec::Mp4a);
|
||||||
|
} else {
|
||||||
|
let video = player_data
|
||||||
|
.video_only_streams
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.itag == 398)
|
||||||
|
.unwrap();
|
||||||
|
let audio = player_data
|
||||||
|
.audio_streams
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.itag == 251)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_approx(video.bitrate as f64, 1340829.0);
|
||||||
|
assert_approx(video.average_bitrate as f64, 1233444.0);
|
||||||
|
assert_approx(video.size.unwrap() as f64, 39936630.0);
|
||||||
|
assert_eq!(video.width, 1280);
|
||||||
|
assert_eq!(video.height, 720);
|
||||||
|
assert_eq!(video.fps, 30);
|
||||||
|
assert_eq!(video.quality, "720p");
|
||||||
|
assert_eq!(video.hdr, false);
|
||||||
|
assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
|
||||||
|
assert_eq!(video.format, VideoFormat::Mp4);
|
||||||
|
assert_eq!(video.codec, VideoCodec::Av01);
|
||||||
|
assert_eq!(video.throttled, false);
|
||||||
|
|
||||||
|
assert_approx(audio.bitrate as f64, 142718.0);
|
||||||
|
assert_approx(audio.average_bitrate as f64, 130708.0);
|
||||||
|
assert_approx(audio.size as f64, 4232344.0);
|
||||||
|
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||||
|
assert_eq!(audio.format, AudioFormat::Webm);
|
||||||
|
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||||
|
assert_eq!(audio.throttled, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(player_data.expires_in_seconds > 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[rstest]
|
||||||
|
#[case::desktop(ClientType::Desktop)]
|
||||||
|
// #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||||
|
// #[case::android(ClientType::Android)]
|
||||||
|
// #[case::ios(ClientType::Ios)]
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
async fn get_player_live(#[case] client_type: ClientType) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let player_data = rp.query().player("86YLFOog4GM", client_type).await.unwrap();
|
||||||
|
|
||||||
|
dbg!(&player_data);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
//#PLAYLIST
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::long(
|
||||||
|
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
|
||||||
|
"Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022",
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
Some(("UCIekuFeMaV78xYfvpmoCnPg", "Best Music")),
|
||||||
|
)]
|
||||||
|
#[case::short(
|
||||||
|
"RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk",
|
||||||
|
"Easy Pop",
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)]
|
||||||
|
#[case::nomusic(
|
||||||
|
"PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe",
|
||||||
|
"Minecraft SHINE",
|
||||||
|
false,
|
||||||
|
Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...".to_owned()),
|
||||||
|
Some(("UCQM0bS4_04-Y4JuYrgmnpZQ", "Chaosflo44")),
|
||||||
|
)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_playlist(
|
||||||
|
#[case] id: &str,
|
||||||
|
#[case] name: &str,
|
||||||
|
#[case] is_long: bool,
|
||||||
|
#[case] description: Option<String>,
|
||||||
|
#[case] channel: Option<(&str, &str)>,
|
||||||
|
) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let playlist = rp.query().playlist(id).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(playlist.id, id);
|
||||||
|
assert_eq!(playlist.name, name);
|
||||||
|
assert!(!playlist.videos.is_empty());
|
||||||
|
assert_eq!(!playlist.videos.is_exhausted(), is_long);
|
||||||
|
assert!(playlist.video_count > 10);
|
||||||
|
assert_eq!(playlist.video_count > 100, is_long);
|
||||||
|
assert_eq!(playlist.description, description);
|
||||||
|
match playlist.channel {
|
||||||
|
Some(c) => {
|
||||||
|
let expect = channel.unwrap();
|
||||||
|
assert_eq!(c.id, expect.0);
|
||||||
|
assert_eq!(c.name, expect.1);
|
||||||
|
}
|
||||||
|
None => assert!(channel.is_none()),
|
||||||
|
}
|
||||||
|
assert!(!playlist.thumbnail.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
async fn playlist_cont() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let mut playlist = rp
|
||||||
|
.query()
|
||||||
|
.playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
playlist
|
||||||
|
.videos
|
||||||
|
.extend_pages(rp.query(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(playlist.videos.items.len() > 100);
|
||||||
|
assert!(playlist.videos.count.unwrap() > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
async fn playlist_cont2() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let mut playlist = rp
|
||||||
|
.query()
|
||||||
|
.playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
playlist.videos.extend_limit(rp.query(), 101).await.unwrap();
|
||||||
|
assert!(playlist.videos.items.len() > 100);
|
||||||
|
assert!(playlist.videos.count.unwrap() > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#VIDEO DETAILS
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "ZeerrnuLi5E");
|
||||||
|
assert_eq!(details.title, "aespa 에스파 'Black Mamba' MV");
|
||||||
|
let desc = details.description.to_plaintext();
|
||||||
|
assert!(
|
||||||
|
desc.contains("Listen and download aespa's debut single \"Black Mamba\""),
|
||||||
|
"bad description: {}",
|
||||||
|
desc
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UCEf_Bc-KVd7onSeifS3py9g");
|
||||||
|
assert_eq!(details.channel.name, "SMTOWN");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::Verified);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 30000000,
|
||||||
|
"expected >30M subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 232000000,
|
||||||
|
"expected > 232M views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
details.like_count.unwrap() > 4000000,
|
||||||
|
"expected > 4M likes, got {}",
|
||||||
|
details.like_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2020);
|
||||||
|
assert_eq!(date.month(), 11);
|
||||||
|
assert_eq!(date.day(), 17);
|
||||||
|
|
||||||
|
assert!(!details.is_live);
|
||||||
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
|
assert!(!details.recommended.items.is_empty());
|
||||||
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.top_comments.count.unwrap() > 700000,
|
||||||
|
"expected > 700K comments, got {}",
|
||||||
|
details.top_comments.count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!details.top_comments.is_exhausted());
|
||||||
|
assert!(!details.latest_comments.is_exhausted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details_music() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("XuM2onMGvTI").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "XuM2onMGvTI");
|
||||||
|
assert_eq!(details.title, "Gäa");
|
||||||
|
let desc = details.description.to_plaintext();
|
||||||
|
assert!(desc.contains("Gäa · Oonagh"), "bad description: {}", desc);
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UCVGvnqB-5znqPSbMGlhF4Pw");
|
||||||
|
assert_eq!(details.channel.name, "Sentamusic");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::Artist);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 33000,
|
||||||
|
"expected >33K subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 20309,
|
||||||
|
"expected > 20309 views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
details.like_count.unwrap() > 145,
|
||||||
|
"expected > 145 likes, got {}",
|
||||||
|
details.like_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2020);
|
||||||
|
assert_eq!(date.month(), 8);
|
||||||
|
assert_eq!(date.day(), 6);
|
||||||
|
|
||||||
|
assert!(!details.is_live);
|
||||||
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
|
assert!(!details.recommended.items.is_empty());
|
||||||
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
|
// Comments are disabled for this video
|
||||||
|
assert_eq!(details.top_comments.count, Some(0));
|
||||||
|
assert_eq!(details.latest_comments.count, Some(0));
|
||||||
|
assert!(details.top_comments.is_empty());
|
||||||
|
assert!(details.latest_comments.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details_ccommons() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("0rb9CfOvojk").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "0rb9CfOvojk");
|
||||||
|
assert_eq!(
|
||||||
|
details.title,
|
||||||
|
"BahnMining - Pünktlichkeit ist eine Zier (David Kriesel)"
|
||||||
|
);
|
||||||
|
let desc = details.description.to_plaintext();
|
||||||
|
assert!(
|
||||||
|
desc.contains("Seit Anfang 2019 hat David jeden einzelnen Halt jeder einzelnen Zugfahrt auf jedem einzelnen Fernbahnhof in ganz Deutschland"),
|
||||||
|
"bad description: {}",
|
||||||
|
desc
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UC2TXq_t06Hjdr2g_KdKpHQg");
|
||||||
|
assert_eq!(details.channel.name, "media.ccc.de");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::None);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 170000,
|
||||||
|
"expected >170K subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 2517358,
|
||||||
|
"expected > 2517358 views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
details.like_count.unwrap() > 52330,
|
||||||
|
"expected > 52330 likes, got {}",
|
||||||
|
details.like_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2019);
|
||||||
|
assert_eq!(date.month(), 12);
|
||||||
|
assert_eq!(date.day(), 29);
|
||||||
|
|
||||||
|
assert!(!details.is_live);
|
||||||
|
assert!(details.is_ccommons);
|
||||||
|
|
||||||
|
assert!(!details.recommended.items.is_empty());
|
||||||
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.top_comments.count.unwrap() > 2199,
|
||||||
|
"expected > 2199 comments, got {}",
|
||||||
|
details.top_comments.count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!details.top_comments.is_exhausted());
|
||||||
|
assert!(!details.latest_comments.is_exhausted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details_chapters() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("nFDBxBUfE74").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "nFDBxBUfE74");
|
||||||
|
assert_eq!(details.title, "The Prepper PC");
|
||||||
|
let desc = details.description.to_plaintext();
|
||||||
|
assert!(
|
||||||
|
desc.contains("These days, you can game almost anywhere on the planet, anytime. But what if that planet was in the middle of an apocalypse"),
|
||||||
|
"bad description: {}",
|
||||||
|
desc
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UCXuqSBlHAE6Xw-yeJA0Tunw");
|
||||||
|
assert_eq!(details.channel.name, "Linus Tech Tips");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::Verified);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 14700000,
|
||||||
|
"expected >14.7M subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 1157262,
|
||||||
|
"expected > 1157262 views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
details.like_count.unwrap() > 54670,
|
||||||
|
"expected > 54670 likes, got {}",
|
||||||
|
details.like_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2022);
|
||||||
|
assert_eq!(date.month(), 9);
|
||||||
|
assert_eq!(date.day(), 15);
|
||||||
|
|
||||||
|
assert!(!details.is_live);
|
||||||
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
|
insta::assert_ron_snapshot!(details.chapters, {
|
||||||
|
"[].thumbnail" => insta::dynamic_redaction(move |value, _path| {
|
||||||
|
assert!(!value.as_slice().unwrap().is_empty());
|
||||||
|
"[ok]"
|
||||||
|
}),
|
||||||
|
}, @r###"
|
||||||
|
[
|
||||||
|
Chapter(
|
||||||
|
title: "Intro",
|
||||||
|
position: 0,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "The PC Built for Super Efficiency",
|
||||||
|
position: 42,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Our BURIAL ENCLOSURE?!",
|
||||||
|
position: 161,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Our Power Solution (Thanks Jackery!)",
|
||||||
|
position: 211,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Diggin\' Holes",
|
||||||
|
position: 287,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Colonoscopy?",
|
||||||
|
position: 330,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Diggin\' like a man",
|
||||||
|
position: 424,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "The world\'s worst woodsman",
|
||||||
|
position: 509,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Backyard cable management",
|
||||||
|
position: 543,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Time to bury this boy",
|
||||||
|
position: 602,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Solar Power Generation",
|
||||||
|
position: 646,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Issues",
|
||||||
|
position: 697,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "First Play Test",
|
||||||
|
position: 728,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
Chapter(
|
||||||
|
title: "Conclusion",
|
||||||
|
position: 800,
|
||||||
|
thumbnail: "[ok]",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
assert!(!details.recommended.items.is_empty());
|
||||||
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.top_comments.count.unwrap() > 3199,
|
||||||
|
"expected > 3199 comments, got {}",
|
||||||
|
details.top_comments.count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!details.top_comments.is_exhausted());
|
||||||
|
assert!(!details.latest_comments.is_exhausted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details_live() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("86YLFOog4GM").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "86YLFOog4GM");
|
||||||
|
assert_eq!(
|
||||||
|
details.title,
|
||||||
|
"🌎 Nasa Live Stream - Earth From Space : Live Views from the ISS"
|
||||||
|
);
|
||||||
|
let desc = details.description.to_plaintext();
|
||||||
|
assert!(
|
||||||
|
desc.contains("Live NASA - Views Of Earth from Space"),
|
||||||
|
"bad description: {}",
|
||||||
|
desc
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UCakgsb0w7QB0VHdnCc-OVEA");
|
||||||
|
assert_eq!(details.channel.name, "Space Videos");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::Verified);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 5500000,
|
||||||
|
"expected >5.5M subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 10,
|
||||||
|
"expected > 10 views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
details.like_count.unwrap() > 872290,
|
||||||
|
"expected > 872290 likes, got {}",
|
||||||
|
details.like_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2021);
|
||||||
|
assert_eq!(date.month(), 9);
|
||||||
|
assert_eq!(date.day(), 23);
|
||||||
|
|
||||||
|
assert!(details.is_live);
|
||||||
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
|
assert!(!details.recommended.items.is_empty());
|
||||||
|
assert!(!details.recommended.is_exhausted());
|
||||||
|
|
||||||
|
// No comments because livestream
|
||||||
|
assert_eq!(details.top_comments.count, Some(0));
|
||||||
|
assert_eq!(details.latest_comments.count, Some(0));
|
||||||
|
assert!(details.top_comments.is_empty());
|
||||||
|
assert!(details.latest_comments.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_details_agegate() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("HRKu0cvrr_o").await.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&details);
|
||||||
|
|
||||||
|
assert_eq!(details.id, "HRKu0cvrr_o");
|
||||||
|
assert_eq!(
|
||||||
|
details.title,
|
||||||
|
"AlphaOmegaSin Fanboy Logic: Likes/Dislikes Disabled = Point Invalid Lol wtf?"
|
||||||
|
);
|
||||||
|
insta::assert_ron_snapshot!(details.description, @"RichText([])");
|
||||||
|
|
||||||
|
assert_eq!(details.channel.id, "UCQT2yul0lr6Ie9qNQNmw-sg");
|
||||||
|
assert_eq!(details.channel.name, "PrinceOfFALLEN");
|
||||||
|
assert!(!details.channel.avatar.is_empty(), "no channel avatars");
|
||||||
|
assert_eq!(details.channel.verification, Verification::None);
|
||||||
|
assert!(
|
||||||
|
details.channel.subscriber_count.unwrap() > 1400,
|
||||||
|
"expected >1400 subs, got {}",
|
||||||
|
details.channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
details.view_count > 200,
|
||||||
|
"expected > 200 views, got {}",
|
||||||
|
details.view_count
|
||||||
|
);
|
||||||
|
assert!(details.like_count.is_none(), "like count not hidden");
|
||||||
|
|
||||||
|
let date = details.publish_date.unwrap();
|
||||||
|
assert_eq!(date.year(), 2019);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.day(), 2);
|
||||||
|
|
||||||
|
assert!(!details.is_live);
|
||||||
|
assert!(!details.is_ccommons);
|
||||||
|
|
||||||
|
// No recommendations because agegate
|
||||||
|
assert_eq!(details.recommended.count, Some(0));
|
||||||
|
assert!(details.recommended.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_recommendations() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
||||||
|
let next_recommendations = details.recommended.next(rp.query()).await.unwrap().unwrap();
|
||||||
|
// dbg!(&next_recommendations);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
next_recommendations.items.len() > 10,
|
||||||
|
"expected > 10 next recommendations, got {}",
|
||||||
|
next_recommendations.items.len()
|
||||||
|
);
|
||||||
|
assert!(!next_recommendations.is_exhausted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_video_comments() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let details = rp.query().video_details("ZeerrnuLi5E").await.unwrap();
|
||||||
|
|
||||||
|
let top_comments = details
|
||||||
|
.top_comments
|
||||||
|
.next(rp.query())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
top_comments.items.len() > 10,
|
||||||
|
"expected > 10 next comments, got {}",
|
||||||
|
top_comments.items.len()
|
||||||
|
);
|
||||||
|
assert!(!top_comments.is_exhausted());
|
||||||
|
|
||||||
|
let n_comments = top_comments.count.unwrap();
|
||||||
|
assert!(
|
||||||
|
n_comments > 700000,
|
||||||
|
"expected > 700k comments, got {}",
|
||||||
|
n_comments
|
||||||
|
);
|
||||||
|
// Comment count should be exact after fetching first page
|
||||||
|
assert!(n_comments % 1000 != 0);
|
||||||
|
|
||||||
|
let latest_comments = details
|
||||||
|
.latest_comments
|
||||||
|
.next(rp.query())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
latest_comments.items.len() > 10,
|
||||||
|
"expected > 10 next comments, got {}",
|
||||||
|
latest_comments.items.len()
|
||||||
|
);
|
||||||
|
assert!(!latest_comments.is_exhausted());
|
||||||
|
}
|
||||||
|
|
||||||
|
//#CHANNEL
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::latest(ChannelOrder::Latest)]
|
||||||
|
#[case::oldest(ChannelOrder::Oldest)]
|
||||||
|
#[case::popular(ChannelOrder::Popular)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_videos(#[case] order: ChannelOrder) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_videos_ordered("UC2DjFE7Xf11URZqWBigcVOQ", order)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&channel);
|
||||||
|
assert_channel_eevblog(&channel);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
||||||
|
"got no videos"
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_video = &channel.content.items[0];
|
||||||
|
let first_video_date = first_video.publish_date.unwrap();
|
||||||
|
let age_days = (chrono::Local::now() - first_video_date).num_days();
|
||||||
|
|
||||||
|
match order {
|
||||||
|
ChannelOrder::Latest => {
|
||||||
|
assert!(age_days < 60, "latest video older than 60 days")
|
||||||
|
}
|
||||||
|
ChannelOrder::Oldest => {
|
||||||
|
assert!(age_days > 4700, "oldest video newer than 4700 days")
|
||||||
|
}
|
||||||
|
ChannelOrder::Popular => {
|
||||||
|
assert!(
|
||||||
|
first_video.view_count > 2300000,
|
||||||
|
"most popular video < 2.3M views"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = channel.content.next(rp.query()).await.unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
!next.is_exhausted() && !next.items.is_empty(),
|
||||||
|
"no more videos"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_playlists() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_playlists("UC2DjFE7Xf11URZqWBigcVOQ")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_channel_eevblog(&channel);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!channel.content.items.is_empty() && !channel.content.is_exhausted(),
|
||||||
|
"got no playlists"
|
||||||
|
);
|
||||||
|
|
||||||
|
let next = channel.content.next(rp.query()).await.unwrap().unwrap();
|
||||||
|
assert!(
|
||||||
|
!next.is_exhausted() && !next.items.is_empty(),
|
||||||
|
"no more playlists"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_info() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let channel = rp
|
||||||
|
.query()
|
||||||
|
.channel_info("UC2DjFE7Xf11URZqWBigcVOQ")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// dbg!(&channel);
|
||||||
|
assert_channel_eevblog(&channel);
|
||||||
|
|
||||||
|
let created = channel.content.create_date.unwrap();
|
||||||
|
assert_eq!(created.year(), 2009);
|
||||||
|
assert_eq!(created.month(), 4);
|
||||||
|
assert_eq!(created.day(), 4);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
channel.content.view_count.unwrap() > 186854340,
|
||||||
|
"exp >186M views, got {}",
|
||||||
|
channel.content.view_count.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
insta::assert_ron_snapshot!(channel.content.links, @r###"
|
||||||
|
[
|
||||||
|
("EEVblog Web Site", "http://www.eevblog.com/"),
|
||||||
|
("Twitter", "http://www.twitter.com/eevblog"),
|
||||||
|
("Facebook", "http://www.facebook.com/EEVblog"),
|
||||||
|
("EEVdiscover", "https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ"),
|
||||||
|
("The EEVblog Forum", "http://www.eevblog.com/forum"),
|
||||||
|
("EEVblog Merchandise (T-Shirts)", "http://www.eevblog.com/merch"),
|
||||||
|
("EEVblog Donations", "http://www.eevblog.com/donations/"),
|
||||||
|
("Patreon", "https://www.patreon.com/eevblog"),
|
||||||
|
("SubscribeStar", "https://www.subscribestar.com/eevblog"),
|
||||||
|
("The AmpHour Radio Show", "http://www.theamphour.com/"),
|
||||||
|
("Flickr", "http://www.flickr.com/photos/eevblog"),
|
||||||
|
("EEVblog AMAZON Store", "http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2F&tag=ee04-20&linkCode=ur2&camp=1789&creative=390957"),
|
||||||
|
("2nd EEVblog Channel", "http://www.youtube.com/EEVblog2"),
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_channel_eevblog<T>(channel: &Channel<T>) {
|
||||||
|
assert_eq!(channel.id, "UC2DjFE7Xf11URZqWBigcVOQ");
|
||||||
|
assert_eq!(channel.name, "EEVblog");
|
||||||
|
assert!(
|
||||||
|
channel.subscriber_count.unwrap() > 880000,
|
||||||
|
"exp >880K subscribers, got {}",
|
||||||
|
channel.subscriber_count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(!channel.avatar.is_empty(), "got no thumbnails");
|
||||||
|
assert_eq!(channel.description, "NO SCRIPT, NO FEAR, ALL OPINION\nAn off-the-cuff Video Blog about Electronics Engineering, for engineers, hobbyists, enthusiasts, hackers and Makers\nHosted by Dave Jones from Sydney Australia\n\nDONATIONS:\nBitcoin: 3KqyH1U3qrMPnkLufM2oHDU7YB4zVZeFyZ\nEthereum: 0x99ccc4d2654ba40744a1f678d9868ecb15e91206\nPayPal: david@alternatezone.com\n\nPatreon: https://www.patreon.com/eevblog\n\nEEVblog2: http://www.youtube.com/EEVblog2\nEEVdiscover: https://www.youtube.com/channel/UCkGvUEt8iQLmq3aJIMjT2qQ\n\nEMAIL:\nAdvertising/Commercial: eevblog+business@gmail.com\nFan mail: eevblog+fan@gmail.com\nHate Mail: eevblog+hate@gmail.com\n\nI DON'T DO PAID VIDEO SPONSORSHIPS, DON'T ASK!\n\nPLEASE:\nDo NOT ask for personal advice on something, post it in the EEVblog forum.\nI read ALL email, but please don't be offended if I don't have time to reply, I get a LOT of email.\n\nMailbag\nPO Box 7949\nBaulkham Hills NSW 2153\nAUSTRALIA");
|
||||||
|
assert!(!channel.tags.is_empty(), "got no tags");
|
||||||
|
assert_eq!(
|
||||||
|
channel.vanity_url.as_ref().unwrap(),
|
||||||
|
"https://www.youtube.com/c/EevblogDave"
|
||||||
|
);
|
||||||
|
assert!(!channel.banner.is_empty(), "got no banners");
|
||||||
|
assert!(!channel.mobile_banner.is_empty(), "got no mobile banners");
|
||||||
|
assert!(!channel.tv_banner.is_empty(), "got no tv banners");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::artist("UC_vmjW5e1xEHhYjY2a0kK1A", "Oonagh - Topic", false, false)]
|
||||||
|
#[case::shorts("UCh8gHdtzO2tXd593_bjErWg", "Doobydobap", true, true)]
|
||||||
|
#[case::live(
|
||||||
|
"UChs0pSaEoNLV4mevBFGaoKA",
|
||||||
|
"The Good Life Radio x Sensual Musique",
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)]
|
||||||
|
// TODO: fix YouTube Music extraction error
|
||||||
|
// #[case::music("UC-9-kyTW8ZkZNDHQJ6FgpwQ", "Music", false, false)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn channel_more(
|
||||||
|
#[case] id: &str,
|
||||||
|
#[case] name: &str,
|
||||||
|
#[case] has_videos: bool,
|
||||||
|
#[case] has_playlists: bool,
|
||||||
|
) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
|
||||||
|
fn assert_channel<T>(channel: &Channel<T>, id: &str, name: &str) {
|
||||||
|
assert_eq!(channel.id, id);
|
||||||
|
assert_eq!(channel.name, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_videos = rp.query().channel_videos(&id).await.unwrap();
|
||||||
|
assert_channel(&channel_videos, id, name);
|
||||||
|
if has_videos {
|
||||||
|
assert!(!channel_videos.content.items.is_empty(), "got no videos");
|
||||||
|
} else {
|
||||||
|
assert!(channel_videos.content.items.is_empty(), "got videos");
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_playlists = rp.query().channel_playlists(&id).await.unwrap();
|
||||||
|
assert_channel(&channel_playlists, id, name);
|
||||||
|
if has_playlists {
|
||||||
|
assert!(
|
||||||
|
!channel_playlists.content.items.is_empty(),
|
||||||
|
"got no playlists"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(channel_playlists.content.items.is_empty(), "got playlists");
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_info = rp.query().channel_info(&id).await.unwrap();
|
||||||
|
assert_channel(&channel_info, id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#CHANNEL_RSS
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
|
||||||
|
//#SEARCH
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn search() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let result = rp.query().search("doobydoobap").await.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.items.count.unwrap() > 7000,
|
||||||
|
"expected > 7000 total results, got {}",
|
||||||
|
result.items.count.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.items.items.len() > 10,
|
||||||
|
"expected > 10 search results, got {}",
|
||||||
|
result.items.items.len()
|
||||||
|
);
|
||||||
|
assert!(!result.items.is_exhausted());
|
||||||
|
|
||||||
|
assert_eq!(result.corrected_query.unwrap(), "doobydobap");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::video(search_filter::Entity::Video)]
|
||||||
|
#[case::video(search_filter::Entity::Channel)]
|
||||||
|
#[case::video(search_filter::Entity::Playlist)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn search_filter_entity(#[case] entity: search_filter::Entity) {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let result = rp
|
||||||
|
.query()
|
||||||
|
.search_filter("music", &SearchFilter::new().entity(entity))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.items.items.len() > 10,
|
||||||
|
"expected > 10 search results, got {}",
|
||||||
|
result.items.items.len()
|
||||||
|
);
|
||||||
|
assert!(!result.items.is_exhausted());
|
||||||
|
|
||||||
|
result.items.items.iter().for_each(|item| match item {
|
||||||
|
SearchItem::Video(_) => {
|
||||||
|
assert_eq!(entity, search_filter::Entity::Video);
|
||||||
|
}
|
||||||
|
SearchItem::Channel(_) => {
|
||||||
|
assert_eq!(entity, search_filter::Entity::Channel);
|
||||||
|
}
|
||||||
|
SearchItem::Playlist(_) => {
|
||||||
|
assert_eq!(entity, search_filter::Entity::Playlist);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn search_empty() {
|
||||||
|
let rp = RustyPipe::builder().strict().build();
|
||||||
|
let result = rp
|
||||||
|
.query()
|
||||||
|
.search_filter(
|
||||||
|
"test",
|
||||||
|
&search_filter::SearchFilter::new()
|
||||||
|
.feature(search_filter::Feature::IsLive)
|
||||||
|
.feature(search_filter::Feature::Is3d),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//#TESTUTIL
|
||||||
|
|
||||||
|
/// Assert equality within 10% margin
|
||||||
|
fn assert_approx(left: f64, right: f64) {
|
||||||
|
if left != right {
|
||||||
|
let f = left / right;
|
||||||
|
assert!(
|
||||||
|
0.9 < f && f < 1.1,
|
||||||
|
"{} not within 10% margin of {}",
|
||||||
|
left,
|
||||||
|
right
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue