nettoyage des multiple couche de metadata de pmoaudiocache
This commit is contained in:
@@ -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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
let metadata_json: Value = serde_json::to_value(&metadata)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
|
||||
// Stocker dans la DB
|
||||
cache
|
||||
// Métadonnées descriptives (tags)
|
||||
if let Some(title) = metadata.title.clone() {
|
||||
let _ = cache
|
||||
.db
|
||||
.set_metadata(&pk, &metadata_json)
|
||||
.map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
|
||||
.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)));
|
||||
}
|
||||
|
||||
// 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
|
||||
let read_value = |key: &str| -> Result<Option<Value>> {
|
||||
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))?;
|
||||
.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
101
pmoaudiocache/src/metadata_ext.rs
Executable file → Normal 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user