From 579109374daff58400ca379c3afffcd5783a24f2 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Thu, 30 Oct 2025 07:06:19 +0100 Subject: [PATCH] =?UTF-8?q?Cr=C3=A9ation=20de=20la=20crate=20pmometadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 80 ++------ Cargo.toml | 1 + pmometadata/Cargo.toml | 10 + pmometadata/src/lib.rs | 423 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 450 insertions(+), 64 deletions(-) create mode 100644 pmometadata/Cargo.toml create mode 100644 pmometadata/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fdd7fc13..f4fc3d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,15 +24,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -338,21 +329,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.0", -] - [[package]] name = "base64" version = "0.21.7" @@ -1388,12 +1364,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glam" version = "0.30.8" @@ -1894,17 +1864,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2455,15 +2414,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "ogg" version = "0.8.0" @@ -2668,6 +2618,7 @@ dependencies = [ "bytemuck", "paste", "pmoflac", + "pmometadata", "soxr", "tempfile", "tokio", @@ -2826,6 +2777,16 @@ dependencies = [ "utoipa", ] +[[package]] +name = "pmometadata" +version = "0.1.0" +dependencies = [ + "async-trait", + "paste", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "pmoparadise" version = "0.1.0" @@ -3485,12 +3446,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4294,30 +4249,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index eeae84be..68467e41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ members = [ "pmosource", "pmoplaylist", "pmoflac", + "pmometadata", ] diff --git a/pmometadata/Cargo.toml b/pmometadata/Cargo.toml new file mode 100644 index 00000000..e9ce4b82 --- /dev/null +++ b/pmometadata/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pmometadata" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-trait = "0.1.89" +paste = "1" +thiserror = "2.0.17" +tokio = { version = "1", features = ["full"] } diff --git a/pmometadata/src/lib.rs b/pmometadata/src/lib.rs new file mode 100644 index 00000000..51938dbb --- /dev/null +++ b/pmometadata/src/lib.rs @@ -0,0 +1,423 @@ +//! Minimal metadata abstraction shared between PMO crates. +#![allow(async_fn_in_trait)] + +use std::{ + collections::HashMap, + time::{Duration, SystemTime}, +}; +use tokio::sync::RwLock; +use std::sync::Arc; +use async_trait::async_trait; + +/// Convenience alias for metadata operations that can fail or return no value. +pub type MetadataResult = Result, MetadataError>; + +/// Errors that can occur when manipulating metadata. +#[derive(Debug, thiserror::Error)] +pub enum MetadataError { + #[error("metadata field is not implemented")] + NotImplemented, + #[error("metadata field is read-only")] + ReadOnly, + #[error("backend error: {0}")] + Backend(String), +} + +impl MetadataError { + fn is_transient(&self) -> bool { + matches!( + self, + MetadataError::NotImplemented | MetadataError::ReadOnly + ) + } +} + +/// Trait implemented by metadata providers. +/// +/// Only the fields supported by the implementation have to be overridden. +#[async_trait] +pub trait TrackMetadata: Send + Sync { + async fn get_title(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_title(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_artist(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_artist(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_album(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_album(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_year(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_year(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_duration(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_duration(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_track_id(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_track_id(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_channel_id(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_channel_id(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_event(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_event(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_rating(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_rating(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_cover_url(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_cover_url(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_cover_pk(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn set_cover_pk(&mut self, _value: Option) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_extra(&self) -> MetadataResult> { + Err(MetadataError::NotImplemented) + } + + async fn set_extra(&mut self, _value: Option>) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } + + async fn get_updated_at(&self) -> MetadataResult { + Err(MetadataError::NotImplemented) + } + + async fn touch(&mut self) -> MetadataResult<()> { + Err(MetadataError::NotImplemented) + } +} + +pub async fn copy_metadata_into( + src: &Arc>, + dest: &Arc>, +) -> Result<(), MetadataError> +where + S: TrackMetadata + ?Sized, + D: TrackMetadata + ?Sized, +{ + let src_guard = src.read().await; + + // Expansion de copy_metadata! pour toutes les propriétés + match src_guard.get_title().await { + Ok(Some(value)) => { + dest.write().await.set_title(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_title(None).await.ok(); + } + } + + match src_guard.get_artist().await { + Ok(Some(value)) => { + dest.write().await.set_artist(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_artist(None).await.ok(); + } + } + + match src_guard.get_album().await { + Ok(Some(value)) => { + dest.write().await.set_album(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_album(None).await.ok(); + } + } + + match src_guard.get_year().await { + Ok(Some(value)) => { + dest.write().await.set_year(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_year(None).await.ok(); + } + } + + match src_guard.get_duration().await { + Ok(Some(value)) => { + dest.write().await.set_duration(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_duration(None).await.ok(); + } + } + + match src_guard.get_track_id().await { + Ok(Some(value)) => { + dest.write().await.set_track_id(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_track_id(None).await.ok(); + } + } + + match src_guard.get_channel_id().await { + Ok(Some(value)) => { + dest.write().await.set_channel_id(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_channel_id(None).await.ok(); + } + } + + match src_guard.get_event().await { + Ok(Some(value)) => { + dest.write().await.set_event(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_event(None).await.ok(); + } + } + + match src_guard.get_rating().await { + Ok(Some(value)) => { + dest.write().await.set_rating(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_rating(None).await.ok(); + } + } + + match src_guard.get_cover_url().await { + Ok(Some(value)) => { + dest.write().await.set_cover_url(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_cover_url(None).await.ok(); + } + } + + match src_guard.get_cover_pk().await { + Ok(Some(value)) => { + dest.write().await.set_cover_pk(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_cover_pk(None).await.ok(); + } + } + + match src_guard.get_extra().await { + Ok(Some(value)) => { + dest.write().await.set_extra(Some(value)).await?; + }, + Ok(None) | Err(_) => { + dest.write().await.set_extra(None).await.ok(); + } + } + + dest.write().await.touch().await?; + Ok(()) +} + +/// In-memory metadata implementation with full read/write support. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MemoryTrackMetadata { + title: Option, + artist: Option, + album: Option, + year: Option, + duration: Option, + elapsed: Option, + track_id: Option, + channel_id: Option, + event: Option, + rating: Option, + cover_url: Option, + cover_pk: Option, + updated_at: Option, + extra: Option>, +} + +impl MemoryTrackMetadata { + pub fn new() -> Self { + Self::default() + } +} + +#[async_trait] +impl TrackMetadata for MemoryTrackMetadata { + async fn get_title(&self) -> MetadataResult { + Ok(self.title.clone()) + } + + async fn set_title(&mut self, value: Option) -> MetadataResult<()> { + self.title = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_artist(&self) -> MetadataResult { + Ok(self.artist.clone()) + } + + async fn set_artist(&mut self, value: Option) -> MetadataResult<()> { + self.artist = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_album(&self) -> MetadataResult { + Ok(self.album.clone()) + } + + async fn set_album(&mut self, value: Option) -> MetadataResult<()> { + self.album = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_year(&self) -> MetadataResult { + Ok(self.year) + } + + async fn set_year(&mut self, value: Option) -> MetadataResult<()> { + self.year = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_duration(&self) -> MetadataResult { + Ok(self.duration) + } + + async fn set_duration(&mut self, value: Option) -> MetadataResult<()> { + self.duration = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_track_id(&self) -> MetadataResult { + Ok(self.track_id.clone()) + } + + async fn set_track_id(&mut self, value: Option) -> MetadataResult<()> { + self.track_id = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_channel_id(&self) -> MetadataResult { + Ok(self.channel_id.clone()) + } + + async fn set_channel_id(&mut self, value: Option) -> MetadataResult<()> { + self.channel_id = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_event(&self) -> MetadataResult { + Ok(self.event.clone()) + } + + async fn set_event(&mut self, value: Option) -> MetadataResult<()> { + self.event = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_rating(&self) -> MetadataResult { + Ok(self.rating) + } + + async fn set_rating(&mut self, value: Option) -> MetadataResult<()> { + self.rating = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_cover_url(&self) -> MetadataResult { + Ok(self.cover_url.clone()) + } + + async fn set_cover_url(&mut self, value: Option) -> MetadataResult<()> { + self.cover_url = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_cover_pk(&self) -> MetadataResult { + Ok(self.cover_pk.clone()) + } + + async fn set_cover_pk(&mut self, value: Option) -> MetadataResult<()> { + self.cover_pk = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_extra(&self) -> MetadataResult> { + Ok(self.extra.clone()) + } + + async fn set_extra(&mut self, value: Option>) -> MetadataResult<()> { + self.extra = value; + self.touch().await?; + Ok(Some(())) + } + + async fn get_updated_at(&self) -> MetadataResult { + Ok(self.updated_at) + } + + async fn touch(&mut self) -> MetadataResult<()> { + self.updated_at = Some(SystemTime::now()); + Ok(Some(())) + } +}