Compare commits
8 commits
main
...
feat/timez
Author | SHA1 | Date | |
---|---|---|---|
052eaf7e69 | |||
73bd9b34b1 | |||
137f6bac83 | |||
93eb3b4f04 | |||
f80a1bf054 | |||
e4e93ad0bd | |||
2528968131 | |||
e97df3e615 |
18 changed files with 236 additions and 79 deletions
|
@ -45,6 +45,7 @@ time = { version = "0.3.37", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"serde-human-readable",
|
"serde-human-readable",
|
||||||
"serde-well-known",
|
"serde-well-known",
|
||||||
|
"local-offset",
|
||||||
] }
|
] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
ress = "0.11.0"
|
ress = "0.11.0"
|
||||||
|
@ -54,6 +55,7 @@ data-encoding = "2.0.0"
|
||||||
urlencoding = "2.1.0"
|
urlencoding = "2.1.0"
|
||||||
quick-xml = { version = "0.37.0", features = ["serialize"] }
|
quick-xml = { version = "0.37.0", features = ["serialize"] }
|
||||||
tracing = { version = "0.1.0", features = ["log"] }
|
tracing = { version = "0.1.0", features = ["log"] }
|
||||||
|
localzone = "0.3.1"
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
indicatif = "0.17.0"
|
indicatif = "0.17.0"
|
||||||
|
@ -112,6 +114,7 @@ phf.workspace = true
|
||||||
data-encoding.workspace = true
|
data-encoding.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
localzone.workspace = true
|
||||||
quick-xml = { workspace = true, optional = true }
|
quick-xml = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
BIN
bg_snapshot.bin
Normal file
BIN
bg_snapshot.bin
Normal file
Binary file not shown.
|
@ -12,6 +12,7 @@ description = "CLI for RustyPipe - download videos and extract data from YouTube
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["native-tls"]
|
default = ["native-tls"]
|
||||||
|
timezone = ["dep:time", "dep:time-tz"]
|
||||||
|
|
||||||
# Reqwest TLS options
|
# Reqwest TLS options
|
||||||
native-tls = [
|
native-tls = [
|
||||||
|
@ -49,6 +50,8 @@ futures-util.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
quick-xml.workspace = true
|
quick-xml.workspace = true
|
||||||
|
time = { workspace = true, optional = true }
|
||||||
|
time-tz = { version = "2.0.0", optional = true }
|
||||||
|
|
||||||
indicatif.workspace = true
|
indicatif.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
@ -135,6 +135,12 @@ Fetch a list of all the items saved in your YouTube/YouTube Music profile.
|
||||||
`--auth` flag you can use authentication for any request.
|
`--auth` flag you can use authentication for any request.
|
||||||
- `--lang` Change the YouTube content language
|
- `--lang` Change the YouTube content language
|
||||||
- `--country` Change the YouTube content country
|
- `--country` Change the YouTube content country
|
||||||
|
- `--tz` Use a specific
|
||||||
|
[timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g.
|
||||||
|
Europe/Berlin, Australia/Sydney)
|
||||||
|
|
||||||
|
**Note:** this requires building rustypipe-cli with the `timezone` feature
|
||||||
|
- `--local-tz` Use the local timezone instead of UTC
|
||||||
- `--report` Generate a report on every request and store it in a `rustypipe_reports`
|
- `--report` Generate a report on every request and store it in a `rustypipe_reports`
|
||||||
folder in the current directory
|
folder in the current directory
|
||||||
- `--cache-file` Change the RustyPipe cache file location (Default:
|
- `--cache-file` Change the RustyPipe cache file location (Default:
|
||||||
|
|
|
@ -55,6 +55,13 @@ struct Cli {
|
||||||
/// YouTube content country
|
/// YouTube content country
|
||||||
#[clap(long, global = true)]
|
#[clap(long, global = true)]
|
||||||
country: Option<String>,
|
country: Option<String>,
|
||||||
|
/// Use a specific timezone (e.g. Europe/Berlin, Australia/Sydney)
|
||||||
|
#[cfg(feature = "timezone")]
|
||||||
|
#[clap(long, global = true)]
|
||||||
|
tz: Option<String>,
|
||||||
|
/// Use local timezone
|
||||||
|
#[clap(long, global = true)]
|
||||||
|
tz_local: bool,
|
||||||
/// Use authentication
|
/// Use authentication
|
||||||
#[clap(long, global = true)]
|
#[clap(long, global = true)]
|
||||||
auth: bool,
|
auth: bool,
|
||||||
|
@ -913,6 +920,23 @@ async fn run() -> anyhow::Result<()> {
|
||||||
if let Some(botguard_bin) = cli.botguard_bin {
|
if let Some(botguard_bin) = cli.botguard_bin {
|
||||||
rp = rp.botguard_bin(botguard_bin);
|
rp = rp.botguard_bin(botguard_bin);
|
||||||
}
|
}
|
||||||
|
if cli.tz_local {
|
||||||
|
rp = rp.timezone_local();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "timezone")]
|
||||||
|
if let Some(timezone) = cli.tz {
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use time_tz::{Offset, TimeZone};
|
||||||
|
|
||||||
|
let tz = time_tz::timezones::get_by_name(&timezone).expect("invalid timezone");
|
||||||
|
let offset = tz
|
||||||
|
.get_offset_utc(&OffsetDateTime::now_utc())
|
||||||
|
.to_utc()
|
||||||
|
.whole_minutes();
|
||||||
|
rp = rp.timezone(tz.name(), offset);
|
||||||
|
}
|
||||||
|
|
||||||
if cli.no_botguard {
|
if cli.no_botguard {
|
||||||
rp = rp.no_botguard();
|
rp = rp.no_botguard();
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,7 +280,7 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
|
|
||||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||||
// Channel info is always fetched in English. There is no localized data there
|
// Channel info is always fetched in English. There is no localized data
|
||||||
// and it allows parsing the country name.
|
// and it allows parsing the country name.
|
||||||
let lang = Language::En;
|
let lang = Language::En;
|
||||||
|
|
||||||
|
@ -335,7 +335,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
.video_count_text
|
.video_count_text
|
||||||
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
.and_then(|txt| util::parse_numeric_or_warn(&txt, &mut warnings)),
|
||||||
create_date: about.joined_date_text.and_then(|txt| {
|
create_date: about.joined_date_text.and_then(|txt| {
|
||||||
timeago::parse_textual_date_or_warn(lang, &txt, &mut warnings)
|
timeago::parse_textual_date_or_warn(lang, ctx.utc_offset, &txt, &mut warnings)
|
||||||
.map(OffsetDateTime::date)
|
.map(OffsetDateTime::date)
|
||||||
}),
|
}),
|
||||||
view_count: about
|
view_count: about
|
||||||
|
|
|
@ -181,6 +181,7 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::History {
|
||||||
mapper.map_response(contents);
|
mapper.map_response(contents);
|
||||||
mapper.conv_history_items(
|
mapper.conv_history_items(
|
||||||
header.map(|h| h.item_section_header_renderer.title),
|
header.map(|h| h.item_section_header_renderer.title),
|
||||||
|
ctx.utc_offset,
|
||||||
&mut map_res,
|
&mut map_res,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ use regex::Regex;
|
||||||
use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response, StatusCode};
|
use reqwest::{header, Client, ClientBuilder, Request, RequestBuilder, Response, StatusCode};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use time::OffsetDateTime;
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use tokio::sync::RwLock as AsyncRwLock;
|
use tokio::sync::RwLock as AsyncRwLock;
|
||||||
|
|
||||||
use crate::error::AuthError;
|
use crate::error::AuthError;
|
||||||
|
@ -83,7 +83,6 @@ pub enum ClientType {
|
||||||
/// Client used by the iOS app
|
/// Client used by the iOS app
|
||||||
///
|
///
|
||||||
/// - no obfuscated stream URLs
|
/// - no obfuscated stream URLs
|
||||||
/// - does not include opus audio streams
|
|
||||||
Ios,
|
Ios,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,6 +386,8 @@ struct RustyPipeRef {
|
||||||
struct RustyPipeOpts {
|
struct RustyPipeOpts {
|
||||||
lang: Language,
|
lang: Language,
|
||||||
country: Country,
|
country: Country,
|
||||||
|
timezone: Option<String>,
|
||||||
|
utc_offset_minutes: i16,
|
||||||
report: bool,
|
report: bool,
|
||||||
strict: bool,
|
strict: bool,
|
||||||
auth: Option<bool>,
|
auth: Option<bool>,
|
||||||
|
@ -526,6 +527,8 @@ impl Default for RustyPipeOpts {
|
||||||
Self {
|
Self {
|
||||||
lang: Language::En,
|
lang: Language::En,
|
||||||
country: Country::Us,
|
country: Country::Us,
|
||||||
|
timezone: None,
|
||||||
|
utc_offset_minutes: 0,
|
||||||
report: false,
|
report: false,
|
||||||
strict: false,
|
strict: false,
|
||||||
auth: None,
|
auth: None,
|
||||||
|
@ -891,6 +894,29 @@ impl RustyPipeBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the timezone and its associated UTC offset in minutes used
|
||||||
|
/// when accessing the YouTube API.
|
||||||
|
///
|
||||||
|
/// **Default value**: `0` (UTC)
|
||||||
|
///
|
||||||
|
/// **Info**: you can set this option for individual queries, too
|
||||||
|
#[must_use]
|
||||||
|
pub fn timezone<S: Into<String>>(mut self, timezone: S, utc_offset_minutes: i16) -> Self {
|
||||||
|
self.default_opts.timezone = Some(timezone.into());
|
||||||
|
self.default_opts.utc_offset_minutes = utc_offset_minutes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the YouTube API using the local system timezone
|
||||||
|
///
|
||||||
|
/// If the local timezone could not be determined, an error is logged and RustyPipe falls
|
||||||
|
/// back to UTC.
|
||||||
|
#[must_use]
|
||||||
|
pub fn timezone_local(self) -> Self {
|
||||||
|
let (timezone, utc_offset_minutes) = local_tz_offset();
|
||||||
|
self.timezone(timezone, utc_offset_minutes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a report on every operation.
|
/// Generate a report on every operation.
|
||||||
///
|
///
|
||||||
/// This should only be used for debugging.
|
/// This should only be used for debugging.
|
||||||
|
@ -1669,6 +1695,22 @@ impl RustyPipeQuery {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the timezone and its associated UTC offset in minutes used
|
||||||
|
/// when accessing the YouTube API.
|
||||||
|
#[must_use]
|
||||||
|
pub fn timezone<S: Into<String>>(mut self, timezone: S, utc_offset_minutes: i16) -> Self {
|
||||||
|
self.opts.timezone = Some(timezone.into());
|
||||||
|
self.opts.utc_offset_minutes = utc_offset_minutes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the YouTube API using the local system timezone
|
||||||
|
#[must_use]
|
||||||
|
pub fn timezone_local(self) -> Self {
|
||||||
|
let (timezone, utc_offset_minutes) = local_tz_offset();
|
||||||
|
self.timezone(timezone, utc_offset_minutes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a report on every operation.
|
/// Generate a report on every operation.
|
||||||
///
|
///
|
||||||
/// This should only be used for debugging.
|
/// This should only be used for debugging.
|
||||||
|
@ -1823,6 +1865,8 @@ impl RustyPipeQuery {
|
||||||
} else {
|
} else {
|
||||||
(Language::En, Country::Us)
|
(Language::En, Country::Us)
|
||||||
};
|
};
|
||||||
|
let utc_offset_minutes = self.opts.utc_offset_minutes;
|
||||||
|
let time_zone = self.opts.timezone.as_deref().unwrap_or("UTC");
|
||||||
|
|
||||||
match ctype {
|
match ctype {
|
||||||
ClientType::Desktop => YTContext {
|
ClientType::Desktop => YTContext {
|
||||||
|
@ -1834,6 +1878,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -1849,6 +1895,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -1864,6 +1912,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -1880,6 +1930,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -1899,6 +1951,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: None,
|
request: None,
|
||||||
|
@ -1916,6 +1970,8 @@ impl RustyPipeQuery {
|
||||||
visitor_data,
|
visitor_data,
|
||||||
hl,
|
hl,
|
||||||
gl,
|
gl,
|
||||||
|
time_zone,
|
||||||
|
utc_offset_minutes,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
request: None,
|
request: None,
|
||||||
|
@ -2213,6 +2269,8 @@ impl RustyPipeQuery {
|
||||||
let ctx = MapRespCtx {
|
let ctx = MapRespCtx {
|
||||||
id,
|
id,
|
||||||
lang: self.opts.lang,
|
lang: self.opts.lang,
|
||||||
|
utc_offset: UtcOffset::from_whole_seconds(i32::from(self.opts.utc_offset_minutes) * 60)
|
||||||
|
.map_err(|_| Error::Other("utc_offset overflow".into()))?,
|
||||||
deobf: ctx_src.deobf,
|
deobf: ctx_src.deobf,
|
||||||
visitor_data: Some(&visitor_data),
|
visitor_data: Some(&visitor_data),
|
||||||
client_type: ctype,
|
client_type: ctype,
|
||||||
|
@ -2499,6 +2557,7 @@ impl AsRef<RustyPipeQuery> for RustyPipeQuery {
|
||||||
struct MapRespCtx<'a> {
|
struct MapRespCtx<'a> {
|
||||||
id: &'a str,
|
id: &'a str,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
deobf: Option<&'a DeobfData>,
|
deobf: Option<&'a DeobfData>,
|
||||||
visitor_data: Option<&'a str>,
|
visitor_data: Option<&'a str>,
|
||||||
client_type: ClientType,
|
client_type: ClientType,
|
||||||
|
@ -2526,6 +2585,7 @@ impl<'a> MapRespCtx<'a> {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
lang: Language::En,
|
lang: Language::En,
|
||||||
|
utc_offset: UtcOffset::UTC,
|
||||||
deobf: None,
|
deobf: None,
|
||||||
visitor_data: None,
|
visitor_data: None,
|
||||||
client_type: ClientType::Desktop,
|
client_type: ClientType::Desktop,
|
||||||
|
@ -2564,6 +2624,19 @@ fn validate_country(country: Country) -> Country {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local_tz_offset() -> (String, i16) {
|
||||||
|
match (
|
||||||
|
localzone::get_local_zone().ok_or(Error::Other("could not get local timezone".into())),
|
||||||
|
UtcOffset::current_local_offset().map_err(|_| Error::Other("indeterminate offset".into())),
|
||||||
|
) {
|
||||||
|
(Ok(timezone), Ok(offset)) => (timezone, offset.whole_minutes()),
|
||||||
|
(Err(e), _) | (_, Err(e)) => {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
("UTC".to_owned(), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -160,7 +160,7 @@ impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicHistory {
|
||||||
};
|
};
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||||
mapper.map_response(shelf.contents);
|
mapper.map_response(shelf.contents);
|
||||||
mapper.conv_history_items(shelf.title, &mut map_res);
|
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctoken = contents
|
let ctoken = contents
|
||||||
|
|
|
@ -241,6 +241,7 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
|
||||||
mapper.map_response(contents);
|
mapper.map_response(contents);
|
||||||
mapper.conv_history_items(
|
mapper.conv_history_items(
|
||||||
header.map(|h| h.item_section_header_renderer.title),
|
header.map(|h| h.item_section_header_renderer.title),
|
||||||
|
ctx.utc_offset,
|
||||||
&mut map_res,
|
&mut map_res,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -280,7 +281,7 @@ impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicContinuat
|
||||||
let mut map_shelf = |shelf: response::music_item::MusicShelf| {
|
let mut map_shelf = |shelf: response::music_item::MusicShelf| {
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(ctx.lang);
|
||||||
mapper.map_response(shelf.contents);
|
mapper.map_response(shelf.contents);
|
||||||
mapper.conv_history_items(shelf.title, &mut map_res);
|
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
|
||||||
continuations.extend(shelf.continuations);
|
continuations.extend(shelf.continuations);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -937,6 +937,7 @@ mod tests {
|
||||||
|
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
use time::UtcOffset;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
|
use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
|
||||||
|
@ -968,6 +969,7 @@ mod tests {
|
||||||
.map_response(&MapRespCtx {
|
.map_response(&MapRespCtx {
|
||||||
id: "pPvd8UxmSbQ",
|
id: "pPvd8UxmSbQ",
|
||||||
lang: Language::En,
|
lang: Language::En,
|
||||||
|
utc_offset: UtcOffset::UTC,
|
||||||
deobf: Some(&DEOBF_DATA),
|
deobf: Some(&DEOBF_DATA),
|
||||||
visitor_data: None,
|
visitor_data: None,
|
||||||
client_type,
|
client_type,
|
||||||
|
|
|
@ -225,8 +225,13 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or(last_update_txt2.as_deref())
|
.or(last_update_txt2.as_deref())
|
||||||
.and_then(|txt| {
|
.and_then(|txt| {
|
||||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings)
|
timeago::parse_textual_date_or_warn(
|
||||||
.map(OffsetDateTime::date)
|
ctx.lang,
|
||||||
|
ctx.utc_offset,
|
||||||
|
txt,
|
||||||
|
&mut mapper.warnings,
|
||||||
|
)
|
||||||
|
.map(OffsetDateTime::date)
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
|
||||||
|
use time::UtcOffset;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
|
@ -1272,6 +1273,7 @@ impl MusicListMapper {
|
||||||
pub fn conv_history_items(
|
pub fn conv_history_items(
|
||||||
self,
|
self,
|
||||||
date_txt: Option<String>,
|
date_txt: Option<String>,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
res: &mut MapResult<Vec<HistoryItem<TrackItem>>>,
|
res: &mut MapResult<Vec<HistoryItem<TrackItem>>>,
|
||||||
) {
|
) {
|
||||||
res.warnings.extend(self.warnings);
|
res.warnings.extend(self.warnings);
|
||||||
|
@ -1282,7 +1284,12 @@ impl MusicListMapper {
|
||||||
.map(|item| HistoryItem {
|
.map(|item| HistoryItem {
|
||||||
item,
|
item,
|
||||||
playback_date: date_txt.as_deref().and_then(|s| {
|
playback_date: date_txt.as_deref().and_then(|s| {
|
||||||
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
|
timeago::parse_textual_date_to_d(
|
||||||
|
self.lang,
|
||||||
|
utc_offset,
|
||||||
|
s,
|
||||||
|
&mut res.warnings,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
playback_date_txt: date_txt.clone(),
|
playback_date_txt: date_txt.clone(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ use serde::Deserialize;
|
||||||
use serde_with::{
|
use serde_with::{
|
||||||
rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError,
|
rust::deserialize_ignore_any, serde_as, DefaultOnError, DisplayFromStr, VecSkipError,
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer,
|
ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, SimpleHeaderRenderer,
|
||||||
|
@ -786,7 +786,7 @@ impl<T> YouTubeListMapper<T> {
|
||||||
thumbnail: tn.image.into(),
|
thumbnail: tn.image.into(),
|
||||||
channel,
|
channel,
|
||||||
publish_date: publish_date_txt.as_deref().and_then(|t| {
|
publish_date: publish_date_txt.as_deref().and_then(|t| {
|
||||||
timeago::parse_textual_date_or_warn(self.lang, t, &mut self.warnings)
|
timeago::parse_timeago_dt_or_warn(self.lang, t, &mut self.warnings)
|
||||||
}),
|
}),
|
||||||
publish_date_txt,
|
publish_date_txt,
|
||||||
view_count,
|
view_count,
|
||||||
|
@ -907,17 +907,16 @@ impl YouTubeListMapper<VideoItem> {
|
||||||
pub(crate) fn conv_history_items(
|
pub(crate) fn conv_history_items(
|
||||||
self,
|
self,
|
||||||
date_txt: Option<String>,
|
date_txt: Option<String>,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
res: &mut MapResult<Vec<HistoryItem<VideoItem>>>,
|
res: &mut MapResult<Vec<HistoryItem<VideoItem>>>,
|
||||||
) {
|
) {
|
||||||
res.warnings.extend(self.warnings);
|
res.warnings.extend(self.warnings);
|
||||||
res.c.extend(self.items.into_iter().map(|item| {
|
res.c.extend(self.items.into_iter().map(|item| HistoryItem {
|
||||||
HistoryItem {
|
item,
|
||||||
item,
|
playback_date: date_txt.as_deref().and_then(|s| {
|
||||||
playback_date: date_txt.as_deref().and_then(|s| {
|
timeago::parse_textual_date_to_d(self.lang, utc_offset, s, &mut res.warnings)
|
||||||
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
|
}),
|
||||||
}),
|
playback_date_txt: date_txt.clone(),
|
||||||
playback_date_txt: date_txt.clone(),
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,12 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
// so we ignore parse errors here for now
|
// so we ignore parse errors here for now
|
||||||
like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
|
like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
|
||||||
date_text.as_deref().and_then(|txt| {
|
date_text.as_deref().and_then(|txt| {
|
||||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings)
|
timeago::parse_textual_date_or_warn(
|
||||||
|
ctx.lang,
|
||||||
|
ctx.utc_offset,
|
||||||
|
txt,
|
||||||
|
&mut warnings,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
date_text,
|
date_text,
|
||||||
view_count
|
view_count
|
||||||
|
|
|
@ -25,7 +25,8 @@ pub fn shift_years(date: Date, years: i32) -> Date {
|
||||||
shift_months(date, years * 12)
|
shift_months(date, years * 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shift_weeks_mo(date: Date, weeks: i32) -> Date {
|
/// Shift a date to the monday of its week, plus/minus the given amount of weeks
|
||||||
|
pub fn shift_weeks_monday(date: Date, weeks: i32) -> Date {
|
||||||
let d = date + Duration::weeks(weeks.into());
|
let d = date + Duration::weeks(weeks.into());
|
||||||
Date::from_iso_week_date(d.year(), d.iso_week(), time::Weekday::Monday).unwrap()
|
Date::from_iso_week_date(d.year(), d.iso_week(), time::Weekday::Monday).unwrap()
|
||||||
}
|
}
|
||||||
|
@ -40,3 +41,18 @@ pub fn now_sec() -> OffsetDateTime {
|
||||||
.replace_nanosecond(0)
|
.replace_nanosecond(0)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
use time::{macros::date, Date};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::this_week(date!(2025-01-17), 0, date!(2025-01-13))]
|
||||||
|
#[case::last_week(date!(2025-01-17), -1, date!(2025-01-06))]
|
||||||
|
#[case::last_month(date!(2025-01-17), -4, date!(2024-12-16))]
|
||||||
|
fn shift_weeks_monday(#[case] date: Date, #[case] weeks: i32, #[case] expect: Date) {
|
||||||
|
let res = super::shift_weeks_monday(date, weeks);
|
||||||
|
assert_eq!(res, expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod visitor_data;
|
||||||
pub mod dictionary;
|
pub mod dictionary;
|
||||||
pub mod timeago;
|
pub mod timeago;
|
||||||
|
|
||||||
pub use date::{now_sec, shift_months, shift_weeks_mo, shift_years};
|
pub use date::{now_sec, shift_months, shift_weeks_monday, shift_years};
|
||||||
pub use protobuf::{string_from_pb, ProtoBuilder};
|
pub use protobuf::{string_from_pb, ProtoBuilder};
|
||||||
pub use visitor_data::VisitorDataCache;
|
pub use visitor_data::VisitorDataCache;
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
use std::ops::Mul;
|
use std::ops::Mul;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::{Date, Duration, Month, OffsetDateTime};
|
use time::{Date, Duration, Month, OffsetDateTime, UtcOffset};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
param::Language,
|
param::Language,
|
||||||
|
@ -97,6 +97,26 @@ impl TimeAgo {
|
||||||
fn secs(self) -> u32 {
|
fn secs(self) -> u32 {
|
||||||
u32::from(self.n) * self.unit.secs()
|
u32::from(self.n) * self.unit.secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime {
|
||||||
|
let ts = util::now_sec().to_offset(utc_offset);
|
||||||
|
match self.unit {
|
||||||
|
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(self.n))),
|
||||||
|
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(self.n))),
|
||||||
|
TimeUnit::LastWeek => {
|
||||||
|
ts.replace_date(util::shift_weeks_monday(ts.date(), -i32::from(self.n)))
|
||||||
|
}
|
||||||
|
TimeUnit::LastWeekday => ts.replace_date(
|
||||||
|
Date::from_iso_week_date(
|
||||||
|
ts.year(),
|
||||||
|
ts.iso_week(),
|
||||||
|
time::Weekday::Monday.nth_next(self.n),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
_ => ts - Duration::from(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mul<u8> for TimeAgo {
|
impl Mul<u8> for TimeAgo {
|
||||||
|
@ -116,33 +136,11 @@ impl From<TimeAgo> for Duration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TimeAgo> for OffsetDateTime {
|
impl ParsedDate {
|
||||||
fn from(ta: TimeAgo) -> Self {
|
fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime {
|
||||||
let ts = util::now_sec();
|
match self {
|
||||||
match ta.unit {
|
ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_offset(utc_offset),
|
||||||
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(ta.n))),
|
ParsedDate::Relative(timeago) => timeago.into_datetime(utc_offset),
|
||||||
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(ta.n))),
|
|
||||||
TimeUnit::LastWeek => {
|
|
||||||
ts.replace_date(util::shift_weeks_mo(ts.date(), -i32::from(ta.n)))
|
|
||||||
}
|
|
||||||
TimeUnit::LastWeekday => ts.replace_date(
|
|
||||||
Date::from_iso_week_date(
|
|
||||||
ts.year(),
|
|
||||||
ts.iso_week(),
|
|
||||||
time::Weekday::Monday.nth_next(ta.n),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
_ => ts - Duration::from(ta),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParsedDate> for OffsetDateTime {
|
|
||||||
fn from(date: ParsedDate) -> Self {
|
|
||||||
match date {
|
|
||||||
ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_utc(),
|
|
||||||
ParsedDate::Relative(timeago) => timeago.into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +245,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the date could not be parsed.
|
/// Returns [`None`] if the date could not be parsed.
|
||||||
pub fn parse_timeago_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(OffsetDateTime::from)
|
parse_timeago(lang, textual_date).map(|t| t.into_datetime(UtcOffset::UTC))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_timeago_dt_or_warn(
|
pub fn parse_timeago_dt_or_warn(
|
||||||
|
@ -265,7 +263,11 @@ pub fn parse_timeago_dt_or_warn(
|
||||||
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
|
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the date could not be parsed.
|
/// Returns [`None`] if the date could not be parsed.
|
||||||
pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDate> {
|
pub fn parse_textual_date(
|
||||||
|
lang: Language,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
|
textual_date: &str,
|
||||||
|
) -> Option<ParsedDate> {
|
||||||
let entry = dictionary::entry(lang);
|
let entry = dictionary::entry(lang);
|
||||||
let by_char = util::lang_by_char(lang);
|
let by_char = util::lang_by_char(lang);
|
||||||
let filtered_str = filter_datestr(textual_date);
|
let filtered_str = filter_datestr(textual_date);
|
||||||
|
@ -317,8 +319,9 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|m| {
|
.and_then(|m| {
|
||||||
Date::from_calendar_date(
|
Date::from_calendar_date(
|
||||||
y.map(i32::from)
|
y.map(i32::from).unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| OffsetDateTime::now_utc().year()),
|
OffsetDateTime::now_utc().to_offset(utc_offset).year()
|
||||||
|
}),
|
||||||
m,
|
m,
|
||||||
d.unwrap_or(1) as u8,
|
d.unwrap_or(1) as u8,
|
||||||
)
|
)
|
||||||
|
@ -333,8 +336,12 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
|
||||||
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a OffsetDateTime object.
|
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a OffsetDateTime object.
|
||||||
///
|
///
|
||||||
/// Returns None if the date could not be parsed.
|
/// Returns None if the date could not be parsed.
|
||||||
pub fn parse_textual_date_to_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
|
pub fn parse_textual_date_to_dt(
|
||||||
parse_textual_date(lang, textual_date).map(OffsetDateTime::from)
|
lang: Language,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
|
textual_date: &str,
|
||||||
|
) -> Option<OffsetDateTime> {
|
||||||
|
parse_textual_date(lang, utc_offset, textual_date).map(|t| t.into_datetime(utc_offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object.
|
/// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object.
|
||||||
|
@ -342,18 +349,21 @@ pub fn parse_textual_date_to_dt(lang: Language, textual_date: &str) -> Option<Of
|
||||||
/// Returns None if the date could not be parsed.
|
/// Returns None if the date could not be parsed.
|
||||||
pub fn parse_textual_date_to_d(
|
pub fn parse_textual_date_to_d(
|
||||||
lang: Language,
|
lang: Language,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
textual_date: &str,
|
textual_date: &str,
|
||||||
warnings: &mut Vec<String>,
|
warnings: &mut Vec<String>,
|
||||||
) -> Option<Date> {
|
) -> Option<Date> {
|
||||||
parse_textual_date_or_warn(lang, textual_date, warnings).map(OffsetDateTime::date)
|
parse_textual_date_or_warn(lang, utc_offset, textual_date, warnings)
|
||||||
|
.map(|d| d.to_offset(utc_offset).date())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_textual_date_or_warn(
|
pub fn parse_textual_date_or_warn(
|
||||||
lang: Language,
|
lang: Language,
|
||||||
|
utc_offset: UtcOffset,
|
||||||
textual_date: &str,
|
textual_date: &str,
|
||||||
warnings: &mut Vec<String>,
|
warnings: &mut Vec<String>,
|
||||||
) -> Option<OffsetDateTime> {
|
) -> Option<OffsetDateTime> {
|
||||||
let res = parse_textual_date_to_dt(lang, textual_date);
|
let res = parse_textual_date_to_dt(lang, utc_offset, textual_date);
|
||||||
if res.is_none() {
|
if res.is_none() {
|
||||||
warnings.push(format!("could not parse textual date `{textual_date}`"));
|
warnings.push(format!("could not parse textual date `{textual_date}`"));
|
||||||
}
|
}
|
||||||
|
@ -864,7 +874,7 @@ mod tests {
|
||||||
for (t, entry) in entries {
|
for (t, entry) in entries {
|
||||||
entry.cases.iter().for_each(|(txt, n)| {
|
entry.cases.iter().for_each(|(txt, n)| {
|
||||||
let timeago = parse_timeago(*lang, txt);
|
let timeago = parse_timeago(*lang, txt);
|
||||||
let textual_date = parse_textual_date(*lang, txt);
|
let textual_date = parse_textual_date(*lang, UtcOffset::UTC, txt);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
timeago,
|
timeago,
|
||||||
Some(TimeAgo { n: *n, unit: *t }),
|
Some(TimeAgo { n: *n, unit: *t }),
|
||||||
|
@ -906,7 +916,7 @@ mod tests {
|
||||||
#[case] textual_date: &str,
|
#[case] textual_date: &str,
|
||||||
#[case] expect: Option<ParsedDate>,
|
#[case] expect: Option<ParsedDate>,
|
||||||
) {
|
) {
|
||||||
let parsed_date = parse_textual_date(lang, textual_date);
|
let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date);
|
||||||
assert_eq!(parsed_date, expect);
|
assert_eq!(parsed_date, expect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -917,7 +927,7 @@ mod tests {
|
||||||
#[case] textual_date: &str,
|
#[case] textual_date: &str,
|
||||||
#[case] expect: Date,
|
#[case] expect: Date,
|
||||||
) {
|
) {
|
||||||
let parsed_date = parse_textual_date(lang, textual_date);
|
let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date);
|
||||||
let expected_date = expect
|
let expected_date = expect
|
||||||
.replace_year(OffsetDateTime::now_utc().year())
|
.replace_year(OffsetDateTime::now_utc().year())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -933,7 +943,7 @@ mod tests {
|
||||||
|
|
||||||
for (lang, samples) in &date_samples {
|
for (lang, samples) in &date_samples {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Today").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Today").unwrap()),
|
||||||
Some(ParsedDate::Relative(TimeAgo {
|
Some(ParsedDate::Relative(TimeAgo {
|
||||||
n: 0,
|
n: 0,
|
||||||
unit: TimeUnit::Day
|
unit: TimeUnit::Day
|
||||||
|
@ -941,7 +951,7 @@ mod tests {
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Yesterday").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Yesterday").unwrap()),
|
||||||
Some(ParsedDate::Relative(TimeAgo {
|
Some(ParsedDate::Relative(TimeAgo {
|
||||||
n: 1,
|
n: 1,
|
||||||
unit: TimeUnit::Day
|
unit: TimeUnit::Day
|
||||||
|
@ -949,7 +959,7 @@ mod tests {
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Ago").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Ago").unwrap()),
|
||||||
Some(ParsedDate::Relative(TimeAgo {
|
Some(ParsedDate::Relative(TimeAgo {
|
||||||
n: 5,
|
n: 5,
|
||||||
unit: TimeUnit::Day
|
unit: TimeUnit::Day
|
||||||
|
@ -957,62 +967,62 @@ mod tests {
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Jan").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jan").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2020 - 1 - 3))),
|
Some(ParsedDate::Absolute(date!(2020 - 1 - 3))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Feb").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Feb").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2016 - 2 - 7))),
|
Some(ParsedDate::Absolute(date!(2016 - 2 - 7))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Mar").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Mar").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2015 - 3 - 9))),
|
Some(ParsedDate::Absolute(date!(2015 - 3 - 9))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Apr").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Apr").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2017 - 4 - 2))),
|
Some(ParsedDate::Absolute(date!(2017 - 4 - 2))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("May").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("May").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2014 - 5 - 22))),
|
Some(ParsedDate::Absolute(date!(2014 - 5 - 22))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Jun").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jun").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2014 - 6 - 28))),
|
Some(ParsedDate::Absolute(date!(2014 - 6 - 28))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Jul").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jul").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2014 - 7 - 2))),
|
Some(ParsedDate::Absolute(date!(2014 - 7 - 2))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Aug").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Aug").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2015 - 8 - 23))),
|
Some(ParsedDate::Absolute(date!(2015 - 8 - 23))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Sep").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Sep").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2018 - 9 - 16))),
|
Some(ParsedDate::Absolute(date!(2018 - 9 - 16))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Oct").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Oct").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2014 - 10 - 31))),
|
Some(ParsedDate::Absolute(date!(2014 - 10 - 31))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Nov").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Nov").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2016 - 11 - 3))),
|
Some(ParsedDate::Absolute(date!(2016 - 11 - 3))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(*lang, samples.get("Dec").unwrap()),
|
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Dec").unwrap()),
|
||||||
Some(ParsedDate::Absolute(date!(2021 - 12 - 24))),
|
Some(ParsedDate::Absolute(date!(2021 - 12 - 24))),
|
||||||
"lang: {lang}"
|
"lang: {lang}"
|
||||||
);
|
);
|
||||||
|
@ -1058,7 +1068,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_textual_date(lang, &v),
|
parse_textual_date(lang, UtcOffset::UTC, &v),
|
||||||
Some(expected),
|
Some(expected),
|
||||||
"lang={lang}; {k}"
|
"lang={lang}; {k}"
|
||||||
);
|
);
|
||||||
|
@ -1101,11 +1111,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn t_to_datetime() {
|
fn t_to_datetime() {
|
||||||
// Absolute date
|
// Absolute date
|
||||||
let date = parse_textual_date_to_dt(Language::En, "Last updated on Jan 3, 2020").unwrap();
|
let date =
|
||||||
|
parse_textual_date_to_dt(Language::En, UtcOffset::UTC, "Last updated on Jan 3, 2020")
|
||||||
|
.unwrap();
|
||||||
assert_eq!(date, datetime!(2020-1-3 0:00 +0));
|
assert_eq!(date, datetime!(2020-1-3 0:00 +0));
|
||||||
|
|
||||||
// Relative date
|
// Relative date
|
||||||
let date = parse_textual_date_to_dt(Language::En, "1 year ago").unwrap();
|
let date = parse_textual_date_to_dt(Language::En, UtcOffset::UTC, "1 year ago").unwrap();
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
assert_eq!(date.year(), now.year() - 1);
|
assert_eq!(date.year(), now.year() - 1);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue