fin de l'unification des métadonnées de pmocacheaudio

This commit is contained in:
2025-11-23 16:16:05 +01:00
parent 17edc082b1
commit b7995a14a1
7 changed files with 363 additions and 225 deletions

View File

@@ -5,10 +5,11 @@
//! des métadonnées en JSON dans la base de données.
use anyhow::Result;
use crate::metadata_ext::AudioTrackMetadataExt;
use pmocache::CacheConfig;
use serde_json::{Number, Value};
use std::sync::Arc;
use pmocache::download::TransformMetadata;
use serde_json::Value;
use std::sync::Arc;
/// Configuration pour le cache audio
pub struct AudioConfig;
@@ -57,32 +58,26 @@ pub fn new_cache(dir: &str, limit: usize) -> Result<Cache> {
Cache::with_transformer(dir, limit, Some(transformer_factory))
}
fn persist_transform_streaminfo(cache: &Cache, pk: &str, tmeta: &TransformMetadata) {
async fn persist_transform_streaminfo(cache: Arc<Cache>, pk: &str, tmeta: &TransformMetadata) {
use std::time::Duration;
let track_meta = cache.track_metadata(pk);
let mut meta = track_meta.write().await;
if let Some(sr) = tmeta.sample_rate {
let _ = cache
.db
.set_a_metadata(pk, "sample_rate", Value::Number(Number::from(sr)));
let _ = meta.set_sample_rate(Some(sr)).await;
}
if let Some(bps) = tmeta.bits_per_sample {
let _ = cache
.db
.set_a_metadata(pk, "bits_per_sample", Value::Number(Number::from(bps)));
}
if let Some(ch) = tmeta.channels {
let _ = cache
.db
.set_a_metadata(pk, "channels", Value::Number(Number::from(ch)));
let _ = meta.set_bits_per_sample(Some(bps)).await;
}
if let Some(ts) = tmeta.total_samples {
let _ = cache
.db
.set_a_metadata(pk, "total_samples", Value::Number(Number::from(ts)));
let _ = meta.set_total_samples(Some(ts)).await;
// Calculer la durée à partir de total_samples et sample_rate
if let Some(sr) = tmeta.sample_rate {
if sr > 0 {
let secs = (ts as f64 / sr as f64).round() as u64;
let _ = cache
.db
.set_a_metadata(pk, "duration_secs", Value::Number(Number::from(secs)));
let _ = meta.set_duration(Some(Duration::from_secs(secs))).await;
}
}
}
@@ -160,208 +155,108 @@ pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result<Arc
/// # }
/// ```
pub async fn add_with_metadata_extraction(
cache: &Cache,
cache: Arc<Cache>,
url: &str,
collection: Option<&str>,
) -> Result<String> {
use std::time::Duration;
// Ajouter au cache (déclenche le download et la conversion)
let pk = cache.add_from_url(url, collection).await?;
// Attendre que le fichier soit téléchargé et converti
cache.wait_until_finished(&pk).await?;
// Persister les métadonnées de transformation (taux d'échantillonnage, bits par échantillon, etc.)
if let Some(transform) = cache.transform_metadata(&pk).await {
persist_transform_streaminfo(cache, &pk, &transform);
persist_transform_streaminfo(cache.clone(), &pk, &transform).await;
}
// Lire le fichier FLAC pour extraire les métadonnées
let file_path = cache.get_file_path(&pk);
let flac_bytes = tokio::fs::read(&file_path).await?;
// 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)?;
// Extraire les métadonnées depuis le fichier audio
let metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?;
// Informations techniques issues du flux FLAC
// Créer une instance TrackMetadata pour persister via l'interface unifiée
let track_meta = cache.clone().track_metadata(&pk);
let mut meta = track_meta.write().await;
// Informations techniques issues du flux FLAC (streaminfo)
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)) = streaminfo {
let _ = cache
.db
.set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr)));
let _ = cache
.db
.set_a_metadata(&pk, "bits_per_sample", Value::Number(Number::from(bps)));
let _ = cache.db.set_a_metadata(
&pk,
"total_samples",
Value::Number(Number::from(total_samples)),
);
let _ = meta.set_sample_rate(Some(sr)).await;
let _ = meta.set_bits_per_sample(Some(bps)).await;
let _ = meta.set_total_samples(Some(total_samples)).await;
// Calculer la durée si elle n'est pas disponible depuis les tags
if metadata.duration_secs.is_none() && sr > 0 {
let secs = (total_samples as f64 / sr as f64).round() as u64;
let _ = cache
.db
.set_a_metadata(&pk, "duration_secs", Value::Number(Number::from(secs)));
metadata.duration_secs = Some(secs);
}
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);
}
let _ = meta.set_duration(Some(Duration::from_secs(secs))).await;
}
}
// Métadonnées descriptives (tags)
if let Some(title) = metadata.title.clone() {
let _ = cache
.db
.set_a_metadata(&pk, "title", Value::String(title));
// Déterminer la collection automatique avant de move les valeurs
let auto_collection = if collection.is_none() {
metadata.collection_key()
} else {
None
};
// Métadonnées descriptives (tags) - en écrasant éventuellement celles du streaminfo
if let Some(d) = metadata.duration_secs {
let _ = meta.set_duration(Some(Duration::from_secs(d))).await;
}
if let Some(artist) = metadata.artist.clone() {
let _ = cache
.db
.set_a_metadata(&pk, "artist", Value::String(artist));
if let Some(title) = metadata.title {
let _ = meta.set_title(Some(title)).await;
}
if let Some(album) = metadata.album.clone() {
let _ = cache
.db
.set_a_metadata(&pk, "album", Value::String(album));
if let Some(artist) = metadata.artist {
let _ = meta.set_artist(Some(artist)).await;
}
if let Some(album) = metadata.album {
let _ = meta.set_album(Some(album)).await;
}
if let Some(year) = metadata.year {
let _ = cache
.db
.set_a_metadata(&pk, "year", Value::Number(Number::from(year)));
let _ = meta.set_year(Some(year)).await;
}
if let Some(genre) = metadata.genre {
let _ = meta.set_genre(Some(genre)).await;
}
if let Some(track_number) = metadata.track_number {
let _ = cache
.db
.set_a_metadata(&pk, "track_number", Value::Number(Number::from(track_number)));
let _ = meta.set_track_number(Some(track_number)).await;
}
if let Some(track_total) = metadata.track_total {
let _ = cache
.db
.set_a_metadata(&pk, "track_total", Value::Number(Number::from(track_total)));
let _ = meta.set_track_total(Some(track_total)).await;
}
if let Some(disc_number) = metadata.disc_number {
let _ = cache
.db
.set_a_metadata(&pk, "disc_number", Value::Number(Number::from(disc_number)));
let _ = meta.set_disc_number(Some(disc_number)).await;
}
if let Some(disc_total) = metadata.disc_total {
let _ = cache
.db
.set_a_metadata(&pk, "disc_total", Value::Number(Number::from(disc_total)));
let _ = meta.set_disc_total(Some(disc_total)).await;
}
if let Some(genre) = metadata.genre.clone() {
let _ = cache
.db
.set_a_metadata(&pk, "genre", Value::String(genre));
if let Some(channels) = metadata.channels {
let _ = meta.set_channels(Some(channels)).await;
}
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)));
if let Some(bitrate) = metadata.bitrate {
let _ = meta.set_bitrate(Some(bitrate)).await;
}
// Libérer le lock explicitement avant les opérations de collection
drop(meta);
// Mettre à jour la collection si les métadonnées en fournissent une
if collection.is_none() {
if let Some(auto_collection) = metadata.collection_key() {
cache
.db
.add(&pk, None, Some(&auto_collection))
.map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
cache.db.set_origin_url(&pk, url)?;
}
if let Some(auto_collection) = auto_collection {
cache
.db
.add(&pk, None, Some(&auto_collection))
.map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
cache.db.set_origin_url(&pk, url)?;
}
Ok(pk)
}
/// Récupère les métadonnées audio d'un fichier en cache
///
/// # Arguments
///
/// * `cache` - Instance du cache
/// * `pk` - Clé primaire du fichier
///
/// # Returns
///
/// Les métadonnées audio désérialisées depuis le JSON stocké en DB
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoaudiocache::cache;
///
/// # async fn example(cache: &pmoaudiocache::cache::Cache, pk: &str) -> anyhow::Result<()> {
/// let metadata = cache::get_metadata(cache, pk)?;
/// println!("Title: {:?}", metadata.title);
/// println!("Artist: {:?}", metadata.artist);
/// # Ok(())
/// # }
/// ```
pub fn get_metadata(cache: &Cache, pk: &str) -> Result<crate::metadata::AudioMetadata> {
let read_value = |key: &str| -> Result<Option<Value>> {
cache
.db
.get_a_metadata(pk, key)
.map_err(|e| anyhow::anyhow!("Database 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)
}
/// Parse minimal FLAC STREAMINFO (first metadata block) to retrieve sample rate,
/// bits per sample, and total samples.
fn parse_flac_streaminfo(data: &[u8]) -> Option<(u32, u8, u64)> {

View File

@@ -28,12 +28,11 @@
//! None,
//! ).await?;
//!
//! // Lecture des métadonnées extraites
//! let metadata = cache::get_metadata(&cache, &pk)?;
//! println!(
//! "Titre: {}",
//! metadata.title.as_deref().unwrap_or("Inconnu")
//! );
//! // Lecture des métadonnées extraites via TrackMetadata
//! use pmoaudiocache::metadata_ext::AudioTrackMetadataExt;
//! let track_meta = cache.track_metadata(&pk);
//! let title = track_meta.read().await.get_title().await?;
//! println!("Titre: {}", title.unwrap_or_else(|| "Inconnu".to_string()));
//!
//! // Accès au fichier FLAC converti
//! let flac_path = cache.get(&pk).await?;
@@ -90,11 +89,10 @@ pub mod config_ext;
// Re-exports principaux
pub use cache::{
add_with_metadata_extraction, get_metadata, new_cache, new_cache_with_consolidation,
AudioConfig, Cache,
add_with_metadata_extraction, new_cache, new_cache_with_consolidation, AudioConfig, Cache,
};
pub use metadata::AudioMetadata;
pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt};
pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt, TrackMetadataDidlExt};
pub use track_metadata::AudioCacheTrackMetadata;
#[cfg(feature = "pmoconfig")]

View File

@@ -83,3 +83,46 @@ fn map_err(field: &str, pk: &str, err: MetadataError) -> anyhow::Error {
}
}
}
/// Extension pour convertir TrackMetadata en Resource DIDL-Lite (UPnP)
#[async_trait::async_trait]
pub trait TrackMetadataDidlExt {
/// Convertit les métadonnées en Resource DIDL-Lite
///
/// # Arguments
///
/// * `url` - URL de la ressource audio
///
/// # Exemple
///
/// ```rust,ignore
/// use pmoaudiocache::metadata_ext::{AudioTrackMetadataExt, TrackMetadataDidlExt};
///
/// let track_meta = cache.track_metadata(&pk);
/// let resource = track_meta.read().await.to_didl_resource("http://example.com/track.flac".to_string()).await;
/// ```
async fn to_didl_resource(&self, url: String) -> pmodidl::Resource;
}
#[async_trait::async_trait]
impl TrackMetadataDidlExt for dyn TrackMetadata {
async fn to_didl_resource(&self, url: String) -> pmodidl::Resource {
// Récupérer la durée et la formater pour DIDL-Lite (H:MM:SS)
let duration = self.get_duration().await.ok().flatten().map(|d| {
let secs = d.as_secs();
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
format!("{}:{:02}:{:02}", hours, minutes, seconds)
});
pmodidl::Resource {
protocol_info: "http-get:*:audio/flac:*".to_string(),
bits_per_sample: self.get_bits_per_sample().await.ok().flatten().map(|b| b.to_string()),
sample_frequency: self.get_sample_rate().await.ok().flatten().map(|sr| sr.to_string()),
nr_audio_channels: self.get_channels().await.ok().flatten().map(|ch| ch.to_string()),
duration,
url,
}
}
}

View File

@@ -176,6 +176,68 @@ impl TrackMetadata for AudioCacheTrackMetadata {
Ok(Some(()))
}
async fn get_genre(&self) -> MetadataResult<String> {
Ok(self.read_string("genre")?)
}
async fn set_genre(&mut self, value: Option<String>) -> MetadataResult<()> {
self.write_string("genre", value)?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_track_number(&self) -> MetadataResult<u32> {
Ok(match self.read_number("track_number")? {
Some(n) => n.as_i64().and_then(|v| u32::try_from(v).ok()),
None => None,
})
}
async fn set_track_number(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.write_number("track_number", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_track_total(&self) -> MetadataResult<u32> {
Ok(match self.read_number("track_total")? {
Some(n) => n.as_i64().and_then(|v| u32::try_from(v).ok()),
None => None,
})
}
async fn set_track_total(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.write_number("track_total", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_disc_number(&self) -> MetadataResult<u32> {
Ok(match self.read_number("disc_number")? {
Some(n) => n.as_i64().and_then(|v| u32::try_from(v).ok()),
None => None,
})
}
async fn set_disc_number(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.write_number("disc_number", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_disc_total(&self) -> MetadataResult<u32> {
Ok(match self.read_number("disc_total")? {
Some(n) => n.as_i64().and_then(|v| u32::try_from(v).ok()),
None => None,
})
}
async fn set_disc_total(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.write_number("disc_total", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_duration(&self) -> MetadataResult<Duration> {
Ok(self.read_duration()?)
}
@@ -225,6 +287,32 @@ impl TrackMetadata for AudioCacheTrackMetadata {
Ok(Some(()))
}
async fn get_channels(&self) -> MetadataResult<u8> {
Ok(match self.read_number("channels")? {
Some(n) => n.as_i64().and_then(|v| u8::try_from(v).ok()),
None => None,
})
}
async fn set_channels(&mut self, value: Option<u8>) -> MetadataResult<()> {
self.write_number("channels", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_bitrate(&self) -> MetadataResult<u32> {
Ok(match self.read_number("bitrate")? {
Some(n) => n.as_i64().and_then(|v| u32::try_from(v).ok()),
None => None,
})
}
async fn set_bitrate(&mut self, value: Option<u32>) -> MetadataResult<()> {
self.write_number("bitrate", value.map(|v| v as i64))?;
let _ = self.touch().await?;
Ok(Some(()))
}
async fn get_track_id(&self) -> MetadataResult<String> {
Ok(self.read_string("track_id")?)
}