feat: add track details, radios

This commit is contained in:
ThetaDev 2022-11-10 23:19:11 +01:00
parent 556575f5ff
commit e4046aef00
22 changed files with 19960 additions and 30 deletions

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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};