From ee5e82f6bb428bbfb214b1432ef7c5f573fba384 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 8 Feb 2023 12:28:30 +0100 Subject: [PATCH 1/5] fix: instantiate deobfuscator in mapresponse fn Reason: quickjs context is not Send --- src/client/channel.rs | 6 ++--- src/client/mod.rs | 19 +++++++------- src/client/music_artist.rs | 6 ++--- src/client/music_charts.rs | 2 +- src/client/music_details.rs | 8 +++--- src/client/music_genres.rs | 4 +-- src/client/music_new.rs | 2 +- src/client/music_playlist.rs | 4 +-- src/client/music_search.rs | 6 ++--- src/client/pagination.rs | 4 +-- src/client/player.rs | 51 ++++++++++++++++++------------------ src/client/playlist.rs | 12 ++++----- src/client/search.rs | 7 +++-- src/client/trends.rs | 4 +-- src/client/url_resolver.rs | 2 +- src/client/video_details.rs | 4 +-- src/deobfuscate.rs | 17 +++--------- 17 files changed, 74 insertions(+), 84 deletions(-) diff --git a/src/client/channel.rs b/src/client/channel.rs index 6238f68..89334d4 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -162,7 +162,7 @@ impl MapResponse>> for response::Channel { self, id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>>, ExtractionError> { let content = map_channel_content(self.contents, self.alerts)?; @@ -202,7 +202,7 @@ impl MapResponse>> for response::Channel { self, id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>>, ExtractionError> { let content = map_channel_content(self.contents, self.alerts)?; @@ -236,7 +236,7 @@ impl MapResponse> for response::Channel { self, id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let content = map_channel_content(self.contents, self.alerts)?; let channel_data = map_channel( diff --git a/src/client/mod.rs b/src/client/mod.rs index 6ec5717..d94a856 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -35,7 +35,7 @@ use tokio::sync::RwLock; use crate::{ cache::{CacheStorage, FileStorage}, - deobfuscate::{DeobfData, Deobfuscator}, + deobfuscate::DeobfData, error::{Error, ExtractionError}, param::{Country, Language}, report::{FileReporter, Level, Report, Reporter}, @@ -706,20 +706,19 @@ impl RustyPipe { } /// Instantiate a new deobfuscator from either cached or extracted YouTube JavaScript code. - async fn get_deobf(&self) -> Result { + async fn get_deobf_data(&self) -> Result { // Write lock here to prevent concurrent tasks from fetching the same data let mut deobf_data = self.inner.cache.deobf.write().await; match deobf_data.get() { - Some(deobf_data) => Ok(Deobfuscator::new(deobf_data.clone())?), + Some(deobf_data) => Ok(deobf_data.clone()), None => { log::debug!("getting deobfuscator"); - let data = DeobfData::download(self.inner.http.clone()).await?; - let new_deobf = Deobfuscator::new(data.clone())?; - *deobf_data = CacheEntry::from(data); + let new_data = DeobfData::download(self.inner.http.clone()).await?; + *deobf_data = CacheEntry::from(new_data.clone()); drop(deobf_data); self.store_cache().await; - Ok(new_deobf) + Ok(new_data) } } } @@ -1027,7 +1026,7 @@ impl RustyPipeQuery { id: &str, endpoint: &str, body: &B, - deobf: Option<&Deobfuscator>, + deobf: Option<&DeobfData>, ) -> Result { log::debug!("getting {}({})", operation, id); @@ -1056,7 +1055,7 @@ impl RustyPipeQuery { operation: format!("{operation}({id})"), error, msgs, - deobf_data: deobf.map(Deobfuscator::get_data), + deobf_data: deobf.cloned(), http_request: crate::report::HTTPRequest { url: request_url, method: "POST".to_string(), @@ -1197,7 +1196,7 @@ trait MapResponse { self, id: &str, lang: Language, - deobf: Option<&Deobfuscator>, + deobf: Option<&DeobfData>, ) -> Result, ExtractionError>; } diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index 287a50b..d11f863 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -145,7 +145,7 @@ impl MapResponse for response::MusicArtist { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let mapped = map_artist_page(self, id, lang, false)?; Ok(MapResult { @@ -160,7 +160,7 @@ impl MapResponse<(MusicArtist, Vec)> for response::MusicArtist { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result)>, ExtractionError> { map_artist_page(self, id, lang, true) } @@ -328,7 +328,7 @@ impl MapResponse> for response::MusicArtistAlbums { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { // dbg!(&self); diff --git a/src/client/music_charts.rs b/src/client/music_charts.rs index ff0007d..574787e 100644 --- a/src/client/music_charts.rs +++ b/src/client/music_charts.rs @@ -59,7 +59,7 @@ impl MapResponse for response::MusicCharts { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, crate::error::ExtractionError> { let countries = self .framework_updates diff --git a/src/client/music_details.rs b/src/client/music_details.rs index 346dd81..e40c4d8 100644 --- a/src/client/music_details.rs +++ b/src/client/music_details.rs @@ -156,7 +156,7 @@ impl MapResponse for response::MusicDetails { self, id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let tabs = self .contents @@ -232,7 +232,7 @@ impl MapResponse> for response::MusicDetails { self, _id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let tabs = self .contents @@ -287,7 +287,7 @@ impl MapResponse for response::MusicLyrics { self, _id: &str, _lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let lyrics = self .contents @@ -317,7 +317,7 @@ impl MapResponse for response::MusicRelated { self, _id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { // Find artist let artist_id = self diff --git a/src/client/music_genres.rs b/src/client/music_genres.rs index 2b9dad9..30877ad 100644 --- a/src/client/music_genres.rs +++ b/src/client/music_genres.rs @@ -66,7 +66,7 @@ impl MapResponse> for response::MusicGenres { self, _id: &str, _lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let content = self .contents @@ -119,7 +119,7 @@ impl MapResponse for response::MusicGenre { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { // dbg!(&self); diff --git a/src/client/music_new.rs b/src/client/music_new.rs index 2546254..de20415 100644 --- a/src/client/music_new.rs +++ b/src/client/music_new.rs @@ -51,7 +51,7 @@ impl MapResponse> for response::MusicNew { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let items = self .contents diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 1e1e24a..ca3c6cf 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -104,7 +104,7 @@ impl MapResponse for response::MusicPlaylist { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { // dbg!(&self); @@ -242,7 +242,7 @@ impl MapResponse for response::MusicPlaylist { self, id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { // dbg!(&self); diff --git a/src/client/music_search.rs b/src/client/music_search.rs index 80de03f..833152b 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -230,7 +230,7 @@ impl MapResponse for response::MusicSearch { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, crate::error::ExtractionError> { // dbg!(&self); @@ -284,7 +284,7 @@ impl MapResponse> for response::MusicSearc self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { // dbg!(&self); @@ -339,7 +339,7 @@ impl MapResponse for response::MusicSearchSuggestion { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let mut mapper = MusicListMapper::new(lang); let mut terms = Vec::new(); diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 97f8e47..8a599b9 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -98,7 +98,7 @@ impl MapResponse> for response::Continuation { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let items = self .on_response_received_actions @@ -130,7 +130,7 @@ impl MapResponse> for response::MusicContinuation { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let mut mapper = MusicListMapper::new(lang); let mut continuations = Vec::new(); diff --git a/src/client/player.rs b/src/client/player.rs index c95b6c0..f7bcbdc 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -30,7 +30,7 @@ struct QPlayer<'a> { context: YTContext<'a>, /// Website playback context #[serde(skip_serializing_if = "Option::is_none")] - playback_context: Option, + playback_context: Option>, /// Content playback nonce (mobile only, 16 random chars) #[serde(skip_serializing_if = "Option::is_none")] cpn: Option, @@ -44,15 +44,15 @@ struct QPlayer<'a> { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -struct QPlaybackContext { - content_playback_context: QContentPlaybackContext, +struct QPlaybackContext<'a> { + content_playback_context: QContentPlaybackContext<'a>, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -struct QContentPlaybackContext { +struct QContentPlaybackContext<'a> { /// Signature timestamp extracted from player.js - signature_timestamp: String, + signature_timestamp: &'a str, /// Referer URL from website referer: String, } @@ -95,7 +95,7 @@ impl RustyPipeQuery { let video_id = video_id.as_ref(); let (context, deobf) = tokio::join!( self.get_context(client_type, false, None), - self.client.get_deobf() + self.client.get_deobf_data() ); let deobf = deobf?; @@ -104,7 +104,7 @@ impl RustyPipeQuery { context, playback_context: Some(QPlaybackContext { content_playback_context: QContentPlaybackContext { - signature_timestamp: deobf.get_sts(), + signature_timestamp: &deobf.sts, referer: format!("https://www.youtube.com/watch?v={video_id}"), }, }), @@ -141,9 +141,10 @@ impl MapResponse for response::Player { self, id: &str, _lang: Language, - deobf: Option<&Deobfuscator>, + deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { - let deobf = deobf.unwrap(); + let deobf = Deobfuscator::new(deobf.unwrap()) + .map_err(|e| ExtractionError::InvalidData(e.to_string().into()))?; let mut warnings = vec![]; // Check playability status @@ -253,21 +254,21 @@ impl MapResponse for response::Player { match (f.is_video(), f.is_audio()) { (true, true) => { - let mut map_res = map_video_stream(f, deobf, &mut last_nsig); + let mut map_res = map_video_stream(f, &deobf, &mut last_nsig); warnings.append(&mut map_res.warnings); if let Some(c) = map_res.c { video_streams.push(c); }; } (true, false) => { - let mut map_res = map_video_stream(f, deobf, &mut last_nsig); + let mut map_res = map_video_stream(f, &deobf, &mut last_nsig); warnings.append(&mut map_res.warnings); if let Some(c) = map_res.c { video_only_streams.push(c); }; } (false, true) => { - let mut map_res = map_audio_stream(f, deobf, &mut last_nsig); + let mut map_res = map_audio_stream(f, &deobf, &mut last_nsig); warnings.append(&mut map_res.warnings); if let Some(c) = map_res.c { audio_streams.push(c); @@ -612,20 +613,19 @@ mod tests { use std::{fs::File, io::BufReader}; use path_macro::path; - use rstest::{fixture, rstest}; + use rstest::rstest; use super::*; use crate::deobfuscate::DeobfData; - #[fixture] - fn deobf() -> Deobfuscator { - Deobfuscator::new(DeobfData { - js_url: "https://www.youtube.com/s/player/c8b8a173/player_ias.vflset/en_US/base.js".to_owned(), - sig_fn: "var oB={B4:function(a){a.reverse()},xm:function(a,b){a.splice(0,b)},dC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};var Vva=function(a){a=a.split(\"\");oB.dC(a,42);oB.xm(a,3);oB.dC(a,48);oB.B4(a,68);return a.join(\"\")};var deobf_sig=Vva;".to_owned(), - nsig_fn: "Ska=function(a){var b=a.split(\"\"),c=[-1505243983,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},\n-1692381986,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},\n-262444939,\"unshift\",function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},\n1201502951,-546377604,-504264123,-1978377336,1042456724,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},\n711986897,406699922,-1842537993,-1678108293,1803491779,1671716087,12778705,-718839990,null,null,-1617525823,342523552,-1338406651,-399705108,-696713950,b,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},\nfunction(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)},\n-980602034,356396192,null,-1617525823,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n-1029864222,-641353250,-1681901809,-1391247867,1707415199,-1957855835,b,function(){for(var d=64,e=[];++d-e.length-32;)switch(d){case 58:d=96;continue;case 91:d=44;break;case 65:d=47;continue;case 46:d=153;case 123:d-=58;default:e.push(String.fromCharCode(d))}return e},\n-1936558978,-1505243983,function(d){d.reverse()},\n1296889058,-1813915420,-943019300,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n\"join\",b,-2061642263];c[21]=c;c[22]=c;c[33]=c;try{c[3](c[33],c[9]),c[29](c[22],c[25]),c[29](c[22],c[19]),c[29](c[33],c[17]),c[29](c[21],c[2]),c[29](c[42],c[10]),c[1](c[52],c[40]),c[12](c[28],c[8]),c[29](c[21],c[45]),c[1](c[21],c[48]),c[44](c[26]),c[39](c[5],c[2]),c[31](c[53],c[16]),c[30](c[29],c[8]),c[51](c[29],c[6],c[44]()),c[4](c[43],c[1]),c[2](c[23],c[42]),c[2](c[0],c[46]),c[38](c[14],c[52]),c[32](c[5]),c[26](c[29],c[46]),c[26](c[5],c[13]),c[28](c[1],c[37]),c[26](c[31],c[13]),c[26](c[1],c[34]),\nc[46](c[1],c[32],c[40]()),c[26](c[50],c[44]),c[17](c[50],c[51]),c[0](c[3],c[24]),c[32](c[13]),c[43](c[3],c[51]),c[0](c[34],c[17]),c[16](c[45],c[53]),c[29](c[44],c[13]),c[42](c[1],c[50]),c[47](c[22],c[53]),c[37](c[22]),c[13](c[52],c[21]),c[6](c[43],c[34]),c[6](c[31],c[46])}catch(d){return\"enhanced_except_gZYB_un-_w8_\"+a}return b.join(\"\")};var deobf_nsig=Ska;".to_owned(), - sts: "19201".to_owned(), - }).unwrap() + static DEOBF_DATA: Lazy = Lazy::new(|| { + DeobfData { + js_url: "https://www.youtube.com/s/player/c8b8a173/player_ias.vflset/en_US/base.js".to_owned(), + sig_fn: "var oB={B4:function(a){a.reverse()},xm:function(a,b){a.splice(0,b)},dC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};var Vva=function(a){a=a.split(\"\");oB.dC(a,42);oB.xm(a,3);oB.dC(a,48);oB.B4(a,68);return a.join(\"\")};var deobf_sig=Vva;".to_owned(), + nsig_fn: "Ska=function(a){var b=a.split(\"\"),c=[-1505243983,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},\n-1692381986,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},\n-262444939,\"unshift\",function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},\n1201502951,-546377604,-504264123,-1978377336,1042456724,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},\n711986897,406699922,-1842537993,-1678108293,1803491779,1671716087,12778705,-718839990,null,null,-1617525823,342523552,-1338406651,-399705108,-696713950,b,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},\nfunction(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)},\n-980602034,356396192,null,-1617525823,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n-1029864222,-641353250,-1681901809,-1391247867,1707415199,-1957855835,b,function(){for(var d=64,e=[];++d-e.length-32;)switch(d){case 58:d=96;continue;case 91:d=44;break;case 65:d=47;continue;case 46:d=153;case 123:d-=58;default:e.push(String.fromCharCode(d))}return e},\n-1936558978,-1505243983,function(d){d.reverse()},\n1296889058,-1813915420,-943019300,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n\"join\",b,-2061642263];c[21]=c;c[22]=c;c[33]=c;try{c[3](c[33],c[9]),c[29](c[22],c[25]),c[29](c[22],c[19]),c[29](c[33],c[17]),c[29](c[21],c[2]),c[29](c[42],c[10]),c[1](c[52],c[40]),c[12](c[28],c[8]),c[29](c[21],c[45]),c[1](c[21],c[48]),c[44](c[26]),c[39](c[5],c[2]),c[31](c[53],c[16]),c[30](c[29],c[8]),c[51](c[29],c[6],c[44]()),c[4](c[43],c[1]),c[2](c[23],c[42]),c[2](c[0],c[46]),c[38](c[14],c[52]),c[32](c[5]),c[26](c[29],c[46]),c[26](c[5],c[13]),c[28](c[1],c[37]),c[26](c[31],c[13]),c[26](c[1],c[34]),\nc[46](c[1],c[32],c[40]()),c[26](c[50],c[44]),c[17](c[50],c[51]),c[0](c[3],c[24]),c[32](c[13]),c[43](c[3],c[51]),c[0](c[34],c[17]),c[16](c[45],c[53]),c[29](c[44],c[13]),c[42](c[1],c[50]),c[47](c[22],c[53]),c[37](c[22]),c[13](c[52],c[21]),c[6](c[43],c[34]),c[6](c[31],c[46])}catch(d){return\"enhanced_except_gZYB_un-_w8_\"+a}return b.join(\"\")};var deobf_nsig=Ska;".to_owned(), + sts: "19201".to_owned(), } + }); #[rstest] #[case::desktop("desktop")] @@ -633,13 +633,13 @@ mod tests { #[case::tv_html5_embed("tvhtml5embed")] #[case::android("android")] #[case::ios("ios")] - fn map_player_data(#[case] name: &str, deobf: Deobfuscator) { + fn map_player_data(#[case] name: &str) { let json_path = path!("testfiles" / "player" / format!("{name}_video.json")); let json_file = File::open(json_path).unwrap(); let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res = resp - .map_response("pPvd8UxmSbQ", Language::En, Some(&deobf)) + .map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA)) .unwrap(); assert!( @@ -661,10 +661,11 @@ mod tests { }); } - #[rstest] - fn cipher_to_url(deobf: Deobfuscator) { + #[test] + 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 deobf = Deobfuscator::new(&DEOBF_DATA).unwrap(); let map_res = map_url( &None, &Some(signature_cipher.to_owned()), diff --git a/src/client/playlist.rs b/src/client/playlist.rs index bd60bc9..e14c98a 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -3,10 +3,8 @@ use std::{borrow::Cow, convert::TryFrom}; use time::OffsetDateTime; use crate::{ - deobfuscate::Deobfuscator, error::{Error, ExtractionError}, model::{paginator::Paginator, ChannelId, Playlist, PlaylistVideo}, - param::Language, timeago, util::{self, TryRemove}, }; @@ -60,8 +58,8 @@ impl MapResponse for response::Playlist { fn map_response( self, id: &str, - lang: Language, - _deobf: Option<&Deobfuscator>, + lang: crate::param::Language, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let (contents, header) = match (self.contents, self.header) { (Some(contents), Some(header)) => (contents, header), @@ -183,8 +181,8 @@ impl MapResponse> for response::PlaylistCont { fn map_response( self, _id: &str, - _lang: Language, - _deobf: Option<&Deobfuscator>, + _lang: crate::param::Language, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let mut actions = self.on_response_received_actions; let action = actions @@ -235,6 +233,8 @@ mod tests { use path_macro::path; use rstest::rstest; + use crate::param::Language; + use super::*; #[rstest] diff --git a/src/client/search.rs b/src/client/search.rs index 15b1988..475f9f4 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -3,10 +3,9 @@ use std::borrow::Cow; use serde::{de::IgnoredAny, Serialize}; use crate::{ - deobfuscate::Deobfuscator, error::{Error, ExtractionError}, model::{paginator::Paginator, SearchResult, YouTubeItem}, - param::{search_filter::SearchFilter, Language}, + param::search_filter::SearchFilter, }; use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext}; @@ -97,8 +96,8 @@ impl MapResponse for response::Search { fn map_response( self, _id: &str, - lang: Language, - _deobf: Option<&Deobfuscator>, + lang: crate::param::Language, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let items = self .contents diff --git a/src/client/trends.rs b/src/client/trends.rs index 6a0fba9..043086a 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -53,7 +53,7 @@ impl MapResponse> for response::Startpage { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let mut contents = self.contents.two_column_browse_results_renderer.tabs; let grid = contents @@ -77,7 +77,7 @@ impl MapResponse> for response::Trending { self, _id: &str, lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let mut contents = self.contents.two_column_browse_results_renderer.tabs; let items = contents diff --git a/src/client/url_resolver.rs b/src/client/url_resolver.rs index c3caa64..19d2fd0 100644 --- a/src/client/url_resolver.rs +++ b/src/client/url_resolver.rs @@ -301,7 +301,7 @@ impl MapResponse for response::ResolvedUrl { self, _id: &str, _lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let browse_endpoint = self .endpoint diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 55830cf..732827f 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -82,7 +82,7 @@ impl MapResponse for response::VideoDetails { self, id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { let mut warnings = Vec::new(); @@ -367,7 +367,7 @@ impl MapResponse> for response::VideoComments { self, _id: &str, lang: Language, - _deobf: Option<&crate::deobfuscate::Deobfuscator>, + _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { let received_endpoints = self.on_response_received_endpoints; let mut warnings = received_endpoints.warnings; diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index 26f65b4..fd59501 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -9,7 +9,6 @@ use crate::{error::DeobfError, util}; type Result = core::result::Result; pub struct Deobfuscator { - data: DeobfData, ctx: quick_js::Context, } @@ -42,13 +41,13 @@ impl DeobfData { } impl Deobfuscator { - pub fn new(data: DeobfData) -> Result { + pub fn new(data: &DeobfData) -> Result { let ctx = quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; ctx.eval(&data.sig_fn)?; ctx.eval(&data.nsig_fn)?; - Ok(Self { data, ctx }) + Ok(Self { ctx }) } pub fn deobfuscate_sig(&self, sig: &str) -> Result { @@ -74,14 +73,6 @@ impl Deobfuscator { }, ) } - - pub fn get_sts(&self) -> String { - self.data.sts.to_owned() - } - - pub fn get_data(&self) -> DeobfData { - self.data.to_owned() - } } const DEOBF_SIG_FUNC_NAME: &str = "deobf_sig"; @@ -322,7 +313,7 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c #[fixture] fn deobf() -> Deobfuscator { - Deobfuscator::new(DeobfData { + Deobfuscator::new(&DeobfData { js_url: String::default(), sig_fn: SIG_DEOBF_FUNC.to_owned(), nsig_fn: NSIG_DEOBF_FUNC.to_owned(), @@ -406,7 +397,7 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c fn t_update() { let client = Client::new(); let deobf_data = tokio_test::block_on(DeobfData::download(client)).unwrap(); - let deobf = Deobfuscator::new(deobf_data).unwrap(); + let deobf = Deobfuscator::new(&deobf_data).unwrap(); let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap(); println!("{deobf_sig}"); From 49148711e004cbe57f04415b28910cd964078154 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 8 Feb 2023 16:51:50 +0100 Subject: [PATCH 2/5] fix: ignore extraction error when fetching album playlist --- src/client/music_playlist.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index ca3c6cf..107a0b8 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -75,23 +75,27 @@ impl RustyPipeQuery { .collect::>(); if !to_replace.is_empty() { - let playlist = self.music_playlist(playlist_id).await?; - - for (i, title) in to_replace { - let found_track = playlist.tracks.items.iter().find_map(|track| { - if track.name == title && !track.is_video { - Some((track.id.to_owned(), track.duration)) - } else { - None + match self.music_playlist(playlist_id).await { + Ok(playlist) => { + for (i, title) in to_replace { + let found_track = playlist.tracks.items.iter().find_map(|track| { + if track.name == title && !track.is_video { + Some((track.id.to_owned(), track.duration)) + } else { + None + } + }); + if let Some((track_id, duration)) = found_track { + album.tracks[i].id = track_id; + if let Some(duration) = duration { + album.tracks[i].duration = Some(duration); + } + album.tracks[i].is_video = false; + } } - }); - if let Some((track_id, duration)) = found_track { - album.tracks[i].id = track_id; - if let Some(duration) = duration { - album.tracks[i].duration = Some(duration); - } - album.tracks[i].is_video = false; } + Err(Error::Extraction(_)) => {} + Err(e) => return Err(e), } } } From f98c85b38510f47379808c1953ee549ee2a6cabc Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 8 Feb 2023 16:54:35 +0100 Subject: [PATCH 3/5] fix: parsing rss feeds of empty channels --- src/client/response/channel_rss.rs | 1 + tests/youtube.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/client/response/channel_rss.rs b/src/client/response/channel_rss.rs index b2a963a..2ce9859 100644 --- a/src/client/response/channel_rss.rs +++ b/src/client/response/channel_rss.rs @@ -11,6 +11,7 @@ pub(crate) struct ChannelRss { pub author: Author, #[serde(rename = "published", with = "time::serde::rfc3339")] pub create_date: OffsetDateTime, + #[serde(default)] pub entry: Vec, } diff --git a/tests/youtube.rs b/tests/youtube.rs index 4eadd4c..98232c3 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -996,6 +996,17 @@ mod channel_rss { assert!(!channel.videos.is_empty()); } + #[rstest] + fn get_channel_rss_empty(rp: RustyPipe) { + let channel = + tokio_test::block_on(rp.query().channel_rss("UC4fJNIVEOQ1fk15B_sqoOqg")).unwrap(); + + assert_eq!(channel.id, "UC4fJNIVEOQ1fk15B_sqoOqg"); + assert_eq!(channel.name, "Bilal Saeed - Topic"); + + assert!(channel.videos.is_empty()); + } + #[rstest] fn get_channel_rss_not_found(rp: RustyPipe) { let err = From 3574a44b7783b61b65485e28200054ab7f946bd5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 8 Feb 2023 21:38:58 +0100 Subject: [PATCH 4/5] fix: handle empty artists --- src/client/music_artist.rs | 17 +++++++++-------- src/client/response/music_artist.rs | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index d11f863..b0efdf4 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -191,14 +191,15 @@ fn map_artist_page( } } - let mut content = res.contents.single_column_browse_results_renderer.contents; - let sections = content - .try_swap_remove(0) - .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))? - .tab_renderer - .content - .section_list_renderer - .contents; + let sections = res + .contents + .single_column_browse_results_renderer + .contents + .into_iter() + .next() + .and_then(|tab| tab.tab_renderer.content) + .map(|c| c.section_list_renderer.contents) + .unwrap_or_default(); let mut mapper = MusicListMapper::with_artist( lang, diff --git a/src/client/response/music_artist.rs b/src/client/response/music_artist.rs index e27f3a0..c886105 100644 --- a/src/client/response/music_artist.rs +++ b/src/client/response/music_artist.rs @@ -14,7 +14,7 @@ use super::{ #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicArtist { - pub contents: SingleColumnBrowseResult>>, + pub contents: SingleColumnBrowseResult>>>, pub header: Header, } From d0a8b6fabe9c262a89f162076712bb794a105cd3 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 9 Feb 2023 00:30:22 +0100 Subject: [PATCH 5/5] fix: rewritten album mv replacement --- src/client/mod.rs | 8 + src/client/music_artist.rs | 11 +- src/client/music_genres.rs | 14 +- src/client/music_playlist.rs | 42 +- src/client/playlist.rs | 28 +- tests/snapshots/youtube__music_album_ep.snap | 2 +- .../youtube__music_album_tn_zero.snap | 780 ++++++++++++++++++ tests/youtube.rs | 1 + 8 files changed, 841 insertions(+), 45 deletions(-) create mode 100644 tests/snapshots/youtube__music_album_tn_zero.snap diff --git a/src/client/mod.rs b/src/client/mod.rs index d94a856..0615b6a 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -146,6 +146,14 @@ struct QBrowse<'a> { browse_id: &'a str, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct QBrowseParams<'a> { + context: YTContext<'a>, + browse_id: &'a str, + params: &'a str, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct QContinuation<'a> { diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index b0efdf4..218ec04 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -3,7 +3,6 @@ use std::{borrow::Cow, rc::Rc}; use futures::{stream, StreamExt}; use once_cell::sync::Lazy; use regex::Regex; -use serde::Serialize; use crate::{ error::{Error, ExtractionError}, @@ -14,17 +13,9 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::PageType}, - ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext, + ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, }; -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct QBrowseParams<'a> { - context: YTContext<'a>, - browse_id: &'a str, - params: &'a str, -} - impl RustyPipeQuery { /// Get a YouTube Music artist page /// diff --git a/src/client/music_genres.rs b/src/client/music_genres.rs index 30877ad..380b4b0 100644 --- a/src/client/music_genres.rs +++ b/src/client/music_genres.rs @@ -1,7 +1,5 @@ use std::borrow::Cow; -use serde::Serialize; - use crate::{ error::{Error, ExtractionError}, model::{MusicGenre, MusicGenreItem, MusicGenreSection}, @@ -10,17 +8,9 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper}, - ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext, + ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, }; -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct QGenre<'a> { - context: YTContext<'a>, - browse_id: &'a str, - params: &'a str, -} - impl RustyPipeQuery { /// Get a list of moods and genres from YouTube Music pub async fn music_genres(&self) -> Result, Error> { @@ -44,7 +34,7 @@ impl RustyPipeQuery { pub async fn music_genre>(&self, genre_id: S) -> Result { let genre_id = genre_id.as_ref(); let context = self.get_context(ClientType::DesktopMusic, true, None).await; - let request_body = QGenre { + let request_body = QBrowseParams { context, browse_id: "FEmusic_moods_and_genres_category", params: genre_id, diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 107a0b8..3a646ca 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -57,6 +57,19 @@ impl RustyPipeQuery { ) .await?; + // In rare cases, albums may have track numbers =0 (example: MPREb_RM0QfZ0eSKL) + // They should be replaced with the track number derived from the previous track. + let mut n_prev = 0; + for track in album.tracks.iter_mut() { + let tn = track.track_nr.unwrap_or_default(); + if tn == 0 { + n_prev += 1; + track.track_nr = Some(n_prev); + } else { + n_prev = tn; + } + } + // YouTube Music is replacing album tracks with their respective music videos. To get the original // tracks, we have to fetch the album as a playlist and replace the offending track ids. if let Some(playlist_id) = &album.playlist_id { @@ -67,7 +80,7 @@ impl RustyPipeQuery { .enumerate() .filter_map(|(i, track)| { if track.is_video { - Some((i, track.name.to_owned())) + track.track_nr.map(|n| (i, n)) } else { None } @@ -75,27 +88,14 @@ impl RustyPipeQuery { .collect::>(); if !to_replace.is_empty() { - match self.music_playlist(playlist_id).await { - Ok(playlist) => { - for (i, title) in to_replace { - let found_track = playlist.tracks.items.iter().find_map(|track| { - if track.name == title && !track.is_video { - Some((track.id.to_owned(), track.duration)) - } else { - None - } - }); - if let Some((track_id, duration)) = found_track { - album.tracks[i].id = track_id; - if let Some(duration) = duration { - album.tracks[i].duration = Some(duration); - } - album.tracks[i].is_video = false; - } - } + let playlist = self.playlist_w_unavail(playlist_id).await?; + + for (i, track_n) in to_replace { + if let Some(t) = playlist.videos.items.get(track_n as usize - 1) { + album.tracks[i].id = t.id.to_owned(); + album.tracks[i].duration = Some(t.length); + album.tracks[i].is_video = false; } - Err(Error::Extraction(_)) => {} - Err(e) => return Err(e), } } } diff --git a/src/client/playlist.rs b/src/client/playlist.rs index e14c98a..fae3622 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -9,7 +9,10 @@ use crate::{ util::{self, TryRemove}, }; -use super::{response, ClientType, MapResponse, MapResult, QBrowse, QContinuation, RustyPipeQuery}; +use super::{ + response, ClientType, MapResponse, MapResult, QBrowse, QBrowseParams, QContinuation, + RustyPipeQuery, +}; impl RustyPipeQuery { /// Get a YouTube playlist @@ -31,6 +34,29 @@ impl RustyPipeQuery { .await } + /// Get a YouTube playlist including unavailable tracks + pub(crate) async fn playlist_w_unavail>( + &self, + playlist_id: S, + ) -> Result { + let playlist_id = playlist_id.as_ref(); + let context = self.get_context(ClientType::Desktop, true, None).await; + let request_body = QBrowseParams { + context, + browse_id: &format!("VL{playlist_id}"), + params: "wgYCCAA%3D", + }; + + self.execute_request::( + ClientType::Desktop, + "playlist", + playlist_id, + "browse", + &request_body, + ) + .await + } + /// Get more playlist items using the given continuation token pub async fn playlist_continuation>( &self, diff --git a/tests/snapshots/youtube__music_album_ep.snap b/tests/snapshots/youtube__music_album_ep.snap index 161096d..ad3d47b 100644 --- a/tests/snapshots/youtube__music_album_ep.snap +++ b/tests/snapshots/youtube__music_album_ep.snap @@ -43,7 +43,7 @@ MusicAlbum( TrackItem( id: "Jz-26iiDuYs", name: "Waldbrand", - duration: Some(208), + duration: Some(209), cover: [], artists: [ ArtistId( diff --git a/tests/snapshots/youtube__music_album_tn_zero.snap b/tests/snapshots/youtube__music_album_tn_zero.snap new file mode 100644 index 0000000..c4e7f6e --- /dev/null +++ b/tests/snapshots/youtube__music_album_tn_zero.snap @@ -0,0 +1,780 @@ +--- +source: tests/youtube.rs +expression: album +--- +MusicAlbum( + id: "MPREb_RM0QfZ0eSKL", + playlist_id: Some("OLAK5uy_kJpQ8rrI50kwRV-FTS92jdE-RAkUnFFTc"), + name: "Wake Your Mind (Deluxe Edition)", + cover: "[cover]", + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + description: Some("Wake Your Mind is the fifth studio album by German Trance duo Cosmic Gate. It was released on October 24, 2011 as a digital release on Beatport and October 31, 2011 on all other digital retailers.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/Wake_Your_Mind) under Creative Commons Attribution CC-BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/legalcode)"), + album_type: Album, + year: Some(2011), + by_va: false, + tracks: [ + TrackItem( + id: "i2BXHjoK6Pc", + name: "Sometimes They Come Back for More", + duration: Some(448), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCeaHkytFYZHuP_5My8uhaRQ"), + name: "Arnej", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(1), + by_va: false, + ), + TrackItem( + id: "HbjCfOa8P5Y", + name: "Be Your Sound", + duration: Some(252), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU-OklRKmlSN9FUhyvwkylg"), + name: "Emma Hewitt", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(2), + by_va: false, + ), + TrackItem( + id: "qRicdCPpo9Q", + name: "Wake Your Mind", + duration: Some(365), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCc7OiFZMRwpZXRJ6jQas2pg"), + name: "Cary Brothers", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(3), + by_va: false, + ), + TrackItem( + id: "Sdvmezb4uTw", + name: "The Theme", + duration: Some(260), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(4), + by_va: false, + ), + TrackItem( + id: "hNDXx4vaoKs", + name: "All Around You", + duration: Some(334), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UC5hgy_aZwBDZLfxrMHFoD_Q"), + name: "Aruna", + ), + ArtistId( + id: Some("UCq346_97fIcWXPiGOtqLPtg"), + name: "Shane 54", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(5), + by_va: false, + ), + TrackItem( + id: "qB1Y4-O9MRM", + name: "Never Apart", + duration: Some(330), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCkWEAKM8DvaE0dEqPgxK-3Q"), + name: "Alana Aldea", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(6), + by_va: false, + ), + TrackItem( + id: "bZkY60_Ohvs", + name: "Over the Rainbow", + duration: Some(293), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate and J\'Something", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(7), + by_va: false, + ), + TrackItem( + id: "znQfnmObaDg", + name: "Nothing Ever Lasts", + duration: Some(368), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UC2-3iA9cVO3zmfqBpI_9SAw"), + name: "Andrew Bayer", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(8), + by_va: false, + ), + TrackItem( + id: "Mu0HlrLCP44", + name: "Calm Down", + duration: Some(337), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU-OklRKmlSN9FUhyvwkylg"), + name: "Emma Hewitt", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(9), + by_va: false, + ), + TrackItem( + id: "87L2Lqeaz4Y", + name: "Barra", + duration: Some(222), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(10), + by_va: false, + ), + TrackItem( + id: "ZPrAwsjeUBo", + name: "Drifting Away", + duration: Some(336), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU22cXBIulEYuIjQ3ez7Cbg"), + name: "Cathy Burton", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(11), + by_va: false, + ), + TrackItem( + id: "Y9FknSw3x6U", + name: "Flying Blind", + duration: Some(365), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCYMm13OXcL9llzuervQ1_Ig"), + name: "JES", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(12), + by_va: false, + ), + TrackItem( + id: "w9SUevHpYaU", + name: "Perfect Stranger", + duration: Some(331), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(13), + by_va: false, + ), + TrackItem( + id: "3UweyDiE1Og", + name: "Beautiful Destruction", + duration: Some(361), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCkWEAKM8DvaE0dEqPgxK-3Q"), + name: "Alana Aldea", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(14), + by_va: false, + ), + TrackItem( + id: "huS3sgQ7ZiI", + name: "Free Falling [Barra]", + duration: Some(226), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UC5hgy_aZwBDZLfxrMHFoD_Q"), + name: "Aruna", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(15), + by_va: false, + ), + TrackItem( + id: "PbTuejdARwQ", + name: "Sometimes They Come Back for More (Stoneface & Terminal Remix)", + duration: Some(463), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate and Arnej", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(16), + by_va: false, + ), + TrackItem( + id: "B1u98t6fwjs", + name: "Be Your Sound (Orjan Nilsen Remix)", + duration: Some(537), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU-OklRKmlSN9FUhyvwkylg"), + name: "Emma Hewitt", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(17), + by_va: false, + ), + TrackItem( + id: "npQDHZh3xps", + name: "Wake Your Mind (Tritonal Remix)", + duration: Some(416), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCc7OiFZMRwpZXRJ6jQas2pg"), + name: "Cary Brothers", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(18), + by_va: false, + ), + TrackItem( + id: "mbgIthuB8dY", + name: "The Blue Theme (Ferry Corsten Fix)", + duration: Some(446), + cover: [], + artists: [ + ArtistId( + id: Some("UC53Zmeku4tigP7KwAHxWX8A"), + name: "System F", + ), + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC53Zmeku4tigP7KwAHxWX8A"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(19), + by_va: false, + ), + TrackItem( + id: "uMfVA0Atofk", + name: "All Around You (Alexander Popov Remix)", + duration: Some(437), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCq346_97fIcWXPiGOtqLPtg"), + name: "Shane 54", + ), + ArtistId( + id: Some("UC5hgy_aZwBDZLfxrMHFoD_Q"), + name: "Aruna", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(20), + by_va: false, + ), + TrackItem( + id: "W_8gRFJOLsY", + name: "Never Apart (Steve Brian Remix)", + duration: Some(406), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCkWEAKM8DvaE0dEqPgxK-3Q"), + name: "Alana Aldea", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(21), + by_va: false, + ), + TrackItem( + id: "p_0jK0XDrg8", + name: "Over the Rainbow (W&W Remix)", + duration: Some(339), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCx6EvdNe0luLrC_Rj8Wk10w"), + name: "J’Something", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(22), + by_va: false, + ), + TrackItem( + id: "CdCjMlyjpAg", + name: "Nothing Ever Lasts (Nitrous Oxide Remix)", + duration: Some(455), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate and Andrew Bayer", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(23), + by_va: false, + ), + TrackItem( + id: "c7yShI25Y-Q", + name: "Calm Down (Omnia Remix)", + duration: Some(391), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU-OklRKmlSN9FUhyvwkylg"), + name: "Emma Hewitt", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(24), + by_va: false, + ), + TrackItem( + id: "gJB7xwuvREs", + name: "Drifting Away (Faruk Sabanci Remix)", + duration: Some(393), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ArtistId( + id: Some("UCU22cXBIulEYuIjQ3ez7Cbg"), + name: "Cathy Burton", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(25), + by_va: false, + ), + TrackItem( + id: "lfOhL0ah0lw", + name: "Flying Blind (Tom Fall Remix)", + duration: Some(469), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate and JES", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(26), + by_va: false, + ), + TrackItem( + id: "ilrtbPk2RE8", + name: "Perfect Stranger (Wezz Devall Remix)", + duration: Some(404), + cover: [], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album: Some(AlbumId( + id: "MPREb_RM0QfZ0eSKL", + name: "Wake Your Mind (Deluxe Edition)", + )), + view_count: None, + is_video: false, + track_nr: Some(27), + by_va: false, + ), + ], + variants: [ + AlbumItem( + id: "MPREb_75NZMCMZQW4", + name: "Wake Your Mind", + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/gta0XN_TQLselp1ymFyIACP2_Px4wvoSdI0XKOAWKqlSuYvRGLg9FuKPX0DkJifUYAm7fNJmRpupyvgO=w226-h226-l90-rj", + width: 226, + height: 226, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/gta0XN_TQLselp1ymFyIACP2_Px4wvoSdI0XKOAWKqlSuYvRGLg9FuKPX0DkJifUYAm7fNJmRpupyvgO=w544-h544-l90-rj", + width: 544, + height: 544, + ), + ], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album_type: Album, + year: None, + by_va: false, + ), + AlbumItem( + id: "MPREb_csntSntqO8R", + name: "Wake Your Mind", + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/Rxmu8lBHszFtHGyToeorDBCpT9pmNQBWZLq7KXfxysktTx-ebcrIOBwpfuNbaNtGvrAfTSvAZelB5dXT6w=w226-h226-l90-rj", + width: 226, + height: 226, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/Rxmu8lBHszFtHGyToeorDBCpT9pmNQBWZLq7KXfxysktTx-ebcrIOBwpfuNbaNtGvrAfTSvAZelB5dXT6w=w544-h544-l90-rj", + width: 544, + height: 544, + ), + ], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album_type: Album, + year: None, + by_va: false, + ), + AlbumItem( + id: "MPREb_lidKOifLvXm", + name: "Wake Your Mind (The Extended Mixes)", + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/Odk-iPowyYddbohhb20Zf23qopAWms68hiWS1uHX_ej4Gab0-Dh3ZuhBIdumE6rqk5XD1faZhVBK59lg1Q=w226-h226-l90-rj", + width: 226, + height: 226, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/Odk-iPowyYddbohhb20Zf23qopAWms68hiWS1uHX_ej4Gab0-Dh3ZuhBIdumE6rqk5XD1faZhVBK59lg1Q=w544-h544-l90-rj", + width: 544, + height: 544, + ), + ], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album_type: Album, + year: None, + by_va: false, + ), + AlbumItem( + id: "MPREb_qSDBQBGK1bP", + name: "Wake Your Mind (Deluxe Edition)", + cover: [ + Thumbnail( + url: "https://lh3.googleusercontent.com/R39ek9HrT7nWzVZNj2GUR3owNlbgyT7e-W-5SuPRRpLgbrE_OSTAy70LzLlk42ftNtbRJQYSMrat8VfSFg=w226-h226-l90-rj", + width: 226, + height: 226, + ), + Thumbnail( + url: "https://lh3.googleusercontent.com/R39ek9HrT7nWzVZNj2GUR3owNlbgyT7e-W-5SuPRRpLgbrE_OSTAy70LzLlk42ftNtbRJQYSMrat8VfSFg=w544-h544-l90-rj", + width: 544, + height: 544, + ), + ], + artists: [ + ArtistId( + id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + name: "Cosmic Gate", + ), + ], + artist_id: Some("UC2JiS71Dbgd_4bB4hMKebeg"), + album_type: Album, + year: None, + by_va: false, + ), + ], +) diff --git a/tests/youtube.rs b/tests/youtube.rs index 98232c3..a3ddee6 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -1289,6 +1289,7 @@ fn music_playlist_not_found(rp: RustyPipe) { #[case::no_year("no_year", "MPREb_F3Af9UZZVxX")] #[case::version_no_artist("version_no_artist", "MPREb_h8ltx5oKvyY")] #[case::no_artist("no_artist", "MPREb_bqWA6mAZFWS")] +#[case::tn_zero("tn_zero", "MPREb_RM0QfZ0eSKL")] fn music_album(#[case] name: &str, #[case] id: &str, rp: RustyPipe) { let album = tokio_test::block_on(rp.query().music_album(id)).unwrap();