feat: add related playlists
This commit is contained in:
parent
dfd33d5e9b
commit
8af1ae303d
11 changed files with 4712 additions and 16 deletions
|
|
@ -40,6 +40,7 @@ pub async fn download_testfiles(project_root: &Path) {
|
|||
|
||||
music_playlist(&testfiles).await;
|
||||
music_playlist_cont(&testfiles).await;
|
||||
music_playlist_related(&testfiles).await;
|
||||
music_album(&testfiles).await;
|
||||
music_search(&testfiles).await;
|
||||
music_search_tracks(&testfiles).await;
|
||||
|
|
@ -535,6 +536,30 @@ async fn music_playlist_cont(testfiles: &Path) {
|
|||
playlist.tracks.next(&rp.query()).await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
async fn music_playlist_related(testfiles: &Path) {
|
||||
let mut json_path = testfiles.to_path_buf();
|
||||
json_path.push("music_playlist");
|
||||
json_path.push("playlist_related.json");
|
||||
if json_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rp = RustyPipe::new();
|
||||
let playlist = rp
|
||||
.query()
|
||||
.music_playlist("PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rp = rp_testfile(&json_path);
|
||||
playlist
|
||||
.related_playlists
|
||||
.next(&rp.query())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn music_album(testfiles: &Path) {
|
||||
for (name, id) in [
|
||||
("one_artist", "MPREb_nlBWQROfvjo"),
|
||||
|
|
|
|||
|
|
@ -63,12 +63,13 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
let header = self.header.music_detail_header_renderer;
|
||||
|
||||
let mut content = self.contents.single_column_browse_results_renderer.contents;
|
||||
let mut shelf = content
|
||||
let mut music_contents = content
|
||||
.try_swap_remove(0)
|
||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))?
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.section_list_renderer;
|
||||
let mut shelf = music_contents
|
||||
.contents
|
||||
.into_iter()
|
||||
.find_map(|section| match section {
|
||||
|
|
@ -121,6 +122,11 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
None => Some(map_res.c.len() as u64),
|
||||
};
|
||||
|
||||
let related_ctoken = music_contents
|
||||
.continuations
|
||||
.try_swap_remove(0)
|
||||
.map(|c| c.next_continuation_data.continuation);
|
||||
|
||||
Ok(MapResult {
|
||||
c: MusicPlaylist {
|
||||
id: playlist_id,
|
||||
|
|
@ -137,6 +143,13 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
None,
|
||||
crate::param::ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
related_playlists: Paginator::new_ext(
|
||||
None,
|
||||
Vec::new(),
|
||||
related_ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
},
|
||||
warnings: map_res.warnings,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -128,12 +128,34 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
||||
let mut mapper = MusicListMapper::new(lang);
|
||||
let mut shelf = self.continuation_contents.music_shelf_continuation;
|
||||
mapper.map_response(shelf.contents);
|
||||
let map_res = mapper.items();
|
||||
let mut continuations = Vec::new();
|
||||
|
||||
let ctoken = shelf
|
||||
.continuations
|
||||
match self.continuation_contents {
|
||||
response::music_item::ContinuationContents::MusicShelfContinuation(mut shelf) => {
|
||||
mapper.map_response(shelf.contents);
|
||||
continuations.append(&mut shelf.continuations);
|
||||
}
|
||||
response::music_item::ContinuationContents::SectionListContinuation(contents) => {
|
||||
for c in contents.contents {
|
||||
match c {
|
||||
response::music_item::ItemSection::MusicShelfRenderer(mut shelf) => {
|
||||
mapper.map_response(shelf.contents);
|
||||
continuations.append(&mut shelf.continuations);
|
||||
}
|
||||
response::music_item::ItemSection::MusicCarouselShelfRenderer {
|
||||
contents,
|
||||
..
|
||||
} => {
|
||||
mapper.map_response(contents);
|
||||
}
|
||||
response::music_item::ItemSection::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let map_res = mapper.items();
|
||||
let ctoken = continuations
|
||||
.try_swap_remove(0)
|
||||
.map(|cont| cont.next_continuation_data.continuation);
|
||||
|
||||
|
|
@ -281,7 +303,7 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::model::{PlaylistItem, TrackItem};
|
||||
use crate::model::{MusicPlaylistItem, PlaylistItem, TrackItem};
|
||||
use crate::param::Language;
|
||||
|
||||
#[rstest]
|
||||
|
|
@ -352,4 +374,26 @@ mod tests {
|
|||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{}", name), paginator);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("playlist_related", "music_playlist/playlist_related")]
|
||||
fn map_continuation_music_playlists(#[case] name: &str, #[case] path: &str) {
|
||||
let filename = format!("testfiles/{}.json", path);
|
||||
let json_path = Path::new(&filename);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
||||
let items: response::MusicContinuation =
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||
let map_res: MapResult<Paginator<MusicItem>> =
|
||||
items.map_response("", Language::En, None).unwrap();
|
||||
let paginator: Paginator<MusicPlaylistItem> =
|
||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||
|
||||
assert!(
|
||||
map_res.warnings.is_empty(),
|
||||
"deserialization/mapping warnings: {:?}",
|
||||
map_res.warnings
|
||||
);
|
||||
insta::assert_ron_snapshot!(format!("map_{}", name), paginator);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType},
|
||||
MusicContinuationData, ThumbnailsWrap,
|
||||
ContentsRenderer, MusicContinuationData, ThumbnailsWrap,
|
||||
};
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -156,16 +156,15 @@ 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>,
|
||||
/*
|
||||
/// Continuation token for fetching recommended items
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub continuations: Vec<MusicContinuation>,
|
||||
*/
|
||||
pub continuations: Vec<MusicContinuationData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -204,9 +203,10 @@ pub(crate) struct MusicContinuation {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationContents {
|
||||
pub(crate) enum ContinuationContents {
|
||||
#[serde(alias = "musicPlaylistShelfContinuation")]
|
||||
pub music_shelf_continuation: MusicShelf,
|
||||
MusicShelfContinuation(MusicShelf),
|
||||
SectionListContinuation(ContentsRenderer<ItemSection>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -2268,4 +2268,10 @@ MusicPlaylist(
|
|||
ctoken: Some("4qmFsgI-EiRWTFBMNWREeDY4MVQ0YlI3WkYxSXVXek92MW9tbFJiRTdQaUoaFmVnWlFWRHBEUjFtU0FRTUl1Z1ElM0Q%3D"),
|
||||
endpoint: music_browse,
|
||||
),
|
||||
related_playlists: Paginator(
|
||||
count: None,
|
||||
items: [],
|
||||
ctoken: Some("4qmFsgIwEiRWTFBMNWREeDY4MVQ0YlI3WkYxSXVXek92MW9tbFJiRTdQaUoaCGtnRURDTTBH"),
|
||||
endpoint: music_browse,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1488,4 +1488,10 @@ MusicPlaylist(
|
|||
ctoken: None,
|
||||
endpoint: music_browse,
|
||||
),
|
||||
related_playlists: Paginator(
|
||||
count: None,
|
||||
items: [],
|
||||
ctoken: Some("4qmFsgIwEiRWTFBMMUotNkpPY2tadEVfUDlYeDhEM2IyTzZ3MGlkaHVLQmUaCGtnRURDTTBH"),
|
||||
endpoint: music_browse,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2048,4 +2048,10 @@ MusicPlaylist(
|
|||
ctoken: None,
|
||||
endpoint: music_browse,
|
||||
),
|
||||
related_playlists: Paginator(
|
||||
count: None,
|
||||
items: [],
|
||||
ctoken: Some("4qmFsgI5Ei1WTFJEQ0xBSzV1eV9rRlFYZG5xTWFRQ1Z4MndwVU00WmZic0dDRGliWnRrSmsaCGtnRURDTTBH"),
|
||||
endpoint: music_browse,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,204 @@
|
|||
---
|
||||
source: src/client/pagination.rs
|
||||
expression: paginator
|
||||
---
|
||||
Paginator(
|
||||
count: Some(10),
|
||||
items: [
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_meKJG4fRVGUG1Z5P6O-OVRJIja9U3Ctt8",
|
||||
name: "Party Schlager",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/xrAa318NOyL334j4P3KOeuvMh2NdaC3zbcRfdP2VPQ3o_MDthD0ueeMTh-Hfk5kzmkp73nH8enOH1as=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/xrAa318NOyL334j4P3KOeuvMh2NdaC3zbcRfdP2VPQ3o_MDthD0ueeMTh-Hfk5kzmkp73nH8enOH1as=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nCUL5fa0G5mSAxmXU9tu4uGM1SoZ44OPA",
|
||||
name: "Happy German Pop",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/vqBjVRRp93TYop30YxN84sS9ITPVBWYCpzGMBBv-1KVpoQaS0kLixwXYTMX8p84wWrMRj-IrWipg6rE=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/vqBjVRRp93TYop30YxN84sS9ITPVBWYCpzGMBBv-1KVpoQaS0kLixwXYTMX8p84wWrMRj-IrWipg6rE=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_ksgASdLdWz-NAd_rDVowAxqT6xAEN5JAA",
|
||||
name: "Oktoberfest",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/_wFTtKjF2JrQPeF4FGAJeayJD1p5I24gLf92WZc7Q6M5uOMSVouuSOB0iS7jlV6vG18kMViPjBmgW6Y=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/_wFTtKjF2JrQPeF4FGAJeayJD1p5I24gLf92WZc7Q6M5uOMSVouuSOB0iS7jlV6vG18kMViPjBmgW6Y=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_lEt1yX7jdZ0dcr4e8N5Y8pUYsaO8nUc2E",
|
||||
name: "\'10s German Pop",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/slwtKgp4BHop8sstyZ2YRVKhg-cOpcFx2vWSmBaeBP4bloW0gVe6eJcqFcO-4VkjfUTDJTdcIrzGoch6=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/slwtKgp4BHop8sstyZ2YRVKhg-cOpcFx2vWSmBaeBP4bloW0gVe6eJcqFcO-4VkjfUTDJTdcIrzGoch6=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_kn1IcNFwwz5N_qekUFnvxYzPYHHRQ9_mY",
|
||||
name: "\'10s Schlager",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Lmxml0utaSLguhcR3Fre_60dbUfF-T87l4KouMD_nWdVEz32sn4RNOZNOLiqhBjMf0bs4LnFPSsUdJY=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/Lmxml0utaSLguhcR3Fre_60dbUfF-T87l4KouMD_nWdVEz32sn4RNOZNOLiqhBjMf0bs4LnFPSsUdJY=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_kvH_VWOl1tx0Ad4H2F_4jaTMnImcuCFXc",
|
||||
name: "German Pop Throwback",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/gCaww9YpZ-Th2I6Y7mTarqo_JJPkBerhUPTchgu06VowkRZc8ndS9Efa-AIz5uqKoGWiNf32YzkFUQ=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/gCaww9YpZ-Th2I6Y7mTarqo_JJPkBerhUPTchgu06VowkRZc8ndS9Efa-AIz5uqKoGWiNf32YzkFUQ=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_kSJ30ym-GRlGJvlZsM9xnK9dAZdonBoB0",
|
||||
name: "Presenting Mark Forster",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ZarhsSrgS22O6JwaZl4FfgeXOtaVyq0GWZKTLCrByxYgQhrEVRj1oP4MO__moYmQuKCRB1BB67JDZfg=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/ZarhsSrgS22O6JwaZl4FfgeXOtaVyq0GWZKTLCrByxYgQhrEVRj1oP4MO__moYmQuKCRB1BB67JDZfg=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "PLW4r_btbY6e3p8RuogQM9F8S3dFNgsMqn",
|
||||
name: "German Pop",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/DN7nE9pKDZ6gU-k6TJbMmIhBf-c_CfczCJ_hACD0MMIQzCXf9h69_b8y5PaStkznCsiPBHYcuVc=s192",
|
||||
width: 192,
|
||||
height: 192,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://yt3.ggpht.com/DN7nE9pKDZ6gU-k6TJbMmIhBf-c_CfczCJ_hACD0MMIQzCXf9h69_b8y5PaStkznCsiPBHYcuVc=s576",
|
||||
width: 576,
|
||||
height: 576,
|
||||
),
|
||||
],
|
||||
channel: Some(ChannelId(
|
||||
id: "UCnOnrhWLv3YMoTVWqF4k82w",
|
||||
name: "botevpd",
|
||||
)),
|
||||
track_count: Some(49),
|
||||
from_ytm: false,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_m55iAnFa25l8bl7m8vsTsmGph3-_yVBW0",
|
||||
name: "German Singalong Songs",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/JLLNoS2hhZ54GcYoY_mD0fxVVxcZ7Ay_rT41TIbjv6n3efAUqAexZpcbK7qAS8vLw_K4NuV2R3_iRPfs=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/JLLNoS2hhZ54GcYoY_mD0fxVVxcZ7Ay_rT41TIbjv6n3efAUqAexZpcbK7qAS8vLw_K4NuV2R3_iRPfs=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
MusicPlaylistItem(
|
||||
id: "RDCLAK5uy_nLm6_mnMdOQD1vZypJ0yPpzJPuzfPN4d4",
|
||||
name: "German Summer",
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/DKPAdAdEmbP1UYpXmQaFRIvIuZXVkRmgzxx9V_PtsvSZLBRDL8gzv9ibIJbRBcNV0toryLF4tFxEIaE=w226-h226-l90-rj",
|
||||
width: 226,
|
||||
height: 226,
|
||||
),
|
||||
Thumbnail(
|
||||
url: "https://lh3.googleusercontent.com/DKPAdAdEmbP1UYpXmQaFRIvIuZXVkRmgzxx9V_PtsvSZLBRDL8gzv9ibIJbRBcNV0toryLF4tFxEIaE=w544-h544-l90-rj",
|
||||
width: 544,
|
||||
height: 544,
|
||||
),
|
||||
],
|
||||
channel: None,
|
||||
track_count: None,
|
||||
from_ytm: true,
|
||||
),
|
||||
],
|
||||
ctoken: None,
|
||||
endpoint: music_browse,
|
||||
)
|
||||
|
|
@ -1014,6 +1014,8 @@ pub struct MusicPlaylist {
|
|||
pub from_ytm: bool,
|
||||
/// Playlist tracks
|
||||
pub tracks: Paginator<TrackItem>,
|
||||
/// Related playlists
|
||||
pub related_playlists: Paginator<MusicPlaylistItem>,
|
||||
}
|
||||
|
||||
/// YouTube music album object
|
||||
|
|
|
|||
4367
testfiles/music_playlist/playlist_related.json
Normal file
4367
testfiles/music_playlist/playlist_related.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1292,8 +1292,31 @@ async fn music_playlist_cont() {
|
|||
.extend_pages(&rp.query(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(playlist.tracks.items.len() > 100);
|
||||
assert!(playlist.tracks.count.unwrap() > 100);
|
||||
|
||||
assert_gte(playlist.tracks.items.len(), 100, "tracks");
|
||||
assert_gte(playlist.tracks.count.unwrap(), 100, "track count");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn music_playlist_related() {
|
||||
let rp = RustyPipe::builder().strict().build();
|
||||
let mut playlist = rp
|
||||
.query()
|
||||
.music_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
playlist
|
||||
.related_playlists
|
||||
.extend(&rp.query())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_gte(
|
||||
playlist.related_playlists.items.len(),
|
||||
10,
|
||||
"related playlists",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Reference in a new issue