diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index e7610ed..49e7479 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -35,10 +35,15 @@ jobs: rustypipe-botguard --version - name: ๐Ÿ“Ž Clippy - run: cargo clippy --all --tests --features=rss,indicatif,audiotag -- -D warnings + run: | + cargo clippy --all --tests --features=rss,userdata,indicatif,audiotag -- -D warnings + cargo clippy --package=rustypipe --tests -- -D warnings + cargo clippy --package=rustypipe-downloader -- -D warnings + cargo clippy --package=rustypipe-cli -- -D warnings + cargo clippy --package=rustypipe-cli --features=timezone -- -D warnings - name: ๐Ÿงช Test - run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace -- --skip 'cookie_auth::' + run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss,userdata --workspace -- --skip 'user_data::' env: ALL_PROXY: "http://warpproxy:8124" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d48fd4e..9a0cbb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,4 +10,8 @@ repos: hooks: - id: cargo-fmt - id: cargo-clippy - args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"] + name: cargo-clippy rustypipe + args: ["--package=rustypipe", "--tests", "--", "-D", "warnings"] + - id: cargo-clippy + name: cargo-clippy workspace + args: ["--all", "--tests", "--features=rss,userdata,indicatif,audiotag", "--", "-D", "warnings"] diff --git a/.woodpecker.yml b/.woodpecker.yml deleted file mode 100644 index c76d6d0..0000000 --- a/.woodpecker.yml +++ /dev/null @@ -1,10 +0,0 @@ -steps: - test: - image: rust:latest - environment: - - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse - commands: - - rustup component add rustfmt clippy - - cargo fmt --all --check - - cargo clippy --all --features=rss -- -D warnings - - cargo test --features=rss --workspace diff --git a/Cargo.toml b/Cargo.toml index 4831324..b9a7dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ rustypipe-downloader = { path = "./downloader", version = "0.2.1", default-featu default = ["default-tls"] rss = ["dep:quick-xml"] +userdata = [] # Reqwest TLS options default-tls = ["reqwest/default-tls"] @@ -126,6 +127,6 @@ tracing-test.workspace = true [package.metadata.docs.rs] # To build locally: -# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss --no-deps --open -features = ["rss"] +# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features rss,userdata --no-deps --open +features = ["rss", "userdata"] rustdoc-args = ["--cfg", "docsrs"] diff --git a/Justfile b/Justfile index 76e8102..d8bd7aa 100644 --- a/Justfile +++ b/Justfile @@ -1,19 +1,19 @@ test: - # cargo test --features=rss - cargo nextest run --workspace --features=rss --no-fail-fast --retries 1 -- --skip 'cookie_auth::' + # cargo test --features=rss,userdata + cargo nextest run --workspace --features=rss,userdata --no-fail-fast --retries 1 -- --skip 'user_data::' unittest: - cargo nextest run --features=rss --no-fail-fast --lib + cargo nextest run --features=rss,userdata --no-fail-fast --lib testyt: - cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube -- --skip 'cookie_auth::' + cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- --skip 'user_data::' testyt-cookie: - cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube + cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube testyt-localized: - YT_LANG=th cargo nextest run --features=rss --no-fail-fast --retries 1 --test youtube -- \ - --skip 'cookie_auth::' --skip 'search_suggestion' --skip 'isrc_search_languages' + YT_LANG=th cargo nextest run --features=rss,userdata --no-fail-fast --retries 1 --test youtube -- \ + --skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages' testintl: #!/usr/bin/env bash @@ -33,7 +33,7 @@ testintl: echo "---TESTS FOR $YT_LANG ---" if YT_LANG="$YT_LANG" cargo nextest run --no-fail-fast --retries 1 --test-threads 4 --test youtube -- \ - --skip 'cookie_auth::' --skip 'search_suggestion' --skip 'isrc_search_languages' --skip 'resolve_'; then + --skip 'user_data::' --skip 'search_suggestion' --skip 'isrc_search_languages' --skip 'resolve_'; then echo "--- $YT_LANG COMPLETED ---" else echo "--- $YT_LANG FAILED ---" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c4cd005..d6954b5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -42,7 +42,7 @@ rustls-tls-native-roots = [ ] [dependencies] -rustypipe = { workspace = true, features = ["rss"] } +rustypipe = { workspace = true, features = ["rss", "userdata"] } rustypipe-downloader.workspace = true reqwest.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 0ccb5ac..4b602d8 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true publish = false [dependencies] -rustypipe = { path = "../" } +rustypipe = { path = "../", features = ["userdata"] } reqwest.workspace = true tokio = { workspace = true, features = ["rt-multi-thread"] } futures-util.workspace = true diff --git a/codegen/src/download_testfiles.rs b/codegen/src/download_testfiles.rs index 9e6f2bf..cd85654 100644 --- a/codegen/src/download_testfiles.rs +++ b/codegen/src/download_testfiles.rs @@ -39,9 +39,6 @@ pub async fn download_testfiles() { search_playlists().await; search_empty().await; trending().await; - history().await; - subscriptions().await; - subscription_feed().await; music_playlist().await; music_playlist_cont().await; @@ -65,6 +62,12 @@ pub async fn download_testfiles() { music_charts().await; music_genres().await; music_genre().await; + + // User data + history().await; + subscriptions().await; + subscription_feed().await; + music_history().await; music_saved_artists().await; music_saved_albums().await; @@ -464,7 +467,7 @@ async fn trending() { } async fn history() { - let json_path = path!(*TESTFILES_DIR / "history" / "history.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "history.json"); if json_path.exists() { return; } @@ -474,7 +477,7 @@ async fn history() { } async fn subscriptions() { - let json_path = path!(*TESTFILES_DIR / "history" / "subscriptions.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "subscriptions.json"); if json_path.exists() { return; } @@ -484,7 +487,7 @@ async fn subscriptions() { } async fn subscription_feed() { - let json_path = path!(*TESTFILES_DIR / "history" / "subscription_feed.json"); + let json_path = path!(*TESTFILES_DIR / "userdata" / "subscription_feed.json"); if json_path.exists() { return; } @@ -816,7 +819,7 @@ async fn music_genre() { } async fn music_history() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "music_history.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "music_history.json"); if json_path.exists() { return; } @@ -826,7 +829,7 @@ async fn music_history() { } async fn music_saved_artists() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_artists.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_artists.json"); if json_path.exists() { return; } @@ -836,7 +839,7 @@ async fn music_saved_artists() { } async fn music_saved_albums() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_albums.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_albums.json"); if json_path.exists() { return; } @@ -846,7 +849,7 @@ async fn music_saved_albums() { } async fn music_saved_tracks() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_tracks.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_tracks.json"); if json_path.exists() { return; } @@ -856,7 +859,7 @@ async fn music_saved_tracks() { } async fn music_saved_playlists() { - let json_path = path!(*TESTFILES_DIR / "music_history" / "saved_playlists.json"); + let json_path = path!(*TESTFILES_DIR / "music_userdata" / "saved_playlists.json"); if json_path.exists() { return; } diff --git a/src/client/mod.rs b/src/client/mod.rs index 61e358e..cbdd5b4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -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; diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index b09656a..002d7c1 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -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 { - self.clone() - .authenticated() - .music_playlist("LM") - .await - .map_err(util::map_internal_playlist_err) - } } impl MapResponse for response::MusicPlaylist { diff --git a/src/client/music_history.rs b/src/client/music_userdata.rs similarity index 90% rename from src/client/music_history.rs rename to src/client/music_userdata.rs index 50c5844..8c256cb 100644 --- a/src/client/music_history.rs +++ b/src/client/music_userdata.rs @@ -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 { + self.clone() + .authenticated() + .music_playlist("LM") + .await + .map_err(crate::util::map_internal_playlist_err) + } } impl MapResponse>> 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 = diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 1440ebe..49093eb 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -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> for response::MusicContinuation { } } +#[cfg(feature = "userdata")] impl MapResponse>> for response::Continuation { fn map_response( self, @@ -270,6 +274,7 @@ impl MapResponse>> for response::Continuation { } } +#[cfg(feature = "userdata")] impl MapResponse>> for response::MusicContinuation { fn map_response( self, @@ -422,6 +427,8 @@ impl Paginator { } } +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] impl Paginator> { /// Get the next page from the paginator (or `None` if the paginator is exhausted) pub async fn next>(&self, query: Q) -> Result, Error> { @@ -437,6 +444,8 @@ impl Paginator> { } } +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] impl Paginator> { /// Get the next page from the paginator (or `None` if the paginator is exhausted) pub async fn next>(&self, query: Q) -> Result, Error> { @@ -533,7 +542,11 @@ macro_rules! paginator { } paginator!(Comment); +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] paginator!(HistoryItem); +#[cfg(feature = "userdata")] +#[cfg_attr(docsrs, doc(cfg(feature = "userdata")))] paginator!(HistoryItem); #[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(); diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 79e2329..b911b99 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -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 { - 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 { - self.clone() - .authenticated() - .playlist("WL") - .await - .map_err(util::map_internal_playlist_err) - } } impl MapResponse for response::Playlist { diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index c23c449..0dc466f 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -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; diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index d2bf983..f7cadf6 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -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")] pub title: Option, /// Playlist ID (only for playlists) @@ -1270,6 +1275,7 @@ impl MusicListMapper { } } + #[cfg(feature = "userdata")] pub fn conv_history_items( self, date_txt: Option, diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index f855f3f..2a48bc6 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -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, #[serde(alias = "items")] contents: MapResult>, @@ -298,6 +301,7 @@ pub(crate) struct YouTubeListRenderer { pub contents: MapResult>, } +#[cfg(feature = "userdata")] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ItemSectionHeader { @@ -904,6 +908,7 @@ impl YouTubeListMapper { res.c.into_iter().for_each(|item| self.map_item(item)); } + #[cfg(feature = "userdata")] pub(crate) fn conv_history_items( self, date_txt: Option, diff --git a/src/client/snapshots/rustypipe__client__music_history__tests__map_history.snap b/src/client/snapshots/rustypipe__client__music_userdata__tests__map_history.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__music_history__tests__map_history.snap rename to src/client/snapshots/rustypipe__client__music_userdata__tests__map_history.snap diff --git a/src/client/snapshots/rustypipe__client__history__tests__map_history.snap b/src/client/snapshots/rustypipe__client__userdata__tests__map_history.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__history__tests__map_history.snap rename to src/client/snapshots/rustypipe__client__userdata__tests__map_history.snap diff --git a/src/client/snapshots/rustypipe__client__history__tests__map_subscription_feed.snap b/src/client/snapshots/rustypipe__client__userdata__tests__map_subscription_feed.snap similarity index 100% rename from src/client/snapshots/rustypipe__client__history__tests__map_subscription_feed.snap rename to src/client/snapshots/rustypipe__client__userdata__tests__map_subscription_feed.snap diff --git a/src/client/history.rs b/src/client/userdata.rs similarity index 90% rename from src/client/history.rs rename to src/client/userdata.rs index 427715c..c2e4815 100644 --- a/src/client/history.rs +++ b/src/client/userdata.rs @@ -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 { + 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 { + self.clone() + .authenticated() + .playlist("WL") + .await + .map_err(crate::util::map_internal_playlist_err) + } } impl MapResponse>> 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 = diff --git a/src/util/mod.rs b/src/util/mod.rs index 0f4499f..fc6af15 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -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 } diff --git a/src/util/timeago.rs b/src/util/timeago.rs index 011b3ed..8452415 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -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, diff --git a/testfiles/music_history/music_history.json b/testfiles/music_userdata/music_history.json similarity index 100% rename from testfiles/music_history/music_history.json rename to testfiles/music_userdata/music_history.json diff --git a/testfiles/music_history/saved_albums.json b/testfiles/music_userdata/saved_albums.json similarity index 100% rename from testfiles/music_history/saved_albums.json rename to testfiles/music_userdata/saved_albums.json diff --git a/testfiles/music_history/saved_artists.json b/testfiles/music_userdata/saved_artists.json similarity index 100% rename from testfiles/music_history/saved_artists.json rename to testfiles/music_userdata/saved_artists.json diff --git a/testfiles/music_history/saved_playlists.json b/testfiles/music_userdata/saved_playlists.json similarity index 100% rename from testfiles/music_history/saved_playlists.json rename to testfiles/music_userdata/saved_playlists.json diff --git a/testfiles/music_history/saved_tracks.json b/testfiles/music_userdata/saved_tracks.json similarity index 100% rename from testfiles/music_history/saved_tracks.json rename to testfiles/music_userdata/saved_tracks.json diff --git a/testfiles/history/history.json b/testfiles/userdata/history.json similarity index 100% rename from testfiles/history/history.json rename to testfiles/userdata/history.json diff --git a/testfiles/history/subscription_feed.json b/testfiles/userdata/subscription_feed.json similarity index 100% rename from testfiles/history/subscription_feed.json rename to testfiles/userdata/subscription_feed.json diff --git a/testfiles/history/subscriptions.json b/testfiles/userdata/subscriptions.json similarity index 100% rename from testfiles/history/subscriptions.json rename to testfiles/userdata/subscriptions.json diff --git a/tests/youtube.rs b/tests/youtube.rs index 140770a..9a5fc35 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -5,7 +5,7 @@ use std::fmt::Display; use std::str::FromStr; use rstest::{fixture, rstest}; -use rustypipe::model::{HistoryItem, TrackItem, TrackType, VideoItem}; +use rustypipe::model::TrackType; use rustypipe::param::{AlbumOrder, LANGUAGES}; use time::{macros::date, OffsetDateTime}; @@ -2728,9 +2728,12 @@ async fn isrc_search_languages(rp: RustyPipe) { } } -mod cookie_auth { +#[cfg(feature = "userdata")] +mod user_data { use super::*; + use rustypipe::model::{HistoryItem, TrackItem, VideoItem}; + #[rstest] #[tokio::test] async fn history(rp: RustyPipe) { @@ -2814,6 +2817,30 @@ mod cookie_auth { let tracks = rp.query().music_liked_tracks().await.unwrap(); assert_next_items(tracks.tracks, rp.query(), 5).await; } + + /// Assert that the history paginator produces at least n items + async fn assert_next_history>( + paginator: Paginator>, + query: Q, + n_items: usize, + ) { + let mut p = paginator; + let query = query.as_ref(); + p.extend_limit(query, n_items).await.unwrap(); + assert_gte(p.items.len(), n_items, "items"); + } + + /// Assert that the music history paginator produces at least n items + async fn assert_next_music_history>( + paginator: Paginator>, + query: Q, + n_items: usize, + ) { + let mut p = paginator; + let query = query.as_ref(); + p.extend_limit(query, n_items).await.unwrap(); + assert_gte(p.items.len(), n_items, "items"); + } } #[rstest] @@ -2940,30 +2967,6 @@ async fn assert_next_items>( assert_gte(p.items.len(), n_items, "items"); } -/// Assert that the history paginator produces at least n items -async fn assert_next_history>( - paginator: Paginator>, - query: Q, - n_items: usize, -) { - let mut p = paginator; - let query = query.as_ref(); - p.extend_limit(query, n_items).await.unwrap(); - assert_gte(p.items.len(), n_items, "items"); -} - -/// Assert that the music history paginator produces at least n items -async fn assert_next_music_history>( - paginator: Paginator>, - query: Q, - n_items: usize, -) { - let mut p = paginator; - let query = query.as_ref(); - p.extend_limit(query, n_items).await.unwrap(); - assert_gte(p.items.len(), n_items, "items"); -} - #[track_caller] fn assert_frameset(frameset: &Frameset) { assert_gte(frameset.frame_height, 20, "frame height"); @@ -3025,10 +3028,6 @@ async fn all_send_and_sync() { rp.query() .drm_license("", rustypipe::model::DrmSystem::Widevine, "", "", &[]), ); - send_and_sync(rp.query().history()); - send_and_sync(rp.query().history_continuation("", None)); - send_and_sync(rp.query().history_search("")); - send_and_sync(rp.query().liked_videos()); send_and_sync(rp.query().music_album("")); send_and_sync(rp.query().music_artist("", false)); send_and_sync(rp.query().music_artist_albums("", None, None)); @@ -3037,9 +3036,6 @@ async fn all_send_and_sync() { send_and_sync(rp.query().music_details("")); send_and_sync(rp.query().music_genre("")); send_and_sync(rp.query().music_genres()); - send_and_sync(rp.query().music_history()); - send_and_sync(rp.query().music_history_continuation("", None)); - send_and_sync(rp.query().music_liked_tracks()); send_and_sync(rp.query().music_lyrics("")); send_and_sync(rp.query().music_new_albums()); send_and_sync(rp.query().music_new_videos()); @@ -3048,10 +3044,6 @@ async fn all_send_and_sync() { send_and_sync(rp.query().music_radio_playlist("")); send_and_sync(rp.query().music_radio_track("")); send_and_sync(rp.query().music_related("")); - send_and_sync(rp.query().music_saved_albums()); - send_and_sync(rp.query().music_saved_artists()); - send_and_sync(rp.query().music_saved_playlists()); - send_and_sync(rp.query().music_saved_tracks()); send_and_sync(rp.query().music_search::("", None)); send_and_sync(rp.query().music_search_albums("")); send_and_sync(rp.query().music_search_artists("")); @@ -3068,17 +3060,32 @@ async fn all_send_and_sync() { send_and_sync(rp.query().raw(ClientType::Desktop, "", "")); send_and_sync(rp.query().resolve_string("", false)); send_and_sync(rp.query().resolve_url("", false)); - send_and_sync(rp.query().saved_playlists()); send_and_sync(rp.query().search::("")); send_and_sync( rp.query() .search_filter::("", &SearchFilter::default()), ); send_and_sync(rp.query().search_suggestion("")); - send_and_sync(rp.query().subscription_feed()); - send_and_sync(rp.query().subscriptions()); send_and_sync(rp.query().trending()); send_and_sync(rp.query().video_comments("", None)); send_and_sync(rp.query().video_details("")); - send_and_sync(rp.query().watch_later()); + + #[cfg(feature = "userdata")] + { + send_and_sync(rp.query().history()); + send_and_sync(rp.query().history_continuation("", None)); + send_and_sync(rp.query().history_search("")); + send_and_sync(rp.query().liked_videos()); + send_and_sync(rp.query().watch_later()); + send_and_sync(rp.query().music_history()); + send_and_sync(rp.query().music_history_continuation("", None)); + send_and_sync(rp.query().music_saved_albums()); + send_and_sync(rp.query().music_saved_artists()); + send_and_sync(rp.query().music_saved_playlists()); + send_and_sync(rp.query().music_saved_tracks()); + send_and_sync(rp.query().saved_playlists()); + send_and_sync(rp.query().subscription_feed()); + send_and_sync(rp.query().subscriptions()); + send_and_sync(rp.query().music_liked_tracks()); + } }