feat: add music artists

This commit is contained in:
ThetaDev 2022-11-06 18:30:06 +01:00
parent d3aacc77aa
commit 6f07095757
49 changed files with 139065 additions and 821 deletions

View file

@ -1,4 +1,5 @@
pub(crate) mod channel;
pub(crate) mod music_artist;
pub(crate) mod music_item;
pub(crate) mod music_playlist;
pub(crate) mod music_search;
@ -11,6 +12,8 @@ pub(crate) mod video_details;
pub(crate) mod video_item;
pub(crate) use channel::Channel;
pub(crate) use music_artist::MusicArtist;
pub(crate) use music_artist::MusicArtistAlbums;
pub(crate) use music_item::MusicContinuation;
pub(crate) use music_playlist::MusicPlaylist;
pub(crate) use music_search::MusicSearch;

View file

@ -0,0 +1,143 @@
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError};
use crate::serializer::{ignore_any, text::Text, MapResult, VecLogError};
use super::{
music_item::{MusicResponseItem, MusicShelf, MusicThumbnailRenderer},
url_endpoint::NavigationEndpoint,
ContentsRenderer, Tab,
};
/// Response model for YouTube Music artists
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtist {
pub contents: Contents<ItemSection>,
pub header: Header,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents<T> {
pub single_column_browse_results_renderer: ContentsRenderer<Tab<SectionList<T>>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SectionList<T> {
pub section_list_renderer: ContentsRenderer<T>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ItemSection {
MusicShelfRenderer(MusicShelf),
MusicCarouselShelfRenderer {
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
header: Option<MusicCarouselShelfHeader>,
#[serde_as(as = "VecLogError<_>")]
contents: MapResult<Vec<MusicResponseItem>>,
},
#[serde(other, deserialize_with = "ignore_any")]
None,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Header {
#[serde(alias = "musicVisualHeaderRenderer")]
pub music_immersive_header_renderer: MusicHeaderRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub subscription_button: Option<SubscriptionButton>,
#[serde(default)]
#[serde_as(as = "Text")]
pub description: String,
#[serde(default)]
pub thumbnail: MusicThumbnailRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SubscriptionButton {
pub subscribe_button_renderer: SubscriptionButtonRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SubscriptionButtonRenderer {
#[serde_as(as = "Text")]
pub subscriber_count_text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicCarouselShelfHeader {
pub music_carousel_shelf_basic_header_renderer: MusicCarouselShelfHeaderRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicCarouselShelfHeaderRenderer {
pub more_content_button: MoreContentButton,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoreContentButton {
pub button_renderer: ButtonRenderer,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ButtonRenderer {
pub navigation_endpoint: NavigationEndpoint,
}
/// Response model for YouTube Music artist album page
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicArtistAlbums {
pub header: SimpleHeader,
pub contents: Contents<Grid>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Grid {
pub grid_renderer: GridRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GridRenderer {
#[serde_as(as = "VecLogError<_>")]
pub items: MapResult<Vec<MusicResponseItem>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeader {
pub music_header_renderer: SimpleHeaderRenderer,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SimpleHeaderRenderer {
#[serde_as(as = "Text")]
pub title: String,
}

View file

@ -15,7 +15,7 @@ use crate::{
};
use super::{
url_endpoint::{NavigationEndpoint, PageType},
url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType},
MusicContinuationData, ThumbnailsWrap,
};
@ -31,6 +31,10 @@ pub(crate) struct MusicShelf {
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub continuations: Vec<MusicContinuationData>,
/// "More" button at the bottom (artist pages)
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub bottom_endpoint: Option<BrowseEndpointWrap>,
}
#[derive(Debug, Deserialize)]
@ -222,23 +226,19 @@ impl MusicListMapper {
}
}
/*
pub fn with_artists(
lang: Language,
artists: Vec<ArtistId>,
by_va: bool,
artist_page: bool,
) -> Self {
/// Create a new MusicListMapper for an artist page
pub fn with_artist(lang: Language, artist: ArtistId) -> Self {
Self {
lang,
artists: Some((artists, by_va)),
artists: Some((vec![artist], false)),
album: None,
artist_page,
artist_page: true,
items: Vec::new(),
warnings: Vec::new(),
}
}*/
}
/// Create a new MusicListMapper for an album page
pub fn with_album(lang: Language, artists: Vec<ArtistId>, by_va: bool, album: AlbumId) -> Self {
Self {
lang,
@ -399,9 +399,8 @@ impl MusicListMapper {
}
};
let duration = duration_p
.and_then(|p| util::parse_video_length(p.first_str()))
.ok_or_else(|| format!("track {}: could not parse duration", id))?;
let duration =
duration_p.and_then(|p| util::parse_video_length(p.first_str()));
let (album, view_count) = match (item.flex_column_display_style, is_video) {
// The album field contains the view count for search videos
@ -455,87 +454,117 @@ impl MusicListMapper {
let subtitle_p2 = subtitle_parts.next();
let subtitle_p3 = subtitle_parts.next();
let (page_type, id) = item
.navigation_endpoint
.music_page()
.ok_or_else(|| "could not get navigation endpoint".to_owned())?;
match page_type {
PageType::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) {
// "2022" (Artist singles)
(Some(year_txt), None, Some(artists), true) => {
year = util::parse_numeric(year_txt.first_str()).ok();
artists.clone()
}
// "Album", "2022" (Artist albums)
(Some(atype_txt), Some(year_txt), Some(artists), true) => {
year = util::parse_numeric(year_txt.first_str()).ok();
album_type = map_album_type(atype_txt.first_str(), self.lang);
artists.clone()
}
// "Album", <"Oonagh"> (Album variants, new releases)
(Some(atype_txt), Some(p2), _, false) => {
album_type = map_album_type(atype_txt.first_str(), self.lang);
map_artists(Some(p2))
}
_ => {
return Err(format!(
"could not parse subtitle of album {}",
id
));
}
};
self.items.push(MusicItem::Album(AlbumItem {
id,
name: item.title,
match item.navigation_endpoint.watch_endpoint {
// Music video
Some(wep) => {
self.items.push(MusicItem::Track(TrackItem {
id: wep.video_id,
title: item.title,
duration: None,
cover: item.thumbnail_renderer.into(),
artists,
album_type,
year,
by_va,
artists: map_artists(subtitle_p1).0,
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(MusicEntityType::Album)
Ok(MusicEntityType::Track)
}
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());
// 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::Playlist(MusicPlaylistItem {
id,
name: item.title,
thumbnail: item.thumbnail_renderer.into(),
channel,
track_count,
from_ytm,
}));
Ok(MusicEntityType::Playlist)
}
PageType::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(MusicEntityType::Artist)
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();
artists.clone()
}
// "Album", "2022" (Artist albums)
(Some(atype_txt), Some(year_txt), Some(artists), true) => {
year = util::parse_numeric(year_txt.first_str()).ok();
album_type =
map_album_type(atype_txt.first_str(), self.lang);
artists.clone()
}
// "Album", <"Oonagh"> (Album variants, new releases)
(Some(atype_txt), Some(p2), _, false) => {
album_type =
map_album_type(atype_txt.first_str(), self.lang);
map_artists(Some(p2))
}
_ => {
return Err(format!(
"could not parse subtitle of album {}",
id
));
}
};
self.items.push(MusicItem::Album(AlbumItem {
id,
name: item.title,
cover: item.thumbnail_renderer.into(),
artists,
album_type,
year,
by_va,
}));
Ok(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(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(MusicEntityType::Artist)
}
PageType::Channel => {
Err(format!("channel items unsupported. id: {}", id))
}
}
}
PageType::Channel => Err(format!("channel items unsupported. id: {}", id)),
}
}
}

View file

@ -39,9 +39,16 @@ pub(crate) struct WatchEndpoint {
#[derive(Debug)]
pub(crate) struct BrowseEndpoint {
pub browse_id: String,
pub params: String,
pub browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct BrowseEndpointWrap {
pub browse_endpoint: BrowseEndpoint,
}
impl<'de> Deserialize<'de> for BrowseEndpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@ -51,6 +58,8 @@ impl<'de> Deserialize<'de> for BrowseEndpoint {
#[serde(rename_all = "camelCase")]
struct BEp {
pub browse_id: String,
#[serde(default)]
pub params: String,
pub browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
}
@ -71,6 +80,7 @@ impl<'de> Deserialize<'de> for BrowseEndpoint {
Ok(Self {
browse_id,
params: bep.params,
browse_endpoint_context_supported_configs: bep
.browse_endpoint_context_supported_configs,
})

View file

@ -12,7 +12,7 @@ use crate::serializer::{
};
use super::{
url_endpoint::BrowseEndpoint, ContinuationEndpoint, ContinuationItemRenderer, Icon,
url_endpoint::BrowseEndpointWrap, ContinuationEndpoint, ContinuationItemRenderer, Icon,
MusicContinuationData, Thumbnails,
};
use super::{ChannelBadge, ResponseContext, YouTubeListItem};
@ -525,7 +525,7 @@ pub(crate) struct CommentRenderer {
/// ID of the author's channel
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub author_endpoint: Option<AuthorEndpoint>,
pub author_endpoint: Option<BrowseEndpointWrap>,
/// Comment text
pub content_text: TextComponents,
/// Textual publish date (e.g. `15 minutes ago`, `2 days ago`)
@ -542,12 +542,6 @@ pub(crate) struct CommentRenderer {
pub action_buttons: CommentActionButtons,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AuthorEndpoint {
pub browse_endpoint: BrowseEndpoint,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum CommentPriority {