refactor!: refactored response models
doc: documented all public methods
This commit is contained in:
parent
4c1876cb55
commit
f526ab38eb
37 changed files with 600 additions and 255 deletions
|
|
@ -5,7 +5,7 @@ use url::Url;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{Channel, ChannelInfo, Paginator, PlaylistItem, VideoItem, YouTubeItem},
|
||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem, YouTubeItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
util,
|
||||
|
|
@ -66,6 +66,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the videos from a YouTube channel
|
||||
pub async fn channel_videos<S: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -74,6 +75,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the short videos from a YouTube channel
|
||||
pub async fn channel_shorts<S: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -82,6 +84,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the livestreams from a YouTube channel
|
||||
pub async fn channel_livestreams<S: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -90,6 +93,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search the videos of a channel
|
||||
pub async fn channel_search<S: AsRef<str>, S2: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -104,6 +108,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the playlists of a channel
|
||||
pub async fn channel_playlists<S: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -127,6 +132,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get additional metadata from the *About* tab of a channel
|
||||
pub async fn channel_info<S: AsRef<str>>(
|
||||
&self,
|
||||
channel_id: S,
|
||||
|
|
@ -181,7 +187,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
|||
mapper.items,
|
||||
mapper.ctoken,
|
||||
self.response_context.visitor_data,
|
||||
crate::param::ContinuationEndpoint::Browse,
|
||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
||||
);
|
||||
|
||||
Ok(MapResult {
|
||||
|
|
@ -487,7 +493,7 @@ mod tests {
|
|||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
model::{Channel, ChannelInfo, Paginator, PlaylistItem, VideoItem},
|
||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ use crate::{
|
|||
use super::{response, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the 15 latest videos from the channel's RSS feed
|
||||
///
|
||||
/// Example: <https://www.youtube.com/feeds/videos.xml?channel_id=UC2DjFE7Xf11URZqWBigcVOQ>
|
||||
///
|
||||
/// Fetching RSS feeds is a lot faster than querying the InnerTube API, so this method is great
|
||||
/// for checking a lot of channels or implementing a subscription feed.
|
||||
pub async fn channel_rss<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
|
||||
let url = format!(
|
||||
"https://www.youtube.com/feeds/videos.xml?channel_id={}",
|
||||
|
|
|
|||
|
|
@ -48,20 +48,26 @@ use crate::{
|
|||
///
|
||||
/// There are multiple clients for accessing the YouTube API which have
|
||||
/// slightly different features
|
||||
///
|
||||
/// - **Desktop**: used by youtube.com
|
||||
/// - **DesktopMusic**: used by music.youtube.com, can access special music data,
|
||||
/// cannot access non-music content
|
||||
/// - **TvHtml5Embed**: used by Smart TVs, can access age-restricted videos
|
||||
/// - **Android**: used by the Android app, no obfuscated URLs, includes lower resolution audio streams
|
||||
/// - **Ios**: used by the iOS app, no obfuscated URLs
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClientType {
|
||||
/// Client used by youtube.com
|
||||
Desktop,
|
||||
/// Client used by music.youtube.com
|
||||
///
|
||||
/// can access YTM-specific data, cannot access non-music content
|
||||
DesktopMusic,
|
||||
/// used by Smart TVs
|
||||
///
|
||||
/// can access age-restricted videos, cannot access non-embeddable videos
|
||||
TvHtml5Embed,
|
||||
/// used by the Android app
|
||||
///
|
||||
/// no obfuscated stream URLs, includes lower resolution audio streams
|
||||
Android,
|
||||
/// used by the iOS app
|
||||
///
|
||||
/// no obfuscated stream URLs
|
||||
Ios,
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +80,7 @@ impl ClientType {
|
|||
}
|
||||
}
|
||||
|
||||
/// YouTube context request parameter
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct YTContext<'a> {
|
||||
|
|
@ -204,6 +211,7 @@ struct RustyPipeOpts {
|
|||
visitor_data: Option<String>,
|
||||
}
|
||||
|
||||
/// Builder to construct a new RustyPipe client
|
||||
pub struct RustyPipeBuilder {
|
||||
storage: Option<Box<dyn CacheStorage>>,
|
||||
reporter: Option<Box<dyn Reporter>>,
|
||||
|
|
@ -212,6 +220,10 @@ pub struct RustyPipeBuilder {
|
|||
default_opts: RustyPipeOpts,
|
||||
}
|
||||
|
||||
/// RustyPipe query object
|
||||
///
|
||||
/// Contains a reference to the RustyPipe client as well as query-specific
|
||||
/// options (e.g. language preference).
|
||||
#[derive(Clone)]
|
||||
pub struct RustyPipeQuery {
|
||||
client: RustyPipe,
|
||||
|
|
@ -257,7 +269,7 @@ enum CacheEntry<T> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ClientData {
|
||||
struct ClientData {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ struct QBrowseParams<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a YouTube Music artist page
|
||||
///
|
||||
/// Set `all_albums` to [`true`] if you want to fetch the albums behind the *More* buttons, too.
|
||||
pub async fn music_artist<S: AsRef<str>>(
|
||||
&self,
|
||||
artist_id: S,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ struct FormData {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the YouTube Music charts for a given country
|
||||
pub async fn music_charts(&self, country: Option<Country>) -> Result<MusicCharts, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QCharts {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{ArtistId, Lyrics, MusicRelated, Paginator, TrackDetails, TrackItem},
|
||||
model::{paginator::Paginator, ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
|
@ -37,6 +37,7 @@ struct QRadio<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the metadata of a YouTube music track
|
||||
pub async fn music_details<S: AsRef<str>>(&self, video_id: S) -> Result<TrackDetails, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
@ -58,6 +59,9 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the lyrics of a YouTube music track
|
||||
///
|
||||
/// The `lyrics_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||
pub async fn music_lyrics<S: AsRef<str>>(&self, lyrics_id: S) -> Result<Lyrics, Error> {
|
||||
let lyrics_id = lyrics_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
@ -76,6 +80,9 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get related items (tracks, playlists, artists) to a YouTube Music track
|
||||
///
|
||||
/// The `related_id` has to be obtained using [`RustyPipeQuery::music_details`].
|
||||
pub async fn music_related<S: AsRef<str>>(&self, related_id: S) -> Result<MusicRelated, Error> {
|
||||
let related_id = related_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
@ -94,6 +101,9 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get a YouTube Music radio (a dynamically generated playlist)
|
||||
///
|
||||
/// The `radio_id` can be obtained using [`RustyPipeQuery::music_artist`] to get an artist's radio.
|
||||
pub async fn music_radio<S: AsRef<str>>(
|
||||
&self,
|
||||
radio_id: S,
|
||||
|
|
@ -122,6 +132,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a track
|
||||
pub async fn music_radio_track<S: AsRef<str>>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
|
@ -130,6 +141,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get a YouTube Music radio (a dynamically generated playlist) for a playlist
|
||||
pub async fn music_radio_playlist<S: AsRef<str>>(
|
||||
&self,
|
||||
playlist_id: S,
|
||||
|
|
@ -263,7 +275,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
|||
tracks,
|
||||
ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::MusicNext,
|
||||
crate::model::paginator::ContinuationEndpoint::MusicNext,
|
||||
),
|
||||
warnings: content.contents.warnings,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ struct QGenre<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a list of moods and genres from YouTube Music
|
||||
pub async fn music_genres(&self) -> Result<Vec<MusicGenreItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
|
@ -39,6 +40,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the playlists from a YouTube Music genre
|
||||
pub async fn music_genre<S: AsRef<str>>(&self, genre_id: S) -> Result<MusicGenre, Error> {
|
||||
let genre_id = genre_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ use std::borrow::Cow;
|
|||
use crate::{
|
||||
client::response::music_item::MusicListMapper,
|
||||
error::{Error, ExtractionError},
|
||||
model::{AlbumItem, FromYtItem, TrackItem},
|
||||
model::{traits::FromYtItem, AlbumItem, TrackItem},
|
||||
};
|
||||
|
||||
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the new albums that were released on YouTube Music
|
||||
pub async fn music_new_albums(&self) -> Result<Vec<AlbumItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
|
@ -26,6 +27,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the new music videos that were released on YouTube Music
|
||||
pub async fn music_new_videos(&self) -> Result<Vec<TrackItem>, Error> {
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{AlbumId, ChannelId, MusicAlbum, MusicPlaylist, Paginator, TrackItem},
|
||||
model::{paginator::Paginator, AlbumId, ChannelId, MusicAlbum, MusicPlaylist, TrackItem},
|
||||
serializer::MapResult,
|
||||
util::{self, TryRemove},
|
||||
};
|
||||
|
|
@ -16,6 +16,7 @@ use super::{
|
|||
};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a playlist from YouTube Music
|
||||
pub async fn music_playlist<S: AsRef<str>>(
|
||||
&self,
|
||||
playlist_id: S,
|
||||
|
|
@ -37,6 +38,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get an album from YouTube Music
|
||||
pub async fn music_album<S: AsRef<str>>(&self, album_id: S) -> Result<MusicAlbum, Error> {
|
||||
let album_id = album_id.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
@ -221,14 +223,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
|||
map_res.c,
|
||||
ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::MusicBrowse,
|
||||
crate::model::paginator::ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
related_playlists: Paginator::new_ext(
|
||||
None,
|
||||
Vec::new(),
|
||||
related_ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::MusicBrowse,
|
||||
crate::model::paginator::ContinuationEndpoint::MusicBrowse,
|
||||
),
|
||||
},
|
||||
warnings: map_res.warnings,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use crate::{
|
|||
client::response::music_item::MusicListMapper,
|
||||
error::{Error, ExtractionError},
|
||||
model::{
|
||||
AlbumItem, ArtistItem, FromYtItem, MusicPlaylistItem, MusicSearchFiltered,
|
||||
MusicSearchResult, Paginator, TrackItem,
|
||||
paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistItem, MusicPlaylistItem,
|
||||
MusicSearchFiltered, MusicSearchResult, TrackItem,
|
||||
},
|
||||
serializer::MapResult,
|
||||
util::TryRemove,
|
||||
|
|
@ -50,6 +50,7 @@ enum Params {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Search YouTube Music. Returns items from any type.
|
||||
pub async fn music_search<S: AsRef<str>>(&self, query: S) -> Result<MusicSearchResult, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
|
||||
|
|
@ -69,6 +70,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music tracks
|
||||
pub async fn music_search_tracks<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -76,6 +78,7 @@ impl RustyPipeQuery {
|
|||
self._music_search_tracks(query, Params::Tracks).await
|
||||
}
|
||||
|
||||
/// Search YouTube Music videos
|
||||
pub async fn music_search_videos<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -106,6 +109,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music albums
|
||||
pub async fn music_search_albums<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -128,6 +132,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music artists
|
||||
pub async fn music_search_artists(
|
||||
&self,
|
||||
query: &str,
|
||||
|
|
@ -149,6 +154,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube Music playlists
|
||||
pub async fn music_search_playlists<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -156,6 +162,8 @@ impl RustyPipeQuery {
|
|||
self._music_search_playlists(query, Params::Playlists).await
|
||||
}
|
||||
|
||||
/// Search YouTube Music playlists that were created by users
|
||||
/// (`community=true`) or by YouTube Music (`community=false`)
|
||||
pub async fn music_search_playlists_filter<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -194,6 +202,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get YouTube Music search suggestions
|
||||
pub async fn music_search_suggestion<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -316,7 +325,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
|
|||
map_res.c,
|
||||
ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::MusicSearch,
|
||||
crate::model::paginator::ContinuationEndpoint::MusicSearch,
|
||||
),
|
||||
corrected_query,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::error::{Error, ExtractionError};
|
||||
use crate::model::{Comment, FromYtItem, MusicItem, Paginator, PlaylistVideo, YouTubeItem};
|
||||
use crate::param::ContinuationEndpoint;
|
||||
use crate::model::{
|
||||
paginator::{ContinuationEndpoint, Paginator},
|
||||
traits::FromYtItem,
|
||||
Comment, MusicItem, PlaylistVideo, YouTubeItem,
|
||||
};
|
||||
use crate::serializer::MapResult;
|
||||
use crate::util::TryRemove;
|
||||
|
||||
|
|
@ -10,6 +13,7 @@ use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanel
|
|||
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get more YouTube items from the given continuation token and endpoint
|
||||
pub async fn continuation<T: FromYtItem, S: AsRef<str>>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
|
|
@ -174,6 +178,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
|||
}
|
||||
|
||||
impl<T: FromYtItem> Paginator<T> {
|
||||
/// 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> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
|
|
@ -186,6 +191,9 @@ impl<T: FromYtItem> Paginator<T> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Extend the items of the paginator by the next page
|
||||
///
|
||||
/// Returns false if the paginator is exhausted.
|
||||
pub async fn extend<Q: AsRef<RustyPipeQuery>>(&mut self, query: Q) -> Result<bool, Error> {
|
||||
match self.next(query).await {
|
||||
Ok(Some(paginator)) => {
|
||||
|
|
@ -199,6 +207,8 @@ impl<T: FromYtItem> Paginator<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extend the items of the paginator by the given amount of pages
|
||||
/// or until the paginator is exhausted.
|
||||
pub async fn extend_pages<Q: AsRef<RustyPipeQuery>>(
|
||||
&mut self,
|
||||
query: Q,
|
||||
|
|
@ -215,6 +225,8 @@ impl<T: FromYtItem> Paginator<T> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Extend the items of the paginator until the given amount of items
|
||||
/// is reached or the paginator is exhausted.
|
||||
pub async fn extend_limit<Q: AsRef<RustyPipeQuery>>(
|
||||
&mut self,
|
||||
query: Q,
|
||||
|
|
@ -233,6 +245,7 @@ impl<T: FromYtItem> Paginator<T> {
|
|||
}
|
||||
|
||||
impl Paginator<Comment> {
|
||||
/// 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> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(
|
||||
|
|
@ -247,6 +260,7 @@ impl Paginator<Comment> {
|
|||
}
|
||||
|
||||
impl Paginator<PlaylistVideo> {
|
||||
/// 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> {
|
||||
Ok(match &self.ctoken {
|
||||
Some(ctoken) => Some(query.as_ref().playlist_continuation(ctoken).await?),
|
||||
|
|
@ -258,6 +272,9 @@ impl Paginator<PlaylistVideo> {
|
|||
macro_rules! paginator {
|
||||
($entity_type:ty) => {
|
||||
impl Paginator<$entity_type> {
|
||||
/// Extend the items of the paginator by the next page
|
||||
///
|
||||
/// Returns false if the paginator is exhausted.
|
||||
pub async fn extend<Q: AsRef<RustyPipeQuery>>(
|
||||
&mut self,
|
||||
query: Q,
|
||||
|
|
@ -274,6 +291,8 @@ macro_rules! paginator {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extend the items of the paginator by the given amount of pages
|
||||
/// or until the paginator is exhausted.
|
||||
pub async fn extend_pages<Q: AsRef<RustyPipeQuery>>(
|
||||
&mut self,
|
||||
query: Q,
|
||||
|
|
@ -290,6 +309,8 @@ macro_rules! paginator {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Extend the items of the paginator until the given amount of items
|
||||
/// is reached or the paginator is exhausted.
|
||||
pub async fn extend_limit<Q: AsRef<RustyPipeQuery>>(
|
||||
&mut self,
|
||||
query: Q,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
deobfuscate::Deobfuscator,
|
||||
error::{DeobfError, Error, ExtractionError},
|
||||
model::{
|
||||
AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, QualityOrd, Subtitle,
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle,
|
||||
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
},
|
||||
param::Language,
|
||||
|
|
@ -58,6 +58,7 @@ struct QContentPlaybackContext {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||
pub async fn player<S: AsRef<str>>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||
let video_id = video_id.as_ref();
|
||||
let q1 = self.clone();
|
||||
|
|
@ -86,6 +87,7 @@ impl RustyPipeQuery {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||
pub async fn player_from_client<S: AsRef<str>>(
|
||||
&self,
|
||||
video_id: S,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use time::OffsetDateTime;
|
|||
use crate::{
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{Error, ExtractionError},
|
||||
model::{ChannelId, Paginator, Playlist, PlaylistVideo},
|
||||
model::{paginator::Paginator, ChannelId, Playlist, PlaylistVideo},
|
||||
param::Language,
|
||||
timeago,
|
||||
util::{self, TryRemove},
|
||||
|
|
@ -14,6 +14,7 @@ use crate::{
|
|||
use super::{response, ClientType, MapResponse, MapResult, QBrowse, QContinuation, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get a YouTube playlist
|
||||
pub async fn playlist<S: AsRef<str>>(&self, playlist_id: S) -> Result<Playlist, Error> {
|
||||
let playlist_id = playlist_id.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
|
|
@ -32,6 +33,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get more playlist items using the given continuation token
|
||||
pub async fn playlist_continuation<S: AsRef<str>>(
|
||||
&self,
|
||||
ctoken: S,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkip
|
|||
|
||||
use crate::{
|
||||
model::{
|
||||
self, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId, FromYtItem,
|
||||
MusicEntityType, MusicItem, MusicPlaylistItem, TrackItem,
|
||||
self, traits::FromYtItem, AlbumId, AlbumItem, AlbumType, ArtistId, ArtistItem, ChannelId,
|
||||
MusicItem, MusicItemType, MusicPlaylistItem, TrackItem,
|
||||
},
|
||||
param::Language,
|
||||
serializer::{
|
||||
|
|
@ -465,7 +465,7 @@ impl MusicListMapper {
|
|||
}
|
||||
}
|
||||
|
||||
fn map_item(&mut self, item: MusicResponseItem) -> Result<Option<MusicEntityType>, String> {
|
||||
fn map_item(&mut self, item: MusicResponseItem) -> Result<Option<MusicItemType>, String> {
|
||||
match item {
|
||||
// List item
|
||||
MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
|
||||
|
|
@ -636,7 +636,7 @@ impl MusicListMapper {
|
|||
is_video,
|
||||
track_nr,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Track))
|
||||
Ok(Some(MusicItemType::Track))
|
||||
}
|
||||
// Artist / Album / Playlist
|
||||
Some((page_type, id)) => {
|
||||
|
|
@ -666,7 +666,7 @@ impl MusicListMapper {
|
|||
avatar: item.thumbnail.into(),
|
||||
subscriber_count,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Artist))
|
||||
Ok(Some(MusicItemType::Artist))
|
||||
}
|
||||
MusicPageType::Album => {
|
||||
let album_type = subtitle_p1
|
||||
|
|
@ -690,7 +690,7 @@ impl MusicListMapper {
|
|||
year,
|
||||
by_va,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Album))
|
||||
Ok(Some(MusicItemType::Album))
|
||||
}
|
||||
MusicPageType::Playlist => {
|
||||
// Part 1 may be the "Playlist" label
|
||||
|
|
@ -717,7 +717,7 @@ impl MusicListMapper {
|
|||
track_count,
|
||||
from_ytm,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Playlist))
|
||||
Ok(Some(MusicItemType::Playlist))
|
||||
}
|
||||
MusicPageType::None => {
|
||||
// There may be broken YT channels from the artist search. They can be skipped.
|
||||
|
|
@ -762,7 +762,7 @@ impl MusicListMapper {
|
|||
is_video,
|
||||
track_nr: None,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Track))
|
||||
Ok(Some(MusicItemType::Track))
|
||||
}
|
||||
MusicPageType::Artist => {
|
||||
let subscriber_count = subtitle_p1
|
||||
|
|
@ -774,7 +774,7 @@ impl MusicListMapper {
|
|||
avatar: item.thumbnail_renderer.into(),
|
||||
subscriber_count,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Artist))
|
||||
Ok(Some(MusicItemType::Artist))
|
||||
}
|
||||
MusicPageType::Album => {
|
||||
let mut year = None;
|
||||
|
|
@ -818,7 +818,7 @@ impl MusicListMapper {
|
|||
year,
|
||||
by_va,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Album))
|
||||
Ok(Some(MusicItemType::Album))
|
||||
}
|
||||
MusicPageType::Playlist => {
|
||||
// When the playlist subtitle has only 1 part, it is a playlist from YT Music
|
||||
|
|
@ -841,7 +841,7 @@ impl MusicListMapper {
|
|||
track_count,
|
||||
from_ytm,
|
||||
}));
|
||||
Ok(Some(MusicEntityType::Playlist))
|
||||
Ok(Some(MusicItemType::Playlist))
|
||||
}
|
||||
MusicPageType::None => Ok(None),
|
||||
},
|
||||
|
|
@ -854,7 +854,7 @@ impl MusicListMapper {
|
|||
pub fn map_response(
|
||||
&mut self,
|
||||
mut res: MapResult<Vec<MusicResponseItem>>,
|
||||
) -> Option<MusicEntityType> {
|
||||
) -> Option<MusicItemType> {
|
||||
let mut etype = None;
|
||||
self.warnings.append(&mut res.warnings);
|
||||
res.c
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use serde::{de::IgnoredAny, Serialize};
|
|||
use crate::{
|
||||
deobfuscate::Deobfuscator,
|
||||
error::{Error, ExtractionError},
|
||||
model::{Paginator, SearchResult, YouTubeItem},
|
||||
model::{paginator::Paginator, SearchResult, YouTubeItem},
|
||||
param::{search_filter::SearchFilter, Language},
|
||||
};
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ struct QSearch<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Search YouTube
|
||||
pub async fn search<S: AsRef<str>>(&self, query: S) -> Result<SearchResult, Error> {
|
||||
let query = query.as_ref();
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
|
|
@ -40,6 +41,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Search YouTube using the given [`SearchFilter`]
|
||||
pub async fn search_filter<S: AsRef<str>>(
|
||||
&self,
|
||||
query: S,
|
||||
|
|
@ -63,6 +65,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get YouTube search suggestions
|
||||
pub async fn search_suggestion<S: AsRef<str>>(&self, query: S) -> Result<Vec<String>, Error> {
|
||||
let url = url::Url::parse_with_params("https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&gs_rn=64&gs_ri=youtube&ds=yt&cp=1&gs_id=4&xhr=t&xssi=t",
|
||||
&[("hl", self.opts.lang.to_string()), ("gl", self.opts.country.to_string()), ("q", query.as_ref().to_owned())]
|
||||
|
|
@ -114,7 +117,7 @@ impl MapResponse<SearchResult> for response::Search {
|
|||
mapper.items,
|
||||
mapper.ctoken,
|
||||
None,
|
||||
crate::param::ContinuationEndpoint::Search,
|
||||
crate::model::paginator::ContinuationEndpoint::Search,
|
||||
),
|
||||
corrected_query: mapper.corrected_query,
|
||||
visitor_data: self.response_context.visitor_data,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{Paginator, VideoItem},
|
||||
model::{paginator::Paginator, VideoItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
util::TryRemove,
|
||||
|
|
@ -11,6 +11,7 @@ use crate::{
|
|||
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the videos from the YouTube startpage
|
||||
pub async fn startpage(&self) -> Result<Paginator<VideoItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
|
@ -28,6 +29,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the videos from the YouTube trending page
|
||||
pub async fn trending(&self) -> Result<Vec<VideoItem>, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QBrowse {
|
||||
|
|
@ -110,7 +112,7 @@ fn map_startpage_videos(
|
|||
mapper.items,
|
||||
mapper.ctoken,
|
||||
visitor_data,
|
||||
crate::param::ContinuationEndpoint::Browse,
|
||||
crate::model::paginator::ContinuationEndpoint::Browse,
|
||||
),
|
||||
warnings: mapper.warnings,
|
||||
}
|
||||
|
|
@ -124,7 +126,7 @@ mod tests {
|
|||
|
||||
use crate::{
|
||||
client::{response, MapResponse},
|
||||
model::{Paginator, VideoItem},
|
||||
model::{paginator::Paginator, VideoItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,39 @@ struct QResolveUrl<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Resolve the given YouTube URL and return its associated URL target.
|
||||
///
|
||||
/// Note that the hostname of the URL is not checked, so this function also accepts URLs
|
||||
/// from alternative YouTube frontends like Piped or Invidious.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use rustypipe::client::RustyPipe;
|
||||
/// # use rustypipe::model::UrlTarget;
|
||||
/// # let rp = RustyPipe::new();
|
||||
/// # tokio_test::block_on(async {
|
||||
/// // Channel
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_url("https://www.youtube.com/LinusTechTips", true).await.unwrap(),
|
||||
/// UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()}
|
||||
/// );
|
||||
/// // Video
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_url("https://youtu.be/dQw4w9WgXcQ", true).await.unwrap(),
|
||||
/// UrlTarget::Video {id: "dQw4w9WgXcQ".to_owned(), start_time: 0}
|
||||
/// );
|
||||
/// // Album
|
||||
/// // You can choose whether album URLs should be resolved to their album id or returned as playlists
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_url("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", true).await.unwrap(),
|
||||
/// UrlTarget::Album {id: "MPREb_GyH43gCvdM5".to_owned()}
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_url("https://music.youtube.com/playlist?list=OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE", false).await.unwrap(),
|
||||
/// UrlTarget::Playlist {id: "OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE".to_owned()}
|
||||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn resolve_url<S: AsRef<str>>(
|
||||
self,
|
||||
url: S,
|
||||
|
|
@ -161,6 +194,29 @@ impl RustyPipeQuery {
|
|||
Ok(target)
|
||||
}
|
||||
|
||||
/// Resolve an input string and return a YouTube URL target
|
||||
///
|
||||
/// Accepted input strings include YouTube URLs (see [`RustyPipeQuery::resolve_url`]),
|
||||
/// Video/Channel/Playlist/Album IDs and channel handles / vanity IDs.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use rustypipe::client::RustyPipe;
|
||||
/// # use rustypipe::model::UrlTarget;
|
||||
/// # let rp = RustyPipe::new();
|
||||
/// # tokio_test::block_on(async {
|
||||
/// // Channel
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_string("LinusTechTips", true).await.unwrap(),
|
||||
/// UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()}
|
||||
/// );
|
||||
/// //
|
||||
/// assert_eq!(
|
||||
/// rp.query().resolve_string("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", true).await.unwrap(),
|
||||
/// UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()}
|
||||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn resolve_string(
|
||||
self,
|
||||
string: &str,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::{
|
||||
error::{Error, ExtractionError},
|
||||
model::{ChannelTag, Chapter, Comment, Paginator, VideoDetails, VideoItem},
|
||||
model::{paginator::Paginator, ChannelTag, Chapter, Comment, VideoDetails, VideoItem},
|
||||
param::Language,
|
||||
serializer::MapResult,
|
||||
timeago,
|
||||
|
|
@ -28,6 +28,7 @@ struct QVideo<'a> {
|
|||
}
|
||||
|
||||
impl RustyPipeQuery {
|
||||
/// Get the metadata for a video
|
||||
pub async fn video_details(&self, video_id: &str) -> Result<VideoDetails, Error> {
|
||||
let context = self.get_context(ClientType::Desktop, true, None).await;
|
||||
let request_body = QVideo {
|
||||
|
|
@ -47,6 +48,7 @@ impl RustyPipeQuery {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Get the comments for a video using the continuation token obtained from `rusty_pipe_query.video_details()`
|
||||
pub async fn video_comments(
|
||||
&self,
|
||||
ctoken: &str,
|
||||
|
|
@ -339,14 +341,14 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
Vec::new(),
|
||||
comment_ctoken,
|
||||
visitor_data.clone(),
|
||||
crate::param::ContinuationEndpoint::Next,
|
||||
crate::model::paginator::ContinuationEndpoint::Next,
|
||||
),
|
||||
latest_comments: Paginator::new_ext(
|
||||
comment_count,
|
||||
Vec::new(),
|
||||
latest_comments_ctoken,
|
||||
visitor_data.clone(),
|
||||
crate::param::ContinuationEndpoint::Next,
|
||||
crate::model::paginator::ContinuationEndpoint::Next,
|
||||
),
|
||||
visitor_data,
|
||||
},
|
||||
|
|
@ -437,7 +439,7 @@ fn map_recommendations(
|
|||
mapper.items,
|
||||
mapper.ctoken,
|
||||
visitor_data,
|
||||
crate::param::ContinuationEndpoint::Next,
|
||||
crate::model::paginator::ContinuationEndpoint::Next,
|
||||
),
|
||||
warnings: mapper.warnings,
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue