From fc8bce43fd94bdf431eadf4a5d7b1a8a01fa9a4f Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 24 Nov 2022 21:34:30 +0100 Subject: [PATCH] fix: music item mapping, small refactor fix: music item mapping, small refactor --- src/client/response/music_item.rs | 373 +++++++++++++--------------- src/client/response/url_endpoint.rs | 39 ++- tests/youtube.rs | 26 +- 3 files changed, 235 insertions(+), 203 deletions(-) diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index 6ece502..35039ea 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -15,7 +15,7 @@ use crate::{ }; use super::{ - url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType}, + url_endpoint::{BrowseEndpointWrap, MusicPageType, NavigationEndpoint, PageType}, ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap, }; @@ -408,119 +408,30 @@ impl MusicListMapper { let c2 = columns.next(); let c3 = columns.next(); - match item.navigation_endpoint { - // Artist / Album / Playlist - Some(ne) => { - let mut subtitle_parts = c2 - .ok_or_else(|| "could not get subtitle".to_owned())? - .renderer - .text - .split(util::DOT_SEPARATOR) - .into_iter(); + let first_tn = item + .thumbnail + .music_thumbnail_renderer + .thumbnail + .thumbnails + .first(); - let (page_type, id) = match ne.music_page() { - Some(music_page) => music_page, - None => { - // Ignore radio items - if subtitle_parts.len() == 1 { - return Ok(None); - } - return Err("invalid navigation endpoint".to_string()); - } - }; + let pt_id = item + .navigation_endpoint + .and_then(|ne| ne.music_page()) + .or_else(|| { + item.playlist_item_data + .map(|d| (MusicPageType::Track, d.video_id)) + }) + .or_else(|| { + first_tn.and_then(|tn| { + util::video_id_from_thumbnail_url(&tn.url) + .map(|id| (MusicPageType::Track, id)) + }) + }); - let title = - title.ok_or_else(|| format!("track {}: could not get title", id))?; - - let subtitle_p1 = subtitle_parts.next(); - let subtitle_p2 = subtitle_parts.next(); - let subtitle_p3 = subtitle_parts.next(); - - match page_type { - PageType::Artist => { - let subscriber_count = subtitle_p2.and_then(|p| { - util::parse_large_numstr(p.first_str(), self.lang) - }); - - self.items.push(MusicItem::Artist(ArtistItem { - id, - name: title, - avatar: item.thumbnail.into(), - subscriber_count, - })); - Ok(Some(MusicEntityType::Artist)) - } - PageType::Album => { - let album_type = subtitle_p1 - .map(|st| map_album_type(st.first_str(), self.lang)) - .unwrap_or_default(); - - let (artists, by_va) = map_artists(subtitle_p2); - - let year = subtitle_p3 - .and_then(|st| util::parse_numeric(st.first_str()).ok()); - - self.items.push(MusicItem::Album(AlbumItem { - id, - name: title, - cover: item.thumbnail.into(), - artists, - album_type, - year, - by_va, - })); - Ok(Some(MusicEntityType::Album)) - } - PageType::Playlist => { - // Part 1 may be the "Playlist" label - let (channel_p, tcount_p) = match subtitle_p3 { - Some(_) => (subtitle_p2, subtitle_p3), - None => (subtitle_p1, subtitle_p2), - }; - - let from_ytm = channel_p - .as_ref() - .map(|p| p.first_str() == util::YT_MUSIC_NAME) - .unwrap_or_default(); - let channel = channel_p.and_then(|p| { - p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) - }); - let track_count = - tcount_p.and_then(|p| util::parse_numeric(p.first_str()).ok()); - - self.items.push(MusicItem::Playlist(MusicPlaylistItem { - id, - name: title, - thumbnail: item.thumbnail.into(), - channel, - track_count, - from_ytm, - })); - Ok(Some(MusicEntityType::Playlist)) - } - PageType::Channel => { - // There may be broken YT channels from the artist search. They can be skipped. - Ok(None) - } - } - } + match pt_id { // Track - None => { - let first_tn = item - .thumbnail - .music_thumbnail_renderer - .thumbnail - .thumbnails - .first(); - - let id = item - .playlist_item_data - .map(|d| d.video_id) - .or_else(|| { - first_tn.and_then(|tn| util::video_id_from_thumbnail_url(&tn.url)) - }) - .ok_or_else(|| "no video id".to_owned())?; - + Some((MusicPageType::Track, id)) => { let title = title.ok_or_else(|| format!("track {}: could not get title", id))?; @@ -633,6 +544,92 @@ impl MusicListMapper { })); Ok(Some(MusicEntityType::Track)) } + // Artist / Album / Playlist + Some((page_type, id)) => { + let mut subtitle_parts = c2 + .ok_or_else(|| "could not get subtitle".to_owned())? + .renderer + .text + .split(util::DOT_SEPARATOR) + .into_iter(); + + let title = + title.ok_or_else(|| format!("track {}: could not get title", id))?; + + let subtitle_p1 = subtitle_parts.next(); + let subtitle_p2 = subtitle_parts.next(); + let subtitle_p3 = subtitle_parts.next(); + + match page_type { + MusicPageType::Artist => { + let subscriber_count = subtitle_p2.and_then(|p| { + util::parse_large_numstr(p.first_str(), self.lang) + }); + + self.items.push(MusicItem::Artist(ArtistItem { + id, + name: title, + avatar: item.thumbnail.into(), + subscriber_count, + })); + Ok(Some(MusicEntityType::Artist)) + } + MusicPageType::Album => { + let album_type = subtitle_p1 + .map(|st| map_album_type(st.first_str(), self.lang)) + .unwrap_or_default(); + + let (artists, by_va) = map_artists(subtitle_p2); + + let year = subtitle_p3 + .and_then(|st| util::parse_numeric(st.first_str()).ok()); + + self.items.push(MusicItem::Album(AlbumItem { + id, + name: title, + cover: item.thumbnail.into(), + artists, + album_type, + year, + by_va, + })); + Ok(Some(MusicEntityType::Album)) + } + MusicPageType::Playlist => { + // Part 1 may be the "Playlist" label + let (channel_p, tcount_p) = match subtitle_p3 { + Some(_) => (subtitle_p2, subtitle_p3), + None => (subtitle_p1, subtitle_p2), + }; + + let from_ytm = channel_p + .as_ref() + .map(|p| p.first_str() == util::YT_MUSIC_NAME) + .unwrap_or_default(); + let channel = channel_p.and_then(|p| { + p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) + }); + let track_count = + tcount_p.and_then(|p| util::parse_numeric(p.first_str()).ok()); + + self.items.push(MusicItem::Playlist(MusicPlaylistItem { + id, + name: title, + thumbnail: item.thumbnail.into(), + channel, + track_count, + from_ytm, + })); + Ok(Some(MusicEntityType::Playlist)) + } + MusicPageType::None => { + // There may be broken YT channels from the artist search. They can be skipped. + Ok(None) + } + MusicPageType::Track => unreachable!(), + } + } + None => Err("could not determine item type".to_owned()), } } // Tile @@ -642,44 +639,45 @@ impl MusicListMapper { let subtitle_p2 = subtitle_parts.next(); let subtitle_p3 = subtitle_parts.next(); - match item.navigation_endpoint.watch_endpoint { - // Music video - Some(wep) => { - let artists = map_artists(subtitle_p1).0; + match item.navigation_endpoint.music_page() { + Some((page_type, id)) => match page_type { + MusicPageType::Track => { + let artists = map_artists(subtitle_p1).0; - self.items.push(MusicItem::Track(TrackItem { - id: wep.video_id, - title: item.title, - duration: None, - cover: item.thumbnail_renderer.into(), - artist_id: artists.first().and_then(|a| a.id.to_owned()), - artists, - album: None, - view_count: subtitle_p2 - .and_then(|c| util::parse_large_numstr(c.first_str(), self.lang)), - is_video: true, - track_nr: None, - })); - Ok(Some(MusicEntityType::Track)) - } - // Artist / Album / Playlist - None => { - let (page_type, id) = item - .navigation_endpoint - .music_page() - .ok_or_else(|| "could not get navigation endpoint".to_owned())?; + self.items.push(MusicItem::Track(TrackItem { + id, + title: item.title, + duration: None, + cover: item.thumbnail_renderer.into(), + artist_id: artists.first().and_then(|a| a.id.to_owned()), + artists, + album: None, + view_count: subtitle_p2.and_then(|c| { + util::parse_large_numstr(c.first_str(), self.lang) + }), + is_video: true, + track_nr: None, + })); + Ok(Some(MusicEntityType::Track)) + } + MusicPageType::Artist => { + let subscriber_count = subtitle_p1 + .and_then(|p| util::parse_large_numstr(p.first_str(), self.lang)); - match page_type { - PageType::Album => { - let mut year = None; - let mut album_type = AlbumType::Single; + self.items.push(MusicItem::Artist(ArtistItem { + id, + name: item.title, + avatar: item.thumbnail_renderer.into(), + subscriber_count, + })); + Ok(Some(MusicEntityType::Artist)) + } + MusicPageType::Album => { + let mut year = None; + let mut album_type = AlbumType::Single; - let (artists, by_va) = match ( - subtitle_p1, - subtitle_p2, - &self.artists, - self.artist_page, - ) { + let (artists, by_va) = + match (subtitle_p1, subtitle_p2, &self.artists, self.artist_page) { // "2022" (Artist singles) (Some(year_txt), None, Some(artists), true) => { year = util::parse_numeric(year_txt.first_str()).ok(); @@ -706,56 +704,41 @@ impl MusicListMapper { } }; - self.items.push(MusicItem::Album(AlbumItem { - id, - name: item.title, - cover: item.thumbnail_renderer.into(), - artists, - album_type, - year, - by_va, - })); - Ok(Some(MusicEntityType::Album)) - } - PageType::Playlist => { - let from_ytm = subtitle_p2 - .as_ref() - .map(|p| p.first_str() == util::YT_MUSIC_NAME) - .unwrap_or_default(); - let channel = subtitle_p2.and_then(|p| { - p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) - }); - let track_count = subtitle_p3 - .and_then(|p| util::parse_numeric(p.first_str()).ok()); - - self.items.push(MusicItem::Playlist(MusicPlaylistItem { - id, - name: item.title, - thumbnail: item.thumbnail_renderer.into(), - channel, - track_count, - from_ytm, - })); - Ok(Some(MusicEntityType::Playlist)) - } - PageType::Artist => { - let subscriber_count = subtitle_p1.and_then(|p| { - util::parse_large_numstr(p.first_str(), self.lang) - }); - - self.items.push(MusicItem::Artist(ArtistItem { - id, - name: item.title, - avatar: item.thumbnail_renderer.into(), - subscriber_count, - })); - Ok(Some(MusicEntityType::Artist)) - } - PageType::Channel => { - Err(format!("channel items unsupported. id: {}", id)) - } + self.items.push(MusicItem::Album(AlbumItem { + id, + name: item.title, + cover: item.thumbnail_renderer.into(), + artists, + album_type, + year, + by_va, + })); + Ok(Some(MusicEntityType::Album)) } - } + MusicPageType::Playlist => { + let from_ytm = subtitle_p2 + .as_ref() + .map(|p| p.first_str() == util::YT_MUSIC_NAME) + .unwrap_or_default(); + let channel = subtitle_p2.and_then(|p| { + p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) + }); + let track_count = + subtitle_p3.and_then(|p| util::parse_numeric(p.first_str()).ok()); + + self.items.push(MusicItem::Playlist(MusicPlaylistItem { + id, + name: item.title, + thumbnail: item.thumbnail_renderer.into(), + channel, + track_count, + from_ytm, + })); + Ok(Some(MusicEntityType::Playlist)) + } + MusicPageType::None => Ok(None), + }, + None => Err("could not determine item type".to_owned()), } } } diff --git a/src/client/response/url_endpoint.rs b/src/client/response/url_endpoint.rs index 0627324..09713e1 100644 --- a/src/client/response/url_endpoint.rs +++ b/src/client/response/url_endpoint.rs @@ -32,6 +32,7 @@ pub(crate) struct NavigationEndpoint { #[serde(rename_all = "camelCase")] pub(crate) struct WatchEndpoint { pub video_id: String, + pub playlist_id: Option, #[serde(default)] pub start_time_seconds: u32, } @@ -146,17 +147,51 @@ impl PageType { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) enum MusicPageType { + Artist, + Album, + Playlist, + Track, + None, +} + +impl From for MusicPageType { + fn from(t: PageType) -> Self { + match t { + PageType::Artist => MusicPageType::Artist, + PageType::Album => MusicPageType::Album, + PageType::Playlist => MusicPageType::Playlist, + PageType::Channel => MusicPageType::None, + } + } +} + impl NavigationEndpoint { - pub(crate) fn music_page(self) -> Option<(PageType, String)> { + pub(crate) fn music_page(self) -> Option<(MusicPageType, String)> { match self.browse_endpoint { Some(browse) => match browse.browse_endpoint_context_supported_configs { Some(config) => Some(( - config.browse_endpoint_context_music_config.page_type, + config.browse_endpoint_context_music_config.page_type.into(), browse.browse_id, )), None => None, }, None => None, } + .or_else(|| { + self.watch_endpoint.map(|watch| { + if watch + .playlist_id + .map(|plid| plid.starts_with("RDQM")) + .unwrap_or_default() + { + // Genre radios (e.g. "pop radio") will be skipped + (MusicPageType::None, watch.video_id) + } else { + (MusicPageType::Track, watch.video_id) + } + }) + }) } } diff --git a/tests/youtube.rs b/tests/youtube.rs index ea980b3..685492d 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -1700,12 +1700,14 @@ async fn music_search_artists() { let rp = RustyPipe::builder().strict().build(); let res = rp.query().music_search_artists("namika").await.unwrap(); - let artist = res + let (i, artist) = res .items .items .iter() - .find(|a| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg") + .enumerate() + .find(|(_, a)| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg") .unwrap(); + assert!(i < 3); assert_eq!(artist.name, "Namika"); assert!(!artist.avatar.is_empty(), "got no avatar"); assert!( @@ -1741,9 +1743,15 @@ async fn music_search_playlists(#[case] with_community: bool) { }; assert_eq!(res.corrected_query, None); - let playlist = &res.items.items[0]; + let (i, playlist) = res + .items + .items + .iter() + .enumerate() + .find(|(_, p)| p.id == "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk") + .unwrap(); - assert_eq!(playlist.id, "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"); + assert!(i < 3); assert_eq!(playlist.name, "Easy Pop"); assert!(!playlist.thumbnail.is_empty(), "got no thumbnail"); assert_gte(playlist.track_count.unwrap(), 80, "tracks"); @@ -1761,9 +1769,15 @@ async fn music_search_playlists_community() { .unwrap(); assert_eq!(res.corrected_query, None); - let playlist = &res.items.items[0]; + let (i, playlist) = res + .items + .items + .iter() + .enumerate() + .find(|(_, p)| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u") + .unwrap(); - assert_eq!(playlist.id, "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u"); + assert!(i < 3); assert_eq!( playlist.name, "Best Pop Music Videos - Top Pop Hits Playlist"