On s'attaque au metadata de radio paradise dans le cache
This commit is contained in:
@@ -25,4 +25,4 @@ pub use channel::{
|
|||||||
pub use constants::*; // Export all constants
|
pub use constants::*; // Export all constants
|
||||||
pub use history::{create_history_backend, HistoryBackend, HistoryEntry};
|
pub use history::{create_history_backend, HistoryBackend, HistoryEntry};
|
||||||
pub use playlist::PlaylistEntry;
|
pub use playlist::PlaylistEntry;
|
||||||
pub use worker::{ParadiseWorker, WorkerCommand};
|
pub use worker::{load_rp_metadata, ParadiseWorker, RadioParadiseMetadata, WorkerCommand};
|
||||||
|
|||||||
@@ -682,9 +682,28 @@ impl WorkerState {
|
|||||||
|
|
||||||
metadata.cached_audio_pk = Some(audio_pk.clone());
|
metadata.cached_audio_pk = Some(audio_pk.clone());
|
||||||
self.cache_manager
|
self.cache_manager
|
||||||
.update_metadata(track_id.clone(), metadata)
|
.update_metadata(track_id.clone(), metadata.clone())
|
||||||
.await;
|
.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 file_path = self.cache_manager.audio_file_path(&audio_pk).await;
|
||||||
|
|
||||||
let entry = Arc::new(PlaylistEntry::new(
|
let entry = Arc::new(PlaylistEntry::new(
|
||||||
@@ -720,6 +739,46 @@ impl WorkerState {
|
|||||||
Ok(None)
|
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 {
|
fn compute_track_id(&self, block: &Block, song_index: usize) -> String {
|
||||||
// Use deterministic ID based on block event and song index
|
// Use deterministic ID based on block event and song index
|
||||||
// This allows checking if a song is cached before downloading the block
|
// This allows checking if a song is cached before downloading the block
|
||||||
@@ -1137,3 +1196,100 @@ async fn encode_samples_to_flac(
|
|||||||
})
|
})
|
||||||
.await?
|
.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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ pmoplaylist = { path = "../pmoplaylist" }
|
|||||||
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
|
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
|
||||||
pmocovers = { path = "../pmocovers", 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)
|
# Server extension (optional)
|
||||||
pmoserver = { path = "../pmoserver", optional = true }
|
pmoserver = { path = "../pmoserver", optional = true }
|
||||||
pmoconfig = { path = "../pmoconfig", optional = true }
|
pmoconfig = { path = "../pmoconfig", optional = true }
|
||||||
@@ -37,8 +41,6 @@ pmoupnp = { path = "../pmoupnp", optional = true }
|
|||||||
|
|
||||||
# Web framework for API (optional)
|
# Web framework for API (optional)
|
||||||
axum = { version = "0.8", optional = true }
|
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 }
|
utoipa = { version = "5.3", optional = true }
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
lazy_static = { version = "1.4", optional = true }
|
lazy_static = { version = "1.4", optional = true }
|
||||||
@@ -46,4 +48,4 @@ lazy_static = { version = "1.4", optional = true }
|
|||||||
[features]
|
[features]
|
||||||
default = ["cache"]
|
default = ["cache"]
|
||||||
cache = ["pmoaudiocache", "pmocovers"]
|
cache = ["pmoaudiocache", "pmocovers"]
|
||||||
server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "serde", "serde_json", "utoipa", "tracing", "lazy_static"]
|
server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "utoipa", "tracing", "lazy_static"]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
use crate::{CacheStatus, MusicSourceError, Result};
|
use crate::{CacheStatus, MusicSourceError, Result};
|
||||||
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
use pmoaudiocache::{AudioMetadata, Cache as AudioCache};
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
@@ -263,6 +264,79 @@ impl SourceCacheManager {
|
|||||||
cache.remove(track_id);
|
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
|
/// Obtenir l'ID de collection
|
||||||
pub fn collection_id(&self) -> &str {
|
pub fn collection_id(&self) -> &str {
|
||||||
&self.collection_id
|
&self.collection_id
|
||||||
|
|||||||
Reference in New Issue
Block a user