feat!: add rich text description to playlists and albums
fix: panic when parsing new music album/playlist layout
This commit is contained in:
parent
ff68cfb4e1
commit
95ab7c91c6
12 changed files with 146 additions and 42 deletions
|
|
@ -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<MusicPlaylist> 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<MusicPlaylist> 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<MusicAlbum> 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<MusicAlbum> 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<MusicAlbum> 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,
|
||||
|
|
|
|||
|
|
@ -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<Playlist> for response::Playlist {
|
|||
let mut mapper = response::YouTubeListMapper::<VideoItem>::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<Playlist> 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<Playlist> 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<Playlist> 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
|
||||
|
|
|
|||
|
|
@ -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<Header>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DefaultOnError")]
|
||||
pub microformat: Option<Microformat>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -87,23 +91,20 @@ pub(crate) struct HeaderRenderer {
|
|||
pub buttons: Vec<HeaderMenu>,
|
||||
}
|
||||
|
||||
#[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<MusicItemMenuEntry>,
|
||||
}
|
||||
|
||||
impl From<Description> for String {
|
||||
impl From<Description> for TextComponents {
|
||||
fn from(value: Description) -> Self {
|
||||
match value {
|
||||
Description::Text(v) => v,
|
||||
|
|
@ -133,3 +134,15 @@ impl From<Description> 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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TextComponents>,
|
||||
pub thumbnail_renderer: PlaylistThumbnailRenderer,
|
||||
/// - `"495", " videos"`
|
||||
/// - `"3,310,996 views"`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -513,8 +513,8 @@ pub struct Playlist {
|
|||
pub video_count: u64,
|
||||
/// Playlist thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Playlist description in plaintext format
|
||||
pub description: Option<String>,
|
||||
/// Playlist description in rich text format
|
||||
pub description: Option<RichText>,
|
||||
/// Channel of the playlist
|
||||
pub channel: Option<ChannelId>,
|
||||
/// Last update date
|
||||
|
|
@ -1061,8 +1061,8 @@ pub struct MusicPlaylist {
|
|||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Channel of the playlist
|
||||
pub channel: Option<ChannelId>,
|
||||
/// Playlist description in plaintext format
|
||||
pub description: Option<String>,
|
||||
/// Playlist description in rich text format
|
||||
pub description: Option<RichText>,
|
||||
/// Number of tracks in the playlist
|
||||
pub track_count: Option<u64>,
|
||||
/// True if the playlist is from YouTube Music
|
||||
|
|
@ -1089,8 +1089,8 @@ pub struct MusicAlbum {
|
|||
pub artists: Vec<ArtistId>,
|
||||
/// Primary artist ID
|
||||
pub artist_id: Option<String>,
|
||||
/// Album description in plaintext format
|
||||
pub description: Option<String>,
|
||||
/// Album description in rich text format
|
||||
pub description: Option<RichText>,
|
||||
/// Album type (Album/Single/EP)
|
||||
pub album_type: AlbumType,
|
||||
/// Release year
|
||||
|
|
|
|||
Reference in a new issue