From a6bf9359b93353e2be8e0f87873e02154877d2a4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 13 May 2023 00:08:14 +0200 Subject: [PATCH 1/3] docs: improve documentation --- Cargo.toml | 1 + README.md | 129 ++++++++++++++++++++- cli/Cargo.toml | 5 + codegen/src/abtest.rs | 2 +- codegen/src/collect_album_types.rs | 2 +- codegen/src/collect_large_numbers.rs | 2 +- codegen/src/collect_playlist_dates.rs | 2 +- codegen/src/collect_video_durations.rs | 2 +- downloader/Cargo.toml | 5 + src/cache.rs | 23 +++- src/client/channel.rs | 4 +- src/client/channel_rss.rs | 2 + src/client/mod.rs | 150 +++++++++++++++++++++---- src/client/response/music_item.rs | 6 + src/client/url_resolver.rs | 4 +- src/error.rs | 3 +- src/lib.rs | 5 + src/model/mod.rs | 8 +- src/param/mod.rs | 9 +- src/validate.rs | 4 +- 20 files changed, 323 insertions(+), 45 deletions(-) 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_-`. From 81280200f7c9265cf0a48c021e6c4463f8588a7e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 13 May 2023 00:11:22 +0200 Subject: [PATCH 2/3] fix: add channel playlist ids to regex --- notes/channel_playlist.txt | 18 ++++++++++++++++++ src/util/mod.rs | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 notes/channel_playlist.txt diff --git a/notes/channel_playlist.txt b/notes/channel_playlist.txt new file mode 100644 index 0000000..4310954 --- /dev/null +++ b/notes/channel_playlist.txt @@ -0,0 +1,18 @@ +Source: https://github.com/TeamNewPipe/NewPipe/pull/9182#issuecomment-1508938841 + +Note: we recently discovered that YouTube system playlists exist for regular videos of channels, for livestreams, and shorts as chronological ones (the shorts one was already known) and popular ones. +They correspond basically to the results of the sort filters available on the channels streams tab on YouTube's interface + +So, basically shortcuts for the lazy/incurious? + +Same procedure as the one described in the 0.24.1 changelog, except that you need to change the prefix UU (all user uploads) to: + +UULF for regular videos only, +UULV for livestreams only, +UUSH for shorts only, +UULP for popular regular videos, +UUPS for popular shorts, +UUPV for popular livestreams +UUMF: members only regular videos +UUMV: members only livestreams +UUMS is probably for members-only shorts, we need to found a channel making shorts restricted to channel members diff --git a/src/util/mod.rs b/src/util/mod.rs index f77e600..a4cab18 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -26,7 +26,7 @@ pub static VIDEO_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[A-Za-z0-9_- pub static CHANNEL_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^UC[A-Za-z0-9_-]{22}$").unwrap()); pub static PLAYLIST_ID_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^(?:PL|RDCLAK|OLAK)[A-Za-z0-9_-]{16,50}$").unwrap()); + Lazy::new(|| Regex::new(r"^(?:PL|RDCLAK|OLAK|UU)[A-Za-z0-9_-]{16,50}$").unwrap()); pub static ALBUM_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^MPREb_[A-Za-z0-9_-]{11}$").unwrap()); pub static VANITY_PATH_REGEX: Lazy = Lazy::new(|| { From cbeb14f3fd235cd01ed118c4237e3581a24d7541 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 13 May 2023 02:40:26 +0200 Subject: [PATCH 3/3] fix: add pedantic lints --- cli/src/main.rs | 12 +-- codegen/src/abtest.rs | 4 +- codegen/src/collect_album_types.rs | 6 +- codegen/src/collect_large_numbers.rs | 69 ++++++++------- codegen/src/collect_playlist_dates.rs | 18 ++-- codegen/src/collect_video_durations.rs | 23 ++--- codegen/src/gen_dictionary.rs | 41 ++++++--- codegen/src/gen_locales.rs | 12 +-- codegen/src/main.rs | 4 +- downloader/src/lib.rs | 33 +++---- src/client/channel.rs | 14 ++- src/client/channel_rss.rs | 15 ++-- src/client/mod.rs | 75 ++++++++++------ src/client/music_artist.rs | 9 +- src/client/music_playlist.rs | 14 +-- src/client/music_search.rs | 11 +-- src/client/player.rs | 76 +++++++---------- src/client/playlist.rs | 5 +- src/client/response/channel_rss.rs | 8 +- src/client/response/mod.rs | 2 +- src/client/response/music_item.rs | 25 +++--- src/client/response/url_endpoint.rs | 4 +- src/client/response/video_item.rs | 15 ++-- src/client/url_resolver.rs | 9 +- src/client/video_details.rs | 14 +-- src/deobfuscate.rs | 2 +- src/error.rs | 9 +- src/lib.rs | 16 +++- src/model/mod.rs | 22 ++--- src/model/richtext.rs | 10 +-- src/param/mod.rs | 2 +- src/param/search_filter.rs | 15 +++- src/param/stream_filter.rs | 51 +++++++---- src/report.rs | 28 +++--- src/serializer/text.rs | 29 +++---- src/util/dictionary.rs | 8 +- src/util/mod.rs | 18 ++-- src/util/protobuf.rs | 9 +- src/util/timeago.rs | 21 ++--- src/validate.rs | 114 ++++++++++++++++--------- tests/youtube.rs | 95 +++++++++------------ 41 files changed, 520 insertions(+), 447 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index d205933..2a1ba78 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,5 @@ +#![warn(clippy::todo, clippy::dbg_macro)] + use std::{path::PathBuf, time::Duration}; use anyhow::{Context, Result}; @@ -281,9 +283,9 @@ fn print_data(data: &T, format: Format, pretty: bool) { match format { Format::Json => { if pretty { - serde_json::to_writer_pretty(stdout, data).unwrap() + serde_json::to_writer_pretty(stdout, data).unwrap(); } else { - serde_json::to_writer(stdout, data).unwrap() + serde_json::to_writer(stdout, data).unwrap(); } } Format::Yaml => serde_yaml::to_writer(stdout, data).unwrap(), @@ -360,7 +362,7 @@ async fn download_videos( &video.id, &video.name, output_dir, - output_fname.to_owned(), + output_fname.clone(), resolution, "ffmpeg", rp, @@ -632,9 +634,7 @@ async fn main() { } => match music { None => match channel { Some(channel) => { - if !rustypipe::validate::channel_id(&channel) { - panic!("invalid channel id") - } + rustypipe::validate::channel_id(&channel).unwrap(); let res = rp.query().channel_search(&channel, &query).await.unwrap(); print_data(&res, format, pretty); } diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 233c2c5..d48a05e 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -102,10 +102,10 @@ pub async fn run_test( let count = results.iter().filter(|(p, _)| *p).count(); let vd_present = results .iter() - .find_map(|(p, vd)| if *p { Some(vd.to_owned()) } else { None }); + .find_map(|(p, vd)| if *p { Some(vd.clone()) } else { None }); let vd_absent = results .iter() - .find_map(|(p, vd)| if !*p { Some(vd.to_owned()) } else { None }); + .find_map(|(p, vd)| if *p { None } else { Some(vd.clone()) }); (count, vd_present, vd_absent) } diff --git a/codegen/src/collect_album_types.rs b/codegen/src/collect_album_types.rs index 55d4a14..49fb56a 100644 --- a/codegen/src/collect_album_types.rs +++ b/codegen/src/collect_album_types.rs @@ -58,7 +58,7 @@ pub fn write_samples_to_dict() { let collected: BTreeMap> = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut dict = util::read_dict(); - let langs = dict.keys().map(|k| k.to_owned()).collect::>(); + let langs = dict.keys().copied().collect::>(); for lang in langs { let dict_entry = dict.entry(lang).or_default(); @@ -66,13 +66,13 @@ pub fn write_samples_to_dict() { let mut e_langs = dict_entry.equivalent.clone(); e_langs.push(lang); - e_langs.iter().for_each(|lang| { + for lang in &e_langs { collected.get(lang).unwrap().iter().for_each(|(t, v)| { dict_entry .album_types .insert(v.to_lowercase().trim().to_owned(), *t); }); - }); + } } util::write_dict(dict); diff --git a/codegen/src/collect_large_numbers.rs b/codegen/src/collect_large_numbers.rs index db96066..59979ef 100644 --- a/codegen/src/collect_large_numbers.rs +++ b/codegen/src/collect_large_numbers.rs @@ -111,7 +111,7 @@ pub async fn collect_large_numbers(concurrency: usize) { .unwrap(); channel.view_counts.iter().for_each(|(num, txt)| { - entry.insert(txt.to_owned(), *num); + entry.insert(txt.clone(), *num); }); entry.insert(channel.subscriber_count, subscriber_counts[*ch_id]); @@ -147,7 +147,7 @@ pub fn write_samples_to_dict() { let collected_nums: CollectedNumbers = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut dict = util::read_dict(); - let langs = dict.keys().map(|k| k.to_owned()).collect::>(); + let langs = dict.keys().copied().collect::>(); static POINT_REGEX: Lazy = Lazy::new(|| Regex::new(r"\d(\.|,)\d{1,3}(?:\D|$)").unwrap()); @@ -176,10 +176,7 @@ pub fn write_samples_to_dict() { }) .unwrap(); - let decimal_point = match comma_decimal { - true => ",", - false => ".", - }; + let decimal_point = if comma_decimal { "," } else { "." }; // Search for tokens @@ -217,13 +214,17 @@ pub fn write_samples_to_dict() { for lang in e_langs { let entry = collected_nums.get(&lang).unwrap(); - entry.iter().for_each(|(txt, val)| { + for (txt, val) in entry.iter() { let filtered = util::filter_largenumstr(txt); let mag = get_mag(*val); - let tokens: Vec = match dict_entry.by_char || lang == Language::Ko { - true => filtered.chars().map(|c| c.to_string()).collect(), - false => filtered.split_whitespace().map(|c| c.to_string()).collect(), + let tokens: Vec = if dict_entry.by_char || lang == Language::Ko { + filtered.chars().map(|c| c.to_string()).collect() + } else { + filtered + .split_whitespace() + .map(std::string::ToString::to_string) + .collect() }; match util::parse_numeric::(txt.split(decimal_point).next().unwrap()) { @@ -231,7 +232,7 @@ pub fn write_samples_to_dict() { let mag_before_point = get_mag(num_before_point); let mut mag_remaining = mag - mag_before_point; - tokens.iter().for_each(|t| { + for t in &tokens { // These tokens are correct in all languages // and are used to parse combined prefixes like `1.1K crore` (en-IN) let known_tmag: u8 = if t.len() == 1 { @@ -251,26 +252,26 @@ pub fn write_samples_to_dict() { .checked_sub(known_tmag) .expect("known magnitude incorrect"); } else { - insert_token(t.to_owned(), mag_remaining); + insert_token(t.clone(), mag_remaining); } - insert_nd_token(t.to_owned(), None); - }); + insert_nd_token(t.clone(), None); + } } Err(e) => { if matches!(e.kind(), std::num::IntErrorKind::Empty) { // Text does not contain any digits, search for nd_tokens - tokens.iter().for_each(|t| { + for t in &tokens { insert_nd_token( - t.to_owned(), + t.clone(), Some((*val).try_into().expect("nd_token value too large")), ); - }); + } } else { panic!("{e}, txt: {txt}") } } } - }); + } } // Insert collected data into dictionary @@ -369,7 +370,7 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result Result Result(&resp)?; - continuation - .on_response_received_actions - .iter() - .for_each(|a| { - a.reload_continuation_items_command - .continuation_items - .iter() - .for_each(|itm| { - let v = &itm.rich_item_renderer.content.video_renderer; - view_counts.insert( - util::parse_numeric(&v.view_count_text.text).unwrap(), - v.short_view_count_text.text.to_owned(), - ); - }) - }); + for action in &continuation.on_response_received_actions { + action + .reload_continuation_items_command + .continuation_items + .iter() + .for_each(|itm| { + let v = &itm.rich_item_renderer.content.video_renderer; + view_counts.insert( + util::parse_numeric(&v.view_count_text.text).unwrap(), + v.short_view_count_text.text.clone(), + ); + }); + } } Ok(ChannelData { diff --git a/codegen/src/collect_playlist_dates.rs b/codegen/src/collect_playlist_dates.rs index 82b15c3..4a87d36 100644 --- a/codegen/src/collect_playlist_dates.rs +++ b/codegen/src/collect_playlist_dates.rs @@ -118,7 +118,7 @@ pub fn write_samples_to_dict() { let collected_dates: CollectedDates = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut dict = util::read_dict(); - let langs = dict.keys().map(|k| k.to_owned()).collect::>(); + let langs = dict.keys().copied().collect::>(); let months = [ DateCase::Jan, @@ -159,7 +159,7 @@ pub fn write_samples_to_dict() { .for_each(|l| datestr_tables.push(collected_dates.get(l).unwrap())); let dict_entry = dict.entry(lang).or_default(); - let mut num_order = "".to_owned(); + let mut num_order = String::new(); let collect_nd_tokens = !matches!( lang, @@ -236,30 +236,30 @@ pub fn write_samples_to_dict() { }); }); - month_words.iter().for_each(|(word, m)| { + for (word, m) in &month_words { if *m != 0 { - dict_entry.months.insert(word.to_owned(), *m as u8); + dict_entry.months.insert(word.clone(), *m as u8); }; - }); + } if collect_nd_tokens { - td_words.iter().for_each(|(word, n)| { + for (word, n) in &td_words { match n { // Today 1 => { dict_entry .timeago_nd_tokens - .insert(word.to_owned(), "0D".to_owned()); + .insert(word.clone(), "0D".to_owned()); } // Yesterday 2 => { dict_entry .timeago_nd_tokens - .insert(word.to_owned(), "1D".to_owned()); + .insert(word.clone(), "1D".to_owned()); } _ => {} }; - }); + } if datestr_tables.len() == 1 && dict_entry.timeago_nd_tokens.len() > 2 { println!( diff --git a/codegen/src/collect_video_durations.rs b/codegen/src/collect_video_durations.rs index 3226797..8d5024f 100644 --- a/codegen/src/collect_video_durations.rs +++ b/codegen/src/collect_video_durations.rs @@ -67,7 +67,7 @@ pub fn parse_video_durations() { let durations: CollectedDurations = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut dict = util::read_dict(); - let langs = dict.keys().map(|k| k.to_owned()).collect::>(); + let langs = dict.keys().copied().collect::>(); for lang in langs { let dict_entry = dict.entry(lang).or_default(); @@ -83,7 +83,7 @@ pub fn parse_video_durations() { by_char: bool, val: u32, expect: u32, - w: String, + w: &str, unit: TimeUnit, ) -> bool { let ok = val == expect || val * 2 == expect; @@ -168,23 +168,23 @@ pub fn parse_video_durations() { let p2_n = p2.digits.parse::().unwrap_or(1); assert!( - check_add_word(words, by_char, p1_n, m, p1.word, TimeUnit::Minute), + check_add_word(words, by_char, p1_n, m, &p1.word, TimeUnit::Minute), "{txt}: min parse error" ); assert!( - check_add_word(words, by_char, p2_n, s, p2.word, TimeUnit::Second), + check_add_word(words, by_char, p2_n, s, &p2.word, TimeUnit::Second), "{txt}: sec parse error" ); } None => { if s == 0 { assert!( - check_add_word(words, by_char, p1_n, m, p1.word, TimeUnit::Minute), + check_add_word(words, by_char, p1_n, m, &p1.word, TimeUnit::Minute), "{txt}: min parse error" ); } else if m == 0 { assert!( - check_add_word(words, by_char, p1_n, s, p1.word, TimeUnit::Second), + check_add_word(words, by_char, p1_n, s, &p1.word, TimeUnit::Second), "{txt}: sec parse error" ); } else { @@ -206,11 +206,11 @@ pub fn parse_video_durations() { // dbg!(&words); - words.into_iter().for_each(|(k, v)| { + for (k, v) in words { if let Some(v) = v { dict_entry.timeago_tokens.insert(k, v.to_string()); } - }); + } } } @@ -345,7 +345,8 @@ mod tests { let ul: LanguageIdentifier = lang.to_string().split('-').next().unwrap().parse().unwrap(); - let pr = PluralRules::create(ul, PluralRuleType::CARDINAL).expect(&lang.to_string()); + let pr = PluralRules::create(ul, PluralRuleType::CARDINAL) + .unwrap_or_else(|_| panic!("{}", lang.to_string())); let mut plurals_m: HashSet = HashSet::new(); for n in 1..60 { @@ -353,11 +354,11 @@ mod tests { } let mut plurals_s = plurals_m.clone(); - durations.values().for_each(|v| { + for v in durations.values() { let (m, s) = split_duration(*v); plurals_m.remove(&pr.select(m).unwrap().into()); plurals_s.remove(&pr.select(s).unwrap().into()); - }); + } if !plurals_m.is_empty() { println!("{lang}: missing minutes {plurals_m:?}"); diff --git a/codegen/src/gen_dictionary.rs b/codegen/src/gen_dictionary.rs index e0bc9b1..452a47b 100644 --- a/codegen/src/gen_dictionary.rs +++ b/codegen/src/gen_dictionary.rs @@ -35,14 +35,18 @@ pub fn generate_dictionary() { let code_head = r#"// This file is automatically generated. DO NOT EDIT. // See codegen/gen_dictionary.rs for the generation code. +#![allow(clippy::unreadable_literal)] + +//! The dictionary contains the information required to parse dates and numbers +//! in all supported languages. + use crate::{ model::AlbumType, param::Language, util::timeago::{DateCmp, TaToken, TimeUnit}, }; -/// The dictionary contains the information required to parse dates and numbers -/// in all supported languages. +/// Dictionary entry containing language-specific parsing information pub(crate) struct Entry { /// Tokens for parsing timeago strings. /// @@ -90,11 +94,11 @@ pub(crate) fn entry(lang: Language) -> Entry { "# .to_owned(); - dict.iter().for_each(|(lang, entry)| { + for (lang, entry) in &dict { // Match selector let mut selector = format!("Language::{lang:?}"); entry.equivalent.iter().for_each(|eq| { - let _ = write!(selector, " | Language::{eq:?}"); + write!(selector, " | Language::{eq:?}").unwrap(); }); // Timeago tokens @@ -132,7 +136,7 @@ pub(crate) fn entry(lang: Language) -> Entry { // Date order let mut date_order = "&[".to_owned(); entry.date_order.chars().for_each(|c| { - let _ = write!(date_order, "DateCmp::{c}, "); + write!(date_order, "DateCmp::{c}, ").unwrap(); }); date_order = date_order.trim_end_matches([' ', ',']).to_owned() + "]"; @@ -154,16 +158,31 @@ pub(crate) fn entry(lang: Language) -> Entry { album_types.entry(txt, &format!("AlbumType::{album_type:?}")); }); - let code_ta_tokens = &ta_tokens.build().to_string().replace('\n', "\n "); - let code_ta_nd_tokens = &ta_nd_tokens.build().to_string().replace('\n', "\n "); + let code_ta_tokens = &ta_tokens + .build() + .to_string() + .replace('\n', "\n "); + let code_ta_nd_tokens = &ta_nd_tokens + .build() + .to_string() + .replace('\n', "\n "); let code_months = &months.build().to_string().replace('\n', "\n "); - let code_number_tokens = &number_tokens.build().to_string().replace('\n', "\n "); - let code_number_nd_tokens = &number_nd_tokens.build().to_string().replace('\n', "\n "); - let code_album_types = &album_types.build().to_string().replace('\n', "\n "); + let code_number_tokens = &number_tokens + .build() + .to_string() + .replace('\n', "\n "); + let code_number_nd_tokens = &number_nd_tokens + .build() + .to_string() + .replace('\n', "\n "); + let code_album_types = &album_types + .build() + .to_string() + .replace('\n', "\n "); write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n }},\n ", selector, code_ta_tokens, date_order, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types).unwrap(); - }); + } code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n"; diff --git a/codegen/src/gen_locales.rs b/codegen/src/gen_locales.rs index b31a113..66e1da9 100644 --- a/codegen/src/gen_locales.rs +++ b/codegen/src/gen_locales.rs @@ -227,7 +227,7 @@ pub enum Country { "# .to_owned(); - languages.iter().for_each(|(code, native_name)| { + for (code, native_name) in &languages { let enum_name = code .split('-') .map(|c| { @@ -262,10 +262,10 @@ pub enum Country { " Language::{enum_name} => \"{native_name}\"," ) .unwrap(); - }); + } code_langs += "}\n"; - countries.iter().for_each(|(c, n)| { + for (c, n) in &countries { let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase(); // Country enum @@ -281,7 +281,7 @@ pub enum Country { " Country::{enum_name} => \"{n}\"," ) .unwrap(); - }); + } // Add Country::Zz / Global code_countries += " /// Global (can only be used for music charts)\n"; @@ -368,8 +368,8 @@ fn map_language_section(section: &CompactLinkRendererWrap) -> BTreeMap { let res = abtest::run_all_tests(n, cli.concurrency).await; - println!("{}", serde_json::to_string_pretty(&res).unwrap()) + println!("{}", serde_json::to_string_pretty(&res).unwrap()); } }; } diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index fea74e6..228d5de 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(clippy::todo, clippy::dbg_macro)] + //! # YouTube audio/video downloader mod util; @@ -25,8 +27,8 @@ use util::DownloadError; type Result = core::result::Result; -const CHUNK_SIZE_MIN: u64 = 9000000; -const CHUNK_SIZE_MAX: u64 = 10000000; +const CHUNK_SIZE_MIN: u64 = 9_000_000; +const CHUNK_SIZE_MAX: u64 = 10_000_000; fn get_download_range(offset: u64, size: Option) -> Range { let mut rng = rand::thread_rng(); @@ -34,7 +36,7 @@ fn get_download_range(offset: u64, size: Option) -> Range { let mut chunk_end = offset + chunk_size; if let Some(size) = size { - chunk_end = chunk_end.min(size - 1) + chunk_end = chunk_end.min(size - 1); } Range { @@ -296,7 +298,7 @@ pub async fn download_video( ) -> Result<()> { // Download filepath let download_dir = PathBuf::from(output_dir); - let title = player_data.details.name.to_owned(); + let title = player_data.details.name.clone(); let output_fname_set = output_fname.is_some(); let output_fname = output_fname.unwrap_or_else(|| { filenamify::filenamify(format!("{} [{}]", title, player_data.details.id)) @@ -332,13 +334,12 @@ pub async fn download_video( return Err(DownloadError::Input( format!("File {} already exists", output_path.to_string_lossy()).into(), ))?; - } else { - info!( - "Downloaded video {} already exists", - output_path.to_string_lossy() - ); - return Ok(()); } + info!( + "Downloaded video {} already exists", + output_path.to_string_lossy() + ); + return Ok(()); } match (video, audio) { @@ -364,7 +365,7 @@ pub async fn download_video( output_fname, v.format.extension() )), - url: v.url.to_owned(), + url: v.url.clone(), video_codec: Some(v.codec), audio_codec: None, }); @@ -376,10 +377,10 @@ pub async fn download_video( output_fname, a.format.extension() )), - url: a.url.to_owned(), + url: a.url.clone(), video_codec: None, audio_codec: Some(a.codec), - }) + }); } pb.set_message(format!("Downloading {title}")); @@ -396,7 +397,7 @@ pub async fn download_video( // Delete original files stream::iter(&downloads) - .map(|d| fs::remove_file(d.file.to_owned())) + .map(|d| fs::remove_file(d.file.clone())) .buffer_unordered(downloads.len()) .collect::>() .await @@ -417,7 +418,7 @@ async fn download_streams( let n = downloads.len(); stream::iter(downloads) - .map(|d| download_single_file(&d.url, d.file.to_owned(), http.clone(), pb.clone())) + .map(|d| download_single_file(&d.url, d.file.clone(), http.clone(), pb.clone())) .buffer_unordered(n) .collect::>() .await @@ -439,7 +440,7 @@ async fn convert_streams>( downloads.iter().enumerate().for_each(|(i, d)| { args.push("-i".into()); - args.push(d.file.to_owned().into()); + args.push(d.file.clone().into()); mapping_args.push("-map".into()); mapping_args.push(i.to_string().into()); diff --git a/src/client/channel.rs b/src/client/channel.rs index 83872a3..867cc55 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -322,7 +322,7 @@ fn map_vanity_url(url: &str, id: &str) -> Option { Url::parse(url).ok().map(|mut parsed_url| { // The vanity URL from YouTube is http for some reason - let _ = parsed_url.set_scheme("https"); + _ = parsed_url.set_scheme("https"); parsed_url.to_string() }) } @@ -392,11 +392,8 @@ fn map_channel( content: (), }, response::channel::Header::CarouselHeaderRenderer(carousel) => { - let hdata = carousel - .contents - .into_iter() - .filter_map(|item| { - match item { + let hdata = carousel.contents.into_iter().find_map(|item| { + match item { response::channel::CarouselHeaderRendererItem::TopicChannelDetailsRenderer { subscriber_count_text, subtitle, @@ -404,8 +401,7 @@ fn map_channel( } => Some((subscriber_count_text.or(subtitle), avatar)), response::channel::CarouselHeaderRendererItem::None => None, } - }) - .next(); + }); Channel { id: metadata.external_id, @@ -568,7 +564,7 @@ fn _order_ctoken( pb_80226972.string(3, &pbi.to_base64()); let mut pb = ProtoBuilder::new(); - pb.embedded(80226972, pb_80226972); + pb.embedded(80_226_972, pb_80226972); pb.to_base64() } diff --git a/src/client/channel_rss.rs b/src/client/channel_rss.rs index 9b86e87..68f700c 100644 --- a/src/client/channel_rss.rs +++ b/src/client/channel_rss.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use crate::{ error::{Error, ExtractionError}, model::ChannelRss, - report::Report, + report::{Report, RustyPipeInfo}, }; use super::{response, RustyPipeQuery}; @@ -19,10 +19,7 @@ impl RustyPipeQuery { /// 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!( - "https://www.youtube.com/feeds/videos.xml?channel_id={}", - channel_id, - ); + let url = format!("https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"); let xml = self .client .http_request_txt(&self.client.inner.http.get(&url).build()?) @@ -40,15 +37,15 @@ impl RustyPipeQuery { Err(e) => { if let Some(reporter) = &self.client.inner.reporter { let report = Report { - info: Default::default(), + info: RustyPipeInfo::default(), level: crate::report::Level::ERR, - operation: "channel_rss".to_owned(), + operation: "channel_rss", error: Some(e.to_string()), msgs: Vec::new(), deobf_data: None, http_request: crate::report::HTTPRequest { - url, - method: "GET".to_owned(), + url: &url, + method: "GET", req_header: BTreeMap::new(), req_body: String::new(), status: 200, diff --git a/src/client/mod.rs b/src/client/mod.rs index b6f909c..ae7e180 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -39,7 +39,7 @@ use crate::{ deobfuscate::DeobfData, error::{Error, ExtractionError}, param::{Country, Language}, - report::{FileReporter, Level, Report, Reporter, DEFAULT_REPORT_DIR}, + report::{FileReporter, Level, Report, Reporter, RustyPipeInfo, DEFAULT_REPORT_DIR}, serializer::MapResult, util, }; @@ -73,7 +73,7 @@ pub enum ClientType { } impl ClientType { - fn is_web(&self) -> bool { + fn is_web(self) -> bool { match self { ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true, ClientType::Android | ClientType::Ios => false, @@ -118,11 +118,11 @@ struct ClientInfo<'a> { impl Default for ClientInfo<'_> { fn default() -> Self { Self { - client_name: Default::default(), - client_version: Default::default(), + client_name: "", + client_version: Cow::default(), client_screen: None, device_model: None, - platform: Default::default(), + platform: "", original_url: None, visitor_data: None, hl: Language::En, @@ -432,6 +432,7 @@ impl RustyPipeBuilder { /// Return a new `RustyPipeBuilder`. /// /// This is the same as [`RustyPipe::builder`] + #[must_use] pub fn new() -> Self { RustyPipeBuilder { default_opts: RustyPipeOpts::default(), @@ -445,6 +446,7 @@ impl RustyPipeBuilder { } /// Return a new, configured RustyPipe instance. + #[must_use] pub fn build(self) -> RustyPipe { let mut client_builder = ClientBuilder::new() .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) @@ -509,6 +511,7 @@ impl RustyPipeBuilder { /// This option has no effect if the storage backend or reporter are manually set or disabled. /// /// **Default value**: current working directory + #[must_use] pub fn storage_dir>(mut self, path: P) -> Self { self.storage_dir = Some(path.into()); self @@ -519,12 +522,14 @@ impl RustyPipeBuilder { /// program executions. /// /// **Default value**: [`FileStorage`] in `rustypipe_cache.json` + #[must_use] pub fn storage(mut self, storage: Box) -> Self { self.storage = DefaultOpt::Some(storage); self } /// Disable cache storage + #[must_use] pub fn no_storage(mut self) -> Self { self.storage = DefaultOpt::None; self @@ -533,12 +538,14 @@ impl RustyPipeBuilder { /// Add a `Reporter` to collect error details /// /// **Default value**: [`FileReporter`] creating reports in `./rustypipe_reports` + #[must_use] pub fn reporter(mut self, reporter: Box) -> Self { self.reporter = DefaultOpt::Some(reporter); self } /// Disable the creation of report files in case of errors and warnings. + #[must_use] pub fn no_reporter(mut self) -> Self { self.reporter = DefaultOpt::None; self @@ -550,12 +557,14 @@ impl RustyPipeBuilder { /// response body has finished. /// /// **Default value**: 10s + #[must_use] pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = DefaultOpt::Some(timeout); self } /// Disable the HTTP request timeout. + #[must_use] pub fn no_timeout(mut self) -> Self { self.timeout = DefaultOpt::None; self @@ -570,6 +579,7 @@ impl RustyPipeBuilder { /// random jitter to be less predictable). /// /// **Default value**: 2 + #[must_use] pub fn n_http_retries(mut self, n_retries: u32) -> Self { self.n_http_retries = n_retries; self @@ -579,6 +589,7 @@ impl RustyPipeBuilder { /// /// **Default value**: `Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0` /// (Firefox ESR on Debian) + #[must_use] pub fn user_agent>(mut self, user_agent: S) -> Self { self.user_agent = Some(user_agent.into()); self @@ -591,6 +602,7 @@ impl RustyPipeBuilder { /// **Default value**: `Language::En` (English) /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn lang(mut self, lang: Language) -> Self { self.default_opts.lang = lang; self @@ -603,6 +615,7 @@ impl RustyPipeBuilder { /// **Default value**: `Country::Us` (USA) /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn country(mut self, country: Country) -> Self { self.default_opts.country = validate_country(country); self @@ -613,6 +626,7 @@ impl RustyPipeBuilder { /// This should only be used for debugging. /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn report(mut self) -> Self { self.default_opts.report = true; self @@ -624,6 +638,7 @@ impl RustyPipeBuilder { /// This should only be used for testing. /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn strict(mut self) -> Self { self.default_opts.strict = true; self @@ -643,6 +658,7 @@ impl RustyPipeBuilder { /// visitor, so you should not use the same vistor data cookie for batch operations. /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn visitor_data>(mut self, visitor_data: S) -> Self { self.default_opts.visitor_data = Some(visitor_data.into()); self @@ -653,6 +669,7 @@ impl RustyPipeBuilder { /// see also [`RustyPipeBuilder::visitor_data`] /// /// **Info**: you can set this option for individual queries, too + #[must_use] pub fn visitor_data_opt>(mut self, visitor_data: Option) -> Self { self.default_opts.visitor_data = visitor_data.map(S::into); self @@ -669,6 +686,7 @@ impl RustyPipe { /// Create a new RustyPipe instance with default settings. /// /// To create an instance with custom options, use [`RustyPipeBuilder`] instead. + #[must_use] pub fn new() -> Self { RustyPipeBuilder::new().build() } @@ -676,11 +694,13 @@ impl RustyPipe { /// Create a new [`RustyPipeBuilder`] /// /// This is the same as [`RustyPipeBuilder::new`] + #[must_use] pub fn builder() -> RustyPipeBuilder { RustyPipeBuilder::new() } /// Create a new [`RustyPipeQuery`] to run an API request + #[must_use] pub fn query(&self) -> RustyPipeQuery { RustyPipeQuery { client: self.clone(), @@ -779,7 +799,7 @@ impl RustyPipe { .get(sw_url) .header(header::ORIGIN, origin) .header(header::REFERER, origin) - .header(header::COOKIE, self.inner.consent_cookie.to_owned()) + .header(header::COOKIE, self.inner.consent_cookie.clone()) .build() .unwrap(), ) @@ -828,13 +848,13 @@ impl RustyPipe { let mut desktop_client = self.inner.cache.desktop_client.write().await; match desktop_client.get() { - Some(cdata) => cdata.version.to_owned(), + Some(cdata) => cdata.version.clone(), None => { log::debug!("getting desktop client version"); match self.extract_desktop_client_version().await { Ok(version) => { *desktop_client = CacheEntry::from(ClientData { - version: version.to_owned(), + version: version.clone(), }); drop(desktop_client); self.store_cache().await; @@ -860,13 +880,13 @@ impl RustyPipe { let mut music_client = self.inner.cache.music_client.write().await; match music_client.get() { - Some(cdata) => cdata.version.to_owned(), + Some(cdata) => cdata.version.clone(), None => { log::debug!("getting music client version"); match self.extract_music_client_version().await { Ok(version) => { *music_client = CacheEntry::from(ClientData { - version: version.to_owned(), + version: version.clone(), }); drop(music_client); self.store_cache().await; @@ -944,6 +964,7 @@ impl RustyPipeQuery { /// Set the language parameter used when accessing the YouTube API /// /// This will change multilanguage video titles, descriptions and textual dates + #[must_use] pub fn lang(mut self, lang: Language) -> Self { self.opts.lang = lang; self @@ -952,6 +973,7 @@ impl RustyPipeQuery { /// Set the country parameter used when accessing the YouTube API. /// /// This will change trends and recommended content. + #[must_use] pub fn country(mut self, country: Country) -> Self { self.opts.country = validate_country(country); self @@ -960,6 +982,7 @@ impl RustyPipeQuery { /// Generate a report on every operation. /// /// This should only be used for debugging. + #[must_use] pub fn report(mut self) -> Self { self.opts.report = true; self @@ -969,6 +992,7 @@ impl RustyPipeQuery { /// are warnings during deserialization (e.g. invalid items). /// /// This should only be used for testing. + #[must_use] pub fn strict(mut self) -> Self { self.opts.strict = true; self @@ -986,6 +1010,7 @@ impl RustyPipeQuery { /// /// 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. + #[must_use] pub fn visitor_data>(mut self, visitor_data: S) -> Self { self.opts.visitor_data = Some(visitor_data.into()); self @@ -994,6 +1019,7 @@ impl RustyPipeQuery { /// Set the YouTube visitor data cookie to an optional value /// /// see also [`RustyPipeQuery::visitor_data`] + #[must_use] pub fn visitor_data_opt>(mut self, visitor_data: Option) -> Self { self.opts.visitor_data = visitor_data.map(S::into); self @@ -1011,13 +1037,10 @@ impl RustyPipeQuery { localized: bool, visitor_data: Option<&'a str>, ) -> YTContext { - let hl = match localized { - true => self.opts.lang, - false => Language::En, - }; - let gl = match localized { - true => self.opts.country, - false => Country::Us, + let (hl, gl) = if localized { + (self.opts.lang, self.opts.country) + } else { + (Language::En, Country::Us) }; let visitor_data = self.opts.visitor_data.as_deref().or(visitor_data); @@ -1119,7 +1142,7 @@ impl RustyPipeQuery { )) .header(header::ORIGIN, YOUTUBE_HOME_URL) .header(header::REFERER, YOUTUBE_HOME_URL) - .header(header::COOKIE, self.client.inner.consent_cookie.to_owned()) + .header(header::COOKIE, self.client.inner.consent_cookie.clone()) .header("X-YouTube-Client-Name", "1") .header( "X-YouTube-Client-Version", @@ -1134,7 +1157,7 @@ impl RustyPipeQuery { )) .header(header::ORIGIN, YOUTUBE_MUSIC_HOME_URL) .header(header::REFERER, YOUTUBE_MUSIC_HOME_URL) - .header(header::COOKIE, self.client.inner.consent_cookie.to_owned()) + .header(header::COOKIE, self.client.inner.consent_cookie.clone()) .header("X-YouTube-Client-Name", "67") .header( "X-YouTube-Client-Version", @@ -1187,7 +1210,7 @@ impl RustyPipeQuery { /// Get a YouTube visitor data cookie, which is necessary for certain requests async fn get_visitor_data(&self) -> Result { match &self.opts.visitor_data { - Some(vd) => Ok(vd.to_owned()), + Some(vd) => Ok(vd.clone()), None => self.client.get_visitor_data().await, } } @@ -1333,21 +1356,19 @@ impl RustyPipeQuery { if level > Level::DBG || self.opts.report { if let Some(reporter) = &self.client.inner.reporter { let report = Report { - info: Default::default(), + info: RustyPipeInfo::default(), level, - operation: format!("{operation}({id})"), + operation: &format!("{operation}({id})"), error, msgs, deobf_data: deobf.cloned(), http_request: crate::report::HTTPRequest { - url: request.url().to_string(), - method: "POST".to_string(), + url: request.url().as_str(), + method: request.method().as_str(), req_header: request .headers() .iter() - .map(|(k, v)| { - (k.to_string(), v.to_str().unwrap_or_default().to_owned()) - }) + .map(|(k, v)| (k.as_str(), v.to_str().unwrap_or_default().to_owned())) .collect(), req_body: serde_json::to_string(body).unwrap_or_default(), status: req_res.status.into(), diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index c715c00..98d701b 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -26,9 +26,10 @@ impl RustyPipeQuery { all_albums: bool, ) -> Result { let artist_id = artist_id.as_ref(); - let visitor_data = match all_albums { - true => Some(self.get_visitor_data().await?), - false => None, + let visitor_data = if all_albums { + Some(self.get_visitor_data().await?) + } else { + None }; let res = self._music_artist(artist_id, visitor_data.as_deref()).await; @@ -196,7 +197,7 @@ fn map_artist_page( lang, ArtistId { id: Some(id.to_owned()), - name: header.title.to_owned(), + name: header.title.clone(), }, ); diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 05fce53..0bea1b3 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -60,7 +60,7 @@ impl RustyPipeQuery { // In rare cases, albums may have track numbers =0 (example: MPREb_RM0QfZ0eSKL) // They should be replaced with the track number derived from the previous track. let mut n_prev = 0; - for track in album.tracks.iter_mut() { + for track in &mut album.tracks { let tn = track.track_nr.unwrap_or_default(); if tn == 0 { n_prev += 1; @@ -80,7 +80,7 @@ impl RustyPipeQuery { .enumerate() .filter_map(|(i, track)| { if track.is_video { - Some((i, track.name.to_owned())) + Some((i, track.name.clone())) } else { None } @@ -97,7 +97,7 @@ impl RustyPipeQuery { for (i, title) in to_replace { let found_track = playlist.tracks.items.iter().find_map(|track| { if track.name == title && !track.is_video { - Some((track.id.to_owned(), track.duration)) + Some((track.id.clone(), track.duration)) } else { None } @@ -173,7 +173,7 @@ impl MapResponse for response::MusicPlaylist { .split(|p| p == DOT_SEPARATOR) .collect::>(); parts - .get(if parts.len() > 2 { 1 } else { 0 }) + .get(usize::from(parts.len() > 2)) .and_then(|txt| util::parse_numeric::(&txt[0]).ok()) }) } else { @@ -293,7 +293,7 @@ impl MapResponse for response::MusicPlaylist { match section { response::music_item::ItemSection::MusicShelfRenderer(sh) => shelf = Some(sh), response::music_item::ItemSection::MusicCarouselShelfRenderer(sh) => { - album_variants = Some(sh.contents) + album_variants = Some(sh.contents); } _ => (), } @@ -355,7 +355,7 @@ impl MapResponse for response::MusicPlaylist { ) }) .unwrap_or_default(); - let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.to_owned())); + let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone())); let mut mapper = MusicListMapper::with_album( lang, @@ -363,7 +363,7 @@ impl MapResponse for response::MusicPlaylist { by_va, AlbumId { id: id.to_owned(), - name: header.title.to_owned(), + name: header.title.clone(), }, ); mapper.map_response(shelf.contents); diff --git a/src/client/music_search.rs b/src/client/music_search.rs index 6bdb7c3..eafb524 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -170,9 +170,10 @@ impl RustyPipeQuery { ) -> Result, Error> { self._music_search_playlists( query, - match community { - true => Params::CommunityPlaylists, - false => Params::YtmPlaylists, + if community { + Params::CommunityPlaylists + } else { + Params::YtmPlaylists }, ) .await @@ -266,7 +267,7 @@ impl MapResponse for response::MusicSearch { } response::music_search::ItemSection::ItemSectionRenderer { contents } => { if let Some(corrected) = contents.into_iter().next() { - corrected_query = Some(corrected.showing_results_for_renderer.corrected_query) + corrected_query = Some(corrected.showing_results_for_renderer.corrected_query); } } response::music_search::ItemSection::None => {} @@ -324,7 +325,7 @@ impl MapResponse> for response::MusicSearc } response::music_search::ItemSection::ItemSectionRenderer { contents } => { if let Some(corrected) = contents.into_iter().next() { - corrected_query = Some(corrected.showing_results_for_renderer.corrected_query) + corrected_query = Some(corrected.showing_results_for_renderer.corrected_query); } } response::music_search::ItemSection::None => {} diff --git a/src/client/player.rs b/src/client/player.rs index d84adfb..cda8f70 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -177,12 +177,12 @@ impl MapResponse for response::Player { } response::player::PlayabilityStatus::LoginRequired { reason, messages } => { let mut msg = reason; - messages.iter().for_each(|m| { + for m in &messages { if !msg.is_empty() { msg.push(' '); } msg.push_str(m); - }); + } // reason (age restriction): "Sign in to confirm your age" // or: "This video may be inappropriate for some users." @@ -341,9 +341,9 @@ impl MapResponse for response::Player { + "&sigh=" + sigh; - let sprite_count = ((total_count as f64) - / (frames_per_page_x * frames_per_page_y) as f64) - .ceil() as u32; + let sprite_count = (f64::from(total_count) + / f64::from(frames_per_page_x * frames_per_page_y)) + .ceil() as u32; Some(Frameset { url_template: url, @@ -413,11 +413,11 @@ fn deobf_nsig( let nsig: String; if let Some(n) = url_params.get("n") { nsig = if n == &last_nsig[0] { - last_nsig[1].to_owned() + last_nsig[1].clone() } else { let nsig = deobf.deobfuscate_nsig(n)?; last_nsig[0] = n.to_string(); - last_nsig[1] = nsig.to_owned(); + last_nsig[1] = nsig.clone(); nsig }; @@ -490,25 +490,19 @@ fn map_video_stream( deobf: &Deobfuscator, last_nsig: &mut [String; 2], ) -> MapResult> { - let (mtype, codecs) = match parse_mime(&f.mime_type) { - Some(x) => x, - None => { - return MapResult { - c: None, - warnings: vec![format!( - "Invalid mime type `{}` in video format {:?}", - &f.mime_type, &f - )], - } + let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { + return MapResult { + c: None, + warnings: vec![format!( + "Invalid mime type `{}` in video format {:?}", + &f.mime_type, &f + )], } }; - let format = match get_video_format(mtype) { - Some(f) => f, - None => { - return MapResult { - c: None, - warnings: vec![format!("invalid video format. itag: {}", f.itag)], - } + let Some(format) = get_video_format(mtype) else { + return MapResult { + c: None, + warnings: vec![format!("invalid video format. itag: {}", f.itag)], } }; let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig); @@ -532,9 +526,9 @@ fn map_video_stream( quality: f.quality_label.unwrap(), hdr: f.color_info.unwrap_or_default().primaries == player::Primaries::ColorPrimariesBt2020, - mime: f.mime_type.to_owned(), format, codec: get_video_codec(codecs), + mime: f.mime_type, throttled: url.throttled, }), warnings: map_res.warnings, @@ -551,25 +545,19 @@ fn map_audio_stream( deobf: &Deobfuscator, last_nsig: &mut [String; 2], ) -> MapResult> { - let (mtype, codecs) = match parse_mime(&f.mime_type) { - Some(x) => x, - None => { - return MapResult { - c: None, - warnings: vec![format!( - "Invalid mime type `{}` in video format {:?}", - &f.mime_type, &f - )], - } + let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { + return MapResult { + c: None, + warnings: vec![format!( + "Invalid mime type `{}` in video format {:?}", + &f.mime_type, &f + )], } }; - let format = match get_audio_format(mtype) { - Some(f) => f, - None => { - return MapResult { - c: None, - warnings: vec![format!("invalid audio format. itag: {}", f.itag)], - } + let Some(format) = get_audio_format(mtype) else { + return MapResult { + c: None, + warnings: vec![format!("invalid audio format. itag: {}", f.itag)], } }; let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig); @@ -586,9 +574,9 @@ fn map_audio_stream( index_range: f.index_range, init_range: f.init_range, duration_ms: f.approx_duration_ms, - mime: f.mime_type.to_owned(), format, codec: get_audio_codec(codecs), + mime: f.mime_type, channels: f.audio_channels, loudness_db: f.loudness_db, throttled: url.throttled, @@ -686,7 +674,7 @@ fn map_audio_track( } }, _ => {} - }) + }); } AudioTrack { diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 385f3a1..67f58c6 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -60,9 +60,8 @@ impl MapResponse for response::Playlist { lang: crate::param::Language, _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result, ExtractionError> { - let (contents, header) = match (self.contents, self.header) { - (Some(contents), Some(header)) => (contents, header), - _ => return Err(response::alerts_to_err(id, self.alerts)), + let (Some(contents), Some(header)) = (self.contents, self.header) else { + return Err(response::alerts_to_err(id, self.alerts)); }; let video_items = contents diff --git a/src/client/response/channel_rss.rs b/src/client/response/channel_rss.rs index 2ce9859..d3e1e6f 100644 --- a/src/client/response/channel_rss.rs +++ b/src/client/response/channel_rss.rs @@ -87,11 +87,9 @@ impl From for crate::model::ChannelRss { feed.entry .iter() .find_map(|entry| { - if !entry.channel_id.is_empty() { - Some(entry.channel_id.to_owned()) - } else { - None - } + Some(entry.channel_id.as_str()) + .filter(|id| id.is_empty()) + .map(str::to_owned) }) .or_else(|| { feed.author diff --git a/src/client/response/mod.rs b/src/client/response/mod.rs index a682f95..9f531e2 100644 --- a/src/client/response/mod.rs +++ b/src/client/response/mod.rs @@ -349,7 +349,7 @@ impl From for crate::model::Verification { match icon.icon_type { IconType::Check => Self::Verified, IconType::OfficialArtistBadge => Self::Artist, - _ => Self::None, + IconType::Like => Self::None, } } } diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index 4e8e766..3edd74e 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -500,7 +500,7 @@ impl MusicListMapper { let pt_id = item .navigation_endpoint - .and_then(|ne| ne.music_page()) + .and_then(NavigationEndpoint::music_page) .or_else(|| { c1.and_then(|c1| { c1.renderer.text.0.into_iter().next().and_then(|t| match t { @@ -796,7 +796,7 @@ impl MusicListMapper { name: item.title, duration: None, cover: item.thumbnail_renderer.into(), - artist_id: artists.first().and_then(|a| a.id.to_owned()), + artist_id: artists.first().and_then(|a| a.id.clone()), artists, album: None, view_count: subtitle_p2.and_then(|c| { @@ -872,7 +872,7 @@ impl MusicListMapper { id, name: item.title, cover: item.thumbnail_renderer.into(), - artist_id: artists.first().and_then(|a| a.id.to_owned()), + artist_id: artists.first().and_then(|a| a.id.clone()), artists, album_type, year, @@ -886,8 +886,7 @@ impl MusicListMapper { let from_ytm = subtitle_p2 .as_ref() .and_then(|p| p.0.first()) - .map(util::is_ytm) - .unwrap_or(true); + .map_or(true, util::is_ytm); let channel = subtitle_p2.and_then(|p| { p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok()) }); @@ -973,7 +972,7 @@ impl MusicListMapper { id, name: card.title, cover: card.thumbnail.into(), - artist_id: artists.first().and_then(|a| a.id.to_owned()), + artist_id: artists.first().and_then(|a| a.id.clone()), artists, album_type, year: subtitle_p3.and_then(|y| util::parse_numeric(y.first_str()).ok()), @@ -1010,7 +1009,7 @@ impl MusicListMapper { name: card.title, duration, cover: card.thumbnail.into(), - artist_id: artists.first().and_then(|a| a.id.to_owned()), + artist_id: artists.first().and_then(|a| a.id.clone()), artists, album, view_count, @@ -1024,8 +1023,7 @@ impl MusicListMapper { let from_ytm = subtitle_p2 .as_ref() .and_then(|p| p.0.first()) - .map(util::is_ytm) - .unwrap_or(true); + .map_or(true, util::is_ytm); let channel = subtitle_p2 .and_then(|p| p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok())); let track_count = @@ -1128,9 +1126,10 @@ impl MusicListMapper { /// /// 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())), - false => Ok(()), + if self.has_unknown { + Err(ExtractionError::InvalidData("unknown YTM items".into())) + } else { + Ok(()) } } } @@ -1167,7 +1166,7 @@ fn map_artist_id_fallback( fallback_artist: Option<&ArtistId>, ) -> Option { menu.and_then(|m| map_artist_id(m.menu_renderer.contents)) - .or_else(|| fallback_artist.and_then(|a| a.id.to_owned())) + .or_else(|| fallback_artist.and_then(|a| a.id.clone())) } pub(crate) fn map_artist_id(entries: Vec) -> Option { diff --git a/src/client/response/url_endpoint.rs b/src/client/response/url_endpoint.rs index 3c00354..2a3da26 100644 --- a/src/client/response/url_endpoint.rs +++ b/src/client/response/url_endpoint.rs @@ -69,6 +69,7 @@ impl<'de> Deserialize<'de> for BrowseEndpoint { let bep = BEp::deserialize(deserializer)?; // Remove the VL prefix from the playlist id + #[allow(clippy::map_unwrap_or)] let browse_id = bep .browse_endpoint_context_supported_configs .as_ref() @@ -167,9 +168,8 @@ pub(crate) enum PageType { impl PageType { pub(crate) fn to_url_target(self, id: String) -> Option { match self { - PageType::Artist => Some(UrlTarget::Channel { id }), + PageType::Artist | PageType::Channel => Some(UrlTarget::Channel { id }), PageType::Album => Some(UrlTarget::Album { id }), - PageType::Channel => Some(UrlTarget::Channel { id }), PageType::Playlist => Some(UrlTarget::Playlist { id }), PageType::Unknown => None, } diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index a46d8dc..5dc62f6 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -419,8 +419,8 @@ impl YouTubeListMapper { Self { lang, channel: Some(ChannelTag { - id: channel.id.to_owned(), - name: channel.name.to_owned(), + id: channel.id.clone(), + name: channel.name.clone(), avatar: Vec::new(), verification: channel.verification, subscriber_count: channel.subscriber_count, @@ -572,14 +572,15 @@ impl YouTubeListMapper { fn map_channel(&mut self, channel: ChannelRenderer) -> ChannelItem { // channel handle instead of subscriber count (A/B test 3) - let (sc_txt, vc_text) = match channel + let (sc_txt, vc_text) = if channel .subscriber_count_text .as_ref() .map(|txt| txt.starts_with('@')) .unwrap_or_default() { - true => (channel.video_count_text, None), - false => (channel.subscriber_count_text, channel.video_count_text), + (channel.video_count_text, None) + } else { + (channel.subscriber_count_text, channel.video_count_text) }; ChannelItem { @@ -643,7 +644,7 @@ impl YouTubeListMapper { .map(|url| (l.title, util::sanitize_yt_url(&url.url))) }) .collect(), - }) + }); } YouTubeListItem::RichItemRenderer { content } => { self.map_item(*content); @@ -701,7 +702,7 @@ impl YouTubeListMapper { match item { YouTubeListItem::PlaylistRenderer(playlist) => { let mapped = self.map_playlist(playlist); - self.items.push(mapped) + self.items.push(mapped); } YouTubeListItem::ContinuationItemRenderer { continuation_endpoint, diff --git a/src/client/url_resolver.rs b/src/client/url_resolver.rs index f078397..51d7146 100644 --- a/src/client/url_resolver.rs +++ b/src/client/url_resolver.rs @@ -168,12 +168,13 @@ impl RustyPipeQuery { e, Error::Extraction(ExtractionError::NotFound { .. }) ) { - match util::VIDEO_ID_REGEX.is_match(id) { - true => Ok(UrlTarget::Video { + if util::VIDEO_ID_REGEX.is_match(id) { + Ok(UrlTarget::Video { id: id.to_owned(), start_time: get_start_time(), - }), - false => Err(e), + }) + } else { + Err(e) } } else { Err(e) diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 8bf791c..b04e806 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -393,7 +393,7 @@ impl MapResponse> for response::VideoComments { lang, ); comments.push(res.c); - warnings.append(&mut res.warnings) + warnings.append(&mut res.warnings); } response::video_details::CommentListItem::CommentRenderer(comment) => { let mut res = map_comment( @@ -403,7 +403,7 @@ impl MapResponse> for response::VideoComments { lang, ); comments.push(res.c); - warnings.append(&mut res.warnings) + warnings.append(&mut res.warnings); } response::video_details::CommentListItem::ContinuationItemRenderer { continuation_endpoint, @@ -433,11 +433,11 @@ fn map_recommendations( let mut mapper = response::YouTubeListMapper::::new(lang); mapper.map_response(r); - if let Some(continuations) = continuations { - continuations.into_iter().for_each(|c| { - mapper.ctoken = Some(c.next_continuation_data.continuation); - }) - }; + mapper.ctoken = mapper.ctoken.or_else(|| { + continuations + .and_then(|c| c.into_iter().next()) + .map(|c| c.next_continuation_data.continuation) + }); MapResult { c: Paginator::new_ext( diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index 4ce8bbd..40a32c2 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -238,7 +238,7 @@ fn extract_js_fn(js: &str, name: &str) -> Result { fn get_nsig_fn(player_js: &str) -> Result { let function_name = get_nsig_fn_name(player_js)?; - let function_base = function_name.to_owned() + "=function"; + let function_base = function_name.clone() + "=function"; let offset = player_js.find(&function_base).unwrap_or_default(); extract_js_fn(&player_js[offset..], &function_name) diff --git a/src/error.rs b/src/error.rs index 10de65d..471568c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -124,7 +124,7 @@ impl Display for UnavailabilityReason { } pub(crate) mod internal { - use super::*; + use super::{Error, ExtractionError}; /// Error that occurred during the initialization /// or use of the YouTube URL signature deobfuscator. @@ -167,7 +167,7 @@ impl From for Error { fn from(value: reqwest::Error) -> Self { if value.is_status() { if let Some(status) = value.status() { - return Self::HttpStatus(status.as_u16(), Default::default()); + return Self::HttpStatus(status.as_u16(), Cow::default()); } } Self::Http(value.to_string().into()) @@ -186,8 +186,9 @@ impl Error { matches!( self, Self::HttpStatus(_, _) - | Self::Extraction(ExtractionError::InvalidData(_)) - | Self::Extraction(ExtractionError::WrongResult(_)) + | Self::Extraction( + ExtractionError::InvalidData(_) | ExtractionError::WrongResult(_) + ) ) } diff --git a/src/lib.rs b/src/lib.rs index d6db8d9..3ea4373 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,19 @@ #![doc = include_str!("../README.md")] -#![warn(missing_docs, clippy::todo, clippy::dbg_macro)] +#![warn(missing_docs, clippy::todo, clippy::dbg_macro, clippy::pedantic)] +#![allow( + clippy::doc_markdown, + clippy::similar_names, + clippy::items_after_statements, + clippy::too_many_lines, + clippy::module_name_repetitions, + clippy::must_use_candidate, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::single_match_else, + clippy::missing_errors_doc, + clippy::missing_panics_doc +)] //! ## Go to //! diff --git a/src/model/mod.rs b/src/model/mod.rs index 3047810..ea832f3 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -16,7 +16,7 @@ use serde_with::serde_as; use time::{Date, OffsetDateTime}; use self::{paginator::Paginator, richtext::RichText}; -use crate::{error::Error, param::Country, serializer::DateYmd, util}; +use crate::{error::Error, param::Country, serializer::DateYmd, validate}; /* #COMMON @@ -110,22 +110,10 @@ impl UrlTarget { /// Validate the YouTube ID from the URL target pub(crate) fn validate(&self) -> Result<(), Error> { match self { - UrlTarget::Video { id, .. } => match util::VIDEO_ID_REGEX.is_match(id) { - true => Ok(()), - false => Err(Error::Other("invalid video id".into())), - }, - UrlTarget::Channel { id } => match util::CHANNEL_ID_REGEX.is_match(id) { - true => Ok(()), - false => Err(Error::Other("invalid channel id".into())), - }, - UrlTarget::Playlist { id } => match util::PLAYLIST_ID_REGEX.is_match(id) { - true => Ok(()), - false => Err(Error::Other("invalid playlist id".into())), - }, - UrlTarget::Album { id } => match util::ALBUM_ID_REGEX.is_match(id) { - true => Ok(()), - false => Err(Error::Other("invalid album id".into())), - }, + UrlTarget::Video { id, .. } => validate::video_id(id), + UrlTarget::Channel { id } => validate::channel_id(id), + UrlTarget::Playlist { id } => validate::playlist_id(id), + UrlTarget::Album { id } => validate::album_id(id), } } } diff --git a/src/model/richtext.rs b/src/model/richtext.rs index 836e3f6..63081eb 100644 --- a/src/model/richtext.rs +++ b/src/model/richtext.rs @@ -61,9 +61,9 @@ impl TextComponent { /// Get the text from the component pub fn get_text(&self) -> &str { match self { - TextComponent::Text(text) => text, - TextComponent::Web { text, .. } => text, - TextComponent::YouTube { text, .. } => text, + TextComponent::Text(text) + | TextComponent::Web { text, .. } + | TextComponent::YouTube { text, .. } => text, } } @@ -73,7 +73,7 @@ impl TextComponent { pub fn get_url(&self, yt_host: &str) -> String { match self { TextComponent::Text(_) => String::new(), - TextComponent::Web { url, .. } => url.to_owned(), + TextComponent::Web { url, .. } => url.clone(), TextComponent::YouTube { target, .. } => target.to_url_yt_host(yt_host), } } @@ -82,7 +82,7 @@ impl TextComponent { impl ToPlaintext for TextComponent { fn to_plaintext_yt_host(&self, yt_host: &str) -> String { match self { - TextComponent::Text(text) => text.to_owned(), + TextComponent::Text(text) => text.clone(), _ => self.get_url(yt_host), } } diff --git a/src/param/mod.rs b/src/param/mod.rs index a2aba29..60060bc 100644 --- a/src/param/mod.rs +++ b/src/param/mod.rs @@ -33,7 +33,7 @@ pub enum ChannelOrder { impl ChannelVideoTab { /// Get the tab ID used to create ordered continuation tokens - pub(crate) const fn order_ctoken_id(&self) -> u32 { + pub(crate) const fn order_ctoken_id(self) -> u32 { match self { ChannelVideoTab::Videos => 15, ChannelVideoTab::Shorts => 10, diff --git a/src/param/search_filter.rs b/src/param/search_filter.rs index 489ae63..b0edf71 100644 --- a/src/param/search_filter.rs +++ b/src/param/search_filter.rs @@ -93,77 +93,90 @@ pub enum Length { impl SearchFilter { /// Get a new [`SearchFilter`] + #[must_use] pub fn new() -> Self { Self::default() } /// Sort the search results + #[must_use] pub fn sort(mut self, sort: Order) -> Self { self.sort = Some(sort); self } /// Sort the search results + #[must_use] pub fn sort_opt(mut self, sort: Option) -> Self { self.sort = sort; self } /// Filter videos with specific features + #[must_use] pub fn feature(mut self, feature: Feature) -> Self { self.features.insert(feature); self } /// Filter videos with specific features + #[must_use] pub fn features(mut self, features: BTreeSet) -> Self { self.features = features; self } /// Filter videos by upload date range + #[must_use] pub fn date(mut self, date: UploadDate) -> Self { self.date = Some(date); self } /// Filter videos by upload date range + #[must_use] pub fn date_opt(mut self, date: Option) -> Self { self.date = date; self } /// Filter videos by item type + #[must_use] pub fn item_type(mut self, item_type: ItemType) -> Self { self.item_type = Some(item_type); self } /// Filter videos by item type + #[must_use] pub fn item_type_opt(mut self, item_type: Option) -> Self { self.item_type = item_type; self } /// Filter videos by length range + #[must_use] pub fn length(mut self, length: Length) -> Self { self.length = Some(length); self } /// Filter videos by length range + #[must_use] pub fn length_opt(mut self, length: Option) -> Self { self.length = length; self } /// Disable the automatic correction of mistyped search terms + #[must_use] pub fn verbatim(mut self) -> Self { self.verbatim = true; self } /// Disable the automatic correction of mistyped search terms + #[must_use] pub fn verbatim_set(mut self, verbatim: bool) -> Self { self.verbatim = verbatim; self @@ -197,7 +210,7 @@ impl SearchFilter { if self.verbatim { let mut extras = ProtoBuilder::new(); extras.varint(1, 1); - pb.embedded(8, extras) + pb.embedded(8, extras); } pb.to_base64() diff --git a/src/param/stream_filter.rs b/src/param/stream_filter.rs index f060b5b..8792b82 100644 --- a/src/param/stream_filter.rs +++ b/src/param/stream_filter.rs @@ -32,36 +32,41 @@ enum FilterResult { impl FilterResult { fn hard(val: bool) -> Self { - match val { - true => Self::Match, - false => Self::Deny, + if val { + Self::Match + } else { + Self::Deny } } fn soft(val: bool) -> Self { - match val { - true => Self::Match, - false => Self::AllowLowest, + if val { + Self::Match + } else { + Self::AllowLowest } } fn allow(val: bool) -> Self { - match val { - true => Self::Allow, - false => Self::Deny, + if val { + Self::Allow + } else { + Self::Deny } } fn join(self, other: Self) -> Self { - match self == Self::Deny { - true => Self::Deny, - false => self.min(other), + if self == Self::Deny { + Self::Deny + } else { + self.min(other) } } } impl<'a> StreamFilter<'a> { /// Create a new [`StreamFilter`] + #[must_use] pub fn new() -> Self { Self::default() } @@ -70,6 +75,7 @@ impl<'a> StreamFilter<'a> { /// /// This is a soft filter, so if there is no stream with a bitrate /// <= the limit, the stream with the next higher bitrate is returned. + #[must_use] pub fn audio_max_bitrate(mut self, max_bitrate: u32) -> Self { self.audio_max_bitrate = Some(max_bitrate); self @@ -83,6 +89,7 @@ impl<'a> StreamFilter<'a> { } /// Set the supported audio container formats + #[must_use] pub fn audio_formats(mut self, formats: &'a [AudioFormat]) -> Self { self.audio_formats = Some(formats); self @@ -96,6 +103,7 @@ impl<'a> StreamFilter<'a> { } /// Set the supported audio codecs + #[must_use] pub fn audio_codecs(mut self, codecs: &'a [AudioCodec]) -> Self { self.audio_codecs = Some(codecs); self @@ -114,6 +122,7 @@ impl<'a> StreamFilter<'a> { /// /// If this filter is unset or no stream matches, /// the filter returns the default audio stream. + #[must_use] pub fn audio_language(mut self, language: &'a str) -> Self { self.audio_language = Some(language); self @@ -123,10 +132,13 @@ impl<'a> StreamFilter<'a> { match &self.audio_language { Some(language) => match &stream.track { Some(track) => match &track.lang { - Some(track_lang) => match track_lang == language { - true => FilterResult::Match, - false => FilterResult::allow(track.is_default), - }, + Some(track_lang) => { + if track_lang == language { + FilterResult::Match + } else { + FilterResult::allow(track.is_default) + } + } None => FilterResult::allow(track.is_default), }, None => FilterResult::Match, @@ -140,6 +152,7 @@ impl<'a> StreamFilter<'a> { /// /// This is a soft filter, so if there is no stream with a resolution /// <= the limit, the stream with the next higher resolution is returned. + #[must_use] pub fn video_max_res(mut self, max_res: u32) -> Self { self.video_max_res = Some(max_res); self @@ -156,6 +169,7 @@ impl<'a> StreamFilter<'a> { /// /// This is a soft filter, so if there is no stream with a framerate /// <= the limit, the stream with the next higher framerate is returned. + #[must_use] pub fn video_max_fps(mut self, max_fps: u8) -> Self { self.video_max_fps = Some(max_fps); self @@ -169,6 +183,7 @@ impl<'a> StreamFilter<'a> { } /// Set the supported video container formats + #[must_use] pub fn video_formats(mut self, formats: &'a [VideoFormat]) -> Self { self.video_formats = Some(formats); self @@ -182,6 +197,7 @@ impl<'a> StreamFilter<'a> { } /// Set the supported video codecs + #[must_use] pub fn video_codecs(mut self, codecs: &'a [VideoCodec]) -> Self { self.video_codecs = Some(codecs); self @@ -195,6 +211,7 @@ impl<'a> StreamFilter<'a> { } /// Allow HDR videos + #[must_use] pub fn video_hdr(mut self) -> Self { self.video_hdr = true; self @@ -208,6 +225,7 @@ impl<'a> StreamFilter<'a> { } /// Output no video stream (audio only) + #[must_use] pub fn no_video(mut self) -> Self { self.video_none = true; self @@ -236,6 +254,7 @@ impl<'a> StreamFilter<'a> { impl VideoPlayer { /// Select the audio stream which is the best match for the given [`StreamFilter`] + #[must_use] pub fn select_audio_stream(&self, filter: &StreamFilter) -> Option<&AudioStream> { let mut fallback: Option<&AudioStream> = None; diff --git a/src/report.rs b/src/report.rs index 5fd99f4..1e1aa1f 100644 --- a/src/report.rs +++ b/src/report.rs @@ -37,13 +37,13 @@ const FILENAME_FORMAT: &[time::format_description::FormatItem] = /// RustyPipe error report #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] -pub struct Report { +pub struct Report<'a> { /// Information about the RustyPipe client - pub info: RustyPipeInfo, + pub info: RustyPipeInfo<'a>, /// Severity of the report pub level: Level, /// RustyPipe operation (e.g. `get_player`) - pub operation: String, + pub operation: &'a str, /// Error (if occurred) pub error: Option, /// Detailed error/warning messages @@ -52,17 +52,17 @@ pub struct Report { #[serde(skip_serializing_if = "Option::is_none")] pub deobf_data: Option, /// HTTP request data - pub http_request: HTTPRequest, + pub http_request: HTTPRequest<'a>, } /// Information about the RustyPipe client #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] -pub struct RustyPipeInfo { +pub struct RustyPipeInfo<'a> { /// Rust package name (`rustypipe`) - pub package: String, + pub package: &'a str, /// Package version (`0.1.0`) - pub version: String, + pub version: &'a str, /// Date/Time when the event occurred #[serde(with = "time::serde::rfc3339")] pub date: OffsetDateTime, @@ -71,13 +71,13 @@ pub struct RustyPipeInfo { /// Reported HTTP request data #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] -pub struct HTTPRequest { +pub struct HTTPRequest<'a> { /// Request URL - pub url: String, + pub url: &'a str, /// HTTP method - pub method: String, + pub method: &'a str, /// HTTP request header - pub req_header: BTreeMap, + pub req_header: BTreeMap<&'a str, String>, /// HTTP request body pub req_body: String, /// HTTP response status code @@ -98,11 +98,11 @@ pub enum Level { ERR, } -impl Default for RustyPipeInfo { +impl Default for RustyPipeInfo<'_> { fn default() -> Self { Self { - package: "rustypipe".to_owned(), - version: "0.1.0".to_owned(), + package: env!("CARGO_PKG_NAME"), + version: env!("CARGO_PKG_VERSION"), date: util::now_sec(), } } diff --git a/src/serializer/text.rs b/src/serializer/text.rs index 56f4a03..fcb6f83 100644 --- a/src/serializer/text.rs +++ b/src/serializer/text.rs @@ -349,15 +349,9 @@ impl From for crate::model::ArtistId { name: text, }, }, - TextComponent::Video { text, .. } => Self { - id: None, - name: text, - }, - TextComponent::Web { text, .. } => Self { - id: None, - name: text, - }, - TextComponent::Text { text } => Self { + TextComponent::Video { text, .. } + | TextComponent::Web { text, .. } + | TextComponent::Text { text } => Self { id: None, name: text, }, @@ -406,10 +400,10 @@ impl From for crate::model::richtext::RichText { impl TextComponent { pub fn as_str(&self) -> &str { match self { - TextComponent::Video { text, .. } => text, - TextComponent::Browse { text, .. } => text, - TextComponent::Web { text, .. } => text, - TextComponent::Text { text } => text, + TextComponent::Video { text, .. } + | TextComponent::Browse { text, .. } + | TextComponent::Web { text, .. } + | TextComponent::Text { text } => text, } } } @@ -417,7 +411,10 @@ impl TextComponent { impl TextComponents { /// Return the string representation of the first text component pub fn first_str(&self) -> &str { - self.0.first().map(|t| t.as_str()).unwrap_or_default() + self.0 + .first() + .map(TextComponent::as_str) + .unwrap_or_default() } /// Split the text components using the given separation string. @@ -440,7 +437,7 @@ impl TextComponents { } if !inner.is_empty() { - buf.push(TextComponents(inner)) + buf.push(TextComponents(inner)); } buf @@ -449,7 +446,7 @@ impl TextComponents { impl ToString for TextComponents { fn to_string(&self) -> String { - self.0.iter().map(|x| x.as_str()).collect::() + self.0.iter().map(TextComponent::as_str).collect::() } } diff --git a/src/util/dictionary.rs b/src/util/dictionary.rs index d708915..83d6c51 100644 --- a/src/util/dictionary.rs +++ b/src/util/dictionary.rs @@ -1,13 +1,17 @@ // This file is automatically generated. DO NOT EDIT. // See codegen/gen_dictionary.rs for the generation code. +#![allow(clippy::unreadable_literal)] + +//! The dictionary contains the information required to parse dates and numbers +//! in all supported languages. + use crate::{ model::AlbumType, param::Language, util::timeago::{DateCmp, TaToken, TimeUnit}, }; -/// The dictionary contains the information required to parse dates and numbers -/// in all supported languages. +/// Dictionary entry containing language-specific parsing information pub(crate) struct Entry { /// Tokens for parsing timeago strings. /// diff --git a/src/util/mod.rs b/src/util/mod.rs index a4cab18..b88292c 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -91,7 +91,7 @@ pub fn random_uuid() -> String { rng.gen::(), rng.gen::(), rng.gen::(), - rng.gen::() & 0xffffffffffff, + rng.gen::() & 0xffff_ffff_ffff, ) } @@ -315,10 +315,7 @@ where let dict_entry = dictionary::entry(lang); let by_char = lang_by_char(lang) || lang == Language::Ko; - let decimal_point = match dict_entry.comma_decimal { - true => ',', - false => '.', - }; + let decimal_point = if dict_entry.comma_decimal { ',' } else { '.' }; let mut digits = String::new(); let mut filtered = String::new(); @@ -345,14 +342,14 @@ where if digits.is_empty() { SplitTokens::new(&filtered, by_char) .find_map(|token| dict_entry.number_nd_tokens.get(token)) - .and_then(|n| (*n as u64).try_into().ok()) + .and_then(|n| (u64::from(*n)).try_into().ok()) } else { let num = digits.parse::().ok()?; exp += SplitTokens::new(&filtered, by_char) .filter_map(|token| match token { "k" => Some(3), - _ => dict_entry.number_tokens.get(token).map(|t| *t as i32), + _ => dict_entry.number_tokens.get(token).map(|t| i32::from(*t)), }) .sum::(); @@ -447,9 +444,10 @@ pub enum SplitTokens<'a> { impl<'a> SplitTokens<'a> { pub fn new(s: &'a str, by_char: bool) -> Self { - match by_char { - true => Self::Char(SplitChar::from(s)), - false => Self::Word(s.split_whitespace()), + if by_char { + Self::Char(SplitChar::from(s)) + } else { + Self::Word(s.split_whitespace()) } } } diff --git a/src/util/protobuf.rs b/src/util/protobuf.rs index 8c1b0d6..ade9388 100644 --- a/src/util/protobuf.rs +++ b/src/util/protobuf.rs @@ -33,8 +33,8 @@ impl ProtoBuilder { /// /// Reference: fn _field(&mut self, field: u32, wire: u8) { - let fbits: u64 = (field as u64) << 3; - let wbits = wire as u64 & 0x07; + let fbits = u64::from(field) << 3; + let wbits = u64::from(wire) & 0x07; let val: u64 = fbits | wbits; self._varint(val); } @@ -74,7 +74,7 @@ fn parse_varint>(pb: &mut P) -> Option { for b in pb.by_ref() { let value = b & 0x7f; - result |= (value as u64) << (7 * num_read); + result |= u64::from(value) << (7 * num_read); num_read += 1; if b & 0x80 == 0 { @@ -118,9 +118,8 @@ pub fn string_from_pb>(pb: P, field: u32) -> Option return None, }; diff --git a/src/util/timeago.rs b/src/util/timeago.rs index 9c2931d..be490b6 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -77,7 +77,7 @@ pub enum DateCmp { } impl TimeUnit { - pub fn secs(&self) -> i64 { + pub fn secs(self) -> i64 { match self { TimeUnit::Second => 1, TimeUnit::Minute => 60, @@ -91,7 +91,7 @@ impl TimeUnit { } impl TimeAgo { - fn secs(&self) -> i64 { + fn secs(self) -> i64 { i64::from(self.n) * self.unit.secs() } } @@ -117,8 +117,8 @@ impl From for OffsetDateTime { fn from(ta: TimeAgo) -> Self { let ts = util::now_sec(); match ta.unit { - TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -(ta.n as i32))), - TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -(ta.n as i32))), + TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(ta.n))), + TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(ta.n))), _ => ts - Duration::from(ta), } } @@ -156,9 +156,10 @@ struct TaTokenParser<'a> { impl<'a> TaTokenParser<'a> { fn new(entry: &'a dictionary::Entry, by_char: bool, nd: bool, filtered_str: &'a str) -> Self { - let tokens = match nd { - true => &entry.timeago_nd_tokens, - false => &entry.timeago_tokens, + let tokens = if nd { + &entry.timeago_nd_tokens + } else { + &entry.timeago_tokens }; Self { iter: SplitTokens::new(filtered_str, by_char), @@ -209,7 +210,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option { /// /// Returns [`None`] if the date could not be parsed. pub fn parse_timeago_dt(lang: Language, textual_date: &str) -> Option { - parse_timeago(lang, textual_date).map(|ta| ta.into()) + parse_timeago(lang, textual_date).map(OffsetDateTime::from) } pub fn parse_timeago_dt_or_warn( @@ -260,7 +261,7 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option Option Option { - parse_textual_date(lang, textual_date).map(|ta| ta.into()) + parse_textual_date(lang, textual_date).map(OffsetDateTime::from) } pub fn parse_textual_date_or_warn( diff --git a/src/validate.rs b/src/validate.rs index 009440a..ccaa58a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -11,7 +11,7 @@ //! - 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; +use crate::{error::Error, util}; use once_cell::sync::Lazy; use regex::Regex; @@ -22,12 +22,15 @@ use regex::Regex; /// # Examples /// ``` /// # use rustypipe::validate; -/// assert!(validate::video_id("dQw4w9WgXcQ")); -/// assert!(!validate::video_id("Abcd")); -/// assert!(!validate::video_id("dQw4w9WgXc@")); +/// assert!(validate::video_id("dQw4w9WgXcQ").is_ok()); +/// assert!(validate::video_id("Abcd").is_err()); +/// assert!(validate::video_id("dQw4w9WgXc@").is_err()); /// ``` -pub fn video_id>(video_id: S) -> bool { - util::VIDEO_ID_REGEX.is_match(video_id.as_ref()) +pub fn video_id>(video_id: S) -> Result<(), Error> { + check( + util::VIDEO_ID_REGEX.is_match(video_id.as_ref()), + "invalid video id", + ) } /// Validate the given channel ID @@ -38,12 +41,15 @@ pub fn video_id>(video_id: S) -> bool { /// # Examples /// ``` /// # use rustypipe::validate; -/// assert!(validate::channel_id("UC2DjFE7Xf11URZqWBigcVOQ")); -/// assert!(!validate::channel_id("Abcd")); -/// assert!(!validate::channel_id("XY2DjFE7Xf11URZqWBigcVOQ")); +/// assert!(validate::channel_id("UC2DjFE7Xf11URZqWBigcVOQ").is_ok()); +/// assert!(validate::channel_id("Abcd").is_err()); +/// assert!(validate::channel_id("XY2DjFE7Xf11URZqWBigcVOQ").is_err()); /// ``` -pub fn channel_id>(channel_id: S) -> bool { - util::CHANNEL_ID_REGEX.is_match(channel_id.as_ref()) +pub fn channel_id>(channel_id: S) -> Result<(), Error> { + check( + util::CHANNEL_ID_REGEX.is_match(channel_id.as_ref()), + "invalid channel id", + ) } /// Validate the given playlist ID @@ -55,14 +61,17 @@ pub fn channel_id>(channel_id: S) -> bool { /// # Examples /// ``` /// # use rustypipe::validate; -/// assert!(validate::playlist_id("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI")); -/// assert!(validate::playlist_id("RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")); -/// assert!(validate::playlist_id("OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE")); +/// assert!(validate::playlist_id("PL4lEESSgxM_5O81EvKCmBIm_JT5Q7JeaI").is_ok()); +/// assert!(validate::playlist_id("RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk").is_ok()); +/// assert!(validate::playlist_id("OLAK5uy_k0yFrZlFRgCf3rLPza-lkRmCrtLPbK9pE").is_ok()); /// -/// assert!(!validate::playlist_id("Abcd")); +/// assert!(validate::playlist_id("Abcd").is_err()); /// ``` -pub fn playlist_id>(playlist_id: S) -> bool { - util::PLAYLIST_ID_REGEX.is_match(playlist_id.as_ref()) +pub fn playlist_id>(playlist_id: S) -> Result<(), Error> { + check( + util::PLAYLIST_ID_REGEX.is_match(playlist_id.as_ref()), + "invalid playlist id", + ) } /// Validate the given album ID @@ -73,8 +82,8 @@ pub fn playlist_id>(playlist_id: S) -> bool { /// # Examples /// ``` /// # use rustypipe::validate; -/// assert!(validate::album_id("MPREb_GyH43gCvdM5")); -/// assert!(!validate::album_id("Abcd_GyH43gCvdM5")); +/// assert!(validate::album_id("MPREb_GyH43gCvdM5").is_ok()); +/// assert!(validate::album_id("Abcd_GyH43gCvdM5").is_err()); /// ``` /// /// # Note @@ -86,8 +95,11 @@ pub fn playlist_id>(playlist_id: S) -> bool { /// If you have the playlist ID of an album and need the album ID, you can use the /// [string resolver](crate::client::RustyPipeQuery::resolve_string) with the `resolve_albums` /// option enabled. -pub fn album_id>(album_id: S) -> bool { - util::ALBUM_ID_REGEX.is_match(album_id.as_ref()) +pub fn album_id>(album_id: S) -> Result<(), Error> { + check( + util::ALBUM_ID_REGEX.is_match(album_id.as_ref()), + "invalid album id", + ) } /// Validate the given radio ID @@ -107,15 +119,18 @@ pub fn album_id>(album_id: S) -> bool { /// /// ``` /// # use rustypipe::validate; -/// assert!(validate::radio_id("RDEMSuoM_jxfse1_g8uCO7MCtg")); -/// assert!(!validate::radio_id("Abcd")); -/// assert!(!validate::radio_id("XYEMSuoM_jxfse1_g8uCO7MCtg")); +/// assert!(validate::radio_id("RDEMSuoM_jxfse1_g8uCO7MCtg").is_ok()); +/// assert!(validate::radio_id("Abcd").is_err()); +/// assert!(validate::radio_id("XYEMSuoM_jxfse1_g8uCO7MCtg").is_err()); /// ``` -pub fn radio_id>(radio_id: S) -> bool { +pub fn radio_id>(radio_id: S) -> Result<(), Error> { static RADIO_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^RD[A-Za-z0-9_-]{22,50}$").unwrap()); - RADIO_ID_REGEX.is_match(radio_id.as_ref()) + check( + RADIO_ID_REGEX.is_match(radio_id.as_ref()), + "invalid radio id", + ) } /// Validate the given genre ID @@ -127,15 +142,18 @@ pub fn radio_id>(radio_id: S) -> bool { /// /// ``` /// # use rustypipe::validate; -/// assert!(validate::genre_id("ggMPOg1uX1JOQWZFeDByc2Jm")); -/// assert!(!validate::genre_id("Abcd")); -/// assert!(!validate::genre_id("ggAbcg1uX1JOQWZFeDByc2Jm")); +/// assert!(validate::genre_id("ggMPOg1uX1JOQWZFeDByc2Jm").is_ok()); +/// assert!(validate::genre_id("Abcd").is_err()); +/// assert!(validate::genre_id("ggAbcg1uX1JOQWZFeDByc2Jm").is_err()); /// ``` -pub fn genre_id>(genre_id: S) -> bool { +pub fn genre_id>(genre_id: S) -> Result<(), Error> { static GENRE_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^ggMPO[A-Za-z0-9_-]{19}$").unwrap()); - GENRE_ID_REGEX.is_match(genre_id.as_ref()) + check( + GENRE_ID_REGEX.is_match(genre_id.as_ref()), + "invalid genre id", + ) } /// Validate the given related tracks ID @@ -147,15 +165,18 @@ pub fn genre_id>(genre_id: S) -> bool { /// /// ``` /// # use rustypipe::validate; -/// assert!(validate::track_related_id("MPTRt_wrKjTn9hmry")); -/// assert!(!validate::track_related_id("Abcd")); -/// assert!(!validate::track_related_id("Abcdt_wrKjTn9hmry")); +/// assert!(validate::track_related_id("MPTRt_wrKjTn9hmry").is_ok()); +/// assert!(validate::track_related_id("Abcd").is_err()); +/// assert!(validate::track_related_id("Abcdt_wrKjTn9hmry").is_err()); /// ``` -pub fn track_related_id>(related_id: S) -> bool { +pub fn track_related_id>(related_id: S) -> Result<(), Error> { static RELATED_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^MPTRt_[A-Za-z0-9_-]{11}$").unwrap()); - RELATED_ID_REGEX.is_match(related_id.as_ref()) + check( + RELATED_ID_REGEX.is_match(related_id.as_ref()), + "invalid related track id", + ) } /// Validate the given lyrics ID @@ -167,13 +188,24 @@ pub fn track_related_id>(related_id: S) -> bool { /// /// ``` /// # use rustypipe::validate; -/// assert!(validate::track_lyrics_id("MPLYt_wrKjTn9hmry")); -/// assert!(!validate::track_lyrics_id("Abcd")); -/// assert!(!validate::track_lyrics_id("Abcdt_wrKjTn9hmry")); +/// assert!(validate::track_lyrics_id("MPLYt_wrKjTn9hmry").is_ok()); +/// assert!(validate::track_lyrics_id("Abcd").is_err()); +/// assert!(validate::track_lyrics_id("Abcdt_wrKjTn9hmry").is_err()); /// ``` -pub fn track_lyrics_id>(lyrics_id: S) -> bool { +pub fn track_lyrics_id>(lyrics_id: S) -> Result<(), Error> { static LYRICS_ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^MPLYt_[A-Za-z0-9_-]{11}$").unwrap()); - LYRICS_ID_REGEX.is_match(lyrics_id.as_ref()) + check( + LYRICS_ID_REGEX.is_match(lyrics_id.as_ref()), + "invalid lyrics id", + ) +} + +fn check(res: bool, msg: &'static str) -> Result<(), Error> { + if res { + Ok(()) + } else { + Err(Error::Other(msg.into())) + } } diff --git a/tests/youtube.rs b/tests/youtube.rs index f4dd7c9..3700d7a 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -53,7 +53,7 @@ fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) { assert_eq!(player_data.details.channel.name, "NoCopyrightSounds"); assert_gte(player_data.details.view_count, 146_818_808, "view count"); assert_eq!(player_data.details.keywords[0], "spektrem"); - assert_eq!(player_data.details.is_live_content, false); + assert!(!player_data.details.is_live_content); if client_type == ClientType::Ios { let video = player_data @@ -68,21 +68,21 @@ fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) { .unwrap(); // Bitrates may change between requests - assert_approx(video.bitrate as f64, 1507068.0); - assert_eq!(video.average_bitrate, 1345149); - assert_eq!(video.size.unwrap(), 43553412); + assert_approx(f64::from(video.bitrate), 1_507_068.0); + assert_eq!(video.average_bitrate, 1_345_149); + assert_eq!(video.size.unwrap(), 43_553_412); assert_eq!(video.width, 1280); assert_eq!(video.height, 720); assert_eq!(video.fps, 30); assert_eq!(video.quality, "720p"); - assert_eq!(video.hdr, false); + assert!(!video.hdr); assert_eq!(video.mime, "video/webm; codecs=\"vp09.00.31.08\""); assert_eq!(video.format, VideoFormat::Webm); assert_eq!(video.codec, VideoCodec::Vp9); - assert_approx(audio.bitrate as f64, 130685.0); - assert_approx(audio.average_bitrate as f64, 129496.0); - assert_approx(audio.size as f64, 4193863.0); + assert_approx(f64::from(audio.bitrate), 130_685.0); + assert_approx(f64::from(audio.average_bitrate), 129_496.0); + assert_approx(audio.size as f64, 4_193_863.0); assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\""); assert_eq!(audio.format, AudioFormat::M4a); assert_eq!(audio.codec, AudioCodec::Mp4a); @@ -101,26 +101,26 @@ fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) { .find(|s| s.itag == 251) .expect("audio stream not found"); - assert_approx(video.bitrate as f64, 1340829.0); - assert_approx(video.average_bitrate as f64, 1233444.0); - assert_approx(video.size.unwrap() as f64, 39936630.0); + assert_approx(f64::from(video.bitrate), 1_340_829.0); + assert_approx(f64::from(video.average_bitrate), 1_233_444.0); + assert_approx(video.size.unwrap() as f64, 39_936_630.0); assert_eq!(video.width, 1280); assert_eq!(video.height, 720); assert_eq!(video.fps, 30); assert_eq!(video.quality, "720p"); - assert_eq!(video.hdr, false); + assert!(!video.hdr); assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\""); assert_eq!(video.format, VideoFormat::Mp4); assert_eq!(video.codec, VideoCodec::Av01); - assert_eq!(video.throttled, false); + assert!(!video.throttled); - assert_approx(audio.bitrate as f64, 142718.0); - assert_approx(audio.average_bitrate as f64, 130708.0); - assert_approx(audio.size as f64, 4232344.0); + assert_approx(f64::from(audio.bitrate), 142_718.0); + assert_approx(f64::from(audio.average_bitrate), 130_708.0); + assert_approx(audio.size as f64, 4_232_344.0); assert_eq!(audio.mime, "audio/webm; codecs=\"opus\""); assert_eq!(audio.format, AudioFormat::Webm); assert_eq!(audio.codec, AudioCodec::Opus); - assert_eq!(audio.throttled, false); + assert!(!audio.throttled); check_video_stream(video); check_video_stream(audio); @@ -151,7 +151,7 @@ fn check_video_stream(s: impl YtStream) { 260, "UC2llNlEM62gU-_fXPHfgbDg", "Oonagh", - 830900, + 830_900, false, false )] @@ -873,7 +873,7 @@ fn channel_info(rp: RustyPipe) { assert_gte( channel.content.view_count.unwrap(), - 186854340, + 186_854_340, "channel views", ); @@ -1467,7 +1467,7 @@ fn music_artist( .for_each(|t| assert!(!t.avatar.is_empty())); // Sort albums to ensure consistent order - artist.albums.sort_by_key(|a| a.id.to_owned()); + artist.albums.sort_by_key(|a| a.id.clone()); if unlocalized { insta::assert_ron_snapshot!(format!("music_artist_{name}"), artist, { @@ -1944,19 +1944,19 @@ fn music_related(#[case] id: &str, #[case] full: bool, rp: RustyPipe) { let mut track_albums = 0; for track in related.tracks { - assert_video_id(&track.id); + validate::video_id(&track.id).unwrap(); assert!(!track.name.is_empty()); assert!(!track.cover.is_empty(), "got no cover"); if let Some(artist_id) = track.artist_id { - assert_channel_id(&artist_id); + validate::channel_id(&artist_id).unwrap(); track_artist_ids += 1; } let artist = track.artists.first().unwrap(); assert!(!artist.name.is_empty()); if let Some(artist_id) = &artist.id { - assert_channel_id(artist_id); + validate::channel_id(&artist_id).unwrap(); track_artists += 1; } @@ -1968,7 +1968,7 @@ fn music_related(#[case] id: &str, #[case] full: bool, rp: RustyPipe) { assert!(track.view_count.is_none()); if let Some(album) = track.album { - assert_album_id(&album.id); + validate::album_id(&album.id).unwrap(); assert!(!album.name.is_empty()); track_albums += 1; } @@ -1985,18 +1985,18 @@ fn music_related(#[case] id: &str, #[case] full: bool, rp: RustyPipe) { if full { assert_gte(related.albums.len(), 10, "albums"); for album in related.albums { - assert_album_id(&album.id); + validate::album_id(&album.id).unwrap(); assert!(!album.name.is_empty()); assert!(!album.cover.is_empty(), "got no cover"); let artist = album.artists.first().unwrap(); - assert_channel_id(artist.id.as_ref().unwrap()); + validate::channel_id(artist.id.as_ref().unwrap()).unwrap(); assert!(!artist.name.is_empty()); } assert_gte(related.artists.len(), 10, "artists"); for artist in related.artists { - assert_channel_id(&artist.id); + validate::channel_id(&artist.id).unwrap(); assert!(!artist.name.is_empty()); assert!(!artist.avatar.is_empty(), "got no avatar"); assert_gte(artist.subscriber_count.unwrap(), 5000, "subscribers") @@ -2004,7 +2004,7 @@ fn music_related(#[case] id: &str, #[case] full: bool, rp: RustyPipe) { assert_gte(related.playlists.len(), 10, "playlists"); for playlist in related.playlists { - assert_playlist_id(&playlist.id); + validate::playlist_id(&playlist.id).unwrap(); assert!(!playlist.name.is_empty()); assert!( !playlist.thumbnail.is_empty(), @@ -2018,7 +2018,7 @@ fn music_related(#[case] id: &str, #[case] full: bool, rp: RustyPipe) { playlist.id ); let channel = playlist.channel.unwrap(); - assert_channel_id(&channel.id); + validate::channel_id(&channel.id).unwrap(); assert!(!channel.name.is_empty()); } else { assert!(playlist.channel.is_none()); @@ -2134,7 +2134,7 @@ fn music_new_albums(rp: RustyPipe) { assert_gte(albums.len(), 10, "albums"); for album in albums { - assert_album_id(&album.id); + validate::album_id(&album.id).unwrap(); assert!(!album.name.is_empty()); assert!(!album.cover.is_empty(), "got no cover"); } @@ -2146,7 +2146,7 @@ fn music_new_videos(rp: RustyPipe) { assert_gte(videos.len(), 5, "videos"); for video in videos { - assert_video_id(&video.id); + validate::video_id(&video.id).unwrap(); assert!(!video.name.is_empty()); assert!(!video.cover.is_empty(), "got no cover"); assert_gte(video.view_count.unwrap(), 1000, "views"); @@ -2174,10 +2174,10 @@ fn music_genres(rp: RustyPipe, unlocalized: bool) { assert_eq!(pop.name, "Pop"); assert!(!pop.is_mood); - genres.iter().for_each(|g| { - assert!(validate::genre_id(&g.id)); - assert_gte(g.color, 0xff000000, "color"); - }); + for g in &genres { + validate::genre_id(&g.id).unwrap(); + assert_gte(g.color, 0xff00_0000, "color"); + } } #[rstest] @@ -2202,7 +2202,7 @@ fn music_genre(#[case] id: &str, #[case] name: &str, rp: RustyPipe, unlocalized: genre.sections.iter().for_each(|section| { assert!(!section.name.is_empty()); section.playlists.iter().for_each(|playlist| { - assert_playlist_id(&playlist.id); + validate::playlist_id(&playlist.id).unwrap(); assert!(!playlist.name.is_empty()); assert!(!playlist.thumbnail.is_empty(), "got no cover"); @@ -2213,14 +2213,14 @@ fn music_genre(#[case] id: &str, #[case] name: &str, rp: RustyPipe, unlocalized: playlist.id ); let channel = playlist.channel.as_ref().unwrap(); - assert_channel_id(&channel.id); + validate::channel_id(&channel.id).unwrap(); assert!(!channel.name.is_empty()); } else { assert!(playlist.channel.is_none()); } }); if let Some(subgenre_id) = §ion.subgenre_id { - subgenres.push((subgenre_id.to_owned(), section.name.to_owned())); + subgenres.push((subgenre_id.clone(), section.name.clone())); } }); subgenres @@ -2290,8 +2290,7 @@ fn invalid_ctoken(#[case] ep: ContinuationEndpoint, rp: RustyPipe) { fn lang() -> Language { std::env::var("YT_LANG") .ok() - .map(|l| Language::from_str(&l).unwrap()) - .unwrap_or(Language::En) + .map_or(Language::En, |l| Language::from_str(&l).unwrap()) } /// Get a new RustyPipe instance @@ -2362,22 +2361,6 @@ fn assert_next_items>( assert_gte(p.items.len(), n_items, "items"); } -fn assert_video_id(id: &str) { - assert!(validate::video_id(id), "invalid video id: `{id}`") -} - -fn assert_channel_id(id: &str) { - assert!(validate::channel_id(id), "invalid channel id: `{id}`"); -} - -fn assert_album_id(id: &str) { - assert!(validate::album_id(id), "invalid album id: `{id}`"); -} - -fn assert_playlist_id(id: &str) { - assert!(validate::playlist_id(id), "invalid playlist id: `{id}`"); -} - fn assert_frameset(frameset: &Frameset) { assert_gte(frameset.frame_height, 20, "frame height"); assert_gte(frameset.frame_height, 20, "frame width");