feat: music search filter/cont, refactored paginator

This commit is contained in:
ThetaDev 2022-10-31 23:05:27 +01:00
parent d29bbd8b25
commit dac2b17dc2
38 changed files with 65313 additions and 247 deletions

View file

@ -11,8 +11,8 @@ pub(crate) mod video_details;
pub(crate) mod video_item;
pub(crate) use channel::Channel;
pub(crate) use music_item::MusicContinuation;
pub(crate) use music_playlist::MusicPlaylist;
pub(crate) use music_playlist::MusicPlaylistCont;
pub(crate) use music_search::MusicSearch;
pub(crate) use player::Player;
pub(crate) use playlist::Playlist;
@ -207,13 +207,13 @@ pub(crate) struct RichGridContinuation {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicContinuation {
pub next_continuation_data: MusicContinuationData,
pub(crate) struct MusicContinuationData {
pub next_continuation_data: MusicContinuationDataInner,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicContinuationData {
pub(crate) struct MusicContinuationDataInner {
pub continuation: String,
}

View file

@ -2,7 +2,10 @@ use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::{
model::{self, AlbumItem, AlbumType, ArtistItem, ChannelId, MusicPlaylistItem, TrackItem},
model::{
self, AlbumId, AlbumItem, AlbumType, ArtistItem, ChannelId, FromYtItem, MusicEntityType,
MusicItem, MusicPlaylistItem, TrackItem,
},
param::Language,
serializer::{
text::{Text, TextComponents},
@ -13,7 +16,7 @@ use crate::{
use super::{
url_endpoint::{NavigationEndpoint, PageType},
MusicContinuation, ThumbnailsWrap,
MusicContinuationData, ThumbnailsWrap,
};
#[serde_as]
@ -23,16 +26,16 @@ pub(crate) struct MusicShelf {
/// Playlist ID (only for playlists)
pub playlist_id: Option<String>,
#[serde_as(as = "VecLogError<_>")]
pub contents: MapResult<Vec<MusicItem>>,
pub contents: MapResult<Vec<MusicResponseItem>>,
/// Continuation token for fetching more (>100) playlist items
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub continuations: Vec<MusicContinuation>,
pub continuations: Vec<MusicContinuationData>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum MusicItem {
pub(crate) enum MusicResponseItem {
MusicResponsiveListItemRenderer(ListMusicItem),
MusicTwoRowItemRenderer(CoverMusicItem),
}
@ -52,11 +55,15 @@ pub(crate) struct ListMusicItem {
/// `[<"Der Himmel reißt auf">]` Album track (title)
///
/// `[<"Girls">], ["Song", " • ", <"aespa">, " • ", <"Girls - The 2nd Mini Album">, " • ", "4:01"]`
/// Search track (title, artist, album, duration)
/// Search track (title, artist, album, duration).
///
/// Info: "Song" label is missing in the "Songs" tab
///
/// `[<"Black Mamba">], ["Video", " • ", <"aespa">, " • ", "235M views", " • ", "3:50"]`
/// Search video (title, artist, view count, duration)
///
/// Info: "Video" label is missing in the "Videos" tab
///
/// `["Next Level"], ["Single", " • ", <"aespa">, " • ", "2021"]`
/// Search album (title, type, artist, year)
///
@ -64,6 +71,8 @@ pub(crate) struct ListMusicItem {
///
/// `["aespa - All Songs & MV"], ["Playlist", " • ", <"Jerwen">, " • ", "49 songs"]`
/// Search playlist (title, creator, track count)
///
/// Info: "Playlist" label is missing in the "Playlists" tab
pub flex_columns: Vec<MusicColumn>,
/// Track duration (playlist/album tracks)
///
@ -162,6 +171,19 @@ impl From<MusicThumbnailRenderer> for Vec<model::Thumbnail> {
}
}
/// Music list continuation response model
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicContinuation {
pub continuation_contents: ContinuationContents,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ContinuationContents {
pub music_playlist_shelf_continuation: MusicShelf,
}
/*
#MAPPER
*/
@ -171,13 +193,16 @@ pub(crate) struct MusicListMapper {
lang: Language,
o_artists: Option<(Vec<ChannelId>, String)>,
artist_page: bool,
items: Vec<MusicItem>,
warnings: Vec<String>,
}
#[derive(Debug)]
pub(crate) struct GroupedMusicItems {
pub tracks: Vec<TrackItem>,
pub albums: Vec<AlbumItem>,
pub artists: Vec<ArtistItem>,
pub playlists: Vec<MusicPlaylistItem>,
pub warnings: Vec<String>,
}
impl MusicListMapper {
@ -186,10 +211,7 @@ impl MusicListMapper {
lang,
o_artists: None,
artist_page: false,
tracks: Vec::new(),
albums: Vec::new(),
artists: Vec::new(),
playlists: Vec::new(),
items: Vec::new(),
warnings: Vec::new(),
}
}
@ -204,17 +226,14 @@ impl MusicListMapper {
lang,
o_artists: Some((artists, artists_txt)),
artist_page,
tracks: Vec::new(),
albums: Vec::new(),
artists: Vec::new(),
playlists: Vec::new(),
items: Vec::new(),
warnings: Vec::new(),
}
}
fn map_item(&mut self, item: MusicItem) -> Result<(), String> {
fn map_item(&mut self, item: MusicResponseItem) -> Result<MusicEntityType, String> {
match item {
MusicItem::MusicResponsiveListItemRenderer(item) => {
MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
let mut columns = item.flex_columns.into_iter();
let title = columns.next().map(|col| col.renderer.text.to_string());
let c2 = columns.next();
@ -246,13 +265,13 @@ impl MusicListMapper {
util::parse_large_numstr(&p.to_string(), self.lang)
});
self.artists.push(ArtistItem {
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: title,
avatar: item.thumbnail.into(),
subscriber_count,
});
Ok(())
}));
Ok(MusicEntityType::Artist)
}
PageType::Album => {
let album_type = subtitle_p1
@ -264,7 +283,7 @@ impl MusicListMapper {
let year = subtitle_p3
.and_then(|st| util::parse_numeric(&st.to_string()).ok());
self.albums.push(AlbumItem {
self.items.push(MusicItem::Album(AlbumItem {
id,
name: title,
cover: item.thumbnail.into(),
@ -272,31 +291,37 @@ impl MusicListMapper {
artists_txt,
album_type,
year,
});
Ok(())
}));
Ok(MusicEntityType::Album)
}
PageType::Playlist => {
let from_ytm = subtitle_p2
// Part 1 may be the "Playlist" label
let (channel_p, tcount_p) = match subtitle_p3 {
Some(_) => (subtitle_p2, subtitle_p3),
None => (subtitle_p1, subtitle_p2),
};
let from_ytm = channel_p
.as_ref()
.and_then(|p| {
p.0.first().map(|txt| txt.as_str() == util::YT_MUSIC_NAME)
})
.unwrap_or_default();
let channel = subtitle_p2.and_then(|p| {
let channel = channel_p.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.to_string()).ok());
let track_count =
tcount_p.and_then(|p| util::parse_numeric(&p.to_string()).ok());
self.playlists.push(MusicPlaylistItem {
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
id,
name: title,
thumbnail: item.thumbnail.into(),
channel,
track_count,
from_ytm,
});
Ok(())
}));
Ok(MusicEntityType::Playlist)
}
PageType::Channel => {
Err(format!("channel items unsupported. id: {}", id))
@ -336,7 +361,9 @@ impl MusicListMapper {
.split(util::DOT_SEPARATOR)
.into_iter();
// Skip first part (track type)
subtitle_parts.next();
if subtitle_parts.len() > 3 {
subtitle_parts.next();
}
(
subtitle_parts.next(),
subtitle_parts.next(),
@ -367,8 +394,7 @@ impl MusicListMapper {
),
(_, false) => (
album_p.and_then(|p| {
p.0.into_iter()
.find_map(|c| model::AlbumId::try_from(c).ok())
p.0.into_iter().find_map(|c| AlbumId::try_from(c).ok())
}),
None,
),
@ -393,7 +419,7 @@ impl MusicListMapper {
}
}
self.tracks.push(TrackItem {
self.items.push(MusicItem::Track(TrackItem {
id,
title,
duration,
@ -403,12 +429,12 @@ impl MusicListMapper {
album,
view_count,
is_video,
});
Ok(())
}));
Ok(MusicEntityType::Track)
}
}
}
MusicItem::MusicTwoRowItemRenderer(item) => {
MusicResponseItem::MusicTwoRowItemRenderer(item) => {
let mut subtitle_parts = item.subtitle.split(util::DOT_SEPARATOR).into_iter();
let subtitle_p1 = subtitle_parts.next();
let subtitle_p2 = subtitle_parts.next();
@ -455,7 +481,7 @@ impl MusicListMapper {
}
};
self.albums.push(AlbumItem {
self.items.push(MusicItem::Album(AlbumItem {
id,
name: item.title,
cover: item.thumbnail_renderer.into(),
@ -463,8 +489,8 @@ impl MusicListMapper {
artists_txt,
year,
album_type,
});
Ok(())
}));
Ok(MusicEntityType::Album)
}
PageType::Playlist => {
// TODO: make component to string zero-copy if len=1
@ -480,27 +506,27 @@ impl MusicListMapper {
let track_count =
subtitle_p3.and_then(|p| util::parse_numeric(&p.to_string()).ok());
self.playlists.push(MusicPlaylistItem {
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
id,
name: item.title,
thumbnail: item.thumbnail_renderer.into(),
channel,
track_count,
from_ytm,
});
Ok(())
}));
Ok(MusicEntityType::Playlist)
}
PageType::Artist => {
let subscriber_count = subtitle_p1
.and_then(|p| util::parse_large_numstr(&p.to_string(), self.lang));
self.artists.push(ArtistItem {
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: item.title,
avatar: item.thumbnail_renderer.into(),
subscriber_count,
});
Ok(())
}));
Ok(MusicEntityType::Artist)
}
PageType::Channel => Err(format!("channel items unsupported. id: {}", id)),
}
@ -508,13 +534,63 @@ impl MusicListMapper {
}
}
pub fn map_response(&mut self, mut res: MapResult<Vec<MusicItem>>) {
pub fn map_response(
&mut self,
mut res: MapResult<Vec<MusicResponseItem>>,
) -> Option<MusicEntityType> {
let mut etype = None;
self.warnings.append(&mut res.warnings);
res.c.into_iter().for_each(|item| {
if let Err(e) = self.map_item(item) {
self.warnings.push(e);
res.c
.into_iter()
.for_each(|item| match self.map_item(item) {
Ok(t) => etype = Some(t),
Err(e) => self.warnings.push(e),
});
etype
}
pub fn items(self) -> MapResult<Vec<MusicItem>> {
MapResult {
c: self.items,
warnings: self.warnings,
}
}
pub fn conv_items<T: FromYtItem>(self) -> MapResult<Vec<T>> {
MapResult {
c: self
.items
.into_iter()
.filter_map(T::from_ytm_item)
.collect(),
warnings: self.warnings,
}
}
pub fn group_items(self) -> MapResult<GroupedMusicItems> {
let mut tracks = Vec::new();
let mut albums = Vec::new();
let mut artists = Vec::new();
let mut playlists = Vec::new();
for item in self.items {
match item {
MusicItem::Track(track) => tracks.push(track),
MusicItem::Album(album) => albums.push(album),
MusicItem::Artist(artist) => artists.push(artist),
MusicItem::Playlist(playlist) => playlists.push(playlist),
}
});
}
MapResult {
c: GroupedMusicItems {
tracks,
albums,
artists,
playlists,
},
warnings: self.warnings,
}
}
}

View file

@ -7,7 +7,9 @@ use crate::serializer::{
MapResult, VecLogError,
};
use super::music_item::{MusicContentsRenderer, MusicItem, MusicShelf, MusicThumbnailRenderer};
use super::music_item::{
MusicContentsRenderer, MusicResponseItem, MusicShelf, MusicThumbnailRenderer,
};
use super::{ContentsRenderer, Tab};
/// Response model for YouTube Music playlists and albums
@ -18,12 +20,6 @@ pub(crate) struct MusicPlaylist {
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 {
@ -45,7 +41,7 @@ pub(crate) enum ItemSection {
MusicShelfRenderer(MusicShelf),
MusicCarouselShelfRenderer {
#[serde_as(as = "VecLogError<_>")]
contents: MapResult<Vec<MusicItem>>,
contents: MapResult<Vec<MusicResponseItem>>,
},
#[serde(other, deserialize_with = "ignore_any")]
None,
@ -129,9 +125,3 @@ pub(crate) struct PlaylistEndpoint {
pub(crate) struct PlaylistWatchEndpoint {
pub playlist_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ContinuationContents {
pub music_playlist_shelf_continuation: MusicShelf,
}

View file

@ -13,7 +13,7 @@ use crate::serializer::{
use super::{
url_endpoint::BrowseEndpoint, ContinuationEndpoint, ContinuationItemRenderer, Icon,
MusicContinuation, Thumbnails,
MusicContinuationData, Thumbnails,
};
use super::{ChannelBadge, ResponseContext, YouTubeListItem};
@ -308,7 +308,7 @@ pub(crate) struct RecommendationResults {
#[serde_as(as = "Option<VecLogError<_>>")]
pub results: Option<MapResult<Vec<YouTubeListItem>>>,
#[serde_as(as = "Option<VecSkipError<_>>")]
pub continuations: Option<Vec<MusicContinuation>>,
pub continuations: Option<Vec<MusicContinuationData>>,
}
/// The engagement panels are displayed below the video and contain chapter markers