use std::fmt::Debug;
use crate::{
client::{
response::{self, music_item::MusicListMapper},
ClientType, MapResponse, QBrowseParams, RustyPipeQuery,
},
error::{Error, ExtractionError},
model::{
paginator::{ContinuationEndpoint, Paginator},
AlbumItem, ArtistItem, HistoryItem, MusicPlaylist, MusicPlaylistItem, TrackItem,
},
serializer::MapResult,
};
use super::{MapRespCtx, MapRespOptions, QContinuation};
impl RustyPipeQuery {
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_history(&self) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
let request_body = QBrowseParams {
browse_id: "FEmusic_history",
params: "oggECgIIAQ%3D%3D",
};
self.clone()
.authenticated()
.execute_request::<response::MusicHistory, _, _>(
ClientType::DesktopMusic,
"music_history",
"",
"browse",
&request_body,
)
.await
}
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_history_continuation<S: AsRef<str> + Debug>(
&self,
ctoken: S,
visitor_data: Option<&str>,
) -> Result<Paginator<HistoryItem<TrackItem>>, Error> {
let ctoken = ctoken.as_ref();
let request_body = QContinuation {
continuation: ctoken,
};
self.clone()
.authenticated()
.execute_request_ctx::<response::MusicContinuation, _, _>(
ClientType::Desktop,
"history_continuation",
ctoken,
"browse",
&request_body,
MapRespOptions {
visitor_data,
..Default::default()
},
)
.await
}
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_saved_artists(&self) -> Result<Paginator<ArtistItem>, Error> {
self.clone()
.authenticated()
.continuation(
"4qmFsgIyEh5GRW11c2ljX2xpYnJhcnlfY29ycHVzX2FydGlzdHMaEGdnTUdLZ1FJQUJBQm9BWUI%3D",
ContinuationEndpoint::MusicBrowse,
None,
)
.await
}
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_saved_albums(&self) -> Result<Paginator<AlbumItem>, Error> {
self.clone()
.authenticated()
.continuation(
"4qmFsgIoEhRGRW11c2ljX2xpa2VkX2FsYnVtcxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
ContinuationEndpoint::MusicBrowse,
None,
)
.await
}
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_saved_tracks(&self) -> Result<Paginator<TrackItem>, Error> {
self.clone()
.authenticated()
.continuation(
"4qmFsgIoEhRGRW11c2ljX2xpa2VkX3ZpZGVvcxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
ContinuationEndpoint::MusicBrowse,
None,
)
.await
}
#[tracing::instrument(skip(self), level = "error")]
pub async fn music_saved_playlists(&self) -> Result<Paginator<MusicPlaylistItem>, Error> {
self.clone()
.authenticated()
.continuation(
"4qmFsgIrEhdGRW11c2ljX2xpa2VkX3BsYXlsaXN0cxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D",
ContinuationEndpoint::MusicBrowse,
None,
)
.await
}
pub async fn music_liked_tracks(&self) -> Result<MusicPlaylist, Error> {
self.clone()
.authenticated()
.music_playlist("LM")
.await
.map_err(crate::util::map_internal_playlist_err)
}
}
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicHistory {
fn map_response(
self,
ctx: &MapRespCtx<'_>,
) -> Result<MapResult<Paginator<HistoryItem<TrackItem>>>, ExtractionError> {
let contents = match self.contents {
response::music_playlist::Contents::SingleColumnBrowseResultsRenderer(c) => {
c.contents
.into_iter()
.next()
.ok_or(ExtractionError::InvalidData("no content".into()))?
.tab_renderer
.content
.section_list_renderer
}
response::music_playlist::Contents::TwoColumnBrowseResultsRenderer {
secondary_contents,
..
} => secondary_contents.section_list_renderer,
};
let mut map_res = MapResult::default();
for shelf in contents.contents {
let shelf = if let response::music_item::ItemSection::MusicShelfRenderer(s) = shelf {
s
} else {
continue;
};
let mut mapper = MusicListMapper::new(ctx.lang);
mapper.map_response(shelf.contents);
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
}
let ctoken = contents
.continuations
.into_iter()
.next()
.map(|c| c.next_continuation_data.continuation);
Ok(MapResult {
c: Paginator::new_ext(
None,
map_res.c,
ctoken,
ctx.visitor_data.map(str::to_owned),
ContinuationEndpoint::MusicBrowse,
true,
),
warnings: map_res.warnings,
})
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader};
use path_macro::path;
use crate::util::tests::TESTFILES;
use super::*;
#[test]
fn map_history() {
let json_path = path!(*TESTFILES / "music_userdata" / "music_history.json");
let json_file = File::open(json_path).unwrap();
let history: response::MusicHistory =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = history.map_response(&MapRespCtx::test("")).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(map_res.c, {
".items[].playback_date" => "[date]",
});
}
}