//! Module de gestion du cache audio avec conversion FLAC //! //! Ce module étend le cache générique de `pmocache` avec des fonctionnalités //! spécifiques aux fichiers audio : conversion FLAC automatique et stockage //! des métadonnées en JSON dans la base de données. use crate::metadata_ext::AudioTrackMetadataExt; use anyhow::{anyhow, Result}; use pmocache::download::{read_exact_or_eof, TransformMetadata}; use pmocache::{pk_from_content_header, CacheConfig}; use pmoflac::is_flac_magic_header; use serde_json::json; use std::sync::Arc; use tokio::io::AsyncSeekExt; /// Configuration pour le cache audio pub struct AudioConfig; impl CacheConfig for AudioConfig { fn file_extension() -> &'static str { "flac" } fn cache_type() -> &'static str { "flac" } fn cache_name() -> &'static str { "audio" } fn default_param() -> &'static str { "orig" } } /// Type alias pour le cache audio avec conversion FLAC pub type Cache = pmocache::Cache; /// Crée un cache audio avec conversion FLAC automatique /// /// # Arguments /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre de pistes) /// /// # Returns /// /// Instance du cache configurée pour la conversion FLAC automatique /// /// # Exemple /// /// ```rust,no_run /// use pmoaudiocache::cache; /// /// let cache = cache::new_cache("./audio_cache", 1000).unwrap(); /// ``` pub fn new_cache(dir: &str, limit: usize) -> Result { let transformer_factory = Arc::new(|| crate::streaming::create_streaming_flac_transformer()); Cache::with_transformer(dir, limit, Some(transformer_factory)) } async fn persist_transform_streaminfo(cache: Arc, 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 _ = meta.set_sample_rate(Some(sr)).await; } if let Some(bps) = tmeta.bits_per_sample { let _ = meta.set_bits_per_sample(Some(bps)).await; } if let Some(ts) = tmeta.total_samples { 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 _ = meta.set_duration(Some(Duration::from_secs(secs))).await; } } } } /// Crée un cache audio et lance la consolidation en arrière-plan /// /// Cette fonction crée le cache et lance immédiatement une consolidation /// pour nettoyer les fichiers incomplets (sans marker de complétion). /// /// # Arguments /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre de pistes) /// /// # Returns /// /// Arc vers l'instance du cache configurée pour la conversion FLAC automatique /// /// # Exemple /// /// ```rust,no_run /// use pmoaudiocache::cache; /// /// # async fn example() -> anyhow::Result<()> { /// let cache = cache::new_cache_with_consolidation("./audio_cache", 1000).await?; /// # Ok(()) /// # } /// ``` pub async fn new_cache_with_consolidation(dir: &str, limit: usize) -> Result> { let cache = Arc::new(new_cache(dir, limit)?); Ok(pmocache::Cache::with_consolidation(cache).await) } /// Ajoute un fichier audio local. Les FLAC sont référencés sans copie, les autres formats /// sont convertis via le pipeline classique. pub async fn add_local_file(cache: &Cache, path: &str, collection: Option<&str>) -> Result { let canonical_path = std::fs::canonicalize(path)?; let file_url = format!("file://{}", canonical_path.display()); let length = tokio::fs::metadata(&canonical_path) .await .ok() .map(|m| m.len()); let mut reader = tokio::fs::File::open(&canonical_path).await?; let header = read_exact_or_eof(&mut reader, 1024) .await .map_err(|e| anyhow!("Failed to read header bytes: {}", e))?; let pk_bytes = if header.len() >= 1024 { &header[512..] } else { &header[..] }; let pk = pk_from_content_header(pk_bytes); if cache.db.get(&pk, false).is_ok() { cache.db.update_hit(&pk)?; return Ok(pk); } if let Some(download) = cache.get_download(&pk).await { if download.finished().await { cache.db.update_hit(&pk)?; } return Ok(pk); } let is_flac = is_flac_magic_header(&header); if !is_flac { reader .rewind() .await .map_err(|e| anyhow!("Failed to rewind local file: {}", e))?; return cache .add_from_reader_with_pk(Some(&file_url), reader, length, collection, Some(pk)) .await; } let mut metadata = vec![ ("local_passthrough".to_string(), json!(true)), ( "local_source_path".to_string(), json!(canonical_path.to_string_lossy().to_string()), ), ]; if let Some(len) = length { metadata.push(("source_size".to_string(), json!(len))); } cache .register_local_file_reference( &pk, &canonical_path, collection, Some(&file_url), Some(&metadata), ) .await?; Ok(pk) } /// Ajoute une piste audio depuis une URL avec extraction et stockage des métadonnées /// /// Cette fonction étend `add_from_url` du cache en ajoutant : /// 1. Téléchargement et conversion FLAC (via transformer) /// 2. Extraction et stockage des métadonnées en JSON dans la DB /// /// # Arguments /// /// * `cache` - Instance du cache /// * `url` - URL du fichier audio /// * `collection` - Collection optionnelle (ex: "pink_floyd:wish_you_were_here") /// /// # Returns /// /// Clé primaire (pk) du fichier ajouté /// /// # Exemple /// /// ```rust,no_run /// use pmoaudiocache::cache; /// /// # async fn example() -> anyhow::Result<()> { /// let cache = cache::new_cache("./audio_cache", 1000)?; /// let pk = cache::add_with_metadata_extraction( /// &cache, /// "https://example.com/track.flac", /// Some("artist:album") /// ).await?; /// # Ok(()) /// # } /// ``` pub async fn add_with_metadata_extraction( cache: Arc, url: &str, collection: Option<&str>, ) -> Result { 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.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 depuis le fichier audio let metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?; // 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((sr, bps, total_samples)) = streaminfo { 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 _ = meta.set_duration(Some(Duration::from_secs(secs))).await; } } // 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(title) = metadata.title { let _ = meta.set_title(Some(title)).await; } 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 _ = 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 _ = meta.set_track_number(Some(track_number)).await; } if let Some(track_total) = metadata.track_total { let _ = meta.set_track_total(Some(track_total)).await; } if let Some(disc_number) = metadata.disc_number { let _ = meta.set_disc_number(Some(disc_number)).await; } if let Some(disc_total) = metadata.disc_total { let _ = meta.set_disc_total(Some(disc_total)).await; } if let Some(channels) = metadata.channels { let _ = meta.set_channels(Some(channels)).await; } 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 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) } /// 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)> { // Expect "fLaC" + 4-byte metadata block header + 34-byte STREAMINFO if data.len() < 4 + 4 + 34 { return None; } if &data[0..4] != b"fLaC" { return None; } let block_type = data[4] & 0x7F; let block_len = ((data[5] as usize) << 16) | ((data[6] as usize) << 8) | data[7] as usize; if block_type != 0 || block_len < 34 || data.len() < 8 + block_len { return None; } let s = &data[8..8 + 34]; // sample_rate: 20 bits: bytes 10..12 let sample_rate = ((s[10] as u32) << 12) | ((s[11] as u32) << 4) | ((s[12] as u32 & 0xF0) >> 4); // bits_per_sample: 5 bits spanning byte 12 (lsb) and byte 13 (msb) let bps_raw = (((s[12] & 0x01) as u8) << 4) | ((s[13] & 0xF0) >> 4); let bits_per_sample = bps_raw.saturating_add(1); // total_samples: 36 bits: lower 4 bits of byte 13 + bytes 14..17 let total_samples = (((s[13] & 0x0F) as u64) << 32) | ((s[14] as u64) << 24) | ((s[15] as u64) << 16) | ((s[16] as u64) << 8) | (s[17] as u64); Some((sample_rate, bits_per_sample, total_samples)) }