feat: add music album

This commit is contained in:
ThetaDev 2022-10-29 23:45:03 +02:00
parent 566b3e5bfc
commit 3b738a55ad
12 changed files with 14904 additions and 17 deletions

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use crate::{
error::{Error, ExtractionError},
model::{ChannelId, MusicPlaylist, Paginator, TrackItem},
model::{AlbumType, ChannelId, MusicAlbum, MusicPlaylist, Paginator, TrackItem},
serializer::MapResult,
util::{self, TryRemove},
};
@ -49,6 +49,23 @@ impl RustyPipeQuery {
)
.await
}
pub async fn music_album(&self, album_id: &str) -> Result<MusicAlbum, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
context,
browse_id: album_id.to_owned(),
};
self.execute_request::<response::MusicPlaylist, _, _>(
ClientType::DesktopMusic,
"music_album",
album_id,
"browse",
&request_body,
)
.await
}
}
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
@ -70,11 +87,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
.content
.section_list_renderer
.contents
.try_swap_remove(0)
.into_iter()
.find_map(|section| match section {
response::music_playlist::ItemSection::MusicShelfRenderer(shelf) => Some(shelf),
_ => None,
})
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no sectionListRenderer content",
)))?
.music_shelf_renderer;
)))?;
let playlist_id = shelf
.playlist_id
@ -156,6 +176,118 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicPlaylistCont {
}
}
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
fn map_response(
self,
id: &str,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
// dbg!(&self);
let header = self.header.music_detail_header_renderer;
let mut content = self.contents.single_column_browse_results_renderer.contents;
let sections = content
.try_swap_remove(0)
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
.tab_renderer
.content
.section_list_renderer
.contents;
let mut shelf = None;
let mut album_versions = None;
for section in sections {
match section {
response::music_playlist::ItemSection::MusicShelfRenderer(sh) => shelf = Some(sh),
response::music_playlist::ItemSection::MusicCarouselShelfRenderer { contents } => {
album_versions = Some(contents)
}
response::music_playlist::ItemSection::None => (),
}
}
let shelf = shelf.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no sectionListRenderer content",
)))?;
let playlist_id = header.menu.and_then(|mut menu| {
menu.menu_renderer
.top_level_buttons
.try_swap_remove(0)
.map(|btn| {
btn.button_renderer
.navigation_endpoint
.watch_playlist_endpoint
.playlist_id
})
});
let subtitle_len = header.subtitle.0.len();
if subtitle_len < 5 {
return Err(ExtractionError::InvalidData(Cow::Owned(format!(
"header text is missing elements: {}",
header.subtitle.to_string()
))));
}
let mut artists = Vec::new();
let mut artists_txt = String::new();
let mut st_parts = header.subtitle.0.into_iter();
let album_type_txt = st_parts.next().unwrap();
st_parts.next();
for _ in 0..subtitle_len - 4 {
let part = st_parts.next().unwrap();
artists_txt += part.as_str();
if let Ok(a) = ChannelId::try_from(part) {
artists.push(a);
}
}
st_parts.next();
let year_txt = st_parts.next().unwrap();
let by_va = artists_txt == "Various Artists";
// TODO: add support for different languages
let album_type = match album_type_txt.as_str() {
"Single" => AlbumType::Single,
"EP" => AlbumType::Ep,
_ => AlbumType::Album,
};
let year = util::parse_numeric(year_txt.as_str())
.ok()
.unwrap_or_default();
let mut mapper = match by_va {
true => MusicListMapper::<TrackItem>::new(),
false => {
MusicListMapper::<TrackItem>::with_artists(artists.clone(), artists_txt.clone())
}
};
mapper.map_response(shelf.contents);
Ok(MapResult {
c: MusicAlbum {
id: id.to_owned(),
playlist_id,
name: header.title,
cover: header.thumbnail.into(),
artists,
artists_txt,
album_type,
year,
by_va,
tracks: mapper.items,
},
warnings: mapper.warnings,
})
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
@ -163,7 +295,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::param::Language;
use crate::{model, param::Language};
#[rstest]
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
@ -176,7 +308,8 @@ mod tests {
let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = playlist.map_response(id, Language::En, None).unwrap();
let map_res: MapResult<model::MusicPlaylist> =
playlist.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
@ -204,4 +337,28 @@ mod tests {
);
insta::assert_ron_snapshot!("map_music_playlist_cont", map_res.c);
}
#[rstest]
#[case::one_artist("one_artist", "MPREb_nlBWQROfvjo")]
#[case::various_artists("various_artists", "MPREb_8QkDeEIawvX")]
#[case::single("single", "MPREb_bHfHGoy7vuv")]
fn map_music_album(#[case] name: &str, #[case] id: &str) {
let filename = format!("testfiles/music_playlist/album_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let playlist: response::MusicPlaylist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<model::MusicAlbum> =
playlist.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_album_{}", name), map_res.c, {
".last_update" => "[date]"
});
}
}