tests: run tests with different lang settings

fix: parsing subscriber count on channel search itms
fix: add warnings for all date and numstr parsing
fix: error parsing search suggestions
This commit is contained in:
ThetaDev 2023-05-04 21:44:10 +02:00
parent 6a99540ef5
commit b88faa9d05
32 changed files with 6501 additions and 214 deletions

View file

@ -179,8 +179,11 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
lang,
)?;
let mut mapper =
response::YouTubeListMapper::<VideoItem>::with_channel(lang, &channel_data);
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
lang,
&channel_data.c,
channel_data.warnings,
);
mapper.map_response(content.content);
let p = Paginator::new_ext(
None,
@ -191,7 +194,7 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
);
Ok(MapResult {
c: combine_channel_data(channel_data, p),
c: combine_channel_data(channel_data.c, p),
warnings: mapper.warnings,
})
}
@ -219,13 +222,16 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
lang,
)?;
let mut mapper =
response::YouTubeListMapper::<PlaylistItem>::with_channel(lang, &channel_data);
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
lang,
&channel_data.c,
channel_data.warnings,
);
mapper.map_response(content.content);
let p = Paginator::new(None, mapper.items, mapper.ctoken);
Ok(MapResult {
c: combine_channel_data(channel_data, p),
c: combine_channel_data(channel_data.c, p),
warnings: mapper.warnings,
})
}
@ -266,7 +272,7 @@ impl MapResponse<Channel<ChannelInfo>> for response::Channel {
});
Ok(MapResult {
c: combine_channel_data(channel_data, cinfo),
c: combine_channel_data(channel_data.c, cinfo),
warnings,
})
}
@ -297,7 +303,7 @@ fn map_channel(
d: MapChannelData,
id: &str,
lang: Language,
) -> Result<Channel<()>, ExtractionError> {
) -> Result<MapResult<Channel<()>>, ExtractionError> {
let header = d
.header
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
@ -326,33 +332,35 @@ fn map_channel(
.vanity_channel_url
.as_ref()
.and_then(|url| map_vanity_url(url, id));
let mut warnings = Vec::new();
Ok(match header {
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
id: metadata.external_id,
name: metadata.title,
subscriber_count: header
.subscriber_count_text
.and_then(|txt| util::parse_large_numstr(&txt, lang)),
avatar: header.avatar.into(),
verification: header.badges.into(),
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: header.banner.into(),
mobile_banner: header.mobile_banner.into(),
tv_banner: header.tv_banner.into(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
content: (),
},
response::channel::Header::CarouselHeaderRenderer(carousel) => {
let hdata = carousel
.contents
.into_iter()
.filter_map(|item| {
match item {
Ok(MapResult {
c: match header {
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
id: metadata.external_id,
name: metadata.title,
subscriber_count: header
.subscriber_count_text
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)),
avatar: header.avatar.into(),
verification: header.badges.into(),
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: header.banner.into(),
mobile_banner: header.mobile_banner.into(),
tv_banner: header.tv_banner.into(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
content: (),
},
response::channel::Header::CarouselHeaderRenderer(carousel) => {
let hdata = carousel
.contents
.into_iter()
.filter_map(|item| {
match item {
response::channel::CarouselHeaderRendererItem::TopicChannelDetailsRenderer {
subscriber_count_text,
subtitle,
@ -360,32 +368,33 @@ fn map_channel(
} => Some((subscriber_count_text.or(subtitle), avatar)),
response::channel::CarouselHeaderRendererItem::None => None,
}
})
.next();
})
.next();
Channel {
id: metadata.external_id,
name: metadata.title,
subscriber_count: hdata.as_ref().and_then(|hdata| {
hdata
.0
.as_ref()
.and_then(|txt| util::parse_large_numstr(txt, lang))
}),
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
verification: crate::model::Verification::Verified,
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: Vec::new(),
mobile_banner: Vec::new(),
tv_banner: Vec::new(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
content: (),
Channel {
id: metadata.external_id,
name: metadata.title,
subscriber_count: hdata.as_ref().and_then(|hdata| {
hdata.0.as_ref().and_then(|txt| {
util::parse_large_numstr_or_warn(txt, lang, &mut warnings)
})
}),
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
verification: crate::model::Verification::Verified,
description: metadata.description,
tags: microformat.microformat_data_renderer.tags,
vanity_url,
banner: Vec::new(),
mobile_banner: Vec::new(),
tv_banner: Vec::new(),
has_shorts: d.has_shorts,
has_live: d.has_live,
visitor_data: d.visitor_data,
content: (),
}
}
}
},
warnings,
})
}

View file

@ -269,7 +269,7 @@ fn map_artist_page(
}
}
let mapped = mapper.group_items();
let mut mapped = mapper.group_items();
static WIKIPEDIA_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\(?https://[a-z\d-]+\.wikipedia.org/wiki/[^\s]+").unwrap());
@ -302,9 +302,10 @@ fn map_artist_page(
description: header.description,
wikipedia_url,
subscriber_count: header.subscription_button.and_then(|btn| {
util::parse_large_numstr(
util::parse_large_numstr_or_warn(
&btn.subscribe_button_renderer.subscriber_count_text,
lang,
&mut mapped.warnings,
)
}),
tracks: mapped.c.tracks,

View file

@ -207,22 +207,25 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
response::music_item::PlaylistPanelVideo::None => None,
})
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
let track = map_queue_item(track_item, lang);
let mut track = map_queue_item(track_item, lang);
if track.id != id {
if track.c.id != id {
return Err(ExtractionError::WrongResult(format!(
"got wrong video id {}, expected {}",
track.id, id
track.c.id, id
)));
}
let mut warnings = content.contents.warnings;
warnings.append(&mut track.warnings);
Ok(MapResult {
c: TrackDetails {
track,
track: track.c,
lyrics_id,
related_id,
},
warnings: content.contents.warnings,
warnings,
})
}
}
@ -251,13 +254,17 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
.content
.playlist_panel_renderer;
let mut warnings = content.contents.warnings;
let tracks = content
.contents
.c
.into_iter()
.filter_map(|item| match item {
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
Some(map_queue_item(item, lang))
let mut track = map_queue_item(item, lang);
warnings.append(&mut track.warnings);
Some(track.c)
}
response::music_item::PlaylistPanelVideo::None => None,
})
@ -277,7 +284,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
None,
crate::model::paginator::ContinuationEndpoint::MusicNext,
),
warnings: content.contents.warnings,
warnings,
})
}
}

View file

@ -157,7 +157,9 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
mapper.add_warnings(&mut panel.contents.warnings);
panel.contents.c.into_iter().for_each(|item| {
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
mapper.add_item(MusicItem::Track(map_queue_item(item, lang)))
let mut track = map_queue_item(item, lang);
mapper.add_item(MusicItem::Track(track.c));
mapper.add_warnings(&mut track.warnings);
}
});
}

View file

@ -618,7 +618,11 @@ impl MusicListMapper {
(FlexColumnDisplayStyle::TwoLines, true) => (
None,
album_p.and_then(|p| {
util::parse_large_numstr(p.first_str(), self.lang)
util::parse_large_numstr_or_warn(
p.first_str(),
self.lang,
&mut self.warnings,
)
}),
),
(_, false) => (
@ -692,7 +696,11 @@ impl MusicListMapper {
match page_type {
MusicPageType::Artist => {
let subscriber_count = subtitle_p2.and_then(|p| {
util::parse_large_numstr(p.first_str(), self.lang)
util::parse_large_numstr_or_warn(
p.first_str(),
self.lang,
&mut self.warnings,
)
});
self.items.push(MusicItem::Artist(ArtistItem {
@ -792,7 +800,11 @@ impl MusicListMapper {
artists,
album: None,
view_count: subtitle_p2.and_then(|c| {
util::parse_large_numstr(c.first_str(), self.lang)
util::parse_large_numstr_or_warn(
c.first_str(),
self.lang,
&mut self.warnings,
)
}),
is_video,
track_nr: None,
@ -801,8 +813,13 @@ impl MusicListMapper {
Ok(Some(MusicItemType::Track))
}
MusicPageType::Artist => {
let subscriber_count = subtitle_p1
.and_then(|p| util::parse_large_numstr(p.first_str(), self.lang));
let subscriber_count = subtitle_p1.and_then(|p| {
util::parse_large_numstr_or_warn(
p.first_str(),
self.lang,
&mut self.warnings,
)
});
self.items.push(MusicItem::Artist(ArtistItem {
id,
@ -927,8 +944,13 @@ impl MusicListMapper {
let item_type = match card.on_tap.music_page() {
Some((page_type, id)) => match page_type {
MusicPageType::Artist => {
let subscriber_count = subtitle_p2
.and_then(|p| util::parse_large_numstr(p.first_str(), self.lang));
let subscriber_count = subtitle_p2.and_then(|p| {
util::parse_large_numstr_or_warn(
p.first_str(),
self.lang,
&mut self.warnings,
)
});
self.items.push(MusicItem::Artist(ArtistItem {
id,
@ -963,8 +985,13 @@ impl MusicListMapper {
let (album, view_count) = if is_video {
(
None,
subtitle_p3
.and_then(|p| util::parse_large_numstr(p.first_str(), self.lang)),
subtitle_p3.and_then(|p| {
util::parse_large_numstr_or_warn(
p.first_str(),
self.lang,
&mut self.warnings,
)
}),
)
} else {
(
@ -1149,7 +1176,8 @@ pub(crate) fn map_album_type(txt: &str, lang: Language) -> AlbumType {
.unwrap_or_default()
}
pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> TrackItem {
pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> MapResult<TrackItem> {
let mut warnings = Vec::new();
let mut subtitle_parts = item.long_byline_text.split(util::DOT_SEPARATOR).into_iter();
let is_video = !item
@ -1167,7 +1195,8 @@ pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> TrackItem
let (album, view_count) = if is_video {
(
None,
subtitle_p2.and_then(|p| util::parse_large_numstr(p.first_str(), lang)),
subtitle_p2
.and_then(|p| util::parse_large_numstr_or_warn(p.first_str(), lang, &mut warnings)),
)
} else {
(
@ -1176,20 +1205,23 @@ pub(crate) fn map_queue_item(item: QueueMusicItem, lang: Language) -> TrackItem
)
};
TrackItem {
id: item.video_id,
name: item.title,
duration: item
.length_text
.and_then(|txt| util::parse_video_length(&txt)),
cover: item.thumbnail.into(),
artists,
artist_id,
album,
view_count,
is_video,
track_nr: None,
by_va,
MapResult {
c: TrackItem {
id: item.video_id,
name: item.title,
duration: item
.length_text
.and_then(|txt| util::parse_video_length(&txt)),
cover: item.thumbnail.into(),
artists,
artist_id,
album,
view_count,
is_video,
track_nr: None,
by_va,
},
warnings,
}
}

View file

@ -1,4 +1,7 @@
use serde::{de::IgnoredAny, Deserialize};
use serde::{
de::{IgnoredAny, Visitor},
Deserialize,
};
use serde_with::{json::JsonString, serde_as};
use super::{video_item::YouTubeListRendererWrap, ResponseContext};
@ -26,8 +29,40 @@ pub(crate) struct TwoColumnSearchResultsRenderer {
}
#[derive(Debug, Deserialize)]
pub(crate) struct SearchSuggestion(
IgnoredAny,
pub Vec<(String, IgnoredAny, IgnoredAny)>,
IgnoredAny,
);
pub(crate) struct SearchSuggestion(IgnoredAny, pub Vec<SearchSuggestionItem>, IgnoredAny);
#[derive(Debug)]
pub(crate) struct SearchSuggestionItem(pub String);
impl<'de> Deserialize<'de> for SearchSuggestionItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ItemVisitor;
impl<'de> Visitor<'de> for ItemVisitor {
type Value = SearchSuggestionItem;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("search suggestion item")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
match seq.next_element::<String>()? {
Some(s) => {
// Ignore the rest of the list
while seq.next_element::<IgnoredAny>()?.is_some() {}
Ok(SearchSuggestionItem(s))
}
None => Err(serde::de::Error::invalid_length(0, &"1")),
}
}
}
deserializer.deserialize_seq(ItemVisitor)
}
}

View file

@ -536,6 +536,8 @@ pub(crate) struct CommentRenderer {
pub author_comment_badge: Option<AuthorCommentBadge>,
#[serde(default)]
pub reply_count: u64,
#[serde_as(as = "Option<Text>")]
pub vote_count: Option<String>,
/// Buttons for comment interaction (Like/Dislike/Reply)
pub action_buttons: CommentActionButtons,
}
@ -581,7 +583,6 @@ pub(crate) struct CommentActionButtons {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CommentActionButtonsRenderer {
pub like_button: ToggleButtonWrap,
pub creator_heart: Option<CreatorHeart>,
}

View file

@ -415,7 +415,7 @@ impl<T> YouTubeListMapper<T> {
}
}
pub fn with_channel<C>(lang: Language, channel: &Channel<C>) -> Self {
pub fn with_channel<C>(lang: Language, channel: &Channel<C>, warnings: Vec<String>) -> Self {
Self {
lang,
channel: Some(ChannelTag {
@ -426,7 +426,7 @@ impl<T> YouTubeListMapper<T> {
subscriber_count: channel.subscriber_count,
}),
items: Vec::new(),
warnings: Vec::new(),
warnings,
ctoken: None,
corrected_query: None,
channel_info: None,
@ -572,10 +572,12 @@ impl<T> YouTubeListMapper<T> {
name: channel.title,
avatar: channel.thumbnail.into(),
verification: channel.owner_badges.into(),
subscriber_count: sc_txt
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut self.warnings)),
video_count: vc_text
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut self.warnings)),
subscriber_count: sc_txt.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
video_count: vc_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, self.lang, &mut self.warnings)
}),
short_description: channel.description_snippet,
}
}

View file

@ -22,7 +22,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(582),
subscriber_count: Some(582000),
video_count: None,
short_description: "Music Submissions: https://monstafluff.edmdistrict.com/",
)),
@ -42,7 +42,7 @@ SearchResult(
),
],
verification: Artist,
subscriber_count: Some(403),
subscriber_count: Some(4030000),
video_count: None,
short_description: "Welcome to the official Music Travel Love YouTube channel! We travel the world making music, friends, videos and memories!",
)),
@ -62,7 +62,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(167),
subscriber_count: Some(167000),
video_count: None,
short_description: "MUSIC IN HARMONY WITH YOUR LIFE!!! If any producer, label, artist or photographer has an issue with any of the music or\u{a0}...",
)),
@ -82,7 +82,7 @@ SearchResult(
),
],
verification: Artist,
subscriber_count: Some(411),
subscriber_count: Some(411000),
video_count: None,
short_description: "The official YouTube channel of HAEVN Music. Receiving a piano from his grandfather had a great impact on Jorrit\'s life.",
)),
@ -102,7 +102,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(312),
subscriber_count: Some(31200),
video_count: None,
short_description: "Hello and welcome to \"Artemis Music\"! Music can play an effective role in helping us lead a better and more productive life.",
)),
@ -122,7 +122,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(372),
subscriber_count: Some(372000),
video_count: None,
short_description: "Music is the only language in which you cannot say a mean or sarcastic thing. Have fun listening to music.",
)),
@ -142,7 +142,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(178),
subscriber_count: Some(178000),
video_count: None,
short_description: "S!X - Music is an independent Hip-Hop label. Soundcloud : https://soundcloud.com/s1xmusic Facebook\u{a0}...",
)),
@ -162,7 +162,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(104),
subscriber_count: Some(1040000),
video_count: None,
short_description: "Welcome to Shake Music, a Trap & Bass Channel / Record Label dedicated to bringing you the best tracks. All tracks on Shake\u{a0}...",
)),
@ -182,7 +182,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(822),
subscriber_count: Some(822000),
video_count: None,
short_description: "Welcome to Miracle Music! On this channel you will find a wide variety of different Deep House, Tropical House, Chill Out, EDM,.",
)),
@ -202,7 +202,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(462),
subscriber_count: Some(4620000),
video_count: None,
short_description: "",
)),
@ -222,7 +222,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(105),
subscriber_count: Some(1050000),
video_count: None,
short_description: "BRINGING YOU ONLY THE BEST EDM - TRAP Submit your own track for promotion here:\u{a0}...",
)),
@ -242,7 +242,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(709),
subscriber_count: Some(709000),
video_count: None,
short_description: "Hey there! I am Mr MoMo My channel focus on Japan music, lofi, trap & bass type beat and Japanese instrumental. I mindfully\u{a0}...",
)),
@ -262,7 +262,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(544),
subscriber_count: Some(54400),
video_count: None,
short_description: "",
)),
@ -282,7 +282,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(359),
subscriber_count: Some(3590),
video_count: None,
short_description: "Welcome to our Energy Transformation Relaxing Music . This chakra music channel will focus on developing the best chakra\u{a0}...",
)),
@ -302,7 +302,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(416),
subscriber_count: Some(416000),
video_count: None,
short_description: "Nonstop Music - Home of 1h videos of your favourite songs and mixes. Nonstop Genres: Pop • Chillout • Tropical House • Deep\u{a0}...",
)),
@ -322,7 +322,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(3),
subscriber_count: Some(3000000),
video_count: None,
short_description: "Vibe Music strives to bring the best lyric videos of popular Rap & Hip Hop songs. Be sure to Subscribe to see new videos we\u{a0}...",
)),
@ -342,7 +342,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(120),
subscriber_count: Some(120000),
video_count: None,
short_description: "",
)),
@ -362,7 +362,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(817),
subscriber_count: Some(81700),
video_count: None,
short_description: "",
)),
@ -382,7 +382,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(53),
subscriber_count: Some(53000),
video_count: None,
short_description: "Welcome to my channel - Helios Music. I created this channel to help people have the most relaxing, refreshing and comfortable\u{a0}...",
)),
@ -402,7 +402,7 @@ SearchResult(
),
],
verification: None,
subscriber_count: Some(129),
subscriber_count: Some(129000),
video_count: None,
short_description: "Music On (UNOFFICIAL CHANNEL)",
)),

View file

@ -22,7 +22,7 @@ SearchResult(
),
],
verification: Verified,
subscriber_count: Some(292),
subscriber_count: Some(2920000),
video_count: Some(219),
short_description: "Hi, I\'m Tina, aka Doobydobap! Food is the medium I use to tell stories and connect with people who share the same passion as I\u{a0}...",
)),

View file

@ -191,9 +191,10 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
};
let comment_count = comment_count_section.and_then(|s| {
util::parse_large_numstr::<u64>(
util::parse_large_numstr_or_warn::<u64>(
&s.comments_entry_point_header_renderer.comment_count,
lang,
&mut warnings,
)
});
@ -331,9 +332,9 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
name: channel_name,
avatar: owner.thumbnail.into(),
verification: owner.badges.into(),
subscriber_count: owner
.subscriber_count_text
.and_then(|txt| util::parse_large_numstr(&txt, lang)),
subscriber_count: owner.subscriber_count_text.and_then(|txt| {
util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)
}),
},
view_count,
like_count,
@ -505,16 +506,16 @@ fn map_comment(
}),
_ => None,
},
publish_date: timeago::parse_timeago_to_dt(lang, &c.published_time_text),
publish_date_txt: c.published_time_text,
like_count: util::parse_numeric_or_warn(
&c.action_buttons
.comment_action_buttons_renderer
.like_button
.toggle_button_renderer
.accessibility_data,
publish_date: timeago::parse_timeago_dt_or_warn(
lang,
&c.published_time_text,
&mut warnings,
),
publish_date_txt: c.published_time_text,
like_count: match c.vote_count {
Some(txt) => util::parse_numeric_or_warn(&txt, &mut warnings),
None => Some(0),
},
reply_count: c.reply_count as u32,
replies: replies
.map(|items| Paginator::new(Some(c.reply_count), items, reply_ctoken))

View file

@ -198,7 +198,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
/// Parse a TimeAgo string (e.g. "29 minutes ago") into a Chrono DateTime object.
///
/// Returns None if the date could not be parsed.
pub fn parse_timeago_to_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
pub fn parse_timeago_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
parse_timeago(lang, textual_date).map(|ta| ta.into())
}
@ -219,7 +219,7 @@ pub(crate) fn parse_timeago_dt_or_warn(
textual_date: &str,
warnings: &mut Vec<String>,
) -> Option<OffsetDateTime> {
let res = parse_timeago_to_dt(lang, textual_date);
let res = parse_timeago_dt(lang, textual_date);
if res.is_none() {
warnings.push(format!("could not parse timeago `{textual_date}`"));
}

View file

@ -5571,17 +5571,19 @@ pub(crate) fn entry(lang: Language) -> Entry {
timeago_tokens: ::phf::Map {
key: 15467950696543387533,
disps: &[
(3, 5),
(0, 0),
(0, 1),
(2, 0),
],
entries: &[
("\u{e34}นาท\u{e35}\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Second) }),
("\u{e31}นท\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Day) }),
("นาท\u{e35}\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Minute) }),
("\u{e31}\u{e48}วโมงท\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Hour) }),
("\u{e31}ปดาห\u{e4c}\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Week) }),
("\u{e35}\u{e35}\u{e48}แล\u{e49}", TaToken { n: 1, unit: Some(TimeUnit::Year) }),
("เด\u{e37}อนท\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Month) }),
("\u{e31}\u{e48}วโมงท\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Hour) }),
("นาท\u{e35}", TaToken { n: 1, unit: Some(TimeUnit::Minute) }),
("\u{e31}นท\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Day) }),
("\u{e31}ปดาห\u{e4c}\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Week) }),
("\u{e34}นาท\u{e35}", TaToken { n: 1, unit: Some(TimeUnit::Second) }),
("\u{e34}นาท\u{e35}\u{e35}\u{e48}\u{e48}านมา", TaToken { n: 1, unit: Some(TimeUnit::Second) }),
],
},
date_order: &[DateCmp::D, DateCmp::Y],

View file

@ -328,6 +328,21 @@ where
.ok()
}
pub fn parse_large_numstr_or_warn<F>(
string: &str,
lang: Language,
warnings: &mut Vec<String>,
) -> Option<F>
where
F: TryFrom<u64>,
{
let res = parse_large_numstr::<F>(string, lang);
if res.is_none() {
warnings.push(format!("could not parse numstr `{string}`"));
}
res
}
/// Replace all html control characters to make a string safe for inserting into HTML.
pub fn escape_html(input: &str) -> String {
let mut buf = String::with_capacity(input.len());