From 4dfdb47cae3d132c673857b6c4e0c53c9f7bd09b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 10 Oct 2022 22:25:20 +0200 Subject: [PATCH 1/2] fix: don't use a specific letter for the nsig function name regex --- src/deobfuscate.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index 961419e..60ae12e 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -68,7 +68,7 @@ impl From for Deobfuscator { const DEOBFUSCATION_FUNC_NAME: &str = "deobfuscate"; fn get_sig_fn_name(player_js: &str) -> Result { - 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("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)").unwrap(), @@ -79,7 +79,7 @@ fn get_sig_fn_name(player_js: &str) -> Result { ] }); - 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")) } @@ -107,10 +107,10 @@ fn get_sig_fn(player_js: &str) -> Result { .as_str() + ";"; - static HELPER_OBJECT_NAME_PATTERN: Lazy = + static HELPER_OBJECT_NAME_REGEX: Lazy = Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap()); let helper_object_name = some_or_bail!( - HELPER_OBJECT_NAME_PATTERN + HELPER_OBJECT_NAME_REGEX .captures(&deobfuscate_function) .ok() .flatten(), @@ -151,13 +151,13 @@ fn deobfuscate_sig(sig: &str, sig_fn: &str) -> Result { } fn get_nsig_fn_name(player_js: &str) -> Result { - static FUNCTION_NAME_PATTERN: Lazy = Lazy::new(|| { - Regex::new("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9]\\)") + static FUNCTION_NAME_REGEX: Lazy = Lazy::new(|| { + Regex::new("\\.get\\(\"n\"\\)\\)&&\\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9$_]\\)") .unwrap() }); 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")) ); From 1fb4a2664e576dfe39569a31fa9a8e1615a83110 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 11 Oct 2022 00:31:04 +0200 Subject: [PATCH 2/2] fix: moved integration tests, fixed missing search video count, error on missing search channel description snipped, error on empty channel info link list, --- src/client/channel.rs | 148 +-- src/client/channel_rss.rs | 27 +- src/client/player.rs | 131 +-- src/client/playlist.rs | 88 +- src/client/response/channel.rs | 1 + src/client/response/search.rs | 8 +- src/client/search.rs | 17 +- ...nt__search__tests__map_search_default.snap | 1 + src/client/video_details.rs | 467 +-------- src/error.rs | 2 +- src/lib.rs | 2 +- src/model/mod.rs | 2 + tests/youtube.rs | 968 ++++++++++++++++++ 13 files changed, 998 insertions(+), 864 deletions(-) create mode 100644 tests/youtube.rs diff --git a/src/client/channel.rs b/src/client/channel.rs index 6b16fa7..a240b83 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -494,149 +494,15 @@ fn map_channel_content( mod tests { use std::{fs::File, io::BufReader, path::Path}; - use chrono::Datelike; use rstest::rstest; use crate::{ - client::{response, MapResponse, RustyPipe}, + client::{response, MapResponse}, model::{Channel, ChannelInfo, ChannelPlaylist, ChannelVideo, Paginator}, - param::{ChannelOrder, Language}, + param::Language, 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(channel: &Channel) { - 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::base("base", "UC2DjFE7Xf11URZqWBigcVOQ")] #[case::music("music", "UC_vmjW5e1xEHhYjY2a0kK1A")] @@ -644,7 +510,7 @@ mod tests { #[case::live("live", "UChs0pSaEoNLV4mevBFGaoKA")] #[case::empty("empty", "UCxBa895m48H5idw5li7h-0g")] #[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 json_path = Path::new(&filename); let json_file = File::open(json_path).unwrap(); @@ -672,7 +538,7 @@ mod tests { } #[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_file = File::open(json_path).unwrap(); @@ -693,7 +559,7 @@ mod tests { } #[test] - fn t_map_channel_playlists() { + fn map_channel_playlists() { let json_path = Path::new("testfiles/channel/channel_playlists.json"); let json_file = File::open(json_path).unwrap(); @@ -712,7 +578,7 @@ mod tests { } #[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_file = File::open(json_path).unwrap(); @@ -731,7 +597,7 @@ mod tests { } #[test] - fn t_map_channel_info() { + fn map_channel_info() { let json_path = Path::new("testfiles/channel/channel_info.json"); let json_file = File::open(json_path).unwrap(); diff --git a/src/client/channel_rss.rs b/src/client/channel_rss.rs index 44fa26b..80de0e2 100644 --- a/src/client/channel_rss.rs +++ b/src/client/channel_rss.rs @@ -56,32 +56,7 @@ impl RustyPipeQuery { 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()); - } + use crate::{client::response, model::ChannelRss}; #[test] fn map_channel_rss() { diff --git a/src/client/player.rs b/src/client/player.rs index 03e03ab..87543d9 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -559,7 +559,7 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec { mod tests { use std::{fs::File, io::BufReader, path::Path}; - use crate::{client::RustyPipe, deobfuscate::DeobfData}; + use crate::deobfuscate::DeobfData; use super::*; use rstest::rstest; @@ -579,7 +579,7 @@ mod tests { #[case::tv_html5_embed("tvhtml5embed")] #[case::android("android")] #[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 json_path = Path::new(&filename); 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] - 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 mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()]; let map_res = map_url( diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 2cb03e5..f137a80 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -248,67 +248,13 @@ mod tests { use rstest::rstest; - use crate::client::RustyPipe; - 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, - #[case] channel: Option, - ) { - 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] #[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")] #[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")] #[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 json_path = Path::new(&filename); let json_file = File::open(json_path).unwrap(); @@ -326,36 +272,4 @@ mod tests { ".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); - } } diff --git a/src/client/response/channel.rs b/src/client/response/channel.rs index 3f37ca3..9f38a03 100644 --- a/src/client/response/channel.rs +++ b/src/client/response/channel.rs @@ -148,6 +148,7 @@ pub struct ChannelFullMetadata { pub joined_date_text: String, #[serde_as(as = "Text")] pub view_count_text: String, + #[serde(default)] #[serde_as(as = "VecSkipError<_>")] pub primary_links: Vec, } diff --git a/src/client/response/search.rs b/src/client/response/search.rs index fe7509b..9ebe29d 100644 --- a/src/client/response/search.rs +++ b/src/client/response/search.rs @@ -165,10 +165,14 @@ pub struct ChannelRenderer { pub title: String, pub thumbnail: Thumbnails, /// Abbreviated channel description + /// + /// Not present if the channel has no description + #[serde(default)] #[serde_as(as = "Text")] pub description_snippet: String, - #[serde_as(as = "Text")] - pub video_count_text: String, + /// Not present if the channel has no videos + #[serde_as(as = "Option")] + pub video_count_text: Option, #[serde_as(as = "Option")] pub subscriber_count_text: Option, /// Channel verification badge diff --git a/src/client/search.rs b/src/client/search.rs index 5b1e8b5..c6a3500 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -257,6 +257,10 @@ fn map_search_items( subscriber_count: channel .subscriber_count_text .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, })) } @@ -281,7 +285,7 @@ mod tests { use std::{fs::File, io::BufReader, path::Path}; use crate::{ - client::{response, MapResponse, RustyPipe}, + client::{response, MapResponse}, model::{Paginator, SearchItem, SearchResult}, param::Language, serializer::MapResult, @@ -289,17 +293,6 @@ mod tests { 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] #[case::default("default")] #[case::playlists("playlists")] diff --git a/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap b/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap index 5b27ca5..8e9df34 100644 --- a/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap +++ b/src/client/snapshots/rustypipe__client__search__tests__map_search_default.snap @@ -23,6 +23,7 @@ SearchResult( ], verification: verified, 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}...", )), Video(SearchVideo( diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 49eb3dc..e718c46 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -570,478 +570,13 @@ fn map_comment( mod tests { use std::{fs::File, io::BufReader, path::Path}; - use chrono::Datelike; use rstest::rstest; use crate::{ - client::{response, MapResponse, RustyPipe}, - model::{richtext::ToPlaintext, Verification}, + client::{response, MapResponse}, 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] #[case::mv("mv", "ZeerrnuLi5E")] #[case::music("music", "XuM2onMGvTI")] diff --git a/src/error.rs b/src/error.rs index 35875a5..572fd9e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,7 +81,7 @@ pub enum ExtractionError { InvalidData(Cow<'static, str>), #[error("got wrong result from YT: {0}")] WrongResult(String), - #[error("Warnings during deserialization/mapping in strict mode")] + #[error("Warnings during deserialization/mapping")] DeserializationWarnings, } diff --git a/src/lib.rs b/src/lib.rs index 27612c5..f06ca4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! Client for the public YouTube / YouTube Music API (Innertube), //! inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). -#![warn(clippy::todo)] +#![warn(clippy::todo, clippy::dbg_macro)] #[macro_use] mod macros; diff --git a/src/model/mod.rs b/src/model/mod.rs index 79e426c..883ee27 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -835,6 +835,8 @@ pub struct SearchChannel { /// /// [`None`] if hidden by the owner or not present. pub subscriber_count: Option, + /// Number of videos from the channel + pub video_count: u64, /// Abbreviated channel description pub short_description: String, } diff --git a/tests/youtube.rs b/tests/youtube.rs new file mode 100644 index 0000000..9e50617 --- /dev/null +++ b/tests/youtube.rs @@ -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, + #[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(channel: &Channel) { + 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(channel: &Channel, 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 + ); + } +}