On s'attaque au metadata de radio paradise dans le cache

This commit is contained in:
2025-10-26 17:51:41 +01:00
parent 87bf71f81b
commit a9ec9e9991
4 changed files with 237 additions and 5 deletions

View File

@@ -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};

View File

@@ -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<String>,
/// Année de sortie (optionnelle)
pub year: Option<u32>,
/// 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<f32>,
/// URL de la couverture (optionnelle)
pub cover_url: Option<String>,
/// PK de la couverture dans le cache (optionnelle)
pub cover_pk: Option<String>,
}
/// 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<RadioParadiseMetadata> {
// 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),
})
}

View File

@@ -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"]

View File

@@ -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<Option<JsonValue>> {
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