diff --git a/README.md b/README.md index a768901..62df601 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # RustyPipe -[![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). @@ -9,25 +7,25 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). ### YouTube -- **Player** (video/audio streams, subtitles) -- **Playlist** -- **VideoDetails** (metadata, comments, recommended videos) -- **Channel** (videos, shorts, livestreams, playlists, info, search) -- **ChannelRSS** -- **Search** (with filters) -- **Search suggestions** -- **Trending** -- **URL resolver** +- [X] **Player** (video/audio streams, subtitles) +- [X] **Playlist** +- [X] **VideoDetails** (metadata, comments, recommended videos) +- [X] **Channel** (videos, shorts, livestreams, playlists, info, search) +- [X] **ChannelRSS** +- [X] **Search** (with filters) +- [X] **Search suggestions** +- [X] **Trending** +- [X] **URL resolver** ### YouTube Music -- **Playlist** -- **Album** -- **Artist** -- **Search** -- **Search suggestions** -- **Radio** -- **Track details** (lyrics, recommendations) -- **Moods/Genres** -- **Charts** -- **New** (albums, music videos) +- [X] **Playlist** +- [X] **Album** +- [X] **Artist** +- [X] **Search** +- [X] **Search suggestions** +- [X] **Radio** +- [X] **Track details** (lyrics, recommendations) +- [X] **Moods/Genres** +- [X] **Charts** +- [X] **New** diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index 2c8ae8a..e9d2eae 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -8,7 +8,7 @@ use crate::{ error::{Error, ExtractionError}, model::{AlbumItem, ArtistId, MusicArtist}, serializer::MapResult, - util, + util::{self, TryRemove}, }; use super::{ @@ -331,12 +331,9 @@ impl MapResponse> for response::MusicArtistAlbums { ) -> Result>, ExtractionError> { // dbg!(&self); - let grids = self - .contents - .single_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut content = self.contents.single_column_browse_results_renderer.contents; + let grids = content + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))? .tab_renderer .content diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 953a569..5369e42 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -125,17 +125,14 @@ impl MapResponse for response::MusicPlaylist { ) -> Result, ExtractionError> { // dbg!(&self); - let music_contents = self - .contents - .single_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut content = self.contents.single_column_browse_results_renderer.contents; + let mut music_contents = content + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))? .tab_renderer .content .section_list_renderer; - let shelf = music_contents + let mut shelf = music_contents .contents .into_iter() .find_map(|section| match section { @@ -160,8 +157,7 @@ impl MapResponse for response::MusicPlaylist { let ctoken = shelf .continuations - .into_iter() - .next() + .try_swap_remove(0) .map(|cont| cont.next_continuation_data.continuation); let track_count = if ctoken.is_some() { @@ -181,8 +177,7 @@ impl MapResponse for response::MusicPlaylist { let related_ctoken = music_contents .continuations - .into_iter() - .next() + .try_swap_remove(0) .map(|c| c.next_continuation_data.continuation); let (from_ytm, channel, name, thumbnail, description) = match self.header { @@ -274,12 +269,9 @@ impl MapResponse for response::MusicPlaylist { .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no header")))? .music_detail_header_renderer; - let sections = self - .contents - .single_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut content = self.contents.single_column_browse_results_renderer.contents; + let sections = content + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no content")))? .tab_renderer .content @@ -328,8 +320,7 @@ impl MapResponse for response::MusicPlaylist { let (artists, by_va) = map_artists(artists_p); let album_type_txt = subtitle_split - .into_iter() - .next() + .try_swap_remove(0) .map(|part| part.to_string()) .unwrap_or_default(); @@ -338,13 +329,12 @@ impl MapResponse for response::MusicPlaylist { let (artist_id, playlist_id) = header .menu - .map(|menu| { + .map(|mut menu| { ( map_artist_id(menu.menu_renderer.items), menu.menu_renderer .top_level_buttons - .into_iter() - .next() + .try_swap_remove(0) .map(|btn| { btn.button_renderer .navigation_endpoint diff --git a/src/client/music_search.rs b/src/client/music_search.rs index bddaf11..aeb98ba 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -10,6 +10,7 @@ use crate::{ MusicSearchFiltered, MusicSearchResult, MusicSearchSuggestion, TrackItem, }, serializer::MapResult, + util::TryRemove, }; use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext}; @@ -233,12 +234,9 @@ impl MapResponse for response::MusicSearch { ) -> Result, crate::error::ExtractionError> { // dbg!(&self); - let sections = self - .contents - .tabbed_search_results_renderer - .contents - .into_iter() - .next() + let mut tabs = self.contents.tabbed_search_results_renderer.contents; + let sections = tabs + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no tab")))? .tab_renderer .content @@ -264,8 +262,8 @@ impl MapResponse for response::MusicSearch { } } } - response::music_search::ItemSection::ItemSectionRenderer { contents } => { - if let Some(corrected) = contents.into_iter().next() { + response::music_search::ItemSection::ItemSectionRenderer { mut contents } => { + if let Some(corrected) = contents.try_swap_remove(0) { corrected_query = Some(corrected.showing_results_for_renderer.corrected_query) } } @@ -297,10 +295,9 @@ impl MapResponse> for response::MusicSearc ) -> Result>, ExtractionError> { // dbg!(&self); - let tabs = self.contents.tabbed_search_results_renderer.contents; + let mut tabs = self.contents.tabbed_search_results_renderer.contents; let sections = tabs - .into_iter() - .next() + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no tab")))? .tab_renderer .content @@ -312,17 +309,17 @@ impl MapResponse> for response::MusicSearc let mut mapper = MusicListMapper::new(lang); sections.into_iter().for_each(|section| match section { - response::music_search::ItemSection::MusicShelfRenderer(shelf) => { + response::music_search::ItemSection::MusicShelfRenderer(mut shelf) => { mapper.map_response(shelf.contents); - if let Some(cont) = shelf.continuations.into_iter().next() { + if let Some(cont) = shelf.continuations.try_swap_remove(0) { ctoken = Some(cont.next_continuation_data.continuation); } } response::music_search::ItemSection::MusicCardShelfRenderer(card) => { mapper.map_card(card); } - response::music_search::ItemSection::ItemSectionRenderer { contents } => { - if let Some(corrected) = contents.into_iter().next() { + response::music_search::ItemSection::ItemSectionRenderer { mut contents } => { + if let Some(corrected) = contents.try_swap_remove(0) { corrected_query = Some(corrected.showing_results_for_renderer.corrected_query) } } @@ -407,7 +404,7 @@ mod tests { #[case::default("default")] #[case::typo("typo")] #[case::radio("radio")] - #[case::artist("artist")] + #[case::radio("artist")] fn map_music_search_main(#[case] name: &str) { let json_path = path!(*TESTFILES / "music_search" / format!("main_{name}.json")); let json_file = File::open(json_path).unwrap(); diff --git a/src/client/pagination.rs b/src/client/pagination.rs index bfb07cd..42ee8d9 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -5,6 +5,7 @@ use crate::model::{ Comment, MusicItem, PlaylistVideo, YouTubeItem, }; use crate::serializer::MapResult; +use crate::util::TryRemove; use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}; use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery}; @@ -99,10 +100,9 @@ impl MapResponse> for response::Continuation { ) -> Result>, ExtractionError> { let items = self .on_response_received_actions - .and_then(|actions| { + .and_then(|mut actions| { actions - .into_iter() - .next() + .try_swap_remove(0) .map(|action| action.append_continuation_items_action.continuation_items) }) .or_else(|| { @@ -168,8 +168,7 @@ impl MapResponse> for response::MusicContinuation { let map_res = mapper.items(); let ctoken = continuations - .into_iter() - .next() + .try_swap_remove(0) .map(|cont| cont.next_continuation_data.continuation); Ok(MapResult { diff --git a/src/client/playlist.rs b/src/client/playlist.rs index 5fed815..2130f6a 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -65,11 +65,10 @@ impl MapResponse for response::Playlist { _ => return Err(response::alerts_to_err(self.alerts)), }; - let video_items = contents - .two_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut tcbr_contents = contents.two_column_browse_results_renderer.contents; + + let video_items = tcbr_contents + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed( "twoColumnBrowseResultsRenderer empty", )))? @@ -77,15 +76,13 @@ impl MapResponse for response::Playlist { .content .section_list_renderer .contents - .into_iter() - .next() + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed( "sectionListRenderer empty", )))? .item_section_renderer .contents - .into_iter() - .next() + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed( "itemSectionRenderer empty", )))? @@ -96,11 +93,10 @@ impl MapResponse for response::Playlist { let (thumbnails, last_update_txt) = match self.sidebar { Some(sidebar) => { - let sidebar_items = sidebar.playlist_sidebar_renderer.contents; + let mut sidebar_items = sidebar.playlist_sidebar_renderer.contents; let mut primary = sidebar_items - .into_iter() - .next() + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed( "no primary sidebar", )))?; diff --git a/src/client/response/music_item.rs b/src/client/response/music_item.rs index 7d9f6ea..b6bb9b6 100644 --- a/src/client/response/music_item.rs +++ b/src/client/response/music_item.rs @@ -11,7 +11,7 @@ use crate::{ text::{Text, TextComponents}, MapResult, }, - util::{self, dictionary}, + util::{self, dictionary, TryRemove}, }; use super::{ @@ -587,14 +587,14 @@ impl MusicListMapper { } } // Playlist item - FlexColumnDisplayStyle::Default => ( - c2.map(TextComponents::from), - c3.map(TextComponents::from), - item.fixed_columns - .into_iter() - .next() - .map(TextComponents::from), - ), + FlexColumnDisplayStyle::Default => { + let mut fixed_columns = item.fixed_columns; + ( + c2.map(TextComponents::from), + c3.map(TextComponents::from), + fixed_columns.try_swap_remove(0).map(TextComponents::from), + ) + } }; let duration = diff --git a/src/client/response/video_item.rs b/src/client/response/video_item.rs index 52f6491..29df448 100644 --- a/src/client/response/video_item.rs +++ b/src/client/response/video_item.rs @@ -477,7 +477,7 @@ impl YouTubeListMapper { is_upcoming: video.upcoming_event_data.is_some(), short_description: video .detailed_metadata_snippets - .and_then(|snippets| snippets.into_iter().next().map(|s| s.snippet_text)) + .and_then(|mut snippets| snippets.try_swap_remove(0).map(|s| s.snippet_text)) .or(video.description_snippet), } } diff --git a/src/client/trends.rs b/src/client/trends.rs index cc7408f..cc62032 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -5,6 +5,7 @@ use crate::{ model::{paginator::Paginator, VideoItem}, param::Language, serializer::MapResult, + util::TryRemove, }; use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery}; @@ -55,12 +56,9 @@ impl MapResponse> for response::Startpage { lang: crate::param::Language, _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { - let grid = self - .contents - .two_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut contents = self.contents.two_column_browse_results_renderer.contents; + let grid = contents + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))? .tab_renderer .content @@ -82,12 +80,9 @@ impl MapResponse> for response::Trending { lang: crate::param::Language, _deobf: Option<&crate::deobfuscate::DeobfData>, ) -> Result>, ExtractionError> { - let items = self - .contents - .two_column_browse_results_renderer - .contents - .into_iter() - .next() + let mut contents = self.contents.two_column_browse_results_renderer.contents; + let items = contents + .try_swap_remove(0) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))? .tab_renderer .content diff --git a/src/client/video_details.rs b/src/client/video_details.rs index 68754f1..0c20594 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -129,11 +129,11 @@ impl MapResponse for response::VideoDetails { } response::video_details::VideoResultsItem::ItemSectionRenderer(section) => { match section { - response::video_details::ItemSection::CommentsEntryPoint { contents } => { - comment_count_section = contents.into_iter().next(); + response::video_details::ItemSection::CommentsEntryPoint { mut contents } => { + comment_count_section = contents.try_swap_remove(0); } - response::video_details::ItemSection::CommentItemSection { contents } => { - comment_ctoken_section = contents.into_iter().next(); + response::video_details::ItemSection::CommentItemSection { mut contents } => { + comment_ctoken_section = contents.try_swap_remove(0); } response::video_details::ItemSection::None => {} } diff --git a/src/util/dictionary.rs b/src/util/dictionary.rs index e0049a8..7d2d2b3 100644 --- a/src/util/dictionary.rs +++ b/src/util/dictionary.rs @@ -393,14 +393,13 @@ pub(crate) fn entry(lang: Language) -> Entry { ], }, number_nd_tokens: ::phf::Map { - key: 12913932095322966823, + key: 15467950696543387533, disps: &[ (0, 0), ], entries: &[ - ("১", 1), ("ন\u{9be}ই", 0), - ("১ট\u{9be}", 1), + ("১", 1), ], }, album_types: ::phf::Map { @@ -4660,7 +4659,6 @@ pub(crate) fn entry(lang: Language) -> Entry { ], entries: &[ ("ingen", 0), - ("én", 1), ], }, album_types: ::phf::Map { @@ -5034,10 +5032,8 @@ pub(crate) fn entry(lang: Language) -> Entry { number_nd_tokens: ::phf::Map { key: 12913932095322966823, disps: &[ - (0, 0), ], entries: &[ - ("um", 1), ], }, album_types: ::phf::Map { diff --git a/src/util/mod.rs b/src/util/mod.rs index a91375f..b005caf 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -10,7 +10,7 @@ pub use protobuf::{string_from_pb, ProtoBuilder}; use std::{ borrow::{Borrow, Cow}, collections::BTreeMap, - str::{FromStr, SplitWhitespace}, + str::FromStr, }; use base64::Engine; @@ -331,18 +331,36 @@ 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()) + if by_char { + filtered + .chars() + .find_map(|c| dict_entry.number_nd_tokens.get(&c.to_string())) + .and_then(|n| (*n as u64).try_into().ok()) + } else { + filtered + .split_whitespace() + .find_map(|token| dict_entry.number_nd_tokens.get(token)) + .and_then(|n| (*n as u64).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), - }) - .sum::(); + let lookup_token = |token: &str| match token { + "k" => Some(3), + _ => dict_entry.number_tokens.get(token).map(|t| *t as i32), + }; + + if by_char { + exp += filtered + .chars() + .filter_map(|token| lookup_token(&token.to_string())) + .sum::(); + } else { + exp += filtered + .split_whitespace() + .filter_map(lookup_token) + .sum::(); + } F::try_from(num.checked_mul((10_u64).checked_pow(exp.try_into().ok()?)?)?).ok() } @@ -397,62 +415,6 @@ pub fn b64_decode>(input: T) -> Result, base64::DecodeErr base64::engine::general_purpose::STANDARD.decode(input) } -/// An iterator over the chars in a string (in str format) -pub struct SplitChar<'a> { - txt: &'a str, - index: usize, -} - -impl<'a> From<&'a str> for SplitChar<'a> { - fn from(value: &'a str) -> Self { - Self { - txt: value, - index: 0, - } - } -} - -impl<'a> Iterator for SplitChar<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - self.txt - .get(self.index..) - .and_then(|txt| txt.chars().next()) - .map(|c| { - let start = self.index; - self.index += c.len_utf8(); - &self.txt[start..self.index] - }) - } -} - -/// An iterator for parsing strings. It can either iterate over words or characters. -pub enum SplitTokens<'a> { - Word(SplitWhitespace<'a>), - Char(SplitChar<'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()), - } - } -} - -impl<'a> Iterator for SplitTokens<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - match self { - SplitTokens::Word(iter) => iter.next(), - SplitTokens::Char(iter) => iter.next(), - } - } -} - #[cfg(test)] pub(crate) mod tests { use std::{fs::File, io::BufReader, path::PathBuf}; @@ -588,22 +550,4 @@ pub(crate) mod tests { let res = parse_large_numstr::(string, lang).expect(&emsg); assert_eq!(res, rounded, "{emsg}"); } - - #[test] - fn split_char() { - let teststr = "abc今天更新def"; - let res = SplitTokens::new(teststr, true).collect::>(); - assert_eq!(res.len(), 10); - let res_str = res.into_iter().collect::(); - assert_eq!(res_str, teststr) - } - - #[test] - fn split_words() { - let teststr = "abc 今天更新 ghi"; - let res = SplitTokens::new(teststr, false).collect::>(); - assert_eq!(res.len(), 3); - let res_str = res.join(" "); - assert_eq!(res_str, teststr) - } } diff --git a/src/util/timeago.rs b/src/util/timeago.rs index f5c368e..a384d9a 100644 --- a/src/util/timeago.rs +++ b/src/util/timeago.rs @@ -17,7 +17,7 @@ use time::{Date, Duration, Month, OffsetDateTime}; use crate::{ param::Language, - util::{self, dictionary, SplitTokens}, + util::{self, dictionary}, }; /// Parsed TimeAgo string, contains amount and time unit. @@ -149,32 +149,21 @@ fn filter_str(string: &str) -> String { .collect() } -struct TaTokenParser<'a> { - iter: SplitTokens<'a>, - tokens: &'a phf::Map<&'static str, TaToken>, -} +fn parse_ta_token( + entry: &dictionary::Entry, + by_char: bool, + nd: bool, + filtered_str: &str, +) -> Option { + let tokens = match nd { + true => &entry.timeago_nd_tokens, + false => &entry.timeago_tokens, + }; + let mut qu = 1; -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, - }; - Self { - iter: SplitTokens::new(filtered_str, by_char), - tokens, - } - } -} - -impl<'a> Iterator for TaTokenParser<'a> { - type Item = TimeAgo; - - fn next(&mut self) -> Option { - // Quantity for parsing separate quantity + unit tokens - let mut qu = 1; - self.iter.find_map(|word| { - self.tokens.get(word).and_then(|t| match t.unit { + if by_char { + filtered_str.chars().find_map(|word| { + tokens.get(&word.to_string()).and_then(|t| match t.unit { Some(unit) => Some(TimeAgo { n: t.n * qu, unit }), None => { qu = t.n; @@ -182,6 +171,57 @@ impl<'a> Iterator for TaTokenParser<'a> { } }) }) + } else { + filtered_str.split_whitespace().find_map(|word| { + tokens.get(word).and_then(|t| match t.unit { + Some(unit) => Some(TimeAgo { n: t.n * qu, unit }), + None => { + qu = t.n; + None + } + }) + }) + } +} + +fn parse_ta_tokens( + entry: &dictionary::Entry, + by_char: bool, + nd: bool, + filtered_str: &str, +) -> Vec { + let tokens = match nd { + true => &entry.timeago_nd_tokens, + false => &entry.timeago_tokens, + }; + let mut qu = 1; + + if by_char { + filtered_str + .chars() + .filter_map(|word| { + tokens.get(&word.to_string()).and_then(|t| match t.unit { + Some(unit) => Some(TimeAgo { n: t.n * qu, unit }), + None => { + qu = t.n; + None + } + }) + }) + .collect() + } else { + filtered_str + .split_whitespace() + .filter_map(|word| { + tokens.get(word).and_then(|t| match t.unit { + Some(unit) => Some(TimeAgo { n: t.n * qu, unit }), + None => { + qu = t.n; + None + } + }) + }) + .collect() } } @@ -200,9 +240,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option { let qu: u8 = util::parse_numeric(textual_date).unwrap_or(1); - TaTokenParser::new(&entry, util::lang_by_char(lang), false, &filtered_str) - .next() - .map(|ta| ta * qu) + parse_ta_token(&entry, util::lang_by_char(lang), false, &filtered_str).map(|ta| ta * qu) } /// Parse a TimeAgo string (e.g. "29 minutes ago") into a Chrono DateTime object. @@ -235,14 +273,11 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option(textual_date); match nums.len() { - 0 => match TaTokenParser::new(&entry, by_char, true, &filtered_str).next() { + 0 => match parse_ta_token(&entry, by_char, true, &filtered_str) { Some(timeago) => Some(ParsedDate::Relative(timeago)), - None => TaTokenParser::new(&entry, by_char, false, &filtered_str) - .next() - .map(ParsedDate::Relative), + None => parse_ta_token(&entry, by_char, false, &filtered_str).map(ParsedDate::Relative), }, - 1 => TaTokenParser::new(&entry, by_char, false, &filtered_str) - .next() + 1 => parse_ta_token(&entry, by_char, false, &filtered_str) .map(|timeago| ParsedDate::Relative(timeago * nums[0] as u8)), 2..=3 => { if nums.len() == entry.date_order.len() { @@ -313,10 +348,12 @@ pub fn parse_video_duration(lang: Language, video_duration: &str) -> Option } else { part.digits.parse::().ok()? }; - let mut tokens = TaTokenParser::new(&entry, by_char, false, &part.word).peekable(); - tokens.peek()?; + let tokens = parse_ta_tokens(&entry, by_char, false, &part.word); + if tokens.is_empty() { + return None; + } - tokens.for_each(|ta| { + tokens.iter().for_each(|ta| { secs += n * ta.secs() as u32; n = 1; }); @@ -768,12 +805,4 @@ mod tests { let now = OffsetDateTime::now_utc(); assert_eq!(date.year(), now.year() - 1); } - - #[test] - fn tx() { - let s = "Abcdef"; - let lc: (usize, char) = s.char_indices().last().unwrap(); - let t = &s[(lc.0 + lc.1.len_utf8())..]; - dbg!(&t); - } } diff --git a/testfiles/dict/dictionary.json b/testfiles/dict/dictionary.json index 1f62381..8487db5 100644 --- a/testfiles/dict/dictionary.json +++ b/testfiles/dict/dictionary.json @@ -201,8 +201,7 @@ }, "number_nd_tokens": { "নাই": 0, - "১": 1, - "১টা": 1 + "১": 1 }, "album_types": { "ep": "Ep", @@ -2663,8 +2662,7 @@ "mrd": 9 }, "number_nd_tokens": { - "ingen": 0, - "én": 1 + "ingen": 0 }, "album_types": { "album": "Album", @@ -2887,9 +2885,7 @@ "mi": 6, "mil": 3 }, - "number_nd_tokens": { - "um": 1 - }, + "number_nd_tokens": {}, "album_types": { "audiolivro": "Audiobook", "ep": "Ep", diff --git a/testfiles/dict/dictionary_override.json b/testfiles/dict/dictionary_override.json index bc02b05..e162a96 100644 --- a/testfiles/dict/dictionary_override.json +++ b/testfiles/dict/dictionary_override.json @@ -16,9 +16,7 @@ "শঃ": null }, "number_nd_tokens": { - "কোনো": null, - "ভিডিঅ’": null, - "১টা": 1 + "কোনো": null } }, "bn": { @@ -113,8 +111,7 @@ }, "no": { "number_nd_tokens": { - "avspillinger": null, - "én": 1 + "avspillinger": null } }, "or": { @@ -132,11 +129,6 @@ "ਨੇ": null } }, - "pt": { - "number_nd_tokens": { - "um": 1 - } - }, "ro": { "number_nd_tokens": { "abonat": null, diff --git a/tests/youtube.rs b/tests/youtube.rs index 706c257..1d5abe5 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -1108,8 +1108,7 @@ fn search_empty(rp: RustyPipe) { fn search_suggestion(rp: RustyPipe) { let result = tokio_test::block_on(rp.query().search_suggestion("hunger ga")).unwrap(); - assert!(result.iter().any(|s| s.starts_with("hunger games "))); - assert_gte(result.len(), 10, "search suggestions"); + assert!(result.contains(&"hunger games".to_owned())); } #[rstest]