feat: add track details, radios

This commit is contained in:
ThetaDev 2022-11-10 23:19:11 +01:00
parent 556575f5ff
commit e4046aef00
22 changed files with 19960 additions and 30 deletions

View file

@ -1,18 +1,22 @@
use std::borrow::Cow;
use serde::Serialize;
use crate::{
error::{Error, ExtractionError},
model::MusicDetails,
model::{Paginator, TrackDetails, TrackItem},
param::Language,
serializer::MapResult,
};
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
use super::{
response::{self, music_item::map_queue_item},
ClientType, MapResponse, RustyPipeQuery, YTContext,
};
#[derive(Debug, Serialize)]
struct QMusicDetails<'a> {
context: YTContext<'a>,
// YouTube video ID
video_id: &'a str,
enable_persistent_playlist_panel: bool,
is_audio_only: bool,
@ -30,7 +34,27 @@ struct QRadio<'a> {
}
impl RustyPipeQuery {
pub async fn music_radio(&self, radio_id: &str) -> Result<MusicDetails, Error> {
pub async fn music_details(&self, video_id: &str) -> Result<TrackDetails, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QMusicDetails {
context,
video_id,
enable_persistent_playlist_panel: true,
is_audio_only: true,
tuner_setting_value: "AUTOMIX_SETTING_NORMAL",
};
self.execute_request::<response::MusicDetails, _, _>(
ClientType::DesktopMusic,
"music_details",
video_id,
"next",
&request_body,
)
.await
}
pub async fn music_radio(&self, radio_id: &str) -> Result<Paginator<TrackItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QRadio {
context,
@ -50,20 +74,194 @@ impl RustyPipeQuery {
)
.await
}
pub async fn music_radio_track(&self, video_id: &str) -> Result<Paginator<TrackItem>, Error> {
self.music_radio(&format!("RDAMVM{}", video_id)).await
}
pub async fn music_radio_playlist(
&self,
playlist_id: &str,
) -> Result<Paginator<TrackItem>, Error> {
self.music_radio(&format!("RDAMPL{}", playlist_id)).await
}
}
impl MapResponse<MusicDetails> for response::MusicDetails {
impl MapResponse<TrackDetails> for response::MusicDetails {
fn map_response(
self,
id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<MusicDetails>, ExtractionError> {
dbg!(&self);
) -> Result<MapResult<TrackDetails>, ExtractionError> {
let tabs = self
.contents
.single_column_music_watch_next_results_renderer
.tabbed_renderer
.watch_next_tabbed_results_renderer
.tabs;
let mut content = None;
let mut lyrics_id = None;
let mut related_id = None;
for t in tabs {
match (t.tab_renderer.content, t.tab_renderer.endpoint) {
(Some(tc), _) => {
content = Some(tc.music_queue_renderer.content.playlist_panel_renderer);
}
(_, Some(endpoint)) => {
match endpoint
.browse_endpoint
.browse_endpoint_context_supported_configs
.browse_endpoint_context_music_config
.page_type
{
response::music_details::TabType::Lyrics => {
lyrics_id = Some(endpoint.browse_endpoint.browse_id);
}
response::music_details::TabType::Related => {
related_id = Some(endpoint.browse_endpoint.browse_id);
}
}
}
(None, None) => {}
}
}
let content = content.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?;
let track_item = content
.contents
.c
.into_iter()
.find_map(|item| match item {
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(track) => {
Some(track)
}
response::music_item::PlaylistPanelVideo::None => None,
})
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
let track = map_queue_item(track_item, lang);
if track.id != id {
return Err(ExtractionError::WrongResult(format!(
"got wrong video id {}, expected {}",
track.id, id
)));
}
Ok(MapResult {
c: MusicDetails {},
warnings: Vec::new(),
c: TrackDetails {
track,
lyrics_id,
related_id,
},
warnings: content.contents.warnings,
})
}
}
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
fn map_response(
self,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
let tabs = self
.contents
.single_column_music_watch_next_results_renderer
.tabbed_renderer
.watch_next_tabbed_results_renderer
.tabs;
let content = tabs
.into_iter()
.find_map(|t| t.tab_renderer.content)
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
.music_queue_renderer
.content
.playlist_panel_renderer;
let tracks = content
.contents
.c
.into_iter()
.filter_map(|item| match item {
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
Some(map_queue_item(item, lang))
}
response::music_item::PlaylistPanelVideo::None => None,
})
.collect::<Vec<_>>();
let ctoken = content
.continuations
.into_iter()
.next()
.map(|c| c.next_continuation_data.continuation);
Ok(MapResult {
c: Paginator::new_ext(
None,
tracks,
ctoken,
None,
crate::param::ContinuationEndpoint::MusicNext,
),
warnings: content.contents.warnings,
})
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use rstest::rstest;
use super::*;
use crate::{model, param::Language};
#[rstest]
#[case::mv("mv", "ZeerrnuLi5E")]
#[case::track("track", "7nigXQS1Xb0")]
fn map_music_details(#[case] name: &str, #[case] id: &str) {
let filename = format!("testfiles/music_details/details_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let details: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::TrackDetails> =
details.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_details_{}", name), map_res.c);
}
#[rstest]
#[case::mv("mv", "RDAMVMZeerrnuLi5E")]
#[case::track("track", "RDAMVM7nigXQS1Xb0")]
fn map_music_radio(#[case] name: &str, #[case] id: &str) {
let filename = format!("testfiles/music_details/radio_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let radio: response::MusicDetails =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Paginator<TrackItem>> =
radio.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_radio_{}", name), map_res.c);
}
}

View file

@ -6,7 +6,7 @@ use crate::param::ContinuationEndpoint;
use crate::serializer::MapResult;
use crate::util::TryRemove;
use super::response::music_item::MusicListMapper;
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
impl RustyPipeQuery {
@ -152,6 +152,15 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
}
}
}
response::music_item::ContinuationContents::PlaylistPanelContinuation(mut panel) => {
continuations.append(&mut panel.continuations);
mapper.add_warnings(&mut panel.contents.warnings);
panel.contents.c.into_iter().for_each(|item| {
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
mapper.add_item(MusicItem::Track(map_queue_item(item, lang)))
}
});
}
}
let map_res = mapper.items();
@ -355,6 +364,7 @@ mod tests {
#[rstest]
#[case("playlist_tracks", "music_playlist/playlist_cont")]
#[case("search_tracks", "music_search/tracks_cont")]
#[case("radio_tracks", "music_details/radio_cont")]
fn map_continuation_tracks(#[case] name: &str, #[case] path: &str) {
let filename = format!("testfiles/{}.json", path);
let json_path = Path::new(&filename);

View file

@ -213,6 +213,7 @@ pub(crate) struct RichGridContinuation {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicContinuationData {
#[serde(alias = "nextRadioContinuationData")]
pub next_continuation_data: MusicContinuationDataInner,
}

View file

@ -1,6 +1,93 @@
use serde::Deserialize;
use super::{music_item::PlaylistPanelRenderer, ContentRenderer};
/// Response model for YouTube Music track details
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicDetails {}
pub(crate) struct MusicDetails {
pub contents: Contents,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents {
pub single_column_music_watch_next_results_renderer: WatchNextResultsRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WatchNextResultsRenderer {
pub tabbed_renderer: TabbedRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabbedRenderer {
pub watch_next_tabbed_results_renderer: TabbedRendererInner,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabbedRendererInner {
pub tabs: Vec<Tab>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Tab {
pub tab_renderer: TabRenderer,
}
/// Watch next tab
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabRenderer {
pub content: Option<TabContent>,
pub endpoint: Option<TabEndpoint>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabEndpoint {
pub browse_endpoint: TabBrowseEndpoint,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabBrowseEndpoint {
pub browse_id: String,
pub browse_endpoint_context_supported_configs: TabBrowseEndpointSupportedConfigs,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabBrowseEndpointSupportedConfigs {
pub browse_endpoint_context_music_config: TabBrowseEndpointMusicConfig,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabBrowseEndpointMusicConfig {
pub page_type: TabType,
}
#[derive(Debug, Deserialize)]
pub(crate) enum TabType {
#[serde(rename = "MUSIC_PAGE_TYPE_TRACK_LYRICS")]
Lyrics,
#[serde(rename = "MUSIC_PAGE_TYPE_TRACK_RELATED")]
Related,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabContent {
pub music_queue_renderer: ContentRenderer<PlaylistPanel>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistPanel {
pub playlist_panel_renderer: PlaylistPanelRenderer,
}

View file

@ -17,7 +17,7 @@ use crate::{
use super::{
url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType},
ContentsRenderer, MusicContinuationData, ThumbnailsWrap,
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
};
#[serde_as]
@ -176,6 +176,48 @@ pub(crate) struct CoverMusicItem {
pub navigation_endpoint: NavigationEndpoint,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PlaylistPanelRenderer {
#[serde_as(as = "VecLogError<_>")]
pub contents: MapResult<Vec<PlaylistPanelVideo>>,
/// Continuation token for fetching more radio items
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub continuations: Vec<MusicContinuationData>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum PlaylistPanelVideo {
PlaylistPanelVideoRenderer(QueueMusicItem),
#[serde(other, deserialize_with = "ignore_any")]
None,
}
/// Music item from a playback queue (`playlistPanelVideoRenderer`)
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct QueueMusicItem {
pub video_id: String,
#[serde_as(as = "Text")]
pub title: String,
#[serde_as(as = "Option<Text>")]
pub length_text: Option<String>,
/// Artist + Album + Year (for tracks)
/// `<"IVE">, " • ", <"LOVE DIVE (LOVE DIVE)">, " • ", "2022"`
///
/// Artist + view count + like count (for videos)
/// `<"aespa">, " • ", "250M views", " • ", "3.6M likes"`
#[serde(default)]
pub long_byline_text: TextComponents,
#[serde(default)]
pub thumbnail: Thumbnails,
pub menu: Option<MusicItemMenu>,
}
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicThumbnailRenderer {
@ -236,10 +278,12 @@ pub(crate) struct MusicContinuation {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::enum_variant_names)]
pub(crate) enum ContinuationContents {
#[serde(alias = "musicPlaylistShelfContinuation")]
MusicShelfContinuation(MusicShelf),
SectionListContinuation(ContentsRenderer<ItemSection>),
PlaylistPanelContinuation(PlaylistPanelRenderer),
}
#[derive(Debug, Deserialize)]
@ -712,6 +756,14 @@ impl MusicListMapper {
etype
}
pub fn add_item(&mut self, item: MusicItem) {
self.items.push(item);
}
pub fn add_warnings(&mut self, warnings: &mut Vec<String>) {
self.warnings.append(warnings);
}
pub fn items(self) -> MapResult<Vec<MusicItem>> {
MapResult {
c: self.items,
@ -783,7 +835,7 @@ pub(crate) fn map_artists(artists_p: Option<TextComponents>) -> (Vec<ArtistId>,
(artists, by_va)
}
fn map_artist_id(
pub(crate) fn map_artist_id(
menu: Option<MusicItemMenu>,
fallback_artist: Option<&ArtistId>,
) -> Option<String> {
@ -816,6 +868,49 @@ pub(crate) fn map_album_type(txt: &str, lang: Language) -> AlbumType {
.unwrap_or_default()
}
pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> TrackItem {
let mut subtitle_parts = item.long_byline_text.split(util::DOT_SEPARATOR).into_iter();
let is_video = !item
.thumbnail
.thumbnails
.first()
.map(|tn| tn.height == tn.width)
.unwrap_or_default();
let artist_p = subtitle_parts.next();
let (artists, _) = map_artists(artist_p);
let artist_id = map_artist_id(item.menu, artists.first());
let subtitle_p2 = subtitle_parts.next();
let (album, view_count) = if is_video {
(
None,
subtitle_p2.and_then(|p| util::parse_large_numstr(p.first_str(), lang)),
)
} else {
(
subtitle_p2.and_then(|p| p.0.into_iter().find_map(|c| AlbumId::try_from(c).ok())),
None,
)
};
TrackItem {
id: item.video_id,
title: item.title,
duration: item
.length_text
.and_then(|txt| util::parse_video_length(&txt)),
cover: item.thumbnail.into(),
artists,
artist_id,
album,
view_count,
is_video,
track_nr: None,
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path};

View file

@ -0,0 +1,41 @@
---
source: src/client/music_details.rs
expression: map_res.c
---
TrackDetails(
track: TrackItem(
id: "ZeerrnuLi5E",
title: "Black Mamba",
duration: Some(230),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/ZeerrnuLi5E/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3maNxpYzTFmXZBd8s1w1iE6rTBDaw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/ZeerrnuLi5E/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k5q17nduJ8-t3h9_obEVMVi8Cz3A",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/ZeerrnuLi5E/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k7CsaxHObhW1JXPtGyKE1fgSGZ3Q",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(235000000),
is_video: true,
track_nr: None,
),
lyrics_id: Some("MPLYt_wrKjTn9hmry"),
related_id: Some("MPTRt_wrKjTn9hmry"),
)

View file

@ -0,0 +1,59 @@
---
source: src/client/music_details.rs
expression: map_res.c
---
TrackDetails(
track: TrackItem(
id: "7nigXQS1Xb0",
title: "INVU",
duration: Some(205),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w180-h180-l90-rj",
width: 180,
height: 180,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w302-h302-l90-rj",
width: 302,
height: 302,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/2a1Lr46_ibhVNX5tZK_PnsmKFpB1ptZ9eUqtlcCXTRSxAcOLC7HpAO0pqyFJTttUPHAiYpqkTH251DIQ9A=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UCwzCuKxyMY_sT7hr1E8G1XA"),
name: "TAEYEON",
),
],
artist_id: Some("UCwzCuKxyMY_sT7hr1E8G1XA"),
album: Some(AlbumId(
id: "MPREb_4xbv14CiQJm",
name: "INVU - The 3rd Album",
)),
view_count: None,
is_video: false,
track_nr: None,
),
lyrics_id: Some("MPLYt_4xbv14CiQJm-1"),
related_id: Some("MPTRt_4xbv14CiQJm-1"),
)

View file

@ -0,0 +1,826 @@
---
source: src/client/music_details.rs
expression: map_res.c
---
Paginator(
count: None,
items: [
TrackItem(
id: "4TWR90KJl84",
title: "Next Level",
duration: Some(236),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/4TWR90KJl84/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kl3LTK647n1QMNk2ltojkKT5jR8w",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/4TWR90KJl84/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lCHWpapuNMHxDRnGHl_AKqq73fAw",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/4TWR90KJl84/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k49HRWAedtI0Zqb7Noov7jBviZig",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(250000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "Y8JFxS1HlDo",
title: "LOVE DIVE",
duration: Some(179),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Y8JFxS1HlDo/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k7dfJvms48b2vkzgD8IgO7NeY6cQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Y8JFxS1HlDo/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3llrmra1TaoqopbxBevNFRK_6Xc2w",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Y8JFxS1HlDo/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3moJU9Sl3QbDvSvlGR2Q2cngtnKMw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UC_4Y1QqJr60C5Z7-eQWy-mw"),
name: "IVE",
),
],
artist_id: Some("UC_4Y1QqJr60C5Z7-eQWy-mw"),
album: None,
view_count: Some(168000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "CM4CkVFmTds",
title: "I CAN\'T STOP ME",
duration: Some(221),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/CM4CkVFmTds/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3n_4nSKMrgw65E7qu7SXopvURCqLg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/CM4CkVFmTds/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mw6J7Z0DXh2ashrL5DBTZm5Z5sXA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/CM4CkVFmTds/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3laWy4cMXts0_azK9y7-nvHE-TTzQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCAq0pFGa2w9SjxOq0ZxKVIw"),
name: "TWICE",
),
],
artist_id: Some("UCAq0pFGa2w9SjxOq0ZxKVIw"),
album: None,
view_count: Some(464000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "_ysomCGaZLw",
title: "In the Morning",
duration: Some(185),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/_ysomCGaZLw/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mmHZHNUSSMjNtqYD5P3vpl3fhnTA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/_ysomCGaZLw/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kpPew9xvDjRS-Psi-SqcKDDVwbCw",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/_ysomCGaZLw/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k8Q_n6ukQ9LlhJzZ6gHskmdFFmhg",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
name: "ITZY",
),
],
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
album: None,
view_count: Some(230000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "gQlMMD8auMs",
title: "Pink Venom",
duration: Some(194),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/gQlMMD8auMs/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nEM0b-vFFexT2C8d4yzP8hQi60Sg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/gQlMMD8auMs/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3khUMi18G93F7jAInIz62E5CIBUFw",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/gQlMMD8auMs/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mkn0qyMCzW43mTrIGr6lana1WZpg",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCkbbMCA40i18i7UdjayMPAg"),
name: "BLACKPINK",
),
],
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
album: None,
view_count: Some(422000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "uR8Mrt1IpXg",
title: "Psycho",
duration: Some(216),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/uR8Mrt1IpXg/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mhufeImZ0Df0rKCh6-W4M5GF9tGg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/uR8Mrt1IpXg/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3neHndOeWLSL1Sb73WcnsA7Iiq0mg",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/uR8Mrt1IpXg/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mgmwQ-4E42UvQGZvyQP86E3eKUWw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCHmZYTfdTyVKQEJicLiXEOg"),
name: "Red Velvet",
),
],
artist_id: Some("UCHmZYTfdTyVKQEJicLiXEOg"),
album: None,
view_count: Some(349000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "PkKnp4SdE-w",
title: "Hot Sauce",
duration: Some(212),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/PkKnp4SdE-w/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kWfgwfdbEnHIDeILZPWhTgwuGDRw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/PkKnp4SdE-w/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mvnlVkFKrInqbVQbZu_ttrFbih4g",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/PkKnp4SdE-w/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kBZU4LKqDi5yxDZpP3dUeiPzZWXw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCuKdaTsJ9Jv94hVV_I9aRxQ"),
name: "NCT DREAM",
),
],
artist_id: Some("UCuKdaTsJ9Jv94hVV_I9aRxQ"),
album: None,
view_count: Some(167000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "4vbDFu0PUew",
title: "FEARLESS OFFICIAL M/V",
duration: Some(183),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/4vbDFu0PUew/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3khlrmZ55Elav20m6uPsZObHLhb1Q",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/4vbDFu0PUew/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lYxFh0M0OcTQEKuGinVKYcZYNGhg",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/4vbDFu0PUew/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kvF9SdDGxAVrh5AUiDmF1jW31bzg",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UC-clMkTZa7k-FxmNgMjoCgQ"),
name: "LE SSERAFIM",
),
],
artist_id: Some("UC-clMkTZa7k-FxmNgMjoCgQ"),
album: None,
view_count: Some(124000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "A5H8zBb3iao",
title: "90\'s Love",
duration: Some(227),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/A5H8zBb3iao/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3k_nifrnpSLq1wQ8WX1XEx2azAmJw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/A5H8zBb3iao/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ly97BC743uywuuUbBd27U6QgyYXw",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/A5H8zBb3iao/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kJoTPcNu84VWy0DE4qX83EmK6qXQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEf_Bc-KVd7onSeifS3py9g"),
name: "SMTOWN",
),
],
artist_id: Some("UCEf_Bc-KVd7onSeifS3py9g"),
album: None,
view_count: Some(127000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "_xJUCsyMQes",
title: "Best Friend (feat. Doja Cat)",
duration: Some(202),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/_xJUCsyMQes/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lR1nuc9rfKYua1azmFgfgI0NI_DA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/_xJUCsyMQes/sddefault.jpg?sqp=-oaymwEWCKoDEPABIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lUfitpkawiQB5Eh2qeIRKmck_H5Q",
width: 426,
height: 240,
),
],
artists: [
ArtistId(
id: Some("UCqTaQGqjAI6fYkr84KZgZEg"),
name: "Saweetie",
),
],
artist_id: Some("UCqTaQGqjAI6fYkr84KZgZEg"),
album: None,
view_count: Some(239000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "n0j5NPptyM0",
title: "WA DA DA",
duration: Some(198),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/n0j5NPptyM0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mPDQ3gbOoo_rjjKAzA6RL4atuimw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/n0j5NPptyM0/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3l7mg1I-bMPbXmQuFuTTQZcExRhLQ",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/n0j5NPptyM0/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3m3bKSefnosNWvGi7vR_1_ezSDbnw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCAKvDuIX3m1AUdPpDSqV_3w"),
name: "Kep1er",
),
],
artist_id: Some("UCAKvDuIX3m1AUdPpDSqV_3w"),
album: None,
view_count: Some(140000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "3GWscde8rM8",
title: "O.O",
duration: Some(214),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/3GWscde8rM8/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3njiYPlQmSjbSJxDZ2cazfxOFEw9Q",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/3GWscde8rM8/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lrAIY30SBN9UvKQ8CCLz5HQw2rZw",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/3GWscde8rM8/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ngOrZhqin3AJB9WWNRVnbH5eoT5Q",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UC_Cx288SDUD9liYn7CiJLAA"),
name: "NMIXX",
),
],
artist_id: Some("UC_Cx288SDUD9liYn7CiJLAA"),
album: None,
view_count: Some(90000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "uBY1AoiF5Vo",
title: "Step Back",
duration: Some(231),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/uBY1AoiF5Vo/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3ldz-mtiOTGWMsnjD7IqX9Q2SDDpA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/uBY1AoiF5Vo/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lu0lV6GzYKqCbUm8E-DPD715gTGQ",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/uBY1AoiF5Vo/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3n8F8MoVoc0RWYssIJ591eVxJrAgQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCDDpqmryjNunitS05bv7-8w"),
name: "GOT the beat",
),
],
artist_id: Some("UCDDpqmryjNunitS05bv7-8w"),
album: None,
view_count: Some(137000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "WPdWvnAAurg",
title: "Savage",
duration: Some(259),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/WPdWvnAAurg/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mTnrnrCkW0aHO0t5nRP1ukYRu6vg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/WPdWvnAAurg/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lzqbOcWJEoxbxHT6mLxbSDCx3kPA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/WPdWvnAAurg/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nyqKPkwPLJVpPofie4QX3y807Txw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(220000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "tyrVtwE8Gv0",
title: "Make A Wish (Birthday Song)",
duration: Some(249),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/tyrVtwE8Gv0/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lEIhBp-IAoTTCiDYJLIj4vtl8Hpw",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/tyrVtwE8Gv0/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mLBdpBoRAGP1SKLd_T2SOzM_Gn-g",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/tyrVtwE8Gv0/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lFYENgMq_4Ql9KLEShyeKm7mV2mQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCwPKPUAWE8ah0lkOcvNh8_Q"),
name: "NCT U",
),
],
artist_id: Some("UCwPKPUAWE8ah0lkOcvNh8_Q"),
album: None,
view_count: Some(258000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "Jh4QFaPmdss",
title: "TOMBOY",
duration: Some(198),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Jh4QFaPmdss/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nL0vQ4DGp4rNES4wtIQXf6MMcX4A",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Jh4QFaPmdss/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kTDFBdA22mhhPDMAxJPoFkm9bsLA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/Jh4QFaPmdss/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nDO6v2YyPEsuP9TlHOCXq0b8kq2Q",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCWT2ZfW7d8YI-HinHEVhyCA"),
name: "(G)I-DLE",
),
],
artist_id: Some("UCWT2ZfW7d8YI-HinHEVhyCA"),
album: None,
view_count: Some(181000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "2OvyA2__Eas",
title: "英雄; Kick It",
duration: Some(239),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/2OvyA2__Eas/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3n8rUV8L6DfEtliJa6uRI007X4ryg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/2OvyA2__Eas/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kbaxVY4Hkp0RwwvQtIf8V3kpBl0w",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/2OvyA2__Eas/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nNHdrJrMIksXZ5x2_nabxLC1STXA",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCjqYTQjO-JG-8vLlt6-4iyQ"),
name: "NCT 127",
),
],
artist_id: Some("UCjqYTQjO-JG-8vLlt6-4iyQ"),
album: None,
view_count: Some(165000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "dYRITmpFbJ4",
title: "Girls",
duration: Some(269),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/dYRITmpFbJ4/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mt44xFH24DkaQqASPBttEMuL02aQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/dYRITmpFbJ4/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3k1I7uOUx9-Rs_MiUFD2YWrbmAbJg",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/dYRITmpFbJ4/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kQC3YdpaKYJ5xLF1ryXjTN9wJ_3Q",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(108000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "POe9SOEKotk",
title: "Shut Down",
duration: Some(181),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/POe9SOEKotk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3m512hPlVaRZGGDe7lyzi4uYVVm2Q",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/POe9SOEKotk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kbAXOLxYByimodFUXOfH2mRh45lA",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/POe9SOEKotk/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mP1fmBP5TsNQ8Hkwi_oK9AKmGYNg",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCkbbMCA40i18i7UdjayMPAg"),
name: "BLACKPINK",
),
],
artist_id: Some("UCkbbMCA40i18i7UdjayMPAg"),
album: None,
view_count: Some(222000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "pSudEWBAYRE",
title: "Love Shot",
duration: Some(210),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/pSudEWBAYRE/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lAEYvImiSXeADO3bExIVXqZZ7GKQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/pSudEWBAYRE/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lWkqnBi3qqf4yWDXzR4qDUcuR7ow",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/pSudEWBAYRE/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3kCW9v4BsQjGfWRYYdO1xh6DMJwmg",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEUX9tUYqTFfPQdAgVNsKTA"),
name: "EXO",
),
],
artist_id: Some("UCEUX9tUYqTFfPQdAgVNsKTA"),
album: None,
view_count: Some(540000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "nnVjsos40qk",
title: "환상동화 (Secret Story of the Swan)",
duration: Some(202),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/nnVjsos40qk/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kn_BEjT7jYkFddBxe6yv0igyo-0Q",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/nnVjsos40qk/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nY9IGsviFkfgMsqPBhH2rEAGsGmQ",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/nnVjsos40qk/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lJEM3KYpj5POzRL7MQQBnbRMJIYA",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCG81UKNsFg9Perf0uPQOsQw"),
name: "IZ*ONE",
),
],
artist_id: Some("UCG81UKNsFg9Perf0uPQOsQw"),
album: None,
view_count: Some(90000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "H69tJmsgd9I",
title: "Dreams Come True",
duration: Some(221),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/H69tJmsgd9I/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mWiXkBoELY5U1XWBMe2bEn1OFdgQ",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/H69tJmsgd9I/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3me8GohbnE0UckrhjJg5WtTVFgmfg",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/H69tJmsgd9I/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lKXzVOcOskWpZFhI60ZcbEEyEbiw",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
name: "aespa",
),
],
artist_id: Some("UCEdZAdnnKqbaHOlv8nM6OtA"),
album: None,
view_count: Some(90000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "0IBSemQmno8",
title: "ZOO",
duration: Some(189),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/0IBSemQmno8/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nq0HomQUvUAgv20Rb5KTkOOjYy-A",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/0IBSemQmno8/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3mHDReqIeTQ82otCPBovy0ye0LNSQ",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/0IBSemQmno8/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3koM0EYu_ZhdOjoKBDhhoQgTrpAUA",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: None,
name: "Taeyong, JENO, YANGYANG, 지젤 (GISELLE), and HENDERY",
),
],
artist_id: Some("UCDdCbqagfKo_euzzCV9G2EQ"),
album: None,
view_count: Some(71000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "MjCZfZfucEc",
title: "LOCO",
duration: Some(233),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/MjCZfZfucEc/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3lvN8V98wicGg5vG2F2zon-foZzIA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/MjCZfZfucEc/sddefault.jpg?sqp=-oaymwEWCKoDEPABIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mKJIqURQYCCY_G1XDJnDiyqRZ4kQ",
width: 426,
height: 240,
),
],
artists: [
ArtistId(
id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
name: "ITZY",
),
],
artist_id: Some("UCTP45_DE3fMLujU8sZ-MBzw"),
album: None,
view_count: Some(208000000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "tg2uF3R_Ozo",
title: "DUMB DUMB",
duration: Some(179),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/tg2uF3R_Ozo/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mYPwfsMuBxT19qgv_XmSk2H79jvg",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/tg2uF3R_Ozo/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lqGADl8uyCCDtehV_LAgMphtc57g",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/tg2uF3R_Ozo/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3nHkXP82A1Qe5nY_OQL55o5vtkIOQ",
width: 853,
height: 480,
),
],
artists: [
ArtistId(
id: Some("UCDnYJA3OXXhRKYPe3jzLGeQ"),
name: "SOMI",
),
],
artist_id: Some("UCDnYJA3OXXhRKYPe3jzLGeQ"),
album: None,
view_count: Some(140000000),
is_video: true,
track_nr: None,
),
],
ctoken: Some("CBkSSBILdGcydUYzUl9Pem8iEVJEQU1WTVplZXJybnVMaTVFMg53QUVCOGdFQ2VBRSUzRDgY0AEB-gEQQzcxNUY2RDFGQjIwNEQwQRgKggEVUFQ6RWd0MFp6SjFSak5TWDA5NmJ3"),
endpoint: music_next,
)

File diff suppressed because it is too large Load diff

View file

@ -1234,4 +1234,8 @@ pub struct MusicSearchFiltered<T> {
/// Music track details
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicDetails {}
pub struct TrackDetails {
pub track: TrackItem,
pub lyrics_id: Option<String>,
pub related_id: Option<String>,
}

View file

@ -17,6 +17,7 @@ pub enum ContinuationEndpoint {
Next,
MusicBrowse,
MusicSearch,
MusicNext,
}
impl ContinuationEndpoint {
@ -24,14 +25,16 @@ impl ContinuationEndpoint {
match self {
ContinuationEndpoint::Browse | ContinuationEndpoint::MusicBrowse => "browse",
ContinuationEndpoint::Search | ContinuationEndpoint::MusicSearch => "search",
ContinuationEndpoint::Next => "next",
ContinuationEndpoint::Next | ContinuationEndpoint::MusicNext => "next",
}
}
pub(crate) fn is_music(self) -> bool {
matches!(
self,
ContinuationEndpoint::MusicBrowse | ContinuationEndpoint::MusicSearch
ContinuationEndpoint::MusicBrowse
| ContinuationEndpoint::MusicSearch
| ContinuationEndpoint::MusicNext
)
}
}