Compare commits

..

5 commits

22 changed files with 935 additions and 132 deletions

View file

@ -162,7 +162,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
let content = map_channel_content(self.contents, self.alerts)?;
@ -202,7 +202,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
let content = map_channel_content(self.contents, self.alerts)?;
@ -236,7 +236,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
let content = map_channel_content(self.contents, self.alerts)?;
let channel_data = map_channel(

View file

@ -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},
@ -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> {
@ -706,20 +714,19 @@ impl RustyPipe {
}
/// Instantiate a new deobfuscator from either cached or extracted YouTube JavaScript code.
async fn get_deobf(&self) -> Result<Deobfuscator, Error> {
async fn get_deobf_data(&self) -> Result<DeobfData, Error> {
// 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 +1034,7 @@ impl RustyPipeQuery {
id: &str,
endpoint: &str,
body: &B,
deobf: Option<&Deobfuscator>,
deobf: Option<&DeobfData>,
) -> Result<M, Error> {
log::debug!("getting {}({})", operation, id);
@ -1056,7 +1063,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 +1204,7 @@ trait MapResponse<T> {
self,
id: &str,
lang: Language,
deobf: Option<&Deobfuscator>,
deobf: Option<&DeobfData>,
) -> Result<MapResult<T>, ExtractionError>;
}

View file

@ -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
///
@ -145,7 +136,7 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicArtist>, ExtractionError> {
let mapped = map_artist_page(self, id, lang, false)?;
Ok(MapResult {
@ -160,7 +151,7 @@ impl MapResponse<(MusicArtist, Vec<String>)> for response::MusicArtist {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> {
map_artist_page(self, id, lang, true)
}
@ -191,14 +182,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,
@ -328,7 +320,7 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
// dbg!(&self);

View file

@ -59,7 +59,7 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
let countries = self
.framework_updates

View file

@ -156,7 +156,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<TrackDetails>, ExtractionError> {
let tabs = self
.contents
@ -232,7 +232,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
self,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
let tabs = self
.contents
@ -287,7 +287,7 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
self,
_id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Lyrics>, ExtractionError> {
let lyrics = self
.contents
@ -317,7 +317,7 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
self,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicRelated>, ExtractionError> {
// Find artist
let artist_id = self

View file

@ -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<Vec<MusicGenreItem>, Error> {
@ -44,7 +34,7 @@ impl RustyPipeQuery {
pub async fn music_genre<S: AsRef<str>>(&self, genre_id: S) -> Result<MusicGenre, Error> {
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,
@ -66,7 +56,7 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
self,
_id: &str,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
let content = self
.contents
@ -119,7 +109,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
// dbg!(&self);

View file

@ -51,7 +51,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
let items = self
.contents

View file

@ -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,21 +88,12 @@ impl RustyPipeQuery {
.collect::<Vec<_>>();
if !to_replace.is_empty() {
let playlist = self.music_playlist(playlist_id).await?;
let playlist = self.playlist_w_unavail(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
}
});
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);
}
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;
}
}
@ -104,7 +108,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
// dbg!(&self);
@ -242,7 +246,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
// dbg!(&self);

View file

@ -230,7 +230,7 @@ impl MapResponse<MusicSearchResult> for response::MusicSearch {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicSearchResult>, crate::error::ExtractionError> {
// dbg!(&self);
@ -284,7 +284,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicSearchFiltered<T>>, ExtractionError> {
// dbg!(&self);
@ -339,7 +339,7 @@ impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
let mut mapper = MusicListMapper::new(lang);
let mut terms = Vec::new();

View file

@ -98,7 +98,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
let items = self
.on_response_received_actions
@ -130,7 +130,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
let mut mapper = MusicListMapper::new(lang);
let mut continuations = Vec::new();

View file

@ -30,7 +30,7 @@ struct QPlayer<'a> {
context: YTContext<'a>,
/// Website playback context
#[serde(skip_serializing_if = "Option::is_none")]
playback_context: Option<QPlaybackContext>,
playback_context: Option<QPlaybackContext<'a>>,
/// Content playback nonce (mobile only, 16 random chars)
#[serde(skip_serializing_if = "Option::is_none")]
cpn: Option<String>,
@ -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<VideoPlayer> for response::Player {
self,
id: &str,
_lang: Language,
deobf: Option<&Deobfuscator>,
deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<super::MapResult<VideoPlayer>, 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<VideoPlayer> 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<DeobfData> = 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()),

View file

@ -3,15 +3,16 @@ 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},
};
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
@ -33,6 +34,29 @@ impl RustyPipeQuery {
.await
}
/// Get a YouTube playlist including unavailable tracks
pub(crate) async fn playlist_w_unavail<S: AsRef<str>>(
&self,
playlist_id: S,
) -> Result<Playlist, Error> {
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::<response::Playlist, _, _>(
ClientType::Desktop,
"playlist",
playlist_id,
"browse",
&request_body,
)
.await
}
/// Get more playlist items using the given continuation token
pub async fn playlist_continuation<S: AsRef<str>>(
&self,
@ -60,8 +84,8 @@ impl MapResponse<Playlist> for response::Playlist {
fn map_response(
self,
id: &str,
lang: Language,
_deobf: Option<&Deobfuscator>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Playlist>, ExtractionError> {
let (contents, header) = match (self.contents, self.header) {
(Some(contents), Some(header)) => (contents, header),
@ -183,8 +207,8 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
fn map_response(
self,
_id: &str,
_lang: Language,
_deobf: Option<&Deobfuscator>,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<PlaylistVideo>>, ExtractionError> {
let mut actions = self.on_response_received_actions;
let action = actions
@ -235,6 +259,8 @@ mod tests {
use path_macro::path;
use rstest::rstest;
use crate::param::Language;
use super::*;
#[rstest]

View file

@ -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<Entry>,
}

View file

@ -14,7 +14,7 @@ use super::{
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtist {
pub contents: SingleColumnBrowseResult<Tab<SectionList<ItemSection>>>,
pub contents: SingleColumnBrowseResult<Tab<Option<SectionList<ItemSection>>>>,
pub header: Header,
}

View file

@ -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<SearchResult> for response::Search {
fn map_response(
self,
_id: &str,
lang: Language,
_deobf: Option<&Deobfuscator>,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<SearchResult>, ExtractionError> {
let items = self
.contents

View file

@ -53,7 +53,7 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
let grid = contents
@ -77,7 +77,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
self,
_id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
let items = contents

View file

@ -301,7 +301,7 @@ impl MapResponse<UrlTarget> for response::ResolvedUrl {
self,
_id: &str,
_lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<UrlTarget>, ExtractionError> {
let browse_endpoint = self
.endpoint

View file

@ -82,7 +82,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<VideoDetails>, ExtractionError> {
let mut warnings = Vec::new();
@ -367,7 +367,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
self,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
let received_endpoints = self.on_response_received_endpoints;
let mut warnings = received_endpoints.warnings;

View file

@ -9,7 +9,6 @@ use crate::{error::DeobfError, util};
type Result<T> = core::result::Result<T, DeobfError>;
pub struct Deobfuscator {
data: DeobfData,
ctx: quick_js::Context,
}
@ -42,13 +41,13 @@ impl DeobfData {
}
impl Deobfuscator {
pub fn new(data: DeobfData) -> Result<Self> {
pub fn new(data: &DeobfData) -> Result<Self> {
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<String> {
@ -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}");

View file

@ -43,7 +43,7 @@ MusicAlbum(
TrackItem(
id: "Jz-26iiDuYs",
name: "Waldbrand",
duration: Some(208),
duration: Some(209),
cover: [],
artists: [
ArtistId(

View file

@ -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: "JSomething",
),
],
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,
),
],
)

View file

@ -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 =
@ -1278,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();