feat: add music playlist
This commit is contained in:
parent
b64aabb6b6
commit
566b3e5bfc
24 changed files with 238892 additions and 61192 deletions
|
|
@ -1,7 +1,8 @@
|
|||
pub(crate) mod channel;
|
||||
pub(crate) mod music_item;
|
||||
pub(crate) mod music_playlist;
|
||||
pub(crate) mod player;
|
||||
pub(crate) mod playlist;
|
||||
// pub(crate) mod playlist_music;
|
||||
pub(crate) mod search;
|
||||
pub(crate) mod trends;
|
||||
pub(crate) mod url_endpoint;
|
||||
|
|
@ -9,10 +10,11 @@ pub(crate) mod video_details;
|
|||
pub(crate) mod video_item;
|
||||
|
||||
pub(crate) use channel::Channel;
|
||||
pub(crate) use music_playlist::MusicPlaylist;
|
||||
pub(crate) use music_playlist::MusicPlaylistCont;
|
||||
pub(crate) use player::Player;
|
||||
pub(crate) use playlist::Playlist;
|
||||
pub(crate) use playlist::PlaylistCont;
|
||||
// pub(crate) use playlist_music::PlaylistMusic;
|
||||
pub(crate) use search::Search;
|
||||
pub(crate) use trends::Startpage;
|
||||
pub(crate) use trends::Trending;
|
||||
|
|
@ -47,7 +49,7 @@ pub(crate) struct ContentsRenderer<T> {
|
|||
pub contents: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ThumbnailsWrap {
|
||||
#[serde(default)]
|
||||
|
|
@ -195,61 +197,6 @@ pub(crate) struct RichGridContinuation {
|
|||
pub contents: MapResult<Vec<YouTubeListItem>>,
|
||||
}
|
||||
|
||||
// YouTube Music
|
||||
|
||||
/*
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicItem {
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub playlist_item_data: Option<PlaylistItemData>,
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub flex_columns: Vec<MusicColumn>,
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub fixed_columns: Vec<MusicColumn>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicThumbnailRenderer {
|
||||
#[serde(alias = "croppedSquareThumbnailRenderer")]
|
||||
pub music_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistItemData {
|
||||
pub video_id: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicContentsRenderer<T> {
|
||||
pub contents: Vec<T>,
|
||||
#[serde_as(as = "Option<VecSkipError<_>>")]
|
||||
pub continuations: Option<Vec<MusicContinuation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicColumn {
|
||||
#[serde(
|
||||
rename = "musicResponsiveListItemFlexColumnRenderer",
|
||||
alias = "musicResponsiveListItemFixedColumnRenderer"
|
||||
)]
|
||||
pub renderer: MusicColumnRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicColumnRenderer {
|
||||
pub text: TextComponent,
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicContinuation {
|
||||
|
|
|
|||
220
src/client/response/music_item.rs
Normal file
220
src/client/response/music_item.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError};
|
||||
|
||||
use crate::{
|
||||
model::{self, ChannelId},
|
||||
serializer::{text::TextComponents, MapResult},
|
||||
util::{self, TryRemove},
|
||||
};
|
||||
|
||||
use super::ThumbnailsWrap;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicItem {
|
||||
pub music_responsive_list_item_renderer: InnerMusicItem,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InnerMusicItem {
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub playlist_item_data: Option<PlaylistItemData>,
|
||||
pub flex_columns: Vec<MusicColumn>,
|
||||
pub fixed_columns: Vec<MusicColumn>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicThumbnailRenderer {
|
||||
#[serde(alias = "croppedSquareThumbnailRenderer")]
|
||||
pub music_thumbnail_renderer: ThumbnailsWrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistItemData {
|
||||
pub video_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicContentsRenderer<T> {
|
||||
pub contents: Vec<T>,
|
||||
/*
|
||||
/// Continuation token for fetching recommended items
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub continuations: Vec<MusicContinuation>,
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicColumn {
|
||||
#[serde(
|
||||
rename = "musicResponsiveListItemFlexColumnRenderer",
|
||||
alias = "musicResponsiveListItemFixedColumnRenderer"
|
||||
)]
|
||||
pub renderer: MusicColumnRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct MusicColumnRenderer {
|
||||
pub text: TextComponents,
|
||||
}
|
||||
|
||||
impl From<MusicThumbnailRenderer> for Vec<model::Thumbnail> {
|
||||
fn from(tr: MusicThumbnailRenderer) -> Self {
|
||||
tr.music_thumbnail_renderer.thumbnail.into()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#MAPPER
|
||||
*/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MusicListMapper<T> {
|
||||
artists: Option<Vec<ChannelId>>,
|
||||
|
||||
pub items: Vec<T>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T> MusicListMapper<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
artists: None,
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_artists(artists: Vec<ChannelId>) -> Self {
|
||||
Self {
|
||||
artists: Some(artists),
|
||||
items: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_music_item(&mut self, item: MusicItem) -> Option<model::YouTubeMusicItem> {
|
||||
let item = item.music_responsive_list_item_renderer;
|
||||
|
||||
let first_tn = item
|
||||
.thumbnail
|
||||
.music_thumbnail_renderer
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
.first();
|
||||
|
||||
let id = some_or_bail!(
|
||||
item.playlist_item_data
|
||||
.map(|d| d.video_id)
|
||||
.or_else(|| first_tn.and_then(|tn| util::video_id_from_thumbnail_url(&tn.url))),
|
||||
None
|
||||
);
|
||||
|
||||
let is_video = !first_tn.map(|tn| tn.height == tn.width).unwrap_or_default();
|
||||
|
||||
let duration = item.fixed_columns.first().and_then(|col| {
|
||||
col.renderer
|
||||
.text
|
||||
.0
|
||||
.first()
|
||||
.and_then(|txt| util::parse_video_length(txt.as_str()))
|
||||
});
|
||||
|
||||
let mut columns = item.flex_columns;
|
||||
|
||||
let album = columns.try_swap_remove(2).and_then(|col| {
|
||||
col.renderer
|
||||
.text
|
||||
.0
|
||||
.into_iter()
|
||||
.find_map(|c| model::AlbumId::try_from(c).ok())
|
||||
});
|
||||
|
||||
let artists_col = columns.try_swap_remove(1);
|
||||
let artists_txt = artists_col
|
||||
.as_ref()
|
||||
.map(|col| col.renderer.text.to_string());
|
||||
let mut artists = artists_col
|
||||
.map(|col| {
|
||||
col.renderer
|
||||
.text
|
||||
.0
|
||||
.into_iter()
|
||||
.filter_map(|c| ChannelId::try_from(c).ok())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(a) = &self.artists {
|
||||
if artists.is_empty() {
|
||||
artists = a.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let title = columns
|
||||
.try_swap_remove(0)
|
||||
.map(|col| col.renderer.text.to_string());
|
||||
|
||||
match (title, duration) {
|
||||
(Some(title), Some(duration)) => {
|
||||
Some(model::YouTubeMusicItem::Track(model::TrackItem {
|
||||
id,
|
||||
title,
|
||||
duration,
|
||||
cover: item.thumbnail.into(),
|
||||
artists,
|
||||
artists_txt,
|
||||
album,
|
||||
view_count: None,
|
||||
is_video,
|
||||
}))
|
||||
}
|
||||
(None, _) => {
|
||||
self.warnings
|
||||
.push(format!("track {}: could not get title", id));
|
||||
None
|
||||
}
|
||||
(_, None) => {
|
||||
self.warnings
|
||||
.push(format!("track {}: could not parse duration", id));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl MusicListMapper<model::YouTubeMusicItem> {
|
||||
fn map_item(&mut self, item: MusicItem) {
|
||||
if let Some(mapped) = self.map_music_item(item) {
|
||||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_response(&mut self, mut res: MapResult<Vec<MusicItem>>) {
|
||||
self.warnings.append(&mut res.warnings);
|
||||
res.c.into_iter().for_each(|item| self.map_item(item));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl MusicListMapper<model::TrackItem> {
|
||||
fn map_item(&mut self, item: MusicItem) {
|
||||
if let Some(model::YouTubeMusicItem::Track(track)) = self.map_music_item(item) {
|
||||
self.items.push(track);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_response(&mut self, mut res: MapResult<Vec<MusicItem>>) {
|
||||
self.warnings.append(&mut res.warnings);
|
||||
res.c.into_iter().for_each(|item| self.map_item(item));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,27 @@ use serde::Deserialize;
|
|||
use serde_with::serde_as;
|
||||
use serde_with::VecSkipError;
|
||||
|
||||
use crate::serializer::text::Text;
|
||||
|
||||
use super::MusicThumbnailRenderer;
|
||||
use super::{
|
||||
ContentRenderer, ContentsRenderer, MusicContentsRenderer, MusicContinuation, MusicItem,
|
||||
use crate::serializer::{
|
||||
text::{Text, TextComponents},
|
||||
MapResult, VecLogError,
|
||||
};
|
||||
|
||||
use super::music_item::{MusicContentsRenderer, MusicItem, MusicThumbnailRenderer};
|
||||
use super::{ContentRenderer, ContentsRenderer, MusicContinuation};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistMusic {
|
||||
pub(crate) struct MusicPlaylist {
|
||||
pub contents: Contents,
|
||||
pub header: Header,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct MusicPlaylistCont {
|
||||
pub continuation_contents: ContinuationContents,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Contents {
|
||||
|
|
@ -48,17 +55,12 @@ pub(crate) struct ItemSection {
|
|||
pub(crate) struct MusicShelf {
|
||||
/// Playlist ID (only for playlists)
|
||||
pub playlist_id: Option<String>,
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub contents: Vec<PlaylistMusicItem>,
|
||||
#[serde_as(as = "VecLogError<_>")]
|
||||
pub contents: MapResult<Vec<MusicItem>>,
|
||||
/// Continuation token for fetching more (>100) playlist items
|
||||
#[serde_as(as = "Option<VecSkipError<_>>")]
|
||||
pub continuations: Option<Vec<MusicContinuation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PlaylistMusicItem {
|
||||
pub music_responsive_list_item_renderer: MusicItem,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub continuations: Vec<MusicContinuation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -71,7 +73,7 @@ pub(crate) struct Header {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct HeaderRenderer {
|
||||
#[serde_as(as = "crate::serializer::text::Text")]
|
||||
#[serde_as(as = "Text")]
|
||||
pub title: String,
|
||||
/// Content type + Channel/Artist + Year.
|
||||
/// Missing on artist_tracks view.
|
||||
|
|
@ -79,17 +81,27 @@ pub(crate) struct HeaderRenderer {
|
|||
/// `"Playlist", " • ", <"Best Music">, " • ", "2022"`
|
||||
///
|
||||
/// `"Album", " • ", <"Helene Fischer">, " • ", "2021"`
|
||||
pub subtitle: Option<Text>,
|
||||
#[serde(default)]
|
||||
pub subtitle: TextComponents,
|
||||
/// Playlist description. May contain hashtags which are
|
||||
/// displayed as search links on the YouTube website.
|
||||
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||
#[serde_as(as = "Option<Text>")]
|
||||
pub description: Option<String>,
|
||||
/// Playlist thumbnail / album cover.
|
||||
/// Missing on artist_tracks view.
|
||||
pub thumbnail: Option<MusicThumbnailRenderer>,
|
||||
#[serde(default)]
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
/// Number of tracks + playtime.
|
||||
/// Missing on artist_tracks view.
|
||||
///
|
||||
/// `"64 songs", " • ", "3 hours, 40 minutes"`
|
||||
pub second_subtitle: Option<Text>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "Text")]
|
||||
pub second_subtitle: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationContents {
|
||||
pub music_playlist_shelf_continuation: MusicShelf,
|
||||
}
|
||||
Reference in a new issue