diff --git a/Cargo.toml b/Cargo.toml index 8532046..25116ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ authors = ["ThetaDev "] license = "GPL-3.0" description = "Client for the public YouTube / YouTube Music API (Innertube), inspired by NewPipe" keywords = ["youtube", "video", "music"] +categories = ["api-bindings", "multimedia"] include = ["/src", "README.md", "LICENSE", "!snapshots"] diff --git a/README.md b/README.md index a768901..a237ebc 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ [![CI status](https://ci.thetadev.de/api/badges/ThetaDev/rustypipe/status.svg)](https://ci.thetadev.de/ThetaDev/rustypipe) -Client for the public YouTube / YouTube Music API (Innertube), -inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). +Client for the public YouTube / YouTube Music API (Innertube), inspired by +[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). ## Features ### YouTube - **Player** (video/audio streams, subtitles) -- **Playlist** - **VideoDetails** (metadata, comments, recommended videos) +- **Playlist** - **Channel** (videos, shorts, livestreams, playlists, info, search) - **ChannelRSS** - **Search** (with filters) @@ -31,3 +31,126 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). - **Moods/Genres** - **Charts** - **New** (albums, music videos) + +## Getting started + +### Cargo.toml + +```toml +[dependencies] +rustypipe = "0.1.0" +tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] } +``` + +### Watch a video + +```rust ignore +use std::process::Command; + +use rustypipe::{client::RustyPipe, param::StreamFilter}; + +#[tokio::main] +async fn main() { + // Create a client + let rp = RustyPipe::new(); + // Fetch the player + let player = rp.query().player("pPvd8UxmSbQ").await.unwrap(); + // Select the best streams + let (video, audio) = player.select_video_audio_stream(&StreamFilter::default()); + + // Open mpv player + let mut args = vec![video.expect("no video stream").url.to_owned()]; + if let Some(audio) = audio { + args.push(format!("--audio-file={}", audio.url)); + } + Command::new("mpv").args(args).output().unwrap(); +} +``` + +### Get a playlist + +```rust ignore +use rustypipe::client::RustyPipe + +#[tokio::main] +async fn main() { + // Create a client + let rp = RustyPipe::new(); + // Get the playlist + let playlist = rp + .query() + .playlist("PL2_OBreMn7FrsiSW0VDZjdq0xqUKkZYHT") + .await + .unwrap(); + // Get all items (maximum: 1000) + playlist.videos.extend_limit(rp.query(), 1000).await.unwrap(); + + println!("Name: {}", playlist.name); + println!("Author: {}", playlist.channel.unwrap().name); + println!("Last update: {}", playlist.last_update.unwrap()); + + playlist + .videos + .items + .iter() + .for_each(|v| println!("[{}] {} ({}s)", v.id, v.name, v.length)); +} +``` + +**Output:** + +```txt +Name: Homelab +Author: Jeff Geerling +Last update: 2023-05-04 +[cVWF3u-y-Zg] I put a computer in my computer (720s) +[ecdm3oA-QdQ] 6-in-1: Build a 6-node Ceph cluster on this Mini ITX Motherboard (783s) +[xvE4HNJZeIg] Scrapyard Server: Fastest all-SSD NAS! (733s) +[RvnG-ywF6_s] Nanosecond clock sync with a Raspberry Pi (836s) +[R2S2RMNv7OU] I made the Petabyte Raspberry Pi even faster! (572s) +[FG--PtrDmw4] Hiding Macs in my Rack! (515s) +... +``` + +### Get a channel + +```rust ignore +use rustypipe::client::RustyPipe + +#[tokio::main] +async fn main() { + // Create a client + let rp = RustyPipe::new(); + // Get the channel + let channel = rp + .query() + .channel_videos("UCl2mFZoRqjw_ELax4Yisf6w") + .await + .unwrap(); + + println!("Name: {}", channel.name); + println!("Description: {}", channel.description); + println!("Subscribers: {}", channel.subscriber_count.unwrap()); + + channel + .content + .items + .iter() + .for_each(|v| println!("[{}] {} ({}s)", v.id, v.name, v.length.unwrap())); +} +``` + +**Output:** + +```txt +Name: Louis Rossmann +Description: I discuss random things of interest to me. (...) +Subscribers: 1780000 +[qBHgJx_rb8E] Introducing Rossmann senior, a genuine fossil 😃 (122s) +[TmV8eAtXc3s] Am I wrong about CompTIA? (592s) +[CjOJJc1qzdY] How FUTO projects loosen Google's grip on your life! (588s) +[0A10JtkkL9A] a private moment between a man and his kitten (522s) +[zbHq5_1Cd5U] Is Texas mandating auto repair shops use OEM parts? SB1083 analysis & breakdown; tldr, no. (645s) +[6Fv8bd9ICb4] Who owns this? (199s) +... +``` diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 679bfd5..5dab51e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,6 +2,11 @@ name = "rustypipe-cli" version = "0.1.0" edition = "2021" +authors = ["ThetaDev "] +license = "GPL-3.0" +description = "CLI for RustyPipe - download videos and extract data from YouTube / YouTube Music" +keywords = ["youtube", "video", "music"] +categories = ["multimedia"] [features] default = ["rustls-tls-native-roots"] diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 227ed65..233c2c5 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -22,7 +22,7 @@ pub enum ABTest { TrendsPageHeaderRenderer = 5, } -const TESTS_TO_RUN: [ABTest; 1] = [ABTest::TrendsVideoTab]; +const TESTS_TO_RUN: [ABTest; 2] = [ABTest::TrendsVideoTab, ABTest::TrendsPageHeaderRenderer]; #[derive(Debug, Serialize, Deserialize)] pub struct ABTestRes { diff --git a/codegen/src/collect_album_types.rs b/codegen/src/collect_album_types.rs index acdab8e..55d4a14 100644 --- a/codegen/src/collect_album_types.rs +++ b/codegen/src/collect_album_types.rs @@ -5,7 +5,7 @@ use path_macro::path; use rustypipe::{ client::{ClientType, RustyPipe, RustyPipeQuery}, model::AlbumType, - param::{locale::LANGUAGES, Language}, + param::{Language, LANGUAGES}, }; use serde::Deserialize; diff --git a/codegen/src/collect_large_numbers.rs b/codegen/src/collect_large_numbers.rs index 6d3499c..db96066 100644 --- a/codegen/src/collect_large_numbers.rs +++ b/codegen/src/collect_large_numbers.rs @@ -11,7 +11,7 @@ use once_cell::sync::Lazy; use path_macro::path; use regex::Regex; use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery}; -use rustypipe::param::{locale::LANGUAGES, Language}; +use rustypipe::param::{Language, LANGUAGES}; use serde::Deserialize; use crate::model::{Channel, ContinuationResponse}; diff --git a/codegen/src/collect_playlist_dates.rs b/codegen/src/collect_playlist_dates.rs index 01d68cc..82b15c3 100644 --- a/codegen/src/collect_playlist_dates.rs +++ b/codegen/src/collect_playlist_dates.rs @@ -9,7 +9,7 @@ use futures::{stream, StreamExt}; use path_macro::path; use rustypipe::{ client::RustyPipe, - param::{locale::LANGUAGES, Language}, + param::{Language, LANGUAGES}, }; use serde::{Deserialize, Serialize}; diff --git a/codegen/src/collect_video_durations.rs b/codegen/src/collect_video_durations.rs index 9a2a16b..3226797 100644 --- a/codegen/src/collect_video_durations.rs +++ b/codegen/src/collect_video_durations.rs @@ -9,7 +9,7 @@ use futures::{stream, StreamExt}; use path_macro::path; use rustypipe::{ client::{ClientType, RustyPipe, RustyPipeQuery}, - param::{locale::LANGUAGES, Language}, + param::{Language, LANGUAGES}, }; use crate::{ diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index 3047ce0..5e32056 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -2,6 +2,11 @@ name = "rustypipe-downloader" version = "0.1.0" edition = "2021" +authors = ["ThetaDev "] +license = "GPL-3.0" +description = "Downloader extension for RustyPipe" +keywords = ["youtube", "video", "music"] +categories = ["multimedia"] [features] default = ["default-tls"] diff --git a/src/cache.rs b/src/cache.rs index fb69a5a..970ca1a 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,4 +1,19 @@ -//! Persistent cache storage +//! # Persistent cache storage +//! +//! RustyPipe caches some information fetched from YouTube: specifically +//! the client versions and the JavaScript code used to deobfuscate the stream URLs. +//! +//! Without a persistent cache storage, this information would have to be re-fetched +//! with every new instantiation of the client. This would make operation a lot slower, +//! especially with CLI applications. For this reason, persisting the cache between +//! program executions is recommended. +//! +//! Since there are many diferent ways to store this data (Text file, SQL, Redis, etc), +//! RustyPipe allows you to plug in your own cache storage by implementing the +//! [`CacheStorage`] trait. +//! +//! RustyPipe already comes with the [`FileStorage`] implementation which stores +//! the cache as a JSON file. use std::{ fs, @@ -9,14 +24,16 @@ use log::error; pub(crate) const DEFAULT_CACHE_FILE: &str = "rustypipe_cache.json"; +/// Cache storage trait +/// /// RustyPipe has to cache some information fetched from YouTube: specifically /// the client versions and the JavaScript code used to deobfuscate the stream URLs. /// /// This trait is used to abstract the cache storage behavior so you can store /// cache data in your preferred way (File, SQL, Redis, etc). /// -/// The cache is read when building the [`crate::client::RustyPipe`] client and updated -/// whenever additional data is fetched. +/// The cache is read when building the [`RustyPipe`](crate::client::RustyPipe) +/// client and updated whenever additional data is fetched. pub trait CacheStorage: Sync + Send { /// Write the given string to the cache fn write(&self, data: &str); diff --git a/src/client/channel.rs b/src/client/channel.rs index 0044f3a..83872a3 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -98,7 +98,7 @@ impl RustyPipeQuery { .await } - /// Get the specified video tab from a YouTube channel + /// Get the videos of the given tab (Shorts, Livestreams) from a YouTube channel pub async fn channel_videos_tab>( &self, channel_id: S, @@ -108,7 +108,7 @@ impl RustyPipeQuery { .await } - /// Get a ordered list of videos from the specified tab of a YouTube channel + /// Get a ordered list of videos from the given tab (Shorts, Livestreams) of a YouTube channel /// /// This function does not return channel metadata. pub async fn channel_videos_tab_order>( diff --git a/src/client/channel_rss.rs b/src/client/channel_rss.rs index 77962a9..9b86e87 100644 --- a/src/client/channel_rss.rs +++ b/src/client/channel_rss.rs @@ -15,6 +15,8 @@ impl RustyPipeQuery { /// /// 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. + /// + /// The downside of using the RSS feed is that it does not provide video durations. pub async fn channel_rss>(&self, channel_id: S) -> Result { let channel_id = channel_id.as_ref(); let url = format!( diff --git a/src/client/mod.rs b/src/client/mod.rs index fa9b99f..b6f909c 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -214,9 +214,9 @@ static CLIENT_VERSION_REGEXES: Lazy<[Regex; 1]> = /// The RustyPipe client used to access YouTube's API /// -/// RustyPipe includes an `Arc` internally, so if you are using the client -/// at multiple locations, you can just clone it. Note that options (lang/country/report) -/// are not shared between clones. +/// RustyPipe uses an [`Arc`] internally, so if you are using the client +/// at multiple locations, you can just clone it. Note that query options +/// (lang/country/report/visitor data) are not shared between clones. #[derive(Clone)] pub struct RustyPipe { inner: Arc, @@ -268,10 +268,78 @@ impl DefaultOpt { } } -/// RustyPipe query object +/// # RustyPipe query /// -/// Contains a reference to the RustyPipe client as well as query-specific -/// options (e.g. language preference). +/// ## Queries +/// +/// ### YouTube +/// +/// - **Video** +/// - [`player`](RustyPipeQuery::player) +/// - [`video_details`](RustyPipeQuery::video_details) +/// - [`video_comments`](RustyPipeQuery::video_comments) +/// - **Channel** +/// - [`channel_videos`](RustyPipeQuery::channel_videos) +/// - [`channel_videos_order`](RustyPipeQuery::channel_videos_order) +/// - [`channel_videos_tab`](RustyPipeQuery::channel_videos_tab) +/// - [`channel_videos_tab_order`](RustyPipeQuery::channel_videos_tab_order) +/// - [`channel_playlists`](RustyPipeQuery::channel_playlists) +/// - [`channel_search`](RustyPipeQuery::channel_search) +/// - [`channel_info`](RustyPipeQuery::channel_info) +/// - [`channel_rss`](RustyPipeQuery::channel_rss) (🔒 Feature `rss`) +/// - **Playlist** [`playlist`](RustyPipeQuery::playlist) +/// - **Search** +/// - [`search`](RustyPipeQuery::search) +/// - [`search_filter`](RustyPipeQuery::search_filter) +/// - [`search_suggestion`](RustyPipeQuery::search_suggestion) +/// - **Trending** [`trending`](RustyPipeQuery::trending) +/// - **Resolver** (convert URLs and strings to YouTube IDs) +/// - [`resolve_url`](RustyPipeQuery::resolve_url) +/// - [`resolve_string`](RustyPipeQuery::resolve_string) +/// +/// ### YouTube Music +/// +/// - **Playlist** [`music_playlist`](RustyPipeQuery::music_playlist) +/// - **Album** [`music_album`](RustyPipeQuery::music_album) +/// - **Artist** [`music_artist`](RustyPipeQuery::music_artist) +/// - **Search** +/// - [`music_search`](RustyPipeQuery::music_search) +/// - [`music_search_tracks`](RustyPipeQuery::music_search_tracks) +/// - [`music_search_videos`](RustyPipeQuery::music_search_videos) +/// - [`music_search_albums`](RustyPipeQuery::music_search_albums) +/// - [`music_search_artists`](RustyPipeQuery::music_search_artists) +/// - [`music_search_playlists`](RustyPipeQuery::music_search_playlists) +/// - [`music_search_playlists_filter`](RustyPipeQuery::music_search_playlists_filter) +/// - [`music_search_suggestion`](RustyPipeQuery::music_search_suggestion) +/// - **Radio** +/// - [`music_radio`](RustyPipeQuery::music_radio) +/// - [`music_radio_playlist`](RustyPipeQuery::music_radio_playlist) +/// - [`music_radio_track`](RustyPipeQuery::music_radio_track) +/// - **Track details** +/// - [`music_details`](RustyPipeQuery::music_details) +/// - [`music_lyrics`](RustyPipeQuery::music_lyrics) +/// - [`music_related`](RustyPipeQuery::music_related) +/// - **Moods/Genres** +/// - [`music_genres`](RustyPipeQuery::music_genres) +/// - [`music_genre`](RustyPipeQuery::music_genre) +/// - **Charts** [`music_charts`](RustyPipeQuery::music_charts) +/// - **New** +/// - [`music_new_albums`](RustyPipeQuery::music_new_albums) +/// - [`music_new_videos`](RustyPipeQuery::music_new_videos) +/// +/// ## Options +/// +/// You can set the language, country and visitor data cookie for individual requests. +/// +/// ``` +/// # use rustypipe::client::RustyPipe; +/// let rp = RustyPipe::new(); +/// rp.query() +/// .country(rustypipe::param::Country::De) +/// .lang(rustypipe::param::Language::De) +/// .visitor_data("CgthZVRCd1dkbTlRWSj3v_miBg%3D%3D") +/// .player("ZeerrnuLi5E"); +/// ``` #[derive(Clone)] pub struct RustyPipeQuery { client: RustyPipe, @@ -361,9 +429,9 @@ impl Default for RustyPipeBuilder { } impl RustyPipeBuilder { - /// Constructs a new `RustyPipeBuilder`. + /// Return a new `RustyPipeBuilder`. /// - /// This is the same as `RustyPipe::builder()` + /// This is the same as [`RustyPipe::builder`] pub fn new() -> Self { RustyPipeBuilder { default_opts: RustyPipeOpts::default(), @@ -376,7 +444,7 @@ impl RustyPipeBuilder { } } - /// Returns a new, configured RustyPipe instance. + /// Return a new, configured RustyPipe instance. pub fn build(self) -> RustyPipe { let mut client_builder = ClientBuilder::new() .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) @@ -517,6 +585,7 @@ impl RustyPipeBuilder { } /// Set the language parameter used when accessing the YouTube API. + /// /// This will change multilanguage video titles, descriptions and textual dates /// /// **Default value**: `Language::En` (English) @@ -528,6 +597,7 @@ impl RustyPipeBuilder { } /// Set the country parameter used when accessing the YouTube API. + /// /// This will change trends and recommended content. /// /// **Default value**: `Country::Us` (USA) @@ -539,6 +609,7 @@ impl RustyPipeBuilder { } /// Generate a report on every operation. + /// /// This should only be used for debugging. /// /// **Info**: you can set this option for individual queries, too @@ -549,6 +620,7 @@ impl RustyPipeBuilder { /// Enable strict mode, causing operations to fail if there /// are warnings during deserialization (e.g. invalid items). + /// /// This should only be used for testing. /// /// **Info**: you can set this option for individual queries, too @@ -557,15 +629,32 @@ impl RustyPipeBuilder { self } - /// Set the default YouTube visitor data cookie + /// Set the YouTube visitor data cookie + /// + /// YouTube assigns a session cookie to each user which is used for personalized + /// recommendations. By default, RustyPipe does not send this cookie to preserve + /// user privacy. For requests that mandatate the cookie, a new one is requested + /// for every query. + /// + /// This option allows you to manually set the visitor data cookie of your client, + /// allowing you to get personalized recommendations or reproduce A/B tests. + /// + /// Note that YouTube has a rate limit on the number of requests from a single + /// visitor, so you should not use the same vistor data cookie for batch operations. + /// + /// **Info**: you can set this option for individual queries, too pub fn visitor_data>(mut self, visitor_data: S) -> Self { self.default_opts.visitor_data = Some(visitor_data.into()); self } - /// Set the default YouTube visitor data cookie to an optional value - pub fn visitor_data_opt(mut self, visitor_data: Option) -> Self { - self.default_opts.visitor_data = visitor_data; + /// Set the YouTube visitor data cookie to an optional value + /// + /// see also [`RustyPipeBuilder::visitor_data`] + /// + /// **Info**: you can set this option for individual queries, too + pub fn visitor_data_opt>(mut self, visitor_data: Option) -> Self { + self.default_opts.visitor_data = visitor_data.map(S::into); self } } @@ -579,19 +668,19 @@ impl Default for RustyPipe { impl RustyPipe { /// Create a new RustyPipe instance with default settings. /// - /// To create an instance with custom options, use `RustyPipeBuilder` instead. + /// To create an instance with custom options, use [`RustyPipeBuilder`] instead. pub fn new() -> Self { RustyPipeBuilder::new().build() } - /// Constructs a new `RustyPipeBuilder`. + /// Create a new [`RustyPipeBuilder`] /// - /// This is the same as `RustyPipeBuilder::new()` + /// This is the same as [`RustyPipeBuilder::new`] pub fn builder() -> RustyPipeBuilder { RustyPipeBuilder::new() } - /// Constructs a new `RustyPipeQuery`. + /// Create a new [`RustyPipeQuery`] to run an API request pub fn query(&self) -> RustyPipeQuery { RustyPipeQuery { client: self.clone(), @@ -826,8 +915,12 @@ impl RustyPipe { } } + /// Request a new visitor data cookie from YouTube + /// + /// Since the cookie is shared between YT and YTM and the YTM page loads faster, + /// we request that. async fn get_visitor_data(&self) -> Result { - log::debug!("getting YTM visitor data"); + log::debug!("getting YT visitor data"); let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?; resp.headers() @@ -849,6 +942,7 @@ impl RustyPipe { impl RustyPipeQuery { /// Set the language parameter used when accessing the YouTube API + /// /// This will change multilanguage video titles, descriptions and textual dates pub fn lang(mut self, lang: Language) -> Self { self.opts.lang = lang; @@ -856,6 +950,7 @@ impl RustyPipeQuery { } /// Set the country parameter used when accessing the YouTube API. + /// /// This will change trends and recommended content. pub fn country(mut self, country: Country) -> Self { self.opts.country = validate_country(country); @@ -863,6 +958,7 @@ impl RustyPipeQuery { } /// Generate a report on every operation. + /// /// This should only be used for debugging. pub fn report(mut self) -> Self { self.opts.report = true; @@ -871,6 +967,7 @@ impl RustyPipeQuery { /// Enable strict mode, causing operations to fail if there /// are warnings during deserialization (e.g. invalid items). + /// /// This should only be used for testing. pub fn strict(mut self) -> Self { self.opts.strict = true; @@ -878,14 +975,27 @@ impl RustyPipeQuery { } /// Set the YouTube visitor data cookie + /// + /// YouTube assigns a session cookie to each user which is used for personalized + /// recommendations. By default, RustyPipe does not send this cookie to preserve + /// user privacy. For requests that mandatate the cookie, a new one is requested + /// for every query. + /// + /// This option allows you to manually set the visitor data cookie of your query, + /// allowing you to get personalized recommendations or reproduce A/B tests. + /// + /// Note that YouTube has a rate limit on the number of requests from a single + /// visitor, so you should not use the same vistor data cookie for batch operations. pub fn visitor_data>(mut self, visitor_data: S) -> Self { self.opts.visitor_data = Some(visitor_data.into()); self } /// Set the YouTube visitor data cookie to an optional value - pub fn visitor_data_opt(mut self, visitor_data: Option) -> Self { - self.opts.visitor_data = visitor_data; + /// + /// see also [`RustyPipeQuery::visitor_data`] + pub fn visitor_data_opt>(mut self, visitor_data: Option) -> Self { + self.opts.visitor_data = visitor_data.map(S::into); self } diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index ad5f57a..4e8e766 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -1121,6 +1121,12 @@ impl MusicListMapper { } } + /// Sometimes the YT Music API returns responses containing unknown items. + /// + /// In this case, the response data is likely missing some fields, which leads to + /// parsing errors and wrong data being extracted. + /// + /// Therefore it is safest to discard such responses and retry the request. pub fn check_unknown(&self) -> Result<(), ExtractionError> { match self.has_unknown { true => Err(ExtractionError::InvalidData("unknown YTM items".into())), diff --git a/src/client/url_resolver.rs b/src/client/url_resolver.rs index 86cb18c..f078397 100644 --- a/src/client/url_resolver.rs +++ b/src/client/url_resolver.rs @@ -26,7 +26,7 @@ impl RustyPipeQuery { /// from alternative YouTube frontends like Piped or Invidious. /// /// The `resolve_albums` flag enables resolving YTM album URLs (e.g. - /// `OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE`) to their short album id (`MPREb_GyH43gCvdM5`). + /// `OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE`) to their short album ids (`MPREb_GyH43gCvdM5`). /// /// # Examples /// ``` @@ -217,7 +217,7 @@ impl RustyPipeQuery { /// rp.query().resolve_string("LinusTechTips", true).await.unwrap(), /// UrlTarget::Channel {id: "UCXuqSBlHAE6Xw-yeJA0Tunw".to_owned()} /// ); - /// // + /// // Playlist /// assert_eq!( /// rp.query().resolve_string("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI", true).await.unwrap(), /// UrlTarget::Playlist {id: "PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI".to_owned()} diff --git a/src/error.rs b/src/error.rs index 31d3208..10de65d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,7 +81,8 @@ pub enum ExtractionError { pub enum UnavailabilityReason { /// Video is age restricted. /// - /// Age restriction may be circumvented with the [`crate::client::ClientType::TvHtml5Embed`] client. + /// Age restriction may be circumvented with the + /// [`ClientType::TvHtml5Embed`](crate::client::ClientType::TvHtml5Embed) client. AgeRestricted, /// Video was deleted or censored Deleted, diff --git a/src/lib.rs b/src/lib.rs index 23398bf..d6db8d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,11 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] +//! ## Go to +//! +//! - Client ([`rustypipe::client::Rustypipe`](crate::client::RustyPipe)) +//! - Query ([`rustypipe::client::RustypipeQuery`](crate::client::RustyPipeQuery)) + mod deobfuscate; mod serializer; mod util; diff --git a/src/model/mod.rs b/src/model/mod.rs index 0250aaa..3047810 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -257,15 +257,15 @@ pub struct AudioStream { pub codec: AudioCodec, /// Number of audio channels pub channels: Option, - /// Audio loudness for ReplayGain correction + /// Audio loudness for volume normalization /// /// The track volume correction factor (0-1) can be calculated using this formula /// /// `10^(-loudness_db/20)` /// - /// Note that the value is the inverse of the usual track gain parameter, i.e. a - /// value of 6 means the volume should be reduced by 6dB and the ReplayGain track gain - /// parameter would be -6. + /// Note that the `loudness_db` value is the inverse of the usual ReplayGain track gain + /// parameter, i.e. a value of 6 means the volume should be reduced by 6dB and the + /// track gain parameter would be -6. /// /// More information about ReplayGain and how to apply this infomation to audio files /// can be found here: . diff --git a/src/param/mod.rs b/src/param/mod.rs index 804c7d8..a2aba29 100644 --- a/src/param/mod.rs +++ b/src/param/mod.rs @@ -1,11 +1,14 @@ -//! Query parameters +//! # Query parameters +//! +//! This module contains structs and enums used as input parameters +//! for the functions in RustyPipe. +mod locale; mod stream_filter; -pub mod locale; pub mod search_filter; -pub use locale::{Country, Language}; +pub use locale::{Country, Language, COUNTRIES, LANGUAGES}; pub use stream_filter::StreamFilter; /// Channel video tab diff --git a/src/validate.rs b/src/validate.rs index f82c2ad..009440a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -8,7 +8,7 @@ //! [string resolver](crate::client::RustyPipeQuery::resolve_string) is great for handling //! arbitrary input and returns a [`UrlTarget`](crate::model::UrlTarget) enum that tells you //! whether the given URL points to a video, channel, playlist, etc. -//! - The validation functions of this module are meant vor validating concrete data (video IDs, +//! - The validation functions of this module are meant vor validating specific data (video IDs, //! channel IDs, playlist IDs) and return [`true`] if the given input is valid use crate::util; @@ -138,7 +138,7 @@ pub fn genre_id>(genre_id: S) -> bool { GENRE_ID_REGEX.is_match(genre_id.as_ref()) } -/// Validate the given related ID +/// Validate the given related tracks ID /// /// YouTube related IDs are exactly 17 characters long, start with the characters `MPTRt_`, /// followed by 11 of these characters: `A-Za-z0-9_-`.