feat!: add userdata feature for all personal data queries (playback history, subscriptions)

This commit is contained in:
ThetaDev 2025-02-07 04:43:35 +01:00
parent c87bac1856
commit 65cb4244c6
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
31 changed files with 189 additions and 143 deletions

View file

@ -3,12 +3,10 @@
pub(crate) mod response;
mod channel;
mod history;
mod music_artist;
mod music_charts;
mod music_details;
mod music_genres;
mod music_history;
mod music_new;
mod music_playlist;
mod music_search;
@ -20,6 +18,13 @@ mod trends;
mod url_resolver;
mod video_details;
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
mod music_userdata;
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
mod userdata;
#[cfg(feature = "rss")]
#[cfg_attr(docsrs, doc(cfg(feature = "rss")))]
mod channel_rss;

View file

@ -122,20 +122,6 @@ impl RustyPipeQuery {
}
Ok(album)
}
/// Get all liked YouTube Music tracks of the logged-in user
///
/// The difference to [`RustyPipeQuery::music_saved_tracks`] is that this function only returns
/// tracks that were explicitly liked by the user.
///
/// Requires authentication cookies.
pub async fn music_liked_tracks(&self) -> Result<MusicPlaylist, Error> {
self.clone()
.authenticated()
.music_playlist("LM")
.await
.map_err(util::map_internal_playlist_err)
}
}
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {

View file

@ -8,7 +8,7 @@ use crate::{
error::{Error, ExtractionError},
model::{
paginator::{ContinuationEndpoint, Paginator},
AlbumItem, ArtistItem, HistoryItem, MusicPlaylistItem, TrackItem,
AlbumItem, ArtistItem, HistoryItem, MusicPlaylist, MusicPlaylistItem, TrackItem,
},
serializer::MapResult,
};
@ -127,6 +127,20 @@ impl RustyPipeQuery {
)
.await
}
/// Get all liked YouTube Music tracks of the logged-in user
///
/// The difference to [`RustyPipeQuery::music_saved_tracks`] is that this function only returns
/// tracks that were explicitly liked by the user.
///
/// Requires authentication cookies.
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 {
@ -195,7 +209,7 @@ mod tests {
#[test]
fn map_history() {
let json_path = path!(*TESTFILES / "music_history" / "music_history.json");
let json_path = path!(*TESTFILES / "music_userdata" / "music_history.json");
let json_file = File::open(json_path).unwrap();
let history: response::MusicHistory =

View file

@ -6,12 +6,15 @@ use crate::model::{
traits::FromYtItem,
Comment, MusicItem, YouTubeItem,
};
use crate::model::{HistoryItem, TrackItem, VideoItem};
use crate::serializer::MapResult;
use self::response::YouTubeListItem;
#[cfg(feature = "userdata")]
use crate::model::{HistoryItem, TrackItem, VideoItem};
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
use super::response::{
music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo},
YouTubeListItem,
};
use super::{
response, ClientType, MapRespCtx, MapRespOptions, MapResponse, QContinuation, RustyPipeQuery,
};
@ -225,6 +228,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
}
}
#[cfg(feature = "userdata")]
impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
fn map_response(
self,
@ -270,6 +274,7 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
}
}
#[cfg(feature = "userdata")]
impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicContinuation {
fn map_response(
self,
@ -422,6 +427,8 @@ impl Paginator<Comment> {
}
}
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
impl Paginator<HistoryItem<VideoItem>> {
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
@ -437,6 +444,8 @@ impl Paginator<HistoryItem<VideoItem>> {
}
}
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
impl Paginator<HistoryItem<TrackItem>> {
/// Get the next page from the paginator (or `None` if the paginator is exhausted)
pub async fn next<Q: AsRef<RustyPipeQuery>>(&self, query: Q) -> Result<Option<Self>, Error> {
@ -533,7 +542,11 @@ macro_rules! paginator {
}
paginator!(Comment);
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
paginator!(HistoryItem<VideoItem>);
#[cfg(feature = "userdata")]
#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))]
paginator!(HistoryItem<TrackItem>);
#[cfg(test)]
@ -620,7 +633,7 @@ mod tests {
}
#[rstest]
#[case::subscriptions("subscriptions", path!("history" / "subscriptions.json"))]
#[case::subscriptions("subscriptions", path!("userdata" / "subscriptions.json"))]
fn map_continuation_channels(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -644,7 +657,7 @@ mod tests {
#[case::playlist_tracks("playlist_tracks", path!("music_playlist" / "playlist_cont.json"))]
#[case::search_tracks("search_tracks", path!("music_search" / "tracks_cont.json"))]
#[case::radio_tracks("radio_tracks", path!("music_details" / "radio_cont.json"))]
#[case::saved_tracks("saved_tracks", path!("music_history" / "saved_tracks.json"))]
#[case::saved_tracks("saved_tracks", path!("music_userdata" / "saved_tracks.json"))]
fn map_continuation_tracks(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -665,7 +678,7 @@ mod tests {
}
#[rstest]
#[case::saved_artists("saved_artists", path!("music_history" / "saved_artists.json"))]
#[case::saved_artists("saved_artists", path!("music_userdata" / "saved_artists.json"))]
fn map_continuation_artists(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -686,7 +699,7 @@ mod tests {
}
#[rstest]
#[case::saved_albums("saved_albums", path!("music_history" / "saved_albums.json"))]
#[case::saved_albums("saved_albums", path!("music_userdata" / "saved_albums.json"))]
fn map_continuation_albums(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();
@ -708,7 +721,7 @@ mod tests {
#[rstest]
#[case::playlist_related("playlist_related", path!("music_playlist" / "playlist_related.json"))]
#[case::saved_playlists("saved_playlists", path!("music_history" / "saved_playlists.json"))]
#[case::saved_playlists("saved_playlists", path!("music_userdata" / "saved_playlists.json"))]
fn map_continuation_music_playlists(#[case] name: &str, #[case] path: PathBuf) {
let json_path = path!(*TESTFILES / path);
let json_file = File::open(json_path).unwrap();

View file

@ -33,28 +33,6 @@ impl RustyPipeQuery {
)
.await
}
/// Get all liked videos of the logged-in user
///
/// Requires authentication cookies.
pub async fn liked_videos(&self) -> Result<Playlist, Error> {
self.clone()
.authenticated()
.playlist("LL")
.await
.map_err(util::map_internal_playlist_err)
}
/// Get the "Watch later" playlist of the logged-in user
///
/// Requires authentication cookies.
pub async fn watch_later(&self) -> Result<Playlist, Error> {
self.clone()
.authenticated()
.playlist("WL")
.await
.map_err(util::map_internal_playlist_err)
}
}
impl MapResponse<Playlist> for response::Playlist {

View file

@ -1,10 +1,8 @@
pub(crate) mod channel;
pub(crate) mod history;
pub(crate) mod music_artist;
pub(crate) mod music_charts;
pub(crate) mod music_details;
pub(crate) mod music_genres;
pub(crate) mod music_history;
pub(crate) mod music_item;
pub(crate) mod music_new;
pub(crate) mod music_playlist;
@ -19,7 +17,6 @@ pub(crate) mod video_item;
pub(crate) use channel::Channel;
pub(crate) use channel::ChannelAbout;
pub(crate) use history::History;
pub(crate) use music_artist::MusicArtist;
pub(crate) use music_artist::MusicArtistAlbums;
pub(crate) use music_charts::MusicCharts;
@ -28,7 +25,6 @@ pub(crate) use music_details::MusicLyrics;
pub(crate) use music_details::MusicRelated;
pub(crate) use music_genres::MusicGenre;
pub(crate) use music_genres::MusicGenres;
pub(crate) use music_history::MusicHistory;
pub(crate) use music_item::MusicContinuation;
pub(crate) use music_new::MusicNew;
pub(crate) use music_playlist::MusicPlaylist;
@ -51,6 +47,15 @@ pub(crate) mod channel_rss;
#[cfg(feature = "rss")]
pub(crate) use channel_rss::ChannelRss;
#[cfg(feature = "userdata")]
pub(crate) mod history;
#[cfg(feature = "userdata")]
pub(crate) use history::History;
#[cfg(feature = "userdata")]
pub(crate) mod music_history;
#[cfg(feature = "userdata")]
pub(crate) use music_history::MusicHistory;
use std::borrow::Cow;
use std::collections::HashMap;
use std::marker::PhantomData;

View file

@ -1,11 +1,10 @@
use serde::Deserialize;
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
use time::UtcOffset;
use crate::{
model::{
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
HistoryItem, MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem, UserItem,
},
param::Language,
serializer::{
@ -23,6 +22,11 @@ use super::{
SimpleHeaderRenderer, Thumbnails, ThumbnailsWrap,
};
#[cfg(feature = "userdata")]
use crate::model::HistoryItem;
#[cfg(feature = "userdata")]
use time::UtcOffset;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ItemSection {
@ -40,6 +44,7 @@ pub(crate) enum ItemSection {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicShelf {
#[cfg(feature = "userdata")]
#[serde_as(as = "Option<Text>")]
pub title: Option<String>,
/// Playlist ID (only for playlists)
@ -1270,6 +1275,7 @@ impl MusicListMapper {
}
}
#[cfg(feature = "userdata")]
pub fn conv_history_items(
self,
date_txt: Option<String>,

View file

@ -2,14 +2,11 @@ use serde::Deserialize;
use serde_with::{
rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError,
};
use time::{OffsetDateTime, UtcOffset};
use time::OffsetDateTime;
use super::{
ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer,
Thumbnails,
};
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails};
use crate::{
model::{Channel, ChannelItem, ChannelTag, HistoryItem, PlaylistItem, VideoItem, YouTubeItem},
model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem},
param::Language,
serializer::{
text::{AttributedText, Text, TextComponent},
@ -18,6 +15,11 @@ use crate::{
util::{self, timeago, TryRemove},
};
#[cfg(feature = "userdata")]
use crate::{client::response::SimpleHeaderRenderer, model::HistoryItem};
#[cfg(feature = "userdata")]
use time::UtcOffset;
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -66,6 +68,7 @@ pub(crate) enum YouTubeListItem {
/// GridRenderer: contains videos on channel page
#[serde(alias = "expandedShelfContentsRenderer", alias = "gridRenderer")]
ItemSectionRenderer {
#[cfg(feature = "userdata")]
header: Option<ItemSectionHeader>,
#[serde(alias = "items")]
contents: MapResult<Vec<YouTubeListItem>>,
@ -298,6 +301,7 @@ pub(crate) struct YouTubeListRenderer {
pub contents: MapResult<Vec<YouTubeListItem>>,
}
#[cfg(feature = "userdata")]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ItemSectionHeader {
@ -904,6 +908,7 @@ impl YouTubeListMapper<VideoItem> {
res.c.into_iter().for_each(|item| self.map_item(item));
}
#[cfg(feature = "userdata")]
pub(crate) fn conv_history_items(
self,
date_txt: Option<String>,

View file

@ -7,7 +7,7 @@ use crate::{
error::{Error, ExtractionError},
model::{
paginator::{ContinuationEndpoint, Paginator},
ChannelItem, HistoryItem, PlaylistItem, VideoItem,
ChannelItem, HistoryItem, Playlist, PlaylistItem, VideoItem,
},
serializer::MapResult,
};
@ -148,6 +148,28 @@ impl RustyPipeQuery {
)
.await
}
/// Get all liked videos of the logged-in user
///
/// Requires authentication cookies.
pub async fn liked_videos(&self) -> Result<Playlist, Error> {
self.clone()
.authenticated()
.playlist("LL")
.await
.map_err(crate::util::map_internal_playlist_err)
}
/// Get the "Watch later" playlist of the logged-in user
///
/// Requires authentication cookies.
pub async fn watch_later(&self) -> Result<Playlist, Error> {
self.clone()
.authenticated()
.playlist("WL")
.await
.map_err(crate::util::map_internal_playlist_err)
}
}
impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::History {
@ -258,7 +280,7 @@ mod tests {
#[test]
fn map_history() {
let json_path = path!(*TESTFILES / "history" / "history.json");
let json_path = path!(*TESTFILES / "userdata" / "history.json");
let json_file = File::open(json_path).unwrap();
let history: response::History =
@ -278,7 +300,7 @@ mod tests {
#[test]
fn map_subscription_feed() {
let json_path = path!(*TESTFILES / "history" / "subscription_feed.json");
let json_path = path!(*TESTFILES / "userdata" / "subscription_feed.json");
let json_file = File::open(json_path).unwrap();
let history: response::History =

View file

@ -21,7 +21,7 @@ use regex::Regex;
use url::Url;
use crate::{
error::{AuthError, Error, ExtractionError},
error::Error,
param::{Country, Language, COUNTRIES},
serializer::text::TextComponent,
};
@ -581,9 +581,10 @@ where
///
/// If no user is logged in, YouTube returns a "NotFound" error. This has to be corrected
/// into a NoLogin error.
#[cfg(feature = "userdata")]
pub fn map_internal_playlist_err(e: Error) -> Error {
if let Error::Extraction(ExtractionError::NotFound { .. }) = e {
Error::Auth(AuthError::NoLogin)
if let Error::Extraction(crate::error::ExtractionError::NotFound { .. }) = e {
Error::Auth(crate::error::AuthError::NoLogin)
} else {
e
}

View file

@ -347,6 +347,7 @@ pub fn parse_textual_date_to_dt(
/// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object.
///
/// Returns None if the date could not be parsed.
#[cfg(feature = "userdata")]
pub fn parse_textual_date_to_d(
lang: Language,
utc_offset: UtcOffset,