feat: add track details, radios
This commit is contained in:
parent
556575f5ff
commit
e4046aef00
22 changed files with 19960 additions and 30 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Reference in a new issue