Compare commits
5 commits
d9e07b37e6
...
d0a8b6fabe
Author | SHA1 | Date | |
---|---|---|---|
d0a8b6fabe | |||
3574a44b77 | |||
f98c85b385 | |||
49148711e0 | |||
ee5e82f6bb |
22 changed files with 935 additions and 132 deletions
|
@ -162,7 +162,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
) -> Result<MapResult<Channel<ChannelInfo>>, ExtractionError> {
|
||||||
let content = map_channel_content(self.contents, self.alerts)?;
|
let content = map_channel_content(self.contents, self.alerts)?;
|
||||||
let channel_data = map_channel(
|
let channel_data = map_channel(
|
||||||
|
|
|
@ -35,7 +35,7 @@ use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::{CacheStorage, FileStorage},
|
cache::{CacheStorage, FileStorage},
|
||||||
deobfuscate::{DeobfData, Deobfuscator},
|
deobfuscate::DeobfData,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
param::{Country, Language},
|
param::{Country, Language},
|
||||||
report::{FileReporter, Level, Report, Reporter},
|
report::{FileReporter, Level, Report, Reporter},
|
||||||
|
@ -146,6 +146,14 @@ struct QBrowse<'a> {
|
||||||
browse_id: &'a str,
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QContinuation<'a> {
|
struct QContinuation<'a> {
|
||||||
|
@ -706,20 +714,19 @@ impl RustyPipe {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiate a new deobfuscator from either cached or extracted YouTube JavaScript code.
|
/// 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
|
// Write lock here to prevent concurrent tasks from fetching the same data
|
||||||
let mut deobf_data = self.inner.cache.deobf.write().await;
|
let mut deobf_data = self.inner.cache.deobf.write().await;
|
||||||
|
|
||||||
match deobf_data.get() {
|
match deobf_data.get() {
|
||||||
Some(deobf_data) => Ok(Deobfuscator::new(deobf_data.clone())?),
|
Some(deobf_data) => Ok(deobf_data.clone()),
|
||||||
None => {
|
None => {
|
||||||
log::debug!("getting deobfuscator");
|
log::debug!("getting deobfuscator");
|
||||||
let data = DeobfData::download(self.inner.http.clone()).await?;
|
let new_data = DeobfData::download(self.inner.http.clone()).await?;
|
||||||
let new_deobf = Deobfuscator::new(data.clone())?;
|
*deobf_data = CacheEntry::from(new_data.clone());
|
||||||
*deobf_data = CacheEntry::from(data);
|
|
||||||
drop(deobf_data);
|
drop(deobf_data);
|
||||||
self.store_cache().await;
|
self.store_cache().await;
|
||||||
Ok(new_deobf)
|
Ok(new_data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1027,7 +1034,7 @@ impl RustyPipeQuery {
|
||||||
id: &str,
|
id: &str,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
body: &B,
|
body: &B,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<M, Error> {
|
) -> Result<M, Error> {
|
||||||
log::debug!("getting {}({})", operation, id);
|
log::debug!("getting {}({})", operation, id);
|
||||||
|
|
||||||
|
@ -1056,7 +1063,7 @@ impl RustyPipeQuery {
|
||||||
operation: format!("{operation}({id})"),
|
operation: format!("{operation}({id})"),
|
||||||
error,
|
error,
|
||||||
msgs,
|
msgs,
|
||||||
deobf_data: deobf.map(Deobfuscator::get_data),
|
deobf_data: deobf.cloned(),
|
||||||
http_request: crate::report::HTTPRequest {
|
http_request: crate::report::HTTPRequest {
|
||||||
url: request_url,
|
url: request_url,
|
||||||
method: "POST".to_string(),
|
method: "POST".to_string(),
|
||||||
|
@ -1197,7 +1204,7 @@ trait MapResponse<T> {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<MapResult<T>, ExtractionError>;
|
) -> Result<MapResult<T>, ExtractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ use std::{borrow::Cow, rc::Rc};
|
||||||
use futures::{stream, StreamExt};
|
use futures::{stream, StreamExt};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
|
@ -14,17 +13,9 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
|
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 {
|
impl RustyPipeQuery {
|
||||||
/// Get a YouTube Music artist page
|
/// Get a YouTube Music artist page
|
||||||
///
|
///
|
||||||
|
@ -145,7 +136,7 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
||||||
let mapped = map_artist_page(self, id, lang, false)?;
|
let mapped = map_artist_page(self, id, lang, false)?;
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
@ -160,7 +151,7 @@ impl MapResponse<(MusicArtist, Vec<String>)> for response::MusicArtist {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> {
|
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> {
|
||||||
map_artist_page(self, id, lang, true)
|
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 = res
|
||||||
let sections = content
|
.contents
|
||||||
.try_swap_remove(0)
|
.single_column_browse_results_renderer
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
.contents
|
||||||
.tab_renderer
|
.into_iter()
|
||||||
.content
|
.next()
|
||||||
.section_list_renderer
|
.and_then(|tab| tab.tab_renderer.content)
|
||||||
.contents;
|
.map(|c| c.section_list_renderer.contents)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::with_artist(
|
let mut mapper = MusicListMapper::with_artist(
|
||||||
lang,
|
lang,
|
||||||
|
@ -328,7 +320,7 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
|
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
|
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
|
||||||
let countries = self
|
let countries = self
|
||||||
.framework_updates
|
.framework_updates
|
||||||
|
|
|
@ -156,7 +156,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
||||||
let tabs = self
|
let tabs = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -232,7 +232,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
||||||
let tabs = self
|
let tabs = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -287,7 +287,7 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_lang: Language,
|
_lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Lyrics>, ExtractionError> {
|
) -> Result<MapResult<Lyrics>, ExtractionError> {
|
||||||
let lyrics = self
|
let lyrics = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -317,7 +317,7 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicRelated>, ExtractionError> {
|
) -> Result<MapResult<MusicRelated>, ExtractionError> {
|
||||||
// Find artist
|
// Find artist
|
||||||
let artist_id = self
|
let artist_id = self
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{MusicGenre, MusicGenreItem, MusicGenreSection},
|
model::{MusicGenre, MusicGenreItem, MusicGenreSection},
|
||||||
|
@ -10,17 +8,9 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, music_item::MusicListMapper},
|
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 {
|
impl RustyPipeQuery {
|
||||||
/// Get a list of moods and genres from YouTube Music
|
/// Get a list of moods and genres from YouTube Music
|
||||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
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> {
|
pub async fn music_genre<S: AsRef<str>>(&self, genre_id: S) -> Result<MusicGenre, Error> {
|
||||||
let genre_id = genre_id.as_ref();
|
let genre_id = genre_id.as_ref();
|
||||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||||
let request_body = QGenre {
|
let request_body = QBrowseParams {
|
||||||
context,
|
context,
|
||||||
browse_id: "FEmusic_moods_and_genres_category",
|
browse_id: "FEmusic_moods_and_genres_category",
|
||||||
params: genre_id,
|
params: genre_id,
|
||||||
|
@ -66,7 +56,7 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_lang: crate::param::Language,
|
_lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||||
let content = self
|
let content = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -119,7 +109,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
|
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.contents
|
.contents
|
||||||
|
|
|
@ -57,6 +57,19 @@ impl RustyPipeQuery {
|
||||||
)
|
)
|
||||||
.await?;
|
.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
|
// 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.
|
// tracks, we have to fetch the album as a playlist and replace the offending track ids.
|
||||||
if let Some(playlist_id) = &album.playlist_id {
|
if let Some(playlist_id) = &album.playlist_id {
|
||||||
|
@ -67,7 +80,7 @@ impl RustyPipeQuery {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, track)| {
|
.filter_map(|(i, track)| {
|
||||||
if track.is_video {
|
if track.is_video {
|
||||||
Some((i, track.name.to_owned()))
|
track.track_nr.map(|n| (i, n))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -75,21 +88,12 @@ impl RustyPipeQuery {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if !to_replace.is_empty() {
|
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 {
|
for (i, track_n) in to_replace {
|
||||||
let found_track = playlist.tracks.items.iter().find_map(|track| {
|
if let Some(t) = playlist.videos.items.get(track_n as usize - 1) {
|
||||||
if track.name == title && !track.is_video {
|
album.tracks[i].id = t.id.to_owned();
|
||||||
Some((track.id.to_owned(), track.duration))
|
album.tracks[i].duration = Some(t.length);
|
||||||
} 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;
|
album.tracks[i].is_video = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +108,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
@ -242,7 +246,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
|
|
@ -230,7 +230,7 @@ impl MapResponse<MusicSearchResult> for response::MusicSearch {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicSearchResult>, crate::error::ExtractionError> {
|
) -> Result<MapResult<MusicSearchResult>, crate::error::ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
@ -284,7 +284,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicSearchFiltered<T>>, ExtractionError> {
|
) -> Result<MapResult<MusicSearchFiltered<T>>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
@ -339,7 +339,7 @@ impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
|
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
|
||||||
let mut mapper = MusicListMapper::new(lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
let mut terms = Vec::new();
|
let mut terms = Vec::new();
|
||||||
|
|
|
@ -98,7 +98,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.on_response_received_actions
|
.on_response_received_actions
|
||||||
|
@ -130,7 +130,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
||||||
let mut mapper = MusicListMapper::new(lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
let mut continuations = Vec::new();
|
let mut continuations = Vec::new();
|
||||||
|
|
|
@ -30,7 +30,7 @@ struct QPlayer<'a> {
|
||||||
context: YTContext<'a>,
|
context: YTContext<'a>,
|
||||||
/// Website playback context
|
/// Website playback context
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
playback_context: Option<QPlaybackContext>,
|
playback_context: Option<QPlaybackContext<'a>>,
|
||||||
/// Content playback nonce (mobile only, 16 random chars)
|
/// Content playback nonce (mobile only, 16 random chars)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
cpn: Option<String>,
|
cpn: Option<String>,
|
||||||
|
@ -44,15 +44,15 @@ struct QPlayer<'a> {
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlaybackContext {
|
struct QPlaybackContext<'a> {
|
||||||
content_playback_context: QContentPlaybackContext,
|
content_playback_context: QContentPlaybackContext<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QContentPlaybackContext {
|
struct QContentPlaybackContext<'a> {
|
||||||
/// Signature timestamp extracted from player.js
|
/// Signature timestamp extracted from player.js
|
||||||
signature_timestamp: String,
|
signature_timestamp: &'a str,
|
||||||
/// Referer URL from website
|
/// Referer URL from website
|
||||||
referer: String,
|
referer: String,
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ impl RustyPipeQuery {
|
||||||
let video_id = video_id.as_ref();
|
let video_id = video_id.as_ref();
|
||||||
let (context, deobf) = tokio::join!(
|
let (context, deobf) = tokio::join!(
|
||||||
self.get_context(client_type, false, None),
|
self.get_context(client_type, false, None),
|
||||||
self.client.get_deobf()
|
self.client.get_deobf_data()
|
||||||
);
|
);
|
||||||
let deobf = deobf?;
|
let deobf = deobf?;
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ impl RustyPipeQuery {
|
||||||
context,
|
context,
|
||||||
playback_context: Some(QPlaybackContext {
|
playback_context: Some(QPlaybackContext {
|
||||||
content_playback_context: QContentPlaybackContext {
|
content_playback_context: QContentPlaybackContext {
|
||||||
signature_timestamp: deobf.get_sts(),
|
signature_timestamp: &deobf.sts,
|
||||||
referer: format!("https://www.youtube.com/watch?v={video_id}"),
|
referer: format!("https://www.youtube.com/watch?v={video_id}"),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -141,9 +141,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
_lang: Language,
|
_lang: Language,
|
||||||
deobf: Option<&Deobfuscator>,
|
deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
|
) -> 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![];
|
let mut warnings = vec![];
|
||||||
|
|
||||||
// Check playability status
|
// Check playability status
|
||||||
|
@ -253,21 +254,21 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
|
|
||||||
match (f.is_video(), f.is_audio()) {
|
match (f.is_video(), f.is_audio()) {
|
||||||
(true, true) => {
|
(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);
|
warnings.append(&mut map_res.warnings);
|
||||||
if let Some(c) = map_res.c {
|
if let Some(c) = map_res.c {
|
||||||
video_streams.push(c);
|
video_streams.push(c);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(true, false) => {
|
(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);
|
warnings.append(&mut map_res.warnings);
|
||||||
if let Some(c) = map_res.c {
|
if let Some(c) = map_res.c {
|
||||||
video_only_streams.push(c);
|
video_only_streams.push(c);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(false, true) => {
|
(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);
|
warnings.append(&mut map_res.warnings);
|
||||||
if let Some(c) = map_res.c {
|
if let Some(c) = map_res.c {
|
||||||
audio_streams.push(c);
|
audio_streams.push(c);
|
||||||
|
@ -612,20 +613,19 @@ mod tests {
|
||||||
use std::{fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
|
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::{fixture, rstest};
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::deobfuscate::DeobfData;
|
use crate::deobfuscate::DeobfData;
|
||||||
|
|
||||||
#[fixture]
|
static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| {
|
||||||
fn deobf() -> Deobfuscator {
|
DeobfData {
|
||||||
Deobfuscator::new(DeobfData {
|
js_url: "https://www.youtube.com/s/player/c8b8a173/player_ias.vflset/en_US/base.js".to_owned(),
|
||||||
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(),
|
||||||
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(),
|
||||||
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(),
|
||||||
sts: "19201".to_owned(),
|
|
||||||
}).unwrap()
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::desktop("desktop")]
|
#[case::desktop("desktop")]
|
||||||
|
@ -633,13 +633,13 @@ mod tests {
|
||||||
#[case::tv_html5_embed("tvhtml5embed")]
|
#[case::tv_html5_embed("tvhtml5embed")]
|
||||||
#[case::android("android")]
|
#[case::android("android")]
|
||||||
#[case::ios("ios")]
|
#[case::ios("ios")]
|
||||||
fn 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_path = path!("testfiles" / "player" / format!("{name}_video.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = resp
|
let map_res = resp
|
||||||
.map_response("pPvd8UxmSbQ", Language::En, Some(&deobf))
|
.map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -661,10 +661,11 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn cipher_to_url(deobf: Deobfuscator) {
|
fn cipher_to_url() {
|
||||||
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";
|
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";
|
||||||
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
|
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
|
||||||
|
let deobf = Deobfuscator::new(&DEOBF_DATA).unwrap();
|
||||||
let map_res = map_url(
|
let map_res = map_url(
|
||||||
&None,
|
&None,
|
||||||
&Some(signature_cipher.to_owned()),
|
&Some(signature_cipher.to_owned()),
|
||||||
|
|
|
@ -3,15 +3,16 @@ use std::{borrow::Cow, convert::TryFrom};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
deobfuscate::Deobfuscator,
|
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, ChannelId, Playlist, PlaylistVideo},
|
model::{paginator::Paginator, ChannelId, Playlist, PlaylistVideo},
|
||||||
param::Language,
|
|
||||||
timeago,
|
timeago,
|
||||||
util::{self, TryRemove},
|
util::{self, TryRemove},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapResponse, MapResult, QBrowse, QContinuation, RustyPipeQuery};
|
use super::{
|
||||||
|
response, ClientType, MapResponse, MapResult, QBrowse, QBrowseParams, QContinuation,
|
||||||
|
RustyPipeQuery,
|
||||||
|
};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a YouTube playlist
|
/// Get a YouTube playlist
|
||||||
|
@ -33,6 +34,29 @@ impl RustyPipeQuery {
|
||||||
.await
|
.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
|
/// Get more playlist items using the given continuation token
|
||||||
pub async fn playlist_continuation<S: AsRef<str>>(
|
pub async fn playlist_continuation<S: AsRef<str>>(
|
||||||
&self,
|
&self,
|
||||||
|
@ -60,8 +84,8 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Playlist>, ExtractionError> {
|
) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||||
let (contents, header) = match (self.contents, self.header) {
|
let (contents, header) = match (self.contents, self.header) {
|
||||||
(Some(contents), Some(header)) => (contents, header),
|
(Some(contents), Some(header)) => (contents, header),
|
||||||
|
@ -183,8 +207,8 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_lang: Language,
|
_lang: crate::param::Language,
|
||||||
_deobf: Option<&Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<PlaylistVideo>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<PlaylistVideo>>, ExtractionError> {
|
||||||
let mut actions = self.on_response_received_actions;
|
let mut actions = self.on_response_received_actions;
|
||||||
let action = actions
|
let action = actions
|
||||||
|
@ -235,6 +259,8 @@ mod tests {
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::param::Language;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub(crate) struct ChannelRss {
|
||||||
pub author: Author,
|
pub author: Author,
|
||||||
#[serde(rename = "published", with = "time::serde::rfc3339")]
|
#[serde(rename = "published", with = "time::serde::rfc3339")]
|
||||||
pub create_date: OffsetDateTime,
|
pub create_date: OffsetDateTime,
|
||||||
|
#[serde(default)]
|
||||||
pub entry: Vec<Entry>,
|
pub entry: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use super::{
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct MusicArtist {
|
pub(crate) struct MusicArtist {
|
||||||
pub contents: SingleColumnBrowseResult<Tab<SectionList<ItemSection>>>,
|
pub contents: SingleColumnBrowseResult<Tab<Option<SectionList<ItemSection>>>>,
|
||||||
pub header: Header,
|
pub header: Header,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,9 @@ use std::borrow::Cow;
|
||||||
use serde::{de::IgnoredAny, Serialize};
|
use serde::{de::IgnoredAny, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
deobfuscate::Deobfuscator,
|
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{paginator::Paginator, SearchResult, YouTubeItem},
|
model::{paginator::Paginator, SearchResult, YouTubeItem},
|
||||||
param::{search_filter::SearchFilter, Language},
|
param::search_filter::SearchFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||||
|
@ -97,8 +96,8 @@ impl MapResponse<SearchResult> for response::Search {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<SearchResult>, ExtractionError> {
|
) -> Result<MapResult<SearchResult>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.contents
|
.contents
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
||||||
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
|
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
|
||||||
let grid = contents
|
let grid = contents
|
||||||
|
@ -77,7 +77,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: crate::param::Language,
|
lang: crate::param::Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
||||||
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
|
let mut contents = self.contents.two_column_browse_results_renderer.tabs;
|
||||||
let items = contents
|
let items = contents
|
||||||
|
|
|
@ -301,7 +301,7 @@ impl MapResponse<UrlTarget> for response::ResolvedUrl {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_lang: Language,
|
_lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||||
let browse_endpoint = self
|
let browse_endpoint = self
|
||||||
.endpoint
|
.endpoint
|
||||||
|
|
|
@ -82,7 +82,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
self,
|
self,
|
||||||
id: &str,
|
id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
self,
|
self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
||||||
let received_endpoints = self.on_response_received_endpoints;
|
let received_endpoints = self.on_response_received_endpoints;
|
||||||
let mut warnings = received_endpoints.warnings;
|
let mut warnings = received_endpoints.warnings;
|
||||||
|
|
|
@ -9,7 +9,6 @@ use crate::{error::DeobfError, util};
|
||||||
type Result<T> = core::result::Result<T, DeobfError>;
|
type Result<T> = core::result::Result<T, DeobfError>;
|
||||||
|
|
||||||
pub struct Deobfuscator {
|
pub struct Deobfuscator {
|
||||||
data: DeobfData,
|
|
||||||
ctx: quick_js::Context,
|
ctx: quick_js::Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +41,13 @@ impl DeobfData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deobfuscator {
|
impl Deobfuscator {
|
||||||
pub fn new(data: DeobfData) -> Result<Self> {
|
pub fn new(data: &DeobfData) -> Result<Self> {
|
||||||
let ctx =
|
let ctx =
|
||||||
quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?;
|
quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?;
|
||||||
ctx.eval(&data.sig_fn)?;
|
ctx.eval(&data.sig_fn)?;
|
||||||
ctx.eval(&data.nsig_fn)?;
|
ctx.eval(&data.nsig_fn)?;
|
||||||
|
|
||||||
Ok(Self { data, ctx })
|
Ok(Self { ctx })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
|
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";
|
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]
|
#[fixture]
|
||||||
fn deobf() -> Deobfuscator {
|
fn deobf() -> Deobfuscator {
|
||||||
Deobfuscator::new(DeobfData {
|
Deobfuscator::new(&DeobfData {
|
||||||
js_url: String::default(),
|
js_url: String::default(),
|
||||||
sig_fn: SIG_DEOBF_FUNC.to_owned(),
|
sig_fn: SIG_DEOBF_FUNC.to_owned(),
|
||||||
nsig_fn: NSIG_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() {
|
fn t_update() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let deobf_data = tokio_test::block_on(DeobfData::download(client)).unwrap();
|
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();
|
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
||||||
println!("{deobf_sig}");
|
println!("{deobf_sig}");
|
||||||
|
|
|
@ -43,7 +43,7 @@ MusicAlbum(
|
||||||
TrackItem(
|
TrackItem(
|
||||||
id: "Jz-26iiDuYs",
|
id: "Jz-26iiDuYs",
|
||||||
name: "Waldbrand",
|
name: "Waldbrand",
|
||||||
duration: Some(208),
|
duration: Some(209),
|
||||||
cover: [],
|
cover: [],
|
||||||
artists: [
|
artists: [
|
||||||
ArtistId(
|
ArtistId(
|
||||||
|
|
780
tests/snapshots/youtube__music_album_tn_zero.snap
Normal file
780
tests/snapshots/youtube__music_album_tn_zero.snap
Normal 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: "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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
|
@ -996,6 +996,17 @@ mod channel_rss {
|
||||||
assert!(!channel.videos.is_empty());
|
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]
|
#[rstest]
|
||||||
fn get_channel_rss_not_found(rp: RustyPipe) {
|
fn get_channel_rss_not_found(rp: RustyPipe) {
|
||||||
let err =
|
let err =
|
||||||
|
@ -1278,6 +1289,7 @@ fn music_playlist_not_found(rp: RustyPipe) {
|
||||||
#[case::no_year("no_year", "MPREb_F3Af9UZZVxX")]
|
#[case::no_year("no_year", "MPREb_F3Af9UZZVxX")]
|
||||||
#[case::version_no_artist("version_no_artist", "MPREb_h8ltx5oKvyY")]
|
#[case::version_no_artist("version_no_artist", "MPREb_h8ltx5oKvyY")]
|
||||||
#[case::no_artist("no_artist", "MPREb_bqWA6mAZFWS")]
|
#[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) {
|
fn music_album(#[case] name: &str, #[case] id: &str, rp: RustyPipe) {
|
||||||
let album = tokio_test::block_on(rp.query().music_album(id)).unwrap();
|
let album = tokio_test::block_on(rp.query().music_album(id)).unwrap();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue