From 17edc082b12adedab9dc36e7c5739c75944f9eda Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 22 Nov 2025 11:59:23 +0100 Subject: [PATCH] nettoyage des multiple couche de metadata de pmoaudiocache --- pmoaudiocache/src/cache.rs | 154 +++++++++++++++++++++--------- pmoaudiocache/src/metadata_ext.rs | 101 +++++++++++++------- 2 files changed, 177 insertions(+), 78 deletions(-) mode change 100755 => 100644 pmoaudiocache/src/metadata_ext.rs diff --git a/pmoaudiocache/src/cache.rs b/pmoaudiocache/src/cache.rs index c408d2dd..7f332d71 100644 --- a/pmoaudiocache/src/cache.rs +++ b/pmoaudiocache/src/cache.rs @@ -178,15 +178,17 @@ pub async fn add_with_metadata_extraction( let file_path = cache.get_file_path(&pk); let flac_bytes = tokio::fs::read(&file_path).await?; - // Extraire les métadonnées + // Extraire les métadonnées et persister champ par champ dans la DB (source unique) let mut metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?; - // Propager les métadonnées techniques dans la DB (sans passer par le JSON) + + // Informations techniques issues du flux FLAC + let streaminfo = parse_flac_streaminfo(&flac_bytes); if let Some(d) = metadata.duration_secs { let _ = cache .db .set_a_metadata(&pk, "duration_secs", Value::Number(Number::from(d))); } - if let Some((sr, bps, total_samples)) = parse_flac_streaminfo(&flac_bytes) { + if let Some((sr, bps, total_samples)) = streaminfo { let _ = cache .db .set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr))); @@ -208,45 +210,76 @@ pub async fn add_with_metadata_extraction( if metadata.sample_rate.is_none() { metadata.sample_rate = Some(sr); } - } - - // Extraire aussi les informations FLAC de base (STREAMINFO) pour peupler TrackMetadata - if let Some((sr, bps, total_samples)) = parse_flac_streaminfo(&flac_bytes) { - if let Err(e) = cache - .db - .set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr))) - { - tracing::warn!("Failed to persist sample_rate for {}: {}", pk, e); - } - if let Err(e) = cache - .db - .set_a_metadata(&pk, "bits_per_sample", Value::Number(Number::from(bps))) - { - tracing::warn!("Failed to persist bits_per_sample for {}: {}", pk, e); - } - if let Err(e) = cache - .db - .set_a_metadata(&pk, "total_samples", Value::Number(Number::from(total_samples))) - { - tracing::warn!("Failed to persist total_samples for {}: {}", pk, e); + if metadata.bitrate.is_none() { + // Approximate bitrate: sample_rate * bits_per_sample * channels / 1000 + if let Some(ch) = metadata.channels { + let br = (sr as u64 * bps as u64 * ch as u64) / 1000; + metadata.bitrate = Some(br as u32); + } } } - if let Some(transform) = cache.transform_metadata(&pk).await { - if let Some(mode) = transform.mode { - metadata.conversion = Some(crate::metadata::AudioConversionInfo { - mode, - source_codec: transform.input_codec, - }); - } + // Métadonnées descriptives (tags) + if let Some(title) = metadata.title.clone() { + let _ = cache + .db + .set_a_metadata(&pk, "title", Value::String(title)); + } + if let Some(artist) = metadata.artist.clone() { + let _ = cache + .db + .set_a_metadata(&pk, "artist", Value::String(artist)); + } + if let Some(album) = metadata.album.clone() { + let _ = cache + .db + .set_a_metadata(&pk, "album", Value::String(album)); + } + if let Some(year) = metadata.year { + let _ = cache + .db + .set_a_metadata(&pk, "year", Value::Number(Number::from(year))); + } + if let Some(track_number) = metadata.track_number { + let _ = cache + .db + .set_a_metadata(&pk, "track_number", Value::Number(Number::from(track_number))); + } + if let Some(track_total) = metadata.track_total { + let _ = cache + .db + .set_a_metadata(&pk, "track_total", Value::Number(Number::from(track_total))); + } + if let Some(disc_number) = metadata.disc_number { + let _ = cache + .db + .set_a_metadata(&pk, "disc_number", Value::Number(Number::from(disc_number))); + } + if let Some(disc_total) = metadata.disc_total { + let _ = cache + .db + .set_a_metadata(&pk, "disc_total", Value::Number(Number::from(disc_total))); + } + if let Some(genre) = metadata.genre.clone() { + let _ = cache + .db + .set_a_metadata(&pk, "genre", Value::String(genre)); + } + if let Some(sr) = metadata.sample_rate { + let _ = cache + .db + .set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr))); + } + if let Some(ch) = metadata.channels { + let _ = cache + .db + .set_a_metadata(&pk, "channels", Value::Number(Number::from(ch))); + } + if let Some(br) = metadata.bitrate { + let _ = cache + .db + .set_a_metadata(&pk, "bitrate", Value::Number(Number::from(br))); } - let metadata_json: Value = serde_json::to_value(&metadata) - .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - // Stocker dans la DB - cache - .db - .set_metadata(&pk, &metadata_json) - .map_err(|e| anyhow::anyhow!("Database error: {}", e))?; // Mettre à jour la collection si les métadonnées en fournissent une if collection.is_none() { @@ -286,14 +319,45 @@ pub async fn add_with_metadata_extraction( /// # } /// ``` pub fn get_metadata(cache: &Cache, pk: &str) -> Result { - let metadata_json = cache - .db - .get_metadata_json(pk) - .map_err(|e| anyhow::anyhow!("Database error: {}", e))? - .ok_or_else(|| anyhow::anyhow!("No metadata found for pk: {}", pk))?; + let read_value = |key: &str| -> Result> { + cache + .db + .get_a_metadata(pk, key) + .map_err(|e| anyhow::anyhow!("Database error: {}", e)) + }; - let metadata: crate::metadata::AudioMetadata = serde_json::from_str(&metadata_json) - .map_err(|e| anyhow::anyhow!("Metadata deserialization error: {}", e))?; + let read_string = |key: &str| -> Result> { + Ok(read_value(key)?.and_then(|v| v.as_str().map(|s| s.to_string()))) + }; + + let read_u64 = |key: &str| -> Result> { + Ok(read_value(key)?.and_then(|v| v.as_u64())) + }; + + let read_u32 = |key: &str| -> Result> { + Ok(read_value(key)?.and_then(|v| v.as_u64()).and_then(|n| n.try_into().ok())) + }; + + let read_u8 = |key: &str| -> Result> { + Ok(read_value(key)?.and_then(|v| v.as_u64()).and_then(|n| n.try_into().ok())) + }; + + let metadata = crate::metadata::AudioMetadata { + title: read_string("title")?, + artist: read_string("artist")?, + album: read_string("album")?, + year: read_u32("year")?, + track_number: read_u32("track_number")?, + track_total: read_u32("track_total")?, + disc_number: read_u32("disc_number")?, + disc_total: read_u32("disc_total")?, + genre: read_string("genre")?, + duration_secs: read_u64("duration_secs")?, + sample_rate: read_u32("sample_rate")?, + channels: read_u8("channels")?, + bitrate: read_u32("bitrate")?, + conversion: None, + }; Ok(metadata) } diff --git a/pmoaudiocache/src/metadata_ext.rs b/pmoaudiocache/src/metadata_ext.rs old mode 100755 new mode 100644 index cd920a6d..5c9df714 --- a/pmoaudiocache/src/metadata_ext.rs +++ b/pmoaudiocache/src/metadata_ext.rs @@ -1,42 +1,15 @@ -//! Extension trait pour accéder aux métadonnées audio de manière typée +//! Extension trait pour accéder aux métadonnées audio via `TrackMetadata` //! -//! Ce module utilise la macro `define_metadata_properties!` de pmocache -//! pour générer automatiquement des méthodes d'accès typées aux métadonnées audio. +//! Cette couche est désormais un mince wrapper qui délègue au nœud central +//! `AudioCacheTrackMetadata` (implémentation de `pmometadata::TrackMetadata`). +//! Elle ne touche plus directement la base de données ni ne s'appuie sur des +//! structures parallèles : toutes les lectures passent par `TrackMetadata`. use crate::{AudioCacheTrackMetadata, AudioConfig}; -use pmocache::define_metadata_properties; -use pmometadata::TrackMetadata; +use pmometadata::{MetadataError, TrackMetadata}; use std::sync::Arc; use tokio::sync::RwLock; -// Génération automatique du trait AudioMetadataExt avec toutes les propriétés audio -define_metadata_properties! { - AudioMetadataExt for pmocache::Cache { - // Métadonnées textuelles - title: String as string, - artist: String as string, - album: String as string, - album_artist: String as string, - genre: String as string, - composer: String as string, - comment: String as string, - - // Métadonnées numériques (année, numéros de piste) - year: i64 as i64, - track_number: i64 as i64, - disc_number: i64 as i64, - total_tracks: i64 as i64, - total_discs: i64 as i64, - - // Métadonnées techniques audio - duration_secs: i64 as i64, - sample_rate: i64 as i64, - bitrate: i64 as i64, - channels: i64 as i64, - bit_depth: i64 as i64, - } -} - /// Fournit un accès direct à une implémentation `TrackMetadata` basée sur le cache. pub trait AudioTrackMetadataExt { fn track_metadata(&self, pk: impl Into) -> Arc>; @@ -48,3 +21,65 @@ impl AudioTrackMetadataExt for Arc> { Arc::new(RwLock::new(metadata)) } } + +/// Accès « léger » aux principales métadonnées en s'appuyant sur `TrackMetadata`. +/// +/// Cette version remplace l'ancienne macro `define_metadata_properties!` qui +/// accédait directement aux clés de la DB. Les méthodes restent asynchrones et +/// retournent `Option` ; en cas d'erreur backend, elles lèvent `anyhow::Error`. +pub trait AudioMetadataExt { + async fn get_title(&self, pk: &str) -> anyhow::Result>; + async fn get_artist(&self, pk: &str) -> anyhow::Result>; + async fn get_album(&self, pk: &str) -> anyhow::Result>; + async fn get_duration_secs(&self, pk: &str) -> anyhow::Result>; +} + +impl AudioMetadataExt for Arc> { + async fn get_title(&self, pk: &str) -> anyhow::Result> { + let meta = self.track_metadata(pk); + let res = { + let guard = meta.read().await; + guard.get_title().await + }; + res.map_err(|e| map_err("title", pk, e)) + } + + async fn get_artist(&self, pk: &str) -> anyhow::Result> { + let meta = self.track_metadata(pk); + let res = { + let guard = meta.read().await; + guard.get_artist().await + }; + res.map_err(|e| map_err("artist", pk, e)) + } + + async fn get_album(&self, pk: &str) -> anyhow::Result> { + let meta = self.track_metadata(pk); + let res = { + let guard = meta.read().await; + guard.get_album().await + }; + res.map_err(|e| map_err("album", pk, e)) + } + + async fn get_duration_secs(&self, pk: &str) -> anyhow::Result> { + let meta = self.track_metadata(pk); + let res = { + let guard = meta.read().await; + guard.get_duration().await + }; + let duration = res.map_err(|e| map_err("duration", pk, e))?; + Ok(duration.map(|d| d.as_secs() as i64)) + } +} + +fn map_err(field: &str, pk: &str, err: MetadataError) -> anyhow::Error { + match err { + MetadataError::NotImplemented | MetadataError::ReadOnly => { + anyhow::anyhow!("metadata {field} for {pk} is not implemented") + } + MetadataError::Backend(msg) => { + anyhow::anyhow!("backend error on {field} for {pk}: {msg}") + } + } +}