Skip to content

Commit

Permalink
api/backend: better time serialize/deserialize (#533)
Browse files Browse the repository at this point in the history
* api: serialize as timestamp milliseconds

* api: serialize as time duration instead of u32
  • Loading branch information
vnghia authored Dec 1, 2024
1 parent a010b9a commit 07c76fb
Show file tree
Hide file tree
Showing 33 changed files with 289 additions and 157 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pedantic = { level = "deny", priority = -1 }
cast_possible_wrap = { level = "allow", priority = 0 }
duplicated_attributes = { level = "allow", priority = 0 }
missing_panics_doc = { level = "allow", priority = 0 }
missing_errors_doc = { level = "allow", priority = 0 }
must_use_candidate = { level = "allow", priority = 0 }
return_self_not_must_use = { level = "allow", priority = 0 }
struct_excessive_bools = { level = "allow", priority = 0 }
Expand All @@ -33,7 +34,7 @@ itertools = { version = "0.12.1" }
rand = { version = "0.8.5" }
serde = { version = "1.0.215", features = ["derive"] }
serde_html_form = { version = "0.2.6" }
serde_with = { version = "3.11.0" }
serde_with = { version = "3.11.0", features = ["time_0_3"] }
strum = { version = "0.26.3", features = ["derive"] }
time = { version = "0.3.36", features = ["serde-human-readable", "macros"] }
tracing = { version = "0.1.40" }
Expand Down
1 change: 1 addition & 0 deletions nghe-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fake = { workspace = true, optional = true }
nghe_proc_macro = { path = "../nghe-proc-macro" }

md5 = { version = "0.7.0" }
num-traits = { version = "0.2.19" }
paste = { version = "1.0.15" }

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion nghe-api/src/id3/album/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct Album {
pub name: String,
pub cover_art: Option<Uuid>,
pub song_count: u16,
pub duration: u32,
pub duration: time::Duration,
pub created: OffsetDateTime,
pub year: Option<u16>,
pub music_brainz_id: Option<Uuid>,
Expand Down
2 changes: 1 addition & 1 deletion nghe-api/src/id3/genre/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct Genre {
pub name: String,
}

#[api_derive(apply = false)]
#[api_derive(serde_apply = false)]
#[derive(Default)]
#[serde(transparent)]
pub struct Genres {
Expand Down
26 changes: 26 additions & 0 deletions nghe-api/src/id3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,29 @@ pub mod builder {
pub use super::super::song::SongBuilder as Builder;
}
}

#[cfg(test)]
mod tests {
use nghe_proc_macro::api_derive;
use rstest::rstest;
use serde_json::json;

#[api_derive]
pub struct Test {
duration: time::Duration,
}

#[rstest]
#[case(time::Duration::seconds_f32(1.5), 2)]
#[case(time::Duration::seconds_f32(2.1), 3)]
#[case(time::Duration::seconds_f32(10.0), 10)]
fn test_serialize_duration(#[case] duration: time::Duration, #[case] result: i64) {
assert_eq!(
serde_json::to_string(&Test { duration }).unwrap(),
serde_json::to_string(&json!({
"duration": result,
}))
.unwrap()
);
}
}
2 changes: 1 addition & 1 deletion nghe-api/src/id3/song/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct Song {
pub size: u32,
pub content_type: Cow<'static, str>,
pub suffix: Cow<'static, str>,
pub duration: u32,
pub duration: time::Duration,
pub bit_rate: u32,
pub bit_depth: Option<u8>,
pub sampling_rate: u32,
Expand Down
4 changes: 1 addition & 3 deletions nghe-api/src/lists/get_album_list2.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use nghe_proc_macro::api_derive;
use serde_with::serde_as;
use uuid::Uuid;

use crate::id3;

// TODO: Optimize this after https://github.com/serde-rs/serde/issues/1183
#[serde_as]
#[api_derive(copy = false)]
#[api_derive(serde_as = true, copy = false)]
#[serde(tag = "type")]
#[cfg_attr(test, derive(Default))]
pub enum Type {
Expand Down
39 changes: 37 additions & 2 deletions nghe-api/src/media_annotation/scrobble.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
use nghe_proc_macro::api_derive;
use serde_with::{serde_as, TimestampMilliSeconds};
use time::OffsetDateTime;
use uuid::Uuid;

#[api_derive]
#[api_derive(serde_as = true)]
#[endpoint(path = "scrobble")]
#[cfg_attr(test, derive(Default))]
pub struct Request {
#[serde(rename = "id")]
pub ids: Vec<Uuid>,
#[serde(rename = "time")]
pub times: Option<Vec<u64>>,
#[serde_as(as = "Option<Vec<TimestampMilliSeconds<i64>>>")]
pub times: Option<Vec<OffsetDateTime>>,
pub submission: Option<bool>,
}

#[api_derive]
pub struct Response;

#[cfg(test)]
mod tests {
use rstest::rstest;
use time::macros::datetime;
use uuid::uuid;

use super::*;

#[rstest]
#[case(
"id=d4ea6896-a838-446c-ace4-d9d13d336391",
Some(Request {
ids: vec![uuid!("d4ea6896-a838-446c-ace4-d9d13d336391")],
..Default::default()
})
)]
#[case(
"id=d4ea6896-a838-446c-ace4-d9d13d336391&\
time=1000000000000",
Some(Request {
ids: vec![uuid!("d4ea6896-a838-446c-ace4-d9d13d336391")],
times: Some(vec![datetime!(2001-09-09 01:46:40.000 UTC)]),
..Default::default()
})
)]
fn test_deserialize(#[case] url: &str, #[case] request: Option<Request>) {
serde_html_form::from_str::<Request>(url).unwrap();
assert_eq!(serde_html_form::from_str::<Request>(url).ok(), request);
}
}
12 changes: 2 additions & 10 deletions nghe-api/src/playlists/create_playlist.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
use nghe_proc_macro::api_derive;
use serde_with::serde_as;
use uuid::Uuid;

use super::playlist;

// TODO: Optimize this after https://github.com/serde-rs/serde/issues/1183
#[serde_as]
#[api_derive(request = true, copy = false)]
#[serde(untagged)]
pub enum CreateOrUpdate {
Create {
name: String,
},
Update {
#[serde_as(as = "serde_with::DisplayFromStr")]
playlist_id: Uuid,
},
Create { name: String },
Update { playlist_id: Uuid },
}

#[api_derive]
Expand Down
2 changes: 1 addition & 1 deletion nghe-api/src/playlists/playlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct Playlist {
pub comment: Option<String>,
pub public: bool,
pub song_count: u16,
pub duration: u32,
pub duration: time::Duration,
pub created: OffsetDateTime,
pub changed: OffsetDateTime,
}
Expand Down
29 changes: 29 additions & 0 deletions nghe-api/src/time/duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pub mod serde {
use ::serde::{de, ser, Deserialize, Deserializer, Serializer};
use num_traits::ToPrimitive;
use time::Duration;

pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u32(
duration
.as_seconds_f32()
.ceil()
.to_u32()
.ok_or_else(|| ser::Error::custom("Could not serialize duration to integer"))?,
)
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
Ok(Duration::seconds_f32(
<u32>::deserialize(deserializer)?
.to_f32()
.ok_or_else(|| de::Error::custom("Could not deserialize duration from integer"))?,
))
}
}
2 changes: 2 additions & 0 deletions nghe-api/src/time/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![allow(clippy::ref_option)]

pub mod duration;

pub mod serde {
use time::format_description::well_known::{iso8601, Iso8601};

Expand Down
1 change: 0 additions & 1 deletion nghe-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ indexmap = { version = "2.6.0" }
libaes = { version = "0.7.0" }
lofty = { version = "0.21.1" }
loole = { version = "0.4.0" }
num-traits = { version = "0.2.19" }
o2o = { version = "0.5.1-beta1", default-features = false, features = ["syn2"] }
rsmpeg = { version = "0.15.1", default-features = false, features = [
"ffmpeg7",
Expand Down
1 change: 1 addition & 0 deletions nghe-backend/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use o2o::o2o;
#[from_owned(std::num::TryFromIntError)]
#[from_owned(typed_path::StripPrefixError)]
#[from_owned(time::error::ComponentRange)]
#[from_owned(time::error::ConversionRange)]
#[from_owned(tokio::task::JoinError)]
#[from_owned(tokio::sync::AcquireError)]
#[from_owned(SdkError<GetObjectError>)]
Expand Down
94 changes: 94 additions & 0 deletions nghe-backend/src/file/audio/duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::iter::Sum;
use std::ops::Add;

use diesel::sql_types::Float;
use diesel::{AsExpression, FromSqlRow};

use crate::Error;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, AsExpression, FromSqlRow)]
#[repr(transparent)]
#[diesel(sql_type = Float)]
#[cfg_attr(test, derive(fake::Dummy))]
pub struct Duration(pub time::Duration);

impl From<time::Duration> for Duration {
fn from(value: time::Duration) -> Self {
Self(value)
}
}

impl From<Duration> for time::Duration {
fn from(value: Duration) -> Self {
value.0
}
}

impl From<f32> for Duration {
fn from(value: f32) -> Self {
time::Duration::seconds_f32(value).into()
}
}

impl From<Duration> for f32 {
fn from(value: Duration) -> Self {
value.0.as_seconds_f32()
}
}

impl TryFrom<std::time::Duration> for Duration {
type Error = Error;

fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
time::Duration::try_from(value).map_err(Self::Error::from).map(Self::from)
}
}

impl Add<Duration> for Duration {
type Output = Self;

fn add(self, rhs: Duration) -> Self::Output {
(self.0 + rhs.0).into()
}
}

impl Sum<Duration> for Duration {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(Self::default(), Self::add)
}
}

pub trait Trait {
fn duration(&self) -> Duration;
}

impl Trait for Duration {
fn duration(&self) -> Duration {
*self
}
}

impl<D: Trait> Trait for Vec<D> {
fn duration(&self) -> Duration {
self.iter().map(D::duration).sum()
}
}

#[cfg(test)]
mod tests {
use rstest::rstest;

use super::*;

#[rstest]
#[case(&[], 0.0)]
#[case(&[100.2, 200.3], 300.5)]
fn test_sum(#[case] durations: &[f32], #[case] result: f32) {
// Allow microsecond mismatch.
assert!(
(f32::from(durations.iter().copied().map(Duration::from).sum::<Duration>()) - result)
.abs()
< 1e-6
);
}
}
2 changes: 1 addition & 1 deletion nghe-backend/src/file/audio/extract/flac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Property for FlacFile {
fn property(&self) -> Result<audio::Property, Error> {
let properties = self.properties();
Ok(audio::Property {
duration: properties.duration().as_secs_f32(),
duration: properties.duration().try_into()?,
bitrate: properties.audio_bitrate(),
bit_depth: Some(properties.bit_depth()),
sample_rate: properties.sample_rate(),
Expand Down
2 changes: 2 additions & 0 deletions nghe-backend/src/file/audio/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod artist;
mod date;
pub mod duration;
mod extract;
mod genre;
mod information;
Expand All @@ -14,6 +15,7 @@ pub use artist::{Artist, Artists};
pub use date::Date;
use diesel::sql_types::Text;
use diesel::{AsExpression, FromSqlRow};
pub use duration::Duration;
use extract::{Metadata as _, Property as _};
pub use genre::Genres;
pub use information::Information;
Expand Down
Loading

0 comments on commit 07c76fb

Please sign in to comment.