diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index e754e34..8c2d29f 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -5,9 +5,10 @@ use crate::{ error::{Error, ExtractionError}, model::{ paginator::{ContinuationEndpoint, Paginator}, + richtext::RichText, AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem, }, - serializer::MapResult, + serializer::{text::TextComponents, MapResult}, util::{self, TryRemove, DOT_SEPARATOR}, }; @@ -240,7 +241,7 @@ impl MapResponse for response::MusicPlaylist { channel, h.title, h.thumbnail.into(), - h.description.map(String::from), + h.description.map(TextComponents::from), ) } None => { @@ -276,7 +277,7 @@ impl MapResponse for response::MusicPlaylist { name, thumbnail, channel, - description, + description: description.map(RichText::from), track_count, from_ytm, tracks: Paginator::new_ext( @@ -361,14 +362,14 @@ impl MapResponse for response::MusicPlaylist { let mut subtitle_split = header.subtitle.split(util::DOT_SEPARATOR); let (year_txt, artists_p) = match header.strapline_text_one { + // New (2column) album layout Some(sl) => { let year_txt = subtitle_split - .swap_remove(1) - .0 - .first() - .map(|c| c.as_str().to_owned()); + .try_swap_remove(1) + .and_then(|t| t.0.first().map(|c| c.as_str().to_owned())); (year_txt, Some(sl)) } + // Old album layout None => match subtitle_split.len() { 3.. => { let year_txt = subtitle_split @@ -414,22 +415,32 @@ impl MapResponse for response::MusicPlaylist { } } + let playlist_id = self.microformat.and_then(|mf| { + mf.microformat_data_renderer + .url_canonical + .strip_prefix("https://music.youtube.com/playlist?list=") + .map(str::to_owned) + }); let (playlist_id, artist_id) = header .menu .or_else(|| header.buttons.into_iter().next()) .map(|menu| { ( - menu.menu_renderer - .top_level_buttons - .iter() - .find_map(|btn| map_playlist_id(&btn.button_renderer.navigation_endpoint)) - .or_else(|| { - menu.menu_renderer.items.iter().find_map(|itm| { - map_playlist_id( - &itm.menu_navigation_item_renderer.navigation_endpoint, - ) + playlist_id.or_else(|| { + menu.menu_renderer + .top_level_buttons + .iter() + .find_map(|btn| { + map_playlist_id(&btn.button_renderer.navigation_endpoint) }) - }), + .or_else(|| { + menu.menu_renderer.items.iter().find_map(|itm| { + map_playlist_id( + &itm.menu_navigation_item_renderer.navigation_endpoint, + ) + }) + }) + }), map_artist_id(menu.menu_renderer.items), ) }) @@ -464,7 +475,9 @@ impl MapResponse for response::MusicPlaylist { cover: header.thumbnail.into(), artists, artist_id, - description: header.description.map(String::from), + description: header + .description + .map(|t| RichText::from(TextComponents::from(t))), album_type, year, by_va, diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 39485c1..bb3c80e 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -6,8 +6,10 @@ use crate::{ error::{Error, ExtractionError}, model::{ paginator::{ContinuationEndpoint, Paginator}, + richtext::RichText, ChannelId, Playlist, VideoItem, }, + serializer::text::{TextComponent, TextComponents}, util::{self, timeago, TryRemove}, }; @@ -86,7 +88,7 @@ impl MapResponse for response::Playlist { let mut mapper = response::YouTubeListMapper::::new(lang); mapper.map_response(video_items); - let (thumbnails, last_update_txt) = match self.sidebar { + let (description, thumbnails, last_update_txt) = match self.sidebar { Some(sidebar) => { let sidebar_items = sidebar.playlist_sidebar_renderer.contents; let mut primary = @@ -98,6 +100,10 @@ impl MapResponse for response::Playlist { )))?; ( + primary + .playlist_sidebar_primary_info_renderer + .description + .filter(|d| !d.0.is_empty()), primary .playlist_sidebar_primary_info_renderer .thumbnail_renderer @@ -123,6 +129,7 @@ impl MapResponse for response::Playlist { .map(|b| b.playlist_byline_renderer.text); ( + None, header_banner.hero_playlist_thumbnail_renderer.thumbnail, last_update_txt, ) @@ -144,7 +151,14 @@ impl MapResponse for response::Playlist { } let name = header.playlist_header_renderer.title; - let description = header.playlist_header_renderer.description_text; + let description = description + .or_else(|| { + header + .playlist_header_renderer + .description_text + .map(|text| TextComponents(vec![TextComponent::Text { text }])) + }) + .map(RichText::from); let channel = header .playlist_header_renderer .owner_text diff --git a/src/client/response/music_playlist.rs b/src/client/response/music_playlist.rs index 734f6c4..b97ea13 100644 --- a/src/client/response/music_playlist.rs +++ b/src/client/response/music_playlist.rs @@ -11,11 +11,15 @@ use super::{ }; /// Response model for YouTube Music playlists and albums +#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MusicPlaylist { pub contents: Contents, pub header: Option
, + #[serde(default)] + #[serde_as(as = "DefaultOnError")] + pub microformat: Option, } #[serde_as] @@ -87,23 +91,20 @@ pub(crate) struct HeaderRenderer { pub buttons: Vec, } -#[serde_as] #[derive(Debug, Deserialize)] #[serde(untagged)] pub(crate) enum Description { - Text(#[serde_as(as = "Text")] String), #[serde(rename_all = "camelCase")] Shelf { music_description_shelf_renderer: DescriptionShelf, }, + Text(TextComponents), } -#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DescriptionShelf { - #[serde_as(as = "Text")] - pub description: String, + pub description: TextComponents, } #[derive(Debug, Deserialize)] @@ -123,7 +124,7 @@ pub(crate) struct HeaderMenuRenderer { pub items: Vec, } -impl From for String { +impl From for TextComponents { fn from(value: Description) -> Self { match value { Description::Text(v) => v, @@ -133,3 +134,15 @@ impl From for String { } } } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Microformat { + pub microformat_data_renderer: MicroformatData, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MicroformatData { + pub url_canonical: String, +} diff --git a/src/client/response/playlist.rs b/src/client/response/playlist.rs index fbc76b4..7d29635 100644 --- a/src/client/response/playlist.rs +++ b/src/client/response/playlist.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use serde_with::{serde_as, DefaultOnError}; -use crate::serializer::text::{Text, TextComponent}; +use crate::serializer::text::{Text, TextComponent, TextComponents}; use super::{ video_item::YouTubeListRenderer, Alert, ContentsRenderer, ResponseContext, SectionList, Tab, @@ -95,6 +95,7 @@ pub(crate) struct SidebarItemPrimary { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SidebarPrimaryInfoRenderer { + pub description: Option, pub thumbnail_renderer: PlaylistThumbnailRenderer, /// - `"495", " videos"` /// - `"3,310,996 views"` diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap index a969b23..d87dc5f 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_album_description.snap @@ -35,7 +35,11 @@ MusicAlbum( ), ], artist_id: Some("UCRw0x9_EfawqmgDI2IgQLLg"), - description: Some("25 is the third studio album by English singer-songwriter Adele, released on 20 November 2015 by XL Recordings and Columbia Records. The album is titled as a reflection of her life and frame of mind at 25 years old and is termed a \"make-up record\". Its lyrical content features themes of Adele \"yearning for her old self, her nostalgia\", and \"melancholia about the passage of time\" according to an interview with the singer by Rolling Stone, as well as themes of motherhood and regret. In contrast to Adele\'s previous works, the production of 25 incorporated the use of electronic elements and creative rhythmic patterns, with elements of 1980s R&B and organs. Like when recording 21, Adele worked with producer and songwriter Paul Epworth and Ryan Tedder, along with new collaborations with Max Martin and Shellback, Bruno Mars, Greg Kurstin, Danger Mouse, the Smeezingtons, Samuel Dixon, and Tobias Jesso Jr.\n25 received generally positive reviews from music critics, who commended its production and Adele\'s vocal performance.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/25_(Adele_album)) under Creative Commons Attribution CC-BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/legalcode)"), + description: Some(RichText([ + Text( + text: "25 is the third studio album by English singer-songwriter Adele, released on 20 November 2015 by XL Recordings and Columbia Records. The album is titled as a reflection of her life and frame of mind at 25 years old and is termed a \"make-up record\". Its lyrical content features themes of Adele \"yearning for her old self, her nostalgia\", and \"melancholia about the passage of time\" according to an interview with the singer by Rolling Stone, as well as themes of motherhood and regret. In contrast to Adele\'s previous works, the production of 25 incorporated the use of electronic elements and creative rhythmic patterns, with elements of 1980s R&B and organs. Like when recording 21, Adele worked with producer and songwriter Paul Epworth and Ryan Tedder, along with new collaborations with Max Martin and Shellback, Bruno Mars, Greg Kurstin, Danger Mouse, the Smeezingtons, Samuel Dixon, and Tobias Jesso Jr.\n25 received generally positive reviews from music critics, who commended its production and Adele\'s vocal performance.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/25_(Adele_album)) under Creative Commons Attribution CC-BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/legalcode)", + ), + ])), album_type: Album, year: Some(2015), by_va: false, diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_20240228_twoColumns.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_20240228_twoColumns.snap index ec0d90d..8082b38 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_20240228_twoColumns.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_20240228_twoColumns.snap @@ -28,7 +28,26 @@ MusicPlaylist( ), ], channel: None, - description: Some("Kick back and coast to these chillhop and lofi beats. #hiphop #chill #beats"), + description: Some(RichText([ + Text( + text: "Kick back and coast to these chillhop and lofi beats. ", + ), + Text( + text: "#hiphop", + ), + Text( + text: " ", + ), + Text( + text: "#chill", + ), + Text( + text: " ", + ), + Text( + text: "#beats", + ), + ])), track_count: Some(127), from_ytm: true, tracks: Paginator( diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_nomusic.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_nomusic.snap index 8a73bcf..07ca4da 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_nomusic.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_nomusic.snap @@ -26,7 +26,11 @@ MusicPlaylist( id: "UCQM0bS4_04-Y4JuYrgmnpZQ", name: "Chaosflo44", )), - description: Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben..."), + description: Some(RichText([ + Text( + text: "SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...", + ), + ])), track_count: Some(66), from_ytm: false, tracks: Paginator( diff --git a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_short.snap b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_short.snap index fb8b4c8..2d9a1b7 100644 --- a/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_short.snap +++ b/src/client/snapshots/rustypipe__client__music_playlist__tests__map_music_playlist_short.snap @@ -28,7 +28,11 @@ MusicPlaylist( ), ], channel: None, - description: Some("Stress-free tunes from classic rockers and newer artists."), + description: Some(RichText([ + Text( + text: "Stress-free tunes from classic rockers and newer artists.", + ), + ])), track_count: Some(87), from_ytm: true, tracks: Paginator( diff --git a/src/client/snapshots/rustypipe__client__playlist__tests__map_playlist_data_nomusic.snap b/src/client/snapshots/rustypipe__client__playlist__tests__map_playlist_data_nomusic.snap index 18c0b3e..a2587f1 100644 --- a/src/client/snapshots/rustypipe__client__playlist__tests__map_playlist_data_nomusic.snap +++ b/src/client/snapshots/rustypipe__client__playlist__tests__map_playlist_data_nomusic.snap @@ -2741,7 +2741,11 @@ Playlist( height: 188, ), ], - description: Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben..."), + description: Some(RichText([ + Text( + text: "SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...", + ), + ])), channel: Some(ChannelId( id: "UCQM0bS4_04-Y4JuYrgmnpZQ", name: "Chaosflo44", diff --git a/src/model/mod.rs b/src/model/mod.rs index 11a25e6..8b695b4 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -513,8 +513,8 @@ pub struct Playlist { pub video_count: u64, /// Playlist thumbnail pub thumbnail: Vec, - /// Playlist description in plaintext format - pub description: Option, + /// Playlist description in rich text format + pub description: Option, /// Channel of the playlist pub channel: Option, /// Last update date @@ -1061,8 +1061,8 @@ pub struct MusicPlaylist { pub thumbnail: Vec, /// Channel of the playlist pub channel: Option, - /// Playlist description in plaintext format - pub description: Option, + /// Playlist description in rich text format + pub description: Option, /// Number of tracks in the playlist pub track_count: Option, /// True if the playlist is from YouTube Music @@ -1089,8 +1089,8 @@ pub struct MusicAlbum { pub artists: Vec, /// Primary artist ID pub artist_id: Option, - /// Album description in plaintext format - pub description: Option, + /// Album description in rich text format + pub description: Option, /// Album type (Album/Single/EP) pub album_type: AlbumType, /// Release year diff --git a/tests/snapshots/youtube__music_album_one_artist.snap b/tests/snapshots/youtube__music_album_one_artist.snap index 14f672c..a44ac2d 100644 --- a/tests/snapshots/youtube__music_album_one_artist.snap +++ b/tests/snapshots/youtube__music_album_one_artist.snap @@ -14,7 +14,25 @@ MusicAlbum( ), ], artist_id: Some("UCwem2sj-QUJCiWiPAo9JuAw"), - description: Some("Unbroken is the third studio album by American singer Demi Lovato. It was released on September 20, 2011, by Hollywood Records. Primarily a pop record, Lovato described the album as \"more mature\" and with more R&B elements than her previous material, citing Rihanna as the major influence. While some of the album\'s lyrical content was heavily influenced by Lovato\'s personal struggles, it also deals with lighter subjects, such as love, self-empowerment, and having fun. Contributions to the album\'s production came from a wide range of producers, including Toby Gad, Ryan Tedder, Timbaland, Jim Beanz and Rock Mafia.\nLovato initially began recording her third studio album in 2010 before going on tour with the Jonas Brothers on their Live in Concert Tour. After withdrawing from the tour to seek treatment for physical and emotional issues, Lovato continued work on the album and described the recording process as therapeutic. She collaborated with artists such as Missy Elliott, Timbaland, Dev, Iyaz, and Jason Derulo on several tracks.\n\nFrom Wikipedia (https://en.wikipedia.org/wiki/Unbroken_(Demi_Lovato_album)) under Creative Commons Attribution CC-BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/legalcode)"), + description: Some(RichText([ + Text( + text: "Unbroken is the third studio album by American singer Demi Lovato. It was released on September 20, 2011, by Hollywood Records. Primarily a pop record, Lovato described the album as \"more mature\" and with more R&B elements than her previous material, citing Rihanna as the major influence. While some of the album\'s lyrical content was heavily influenced by Lovato\'s personal struggles, it also deals with lighter subjects, such as love, self-empowerment, and having fun. Contributions to the album\'s production came from a wide range of producers, including Toby Gad, Ryan Tedder, Timbaland, Jim Beanz and Rock Mafia.\nLovato initially began recording her third studio album in 2010 before going on tour with the Jonas Brothers on their Live in Concert Tour. After withdrawing from the tour to seek treatment for physical and emotional issues, Lovato continued work on the album and described the recording process as therapeutic. She collaborated with artists such as Missy Elliott, Timbaland, Dev, Iyaz, and Jason Derulo on several tracks.\n\nFrom Wikipedia (", + ), + Web( + text: "https://en.wikipedia.org/wiki/Unbroke...", + url: "https://en.wikipedia.org/wiki/Unbroken_(Demi_Lovato_album)", + ), + Text( + text: ") under Creative Commons Attribution CC-BY-SA 3.0 (", + ), + Web( + text: "https://creativecommons.org/licenses/...", + url: "https://creativecommons.org/licenses/by-sa/3.0/legalcode", + ), + Text( + text: ")", + ), + ])), album_type: Album, year: Some(2011), by_va: false, diff --git a/tests/youtube.rs b/tests/youtube.rs index 6cc69bc..9f5d730 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -383,7 +383,7 @@ fn get_playlist( if is_long { 100 } else { 10 }, "track count", ); - assert_eq!(playlist.description, description); + assert_eq!(playlist.description.map(|d| d.to_plaintext()), description); if let Some(expect) = channel { let c = playlist.channel.expect("channel"); @@ -1338,6 +1338,7 @@ fn resolve_channel_not_found(rp: RustyPipe) { //#TRENDS #[rstest] +#[ignore] fn startpage(rp: RustyPipe) { let startpage = tokio_test::block_on(rp.query().startpage()).unwrap(); @@ -1403,7 +1404,7 @@ fn music_playlist( ); if unlocalized { assert_eq!(playlist.name, name); - assert_eq!(playlist.description, description); + assert_eq!(playlist.description.map(|d| d.to_plaintext()), description); } if let Some(expect) = channel { @@ -1477,7 +1478,16 @@ fn music_playlist_not_found(rp: RustyPipe) { #[case::version_no_artist("version_no_artist", "MPREb_h8ltx5oKvyY")] #[case::no_artist("no_artist", "MPREb_bqWA6mAZFWS")] fn music_album(#[case] name: &str, #[case] id: &str, rp: RustyPipe, unlocalized: bool) { - let album = tokio_test::block_on(rp.query().music_album(id)).unwrap(); + // TODO: remove visitor data if A/B#13 is stabilized + let album = tokio_test::block_on( + rp.query() + .visitor_data_opt( + Some("Cgs1bHFWMlhmM1ZFNCi9jK6vBjIKCgJERRIEEgAgIw%3D%3D") + .filter(|_| name == "one_artist"), + ) + .music_album(id), + ) + .unwrap(); assert!(!album.cover.is_empty(), "got no cover");