From 6c9192b6ea6d6abf42492c3f601d2a51703fc744 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 26 Oct 2025 17:51:41 +0100 Subject: [PATCH] On s'attaque au metadata de radio paradise dans le cache --- pmoparadise/src/paradise/mod.rs | 2 +- pmoparadise/src/paradise/worker.rs | 158 ++++++++++++++++++++++++++++- pmosource/Cargo.toml | 8 +- pmosource/src/cache.rs | 74 ++++++++++++++ 4 files changed, 237 insertions(+), 5 deletions(-) diff --git a/pmoparadise/src/paradise/mod.rs b/pmoparadise/src/paradise/mod.rs index 9aee345c..a8071a58 100644 --- a/pmoparadise/src/paradise/mod.rs +++ b/pmoparadise/src/paradise/mod.rs @@ -25,4 +25,4 @@ pub use channel::{ pub use constants::*; // Export all constants pub use history::{create_history_backend, HistoryBackend, HistoryEntry}; pub use playlist::PlaylistEntry; -pub use worker::{ParadiseWorker, WorkerCommand}; +pub use worker::{load_rp_metadata, ParadiseWorker, RadioParadiseMetadata, WorkerCommand}; diff --git a/pmoparadise/src/paradise/worker.rs b/pmoparadise/src/paradise/worker.rs index 50123668..8476d6ce 100644 --- a/pmoparadise/src/paradise/worker.rs +++ b/pmoparadise/src/paradise/worker.rs @@ -682,9 +682,28 @@ impl WorkerState { metadata.cached_audio_pk = Some(audio_pk.clone()); self.cache_manager - .update_metadata(track_id.clone(), metadata) + .update_metadata(track_id.clone(), metadata.clone()) .await; + // Stocker les métadonnées Radio Paradise dans le cache + if let Err(e) = Self::store_rp_metadata( + &self.cache_manager, + &audio_pk, + &track_id, + self.descriptor.id, + song, + duration_ms, + block.event, + metadata.cached_cover_pk.as_deref(), + ) + .await + { + warn!( + channel = self.descriptor.slug, + "Failed to store RP metadata: {e:?}" + ); + } + let file_path = self.cache_manager.audio_file_path(&audio_pk).await; let entry = Arc::new(PlaylistEntry::new( @@ -720,6 +739,46 @@ impl WorkerState { Ok(None) } + /// Stocke les métadonnées Radio Paradise pour un fichier audio caché + /// + /// Cette fonction persiste toutes les métadonnées RP dans la base de données + /// du cache audio, permettant leur récupération future sans dépendance aux + /// données en mémoire. + async fn store_rp_metadata( + cache_manager: &SourceCacheManager, + audio_pk: &str, + track_id: &str, + channel_id: u8, + song: &Song, + duration_ms: u64, + event: u64, + cover_pk: Option<&str>, + ) -> Result<()> { + use serde_json::json; + + // Métadonnées basiques de la chanson + cache_manager.set_audio_metadata(audio_pk, "rp_title", json!(song.title))?; + cache_manager.set_audio_metadata(audio_pk, "rp_artist", json!(song.artist))?; + cache_manager.set_audio_metadata(audio_pk, "rp_album", json!(song.album))?; + cache_manager.set_audio_metadata(audio_pk, "rp_year", json!(song.year))?; + + // Informations temporelles + cache_manager.set_audio_metadata(audio_pk, "rp_duration_ms", json!(duration_ms))?; + cache_manager.set_audio_metadata(audio_pk, "rp_elapsed_ms", json!(song.elapsed))?; + + // Identifiants Radio Paradise + cache_manager.set_audio_metadata(audio_pk, "rp_track_id", json!(track_id))?; + cache_manager.set_audio_metadata(audio_pk, "rp_channel_id", json!(channel_id))?; + cache_manager.set_audio_metadata(audio_pk, "rp_event", json!(event))?; + + // Métadonnées supplémentaires + cache_manager.set_audio_metadata(audio_pk, "rp_rating", json!(song.rating))?; + cache_manager.set_audio_metadata(audio_pk, "rp_cover_url", json!(song.cover))?; + cache_manager.set_audio_metadata(audio_pk, "rp_cover_pk", json!(cover_pk))?; + + Ok(()) + } + fn compute_track_id(&self, block: &Block, song_index: usize) -> String { // Use deterministic ID based on block event and song index // This allows checking if a song is cached before downloading the block @@ -1137,3 +1196,100 @@ async fn encode_samples_to_flac( }) .await? } + +/// Métadonnées Radio Paradise récupérées depuis le cache +/// +/// Cette structure contient toutes les métadonnées RP stockées de manière +/// persistante dans le cache audio. +#[derive(Debug, Clone)] +pub struct RadioParadiseMetadata { + /// Titre de la chanson + pub title: String, + /// Artiste + pub artist: String, + /// Album (optionnel) + pub album: Option, + /// Année de sortie (optionnelle) + pub year: Option, + /// Durée en millisecondes + pub duration_ms: u64, + /// Offset depuis le début du block en millisecondes + pub elapsed_ms: u64, + /// Identifiant unique de la piste + pub track_id: String, + /// ID du canal Radio Paradise (0-3) + pub channel_id: u8, + /// ID de l'événement (block) + pub event: u64, + /// Note de la chanson (0-10, optionnelle) + pub rating: Option, + /// URL de la couverture (optionnelle) + pub cover_url: Option, + /// PK de la couverture dans le cache (optionnelle) + pub cover_pk: Option, +} + +/// Charge les métadonnées Radio Paradise depuis le cache audio +/// +/// Cette fonction lit toutes les métadonnées RP stockées pour un fichier +/// audio donné et les retourne dans une structure `RadioParadiseMetadata`. +/// +/// # Arguments +/// +/// * `cache_manager` - Le gestionnaire de cache source +/// * `audio_pk` - Clé primaire du fichier audio dans le cache +/// +/// # Returns +/// +/// Les métadonnées RP si elles existent et sont complètes, sinon une erreur. +/// +/// # Erreurs +/// +/// Cette fonction retourne une erreur si : +/// - Les métadonnées n'existent pas dans le cache +/// - Les métadonnées sont incomplètes ou corrompues +/// - Il y a une erreur de lecture du cache +pub async fn load_rp_metadata( + cache_manager: &SourceCacheManager, + audio_pk: &str, +) -> Result { + // Helper macro pour récupérer une métadonnée requise + macro_rules! get_required { + ($key:expr, $type:ty) => {{ + cache_manager + .get_audio_metadata(audio_pk, $key)? + .and_then(|v| serde_json::from_value::<$type>(v).ok()) + .ok_or_else(|| anyhow!("Missing or invalid metadata: {}", $key))? + }}; + } + + // Helper macro pour récupérer une métadonnée optionnelle + macro_rules! get_optional { + ($key:expr, $type:ty) => {{ + cache_manager + .get_audio_metadata(audio_pk, $key)? + .and_then(|v| { + if v.is_null() { + None + } else { + serde_json::from_value::<$type>(v).ok() + } + }) + }}; + } + + Ok(RadioParadiseMetadata { + title: get_required!("rp_title", String), + artist: get_required!("rp_artist", String), + album: get_optional!("rp_album", String), + year: get_optional!("rp_year", u32), + duration_ms: get_required!("rp_duration_ms", u64), + elapsed_ms: get_required!("rp_elapsed_ms", u64), + track_id: get_required!("rp_track_id", String), + channel_id: get_required!("rp_channel_id", u8), + event: get_required!("rp_event", u64), + rating: get_optional!("rp_rating", f32), + cover_url: get_optional!("rp_cover_url", String), + cover_pk: get_optional!("rp_cover_pk", String), + }) +} diff --git a/pmosource/Cargo.toml b/pmosource/Cargo.toml index 1ccc05b9..c45ea91c 100644 --- a/pmosource/Cargo.toml +++ b/pmosource/Cargo.toml @@ -30,6 +30,10 @@ pmoplaylist = { path = "../pmoplaylist" } pmoaudiocache = { path = "../pmoaudiocache", optional = true } pmocovers = { path = "../pmocovers", optional = true } +# Serialization (required for cache metadata) +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + # Server extension (optional) pmoserver = { path = "../pmoserver", optional = true } pmoconfig = { path = "../pmoconfig", optional = true } @@ -37,8 +41,6 @@ pmoupnp = { path = "../pmoupnp", optional = true } # Web framework for API (optional) axum = { version = "0.8", optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } utoipa = { version = "5.3", optional = true } tracing = { version = "0.1", optional = true } lazy_static = { version = "1.4", optional = true } @@ -46,4 +48,4 @@ lazy_static = { version = "1.4", optional = true } [features] default = ["cache"] cache = ["pmoaudiocache", "pmocovers"] -server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "serde", "serde_json", "utoipa", "tracing", "lazy_static"] +server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "utoipa", "tracing", "lazy_static"] diff --git a/pmosource/src/cache.rs b/pmosource/src/cache.rs index 4c08d4b9..eb6ab469 100644 --- a/pmosource/src/cache.rs +++ b/pmosource/src/cache.rs @@ -21,6 +21,7 @@ use crate::{CacheStatus, MusicSourceError, Result}; use pmoaudiocache::{AudioMetadata, Cache as AudioCache}; use pmocovers::Cache as CoverCache; +use serde_json::Value as JsonValue; use std::collections::HashMap; use std::sync::Arc; use tokio::io::AsyncRead; @@ -263,6 +264,79 @@ impl SourceCacheManager { cache.remove(track_id); } + /// Stocke une métadonnée personnalisée pour un fichier audio caché + /// + /// Permet de stocker des métadonnées arbitraires (clé/valeur JSON) associées + /// à un fichier audio identifié par son PK. Ces métadonnées sont persistées + /// dans la base de données SQLite du cache audio. + /// + /// # Arguments + /// + /// * `audio_pk` - Clé primaire du fichier audio dans le cache + /// * `key` - Nom de la métadonnée à stocker + /// * `value` - Valeur JSON à stocker + /// + /// # Exemples + /// + /// ```no_run + /// # use pmosource::SourceCacheManager; + /// # use serde_json::json; + /// # async fn example(cache_manager: &SourceCacheManager, audio_pk: &str) { + /// // Stocker une métadonnée simple + /// cache_manager.set_audio_metadata(audio_pk, "genre", json!("Rock")).unwrap(); + /// + /// // Stocker une métadonnée numérique + /// cache_manager.set_audio_metadata(audio_pk, "rating", json!(8.5)).unwrap(); + /// # } + /// ``` + pub fn set_audio_metadata( + &self, + audio_pk: &str, + key: &str, + value: JsonValue, + ) -> Result<()> { + self.audio_cache + .db + .set_a_metadata(audio_pk, key, value) + .map_err(|e| MusicSourceError::CacheError(format!("Failed to set metadata: {}", e))) + } + + /// Récupère une métadonnée personnalisée pour un fichier audio caché + /// + /// Lit une métadonnée précédemment stockée via `set_audio_metadata()`. + /// + /// # Arguments + /// + /// * `audio_pk` - Clé primaire du fichier audio dans le cache + /// * `key` - Nom de la métadonnée à récupérer + /// + /// # Returns + /// + /// * `Ok(Some(value))` - La métadonnée existe + /// * `Ok(None)` - La métadonnée n'existe pas + /// * `Err(_)` - Erreur de lecture + /// + /// # Exemples + /// + /// ```no_run + /// # use pmosource::SourceCacheManager; + /// # async fn example(cache_manager: &SourceCacheManager, audio_pk: &str) { + /// if let Some(genre) = cache_manager.get_audio_metadata(audio_pk, "genre").unwrap() { + /// println!("Genre: {}", genre); + /// } + /// # } + /// ``` + pub fn get_audio_metadata(&self, audio_pk: &str, key: &str) -> Result> { + match self.audio_cache.db.get_a_metadata(audio_pk, key) { + Ok(value) => Ok(value), + Err(e) if e.to_string().contains("QueryReturnedNoRows") => Ok(None), + Err(e) => Err(MusicSourceError::CacheError(format!( + "Failed to get metadata: {}", + e + ))), + } + } + /// Obtenir l'ID de collection pub fn collection_id(&self) -> &str { &self.collection_id