diff --git a/crates/backend-embedded/src/batch/reindex_tracks.rs b/crates/backend-embedded/src/batch/reindex_tracks.rs index fc142c592..aa9aa41c8 100644 --- a/crates/backend-embedded/src/batch/reindex_tracks.rs +++ b/crates/backend-embedded/src/batch/reindex_tracks.rs @@ -3,7 +3,7 @@ use std::num::NonZeroU64; -use aoide_core::util::clock::DateTime; +use aoide_core::util::clock::OffsetDateTimeMs; use aoide_core_api::{ sorting::SortDirection, track::search::{SortField, SortOrder}, @@ -76,7 +76,7 @@ pub async fn reindex_tracks( #[allow(clippy::cast_possible_truncation)] let mut collector = EntityCollector::new(Vec::with_capacity(batch_size.get() as usize)); // Last timestamp to consider for updates - let mut last_updated_at: Option = None; + let mut last_updated_at: Option = None; connection.transaction::<_, anyhow::Error, _>(|connection| { 'batch_loop: loop { let pagination = Pagination { diff --git a/crates/core-api/src/filtering.rs b/crates/core-api/src/filtering.rs index 0e4e715c2..9788e8227 100644 --- a/crates/core-api/src/filtering.rs +++ b/crates/core-api/src/filtering.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; -use aoide_core::util::clock::DateTime; +use aoide_core::util::clock::OffsetDateTimeMs; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FilterModifier { @@ -136,4 +136,4 @@ pub type NumericValue = f64; pub type NumericPredicate = ScalarPredicate; -pub type DateTimePredicate = ScalarPredicate; +pub type DateTimePredicate = ScalarPredicate; diff --git a/crates/core-api/src/track/search.rs b/crates/core-api/src/track/search.rs index f1b54ee78..1a8471da2 100644 --- a/crates/core-api/src/track/search.rs +++ b/crates/core-api/src/track/search.rs @@ -7,7 +7,7 @@ use aoide_core::{ actor::{Kind as ActorKind, Role as ActorRole}, title::Kind as TitleKind, }, - util::clock::{DateOrDateTime, DateTime}, + util::clock::{DateOrDateTime, OffsetDateTimeMs}, PlaylistUid, TrackUid, }; use strum::FromRepr; @@ -58,7 +58,7 @@ pub enum ConditionFilter { pub type NumericFieldFilter = ScalarFieldFilter; -pub type DateTimeFieldFilter = ScalarFieldFilter; +pub type DateTimeFieldFilter = ScalarFieldFilter; #[derive(Clone, Debug, PartialEq, Eq)] pub struct PhraseFieldFilter { @@ -176,7 +176,7 @@ impl Filter { }), DateOrDateTime::Date(date) => Self::Numeric(NumericFieldFilter { field: NumericField::RecordedAtDate, - predicate: NumericPredicate::Equal(Some(date.to_inner().into())), + predicate: NumericPredicate::Equal(Some(date.value().into())), }), } } @@ -190,7 +190,7 @@ impl Filter { }), DateOrDateTime::Date(date) => Self::Numeric(NumericFieldFilter { field: NumericField::ReleasedAtDate, - predicate: NumericPredicate::Equal(Some(date.to_inner().into())), + predicate: NumericPredicate::Equal(Some(date.value().into())), }), } } @@ -204,7 +204,7 @@ impl Filter { }), DateOrDateTime::Date(date) => Self::Numeric(NumericFieldFilter { field: NumericField::ReleasedOrigAtDate, - predicate: NumericPredicate::Equal(Some(date.to_inner().into())), + predicate: NumericPredicate::Equal(Some(date.value().into())), }), } } diff --git a/crates/core-json/src/media/tests.rs b/crates/core-json/src/media/tests.rs index c6b5193a7..ee9812163 100644 --- a/crates/core-json/src/media/tests.rs +++ b/crates/core-json/src/media/tests.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::{media::content::ContentRevision, util::clock::DateTime}; +use aoide_core::{media::content::ContentRevision, util::clock::OffsetDateTimeMs}; use super::*; @@ -25,7 +25,7 @@ fn serde_digest() { #[test] fn deserialize_audio_source() { - let now = DateTime::now_local_or_utc(); + let now = OffsetDateTimeMs::now_local_or_utc(); let content_rev = ContentRevision::new(345); let json = serde_json::json!({ "collectedAt": now.to_string(), diff --git a/crates/core-json/src/playlist/tests.rs b/crates/core-json/src/playlist/tests.rs index b57011863..b50a6b86d 100644 --- a/crates/core-json/src/playlist/tests.rs +++ b/crates/core-json/src/playlist/tests.rs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later +use aoide_core::util::clock::OffsetDateTimeMs; + use super::*; #[test] @@ -14,8 +16,8 @@ fn serialize_item_default_separator() { #[test] fn deserialize_playlist() { let uid: EntityUid = "01AN4Z07BY79KA1307SR9X4MV3".parse().unwrap(); - let added_at1: aoide_core::util::clock::DateTime = "2020-12-18T21:27:15Z".parse().unwrap(); - let added_at2 = aoide_core::util::clock::DateTime::now_utc(); + let added_at1 = "2020-12-18T21:27:15Z".parse::().unwrap(); + let added_at2 = OffsetDateTimeMs::now_utc(); let playlist = PlaylistWithEntries { playlist: Playlist { title: "Title".to_string(), diff --git a/crates/core-json/src/util/clock/mod.rs b/crates/core-json/src/util/clock/mod.rs index 490f273ac..eebcb0a3c 100644 --- a/crates/core-json/src/util/clock/mod.rs +++ b/crates/core-json/src/util/clock/mod.rs @@ -5,7 +5,7 @@ use std::fmt; use aoide_core::{ prelude::*, - util::clock::{YearType, YYYYMMDD}, + util::clock::{YearType, YyyyMmDdDateValue}, }; use serde::{ de::{self, Visitor as SerdeDeserializeVisitor}, @@ -15,7 +15,7 @@ use serde::{ use crate::prelude::*; mod _core { - pub(super) use aoide_core::util::clock::{DateOrDateTime, DateTime, DateYYYYMMDD}; + pub(super) use aoide_core::util::clock::{DateOrDateTime, OffsetDateTimeMs, YyyyMmDdDate}; } /////////////////////////////////////////////////////////////////////// @@ -31,16 +31,16 @@ pub struct DateTime { feature = "json-schema", schemars(with = "chrono::DateTime") )] - inner: _core::DateTime, + inner: _core::OffsetDateTimeMs, } -impl From<_core::DateTime> for DateTime { - fn from(inner: _core::DateTime) -> Self { +impl From<_core::OffsetDateTimeMs> for DateTime { + fn from(inner: _core::OffsetDateTimeMs) -> Self { Self { inner } } } -impl From for _core::DateTime { +impl From for _core::OffsetDateTimeMs { fn from(from: DateTime) -> Self { let DateTime { inner } = from; inner @@ -63,45 +63,45 @@ impl<'de> Deserialize<'de> for DateTime { D: Deserializer<'de>, { time::serde::rfc3339::deserialize(deserializer) - .map(_core::DateTime::new) + .map(_core::OffsetDateTimeMs::clamp_from) .map(Into::into) } } /////////////////////////////////////////////////////////////////////// -// DateYYYYMMDD +// YyyyMmDdDate /////////////////////////////////////////////////////////////////////// #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[repr(transparent)] #[allow(clippy::upper_case_acronyms)] -pub struct DateYYYYMMDD(_core::DateYYYYMMDD); +pub struct YyyyMmDdDate(_core::YyyyMmDdDate); -impl From<_core::DateYYYYMMDD> for DateYYYYMMDD { - fn from(from: _core::DateYYYYMMDD) -> Self { +impl From<_core::YyyyMmDdDate> for YyyyMmDdDate { + fn from(from: _core::YyyyMmDdDate) -> Self { Self(from) } } -impl From for _core::DateYYYYMMDD { - fn from(from: DateYYYYMMDD) -> Self { +impl From for _core::YyyyMmDdDate { + fn from(from: YyyyMmDdDate) -> Self { from.0 } } #[cfg(feature = "json-schema")] -impl schemars::JsonSchema for DateYYYYMMDD { +impl schemars::JsonSchema for YyyyMmDdDate { fn schema_name() -> String { - "DateYYYYMMDD".to_string() + "YyyyMmDdDate".to_string() } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - gen.subschema_for::() + gen.subschema_for::() } } // Serialize (and deserialize) as string for maximum compatibility and portability -impl Serialize for DateYYYYMMDD { +impl Serialize for YyyyMmDdDate { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -109,20 +109,22 @@ impl Serialize for DateYYYYMMDD { let value = if self.0.is_year() { i32::from(self.0.year()) } else { - self.0.into() + self.0.value() }; serializer.serialize_i32(value) } } #[allow(clippy::upper_case_acronyms)] -struct DateYYYYMMDDDeserializeVisitor; +struct YyyyMmDdDateDeserializeVisitor; -impl<'de> SerdeDeserializeVisitor<'de> for DateYYYYMMDDDeserializeVisitor { - type Value = DateYYYYMMDD; +impl<'de> SerdeDeserializeVisitor<'de> for YyyyMmDdDateDeserializeVisitor { + type Value = YyyyMmDdDate; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_fmt(format_args!("4-digit YYYY or 8-digit YYYYMMDD integer")) + formatter.write_fmt(format_args!( + "4-digit YYYY or 8-digit YyyyMmDdDateValue integer" + )) } #[allow(clippy::cast_possible_truncation)] @@ -130,29 +132,29 @@ impl<'de> SerdeDeserializeVisitor<'de> for DateYYYYMMDDDeserializeVisitor { where E: de::Error, { - let value = value as YYYYMMDD; - let value = if value < _core::DateYYYYMMDD::MIN.into() - && value >= YYYYMMDD::from(_core::DateYYYYMMDD::MIN.year()) - && value <= YYYYMMDD::from(_core::DateYYYYMMDD::MAX.year()) + let value = value as YyyyMmDdDateValue; + let value = if value < _core::YyyyMmDdDate::MIN.value() + && value >= YyyyMmDdDateValue::from(_core::YyyyMmDdDate::MIN.year()) + && value <= YyyyMmDdDateValue::from(_core::YyyyMmDdDate::MAX.year()) { // Special case handling: YYYY - _core::DateYYYYMMDD::from_year(value as YearType) + _core::YyyyMmDdDate::from_year(value as YearType) } else { - _core::DateYYYYMMDD::new(value) + _core::YyyyMmDdDate::new_unchecked(value) }; value .validate() .map_err(|err| E::custom(format!("{err:?}"))) - .map(|()| DateYYYYMMDD(value)) + .map(|()| YyyyMmDdDate(value)) } } -impl<'de> Deserialize<'de> for DateYYYYMMDD { - fn deserialize(deserializer: D) -> Result +impl<'de> Deserialize<'de> for YyyyMmDdDate { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - deserializer.deserialize_u64(DateYYYYMMDDDeserializeVisitor) + deserializer.deserialize_u64(YyyyMmDdDateDeserializeVisitor) } } @@ -165,7 +167,7 @@ impl<'de> Deserialize<'de> for DateYYYYMMDD { #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(untagged)] pub enum DateOrDateTime { - Date(DateYYYYMMDD), + Date(YyyyMmDdDate), DateTime(DateTime), } diff --git a/crates/core-json/src/util/clock/tests.rs b/crates/core-json/src/util/clock/tests.rs index a62e4ee88..fb0a65911 100644 --- a/crates/core-json/src/util/clock/tests.rs +++ b/crates/core-json/src/util/clock/tests.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::util::clock::{DateYYYYMMDD, YEAR_MAX, YEAR_MIN}; +use aoide_core::util::clock::{YyyyMmDdDate, YEAR_MAX, YEAR_MIN}; use serde_json::json; use super::*; @@ -13,8 +13,8 @@ mod _core { #[test] fn deserialize_min() { assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::MIN), - serde_json::from_value::(json!(DateYYYYMMDD::MIN.to_inner())) + _core::DateOrDateTime::Date(YyyyMmDdDate::MIN), + serde_json::from_value::(json!(YyyyMmDdDate::MIN.value())) .unwrap() .into() ); @@ -23,8 +23,8 @@ fn deserialize_min() { #[test] fn deserialize_max() { assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::MAX), - serde_json::from_value::(json!(DateYYYYMMDD::MAX.to_inner())) + _core::DateOrDateTime::Date(YyyyMmDdDate::MAX), + serde_json::from_value::(json!(YyyyMmDdDate::MAX.value())) .unwrap() .into() ); @@ -33,31 +33,31 @@ fn deserialize_max() { #[test] fn deserialize_year() { assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::from_year(YEAR_MIN)), + _core::DateOrDateTime::Date(YyyyMmDdDate::from_year(YEAR_MIN)), serde_json::from_value::(json!(YEAR_MIN)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::from_year(YEAR_MAX)), + _core::DateOrDateTime::Date(YyyyMmDdDate::from_year(YEAR_MAX)), serde_json::from_value::(json!(YEAR_MAX)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_960_000)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_960_000)), serde_json::from_value::(json!(1996)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_960_000)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_960_000)), serde_json::from_value::(json!(19_960_000)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_960_900)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_960_900)), serde_json::from_value::(json!(19_960_900)) .unwrap() .into() @@ -70,34 +70,43 @@ fn deserialize_year() { #[test] fn serialize_min() { - assert!(DateYYYYMMDD::MIN.is_year()); + assert!(YyyyMmDdDate::MIN.is_year()); assert_eq!( - serde_json::to_string(&DateOrDateTime::Date(DateYYYYMMDD::MIN.into())).unwrap(), + serde_json::to_string(&DateOrDateTime::Date(YyyyMmDdDate::MIN.into())).unwrap(), serde_json::to_string(&json!(YEAR_MIN)).unwrap() ); } #[test] fn serialize_max() { - assert!(!DateYYYYMMDD::MAX.is_year()); + assert!(!YyyyMmDdDate::MAX.is_year()); assert_eq!( - serde_json::to_string(&DateOrDateTime::Date(DateYYYYMMDD::MAX.into())).unwrap(), - serde_json::to_string(&json!(DateYYYYMMDD::MAX.to_inner())).unwrap() + serde_json::to_string(&DateOrDateTime::Date(YyyyMmDdDate::MAX.into())).unwrap(), + serde_json::to_string(&json!(YyyyMmDdDate::MAX.value())).unwrap() ); } #[test] fn serialize_year() { assert_eq!( - serde_json::to_string(&DateOrDateTime::Date(DateYYYYMMDD::new(19_960_000).into())).unwrap(), + serde_json::to_string(&DateOrDateTime::Date( + YyyyMmDdDate::new_unchecked(19_960_000).into() + )) + .unwrap(), serde_json::to_string(&json!(1996)).unwrap() ); assert_eq!( - serde_json::to_string(&DateOrDateTime::Date(DateYYYYMMDD::new(19_961_000).into())).unwrap(), + serde_json::to_string(&DateOrDateTime::Date( + YyyyMmDdDate::new_unchecked(19_961_000).into() + )) + .unwrap(), serde_json::to_string(&json!(19_961_000)).unwrap() ); assert_eq!( - serde_json::to_string(&DateOrDateTime::Date(DateYYYYMMDD::new(19_961_031).into())).unwrap(), + serde_json::to_string(&DateOrDateTime::Date( + YyyyMmDdDate::new_unchecked(19_961_031).into() + )) + .unwrap(), serde_json::to_string(&json!(19_961_031)).unwrap() ); } @@ -105,13 +114,13 @@ fn serialize_year() { #[test] fn deserialize_year_month() { assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_960_100)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_960_100)), serde_json::from_value::(json!(19_960_100)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_961_200)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_961_200)), serde_json::from_value::(json!(19_961_200)) .unwrap() .into() @@ -124,13 +133,13 @@ fn deserialize_year_month() { #[test] fn deserialize_year_month_day() { assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_960_101)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_960_101)), serde_json::from_value::(json!(19_960_101)) .unwrap() .into() ); assert_eq!( - _core::DateOrDateTime::Date(DateYYYYMMDD::new(19_961_231)), + _core::DateOrDateTime::Date(YyyyMmDdDate::new_unchecked(19_961_231)), serde_json::from_value::(json!(19_961_231)) .unwrap() .into() diff --git a/crates/core/src/media/mod.rs b/crates/core/src/media/mod.rs index 8c4f552bf..ab136fd3b 100644 --- a/crates/core/src/media/mod.rs +++ b/crates/core/src/media/mod.rs @@ -68,7 +68,7 @@ pub struct Content { #[derive(Clone, Debug, PartialEq)] pub struct Source { - pub collected_at: DateTime, + pub collected_at: OffsetDateTimeMs, pub content: Content, diff --git a/crates/core/src/playlist/mod.rs b/crates/core/src/playlist/mod.rs index d328c9f37..72c1fae7e 100644 --- a/crates/core/src/playlist/mod.rs +++ b/crates/core/src/playlist/mod.rs @@ -84,7 +84,7 @@ impl Validate for Item { pub struct Entry { /// Time stamp added when this entry is part of the playlist, /// i.e. when it has been created and added. - pub added_at: DateTime, + pub added_at: OffsetDateTimeMs, /// Optional title for display. pub title: Option, @@ -221,7 +221,7 @@ pub struct PlaylistWithEntries { impl PlaylistWithEntries { #[must_use] - pub fn entries_added_at_minmax(&self) -> Option<(DateTime, DateTime)> { + pub fn entries_added_at_minmax(&self) -> Option<(OffsetDateTimeMs, OffsetDateTimeMs)> { let mut entries = self.entries.iter(); if let Some(first_added) = entries.next().map(|e| e.added_at) { let mut added_min = first_added; @@ -346,7 +346,7 @@ impl From<(Entity, Vec)> for EntityWithEntries { pub struct EntriesSummary { pub total_count: usize, - pub added_at_minmax: Option<(DateTime, DateTime)>, + pub added_at_minmax: Option<(OffsetDateTimeMs, OffsetDateTimeMs)>, pub tracks: TracksSummary, } diff --git a/crates/core/src/track/mod.rs b/crates/core/src/track/mod.rs index 486ffe52d..d489749d9 100644 --- a/crates/core/src/track/mod.rs +++ b/crates/core/src/track/mod.rs @@ -275,7 +275,7 @@ impl IsCanonical for Track { pub struct EntityBody { pub track: Track, - pub updated_at: DateTime, + pub updated_at: OffsetDateTimeMs, /// Last synchronized track entity revision /// @@ -317,6 +317,6 @@ pub type PlayCount = u64; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct PlayCounter { - pub last_played_at: Option, + pub last_played_at: Option, pub times_played: Option, } diff --git a/crates/core/src/util/clock/mod.rs b/crates/core/src/util/clock/mod.rs index 81e2f9ff8..032f424eb 100644 --- a/crates/core/src/util/clock/mod.rs +++ b/crates/core/src/util/clock/mod.rs @@ -11,110 +11,136 @@ use time::{ use crate::prelude::*; -pub type DateTimeInner = OffsetDateTime; - pub type TimestampMillis = i64; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] -pub struct DateTime(DateTimeInner); +pub struct OffsetDateTimeMs(OffsetDateTime); const NANOS_PER_MILLISECOND: i128 = 1_000_000; const YYYY_MM_DD_FORMAT: &[FormatItem<'static>] = time::macros::format_description!("[year]-[month]-[day]"); -/// A `DateTime` with truncated millisecond precision. -impl DateTime { +/// An [`OffsetDateTime`] with truncated millisecond precision. +impl OffsetDateTimeMs { #[must_use] - pub fn new(inner: DateTimeInner) -> Self { + pub const fn new_unchecked(inner: OffsetDateTime) -> Self { + Self(inner) + } + + #[must_use] + pub fn clamp_from(inner: OffsetDateTime) -> Self { let subsec_nanos_since_last_millis_boundary = inner.unix_timestamp_nanos() % NANOS_PER_MILLISECOND; let subsec_duration_since_last_millis_boundary = Duration::nanoseconds(subsec_nanos_since_last_millis_boundary as i64); - let truncated = inner - subsec_duration_since_last_millis_boundary; - debug_assert_eq!(0, truncated.unix_timestamp_nanos() % NANOS_PER_MILLISECOND); - Self(truncated) + let truncated = Self::new_unchecked(inner - subsec_duration_since_last_millis_boundary); + debug_assert!(truncated.is_valid()); + truncated } #[must_use] - pub fn new_timestamp_millis(timestamp_millis: TimestampMillis) -> Self { - DateTimeInner::from_unix_timestamp_nanos( + pub fn from_timestamp_millis(timestamp_millis: TimestampMillis) -> Self { + let truncated = OffsetDateTime::from_unix_timestamp_nanos( i128::from(timestamp_millis) * NANOS_PER_MILLISECOND, ) - .expect("valid timestamp") - .into() - } - - #[must_use] - pub const fn to_inner(self) -> DateTimeInner { - let Self(inner) = self; - inner + .map(Self::clamp_from) + .expect("valid timestamp"); + debug_assert!(truncated.is_valid()); + truncated } #[must_use] pub fn now_utc() -> Self { - DateTimeInner::now_utc().into() + Self::clamp_from(OffsetDateTime::now_utc()) } #[must_use] pub fn now_local_or_utc() -> Self { - DateTimeInner::now_local().map_or_else(|_: IndeterminateOffset| Self::now_utc(), Into::into) + OffsetDateTime::now_local() + .map_or_else(|_: IndeterminateOffset| Self::now_utc(), Self::clamp_from) } #[must_use] pub fn timestamp_millis(self) -> TimestampMillis { - (self.to_inner().unix_timestamp_nanos() / NANOS_PER_MILLISECOND) as TimestampMillis + (self.0.unix_timestamp_nanos() / NANOS_PER_MILLISECOND) as TimestampMillis } #[must_use] pub fn year(&self) -> YearType { self.0.year() as _ } + + #[must_use] + pub fn is_valid(&self) -> bool { + ::is_valid(self) + } } -impl AsRef for DateTime { - fn as_ref(&self) -> &DateTimeInner { +impl AsRef for OffsetDateTimeMs { + fn as_ref(&self) -> &OffsetDateTime { &self.0 } } -impl From for DateTime { - fn from(from: DateTimeInner) -> Self { - Self::new(from) +impl From for OffsetDateTime { + fn from(from: OffsetDateTimeMs) -> Self { + let OffsetDateTimeMs(into) = from; + into } } -impl From for DateTimeInner { - fn from(from: DateTime) -> Self { - from.to_inner() +impl From for OffsetDateTimeMs { + fn from(from: OffsetDateTime) -> Self { + Self::clamp_from(from) } } -impl From for DateTime { +impl From for OffsetDateTimeMs { fn from(from: SystemTime) -> Self { - Self::new(from.into()) + OffsetDateTime::from(from).into() } } -impl From for SystemTime { - fn from(from: DateTime) -> Self { - from.to_inner().into() +impl From for SystemTime { + fn from(from: OffsetDateTimeMs) -> Self { + OffsetDateTime::from(from).into() } } -impl FromStr for DateTime { +impl FromStr for OffsetDateTimeMs { type Err = ParseError; fn from_str(input: &str) -> Result { - DateTimeInner::parse(input, &Rfc3339).map(Into::into) + OffsetDateTime::parse(input, &Rfc3339).map(Into::into) } } -impl fmt::Display for DateTime { +impl fmt::Display for OffsetDateTimeMs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: Avoid allocation of temporary String? - f.write_str(&self.to_inner().format(&Rfc3339).expect("valid timestamp")) + f.write_str(&self.0.format(&Rfc3339).expect("valid timestamp")) + } +} + +#[derive(Copy, Clone, Debug)] +#[allow(clippy::upper_case_acronyms)] +pub enum OffsetDateTimeMsInvalidity { + /// Higher precision than expected + Unclamped, +} + +impl Validate for OffsetDateTimeMs { + type Invalidity = OffsetDateTimeMsInvalidity; + + fn validate(&self) -> ValidationResult { + ValidationContext::new() + .invalidate_if( + self.0.unix_timestamp_nanos() % NANOS_PER_MILLISECOND != 0, + Self::Invalidity::Unclamped, + ) + .into() } } @@ -130,29 +156,27 @@ pub type DayOfMonthType = i8; pub const YEAR_MIN: YearType = 1; pub const YEAR_MAX: YearType = 9999; -// 8-digit year+month+day (YYYYMMDD) -#[allow(clippy::upper_case_acronyms)] -pub type YYYYMMDD = i32; +pub type YyyyMmDdDateValue = i32; +/// 8-digit year+month+day (YYYYMMDD) #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[allow(clippy::upper_case_acronyms)] #[repr(transparent)] -pub struct DateYYYYMMDD(YYYYMMDD); +pub struct YyyyMmDdDate(YyyyMmDdDateValue); -impl DateYYYYMMDD { +impl YyyyMmDdDate { pub const MIN: Self = Self(10_000); pub const MAX: Self = Self(99_991_231); #[must_use] - pub const fn new(val: YYYYMMDD) -> Self { - Self(val) + pub const fn new_unchecked(value: YyyyMmDdDateValue) -> Self { + Self(value) } #[must_use] - pub const fn to_inner(self) -> YYYYMMDD { - let Self(inner) = self; - inner + pub const fn value(self) -> YyyyMmDdDateValue { + let Self(value) = self; + value } #[must_use] @@ -172,23 +196,28 @@ impl DateYYYYMMDD { #[must_use] pub fn from_year(year: YearType) -> Self { - Self(YYYYMMDD::from(year) * 10_000) + Self(YyyyMmDdDateValue::from(year) * 10_000) } #[must_use] pub fn from_year_month(year: YearType, month: MonthType) -> Self { - Self(YYYYMMDD::from(year) * 10_000 + YYYYMMDD::from(month) * 100) + Self(YyyyMmDdDateValue::from(year) * 10_000 + YyyyMmDdDateValue::from(month) * 100) } #[must_use] pub fn is_year(self) -> bool { Self::from_year(self.year()) == self } + + #[must_use] + pub fn is_valid(&self) -> bool { + ::is_valid(self) + } } #[derive(Copy, Clone, Debug)] #[allow(clippy::upper_case_acronyms)] -pub enum DateYYYYMMDDInvalidity { +pub enum YyyyMmDdDateInvalidity { Min, Max, MonthOutOfRange, @@ -197,8 +226,8 @@ pub enum DateYYYYMMDDInvalidity { Invalid, } -impl Validate for DateYYYYMMDD { - type Invalidity = DateYYYYMMDDInvalidity; +impl Validate for YyyyMmDdDate { + type Invalidity = YyyyMmDdDateInvalidity; fn validate(&self) -> ValidationResult { ValidationContext::new() @@ -233,36 +262,24 @@ impl Validate for DateYYYYMMDD { } } -impl From for DateYYYYMMDD { - fn from(from: YYYYMMDD) -> Self { - Self::new(from) - } -} - -impl From for YYYYMMDD { - fn from(from: DateYYYYMMDD) -> Self { - from.to_inner() - } -} - -impl From for DateYYYYMMDD { - fn from(from: DateTime) -> Self { +impl From for YyyyMmDdDate { + fn from(from: OffsetDateTimeMs) -> Self { from.0.date().into() } } -impl From for DateYYYYMMDD { +impl From for YyyyMmDdDate { #[allow(clippy::cast_possible_wrap)] fn from(from: Date) -> Self { Self( - from.year() as YYYYMMDD * 10_000 - + from.month() as YYYYMMDD * 100 - + YYYYMMDD::from(from.day()), + from.year() as YyyyMmDdDateValue * 10_000 + + from.month() as YyyyMmDdDateValue * 100 + + YyyyMmDdDateValue::from(from.day()), ) } } -impl fmt::Display for DateYYYYMMDD { +impl fmt::Display for YyyyMmDdDate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_year() { return write!(f, "{:04}", self.year()); @@ -288,8 +305,8 @@ impl fmt::Display for DateYYYYMMDD { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DateOrDateTime { - Date(DateYYYYMMDD), - DateTime(DateTime), + Date(YyyyMmDdDate), + DateTime(OffsetDateTimeMs), } impl DateOrDateTime { @@ -300,21 +317,26 @@ impl DateOrDateTime { Self::DateTime(inner) => inner.year(), } } + + #[must_use] + pub fn is_valid(&self) -> bool { + ::is_valid(self) + } } -impl From for DateOrDateTime { - fn from(from: DateTime) -> Self { +impl From for DateOrDateTime { + fn from(from: OffsetDateTimeMs) -> Self { Self::DateTime(from) } } -impl From for DateOrDateTime { - fn from(from: DateYYYYMMDD) -> Self { +impl From for DateOrDateTime { + fn from(from: YyyyMmDdDate) -> Self { Self::Date(from) } } -impl From for DateYYYYMMDD { +impl From for YyyyMmDdDate { fn from(from: DateOrDateTime) -> Self { match from { DateOrDateTime::Date(date) => date, @@ -344,7 +366,7 @@ impl fmt::Display for DateOrDateTime { #[derive(Copy, Clone, Debug)] pub enum DateOrDateTimeInvalidity { - Date(DateYYYYMMDDInvalidity), + Date(YyyyMmDdDateInvalidity), } impl Validate for DateOrDateTime { diff --git a/crates/core/src/util/clock/tests.rs b/crates/core/src/util/clock/tests.rs index 45d618d71..75b35313f 100644 --- a/crates/core/src/util/clock/tests.rs +++ b/crates/core/src/util/clock/tests.rs @@ -7,23 +7,31 @@ use super::*; #[test] fn min_max_date_year() { - assert!(YEAR_MIN <= DateYYYYMMDD::MIN.year()); - assert!(YEAR_MAX <= DateYYYYMMDD::MAX.year()); + assert!(YEAR_MIN <= YyyyMmDdDate::MIN.year()); + assert!(YEAR_MAX <= YyyyMmDdDate::MAX.year()); } #[test] fn into_release_yyyymmdd() { assert_eq!( - DateYYYYMMDD::new(19_961_219), - DateYYYYMMDD::from("1996-12-19T02:00:57Z".parse::().unwrap()), + YyyyMmDdDate::new_unchecked(19_961_219), + YyyyMmDdDate::from("1996-12-19T02:00:57Z".parse::().unwrap()), ); assert_eq!( - DateYYYYMMDD::new(19_961_219), - DateYYYYMMDD::from("1996-12-19T02:00:57-12:00".parse::().unwrap()), + YyyyMmDdDate::new_unchecked(19_961_219), + YyyyMmDdDate::from( + "1996-12-19T02:00:57-12:00" + .parse::() + .unwrap() + ), ); assert_eq!( - DateYYYYMMDD::new(19_961_219), - DateYYYYMMDD::from("1996-12-19T02:00:57+12:00".parse::().unwrap()), + YyyyMmDdDate::new_unchecked(19_961_219), + YyyyMmDdDate::from( + "1996-12-19T02:00:57+12:00" + .parse::() + .unwrap() + ), ); } @@ -32,35 +40,35 @@ fn from_to_string() { assert_eq!( "1996-12-19T02:00:57Z", "1996-12-19T02:00:57Z" - .parse::() + .parse::() .unwrap() .to_string() ); assert_eq!( "1996-12-19T02:00:57Z", "1996-12-19T02:00:57+00:00" - .parse::() + .parse::() .unwrap() .to_string() ); assert_eq!( "1996-12-19T02:00:57Z", "1996-12-19T02:00:57-00:00" - .parse::() + .parse::() .unwrap() .to_string() ); assert_eq!( "1996-12-19T02:00:57-12:00", "1996-12-19T02:00:57-12:00" - .parse::() + .parse::() .unwrap() .to_string() ); assert_eq!( "1996-12-19T02:00:57+12:00", "1996-12-19T02:00:57+12:00" - .parse::() + .parse::() .unwrap() .to_string() ); @@ -68,15 +76,15 @@ fn from_to_string() { #[test] fn validate_date() { - assert!(DateYYYYMMDD::from_year(YEAR_MIN).is_valid()); - assert!(DateYYYYMMDD::from_year_month(YEAR_MIN, 1).is_valid()); - assert!(DateYYYYMMDD::from_year(YEAR_MAX).is_valid()); - assert!(DateYYYYMMDD::from_year_month(YEAR_MAX, 1).is_valid()); - assert!(DateYYYYMMDD::new(19_960_000).is_valid()); - assert!(DateYYYYMMDD::new(19_960_101).is_valid()); - assert!(DateYYYYMMDD::new(19_961_231).is_valid()); - assert!(!DateYYYYMMDD::new(19_960_230).is_valid()); // 1996-02-30 - assert!(!DateYYYYMMDD::new(19_960_001).is_valid()); // 1996-00-01 - assert!(!DateYYYYMMDD::new(1_996_000).is_valid()); - assert!(!DateYYYYMMDD::new(119_960_001).is_valid()); + assert!(YyyyMmDdDate::from_year(YEAR_MIN).is_valid()); + assert!(YyyyMmDdDate::from_year_month(YEAR_MIN, 1).is_valid()); + assert!(YyyyMmDdDate::from_year(YEAR_MAX).is_valid()); + assert!(YyyyMmDdDate::from_year_month(YEAR_MAX, 1).is_valid()); + assert!(YyyyMmDdDate::new_unchecked(19_960_000).is_valid()); + assert!(YyyyMmDdDate::new_unchecked(19_960_101).is_valid()); + assert!(YyyyMmDdDate::new_unchecked(19_961_231).is_valid()); + assert!(!YyyyMmDdDate::new_unchecked(19_960_230).is_valid()); // 1996-02-30 + assert!(!YyyyMmDdDate::new_unchecked(19_960_001).is_valid()); // 1996-00-01 + assert!(!YyyyMmDdDate::new_unchecked(1_996_000).is_valid()); + assert!(!YyyyMmDdDate::new_unchecked(119_960_001).is_valid()); } diff --git a/crates/media-file/src/io/import/mod.rs b/crates/media-file/src/io/import/mod.rs index 5bcc67059..ccad3ad44 100644 --- a/crates/media-file/src/io/import/mod.rs +++ b/crates/media-file/src/io/import/mod.rs @@ -21,7 +21,7 @@ use aoide_core::{ FacetId as TagFacetId, Label as TagLabel, PlainTag, Score as TagScore, ScoreValue, TagsMap, }, track::{actor::Actor, title::Title, Track}, - util::clock::{DateOrDateTime, DateTime}, + util::clock::{DateOrDateTime, OffsetDateTimeMs}, }; use bitflags::bitflags; use image::{io::Reader as ImageReader, DynamicImage}; @@ -122,7 +122,7 @@ impl Default for ImportTrackConfig { #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum ImportTrack { - NewTrack { collected_at: DateTime }, + NewTrack { collected_at: OffsetDateTimeMs }, UpdateTrack(Track), } diff --git a/crates/media-file/src/util/mod.rs b/crates/media-file/src/util/mod.rs index 3c00c8b52..37d9044e5 100644 --- a/crates/media-file/src/util/mod.rs +++ b/crates/media-file/src/util/mod.rs @@ -18,7 +18,7 @@ use aoide_core::{ title::{Kind as TitleKind, Title}, }, util::{ - clock::{DateOrDateTime, DateTime, DateYYYYMMDD, YYYYMMDD}, + clock::{DateOrDateTime, OffsetDateTimeMs, YyyyMmDdDate, YyyyMmDdDateValue}, string::{trimmed_non_empty_from, trimmed_non_empty_from_owned}, }, }; @@ -410,11 +410,11 @@ pub(crate) fn parse_year_tag(value: &str) -> Option { if remainder.is_empty() && (/* YYYY */digits_input.len() == 4 || /*YYYYMM*/ digits_input.len() == 6 || - /*YYYYMMDD*/ digits_input.len() == 8) + /*YyyyMmDdDateValue*/ digits_input.len() == 8) { if let Ok(yyyymmdd) = digits_input - .parse::() + .parse::() .map(|val| match digits_input.len() { 4 => val * 10000, 6 => val * 100, @@ -422,7 +422,7 @@ pub(crate) fn parse_year_tag(value: &str) -> Option { _ => unreachable!(), }) { - let date = DateYYYYMMDD::new(yyyymmdd); + let date = YyyyMmDdDate::new_unchecked(yyyymmdd); if date.is_valid() { return Some(date.into()); } @@ -438,11 +438,11 @@ pub(crate) fn parse_year_tag(value: &str) -> Option { if let Ok((remainder, (year_input, month_input))) = year_month_parsed { if year_input.len() == 4 && month_input.len() <= 2 { if let (Ok(year), Ok(month)) = ( - year_input.parse::(), - month_input.parse::(), + year_input.parse::(), + month_input.parse::(), ) { if remainder.is_empty() { - let date = DateYYYYMMDD::new(year * 10000 + month * 100); + let date = YyyyMmDdDate::new_unchecked(year * 10000 + month * 100); if date.is_valid() { return Some(date.into()); } @@ -451,10 +451,11 @@ pub(crate) fn parse_year_tag(value: &str) -> Option { let day_of_month_parsed: IResult<_, _> = day_of_month_parser(remainder); if let Ok((remainder, day_of_month_input)) = day_of_month_parsed { if remainder.is_empty() { - if let Ok(day_of_month) = day_of_month_input.parse::() { + if let Ok(day_of_month) = day_of_month_input.parse::() { if (0..=31).contains(&day_of_month) { - let date = - DateYYYYMMDD::new(year * 10000 + month * 100 + day_of_month); + let date = YyyyMmDdDate::new_unchecked( + year * 10000 + month * 100 + day_of_month, + ); if date.is_valid() { return Some(date.into()); } @@ -468,13 +469,13 @@ pub(crate) fn parse_year_tag(value: &str) -> Option { if let Ok(date_time) = OffsetDateTime::parse(input, &Rfc3339).or_else(|_| OffsetDateTime::parse(input, &Rfc2822)) { - return Some(DateTime::new(date_time).into()); + return Some(OffsetDateTimeMs::clamp_from(date_time).into()); } if let Ok(date_time) = PrimitiveDateTime::parse(input, RFC3339_WITHOUT_TZ_FORMAT) .or_else(|_| PrimitiveDateTime::parse(input, RFC3339_WITHOUT_T_TZ_FORMAT)) { // Assume UTC if time zone is missing - return Some(DateTime::from(date_time.assume_utc()).into()); + return Some(OffsetDateTimeMs::clamp_from(date_time.assume_utc()).into()); } // Replace arbitrary whitespace by a single space and try again let recombined = input.split_whitespace().collect::>().join(" "); diff --git a/crates/media-file/src/util/tests.rs b/crates/media-file/src/util/tests.rs index e8c1829e0..a22a2a38d 100644 --- a/crates/media-file/src/util/tests.rs +++ b/crates/media-file/src/util/tests.rs @@ -11,27 +11,27 @@ use super::*; fn parse_year_tag_valid() { // All test inputs surrounded by whitespaces! assert_eq!( - Some(DateYYYYMMDD::new(19_780_000).into()), + Some(YyyyMmDdDate::new_unchecked(19_780_000).into()), parse_year_tag(" 1978 ") ); assert_eq!( - Some(DateYYYYMMDD::new(20_041_200).into()), + Some(YyyyMmDdDate::new_unchecked(20_041_200).into()), parse_year_tag(" 200412 ") ); assert_eq!( - Some(DateYYYYMMDD::new(20_010_900).into()), + Some(YyyyMmDdDate::new_unchecked(20_010_900).into()), parse_year_tag(" 2001 \t - 9 ") ); assert_eq!( - Some(DateYYYYMMDD::new(19_990_702).into()), + Some(YyyyMmDdDate::new_unchecked(19_990_702).into()), parse_year_tag(" 1999 - 7 - 2 ") ); assert_eq!( - Some(DateYYYYMMDD::new(19_991_231).into()), + Some(YyyyMmDdDate::new_unchecked(19_991_231).into()), parse_year_tag(" 1999 - 12 - \t 31 ") ); assert_eq!( - Some(DateYYYYMMDD::new(20_200_229).into()), + Some(YyyyMmDdDate::new_unchecked(20_200_229).into()), parse_year_tag(" \t20200229 ") ); assert_eq!( diff --git a/crates/media-file/tests/id3v2.rs b/crates/media-file/tests/id3v2.rs index d5d894d1b..9c49af874 100644 --- a/crates/media-file/tests/id3v2.rs +++ b/crates/media-file/tests/id3v2.rs @@ -3,7 +3,9 @@ use std::{io::BufReader, path::Path}; -use aoide_core::{media::content::ContentLink, music::tempo::TempoBpm, Track}; +use aoide_core::{ + media::content::ContentLink, music::tempo::TempoBpm, util::clock::OffsetDateTimeMs, Track, +}; use aoide_media_file::{ io::{ export::export_track_to_file, @@ -26,7 +28,7 @@ fn import_new_track_from_file_path>( content_type: Option, ) -> Track { let import_track = ImportTrack::NewTrack { - collected_at: aoide_core::util::clock::DateTime::now_utc(), + collected_at: OffsetDateTimeMs::now_utc(), }; let content_type = content_type .or_else(|| guess_mime_from_file_path(file_path.as_ref()).ok()) diff --git a/crates/repo-sqlite/migrations/0002_track/up.sql b/crates/repo-sqlite/migrations/0002_track/up.sql index a885dfe43..906e4c44c 100644 --- a/crates/repo-sqlite/migrations/0002_track/up.sql +++ b/crates/repo-sqlite/migrations/0002_track/up.sql @@ -18,13 +18,13 @@ CREATE TABLE IF NOT EXISTS track ( -- properties: album/release recorded_at TEXT, recorded_ms INTEGER, - recorded_at_yyyymmdd INTEGER, -- naive, gregorian release date as YYYYMMDD (parsed from recorded_at) + recorded_at_yyyymmdd INTEGER, -- naive, gregorian release date as YyyyMmDdDateValue (parsed from recorded_at) released_at TEXT, released_ms INTEGER, - released_at_yyyymmdd INTEGER, -- naive, gregorian release date as YYYYMMDD (parsed from released_at) + released_at_yyyymmdd INTEGER, -- naive, gregorian release date as YyyyMmDdDateValue (parsed from released_at) released_orig_at TEXT, released_orig_ms INTEGER, - released_orig_at_yyyymmdd INTEGER, -- naive, gregorian release date as YYYYMMDD (parsed from released_at) + released_orig_at_yyyymmdd INTEGER, -- naive, gregorian release date as YyyyMmDdDateValue (parsed from released_at) publisher TEXT, -- publisher or record label copyright TEXT, advisory_rating INTEGER, -- 0 = unrated, 1 = explicit, 2 = clean diff --git a/crates/repo-sqlite/src/db/collection/models.rs b/crates/repo-sqlite/src/db/collection/models.rs index 564bad028..65c4aef0d 100644 --- a/crates/repo-sqlite/src/db/collection/models.rs +++ b/crates/repo-sqlite/src/db/collection/models.rs @@ -49,8 +49,8 @@ impl TryFrom for (RecordHeader, CollectionEntity) { } = from; let header = RecordHeader { id: id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let media_source_path_kind = decode_content_path_kind(media_source_path_kind)?; let media_source_root_url = media_source_root_url @@ -108,7 +108,7 @@ pub struct InsertableRecord<'a> { } impl<'a> InsertableRecord<'a> { - pub fn bind(created_at: DateTime, entity: &'a CollectionEntity) -> Self { + pub fn bind(created_at: OffsetDateTimeMs, entity: &'a CollectionEntity) -> Self { let row_created_updated_ms = created_at.timestamp_millis(); let (hdr, body) = entity.into(); let CollectionHeader { uid, rev } = hdr; @@ -159,7 +159,7 @@ pub struct TouchableRecord { } impl TouchableRecord { - pub fn bind(updated_at: DateTime, next_rev: EntityRevision) -> Self { + pub fn bind(updated_at: OffsetDateTimeMs, next_rev: EntityRevision) -> Self { let entity_rev = encode_entity_revision(next_rev); Self { row_updated_ms: updated_at.timestamp_millis(), @@ -184,7 +184,7 @@ pub struct UpdatableRecord<'a> { impl<'a> UpdatableRecord<'a> { pub fn bind( - updated_at: DateTime, + updated_at: OffsetDateTimeMs, next_rev: EntityRevision, collection: &'a Collection, ) -> Self { diff --git a/crates/repo-sqlite/src/db/media_source/models.rs b/crates/repo-sqlite/src/db/media_source/models.rs index 2a1901f19..dd18a5bbf 100644 --- a/crates/repo-sqlite/src/db/media_source/models.rs +++ b/crates/repo-sqlite/src/db/media_source/models.rs @@ -172,8 +172,8 @@ impl TryFrom for (RecordHeader, Source) { let header = RecordHeader { id: id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let collected_at = parse_datetime(&collected_at, collected_ms); @@ -234,7 +234,7 @@ pub struct InsertableRecord<'a> { impl<'a> InsertableRecord<'a> { #[allow(clippy::too_many_lines)] // TODO pub fn bind( - created_at: DateTime, + created_at: OffsetDateTimeMs, collection_id: CollectionId, created_source: &'a Source, ) -> Self { @@ -377,7 +377,7 @@ pub struct UpdatableRecord<'a> { #[allow(clippy::too_many_lines)] // TODO impl<'a> UpdatableRecord<'a> { - pub fn bind(updated_at: DateTime, updated_source: &'a Source) -> Self { + pub fn bind(updated_at: OffsetDateTimeMs, updated_source: &'a Source) -> Self { let Source { collected_at, content: diff --git a/crates/repo-sqlite/src/db/media_tracker/models.rs b/crates/repo-sqlite/src/db/media_tracker/models.rs index 9776bbca6..1edae235d 100644 --- a/crates/repo-sqlite/src/db/media_tracker/models.rs +++ b/crates/repo-sqlite/src/db/media_tracker/models.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::util::clock::{DateTime, TimestampMillis}; +use aoide_core::util::clock::{OffsetDateTimeMs, TimestampMillis}; use aoide_core_api::media::tracker::DirTrackingStatus; use aoide_repo::{ collection::RecordId as CollectionId, @@ -60,7 +60,7 @@ pub struct InsertableRecord<'a> { impl<'a> InsertableRecord<'a> { pub fn bind( - created_at: DateTime, + created_at: OffsetDateTimeMs, collection_id: CollectionId, content_path: &'a str, status: DirTrackingStatus, @@ -89,7 +89,11 @@ pub struct UpdateDigest<'a> { } impl<'a> UpdateDigest<'a> { - pub fn bind(updated_at: DateTime, status: DirTrackingStatus, digest: &'a DigestBytes) -> Self { + pub fn bind( + updated_at: OffsetDateTimeMs, + status: DirTrackingStatus, + digest: &'a DigestBytes, + ) -> Self { let status = encode_dir_tracking_status(status); Self { row_updated_ms: updated_at.timestamp_millis(), diff --git a/crates/repo-sqlite/src/db/playlist/models.rs b/crates/repo-sqlite/src/db/playlist/models.rs index 4a82b8aff..bc338962c 100644 --- a/crates/repo-sqlite/src/db/playlist/models.rs +++ b/crates/repo-sqlite/src/db/playlist/models.rs @@ -46,8 +46,8 @@ impl From for (RecordHeader, Option, PlaylistEnti } = from; let header = RecordHeader { id: id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let collection_id = collection_id.map(Into::into); let entity_hdr = decode_entity_header(&entity_uid, entity_rev); @@ -92,7 +92,7 @@ pub struct InsertableRecord<'a> { impl<'a> InsertableRecord<'a> { pub fn bind( collection_id: Option, - created_at: DateTime, + created_at: OffsetDateTimeMs, entity: &'a PlaylistEntity, ) -> Self { let row_created_updated_ms = created_at.timestamp_millis(); @@ -137,7 +137,7 @@ pub struct TouchableRecord { } impl TouchableRecord { - pub fn bind(updated_at: DateTime, next_rev: EntityRevision) -> Self { + pub fn bind(updated_at: OffsetDateTimeMs, next_rev: EntityRevision) -> Self { let entity_rev = encode_entity_revision(next_rev); Self { row_updated_ms: updated_at.timestamp_millis(), @@ -160,7 +160,11 @@ pub struct UpdatableRecord<'a> { } impl<'a> UpdatableRecord<'a> { - pub fn bind(updated_at: DateTime, next_rev: EntityRevision, playlist: &'a Playlist) -> Self { + pub fn bind( + updated_at: OffsetDateTimeMs, + next_rev: EntityRevision, + playlist: &'a Playlist, + ) -> Self { let entity_rev = encode_entity_revision(next_rev); let Playlist { title, diff --git a/crates/repo-sqlite/src/db/playlist_entry/models.rs b/crates/repo-sqlite/src/db/playlist_entry/models.rs index bcf1f0452..d67275b40 100644 --- a/crates/repo-sqlite/src/db/playlist_entry/models.rs +++ b/crates/repo-sqlite/src/db/playlist_entry/models.rs @@ -3,7 +3,7 @@ use aoide_core::{ playlist::{Entry, Item, SeparatorItem, TrackItem}, - util::clock::{DateTime, TimestampMillis}, + util::clock::{OffsetDateTimeMs, TimestampMillis}, }; use aoide_repo::{playlist::RecordId as PlaylistId, track::RecordId as TrackId}; @@ -81,7 +81,7 @@ impl<'a> InsertableRecord<'a> { playlist_id: PlaylistId, track_id: Option, ordering: i64, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_entry: &'a Entry, ) -> Self { let row_created_updated_ms = created_at.timestamp_millis(); diff --git a/crates/repo-sqlite/src/db/track/models.rs b/crates/repo-sqlite/src/db/track/models.rs index 0721bc81f..9341650e1 100644 --- a/crates/repo-sqlite/src/db/track/models.rs +++ b/crates/repo-sqlite/src/db/track/models.rs @@ -26,13 +26,13 @@ pub struct InsertableRecord<'a> { pub last_synchronized_rev: Option, pub recorded_at: Option, pub recorded_ms: Option, - pub recorded_at_yyyymmdd: Option, + pub recorded_at_yyyymmdd: Option, pub released_at: Option, pub released_ms: Option, - pub released_at_yyyymmdd: Option, + pub released_at_yyyymmdd: Option, pub released_orig_at: Option, pub released_orig_ms: Option, - pub released_orig_at_yyyymmdd: Option, + pub released_orig_at_yyyymmdd: Option, pub publisher: Option<&'a str>, pub copyright: Option<&'a str>, pub advisory_rating: Option, @@ -119,14 +119,14 @@ impl<'a> InsertableRecord<'a> { media_source_id: media_source_id.into(), last_synchronized_rev: last_synchronized_rev.map(encode_entity_revision), recorded_at: recorded_at.as_ref().map(ToString::to_string), - recorded_ms: recorded_at.map(DateTime::timestamp_millis), - recorded_at_yyyymmdd: recorded_at_yyyymmdd.map(Into::into), + recorded_ms: recorded_at.map(OffsetDateTimeMs::timestamp_millis), + recorded_at_yyyymmdd: recorded_at_yyyymmdd.map(YyyyMmDdDate::value), released_at: released_at.as_ref().map(ToString::to_string), - released_ms: released_at.map(DateTime::timestamp_millis), - released_at_yyyymmdd: released_at_yyyymmdd.map(Into::into), + released_ms: released_at.map(OffsetDateTimeMs::timestamp_millis), + released_at_yyyymmdd: released_at_yyyymmdd.map(YyyyMmDdDate::value), released_orig_at: released_orig_at.as_ref().map(ToString::to_string), - released_orig_ms: released_orig_at.map(DateTime::timestamp_millis), - released_orig_at_yyyymmdd: released_orig_at_yyyymmdd.map(Into::into), + released_orig_ms: released_orig_at.map(OffsetDateTimeMs::timestamp_millis), + released_orig_at_yyyymmdd: released_orig_at_yyyymmdd.map(YyyyMmDdDate::value), publisher: publisher.as_ref().map(String::as_str), copyright: copyright.as_ref().map(String::as_str), advisory_rating: advisory_rating.map(encode_advisory_rating), @@ -170,13 +170,13 @@ pub struct UpdatableRecord<'a> { pub last_synchronized_rev: Option, pub recorded_at: Option, pub recorded_ms: Option, - pub recorded_at_yyyymmdd: Option, + pub recorded_at_yyyymmdd: Option, pub released_at: Option, pub released_ms: Option, - pub released_at_yyyymmdd: Option, + pub released_at_yyyymmdd: Option, pub released_orig_at: Option, pub released_orig_ms: Option, - pub released_orig_at_yyyymmdd: Option, + pub released_orig_at_yyyymmdd: Option, pub publisher: Option<&'a str>, pub copyright: Option<&'a str>, pub advisory_rating: Option, @@ -267,14 +267,14 @@ impl<'a> UpdatableRecord<'a> { media_source_id: media_source_id.into(), last_synchronized_rev: last_synchronized_rev.map(encode_entity_revision), recorded_at: recorded_at.as_ref().map(ToString::to_string), - recorded_ms: recorded_at.map(DateTime::timestamp_millis), - recorded_at_yyyymmdd: recorded_at_yyyymmdd.map(Into::into), + recorded_ms: recorded_at.map(OffsetDateTimeMs::timestamp_millis), + recorded_at_yyyymmdd: recorded_at_yyyymmdd.map(YyyyMmDdDate::value), released_at: released_at.as_ref().map(ToString::to_string), - released_ms: released_at.map(DateTime::timestamp_millis), - released_at_yyyymmdd: released_at_yyyymmdd.map(Into::into), + released_ms: released_at.map(OffsetDateTimeMs::timestamp_millis), + released_at_yyyymmdd: released_at_yyyymmdd.map(YyyyMmDdDate::value), released_orig_at: released_orig_at.as_ref().map(ToString::to_string), - released_orig_ms: released_orig_at.map(DateTime::timestamp_millis), - released_orig_at_yyyymmdd: released_orig_at_yyyymmdd.map(Into::into), + released_orig_ms: released_orig_at.map(OffsetDateTimeMs::timestamp_millis), + released_orig_at_yyyymmdd: released_orig_at_yyyymmdd.map(YyyyMmDdDate::value), publisher: publisher.as_ref().map(String::as_str), copyright: copyright.as_ref().map(String::as_str), advisory_rating: advisory_rating.map(encode_advisory_rating), diff --git a/crates/repo-sqlite/src/db/view_album/models.rs b/crates/repo-sqlite/src/db/view_album/models.rs index d4ddce0a9..59b2f180a 100644 --- a/crates/repo-sqlite/src/db/view_album/models.rs +++ b/crates/repo-sqlite/src/db/view_album/models.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::util::clock::YYYYMMDD; +use aoide_core::util::clock::YyyyMmDdDateValue; use crate::prelude::*; @@ -16,10 +16,10 @@ pub struct QueryableRecord { pub track_id_concat: String, pub kind: Option, pub publisher: Option, - pub min_recorded_at_yyyymmdd: Option, - pub max_recorded_at_yyyymmdd: Option, - pub min_released_at_yyyymmdd: Option, - pub max_released_at_yyyymmdd: Option, - pub min_released_orig_at_yyyymmdd: Option, - pub max_released_orig_at_yyyymmdd: Option, + pub min_recorded_at_yyyymmdd: Option, + pub max_recorded_at_yyyymmdd: Option, + pub min_released_at_yyyymmdd: Option, + pub max_released_at_yyyymmdd: Option, + pub min_released_orig_at_yyyymmdd: Option, + pub max_released_orig_at_yyyymmdd: Option, } diff --git a/crates/repo-sqlite/src/db/view_track_search/models.rs b/crates/repo-sqlite/src/db/view_track_search/models.rs index a6259b9ae..bbd4c1a30 100644 --- a/crates/repo-sqlite/src/db/view_track_search/models.rs +++ b/crates/repo-sqlite/src/db/view_track_search/models.rs @@ -33,13 +33,13 @@ pub struct QueryableRecord { pub last_synchronized_rev: Option, pub recorded_at: Option, pub recorded_ms: Option, - pub recorded_at_yyyymmdd: Option, + pub recorded_at_yyyymmdd: Option, pub released_at: Option, pub released_ms: Option, - pub released_at_yyyymmdd: Option, + pub released_at_yyyymmdd: Option, pub released_orig_at: Option, pub released_orig_ms: Option, - pub released_orig_at_yyyymmdd: Option, + pub released_orig_at_yyyymmdd: Option, pub publisher: Option, pub copyright: Option, pub advisory_rating: Option, @@ -82,8 +82,8 @@ impl From for (MediaSourceId, RecordHeader, TrackHeader) { } = from; let record_header = RecordHeader { id: id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let entity_header = decode_entity_header(&entity_uid, entity_rev); ( @@ -146,8 +146,8 @@ pub(crate) fn load_repo_entity( } = queryable; let header = RecordHeader { id: id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let entity_hdr = decode_entity_header(&entity_uid, entity_rev); let last_synchronized_rev = last_synchronized_rev.map(decode_entity_revision); @@ -155,35 +155,44 @@ pub(crate) fn load_repo_entity( let recorded_at = parse_datetime_opt(Some(recorded_at.as_str()), recorded_ms); debug_assert_eq!( recorded_at.map(Into::into), - recorded_at_yyyymmdd.map(DateYYYYMMDD::new), + recorded_at_yyyymmdd.map(YyyyMmDdDate::new_unchecked), ); recorded_at.map(Into::into) } else { - recorded_at_yyyymmdd.map(DateYYYYMMDD::new).map(Into::into) + recorded_at_yyyymmdd + .map(YyyyMmDdDate::new_unchecked) + .map(Into::into) }; + debug_assert!(recorded_at.as_ref().map_or(true, DateOrDateTime::is_valid)); let released_at = if let Some(released_at) = released_at { let released_at = parse_datetime_opt(Some(released_at.as_str()), released_ms); debug_assert_eq!( released_at.map(Into::into), - released_at_yyyymmdd.map(DateYYYYMMDD::new), + released_at_yyyymmdd.map(YyyyMmDdDate::new_unchecked), ); released_at.map(Into::into) } else { - released_at_yyyymmdd.map(DateYYYYMMDD::new).map(Into::into) + released_at_yyyymmdd + .map(YyyyMmDdDate::new_unchecked) + .map(Into::into) }; + debug_assert!(released_at.as_ref().map_or(true, DateOrDateTime::is_valid)); let released_orig_at = if let Some(released_orig_at) = released_orig_at { let released_orig_at = parse_datetime_opt(Some(released_orig_at.as_str()), released_orig_ms); debug_assert_eq!( released_orig_at.map(Into::into), - released_orig_at_yyyymmdd.map(DateYYYYMMDD::new), + released_orig_at_yyyymmdd.map(YyyyMmDdDate::new_unchecked), ); released_orig_at.map(Into::into) } else { released_orig_at_yyyymmdd - .map(DateYYYYMMDD::new) + .map(YyyyMmDdDate::new_unchecked) .map(Into::into) }; + debug_assert!(released_orig_at + .as_ref() + .map_or(true, DateOrDateTime::is_valid)); let advisory_rating = advisory_rating.map(decode_advisory_rating).transpose()?; let album_kind = album_kind.map(decode_album_kind).transpose()?; let album = Canonical::tie(Album { diff --git a/crates/repo-sqlite/src/repo/collection/mod.rs b/crates/repo-sqlite/src/repo/collection/mod.rs index aeae48592..9de75aed3 100644 --- a/crates/repo-sqlite/src/repo/collection/mod.rs +++ b/crates/repo-sqlite/src/repo/collection/mod.rs @@ -42,8 +42,8 @@ impl<'db> EntityRepo for crate::Connection<'db> { .map(|(row_id, row_created_ms, row_updated_ms, entity_rev)| { let header = RecordHeader { id: row_id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; (header, decode_entity_revision(entity_rev)) }) @@ -51,7 +51,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn insert_collection_entity( &mut self, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_entity: &CollectionEntity, ) -> RepoResult { let insertable = InsertableRecord::bind(created_at, created_entity); @@ -64,7 +64,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn touch_collection_entity_revision( &mut self, entity_header: &EntityHeader, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, ) -> RepoResult<(RecordHeader, EntityRevision)> { let EntityHeader { uid, rev } = entity_header; let next_rev = rev @@ -89,7 +89,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn update_collection_entity( &mut self, id: RecordId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, updated_entity: &CollectionEntity, ) -> RepoResult<()> { let updatable = diff --git a/crates/repo-sqlite/src/repo/collection/tests.rs b/crates/repo-sqlite/src/repo/collection/tests.rs index fe613fc57..769f1e48d 100644 --- a/crates/repo-sqlite/src/repo/collection/tests.rs +++ b/crates/repo-sqlite/src/repo/collection/tests.rs @@ -25,7 +25,7 @@ fn create_collection( collection: Collection, ) -> RepoResult { let entity = CollectionEntity::new(CollectionHeader::initial_random(), collection); - repo.insert_collection_entity(DateTime::now_utc(), &entity) + repo.insert_collection_entity(OffsetDateTimeMs::now_utc(), &entity) .and(Ok(entity)) } @@ -77,7 +77,7 @@ fn update_collection() -> TestResult<()> { // Bump revision number for testing let outdated_rev = entity.hdr.rev; entity.hdr.rev = outdated_rev.next().unwrap(); - db.update_collection_entity(id, DateTime::now_utc(), &entity)?; + db.update_collection_entity(id, OffsetDateTimeMs::now_utc(), &entity)?; assert_eq!(entity, db.load_collection_entity(id)?.1); // Prepare update @@ -87,7 +87,7 @@ fn update_collection() -> TestResult<()> { // Revision not bumped -> Conflict assert!(matches!( - db.update_collection_entity_revision(DateTime::now_utc(), &updated_entity), + db.update_collection_entity_revision(OffsetDateTimeMs::now_utc(), &updated_entity), Err(RepoError::Conflict), )); // Unchanged @@ -102,7 +102,7 @@ fn update_collection() -> TestResult<()> { .next_rev() .unwrap(); assert!(matches!( - db.update_collection_entity_revision(DateTime::now_utc(), &updated_entity), + db.update_collection_entity_revision(OffsetDateTimeMs::now_utc(), &updated_entity), Err(RepoError::Conflict), )); // Unchanged @@ -110,12 +110,12 @@ fn update_collection() -> TestResult<()> { // Revision bumped once -> Success updated_entity.raw.hdr = updated_entity.raw.hdr.prev_rev().unwrap(); - db.update_collection_entity_revision(DateTime::now_local_or_utc(), &updated_entity)?; + db.update_collection_entity_revision(OffsetDateTimeMs::now_local_or_utc(), &updated_entity)?; // Updated assert_eq!(updated_entity, db.load_collection_entity(id)?.1); // Revert update - db.update_collection_entity(id, DateTime::now_utc(), &entity)?; + db.update_collection_entity(id, OffsetDateTimeMs::now_utc(), &entity)?; assert_eq!(entity, db.load_collection_entity(id)?.1); Ok(()) diff --git a/crates/repo-sqlite/src/repo/media/source/mod.rs b/crates/repo-sqlite/src/repo/media/source/mod.rs index 6bdc1ee6a..6007ab1af 100644 --- a/crates/repo-sqlite/src/repo/media/source/mod.rs +++ b/crates/repo-sqlite/src/repo/media/source/mod.rs @@ -3,7 +3,7 @@ use aoide_core::{ media::{content::ContentPath, Source}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use aoide_repo::{collection::RecordId as CollectionId, media::source::*}; @@ -20,7 +20,7 @@ impl<'db> Repo for crate::prelude::Connection<'db> { fn update_media_source( &mut self, id: RecordId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, updated_source: &Source, ) -> RepoResult<()> { let updatable = UpdatableRecord::bind(updated_at, updated_source); @@ -96,7 +96,7 @@ impl<'db> CollectionRepo for crate::prelude::Connection<'db> { fn relocate_media_sources_by_content_path_prefix( &mut self, collection_id: CollectionId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, old_content_path_prefix: &ContentPath<'_>, new_content_path_prefix: &ContentPath<'_>, ) -> RepoResult { @@ -187,7 +187,7 @@ impl<'db> CollectionRepo for crate::prelude::Connection<'db> { fn insert_media_source( &mut self, collection_id: CollectionId, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_source: &Source, ) -> RepoResult { let insertable = InsertableRecord::bind(created_at, collection_id, created_source); diff --git a/crates/repo-sqlite/src/repo/media/source/tests.rs b/crates/repo-sqlite/src/repo/media/source/tests.rs index 6f1f3b827..a611cf59c 100644 --- a/crates/repo-sqlite/src/repo/media/source/tests.rs +++ b/crates/repo-sqlite/src/repo/media/source/tests.rs @@ -15,7 +15,7 @@ use aoide_core::{ ContentPathConfig, ContentRevision, }, }, - util::{clock::DateTime, color::RgbColor}, + util::{clock::OffsetDateTimeMs, color::RgbColor}, Collection, CollectionEntity, CollectionHeader, }; use aoide_repo::collection::{EntityRepo as _, RecordId as CollectionId}; @@ -44,7 +44,8 @@ impl Fixture { }; let collection_entity = CollectionEntity::new(CollectionHeader::initial_random(), collection); - let collection_id = db.insert_collection_entity(DateTime::now_utc(), &collection_entity)?; + let collection_id = + db.insert_collection_entity(OffsetDateTimeMs::now_utc(), &collection_entity)?; Ok(Self { collection_id }) } @@ -67,7 +68,7 @@ fn insert_media_source() -> anyhow::Result<()> { let fixture = Fixture::new(&mut db)?; let created_source = media::Source { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), content: media::Content { link: ContentLink { path: ContentPath::from("file:///home/test/file.mp3"), @@ -97,7 +98,7 @@ fn insert_media_source() -> anyhow::Result<()> { }, })), }; - let created_at = DateTime::now_local_or_utc(); + let created_at = OffsetDateTimeMs::now_local_or_utc(); let created_header = db.insert_media_source(fixture.collection_id, created_at, &created_source)?; @@ -120,7 +121,7 @@ fn filter_by_content_path_predicate() -> anyhow::Result<()> { let collection_id = fixture.collection_id; let file_lowercase = media::Source { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), content: media::Content { link: ContentLink { path: ContentPath::from("file:///home/file.mp3"), @@ -138,10 +139,10 @@ fn filter_by_content_path_predicate() -> anyhow::Result<()> { artwork: Default::default(), }; let header_lowercase = - db.insert_media_source(collection_id, DateTime::now_utc(), &file_lowercase)?; + db.insert_media_source(collection_id, OffsetDateTimeMs::now_utc(), &file_lowercase)?; let file_uppercase = media::Source { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), content: media::Content { link: ContentLink { path: ContentPath::from("file:///Home/File.mp3"), @@ -159,7 +160,7 @@ fn filter_by_content_path_predicate() -> anyhow::Result<()> { artwork: Default::default(), }; let header_uppercase = - db.insert_media_source(collection_id, DateTime::now_utc(), &file_uppercase)?; + db.insert_media_source(collection_id, OffsetDateTimeMs::now_utc(), &file_uppercase)?; // Equals is case-sensitive assert_eq!( @@ -304,7 +305,7 @@ fn relocate_by_content_path() -> anyhow::Result<()> { let collection_id = fixture.collection_id; let file_lowercase = media::Source { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), content: media::Content { link: ContentLink { path: ContentPath::from("file:///ho''me/file.mp3"), @@ -322,10 +323,10 @@ fn relocate_by_content_path() -> anyhow::Result<()> { artwork: Default::default(), }; let header_lowercase = - db.insert_media_source(collection_id, DateTime::now_utc(), &file_lowercase)?; + db.insert_media_source(collection_id, OffsetDateTimeMs::now_utc(), &file_lowercase)?; let file_uppercase = media::Source { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), content: media::Content { link: ContentLink { path: ContentPath::from("file:///Ho''me/File.mp3"), @@ -343,9 +344,9 @@ fn relocate_by_content_path() -> anyhow::Result<()> { artwork: Default::default(), }; let header_uppercase = - db.insert_media_source(collection_id, DateTime::now_utc(), &file_uppercase)?; + db.insert_media_source(collection_id, OffsetDateTimeMs::now_utc(), &file_uppercase)?; - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); let old_path_prefix = ContentPath::from("file:///ho''"); let new_path_prefix = ContentPath::from("file:///h'o''"); diff --git a/crates/repo-sqlite/src/repo/media/tracker/mod.rs b/crates/repo-sqlite/src/repo/media/tracker/mod.rs index 6a887df4a..a51c5573c 100644 --- a/crates/repo-sqlite/src/repo/media/tracker/mod.rs +++ b/crates/repo-sqlite/src/repo/media/tracker/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::{media::content::ContentPath, util::clock::DateTime}; +use aoide_core::{media::content::ContentPath, util::clock::OffsetDateTimeMs}; use aoide_core_api::media::tracker::{DirTrackingStatus, DirectoriesStatus}; use aoide_repo::{ collection::RecordId as CollectionId, @@ -21,7 +21,7 @@ use crate::{ impl<'db> Repo for crate::prelude::Connection<'db> { fn media_tracker_update_directories_status( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, path_prefix: &ContentPath<'_>, old_status: Option, @@ -87,7 +87,7 @@ impl<'db> Repo for crate::prelude::Connection<'db> { fn media_tracker_update_directory_digest( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, content_path: &ContentPath<'_>, digest: &DigestBytes, @@ -187,7 +187,7 @@ impl<'db> Repo for crate::prelude::Connection<'db> { fn media_tracker_confirm_directory( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, directory_path: &ContentPath<'_>, digest: &DigestBytes, diff --git a/crates/repo-sqlite/src/repo/media/tracker/tests.rs b/crates/repo-sqlite/src/repo/media/tracker/tests.rs index 98e78581c..fed476309 100644 --- a/crates/repo-sqlite/src/repo/media/tracker/tests.rs +++ b/crates/repo-sqlite/src/repo/media/tracker/tests.rs @@ -4,7 +4,7 @@ use aoide_core::{ collection::MediaSourceConfig, media::content::ContentPathConfig, - util::{clock::DateTime, url::BaseUrl}, + util::{clock::OffsetDateTimeMs, url::BaseUrl}, Collection, CollectionEntity, CollectionHeader, }; use aoide_repo::{ @@ -38,7 +38,7 @@ impl Fixture { let collection_entity = CollectionEntity::new(CollectionHeader::initial_random(), collection); let collection_id = crate::Connection::new(&mut db) - .insert_collection_entity(DateTime::now_utc(), &collection_entity)?; + .insert_collection_entity(OffsetDateTimeMs::now_utc(), &collection_entity)?; Ok(Self { db, collection_id }) } } @@ -48,7 +48,7 @@ fn update_entry_digest() -> anyhow::Result<()> { let mut fixture = Fixture::new()?; let mut db = crate::Connection::new(&mut fixture.db); - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); let collection_id = fixture.collection_id; let path = ContentPath::from("file:///test/"); let mut digest = DigestBytes::default(); @@ -220,7 +220,7 @@ fn reset_entry_status_to_current() -> anyhow::Result<()> { let mut fixture = Fixture::new()?; let mut db = crate::Connection::new(&mut fixture.db); - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); let collection_id = fixture.collection_id; let path = ContentPath::from("file:///test/"); let digest = DigestBytes::default(); diff --git a/crates/repo-sqlite/src/repo/playlist/mod.rs b/crates/repo-sqlite/src/repo/playlist/mod.rs index 4a0d0712d..846e9d310 100644 --- a/crates/repo-sqlite/src/repo/playlist/mod.rs +++ b/crates/repo-sqlite/src/repo/playlist/mod.rs @@ -45,8 +45,8 @@ impl<'db> EntityRepo for crate::Connection<'db> { .map(|(row_id, row_created_ms, row_updated_ms, entity_rev)| { let header = RecordHeader { id: row_id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; (header, decode_entity_revision(entity_rev)) }) @@ -55,7 +55,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn touch_playlist_entity_revision( &mut self, entity_header: &EntityHeader, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, ) -> RepoResult<(RecordHeader, EntityRevision)> { let EntityHeader { uid, rev } = entity_header; let next_rev = rev @@ -80,7 +80,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn update_playlist_entity( &mut self, id: RecordId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, updated_entity: &PlaylistEntity, ) -> RepoResult<()> { let updatable = @@ -128,7 +128,7 @@ impl<'db> EntityRepo for crate::Connection<'db> { fn insert_playlist_entity( &mut self, collection_id: Option, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_entity: &PlaylistEntity, ) -> RepoResult { let insertable = InsertableRecord::bind(collection_id, created_at, created_entity); @@ -414,7 +414,7 @@ impl<'db> EntryRepo for crate::Connection<'db> { } let max_ordering = max_playlist_entry_ordering(self, id)?.unwrap_or(-1); let mut ordering = max_ordering; - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); for entry in new_entries { ordering = ordering.saturating_add(1); let track_id = match &entry.item { @@ -440,7 +440,7 @@ impl<'db> EntryRepo for crate::Connection<'db> { // TODO: Ordering range checks and adjustments when needed! debug_assert!(new_entries.len() as i64 >= 0); let mut ordering = min_ordering.saturating_sub(new_entries.len() as i64); - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); for entry in new_entries { let track_id = match &entry.item { Item::Separator(_) => None, @@ -611,7 +611,7 @@ impl<'db> EntryRepo for crate::Connection<'db> { new_ordering_range }; let mut ordering = new_ordering_range.start; - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); for entry in new_entries { let track_id = match &entry.item { Item::Separator(_) => None, @@ -636,7 +636,7 @@ impl<'db> EntryRepo for crate::Connection<'db> { use playlist_entry_db::{models::*, schema::*}; let records = load_playlist_entry_records(self, source_id)?; let copied_count = records.len(); - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); for record in records { let (_id, ordering, track_id, entry) = record.into(); let insertable = diff --git a/crates/repo-sqlite/src/repo/playlist/tests.rs b/crates/repo-sqlite/src/repo/playlist/tests.rs index da5b7a380..9077f57ed 100644 --- a/crates/repo-sqlite/src/repo/playlist/tests.rs +++ b/crates/repo-sqlite/src/repo/playlist/tests.rs @@ -8,7 +8,7 @@ use aoide_core::{ self, content::{AudioContentMetadata, ContentLink, ContentPathConfig}, }, - util::{clock::DateTime, url::BaseUrl}, + util::{clock::OffsetDateTimeMs, url::BaseUrl}, Collection, CollectionEntity, CollectionHeader, Playlist, PlaylistHeader, Track, TrackBody, TrackEntity, TrackHeader, TrackUid, }; @@ -48,7 +48,7 @@ impl Fixture { let collection_entity = CollectionEntity::new(CollectionHeader::initial_random(), collection); let collection_id = crate::Connection::new(db) - .insert_collection_entity(DateTime::now_utc(), &collection_entity)?; + .insert_collection_entity(OffsetDateTimeMs::now_utc(), &collection_entity)?; Ok(Self { collection_id }) } @@ -59,7 +59,7 @@ impl Fixture { ) -> RepoResult> { let mut created = Vec::with_capacity(count); for i in 0..count { - let created_at = DateTime::now_local_or_utc(); + let created_at = OffsetDateTimeMs::now_local_or_utc(); let media_source = media::Source { collected_at: created_at, content: media::Content { @@ -79,7 +79,11 @@ impl Fixture { artwork: Default::default(), }; let media_source_id = db - .insert_media_source(self.collection_id, DateTime::now_utc(), &media_source)? + .insert_media_source( + self.collection_id, + OffsetDateTimeMs::now_utc(), + &media_source, + )? .id; let track = Track::new_from_media_source(media_source); let entity_body = TrackBody { @@ -113,14 +117,17 @@ impl Fixture { PlaylistScope::Global => None, PlaylistScope::Collection => Some(self.collection_id), }; - let playlist_id = - db.insert_playlist_entity(collection_id, DateTime::now_utc(), &playlist_entity)?; + let playlist_id = db.insert_playlist_entity( + collection_id, + OffsetDateTimeMs::now_utc(), + &playlist_entity, + )?; let media_sources_and_tracks = self.create_media_sources_and_tracks(db, track_count)?; let playlist_entries = media_sources_and_tracks .into_iter() .enumerate() .map(|(i, (_media_source_id, _track_id, track_uid))| Entry { - added_at: DateTime::now_local_or_utc(), + added_at: OffsetDateTimeMs::now_local_or_utc(), title: Some(format!("Entry {i}")), notes: None, item: Item::Track(TrackItem { uid: track_uid }), @@ -135,7 +142,7 @@ impl Fixture { fn new_separator_entry() -> Entry { Entry { - added_at: DateTime::now_local_or_utc(), + added_at: OffsetDateTimeMs::now_local_or_utc(), title: None, notes: None, item: Item::Separator(Default::default()), @@ -144,7 +151,7 @@ fn new_separator_entry() -> Entry { fn new_separator_entry_with_title(title: String) -> Entry { Entry { - added_at: DateTime::now_local_or_utc(), + added_at: OffsetDateTimeMs::now_local_or_utc(), title: Some(title), notes: None, item: Item::Separator(Default::default()), diff --git a/crates/repo-sqlite/src/repo/track/mod.rs b/crates/repo-sqlite/src/repo/track/mod.rs index b02882217..cd83a71ce 100644 --- a/crates/repo-sqlite/src/repo/track/mod.rs +++ b/crates/repo-sqlite/src/repo/track/mod.rs @@ -583,7 +583,7 @@ impl<'db> CollectionRepo for crate::Connection<'db> { { return Ok(ReplaceOutcome::Unchanged(media_source_id, id, entity)); } - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); if preserve_collected_at { if track.media_source.collected_at != entity.body.track.media_source.collected_at { log::debug!( @@ -633,7 +633,7 @@ impl<'db> CollectionRepo for crate::Connection<'db> { if mode == ReplaceMode::UpdateOnly { return Ok(ReplaceOutcome::NotCreated(track)); } - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); let media_source_id = self .insert_media_source(collection_id, created_at, &track.media_source)? .id; @@ -831,8 +831,8 @@ impl<'db> CollectionRepo for crate::Connection<'db> { )| { let record_header = RecordHeader { id: row_id.into(), - created_at: DateTime::new_timestamp_millis(row_created_ms), - updated_at: DateTime::new_timestamp_millis(row_updated_ms), + created_at: OffsetDateTimeMs::from_timestamp_millis(row_created_ms), + updated_at: OffsetDateTimeMs::from_timestamp_millis(row_updated_ms), }; let entity_header = TrackHeader::from_untyped(decode_entity_header( &entity_uid, diff --git a/crates/repo-sqlite/src/repo/track/search.rs b/crates/repo-sqlite/src/repo/track/search.rs index 068d8ccc7..0cdab8345 100644 --- a/crates/repo-sqlite/src/repo/track/search.rs +++ b/crates/repo-sqlite/src/repo/track/search.rs @@ -8,7 +8,7 @@ use aoide_core::{ ChannelFlags, DurationMs, }, tag::FacetKey, - util::clock::YYYYMMDD, + util::clock::YyyyMmDdDateValue, PlaylistUid, TrackUid, }; use aoide_core_api::{tag::search::Filter as TagFilter, track::search::*}; @@ -636,22 +636,22 @@ fn build_numeric_field_filter_expression( RecordedAtDate => { let expr = view_track_search::recorded_at_yyyymmdd; let expr_not_null = ifnull(expr, 0); - // TODO: Check and limit/clamp value range when converting from f64 to YYYYMMDD + // TODO: Check and limit/clamp value range when converting from f64 to YyyyMmDdDateValue match filter.predicate { - LessThan(value) => Box::new(expr_not_null.lt(value as YYYYMMDD)), - LessOrEqual(value) => Box::new(expr_not_null.le(value as YYYYMMDD)), - GreaterThan(value) => Box::new(expr_not_null.gt(value as YYYYMMDD)), - GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YYYYMMDD)), + LessThan(value) => Box::new(expr_not_null.lt(value as YyyyMmDdDateValue)), + LessOrEqual(value) => Box::new(expr_not_null.le(value as YyyyMmDdDateValue)), + GreaterThan(value) => Box::new(expr_not_null.gt(value as YyyyMmDdDateValue)), + GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YyyyMmDdDateValue)), Equal(value) => { if let Some(value) = value { - Box::new(expr_not_null.eq(value as YYYYMMDD)) + Box::new(expr_not_null.eq(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_null()) } } NotEqual(value) => { if let Some(value) = value { - Box::new(expr_not_null.ne(value as YYYYMMDD)) + Box::new(expr_not_null.ne(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_not_null()) } @@ -661,22 +661,22 @@ fn build_numeric_field_filter_expression( ReleasedAtDate => { let expr = view_track_search::released_at_yyyymmdd; let expr_not_null = ifnull(expr, 0); - // TODO: Check and limit/clamp value range when converting from f64 to YYYYMMDD + // TODO: Check and limit/clamp value range when converting from f64 to YyyyMmDdDateValue match filter.predicate { - LessThan(value) => Box::new(expr_not_null.lt(value as YYYYMMDD)), - LessOrEqual(value) => Box::new(expr_not_null.le(value as YYYYMMDD)), - GreaterThan(value) => Box::new(expr_not_null.gt(value as YYYYMMDD)), - GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YYYYMMDD)), + LessThan(value) => Box::new(expr_not_null.lt(value as YyyyMmDdDateValue)), + LessOrEqual(value) => Box::new(expr_not_null.le(value as YyyyMmDdDateValue)), + GreaterThan(value) => Box::new(expr_not_null.gt(value as YyyyMmDdDateValue)), + GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YyyyMmDdDateValue)), Equal(value) => { if let Some(value) = value { - Box::new(expr_not_null.eq(value as YYYYMMDD)) + Box::new(expr_not_null.eq(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_null()) } } NotEqual(value) => { if let Some(value) = value { - Box::new(expr_not_null.ne(value as YYYYMMDD)) + Box::new(expr_not_null.ne(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_not_null()) } @@ -686,22 +686,22 @@ fn build_numeric_field_filter_expression( ReleasedOrigAtDate => { let expr = view_track_search::released_orig_at_yyyymmdd; let expr_not_null = ifnull(expr, 0); - // TODO: Check and limit/clamp value range when converting from f64 to YYYYMMDD + // TODO: Check and limit/clamp value range when converting from f64 to YyyyMmDdDateValue match filter.predicate { - LessThan(value) => Box::new(expr_not_null.lt(value as YYYYMMDD)), - LessOrEqual(value) => Box::new(expr_not_null.le(value as YYYYMMDD)), - GreaterThan(value) => Box::new(expr_not_null.gt(value as YYYYMMDD)), - GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YYYYMMDD)), + LessThan(value) => Box::new(expr_not_null.lt(value as YyyyMmDdDateValue)), + LessOrEqual(value) => Box::new(expr_not_null.le(value as YyyyMmDdDateValue)), + GreaterThan(value) => Box::new(expr_not_null.gt(value as YyyyMmDdDateValue)), + GreaterOrEqual(value) => Box::new(expr_not_null.ge(value as YyyyMmDdDateValue)), Equal(value) => { if let Some(value) = value { - Box::new(expr_not_null.eq(value as YYYYMMDD)) + Box::new(expr_not_null.eq(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_null()) } } NotEqual(value) => { if let Some(value) = value { - Box::new(expr_not_null.ne(value as YYYYMMDD)) + Box::new(expr_not_null.ne(value as YyyyMmDdDateValue)) } else { Box::new(expr.is_not_null()) } diff --git a/crates/repo-sqlite/src/util/clock.rs b/crates/repo-sqlite/src/util/clock.rs index c83bef60d..2ec7ee248 100644 --- a/crates/repo-sqlite/src/util/clock.rs +++ b/crates/repo-sqlite/src/util/clock.rs @@ -7,22 +7,22 @@ use aoide_core::util::clock::*; /// Try to parse a `DateTime` value and fallback to the timestamp /// milliseconds on error (should never happen). -pub(crate) fn parse_datetime(s: &str, timestamp_millis: TimestampMillis) -> DateTime { +pub(crate) fn parse_datetime(s: &str, timestamp_millis: TimestampMillis) -> OffsetDateTimeMs { let res = s.parse(); debug_assert!(res.is_ok()); - res.unwrap_or_else(|_| DateTime::new_timestamp_millis(timestamp_millis)) + res.unwrap_or_else(|_| OffsetDateTimeMs::from_timestamp_millis(timestamp_millis)) } pub(crate) fn parse_datetime_opt( s: Option<&str>, timestamp_millis: Option, -) -> Option { +) -> Option { debug_assert_eq!(s.is_some(), timestamp_millis.is_some()); let res = s.map(FromStr::from_str).transpose(); debug_assert!(res.is_ok()); if let Ok(ok) = res { ok } else { - timestamp_millis.map(DateTime::new_timestamp_millis) + timestamp_millis.map(OffsetDateTimeMs::from_timestamp_millis) } } diff --git a/crates/repo/src/collection/mod.rs b/crates/repo/src/collection/mod.rs index f0e792c51..2d7363dc5 100644 --- a/crates/repo/src/collection/mod.rs +++ b/crates/repo/src/collection/mod.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use aoide_core::{ collection::{Entity, EntityHeader, EntityUid}, - util::{clock::DateTime, url::BaseUrl}, + util::{clock::OffsetDateTimeMs, url::BaseUrl}, }; use aoide_core_api::collection::{EntityWithSummary, LoadScope, Summary}; @@ -32,7 +32,7 @@ pub trait EntityRepo { fn insert_collection_entity( &mut self, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_entity: &Entity, ) -> RepoResult; diff --git a/crates/repo/src/lib.rs b/crates/repo/src/lib.rs index 64b67e77d..09d0c057a 100644 --- a/crates/repo/src/lib.rs +++ b/crates/repo/src/lib.rs @@ -23,6 +23,8 @@ // TODO: Add missing docs #![allow(clippy::missing_errors_doc)] +use aoide_core::util::clock::OffsetDateTimeMs; + #[macro_use] mod macros; @@ -32,15 +34,13 @@ pub mod playlist; pub mod tag; pub mod track; -use aoide_core::util::clock::DateTime; - pub type RecordId = i64; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RecordHeader { pub id: Id, - pub created_at: DateTime, - pub updated_at: DateTime, + pub created_at: OffsetDateTimeMs, + pub updated_at: OffsetDateTimeMs, } pub mod prelude { diff --git a/crates/repo/src/macros.rs b/crates/repo/src/macros.rs index d8ca24e24..b600fdc52 100644 --- a/crates/repo/src/macros.rs +++ b/crates/repo/src/macros.rs @@ -48,12 +48,12 @@ macro_rules! entity_repo_trait_common_functions { fn []( &mut self, entity_header: &$entity_header_type, - updated_at: aoide_core::util::clock::DateTime, + updated_at: aoide_core::util::clock::OffsetDateTimeMs, ) -> $crate::prelude::RepoResult<(crate::RecordHeader<$record_id_type>, aoide_core::EntityRevision)>; fn []( &mut self, - updated_at: aoide_core::util::clock::DateTime, + updated_at: aoide_core::util::clock::OffsetDateTimeMs, updated_entity: &$entity_type, ) -> $crate::prelude::RepoResult<()> { let (id, rev) = @@ -67,7 +67,7 @@ macro_rules! entity_repo_trait_common_functions { fn []( &mut self, id: $record_id_type, - updated_at: aoide_core::util::clock::DateTime, + updated_at: aoide_core::util::clock::OffsetDateTimeMs, updated_entity: &$entity_type, ) -> $crate::prelude::RepoResult<()>; diff --git a/crates/repo/src/media/source/mod.rs b/crates/repo/src/media/source/mod.rs index 52f5f46f2..8f577d292 100644 --- a/crates/repo/src/media/source/mod.rs +++ b/crates/repo/src/media/source/mod.rs @@ -7,7 +7,7 @@ pub type RecordHeader = crate::RecordHeader; use aoide_core::{ media::{content::ContentPath, Source}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use crate::{collection::RecordId as CollectionId, prelude::*}; @@ -16,7 +16,7 @@ pub trait Repo { fn update_media_source( &mut self, id: RecordId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, updated_source: &Source, ) -> RepoResult<()>; @@ -41,7 +41,7 @@ pub trait CollectionRepo { fn insert_media_source( &mut self, collection_id: CollectionId, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_source: &Source, ) -> RepoResult; @@ -54,7 +54,7 @@ pub trait CollectionRepo { fn relocate_media_sources_by_content_path_prefix( &mut self, collection_id: CollectionId, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, old_content_path_prefix: &ContentPath<'_>, new_content_path_prefix: &ContentPath<'_>, ) -> RepoResult; diff --git a/crates/repo/src/media/tracker/mod.rs b/crates/repo/src/media/tracker/mod.rs index 60d49d8ae..e72f26a8a 100644 --- a/crates/repo/src/media/tracker/mod.rs +++ b/crates/repo/src/media/tracker/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::{media::content::ContentPath, util::clock::DateTime}; +use aoide_core::{media::content::ContentPath, util::clock::OffsetDateTimeMs}; use aoide_core_api::media::tracker::{DirTrackingStatus, DirectoriesStatus}; use super::*; @@ -51,7 +51,7 @@ impl From for DirTrackingStatus { pub trait Repo { fn media_tracker_update_directories_status( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, path_prefix: &ContentPath<'_>, old_status: Option, @@ -60,7 +60,7 @@ pub trait Repo { fn media_tracker_update_directory_digest( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, content_path: &ContentPath<'_>, digest: &DigestBytes, @@ -98,7 +98,7 @@ pub trait Repo { /// a directory traversal with calculating new digests. fn media_tracker_mark_current_directories_outdated( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, path_prefix: &ContentPath<'_>, ) -> RepoResult { @@ -115,7 +115,7 @@ pub trait Repo { /// as orphaned. fn media_tracker_mark_outdated_directories_orphaned( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, path_prefix: &ContentPath<'_>, ) -> RepoResult { @@ -155,7 +155,7 @@ pub trait Repo { /// current. Returns false if the operation has been rejected. fn media_tracker_confirm_directory( &mut self, - updated_at: DateTime, + updated_at: OffsetDateTimeMs, collection_id: CollectionId, directory_path: &ContentPath<'_>, digest: &DigestBytes, diff --git a/crates/repo/src/playlist/mod.rs b/crates/repo/src/playlist/mod.rs index f53c5b63a..13bd9d588 100644 --- a/crates/repo/src/playlist/mod.rs +++ b/crates/repo/src/playlist/mod.rs @@ -7,7 +7,7 @@ use aoide_core::{ playlist::{ Entity, EntityHeader, EntityUid, EntityWithEntries, EntriesSummary, Entry, TracksSummary, }, - util::{clock::DateTime, random::adhoc_rng}, + util::{clock::OffsetDateTimeMs, random::adhoc_rng}, }; use aoide_core_api::playlist::EntityWithEntriesSummary; use rand::seq::SliceRandom as _; @@ -48,7 +48,7 @@ pub trait EntityRepo: EntryRepo { fn insert_playlist_entity( &mut self, collection_id: Option, - created_at: DateTime, + created_at: OffsetDateTimeMs, created_entity: &Entity, ) -> RepoResult; diff --git a/crates/search-index-tantivy/src/lib.rs b/crates/search-index-tantivy/src/lib.rs index 645692182..eb219308a 100644 --- a/crates/search-index-tantivy/src/lib.rs +++ b/crates/search-index-tantivy/src/lib.rs @@ -37,7 +37,7 @@ use aoide_core::{ }, PlayCounter, }, - util::clock::{DateTime, DateYYYYMMDD}, + util::clock::{OffsetDateTimeMs, YyyyMmDdDate}, CollectionUid, EncodedEntityUid, EntityRevision, EntityUid, TrackEntity, TrackUid, }; use tantivy::{ @@ -117,8 +117,8 @@ pub struct TrackFields { pub valence: Field, } -fn add_date_field(doc: &mut Document, field: Field, date_time: DateTime) { - doc.add_date(field, tantivy::DateTime::from_utc(date_time.to_inner())); +fn add_date_field(doc: &mut Document, field: Field, date_time: OffsetDateTimeMs) { + doc.add_date(field, tantivy::DateTime::from_utc(date_time.into())); } const TAG_LABEL_PREFIX: char = '#'; @@ -226,19 +226,16 @@ impl TrackFields { { doc.add_text(self.album_artist, album_artist); } - if let Some(recorded_at_yyyymmdd) = entity.body.track.recorded_at.map(DateYYYYMMDD::from) { - doc.add_i64(self.album_artist, recorded_at_yyyymmdd.to_inner().into()); + if let Some(recorded_at_yyyymmdd) = entity.body.track.recorded_at.map(YyyyMmDdDate::from) { + doc.add_i64(self.album_artist, recorded_at_yyyymmdd.value().into()); } - if let Some(released_at_yyyymmdd) = entity.body.track.released_at.map(DateYYYYMMDD::from) { - doc.add_i64(self.album_artist, released_at_yyyymmdd.to_inner().into()); + if let Some(released_at_yyyymmdd) = entity.body.track.released_at.map(YyyyMmDdDate::from) { + doc.add_i64(self.album_artist, released_at_yyyymmdd.value().into()); } if let Some(released_orig_at_yyyymmdd) = - entity.body.track.released_orig_at.map(DateYYYYMMDD::from) + entity.body.track.released_orig_at.map(YyyyMmDdDate::from) { - doc.add_i64( - self.album_artist, - released_orig_at_yyyymmdd.to_inner().into(), - ); + doc.add_i64(self.album_artist, released_orig_at_yyyymmdd.value().into()); } if let Some(tempo_bpm) = entity.body.track.metrics.tempo_bpm { doc.add_f64(self.tempo_bpm, tempo_bpm.value()); diff --git a/crates/search-index-tantivy/src/tests.rs b/crates/search-index-tantivy/src/tests.rs index 60dcc8b17..29ee28ae2 100644 --- a/crates/search-index-tantivy/src/tests.rs +++ b/crates/search-index-tantivy/src/tests.rs @@ -11,7 +11,7 @@ use aoide_core::{ Content, Source as MediaSource, }, track::{Entity, EntityBody, EntityHeader, Track}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use crate::{IndexStorage, TrackIndex}; @@ -28,7 +28,7 @@ fn track_index_smoke_test_to_verify_dynamic_schema_against_static_types() { sample_rate: Some(SampleRateHz::new(44_100.0)), }; let media_source = MediaSource { - collected_at: DateTime::now_utc(), + collected_at: OffsetDateTimeMs::now_utc(), artwork: None, content: Content { link: ContentLink { @@ -60,7 +60,7 @@ fn track_index_smoke_test_to_verify_dynamic_schema_against_static_types() { titles: Default::default(), }; let entity_body = EntityBody { - updated_at: DateTime::now_utc(), + updated_at: OffsetDateTimeMs::now_utc(), track, content_url: Some("https://www.example.com/file.mp3".parse().unwrap()), last_synchronized_rev: None, diff --git a/crates/usecases-sqlite/src/media/source/relocate.rs b/crates/usecases-sqlite/src/media/source/relocate.rs index d263a1de7..64ca6ed7a 100644 --- a/crates/usecases-sqlite/src/media/source/relocate.rs +++ b/crates/usecases-sqlite/src/media/source/relocate.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2018-2023 Uwe Klotz et al. // SPDX-License-Identifier: AGPL-3.0-or-later -use aoide_core::{media::content::ContentPath, util::clock::DateTime}; +use aoide_core::{media::content::ContentPath, util::clock::OffsetDateTimeMs}; use aoide_repo::{collection::EntityRepo as _, media::source::CollectionRepo as _}; use super::*; @@ -14,7 +14,7 @@ pub fn relocate( ) -> Result { let mut repo = RepoConnection::new(connection); let collection_id = repo.resolve_collection_id(collection_uid)?; - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); repo.relocate_media_sources_by_content_path_prefix( collection_id, updated_at, diff --git a/crates/usecases/src/collection/mod.rs b/crates/usecases/src/collection/mod.rs index bd6a2be5a..3b8ea8bc7 100644 --- a/crates/usecases/src/collection/mod.rs +++ b/crates/usecases/src/collection/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use aoide_core::{ - collection::EntityHeader as CollectionEntityHeader, prelude::*, util::clock::DateTime, + collection::EntityHeader as CollectionEntityHeader, prelude::*, util::clock::OffsetDateTimeMs, Collection, CollectionEntity, CollectionUid, }; use aoide_core_api::collection::{EntityWithSummary, LoadScope}; @@ -30,7 +30,7 @@ pub fn create_entity(new_collection: Collection) -> Result { } pub fn store_created_entity(repo: &mut impl EntityRepo, entity: &CollectionEntity) -> Result<()> { - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); repo.insert_collection_entity(created_at, entity)?; Ok(()) } @@ -48,7 +48,7 @@ pub fn update_entity( } pub fn store_updated_entity(repo: &mut impl EntityRepo, entity: &CollectionEntity) -> Result<()> { - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); repo.update_collection_entity_revision(updated_at, entity)?; Ok(()) } diff --git a/crates/usecases/src/media/mod.rs b/crates/usecases/src/media/mod.rs index 976cf4562..790974f60 100644 --- a/crates/usecases/src/media/mod.rs +++ b/crates/usecases/src/media/mod.rs @@ -6,7 +6,7 @@ use std::io::BufReader; use aoide_core::{ media::content::{resolver::vfs::VfsResolver, ContentLink, ContentPath, ContentRevision}, track::Track, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use aoide_core_api::media::SyncMode; use aoide_media_file::{ diff --git a/crates/usecases/src/media/tracker/import_files.rs b/crates/usecases/src/media/tracker/import_files.rs index 879626ecf..ebfb1be60 100644 --- a/crates/usecases/src/media/tracker/import_files.rs +++ b/crates/usecases/src/media/tracker/import_files.rs @@ -191,7 +191,7 @@ where break 'outcome outcome; } } - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); if tracks_summary.failed.is_empty() { match repo.media_tracker_confirm_directory( updated_at, diff --git a/crates/usecases/src/media/tracker/relink.rs b/crates/usecases/src/media/tracker/relink.rs index e29213d40..d9156a328 100644 --- a/crates/usecases/src/media/tracker/relink.rs +++ b/crates/usecases/src/media/tracker/relink.rs @@ -65,7 +65,7 @@ where )); // Finish with updating the old track if updated_track != old_entity.body.track { - let updated_at = DateTime::now_local_or_utc(); + let updated_at = OffsetDateTimeMs::now_local_or_utc(); let updated_entity_body = EntityBody { track: updated_track, updated_at, diff --git a/crates/usecases/src/media/tracker/scan_directories.rs b/crates/usecases/src/media/tracker/scan_directories.rs index ec99a1c80..0629751d0 100644 --- a/crates/usecases/src/media/tracker/scan_directories.rs +++ b/crates/usecases/src/media/tracker/scan_directories.rs @@ -5,7 +5,7 @@ use std::{sync::atomic::AtomicBool, time::Duration}; use aoide_core::{ media::content::resolver::{vfs::RemappingVfsResolver, ContentPathResolver as _}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use aoide_core_api::media::tracker::{ scan_directories::{Outcome, Summary}, @@ -86,7 +86,7 @@ pub fn scan_directories< let collection_id = collection_ctx.record_id; let root_file_path = resolver.build_file_path(resolver.root_path()); let outdated_count = repo.media_tracker_mark_current_directories_outdated( - DateTime::now_utc(), + OffsetDateTimeMs::now_utc(), collection_id, resolver.root_path(), )?; @@ -116,7 +116,7 @@ pub fn scan_directories< log::debug!("Updating digest of content path: {content_path}"); match repo .media_tracker_update_directory_digest( - DateTime::now_utc(), + OffsetDateTimeMs::now_utc(), collection_id, &content_path, &digest.into(), @@ -158,7 +158,7 @@ pub fn scan_directories< // Mark all remaining entries that are unreachable and // have not been visited as orphaned. summary.orphaned = repo.media_tracker_mark_outdated_directories_orphaned( - DateTime::now_utc(), + OffsetDateTimeMs::now_utc(), collection_id, resolver.root_path(), )?; diff --git a/crates/usecases/src/playlist/entries.rs b/crates/usecases/src/playlist/entries.rs index 1acbe0d55..365d83799 100644 --- a/crates/usecases/src/playlist/entries.rs +++ b/crates/usecases/src/playlist/entries.rs @@ -5,7 +5,7 @@ use std::ops::Range; use aoide_core::{ playlist::{EntityHeader, EntityUid, Entry}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use aoide_core_api::playlist::EntityWithEntriesSummary; use aoide_repo::{ @@ -34,7 +34,7 @@ pub fn patch( where Repo: EntityRepo + EntryRepo, { - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); let (record_header, next_rev) = repo.touch_playlist_entity_revision(entity_header, updated_at)?; for operation in operations { diff --git a/crates/usecases/src/playlist/mod.rs b/crates/usecases/src/playlist/mod.rs index ad3ce9880..5ecc19cde 100644 --- a/crates/usecases/src/playlist/mod.rs +++ b/crates/usecases/src/playlist/mod.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use aoide_core::{ playlist::{EntityHeader as PlaylistEntityHeader, EntityWithEntries}, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, Playlist, PlaylistEntity, PlaylistUid, }; use aoide_core_api::playlist::EntityWithEntriesSummary; @@ -46,7 +46,7 @@ where let collection_id = collection_uid .map(|uid| repo.resolve_collection_id(uid)) .transpose()?; - let created_at = DateTime::now_utc(); + let created_at = OffsetDateTimeMs::now_utc(); repo.insert_playlist_entity(collection_id, created_at, entity)?; Ok(()) } @@ -70,7 +70,7 @@ pub fn store_updated_entity( where Repo: EntityRepo, { - let updated_at = DateTime::now_utc(); + let updated_at = OffsetDateTimeMs::now_utc(); repo.update_playlist_entity_revision(updated_at, updated_entity)?; Ok(()) } diff --git a/crates/usecases/src/track/import_and_replace.rs b/crates/usecases/src/track/import_and_replace.rs index 0876c8b59..cdbf8de09 100644 --- a/crates/usecases/src/track/import_and_replace.rs +++ b/crates/usecases/src/track/import_and_replace.rs @@ -11,7 +11,7 @@ use aoide_core::{ resolver::{vfs::VfsResolver, ContentPathResolver as _}, ContentPath, }, - util::clock::DateTime, + util::clock::OffsetDateTimeMs, }; use aoide_core_api::{media::SyncMode, track::replace::Summary}; use aoide_media_file::io::import::{ImportTrack, ImportTrackConfig, Issues}; @@ -70,7 +70,7 @@ where }); let import_track = entity_body.map_or_else( || ImportTrack::NewTrack { - collected_at: DateTime::now_local_or_utc(), + collected_at: OffsetDateTimeMs::now_local_or_utc(), }, |entity_body| ImportTrack::UpdateTrack(entity_body.track), ); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 00cf290a0..c1925fb09 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -236,8 +236,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Log executable path and version on startup -- Support both simple release dates (YYYYMMDD) and full time stamps -- Store complete release dates (YYYYMMDD) instead of only the year for filtering, sorting, and grouping of album tracks +- Support both simple release dates (YyyyMmDdDateValue) and full time stamps +- Store complete release dates (YyyyMmDdDateValue) instead of only the year for filtering, sorting, and grouping of album tracks ## [0.1.1] - 2019-08-31 diff --git a/websrv/res/openapi.yaml b/websrv/res/openapi.yaml index 15d4ce1d6..b8dc89ec0 100644 --- a/websrv/res/openapi.yaml +++ b/websrv/res/openapi.yaml @@ -2533,7 +2533,7 @@ components: RecordedAtDate: oneOf: - $ref: "#/components/schemas/YYYY" - - $ref: "#/components/schemas/YYYYMMDD" + - $ref: "#/components/schemas/YyyyMmDdDateValue" RecordedAtDateTime: allOf: - $ref: "#/components/schemas/DateTime" @@ -2553,7 +2553,7 @@ components: ReleasedAtDate: oneOf: - $ref: "#/components/schemas/YYYY" - - $ref: "#/components/schemas/YYYYMMDD" + - $ref: "#/components/schemas/YyyyMmDdDateValue" ReleasedAtDateTime: allOf: - $ref: "#/components/schemas/DateTime" @@ -2576,7 +2576,7 @@ components: ReleasedOrigAtDate: oneOf: - $ref: "#/components/schemas/YYYY" - - $ref: "#/components/schemas/YYYYMMDD" + - $ref: "#/components/schemas/YyyyMmDdDateValue" ReleasedOrigAtDateTime: allOf: - $ref: "#/components/schemas/DateTime" @@ -3632,7 +3632,7 @@ components: example: 2019 description: | A 4-digit integer representing a year - YYYYMMDD: + YyyyMmDdDateValue: type: integer format: int32 minimum: 10000 @@ -3640,4 +3640,4 @@ components: example: 20191124 description: | An 8-digit integer representing a naive date without any time zone information: - YYYYMMDD = YYYY (year, 4-digits) + MM (month, 2-digits) + DD (day of month, 2 digits): + YyyyMmDdDateValue = YYYY (year, 4-digits) + MM (month, 2-digits) + DD (day of month, 2 digits):