nettoyage des multiple couche de metadata de pmoaudiocache

This commit is contained in:
2025-11-22 11:59:23 +01:00
parent 792ac5cb95
commit 17edc082b1
2 changed files with 177 additions and 78 deletions

View File

@@ -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<crate::metadata::AudioMetadata> {
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<Option<Value>> {
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<Option<String>> {
Ok(read_value(key)?.and_then(|v| v.as_str().map(|s| s.to_string())))
};
let read_u64 = |key: &str| -> Result<Option<u64>> {
Ok(read_value(key)?.and_then(|v| v.as_u64()))
};
let read_u32 = |key: &str| -> Result<Option<u32>> {
Ok(read_value(key)?.and_then(|v| v.as_u64()).and_then(|n| n.try_into().ok()))
};
let read_u8 = |key: &str| -> Result<Option<u8>> {
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)
}

101
pmoaudiocache/src/metadata_ext.rs Executable file → Normal file
View File

@@ -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<AudioConfig> {
// 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<String>) -> Arc<RwLock<dyn TrackMetadata>>;
@@ -48,3 +21,65 @@ impl AudioTrackMetadataExt for Arc<pmocache::Cache<AudioConfig>> {
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<Option<String>>;
async fn get_artist(&self, pk: &str) -> anyhow::Result<Option<String>>;
async fn get_album(&self, pk: &str) -> anyhow::Result<Option<String>>;
async fn get_duration_secs(&self, pk: &str) -> anyhow::Result<Option<i64>>;
}
impl AudioMetadataExt for Arc<pmocache::Cache<AudioConfig>> {
async fn get_title(&self, pk: &str) -> anyhow::Result<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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<Option<i64>> {
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}")
}
}
}