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 indicatif::{ProgressBar, ProgressStyle};
use num_enum::TryFromPrimitive;
use rustypipe::client::{ClientType, RustyPipe, YTContext};
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery, YTContext};
use rustypipe::model::YouTubeItem;
use rustypipe::param::search_filter::{ItemType, SearchFilter};
use serde::de::IgnoredAny;
@ -20,9 +20,14 @@ pub enum ABTest {
ChannelHandlesInSearchResults = 3,
TrendsVideoTab = 4,
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)]
pub struct ABTestRes {
@ -75,20 +80,16 @@ pub async fn run_test(
let http = http.clone();
async move {
let visitor_data = get_visitor_data(&http).await;
let query = rp.query().visitor_data(&visitor_data);
let is_present = match ab {
ABTest::AttributedTextDescription => {
attributed_text_description(&rp, &visitor_data).await
}
ABTest::ThreeTabChannelLayout => {
three_tab_channel_layout(&rp, &visitor_data).await
}
ABTest::AttributedTextDescription => attributed_text_description(&query).await,
ABTest::ThreeTabChannelLayout => three_tab_channel_layout(&query).await,
ABTest::ChannelHandlesInSearchResults => {
channel_handles_in_search_results(&rp, &visitor_data).await
}
ABTest::TrendsVideoTab => trends_video_tab(&rp, &visitor_data).await,
ABTest::TrendsPageHeaderRenderer => {
trends_page_header_renderer(&rp, &visitor_data).await
channel_handles_in_search_results(&query).await
}
ABTest::TrendsVideoTab => trends_video_tab(&query).await,
ABTest::TrendsPageHeaderRenderer => trends_page_header_renderer(&query).await,
ABTest::DiscographyPage => discography_page(&query).await,
}
.unwrap();
pb.inc(1);
@ -143,18 +144,15 @@ pub async fn run_all_tests(n: usize, concurrency: usize) -> Vec<ABTestRes> {
results
}
pub async fn attributed_text_description(rp: &RustyPipe, visitor_data: &str) -> Result<bool> {
let query = rp.query();
let context = query
.get_context(ClientType::Desktop, true, Some(visitor_data))
.await;
pub async fn attributed_text_description(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let q = QVideo {
context,
video_id: "ZeerrnuLi5E",
content_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\"") {
bail!("invalid response data");
@ -163,20 +161,13 @@ pub async fn attributed_text_description(rp: &RustyPipe, visitor_data: &str) ->
Ok(response_txt.contains("\"attributedDescription\""))
}
pub async fn three_tab_channel_layout(rp: &RustyPipe, visitor_data: &str) -> Result<bool> {
let channel = rp
.query()
.visitor_data(visitor_data)
.channel_videos("UCR-DXc1voovS8nhAvccRZhg")
.await
.unwrap();
pub async fn three_tab_channel_layout(rp: &RustyPipeQuery) -> Result<bool> {
let channel = rp.channel_videos("UCR-DXc1voovS8nhAvccRZhg").await.unwrap();
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
.query()
.visitor_data(visitor_data)
.search_filter("rust", &SearchFilter::new().item_type(ItemType::Channel))
.await
.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> {
let query = rp.query().visitor_data(visitor_data);
let context = query.get_context(ClientType::Desktop, true, None).await;
let res = query
pub async fn trends_video_tab(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let res = rp
.raw(
ClientType::Desktop,
"browse",
@ -208,10 +198,9 @@ pub async fn trends_video_tab(rp: &RustyPipe, visitor_data: &str) -> Result<bool
Ok(res.contains("\"4gIOGgxtb3N0X3BvcHVsYXI%3D\""))
}
pub async fn trends_page_header_renderer(rp: &RustyPipe, visitor_data: &str) -> Result<bool> {
let query = rp.query().visitor_data(visitor_data);
let context = query.get_context(ClientType::Desktop, true, None).await;
let res = query
pub async fn trends_page_header_renderer(rp: &RustyPipeQuery) -> Result<bool> {
let context = rp.get_context(ClientType::Desktop, true, None).await;
let res = rp
.raw(
ClientType::Desktop,
"browse",
@ -232,3 +221,12 @@ pub async fn trends_page_header_renderer(rp: &RustyPipe, visitor_data: &str) ->
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() {
for (name, id, all_albums) in [
("default", "UClmXPfaYhXOYsNn_QUyheWQ", true),
("no_more_albums", "UC_vmjW5e1xEHhYjY2a0kK1A", true),
("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", true),
("no_artist", "UCh8gHdtzO2tXd593_bjErWg", 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 regex::Regex;
@ -13,7 +12,7 @@ use crate::{
use super::{
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
ClientType, MapResponse, QBrowse, RustyPipeQuery,
};
impl RustyPipeQuery {
@ -26,99 +25,57 @@ impl RustyPipeQuery {
all_albums: bool,
) -> Result<MusicArtist, Error> {
let artist_id = artist_id.as_ref();
let visitor_data = if all_albums {
Some(self.get_visitor_data().await?)
} else {
None
};
let res = self._music_artist(artist_id, visitor_data.as_deref()).await;
let res = self._music_artist(artist_id, all_albums).await;
if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res {
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 {
res
}
}
async fn _music_artist(
&self,
artist_id: &str,
all_albums_vdata: Option<&str>,
) -> Result<MusicArtist, Error> {
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,
};
async fn _music_artist(&self, artist_id: &str, all_albums: bool) -> Result<MusicArtist, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: artist_id,
};
let (mut artist, album_page_params) = self
.execute_request::<response::MusicArtist, _, _>(
ClientType::DesktopMusic,
"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, _, _>(
if all_albums {
let (mut artist, can_fetch_more) = self
.execute_request::<response::MusicArtist, _, _>(
ClientType::DesktopMusic,
"music_artist",
artist_id,
"browse",
&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(
&self,
artist_id: &str,
params: &str,
visitor_data: &str,
) -> Result<Vec<AlbumItem>, Error> {
let context = self
.get_context(ClientType::DesktopMusic, true, Some(visitor_data))
.await;
let request_body = QBrowseParams {
/// Get a list of all albums of a YouTube Music artist
pub async fn music_artist_albums(&self, artist_id: &str) -> Result<Vec<AlbumItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: artist_id,
params,
browse_id: &format!("{}{}", util::ARTIST_DISCOGRAPHY_PREFIX, artist_id),
};
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(
self,
id: &str,
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> {
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
map_artist_page(self, id, lang, true)
}
}
@ -163,7 +120,7 @@ fn map_artist_page(
id: &str,
lang: crate::param::Language,
skip_extendables: bool,
) -> Result<MapResult<(MusicArtist, Vec<String>)>, ExtractionError> {
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
// dbg!(&res);
let header = res.header.music_immersive_header_renderer;
@ -203,7 +160,7 @@ fn map_artist_page(
let mut tracks_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 {
match section {
@ -242,6 +199,11 @@ fn map_artist_page(
videos_playlist_id = Some(bep.browse_id);
}
}
// Albums
PageType::ArtistDiscography => {
can_fetch_more = true;
extendable_albums = true;
}
// Albums or playlists
PageType::Artist => {
// 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| {
config.browse_endpoint_context_music_config.page_type
})}) {
album_page_params.push(bep.params);
can_fetch_more = true;
extendable_albums = true;
}
}
@ -318,7 +280,7 @@ fn map_artist_page(
videos_playlist_id,
radio_id,
},
album_page_params,
can_fetch_more,
),
warnings: mapped.warnings,
})
@ -333,6 +295,10 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
// dbg!(&self);
let Some(header) = self.header else {
return Err(ExtractionError::NotFound { id: id.into(), msg: "no header".into() });
};
let grids = self
.contents
.single_column_browse_results_renderer
@ -349,7 +315,7 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
lang,
ArtistId {
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]
#[case::default("default", "UClmXPfaYhXOYsNn_QUyheWQ")]
#[case::no_more_albums("no_more_albums", "UC_vmjW5e1xEHhYjY2a0kK1A")]
#[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw")]
#[case::no_artist("no_artist", "UCh8gHdtzO2tXd593_bjErWg")]
#[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_file = File::open(json_path).unwrap();
let mut album_page_paths = Vec::new();
for i in 1..=2 {
let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}_{i}.json"));
if !json_path.exists() {
break;
}
album_page_paths.push(json_path);
let mut album_page_path = None;
let json_path = path!(*TESTFILES / "music_artist" / format!("artist_{name}_1.json"));
if json_path.exists() {
album_page_path = Some(json_path);
}
let resp: response::MusicArtist =
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();
let (mut artist, album_page_params) = map_res.c;
let (mut artist, can_fetch_more) = map_res.c;
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping 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 {
let json_file = File::open(json_path).unwrap();
if let Some(album_page_path) = album_page_path {
let json_file = File::open(album_page_path).unwrap();
let resp: response::MusicArtistAlbums =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
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
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtistAlbums {
pub header: SimpleHeader,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub header: Option<SimpleHeader>,
pub contents: SingleColumnBrowseResult<Tab<SectionList<Grid>>>,
}

View file

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

View file

@ -7,27 +7,27 @@ MusicArtist(
name: "Doobydobap",
header_image: [
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,
height: 225,
),
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,
height: 340,
),
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,
height: 600,
),
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,
height: 800,
),
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,
height: 1015,
),
@ -39,16 +39,82 @@ MusicArtist(
albums: [],
playlists: [
MusicPlaylistItem(
id: "PLwkM1QxaP342hjju64dtqG5wKqx2hNgjr",
name: "After Hours & Doob Gourmand",
id: "PLwkM1QxaP341MxmqdPrw_3ffjqLofZXOj",
name: "🌟 best of... doobyvlog",
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,
height: 225,
),
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,
height: 450,
),
@ -62,15 +128,15 @@ MusicArtist(
),
MusicPlaylistItem(
id: "PLwkM1QxaP342v1hhoB3XLiruSQOzmdmBt",
name: "doobyvlog",
name: "📹 doobyvlog",
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,
height: 225,
),
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,
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)"),
wikipedia_url: Some("https://en.wikipedia.org/wiki/Sulli"),
subscriber_count: Some(74400),
subscriber_count: Some(80800),
tracks: [
TrackItem(
id: "BGcUVJXViqQ",
@ -156,7 +156,7 @@ MusicArtist(
],
artist_id: Some("UCfwCE5VhPMGxNPFxtVv7lRw"),
album: None,
view_count: Some(19000000),
view_count: Some(20000000),
is_video: true,
track_nr: None,
by_va: false,
@ -185,7 +185,7 @@ MusicArtist(
],
artist_id: Some("UClGBYGUZmpzUaHgeb9gOBww"),
album: None,
view_count: Some(206000),
view_count: Some(211000),
is_video: true,
track_nr: None,
by_va: false,
@ -209,7 +209,7 @@ MusicArtist(
artists: [
ArtistId(
id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
name: "iKissesByMaki",
name: "ramuditas",
),
],
artist_id: Some("UCfaO3pZL5XOr8BvNZkrKeVA"),
@ -243,152 +243,36 @@ MusicArtist(
],
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
album: None,
view_count: Some(3600),
view_count: Some(15000),
is_video: true,
track_nr: None,
by_va: false,
),
TrackItem(
id: "94q_2Zsq2os",
name: "Sulli - On The Moon (sped up)",
id: "N217ZuMQnfY",
name: "음악캠프 - Dorothy - Picnic, 도로시 - 소풍, Music Camp 20040417",
duration: None,
cover: [
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,
height: 225,
),
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,
height: 450,
),
],
artists: [
ArtistId(
id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
name: "Jpn Sch",
id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
name: "MBCkpop",
),
],
artist_id: Some("UCgVWicpO5Jn3VfxqgIU6cpA"),
artist_id: Some("UCe52oeb7Xv_KaJsEzcKXJJg"),
album: None,
view_count: Some(2900),
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),
view_count: Some(1200),
is_video: true,
track_nr: None,
by_va: false,
@ -417,7 +301,123 @@ MusicArtist(
],
artist_id: Some("UCFFvwAcyQhpeQfuAgBN1XZw"),
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,
track_nr: None,
by_va: false,
@ -454,89 +454,21 @@ MusicArtist(
playlists: [],
similar_artists: [
ArtistItem(
id: "UCHmZYTfdTyVKQEJicLiXEOg",
name: "Red Velvet",
id: "UCpUChkP-KE20GRsyfjU83_g",
name: "CHUNG HA",
avatar: [
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,
height: 226,
),
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,
height: 544,
),
],
subscriber_count: Some(5200000),
),
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),
subscriber_count: Some(1470000),
),
ArtistItem(
id: "UCmeskqhmPRuteGVH4yCXT0A",
@ -553,24 +485,7 @@ MusicArtist(
height: 544,
),
],
subscriber_count: Some(1480000),
),
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),
subscriber_count: Some(1490000),
),
ArtistItem(
id: "UCVXeNwNQs07XQ8d1HtvuxVg",
@ -587,41 +502,126 @@ MusicArtist(
height: 544,
),
],
subscriber_count: Some(26900),
subscriber_count: Some(31400),
),
ArtistItem(
id: "UCHHz6g3igy0BFUfCSiC_6aw",
name: "HyunA",
id: "UCvAUJvMNPy1LLVKpFiu-X9w",
name: "SEULGI",
avatar: [
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,
height: 226,
),
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,
height: 544,
),
],
subscriber_count: Some(3130000),
subscriber_count: Some(41100),
),
ArtistItem(
id: "UCveaR1hvVMrEQaKR6Xl8NDw",
name: "f(x)",
id: "UC277cpXD9BLeaGgh3yDP40w",
name: "블랙스완 BLACKSWAN",
avatar: [
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,
height: 226,
),
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,
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,

View file

@ -119,6 +119,14 @@ impl RustyPipeQuery {
Ok(UrlTarget::Channel { id: id.to_owned() })
} else if util::ALBUM_ID_REGEX.is_match(id) {
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 {
Err(Error::Other("invalid url: no browse id".into()))
}
@ -261,6 +269,14 @@ impl RustyPipeQuery {
}
} else if util::ALBUM_ID_REGEX.is_match(s) {
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
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 VARIOUS_ARTISTS: &str = "Various Artists";
pub const PLAYLIST_ID_ALBUM_PREFIX: &str = "OLAK";
pub const ARTIST_DISCOGRAPHY_PREFIX: &str = "MPAD";
const CONTENT_PLAYBACK_NONCE_ALPHABET: &[u8; 64] =
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::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/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/MPADUC7cl4MmM6ZZ2TcFyMk_b4pg", UrlTarget::Channel {id: "UC7cl4MmM6ZZ2TcFyMk_b4pg".to_owned()})]
fn resolve_url(#[case] url: &str, #[case] expect: UrlTarget, rp: RustyPipe) {
let target = tokio_test::block_on(rp.query().resolve_url(url, true)).unwrap();
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("OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", 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) {
let target = tokio_test::block_on(rp.query().resolve_string(string, true)).unwrap();
assert_eq!(target, expect);
@ -1424,8 +1426,8 @@ fn music_album_not_found(rp: RustyPipe) {
}
#[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::no_more_albums("no_more_albums", "UCOR4_bSVIXPsGa4BbCSt60Q", true, 15, 0)]
#[case::only_singles("only_singles", "UCfwCE5VhPMGxNPFxtVv7lRw", false, 13, 0)]
@ -1500,6 +1502,26 @@ fn music_artist(
".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]
@ -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]
#[case::default(false)]
#[case::typo(true)]