feat: add track details, radios
This commit is contained in:
parent
556575f5ff
commit
e4046aef00
22 changed files with 19960 additions and 30 deletions
|
|
@ -24,6 +24,8 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
|||
- [X] **Artist**
|
||||
- [X] **Search**
|
||||
- [ ] **Search suggestions**
|
||||
- [ ] **Radio**
|
||||
- [ ] **Track details**
|
||||
- [ ] **Moods**
|
||||
- [ ] **Charts**
|
||||
- [ ] **New**
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ pub async fn download_testfiles(project_root: &Path) {
|
|||
music_search_playlists(&testfiles).await;
|
||||
music_search_cont(&testfiles).await;
|
||||
music_artist(&testfiles).await;
|
||||
music_details(&testfiles).await;
|
||||
music_radio(&testfiles).await;
|
||||
music_radio_cont(&testfiles).await;
|
||||
}
|
||||
|
||||
const CLIENT_TYPES: [ClientType; 5] = [
|
||||
|
|
@ -700,8 +702,8 @@ async fn music_artist(testfiles: &Path) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn music_radio(testfiles: &Path) {
|
||||
for (name, id) in [("mv", "RDAMVMZeerrnuLi5E"), ("track", "RDAMVM7nigXQS1Xb0")] {
|
||||
async fn music_details(testfiles: &Path) {
|
||||
for (name, id) in [("mv", "ZeerrnuLi5E"), ("track", "7nigXQS1Xb0")] {
|
||||
let mut json_path = testfiles.to_path_buf();
|
||||
json_path.push("music_details");
|
||||
json_path.push(format!("details_{}.json", name));
|
||||
|
|
@ -709,7 +711,36 @@ async fn music_radio(testfiles: &Path) {
|
|||
continue;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_details(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn music_radio(testfiles: &Path) {
|
||||
for (name, id) in [("mv", "RDAMVMZeerrnuLi5E"), ("track", "RDAMVM7nigXQS1Xb0")] {
|
||||
let mut json_path = testfiles.to_path_buf();
|
||||
json_path.push("music_details");
|
||||
json_path.push(format!("radio_{}.json", name));
|
||||
if json_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
rp.query().music_radio(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn music_radio_cont(testfiles: &Path) {
|
||||
let mut json_path = testfiles.to_path_buf();
|
||||
json_path.push("music_details");
|
||||
json_path.push("radio_cont.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
let res = rp.query().music_radio("RDAMVM7nigXQS1Xb0").await.unwrap();
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
res.next(&rp.query()).await.unwrap().unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
Track radio: RDAMVM + video id
|
||||
Example: RDAMVMZeerrnuLi5E
|
||||
|
||||
Artist radio: RDEMieiteXw81tMLBdKv8qkChg
|
||||
ID has to be extracted from artist page
|
||||
|
||||
Playlist/album radio: RDAMPL + playlist id
|
||||
|
||||
Genre radio: RDQM1xqCV6EdPUw
|
||||
|
|
@ -70,3 +70,9 @@ Single: MPREb_bHfHGoy7vuv
|
|||
EP: MPREb_u1I69lSAe5v
|
||||
Audiobook: MPREb_gaoNzsQHedo
|
||||
Show: MPREb_cwzk8EUwypZ
|
||||
|
||||
# Radios
|
||||
Track radio (Autoplay): RDAMVM + video id, example: RDAMVMZeerrnuLi5E
|
||||
Artist radio: RDEMieiteXw81tMLBdKv8qkChg (ID from artist page)
|
||||
Playlist/album radio: RDAMPL + playlist id
|
||||
Genre radio: RDQM1xqCV6EdPUw
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
@ -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"),
|
||||
)
|
||||
|
|
@ -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
File diff suppressed because it is too large
Load diff
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1147
testfiles/music_details/details_mv.json
Normal file
1147
testfiles/music_details/details_mv.json
Normal file
File diff suppressed because it is too large
Load diff
1211
testfiles/music_details/details_track.json
Normal file
1211
testfiles/music_details/details_track.json
Normal file
File diff suppressed because it is too large
Load diff
13600
testfiles/music_details/radio_cont.json
Normal file
13600
testfiles/music_details/radio_cont.json
Normal file
File diff suppressed because it is too large
Load diff
25
tests/snapshots/youtube__music_details_mv.snap
Normal file
25
tests/snapshots/youtube__music_details_mv.snap
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: tests/youtube.rs
|
||||
expression: track
|
||||
---
|
||||
TrackDetails(
|
||||
track: TrackItem(
|
||||
id: "ZeerrnuLi5E",
|
||||
title: "Black Mamba",
|
||||
duration: Some(230),
|
||||
cover: "[cover]",
|
||||
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"),
|
||||
)
|
||||
28
tests/snapshots/youtube__music_details_track.snap
Normal file
28
tests/snapshots/youtube__music_details_track.snap
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: tests/youtube.rs
|
||||
expression: track
|
||||
---
|
||||
TrackDetails(
|
||||
track: TrackItem(
|
||||
id: "7nigXQS1Xb0",
|
||||
title: "INVU",
|
||||
duration: Some(205),
|
||||
cover: "[cover]",
|
||||
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"),
|
||||
)
|
||||
|
|
@ -1509,9 +1509,8 @@ async fn music_search(#[case] typo: bool) {
|
|||
assert_eq!(res.corrected_query, None);
|
||||
}
|
||||
|
||||
let track = &res.tracks[0];
|
||||
dbg!(&track);
|
||||
assert_eq!(track.id, "ZeerrnuLi5E");
|
||||
let track = res.tracks.iter().find(|t| t.id == "ZeerrnuLi5E").unwrap();
|
||||
|
||||
assert_eq!(track.title, "Black Mamba");
|
||||
assert_eq!(track.duration.unwrap(), 230);
|
||||
assert!(!track.cover.is_empty(), "got no cover");
|
||||
|
|
@ -1755,6 +1754,39 @@ async fn music_search_genre_radio() {
|
|||
rp.query().music_search("pop radio").await.unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::mv("mv", "ZeerrnuLi5E")]
|
||||
#[case::track("track", "7nigXQS1Xb0")]
|
||||
#[tokio::test]
|
||||
async fn music_details(#[case] name: &str, #[case] id: &str) {
|
||||
let rp = RustyPipe::builder().strict().build();
|
||||
let track = rp.query().music_details(id).await.unwrap();
|
||||
|
||||
assert!(!track.track.cover.is_empty(), "got no cover");
|
||||
|
||||
insta::assert_ron_snapshot!(format!("music_details_{}", name), track,
|
||||
{".track.cover" => "[cover]"}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn music_radio_track() {
|
||||
let rp = RustyPipe::builder().strict().build();
|
||||
let tracks = rp.query().music_radio_track("ZeerrnuLi5E").await.unwrap();
|
||||
assert_next(tracks, &rp.query(), 20, 3).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn music_radio_playlist() {
|
||||
let rp = RustyPipe::builder().strict().build();
|
||||
let tracks = rp
|
||||
.query()
|
||||
.music_radio_playlist("PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_next(tracks, &rp.query(), 20, 1).await;
|
||||
}
|
||||
|
||||
//#TESTUTIL
|
||||
|
||||
/// Assert equality within 10% margin
|
||||
|
|
|
|||
Reference in a new issue