//! 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 anyhow::Result; use pmocache::{CacheConfig, StreamTransformer}; use std::sync::Arc; /// Configuration pour le cache audio pub struct AudioConfig; impl CacheConfig for AudioConfig { fn file_extension() -> &'static str { "flac" } fn table_name() -> &'static str { "audio_tracks" } 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éateur de transformer FLAC /// /// Convertit automatiquement tout fichier audio téléchargé en format FLAC /// en traitant les données au vol, sans tout charger en mémoire. /// /// # Workflow /// /// 1. Télécharger les bytes par chunks depuis le stream HTTP /// 2. Buffer temporaire pour accumuler les données nécessaires à Symphonia /// 3. Décoder l'audio en PCM via Symphonia /// 4. Encoder le PCM en FLAC progressivement via flacenc /// 5. Écrire les frames FLAC directement dans le fichier /// 6. Mettre à jour la progression après chaque chunk /// /// Note: Bien que nous utilisions un buffer temporaire, celui-ci est géré /// de manière efficace et les données FLAC sont écrites au fur et à mesure. fn create_flac_transformer() -> StreamTransformer { Box::new(|response, mut file, progress| { Box::pin(async move { use futures_util::StreamExt; use tokio::io::AsyncWriteExt; // 1. Collecter tous les bytes du stream // Note: Symphonia nécessite un MediaSource avec Read + Seek, // ce qui n'est pas compatible avec un vrai streaming HTTP. // Nous devons donc bufferiser les données. let mut buffer = Vec::new(); let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e| format!("Stream error: {}", e))?; buffer.extend_from_slice(&chunk); } tracing::debug!("Downloaded {} bytes total, starting FLAC conversion", buffer.len()); // 2. Si c'est déjà du FLAC, on l'écrit directement if buffer.len() >= 4 && &buffer[0..4] == b"fLaC" { tracing::debug!("Input is already FLAC, writing directly"); file.write_all(&buffer).await.map_err(|e| e.to_string())?; file.flush().await.map_err(|e| e.to_string())?; progress(buffer.len() as u64); return Ok(()); } tracing::debug!("Converting to FLAC with Symphonia + flacenc"); // 3. Décoder l'audio avec Symphonia let (samples, channels, sample_rate, bits_per_sample) = { use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use symphonia::core::errors::Error as SymphoniaError; use std::io::Cursor; let cursor = Cursor::new(buffer); let mss = MediaSourceStream::new(Box::new(cursor), Default::default()); let hint = Hint::new(); let probed = symphonia::default::get_probe() .format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default()) .map_err(|e| format!("Failed to probe format: {}", e))?; let mut format = probed.format; let track = format .tracks() .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .ok_or_else(|| "No audio track found".to_string())?; let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) .map_err(|e| format!("Failed to create decoder: {}", e))?; let channels = track.codec_params.channels .ok_or_else(|| "No channel info".to_string())? .count(); let sample_rate = track.codec_params.sample_rate .ok_or_else(|| "No sample rate info".to_string())?; let bits_per_sample = track.codec_params.bits_per_sample .unwrap_or(16); let mut samples_i32 = Vec::new(); let track_id = track.id; // Décoder tous les packets loop { let packet = match format.next_packet() { Ok(packet) => packet, Err(SymphoniaError::ResetRequired) => { decoder.reset(); continue; } Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { break; } Err(e) => return Err(format!("Decode error: {}", e)), }; if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(decoded) => { let spec = *decoded.spec(); let duration = decoded.capacity() as u64; // Convertir en i32 pour flacenc // Note: Symphonia retourne des samples i32, nous devons les convertir // en fonction du bits_per_sample réel let mut sample_buf = SampleBuffer::::new(duration, spec); sample_buf.copy_interleaved_ref(decoded); samples_i32.extend_from_slice(sample_buf.samples()); } Err(SymphoniaError::DecodeError(_)) => continue, Err(e) => return Err(format!("Decode error: {}", e)), } } if samples_i32.is_empty() { return Err("No samples decoded".to_string()); } tracing::debug!( "Decoded {} samples (i32), {} channels, {} Hz, {} bits", samples_i32.len(), channels, sample_rate, bits_per_sample ); // Normaliser les samples i32 vers la plage appropriée pour flacenc // Symphonia retourne des samples i32 en pleine échelle (32 bits), // nous devons les normaliser selon le bits_per_sample réel let (normalized_samples, target_bits): (Vec, u32) = match bits_per_sample { 0..=16 => { // Pour 16 bits ou moins, normaliser vers la plage i16 tracing::debug!("Normalizing to 16-bit"); let samples = samples_i32.iter().map(|&s| (s >> 16) as i32).collect(); (samples, 16) }, 17..=24 => { // Pour 17-24 bits, normaliser vers la plage 24-bit tracing::debug!("Normalizing to 24-bit"); let samples = samples_i32.iter().map(|&s| (s >> 8) as i32).collect(); (samples, 24) }, _ => { // Pour 25-32 bits, garder la pleine échelle i32 tracing::debug!("Keeping 32-bit"); (samples_i32, 32) } }; (normalized_samples, channels, sample_rate, target_bits) }; tracing::debug!("Encoding to FLAC: {} samples, {} channels, {} Hz, {} bits", samples.len(), channels, sample_rate, bits_per_sample); // 4. Encoder en FLAC avec flacenc // Note: L'encodage FLAC est une opération bloquante/CPU-intensive, // donc nous l'exécutons dans un thread bloquant pour ne pas bloquer le runtime Tokio let flac_data = tokio::task::spawn_blocking(move || { use flacenc::component::BitRepr; use flacenc::bitsink::ByteSink; use flacenc::error::Verify; let config = flacenc::config::Encoder::default() .into_verified() .map_err(|e| format!("FLAC config error: {:?}", e))?; let source = flacenc::source::MemSource::from_samples( &samples, channels, bits_per_sample as usize, sample_rate as usize, ); let flac_stream = flacenc::encode_with_fixed_block_size( &config, source, config.block_size, ) .map_err(|e| format!("FLAC encode error: {:?}", e))?; let mut sink = ByteSink::new(); flac_stream.write(&mut sink) .map_err(|e| format!("FLAC write error: {:?}", e))?; Ok::, String>(sink.into_inner()) }) .await .map_err(|e| format!("Spawn blocking error: {}", e))??; tracing::debug!("FLAC encoding complete: {} bytes", flac_data.len()); // 5. Écrire le fichier FLAC file.write_all(&flac_data).await.map_err(|e| e.to_string())?; file.flush().await.map_err(|e| e.to_string())?; // 6. Mettre à jour la progression finale progress(flac_data.len() as u64); Ok(()) }) }) } /// 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(|| create_flac_transformer()); Cache::with_transformer(dir, limit, Some(transformer_factory)) } /// 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, "http://localhost:8080")?; /// let pk = cache::add_with_metadata_extraction( /// &cache, /// "http://example.com/track.flac", /// Some("artist:album") /// ).await?; /// # Ok(()) /// # } /// ``` pub async fn add_with_metadata_extraction( cache: &Cache, url: &str, collection: Option<&str>, ) -> Result { // 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?; // Lire le fichier FLAC pour extraire les métadonnées let file_path = cache.file_path(&pk); let flac_bytes = tokio::fs::read(&file_path).await?; // Extraire les métadonnées let metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?; // Sérialiser en JSON let metadata_json = serde_json::to_string(&metadata)?; // Stocker dans la DB cache.db.update_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() { if let Some(auto_collection) = metadata.collection_key() { cache.db.add(&pk, url, Some(&auto_collection)) .map_err(|e| anyhow::anyhow!("Database error: {}", e))?; } } 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 { 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 metadata: crate::metadata::AudioMetadata = serde_json::from_str(&metadata_json) .map_err(|e| anyhow::anyhow!("Metadata deserialization error: {}", e))?; Ok(metadata) }