fix: add pedantic lints

This commit is contained in:
ThetaDev 2023-05-13 02:40:26 +02:00
parent 81280200f7
commit cbeb14f3fd
41 changed files with 520 additions and 447 deletions

View file

@ -322,7 +322,7 @@ fn map_vanity_url(url: &str, id: &str) -> Option<String> {
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()
}

View file

@ -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<S: AsRef<str>>(&self, channel_id: S) -> Result<ChannelRss, Error> {
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,

View file

@ -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<P: Into<PathBuf>>(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<dyn CacheStorage>) -> 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<dyn Reporter>) -> 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<S: Into<String>>(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<S: Into<String>>(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<S: Into<String>>(mut self, visitor_data: Option<S>) -> 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<S: Into<String>>(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<S: Into<String>>(mut self, visitor_data: Option<S>) -> 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<String, Error> {
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(),

View file

@ -26,9 +26,10 @@ impl RustyPipeQuery {
all_albums: bool,
) -> Result<MusicArtist, Error> {
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(),
},
);

View file

@ -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<MusicPlaylist> for response::MusicPlaylist {
.split(|p| p == DOT_SEPARATOR)
.collect::<Vec<_>>();
parts
.get(if parts.len() > 2 { 1 } else { 0 })
.get(usize::from(parts.len() > 2))
.and_then(|txt| util::parse_numeric::<u64>(&txt[0]).ok())
})
} else {
@ -293,7 +293,7 @@ impl MapResponse<MusicAlbum> 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<MusicAlbum> 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<MusicAlbum> for response::MusicPlaylist {
by_va,
AlbumId {
id: id.to_owned(),
name: header.title.to_owned(),
name: header.title.clone(),
},
);
mapper.map_response(shelf.contents);

View file

@ -170,9 +170,10 @@ impl RustyPipeQuery {
) -> Result<MusicSearchFiltered<MusicPlaylistItem>, 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<MusicSearchResult> 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<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> 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 => {}

View file

@ -177,12 +177,12 @@ impl MapResponse<VideoPlayer> 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<VideoPlayer> 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<Option<VideoStream>> {
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<Option<AudioStream>> {
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 {

View file

@ -60,9 +60,8 @@ impl MapResponse<Playlist> for response::Playlist {
lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::DeobfData>,
) -> Result<MapResult<Playlist>, 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

View file

@ -87,11 +87,9 @@ impl From<ChannelRss> 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

View file

@ -349,7 +349,7 @@ impl From<Icon> for crate::model::Verification {
match icon.icon_type {
IconType::Check => Self::Verified,
IconType::OfficialArtistBadge => Self::Artist,
_ => Self::None,
IconType::Like => Self::None,
}
}
}

View file

@ -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<String> {
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<MusicItemMenuEntry>) -> Option<String> {

View file

@ -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<UrlTarget> {
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,
}

View file

@ -419,8 +419,8 @@ impl<T> YouTubeListMapper<T> {
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<T> YouTubeListMapper<T> {
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<YouTubeItem> {
.map(|url| (l.title, util::sanitize_yt_url(&url.url)))
})
.collect(),
})
});
}
YouTubeListItem::RichItemRenderer { content } => {
self.map_item(*content);
@ -701,7 +702,7 @@ impl YouTubeListMapper<PlaylistItem> {
match item {
YouTubeListItem::PlaylistRenderer(playlist) => {
let mapped = self.map_playlist(playlist);
self.items.push(mapped)
self.items.push(mapped);
}
YouTubeListItem::ContinuationItemRenderer {
continuation_endpoint,

View file

@ -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)

View file

@ -393,7 +393,7 @@ impl MapResponse<Paginator<Comment>> 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<Paginator<Comment>> 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::<VideoItem>::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(

View file

@ -238,7 +238,7 @@ fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
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)

View file

@ -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<reqwest::Error> 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(_)
)
)
}

View file

@ -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
//!

View file

@ -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),
}
}
}

View file

@ -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),
}
}

View file

@ -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,

View file

@ -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<Order>) -> 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<Feature>) -> 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<UploadDate>) -> 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<ItemType>) -> 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<Length>) -> 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()

View file

@ -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;

View file

@ -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<String>,
/// Detailed error/warning messages
@ -52,17 +52,17 @@ pub struct Report {
#[serde(skip_serializing_if = "Option::is_none")]
pub deobf_data: Option<DeobfData>,
/// 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<String, String>,
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(),
}
}

View file

@ -349,15 +349,9 @@ impl From<TextComponent> 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<TextComponents> 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::<String>()
self.0.iter().map(TextComponent::as_str).collect::<String>()
}
}

View file

@ -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.
///

View file

@ -91,7 +91,7 @@ pub fn random_uuid() -> String {
rng.gen::<u16>(),
rng.gen::<u16>(),
rng.gen::<u16>(),
rng.gen::<u64>() & 0xffffffffffff,
rng.gen::<u64>() & 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::<u64>().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::<i32>();
@ -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())
}
}
}

View file

@ -33,8 +33,8 @@ impl ProtoBuilder {
///
/// Reference: <https://developers.google.com/protocol-buffers/docs/encoding?hl=en#structure>
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<P: Iterator<Item = u8>>(pb: &mut P) -> Option<u64> {
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<P: IntoIterator<Item = u8>>(pb: P, field: u32) -> Option<S
buf.push(pb.next()?);
}
return String::from_utf8(buf).ok();
} else {
len
}
len
}
_ => return None,
};

View file

@ -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<TimeAgo> 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<TimeAgo> {
///
/// Returns [`None`] if the date could not be parsed.
pub fn parse_timeago_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
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<ParsedDa
// Chinese/Japanese dont use textual months
if m.is_none() && !by_char {
m = parse_textual_month(&entry, &filtered_str).map(|n| n as u16);
m = parse_textual_month(&entry, &filtered_str).map(u16::from);
}
match (y, m, d) {
@ -282,7 +283,7 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
///
/// Returns None if the date could not be parsed.
pub fn parse_textual_date_to_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
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(

View file

@ -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<S: AsRef<str>>(video_id: S) -> bool {
util::VIDEO_ID_REGEX.is_match(video_id.as_ref())
pub fn video_id<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(channel_id: S) -> bool {
util::CHANNEL_ID_REGEX.is_match(channel_id.as_ref())
pub fn channel_id<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(playlist_id: S) -> bool {
util::PLAYLIST_ID_REGEX.is_match(playlist_id.as_ref())
pub fn playlist_id<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(album_id: S) -> bool {
util::ALBUM_ID_REGEX.is_match(album_id.as_ref())
pub fn album_id<S: AsRef<str>>(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<S: AsRef<str>>(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<S: AsRef<str>>(radio_id: S) -> bool {
pub fn radio_id<S: AsRef<str>>(radio_id: S) -> Result<(), Error> {
static RADIO_ID_REGEX: Lazy<Regex> =
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<S: AsRef<str>>(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<S: AsRef<str>>(genre_id: S) -> bool {
pub fn genre_id<S: AsRef<str>>(genre_id: S) -> Result<(), Error> {
static GENRE_ID_REGEX: Lazy<Regex> =
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<S: AsRef<str>>(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<S: AsRef<str>>(related_id: S) -> bool {
pub fn track_related_id<S: AsRef<str>>(related_id: S) -> Result<(), Error> {
static RELATED_ID_REGEX: Lazy<Regex> =
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<S: AsRef<str>>(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<S: AsRef<str>>(lyrics_id: S) -> bool {
pub fn track_lyrics_id<S: AsRef<str>>(lyrics_id: S) -> Result<(), Error> {
static LYRICS_ID_REGEX: Lazy<Regex> =
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()))
}
}