feat: add support for new artist discography page

This commit is contained in:
ThetaDev 2023-05-14 03:05:24 +02:00
parent bf80db8a9a
commit c8e2d342c6
25 changed files with 73368 additions and 91615 deletions

View file

@ -4,7 +4,7 @@ use anyhow::{bail, Result};
use futures::{stream, StreamExt}; use futures::{stream, StreamExt};
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use rustypipe::client::{ClientType, RustyPipe, YTContext}; use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery, YTContext};
use rustypipe::model::YouTubeItem; use rustypipe::model::YouTubeItem;
use rustypipe::param::search_filter::{ItemType, SearchFilter}; use rustypipe::param::search_filter::{ItemType, SearchFilter};
use serde::de::IgnoredAny; use serde::de::IgnoredAny;
@ -20,9 +20,14 @@ pub enum ABTest {
ChannelHandlesInSearchResults = 3, ChannelHandlesInSearchResults = 3,
TrendsVideoTab = 4, TrendsVideoTab = 4,
TrendsPageHeaderRenderer = 5, TrendsPageHeaderRenderer = 5,
DiscographyPage = 6,
} }
const TESTS_TO_RUN: [ABTest; 2] = [ABTest::TrendsVideoTab, ABTest::TrendsPageHeaderRenderer]; const TESTS_TO_RUN: [ABTest; 3] = [
ABTest::TrendsVideoTab,
ABTest::TrendsPageHeaderRenderer,
ABTest::DiscographyPage,
];
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ABTestRes { pub struct ABTestRes {
@ -75,20 +80,16 @@ pub async fn run_test(
let http = http.clone(); let http = http.clone();
async move { async move {
let visitor_data = get_visitor_data(&http).await; let visitor_data = get_visitor_data(&http).await;
let query = rp.query().visitor_data(&visitor_data);
let is_present = match ab { let is_present = match ab {
ABTest::AttributedTextDescription => { ABTest::AttributedTextDescription => attributed_text_description(&query).await,
attributed_text_description(&rp, &visitor_data).await ABTest::ThreeTabChannelLayout => three_tab_channel_layout(&query).await,
}
ABTest::ThreeTabChannelLayout => {
three_tab_channel_layout(&rp, &visitor_data).await
}
ABTest::ChannelHandlesInSearchResults => { ABTest::ChannelHandlesInSearchResults => {
channel_handles_in_search_results(&rp, &visitor_data).await channel_handles_in_search_results(&query).await
}
ABTest::TrendsVideoTab => trends_video_tab(&rp, &visitor_data).await,
ABTest::TrendsPageHeaderRenderer => {
trends_page_header_renderer(&rp, &visitor_data).await
} }
ABTest::TrendsVideoTab => trends_video_tab(&query).await,
ABTest::TrendsPageHeaderRenderer => trends_page_header_renderer(&query).await,
ABTest::DiscographyPage => discography_page(&query).await,
} }
.unwrap(); .unwrap();
pb.inc(1); pb.inc(1);
@ -143,18 +144,15 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
results results
} }
pub async fn attributed_text_description(rp: &RustyPipe, visitor_data: &str) -> Result<bool> { pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
let query = rp.query(); let context = rp.get_context(ClientType::Desktop, true, None).await;
let context = query
.get_context(ClientType::Desktop, true, Some(visitor_data))
.await;
let q = QVideo { let q = QVideo {
context, context,
video_id: "ZeerrnuLi5E", video_id: "ZeerrnuLi5E",
content_check_ok: false, content_check_ok: false,
racy_check_ok: false, racy_check_ok: false,
}; };
let response_txt = query.raw(ClientType::Desktop, "next", &q).await.unwrap(); let response_txt = rp.raw(ClientType::Desktop, "next", &q).await.unwrap();
if !response_txt.contains("\"Black Mamba\"") { if !response_txt.contains("\"Black Mamba\"") {
bail!("invalid response data"); bail!("invalid response data");
@ -163,20 +161,13 @@ pub async fn attributed_text_description(rp: &RustyPipe, visitor_data: &str) ->
Ok(response_txt.contains("\"attributedDescription\"")) Ok(response_txt.contains("\"attributedDescription\""))
} }
pub async fn three_tab_channel_layout(rp: &RustyPipe, visitor_data: &str) -> Result<bool> { pub async fn three_tab_channel_layout(rp: &RustyPipeQuery) -> Result<bool> {
let channel = rp let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await.unwrap();
.query()
.visitor_data(visitor_data)
.channel_videos("UCR-DXc1voovS8nhAvccRZhg")
.await
.unwrap();
Ok(channel.has_live || channel.has_shorts) Ok(channel.has_live || channel.has_shorts)
} }
pub async fn channel_handles_in_search_results(rp: &RustyPipe, visitor_data: &str) -> Result<bool> { pub async fn channel_handles_in_search_results(rp: &RustyPipeQuery) -> Result<bool> {
let search = rp let search = rp
.query()
.visitor_data(visitor_data)
.search_filter("rust", &SearchFilter::new().item_type(ItemType::Channel)) .search_filter("rust", &SearchFilter::new().item_type(ItemType::Channel))
.await .await
.unwrap(); .unwrap();
@ -190,10 +181,9 @@ pub async fn channel_handles_in_search_results(rp: &RustyPipe, visitor_data: &st
})) }))
} }
pub async fn trends_video_tab(rp: &RustyPipe, visitor_data: &str) -> Result<bool> { pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
let query = rp.query().visitor_data(visitor_data); let context = rp.get_context(ClientType::Desktop, true, None).await;
let context = query.get_context(ClientType::Desktop, true, None).await; let res = rp
let res = query
.raw( .raw(
ClientType::Desktop, ClientType::Desktop,
"browse", "browse",
@ -208,10 +198,9 @@ pub async fn trends_video_tab(rp: &RustyPipe, visitor_data: &str) -> Result<bool
Ok(res.contains("\"4gIOGgxtb3N0X3BvcHVsYXI%3D\"")) Ok(res.contains("\"4gIOGgxtb3N0X3BvcHVsYXI%3D\""))
} }
pub async fn trends_page_header_renderer(rp: &RustyPipe, visitor_data: &str) -> Result<bool> { pub async fn trends_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
let query = rp.query().visitor_data(visitor_data); let context = rp.get_context(ClientType::Desktop, true, None).await;
let context = query.get_context(ClientType::Desktop, true, None).await; let res = rp
let res = query
.raw( .raw(
ClientType::Desktop, ClientType::Desktop,
"browse", "browse",
@ -232,3 +221,12 @@ pub async fn trends_page_header_renderer(rp: &RustyPipe, visitor_data: &str) ->
Ok(data.header.contains_key("pageHeaderRenderer")) Ok(data.header.contains_key("pageHeaderRenderer"))
} }
pub async fn discography_page(rp: &RustyPipeQuery) -> Result<bool> {
let artist = rp
.music_artist("UC7cl4MmM6ZZ2TcFyMk_b4pg", false)
.await
.unwrap();
Ok(artist.albums.len() <= 10)
}

View file

@ -652,7 +652,6 @@ async fn music_search_suggestion() {
async fn music_artist() { async fn music_artist() {
for (name, id, all_albums) in [ for (name, id, all_albums) in [
("default", "UClmXPfaYhXOYsNn_QUyheWQ", true), ("default", "UClmXPfaYhXOYsNn_QUyheWQ", true),
("no_more_albums", "UC_vmjW5e1xEHhYjY2a0kK1A", true),
("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", true), ("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", true),
("no_artist", "UCh8gHdtzO2tXd593_bjErWg", true), ("no_artist", "UCh8gHdtzO2tXd593_bjErWg", true),
("only_more_singles", "UC0aXrjVxG5pZr99v77wZdPQ", true), ("only_more_singles", "UC0aXrjVxG5pZr99v77wZdPQ", true),

View file

@ -353,3 +353,26 @@ YouTube changed the header renderer type on the trending page to a `pageHeaderRe
} }
} }
``` ```
## [6] New Music Discography page
- **Encountered on:** 13.05.2023
- **Impact:** 🟡 Medium
- **Endpoint:** browse (music artist)
YouTube merged the 2 sections for singles and albums on artist pages together. Now
there is only a *Top Releases* section.
YouTube also changed the way the full discography page is fetched, surprisingly making
it easier for alternative clients. The discography page now has its own content ID in
the format of `MPAD<channel id>` (Music Page Artist Discography). This page can be
fetched with a regular browse request without requiring parameters to be parsed or a
visitor data cookie to be set, as it was the case with the old system.
**OLD**
![A/B test 4 old screenshot](./_img/ab_6_old.png)
**NEW**
![A/B test 4 old screenshot](./_img/ab_6_new.png)

BIN
notes/_img/ab_6_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
notes/_img/ab_6_old.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View file

@ -1,6 +1,5 @@
use std::{borrow::Cow, rc::Rc}; use std::borrow::Cow;
use futures::{stream, StreamExt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
@ -13,7 +12,7 @@ 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, QBrowseParams, RustyPipeQuery, ClientType, MapResponse, QBrowse, RustyPipeQuery,
}; };
impl RustyPipeQuery { impl RustyPipeQuery {
@ -26,99 +25,57 @@ impl RustyPipeQuery {
all_albums: bool, all_albums: bool,
) -> Result<MusicArtist, Error> { ) -> Result<MusicArtist, Error> {
let artist_id = artist_id.as_ref(); let artist_id = artist_id.as_ref();
let visitor_data = if all_albums { let res = self._music_artist(artist_id, all_albums).await;
Some(self.get_visitor_data().await?)
} else {
None
};
let res = self._music_artist(artist_id, visitor_data.as_deref()).await;
if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res { if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res {
log::debug!("music artist {} redirects to {}", artist_id, &id); log::debug!("music artist {} redirects to {}", artist_id, &id);
self._music_artist(&id, visitor_data.as_deref()).await self._music_artist(&id, all_albums).await
} else { } else {
res res
} }
} }
async fn _music_artist( async fn _music_artist(&self, artist_id: &str, all_albums: bool) -> Result<MusicArtist, Error> {
&self, let context = self.get_context(ClientType::DesktopMusic, true, None).await;
artist_id: &str, let request_body = QBrowse {
all_albums_vdata: Option<&str>, context,
) -> Result<MusicArtist, Error> { browse_id: artist_id,
match all_albums_vdata { };
Some(visitor_data) => {
let context = self
.get_context(ClientType::DesktopMusic, true, Some(visitor_data))
.await;
let request_body = QBrowse {
context,
browse_id: artist_id,
};
let (mut artist, album_page_params) = self if all_albums {
.execute_request::<response::MusicArtist, _, _>( let (mut artist, can_fetch_more) = self
ClientType::DesktopMusic, .execute_request::<response::MusicArtist, _, _>(
"music_artist",
artist_id,
"browse",
&request_body,
)
.await?;
let visitor_data = Rc::new(visitor_data);
let album_page_results = stream::iter(album_page_params)
.map(|params| {
let visitor_data = visitor_data.clone();
async move {
self.music_artist_album_page(artist_id, &params, &visitor_data)
.await
}
})
.buffer_unordered(2)
.collect::<Vec<_>>()
.await;
for res in album_page_results {
let mut res = res?;
artist.albums.append(&mut res);
}
Ok(artist)
}
None => {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: artist_id,
};
self.execute_request::<response::MusicArtist, _, _>(
ClientType::DesktopMusic, ClientType::DesktopMusic,
"music_artist", "music_artist",
artist_id, artist_id,
"browse", "browse",
&request_body, &request_body,
) )
.await .await?;
if can_fetch_more {
artist.albums = self.music_artist_albums(artist_id).await?;
} }
Ok(artist)
} else {
self.execute_request::<response::MusicArtist, _, _>(
ClientType::DesktopMusic,
"music_artist",
artist_id,
"browse",
&request_body,
)
.await
} }
} }
async fn music_artist_album_page( /// Get a list of all albums of a YouTube Music artist
&self, pub async fn music_artist_albums(&self, artist_id: &str) -> Result<Vec<AlbumItem>, Error> {
artist_id: &str, let context = self.get_context(ClientType::DesktopMusic, true, None).await;
params: &str, let request_body = QBrowse {
visitor_data: &str,
) -> Result<Vec<AlbumItem>, Error> {
let context = self
.get_context(ClientType::DesktopMusic, true, Some(visitor_data))
.await;
let request_body = QBrowseParams {
context, context,
browse_id: artist_id, browse_id: &format!("{}{}", util::ARTIST_DISCOGRAPHY_PREFIX, artist_id),
params,
}; };
self.execute_request::<response::MusicArtistAlbums, _, _>( self.execute_request::<response::MusicArtistAlbums, _, _>(
@ -147,13 +104,13 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
} }
} }
impl MapResponse<(MusicArtist, Vec<String>)> for response::MusicArtist { impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
fn map_response( fn map_response(
self, self,
id: &str, id: &str,
lang: crate::param::Language, lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>, _deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> { ) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
map_artist_page(self, id, lang, true) map_artist_page(self, id, lang, true)
} }
} }
@ -163,7 +120,7 @@ fn map_artist_page(
id: &str, id: &str,
lang: crate::param::Language, lang: crate::param::Language,
skip_extendables: bool, skip_extendables: bool,
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> { ) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
// dbg!(&res); // dbg!(&res);
let header = res.header.music_immersive_header_renderer; let header = res.header.music_immersive_header_renderer;
@ -203,7 +160,7 @@ fn map_artist_page(
let mut tracks_playlist_id = None; let mut tracks_playlist_id = None;
let mut videos_playlist_id = None; let mut videos_playlist_id = None;
let mut album_page_params = Vec::new(); let mut can_fetch_more = false;
for section in sections { for section in sections {
match section { match section {
@ -242,6 +199,11 @@ fn map_artist_page(
videos_playlist_id = Some(bep.browse_id); videos_playlist_id = Some(bep.browse_id);
} }
} }
// Albums
PageType::ArtistDiscography => {
can_fetch_more = true;
extendable_albums = true;
}
// Albums or playlists // Albums or playlists
PageType::Artist => { PageType::Artist => {
// Peek at the first item to determine type // Peek at the first item to determine type
@ -250,7 +212,7 @@ fn map_artist_page(
be.browse_endpoint_context_supported_configs.as_ref().map(|config| { be.browse_endpoint_context_supported_configs.as_ref().map(|config| {
config.browse_endpoint_context_music_config.page_type config.browse_endpoint_context_music_config.page_type
})}) { })}) {
album_page_params.push(bep.params); can_fetch_more = true;
extendable_albums = true; extendable_albums = true;
} }
} }
@ -318,7 +280,7 @@ fn map_artist_page(
videos_playlist_id, videos_playlist_id,
radio_id, radio_id,
}, },
album_page_params, can_fetch_more,
), ),
warnings: mapped.warnings, warnings: mapped.warnings,
}) })
@ -333,6 +295,10 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> { ) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
// dbg!(&self); // dbg!(&self);
let Some(header) = self.header else {
return Err(ExtractionError::NotFound { id: id.into(), msg: "no header".into() });
};
let grids = self let grids = self
.contents .contents
.single_column_browse_results_renderer .single_column_browse_results_renderer
@ -349,7 +315,7 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
lang, lang,
ArtistId { ArtistId {
id: Some(id.to_owned()), id: Some(id.to_owned()),
name: self.header.music_header_renderer.title, name: header.music_header_renderer.title,
}, },
); );
@ -380,7 +346,6 @@ mod tests {
#[rstest] #[rstest]
#[case::default("default", "UClmXPfaYhXOYsNn_QUyheWQ")] #[case::default("default", "UClmXPfaYhXOYsNn_QUyheWQ")]
#[case::no_more_albums("no_more_albums", "UC_vmjW5e1xEHhYjY2a0kK1A")]
#[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw")] #[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw")]
#[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg")] #[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg")]
#[case::only_more_singles("only_more_singles", "UC0aXrjVxG5pZr99v77wZdPQ")] #[case::only_more_singles("only_more_singles", "UC0aXrjVxG5pZr99v77wZdPQ")]
@ -388,30 +353,27 @@ mod tests {
let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}.json")); let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}.json"));
let json_file = File::open(json_path).unwrap(); let json_file = File::open(json_path).unwrap();
let mut album_page_paths = Vec::new(); let mut album_page_path = None;
for i in 1..=2 { let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}_1.json"));
let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}_{i}.json")); if json_path.exists() {
if !json_path.exists() { album_page_path = Some(json_path);
break;
}
album_page_paths.push(json_path);
} }
let resp: response::MusicArtist = let resp: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<(MusicArtist, Vec<String>)> = let map_res: MapResult<(MusicArtist, bool)> =
resp.map_response(id, Language::En, None).unwrap(); resp.map_response(id, Language::En, None).unwrap();
let (mut artist, album_page_params) = map_res.c; let (mut artist, can_fetch_more) = map_res.c;
assert!( assert!(
map_res.warnings.is_empty(), map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}", "deserialization/mapping warnings: {:?}",
map_res.warnings map_res.warnings
); );
assert_eq!(album_page_params.len(), album_page_paths.len()); assert_eq!(can_fetch_more, album_page_path.is_some());
for json_path in album_page_paths { if let Some(album_page_path) = album_page_path {
let json_file = File::open(json_path).unwrap(); let json_file = File::open(album_page_path).unwrap();
let resp: response::MusicArtistAlbums = let resp: response::MusicArtistAlbums =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let mut map_res: MapResult<Vec<AlbumItem>> = let mut map_res: MapResult<Vec<AlbumItem>> =

View file

@ -73,9 +73,12 @@ pub(crate) struct ShareEntityEndpoint {
} }
/// Response model for YouTube Music artist album page /// Response model for YouTube Music artist album page
#[serde_as]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtistAlbums { pub(crate) struct MusicArtistAlbums {
pub header: SimpleHeader, #[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub header: Option<SimpleHeader>,
pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>, pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>,
} }

View file

@ -1,7 +1,7 @@
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError}; use serde_with::{serde_as, DefaultOnError};
use crate::model::UrlTarget; use crate::{model::UrlTarget, util};
/// navigation/resolve_url response model /// navigation/resolve_url response model
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -152,6 +152,8 @@ pub(crate) enum PageType {
alias = "MUSIC_PAGE_TYPE_AUDIOBOOK_ARTIST" alias = "MUSIC_PAGE_TYPE_AUDIOBOOK_ARTIST"
)] )]
Artist, Artist,
#[serde(rename = "MUSIC_PAGE_TYPE_ARTIST_DISCOGRAPHY")]
ArtistDiscography,
#[serde(rename = "MUSIC_PAGE_TYPE_ALBUM", alias = "MUSIC_PAGE_TYPE_AUDIOBOOK")] #[serde(rename = "MUSIC_PAGE_TYPE_ALBUM", alias = "MUSIC_PAGE_TYPE_AUDIOBOOK")]
Album, Album,
#[serde( #[serde(
@ -169,6 +171,9 @@ impl PageType {
pub(crate) fn to_url_target(self, id: String) -> Option<UrlTarget> { pub(crate) fn to_url_target(self, id: String) -> Option<UrlTarget> {
match self { match self {
PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }), PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }),
PageType::ArtistDiscography => id
.strip_prefix(util::ARTIST_DISCOGRAPHY_PREFIX)
.map(|id| UrlTarget::Channel { id: id.to_owned() }),
PageType::Album => Some(UrlTarget::Album { id }), PageType::Album => Some(UrlTarget::Album { id }),
PageType::Playlist => Some(UrlTarget::Playlist { id }), PageType::Playlist => Some(UrlTarget::Playlist { id }),
PageType::Unknown => None, PageType::Unknown => None,
@ -192,7 +197,7 @@ impl From<PageType> for MusicPageType {
PageType::Artist => MusicPageType::Artist, PageType::Artist => MusicPageType::Artist,
PageType::Album => MusicPageType::Album, PageType::Album => MusicPageType::Album,
PageType::Playlist => MusicPageType::Playlist, PageType::Playlist => MusicPageType::Playlist,
PageType::Channel => MusicPageType::None, PageType::Channel | PageType::ArtistDiscography => MusicPageType::None,
PageType::Unknown => MusicPageType::Unknown, PageType::Unknown => MusicPageType::Unknown,
} }
} }

View file

@ -7,27 +7,27 @@ MusicArtist(
name: "Doobydobap", name: "Doobydobap",
header_image: [ header_image: [
Thumbnail( Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w540-h225-p-l90-rj", url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w540-h225-p-l90-rj",
width: 540, width: 540,
height: 225, height: 225,
), ),
Thumbnail( Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w816-h340-p-l90-rj", url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w816-h340-p-l90-rj",
width: 816, width: 816,
height: 340, height: 340,
), ),
Thumbnail( Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-h600-p-l90-rj", url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1440-h600-p-l90-rj",
width: 1440, width: 1440,
height: 600, height: 600,
), ),
Thumbnail( Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-h800-p-l90-rj", url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w1920-h800-p-l90-rj",
width: 1920, width: 1920,
height: 800, height: 800,
), ),
Thumbnail( Thumbnail(
url: "https://yt3.ggpht.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2436-h1015-p-l90-rj", url: "https://yt3.googleusercontent.com/BvnAqgiursrXpmS9AgDLtkOSTQfOG_Dqn0KzY5hcwO9XrHTEQTVgaflI913f9KRp7d0U2qBp=w2436-h1015-p-l90-rj",
width: 2436, width: 2436,
height: 1015, height: 1015,
), ),
@ -39,16 +39,82 @@ MusicArtist(
albums: [], albums: [],
playlists: [ playlists: [
MusicPlaylistItem( MusicPlaylistItem(
id: "PLwkM1QxaP342hjju64dtqG5wKqx2hNgjr", id: "PLwkM1QxaP341MxmqdPrw_3ffjqLofZXOj",
name: "After Hours & Doob Gourmand", name: "🌟 best of... doobyvlog",
thumbnail: [ thumbnail: [
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/38Gd6TdmNVs/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lbh9XVK3zCkDHMxgOnDokkqE3kwg", url: "https://i.ytimg.com/vi/0onVbAuBGWI/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mUFLfu6rAMobj83G1P7wFtrbRuqg",
width: 400, width: 400,
height: 225, height: 225,
), ),
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/38Gd6TdmNVs/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k4lcTkiScLDPpmxBPfEub3xft7iQ", url: "https://i.ytimg.com/vi/0onVbAuBGWI/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nU_px4mXhplu4zYKhseBKdYUJh0g",
width: 800,
height: 450,
),
],
channel: Some(ChannelId(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
)),
track_count: None,
from_ytm: false,
),
MusicPlaylistItem(
id: "PLwkM1QxaP343YqeP6g5VPGsgJdO1_SV4I",
name: "love & relationships",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/PXsK9-CFoH4/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kaG7qOCwpNsPCgSCEoxkeeFW2SUg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/PXsK9-CFoH4/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ktLL_rN7UujT_48Np6bSaK0Un1pA",
width: 800,
height: 450,
),
],
channel: Some(ChannelId(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
)),
track_count: None,
from_ytm: false,
),
MusicPlaylistItem(
id: "PLwkM1QxaP340xbkARIPpiD1aHuzJVuZUg",
name: "🏠 opening a restaurant",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/7ebSD1ezP-M/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3n-xqleEhGCII_Qfza-POZT66c3Tg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/7ebSD1ezP-M/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ntBQTkAyTuRhx6HNxBEJtc4RpgWQ",
width: 800,
height: 450,
),
],
channel: Some(ChannelId(
id: "UCh8gHdtzO2tXd593_bjErWg",
name: "Doobydobap",
)),
track_count: None,
from_ytm: false,
),
MusicPlaylistItem(
id: "PLwkM1QxaP342hjju64dtqG5wKqx2hNgjr",
name: "🍽\u{fe0f} after hours & doob gourmand",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/2tg0MNZwUSQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kqvMhctjtDMJ4u84hAp2h5UXT-QA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/2tg0MNZwUSQ/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nxfWmvCHZgvWYolc_mlJEWD1TkZQ",
width: 800, width: 800,
height: 450, height: 450,
), ),
@ -62,15 +128,15 @@ MusicArtist(
), ),
MusicPlaylistItem( MusicPlaylistItem(
id: "PLwkM1QxaP342v1hhoB3XLiruSQOzmdmBt", id: "PLwkM1QxaP342v1hhoB3XLiruSQOzmdmBt",
name: "doobyvlog", name: "📹 doobyvlog",
thumbnail: [ thumbnail: [
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/CN-u8_2ixOU/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kCdNu1hqEubXzcLcci_xhWSI9q-Q", url: "https://i.ytimg.com/vi/7ebSD1ezP-M/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3n-xqleEhGCII_Qfza-POZT66c3Tg",
width: 400, width: 400,
height: 225, height: 225,
), ),
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/CN-u8_2ixOU/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lN1-hs5ItQ_DJXBp-aUQQ-DLjMVg", url: "https://i.ytimg.com/vi/7ebSD1ezP-M/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ntBQTkAyTuRhx6HNxBEJtc4RpgWQ",
width: 800, width: 800,
height: 450, height: 450,
), ),

View file

@ -34,7 +34,7 @@ MusicArtist(
], ],
description: Some("Choi Jin-ri, better known by her stage name Sulli, was a South Korean actress and singer. She first made her debut as a child actress, appearing as a supporting cast member on the SBS historical drama Ballad of Seodong. Following this, she earned a number of guest roles, appearing in the television series Love Needs a Miracle and Drama City, and the film Vacation. She then subsequently appeared in the independent films Punch Lady and BA:BO, the former being her first time cast in a substantial dramatic role.\nAfter signing a record deal with SM Entertainment, Sulli rose to prominence as a member of the girl group f(x) formed in 2009. The group achieved both critical and commercial success, with four Korean number-one singles and international recognition after becoming the first K-pop act to perform at SXSW. Concurrently with her music career, Sulli returned to acting by starring in the SBS romantic comedy series, To the Beautiful You, a Korean adaptation of the shōjo manga Hana-Kimi where her performance was positively received and earned her two SBS Drama Awards and a nomination at the 49th Paeksang Arts Awards.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/Sulli) under Creative Commons Attribution CC-BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/legalcode)"), description: Some("Choi Jin-ri, better known by her stage name Sulli, was a South Korean actress and singer. She first made her debut as a child actress, appearing as a supporting cast member on the SBS historical drama Ballad of Seodong. Following this, she earned a number of guest roles, appearing in the television series Love Needs a Miracle and Drama City, and the film Vacation. She then subsequently appeared in the independent films Punch Lady and BA:BO, the former being her first time cast in a substantial dramatic role.\nAfter signing a record deal with SM Entertainment, Sulli rose to prominence as a member of the girl group f(x) formed in 2009. The group achieved both critical and commercial success, with four Korean number-one singles and international recognition after becoming the first K-pop act to perform at SXSW. Concurrently with her music career, Sulli returned to acting by starring in the SBS romantic comedy series, To the Beautiful You, a Korean adaptation of the shōjo manga Hana-Kimi where her performance was positively received and earned her two SBS Drama Awards and a nomination at the 49th Paeksang Arts Awards.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/Sulli) under Creative Commons Attribution CC-BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/legalcode)"),
wikipedia_url: Some("https://en.wikipedia.org/wiki/Sulli"), wikipedia_url: Some("https://en.wikipedia.org/wiki/Sulli"),
subscriber_count: Some(74400), subscriber_count: Some(80800),
tracks: [ tracks: [
TrackItem( TrackItem(
id: "BGcUVJXViqQ", id: "BGcUVJXViqQ",
@ -156,7 +156,7 @@ MusicArtist(
], ],
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"), artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
album: None, album: None,
view_count: Some(19000000), view_count: Some(20000000),
is_video: true, is_video: true,
track_nr: None, track_nr: None,
by_va: false, by_va: false,
@ -185,7 +185,7 @@ MusicArtist(
], ],
artist_id: Some("UClGBYGUZmpzUaHgeb9gOBww"), artist_id: Some("UClGBYGUZmpzUaHgeb9gOBww"),
album: None, album: None,
view_count: Some(206000), view_count: Some(211000),
is_video: true, is_video: true,
track_nr: None, track_nr: None,
by_va: false, by_va: false,
@ -209,7 +209,7 @@ MusicArtist(
artists: [ artists: [
ArtistId( ArtistId(
id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"), id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
name: "iKissesByMaki", name: "ramuditas",
), ),
], ],
artist_id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"), artist_id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
@ -243,152 +243,36 @@ MusicArtist(
], ],
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"), artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
album: None, album: None,
view_count: Some(3600), view_count: Some(15000),
is_video: true, is_video: true,
track_nr: None, track_nr: None,
by_va: false, by_va: false,
), ),
TrackItem( TrackItem(
id: "94q_2Zsq2os", id: "N217ZuMQnfY",
name: "Sulli - On The Moon (sped up)", name: "음악캠프 - Dorothy - Picnic, 도로시 - 소풍, Music Camp 20040417",
duration: None, duration: None,
cover: [ cover: [
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/94q_2Zsq2os/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kf1HgaRExXYDCNGVqeqe-TKxJ2aQ", url: "https://i.ytimg.com/vi/N217ZuMQnfY/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kBmlxLhMmsi9VLaMVosj6Ie4aAyw",
width: 400, width: 400,
height: 225, height: 225,
), ),
Thumbnail( Thumbnail(
url: "https://i.ytimg.com/vi/94q_2Zsq2os/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mbke4_Zc9nkj4NvVMlYpA7aSrkzQ", url: "https://i.ytimg.com/vi/N217ZuMQnfY/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mHxVVuDdgr8HxoJjgtvYwaxtVbzA",
width: 800, width: 800,
height: 450, height: 450,
), ),
], ],
artists: [ artists: [
ArtistId( ArtistId(
id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"), id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
name: "Jpn Sch", name: "MBCkpop",
), ),
], ],
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"), artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
album: None, album: None,
view_count: Some(2900), view_count: Some(1200),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "fBce3VihpIQ",
name: "sulli - goblin (sped up)",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/fBce3VihpIQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nVd442JLu3mxKbipFglRgRift5cA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/fBce3VihpIQ/sddefault.jpg?sqp=-oaymwEWCKoDEPABIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nxWW7AAdAGbEGuDXOVFgFaLW3XbA",
width: 426,
height: 240,
),
],
artists: [
ArtistId(
id: Some("UCwZLmi2q2ReEJvt-8bgVwwg"),
name: "e p i l o g u e",
),
],
artist_id: Some("UCwZLmi2q2ReEJvt-8bgVwwg"),
album: None,
view_count: Some(11000),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "0-HXdJc-zDQ",
name: "Sulli - Dorothy (sped up)",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/0-HXdJc-zDQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3l_Cj2X9UftBVID7UoJ708Dd6UGUA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/0-HXdJc-zDQ/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n2XskAJVpE4eQ3okG60Lf6dPPFqQ",
width: 800,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
name: "Jpn Sch",
),
],
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
album: None,
view_count: Some(2200),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "Bae4Fv7GlMY",
name: "Sulli\'nin goblin M/V\'sindeki korkutucu detaylar!😱💥",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Bae4Fv7GlMY/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nXyTG8uM24-GkmqaUvx2gQqjJO-g",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Bae4Fv7GlMY/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3m2mg6beaz7Lg-zRFebE5jHHbiiJw",
width: 800,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UCuoa8Ie9kA1-AKIjNhsIGxQ"),
name: "JenDia` #Emrlsoolarbenim",
),
],
artist_id: Some("UCuoa8Ie9kA1-AKIjNhsIGxQ"),
album: None,
view_count: Some(6900),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "Rq_JkcROjsI",
name: "𝗀𝗈𝖻𝗅𝗂𝗇 // 𝗌𝗎𝗅𝗅𝗂𝗌𝗅𝗈𝗐𝖾𝖽 + 𝗋𝖾𝗏𝖾𝗋𝖻",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Rq_JkcROjsI/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3ntvxfQE8ErdwpxdksKA_Gtcza3Rw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Rq_JkcROjsI/sddefault.jpg?sqp=-oaymwEWCKoDEPABIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kyJ7LFsJWz4Ac44uL8GPxsRldOng",
width: 426,
height: 240,
),
],
artists: [
ArtistId(
id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
name: "ᴜᴋɪʏᴏ",
),
],
artist_id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
album: None,
view_count: Some(23000),
is_video: true, is_video: true,
track_nr: None, track_nr: None,
by_va: false, by_va: false,
@ -417,7 +301,123 @@ MusicArtist(
], ],
artist_id: Some("UCFFvwAcyQhpeQfuAgBN1XZw"), artist_id: Some("UCFFvwAcyQhpeQfuAgBN1XZw"),
album: None, album: None,
view_count: Some(9900), view_count: Some(12000),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "v5KZ5dalhzU",
name: "intp kpop songs (kpop mbti series pt.9)",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/v5KZ5dalhzU/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nMdCDQ1v-pG312TsgWId2TsMo1zw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/v5KZ5dalhzU/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mS7zCcBbRf8t29XGtChWkxd3owMg",
width: 800,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UC_xEL8cbkItBH00KrGz9fbQ"),
name: "orbitiny사샤",
),
],
artist_id: Some("UC_xEL8cbkItBH00KrGz9fbQ"),
album: None,
view_count: Some(7400),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "vaSSdzgDNw0",
name: "Goblin~ Sulli English Cover",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/vaSSdzgDNw0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lbkZLY174thtujXlmmPqngKOtxvQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/vaSSdzgDNw0/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k5nsO0ap7vTsm8JoihXd9opC9QwA",
width: 800,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UCaFqztcJss3HrXNurzQJyqQ"),
name: "Bee",
),
],
artist_id: Some("UCaFqztcJss3HrXNurzQJyqQ"),
album: None,
view_count: Some(1400),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "Rq_JkcROjsI",
name: "𝗀𝗈𝖻𝗅𝗂𝗇 // 𝗌𝗎𝗅𝗅𝗂𝗌𝗅𝗈𝗐𝖾𝖽 + 𝗋𝖾𝗏𝖾𝗋𝖻",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Rq_JkcROjsI/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3ntvxfQE8ErdwpxdksKA_Gtcza3Rw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Rq_JkcROjsI/sddefault.jpg?sqp=-oaymwEWCKoDEPABIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kyJ7LFsJWz4Ac44uL8GPxsRldOng",
width: 426,
height: 240,
),
],
artists: [
ArtistId(
id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
name: "ᴜᴋɪʏᴏ",
),
],
artist_id: Some("UCMPqKiPdiSoi8eCW5Dou1IQ"),
album: None,
view_count: Some(25000),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "5VNZWTzJFso",
name: "Dorothy - Picnic, 도로시 - 소풍, Music Camp 20040529",
duration: None,
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/5VNZWTzJFso/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3niVh6jZ_8X2utHrUst0PgILeoY4g",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/5VNZWTzJFso/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nKv3B3uev0Pg01kUNF_F4GHO39qg",
width: 800,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
name: "MBCkpop",
),
],
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
album: None,
view_count: Some(3700),
is_video: true, is_video: true,
track_nr: None, track_nr: None,
by_va: false, by_va: false,
@ -454,89 +454,21 @@ MusicArtist(
playlists: [], playlists: [],
similar_artists: [ similar_artists: [
ArtistItem( ArtistItem(
id: "UCHmZYTfdTyVKQEJicLiXEOg", id: "UCpUChkP-KE20GRsyfjU83_g",
name: "Red Velvet", name: "CHUNG HA",
avatar: [ avatar: [
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/eGQ0NAW1LeeaD0V0pe5HFg2ENDqn-T4qJfi4zBrLuoMHmEzYEHzT64p95_45UdXWTB6WTMskbzfXNw=w226-h226-p-l90-rj", url: "https://lh3.googleusercontent.com/gfAf1lvXJt_ExLMV0FqnWrTGyAiFpRZW7zeb1ooahA542nBqjLTzcxdhFQtvVWvR0_jzcKOccjIyiFnP=w226-h226-l90-rj",
width: 226, width: 226,
height: 226, height: 226,
), ),
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/eGQ0NAW1LeeaD0V0pe5HFg2ENDqn-T4qJfi4zBrLuoMHmEzYEHzT64p95_45UdXWTB6WTMskbzfXNw=w544-h544-p-l90-rj", url: "https://lh3.googleusercontent.com/gfAf1lvXJt_ExLMV0FqnWrTGyAiFpRZW7zeb1ooahA542nBqjLTzcxdhFQtvVWvR0_jzcKOccjIyiFnP=w544-h544-l90-rj",
width: 544, width: 544,
height: 544, height: 544,
), ),
], ],
subscriber_count: Some(5200000), subscriber_count: Some(1470000),
),
ArtistItem(
id: "UCvAUJvMNPy1LLVKpFiu-X9w",
name: "SEULGI",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/Gs5_FbRLCCPLjzgZgJ3Rzz9LJ-U4J825o2dTS-JRnEADJEt9qzMENRA52qRdVx8qavapNYsZDJX4jQ=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Gs5_FbRLCCPLjzgZgJ3Rzz9LJ-U4J825o2dTS-JRnEADJEt9qzMENRA52qRdVx8qavapNYsZDJX4jQ=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(32100),
),
ArtistItem(
id: "UCyoD3vxUcLLdU1JQ0MMet-Q",
name: "Red Velvet - IRENE & SEULGI",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/jwZVdeVkLWjvoi5aXAKbT_ipid5krOyDCo9zGlMLm4uysvbtMwnYWHHemKmCLjM7DcsxNHfouD3caA=w226-h226-p-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/jwZVdeVkLWjvoi5aXAKbT_ipid5krOyDCo9zGlMLm4uysvbtMwnYWHHemKmCLjM7DcsxNHfouD3caA=w544-h544-p-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(34700),
),
ArtistItem(
id: "UCod8XS1t8Y9JADHv0AWDMLw",
name: "Seohyun",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/0uAuF9rHyOlwm18Q0c6DqjLwRzZWqGFwMLWCh5g802HYpy0AVa6VWs1jNmqXhcB-GAMw2fehnrF0P4s=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/0uAuF9rHyOlwm18Q0c6DqjLwRzZWqGFwMLWCh5g802HYpy0AVa6VWs1jNmqXhcB-GAMw2fehnrF0P4s=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(24400),
),
ArtistItem(
id: "UCOCo09j3eIVdj3HkbJ9c40Q",
name: "HYOYEON ",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/N46gjgCRMewGNJBym4Xqa10-GXxo97bcFf_MJeiAxZl2mQpbJn1MWGzzgmYBUG4XUIUlHpkHHrAZlOc=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/N46gjgCRMewGNJBym4Xqa10-GXxo97bcFf_MJeiAxZl2mQpbJn1MWGzzgmYBUG4XUIUlHpkHHrAZlOc=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(133000),
), ),
ArtistItem( ArtistItem(
id: "UCmeskqhmPRuteGVH4yCXT0A", id: "UCmeskqhmPRuteGVH4yCXT0A",
@ -553,24 +485,7 @@ MusicArtist(
height: 544, height: 544,
), ),
], ],
subscriber_count: Some(1480000), subscriber_count: Some(1490000),
),
ArtistItem(
id: "UCF9KUOyCpRNq67dDKlB0tbw",
name: "HA:TFELT",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/9FoZXRMcJp_iIMTqQJ4NHEYumcdwNYN1JfUC2aXUILrRFzbAypG9Yq1r6mMRQzVfDxZ_4OADbruAn5Q=w226-h226-p-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/9FoZXRMcJp_iIMTqQJ4NHEYumcdwNYN1JfUC2aXUILrRFzbAypG9Yq1r6mMRQzVfDxZ_4OADbruAn5Q=w544-h544-p-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(18300),
), ),
ArtistItem( ArtistItem(
id: "UCVXeNwNQs07XQ8d1HtvuxVg", id: "UCVXeNwNQs07XQ8d1HtvuxVg",
@ -587,41 +502,126 @@ MusicArtist(
height: 544, height: 544,
), ),
], ],
subscriber_count: Some(26900), subscriber_count: Some(31400),
), ),
ArtistItem( ArtistItem(
id: "UCHHz6g3igy0BFUfCSiC_6aw", id: "UCvAUJvMNPy1LLVKpFiu-X9w",
name: "HyunA", name: "SEULGI",
avatar: [ avatar: [
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/L8EVj7a6sEbbj2oszOtVxQo_pJ6Mm1pIyVWcDXPyaEDynkHBa0yRMsxmfkwKBfPJSGLynsIM-CGjmw=w226-h226-p-l90-rj", url: "https://lh3.googleusercontent.com/NT9GLSF-1zyAb76sixVb9SfMuGef0Dh5aiKODfWt_wWvtGYBnAPs_DcRtOErLw36zYZb4xbsQEzM8ek=w226-h226-l90-rj",
width: 226, width: 226,
height: 226, height: 226,
), ),
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/L8EVj7a6sEbbj2oszOtVxQo_pJ6Mm1pIyVWcDXPyaEDynkHBa0yRMsxmfkwKBfPJSGLynsIM-CGjmw=w544-h544-p-l90-rj", url: "https://lh3.googleusercontent.com/NT9GLSF-1zyAb76sixVb9SfMuGef0Dh5aiKODfWt_wWvtGYBnAPs_DcRtOErLw36zYZb4xbsQEzM8ek=w544-h544-l90-rj",
width: 544, width: 544,
height: 544, height: 544,
), ),
], ],
subscriber_count: Some(3130000), subscriber_count: Some(41100),
), ),
ArtistItem( ArtistItem(
id: "UCveaR1hvVMrEQaKR6Xl8NDw", id: "UC277cpXD9BLeaGgh3yDP40w",
name: "f(x)", name: "블랙스완 BLACKSWAN",
avatar: [ avatar: [
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/VQcMudR55EKlNeZCdhQKLy1dh_nojH8YAqwjXPAl515U1hFham1IRGWhcPWITm1X3yUHSQBHUYSWlh0=w226-h226-p-l90-rj", url: "https://lh3.googleusercontent.com/a-/ACB-R5RmPcZVcm8zNXAbprhgqATOJMU6RNbj3OuQyERLHA=w226-h226-l90-rj",
width: 226, width: 226,
height: 226, height: 226,
), ),
Thumbnail( Thumbnail(
url: "https://lh3.googleusercontent.com/VQcMudR55EKlNeZCdhQKLy1dh_nojH8YAqwjXPAl515U1hFham1IRGWhcPWITm1X3yUHSQBHUYSWlh0=w544-h544-p-l90-rj", url: "https://lh3.googleusercontent.com/a-/ACB-R5RmPcZVcm8zNXAbprhgqATOJMU6RNbj3OuQyERLHA=w544-h544-l90-rj",
width: 544, width: 544,
height: 544, height: 544,
), ),
], ],
subscriber_count: Some(474000), subscriber_count: Some(603000),
),
ArtistItem(
id: "UCrt7YUoF7m7PvV_Dt9xWqJQ",
name: "LUNA",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/MISR863mLnf03T-2YBEvxYFQPIvPYJ_clea7H5gZhdsN30i8ufw_2NnvOvVxQTOK7-lx_sRW8Q7A5uM=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/MISR863mLnf03T-2YBEvxYFQPIvPYJ_clea7H5gZhdsN30i8ufw_2NnvOvVxQTOK7-lx_sRW8Q7A5uM=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(4510),
),
ArtistItem(
id: "UCBj8m2FEsjfovt0Fxq0HEJg",
name: "Ladies\' Code",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/JC2ZAjp-IUVIStNvPpxgaJknX_WkUZmV1XqLQvJSr-_Kg9_bVfUJi_3DYztbI1ObFA6LejJimJfKdfjp=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/JC2ZAjp-IUVIStNvPpxgaJknX_WkUZmV1XqLQvJSr-_Kg9_bVfUJi_3DYztbI1ObFA6LejJimJfKdfjp=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(11400),
),
ArtistItem(
id: "UCdhWlMXp2dQBaG5rZS_aSkQ",
name: "DAWN",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/a-/ACB-R5QVBnhjI6XKoLtxuTf6LuijMMtJb7e5S2IlOeqo2Q=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/a-/ACB-R5QVBnhjI6XKoLtxuTf6LuijMMtJb7e5S2IlOeqo2Q=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(802000),
),
ArtistItem(
id: "UCWT2ZfW7d8YI-HinHEVhyCA",
name: "(G)I-DLE",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/uZQSZdDnDJCajJtE6Ig9tqgdO7-uogJpdk9TM0p7iEBmnAQXaSGqYET-W-SHTY-NL9UQ2sdOVtIhd54=w226-h226-p-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/uZQSZdDnDJCajJtE6Ig9tqgdO7-uogJpdk9TM0p7iEBmnAQXaSGqYET-W-SHTY-NL9UQ2sdOVtIhd54=w544-h544-p-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(5310000),
),
ArtistItem(
id: "UCOCo09j3eIVdj3HkbJ9c40Q",
name: "HYO",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/olRNTvkhjENW-kLcKRg9LQDmWwS3V0KHhTtw4Y8ETFpFLNxlI371_EDh22CtTw0UKgm7YpbGJUUf6nEt=w226-h226-p-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/olRNTvkhjENW-kLcKRg9LQDmWwS3V0KHhTtw4Y8ETFpFLNxlI371_EDh22CtTw0UKgm7YpbGJUUf6nEt=w544-h544-p-l90-rj",
width: 544,
height: 544,
),
],
subscriber_count: Some(141000),
), ),
], ],
tracks_playlist_id: None, tracks_playlist_id: None,

View file

@ -119,6 +119,14 @@ impl RustyPipeQuery {
Ok(UrlTarget::Channel { id: id.to_owned() }) Ok(UrlTarget::Channel { id: id.to_owned() })
} else if util::ALBUM_ID_REGEX.is_match(id) { } else if util::ALBUM_ID_REGEX.is_match(id) {
Ok(UrlTarget::Album { id: id.to_owned() }) Ok(UrlTarget::Album { id: id.to_owned() })
} else if id
.strip_prefix(util::ARTIST_DISCOGRAPHY_PREFIX)
.map(|cid| util::CHANNEL_ID_REGEX.is_match(cid))
.unwrap_or_default()
{
Ok(UrlTarget::Channel {
id: id[4..].to_owned(),
})
} else { } else {
Err(Error::Other("invalid url: no browse id".into())) Err(Error::Other("invalid url: no browse id".into()))
} }
@ -261,6 +269,14 @@ impl RustyPipeQuery {
} }
} else if util::ALBUM_ID_REGEX.is_match(s) { } else if util::ALBUM_ID_REGEX.is_match(s) {
Ok(UrlTarget::Album { id: s.to_owned() }) Ok(UrlTarget::Album { id: s.to_owned() })
} else if s
.strip_prefix(util::ARTIST_DISCOGRAPHY_PREFIX)
.map(|cid| util::CHANNEL_ID_REGEX.is_match(cid))
.unwrap_or_default()
{
Ok(UrlTarget::Channel {
id: s[4..].to_owned(),
})
} }
// Channel name only // Channel name only
else if util::VANITY_PATH_REGEX.is_match(s) { else if util::VANITY_PATH_REGEX.is_match(s) {

View file

@ -37,6 +37,7 @@ pub static VANITY_PATH_REGEX: Lazy<Regex> = Lazy::new(|| {
pub const DOT_SEPARATOR: &str = ""; pub const DOT_SEPARATOR: &str = "";
pub const VARIOUS_ARTISTS: &str = "Various Artists"; pub const VARIOUS_ARTISTS: &str = "Various Artists";
pub const PLAYLIST_ID_ALBUM_PREFIX: &str = "OLAK"; pub const PLAYLIST_ID_ALBUM_PREFIX: &str = "OLAK";
pub const ARTIST_DISCOGRAPHY_PREFIX: &str = "MPAD";
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] = const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
@ -1216,6 +1216,7 @@ fn search_suggestion_empty(rp: RustyPipe) {
#[case("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})] #[case("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
#[case("https://music.youtube.com/browse/MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})] #[case("https://music.youtube.com/browse/MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
#[case("https://music.youtube.com/browse/UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})] #[case("https://music.youtube.com/browse/UC5I2hjZYiW9gZPVkvzM8_Cw", UrlTarget::Channel {id: "UC5I2hjZYiW9gZPVkvzM8_Cw".to_owned()})]
#[case("https://music.youtube.com/browse/MPADUC7cl4MmM6ZZ2TcFyMk_b4pg", UrlTarget::Channel {id: "UC7cl4MmM6ZZ2TcFyMk_b4pg".to_owned()})]
fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget, rp: RustyPipe) { fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget, rp: RustyPipe) {
let target = tokio_test::block_on(rp.query().resolve_url(url, true)).unwrap(); let target = tokio_test::block_on(rp.query().resolve_url(url, true)).unwrap();
assert_eq!(target, expect); assert_eq!(target, expect);
@ -1233,6 +1234,7 @@ fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget, rp: RustyPipe) {
#[case("RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk", UrlTarget::Playlist {id: "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk".to_owned()})] #[case("RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk", UrlTarget::Playlist {id: "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk".to_owned()})]
#[case("OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})] #[case("OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
#[case("MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})] #[case("MPREb_GyH43gCvdM5", UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()})]
#[case("MPADUC7cl4MmM6ZZ2TcFyMk_b4pg", UrlTarget::Channel {id: "UC7cl4MmM6ZZ2TcFyMk_b4pg".to_owned()})]
fn resolve_string(#[case] string: &str, #[case] expect: UrlTarget, rp: RustyPipe) { fn resolve_string(#[case] string: &str, #[case] expect: UrlTarget, rp: RustyPipe) {
let target = tokio_test::block_on(rp.query().resolve_string(string, true)).unwrap(); let target = tokio_test::block_on(rp.query().resolve_string(string, true)).unwrap();
assert_eq!(target, expect); assert_eq!(target, expect);
@ -1424,8 +1426,8 @@ fn music_album_not_found(rp: RustyPipe) {
} }
#[rstest] #[rstest]
// TODO: fix this/swap artist #[case::basic_all("basic_all", "UC7cl4MmM6ZZ2TcFyMk_b4pg", true, 15, 2)]
// #[case::basic_all("basic_all", "UC7cl4MmM6ZZ2TcFyMk_b4pg", true, 15, 2)] // TODO: wait for A/B test 6 to stabilize
// #[case::basic("basic", "UC7cl4MmM6ZZ2TcFyMk_b4pg", false, 15, 2)] // #[case::basic("basic", "UC7cl4MmM6ZZ2TcFyMk_b4pg", false, 15, 2)]
#[case::no_more_albums("no_more_albums", "UCOR4_bSVIXPsGa4BbCSt60Q", true, 15, 0)] #[case::no_more_albums("no_more_albums", "UCOR4_bSVIXPsGa4BbCSt60Q", true, 15, 0)]
#[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", false, 13, 0)] #[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", false, 13, 0)]
@ -1500,6 +1502,26 @@ fn music_artist(
".similar_artists" => "[artists]", ".similar_artists" => "[artists]",
}); });
} }
// Fetch albums seperately
if name != "no_artist" {
let albums = tokio_test::block_on(rp.query().music_artist_albums(id)).unwrap();
let albums_expect = artist
.albums
.iter()
.map(|a| a.id.to_owned())
.collect::<HashSet<_>>();
let albums_got = albums
.iter()
.map(|a| a.id.to_owned())
.collect::<HashSet<_>>();
if all_albums {
assert_eq!(albums_got, albums_expect);
} else {
assert!(albums_expect.is_subset(&albums_expect));
}
}
} }
#[rstest] #[rstest]
@ -1513,6 +1535,17 @@ fn music_artist_not_found(rp: RustyPipe) {
); );
} }
#[rstest]
fn music_artist_albums_not_found(rp: RustyPipe) {
let err = tokio_test::block_on(rp.query().music_artist_albums("UC7cl4MmM6ZZ2TcFyMk_b4pq"))
.unwrap_err();
assert!(
matches!(err, Error::Extraction(ExtractionError::NotFound { .. })),
"got: {err}"
);
}
#[rstest] #[rstest]
#[case::default(false)] #[case::default(false)]
#[case::typo(true)] #[case::typo(true)]