feat: add music_related

This commit is contained in:
ThetaDev 2022-11-11 23:41:11 +01:00
parent c80e302d72
commit cb38d5a248
11 changed files with 23236 additions and 42 deletions

View file

@ -193,25 +193,26 @@ fn map_artist_page(
response::music_item::ItemSection::MusicCarouselShelfRenderer { header, contents } => {
let mut extendable_albums = false;
if let Some(h) = header {
let ep = h
if let Some(button) = h
.music_carousel_shelf_basic_header_renderer
.more_content_button
.button_renderer
.navigation_endpoint;
if let Some(bep) = ep.browse_endpoint {
if let Some(cfg) = bep.browse_endpoint_context_supported_configs {
match cfg.browse_endpoint_context_music_config.page_type {
PageType::Playlist => {
if videos_playlist_id.is_none() {
videos_playlist_id = Some(bep.browse_id);
{
if let Some(bep) =
button.button_renderer.navigation_endpoint.browse_endpoint
{
if let Some(cfg) = bep.browse_endpoint_context_supported_configs {
match cfg.browse_endpoint_context_music_config.page_type {
PageType::Playlist => {
if videos_playlist_id.is_none() {
videos_playlist_id = Some(bep.browse_id);
}
}
PageType::Artist => {
album_page_params.push(bep.params);
extendable_albums = true;
}
_ => {}
}
PageType::Artist => {
album_page_params.push(bep.params);
extendable_albums = true;
}
_ => {}
}
}
}

View file

@ -4,13 +4,16 @@ use serde::Serialize;
use crate::{
error::{Error, ExtractionError},
model::{Lyrics, Paginator, TrackDetails, TrackItem},
model::{ArtistId, Lyrics, MusicRelated, Paginator, TrackDetails, TrackItem},
param::Language,
serializer::MapResult,
};
use super::{
response::{self, music_item::map_queue_item},
response::{
self,
music_item::{map_queue_item, MusicListMapper},
},
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext,
};
@ -71,6 +74,23 @@ impl RustyPipeQuery {
.await
}
pub async fn music_related(&self, related_id: &str) -> Result<MusicRelated, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: related_id,
};
self.execute_request::<response::MusicRelated, _, _>(
ClientType::DesktopMusic,
"music_related",
related_id,
"browse",
&request_body,
)
.await
}
pub async fn music_radio(&self, radio_id: &str) -> Result<Paginator<TrackItem>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QRadio {
@ -256,6 +276,84 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
}
}
impl MapResponse<MusicRelated> for response::MusicRelated {
fn map_response(
self,
_id: &str,
lang: Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<MusicRelated>, ExtractionError> {
// Find artist
let artist_id = self
.contents
.section_list_renderer
.contents
.iter()
.find_map(|section| match section {
response::music_item::ItemSection::MusicShelfRenderer(_) => None,
response::music_item::ItemSection::MusicCarouselShelfRenderer {
header, ..
} => header.as_ref().and_then(|h| {
h.music_carousel_shelf_basic_header_renderer
.title
.0
.iter()
.find_map(|c| {
let artist = ArtistId::from(c.clone());
if artist.id.is_some() {
Some(artist)
} else {
None
}
})
}),
response::music_item::ItemSection::None => None,
});
let mut mapper_tracks = MusicListMapper::new(lang);
let mut mapper = match artist_id {
Some(artist_id) => MusicListMapper::with_artist(lang, artist_id),
None => MusicListMapper::new(lang),
};
let mut sections = self.contents.section_list_renderer.contents.into_iter();
if let Some(response::music_item::ItemSection::MusicCarouselShelfRenderer {
contents,
..
}) = sections.next()
{
mapper_tracks.map_response(contents);
}
sections.for_each(|section| match section {
response::music_item::ItemSection::MusicShelfRenderer(shelf) => {
mapper.map_response(shelf.contents);
}
response::music_item::ItemSection::MusicCarouselShelfRenderer { contents, .. } => {
mapper.map_response(contents);
}
response::music_item::ItemSection::None => {}
});
let mapped_tracks = mapper_tracks.conv_items();
let mut mapped = mapper.group_items();
let mut warnings = mapped_tracks.warnings;
warnings.append(&mut mapped.warnings);
Ok(MapResult {
c: MusicRelated {
tracks: mapped_tracks.c,
other_versions: mapped.c.tracks,
albums: mapped.c.albums,
artists: mapped.c.artists,
playlists: mapped.c.playlists,
},
warnings,
})
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
@ -323,4 +421,21 @@ mod tests {
);
insta::assert_ron_snapshot!(format!("map_music_lyrics"), map_res.c);
}
#[test]
fn map_related() {
let json_path = Path::new("testfiles/music_details/related.json");
let json_file = File::open(json_path).unwrap();
let lyrics: response::MusicRelated =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<MusicRelated> = lyrics.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_related"), map_res.c);
}
}

View file

@ -17,6 +17,7 @@ pub(crate) use music_artist::MusicArtist;
pub(crate) use music_artist::MusicArtistAlbums;
pub(crate) use music_details::MusicDetails;
pub(crate) use music_details::MusicLyrics;
pub(crate) use music_details::MusicRelated;
pub(crate) use music_item::MusicContinuation;
pub(crate) use music_playlist::MusicPlaylist;
pub(crate) use music_search::MusicSearch;

View file

@ -3,7 +3,10 @@ use serde_with::serde_as;
use crate::serializer::text::Text;
use super::{music_item::PlaylistPanelRenderer, ContentRenderer, SectionList};
use super::{
music_item::{ItemSection, PlaylistPanelRenderer},
ContentRenderer, SectionList,
};
/// Response model for YouTube Music track details
#[derive(Debug, Deserialize)]
@ -116,3 +119,9 @@ pub(crate) struct LyricsRenderer {
#[serde_as(as = "Text")]
pub footer: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicRelated {
pub contents: SectionList<ItemSection>,
}

View file

@ -27,8 +27,6 @@ pub(crate) enum ItemSection {
#[serde(alias = "musicPlaylistShelfRenderer")]
MusicShelfRenderer(MusicShelf),
MusicCarouselShelfRenderer {
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
header: Option<MusicCarouselShelfHeader>,
#[serde_as(as = "VecLogError<_>")]
contents: MapResult<Vec<MusicResponseItem>>,
@ -136,13 +134,15 @@ pub(crate) struct ListMusicItem {
pub navigation_endpoint: Option<NavigationEndpoint>,
#[serde(default)]
pub flex_column_display_style: FlexColumnDisplayStyle,
#[serde(default)]
pub item_height: ItemHeight,
/// Album track number
#[serde_as(as = "Option<Text>")]
pub index: Option<String>,
pub menu: Option<MusicItemMenu>,
}
#[derive(Default, Debug, Deserialize)]
#[derive(Default, Debug, Copy, Clone, Deserialize)]
pub(crate) enum FlexColumnDisplayStyle {
#[serde(rename = "MUSIC_RESPONSIVE_LIST_ITEM_FLEX_COLUMN_DISPLAY_STYLE_TWO_LINE_STACK")]
TwoLines,
@ -151,6 +151,15 @@ pub(crate) enum FlexColumnDisplayStyle {
Default,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
pub(crate) enum ItemHeight {
#[serde(rename = "MUSIC_RESPONSIVE_LIST_ITEM_HEIGHT_MEDIUM_COMPACT")]
Compact,
#[default]
#[serde(other)]
Default,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -295,7 +304,9 @@ pub(crate) struct MusicCarouselShelfHeader {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicCarouselShelfHeaderRenderer {
pub more_content_button: MoreContentButton,
pub more_content_button: Option<MoreContentButton>,
#[serde(default)]
pub title: TextComponents,
}
#[derive(Debug, Deserialize)]
@ -523,27 +534,42 @@ impl MusicListMapper {
{
// Search result
FlexColumnDisplayStyle::TwoLines => {
let mut subtitle_parts = c2
.ok_or_else(|| format!("track {}: could not get subtitle", id))?
.renderer
.text
.split(util::DOT_SEPARATOR)
.into_iter();
// Is it a podcast episode?
if subtitle_parts.len() <= 3 && c3.is_some() {
(subtitle_parts.rev().next(), None, None)
} else {
// Skip first part (track type)
if subtitle_parts.len() > 3 {
subtitle_parts.next();
}
// Is this a related track?
if !is_video && item.item_height == ItemHeight::Compact {
(
subtitle_parts.next(),
subtitle_parts.next(),
subtitle_parts.next(),
c2.map(TextComponents::from),
c3.map(TextComponents::from),
None,
)
} else {
let mut subtitle_parts = c2
.ok_or_else(|| {
format!("track {}: could not get subtitle", id)
})?
.renderer
.text
.split(util::DOT_SEPARATOR)
.into_iter();
// Is this a related video?
if item.item_height == ItemHeight::Compact {
(subtitle_parts.next(), subtitle_parts.next(), None)
}
// Is it a podcast episode?
else if subtitle_parts.len() <= 3 && c3.is_some() {
(subtitle_parts.rev().next(), None, None)
} else {
// Skip first part (track type)
if subtitle_parts.len() > 3 {
subtitle_parts.next();
}
(
subtitle_parts.next(),
subtitle_parts.next(),
subtitle_parts.next(),
)
}
}
}
// Playlist item

View file

@ -1240,9 +1240,28 @@ pub struct TrackDetails {
pub related_id: Option<String>,
}
/// Song lyrics
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Lyrics {
/// Lyrics text
pub body: String,
/// Footer (contains lyrics source)
pub footer: String,
}
/// YouTube Music related entities
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicRelated {
/// Related tracks
pub tracks: Vec<TrackItem>,
/// Other versions of the same track
pub other_versions: Vec<TrackItem>,
/// Related albums
pub albums: Vec<AlbumItem>,
/// Related artists
pub artists: Vec<ArtistItem>,
/// Related playlists
pub playlists: Vec<MusicPlaylistItem>,
}