Files
pmomusic/pmoradiofrance/src/config_ext.rs
Eric Coissac 8adccaa2df feat: implémente client stateful et support playlist UPnP
Ajout d'un client stateful avec gestion automatique du cache pour les stations et métadonnées live.

Fonctionnalités principales :
- Client stateful avec cache à deux niveaux (stations 7 jours, métadonnées TTL dynamique)
- Support des playlists UPnP avec structures StationGroups et StationPlaylist
- Gestion intelligente du TTL respectant delayToRefresh de l'API
- Intégration avec pmoconfig pour le caching des stations
- Support des covers via pmocovers avec fallback sur URLs Pikapi
- Doctests compilés avec contexte async complet
- Feature playlist avec dépendance pmodidl

Corrections et améliorations :
- Ajout d'un variant Error::Config pour conversion automatique
- Gestion propre des erreurs avec conversion anyhow::Error
- Tests unitaires et d'intégration (26/26 passés)
- Optimisations : pooling de connexions HTTP, préchargement intelligent

Les stations sont organisées hiérarchiquement : standalone, groupes avec webradios, radios locales ICI (ex-France Bleu)
Les métadonnées sont mises à jour dynamiquement avec respect des intervalles de polling
2026-01-22 23:50:41 +01:00

244 lines
8.1 KiB
Rust

//! Extension pour intégrer Radio France dans pmoconfig
//!
//! Ce module fournit le trait `RadioFranceConfigExt` qui permet d'ajouter
//! des méthodes de gestion de la configuration Radio France à pmoconfig::Config.
//!
//! # Fonctionnalités
//!
//! - Activation/désactivation de la source
//! - Cache de la liste des stations (TTL configurable, défaut 7 jours)
//! - Configuration minimale (pas de sur-configuration)
//!
//! # Exemple
//!
//! ```no_run
//! use pmoconfig::get_config;
//! use pmoradiofrance::RadioFranceConfigExt;
//!
//! # fn main() -> anyhow::Result<()> {
//! let config = get_config();
//!
//! // Check if enabled
//! if !config.get_radiofrance_enabled()? {
//! println!("Radio France is disabled");
//! return Ok(());
//! }
//!
//! // Get cached stations (or None if cache expired/empty)
//! if let Some(cached) = config.get_radiofrance_cached_stations()? {
//! println!("Found {} cached stations", cached.stations.len());
//! }
//! # Ok(())
//! # }
//! ```
use crate::models::{CachedStationList, Station};
use anyhow::Result;
use pmoconfig::Config;
use serde_yaml::Value;
/// Default TTL for station list cache (7 days in seconds)
pub const DEFAULT_STATION_CACHE_TTL_SECS: u64 = 7 * 24 * 3600;
/// Trait d'extension pour gérer la configuration Radio France dans pmoconfig
///
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
/// à la gestion de Radio France, incluant :
///
/// - Activation/désactivation
/// - Cache de la liste des stations
///
/// # Auto-persist des valeurs par défaut
///
/// Les getters persistent automatiquement les valeurs par défaut dans la
/// configuration si elles n'existent pas encore.
pub trait RadioFranceConfigExt {
// ========================================================================
// Enable/Disable
// ========================================================================
/// Vérifie si Radio France est activé
///
/// # Returns
///
/// `true` si la source est activée (default), `false` sinon.
fn get_radiofrance_enabled(&self) -> Result<bool>;
/// Active ou désactive Radio France
fn set_radiofrance_enabled(&self, enabled: bool) -> Result<()>;
// ========================================================================
// Station Cache
// ========================================================================
/// Récupère la liste des stations en cache
///
/// # Returns
///
/// - `Some(CachedStationList)` si le cache existe et est valide
/// - `None` si le cache n'existe pas ou est expiré
///
/// # Cache Validation
///
/// Le cache est considéré invalide si :
/// - Il n'existe pas
/// - Son TTL est dépassé (configurable, défaut 7 jours)
/// - Sa version ne correspond pas à la version actuelle de l'algorithme
fn get_radiofrance_cached_stations(&self) -> Result<Option<CachedStationList>>;
/// Enregistre la liste des stations en cache
///
/// # Arguments
///
/// * `stations` - Liste des stations découvertes
fn set_radiofrance_cached_stations(&self, stations: &[Station]) -> Result<()>;
/// Récupère le TTL du cache des stations (en secondes)
///
/// # Returns
///
/// Le TTL en secondes (default: 7 jours)
fn get_radiofrance_station_cache_ttl(&self) -> Result<u64>;
/// Définit le TTL du cache des stations (en secondes)
fn set_radiofrance_station_cache_ttl(&self, ttl_secs: u64) -> Result<()>;
/// Vérifie si le cache des stations est valide
///
/// Raccourci pour `get_radiofrance_cached_stations()?.is_some()`
fn is_radiofrance_station_cache_valid(&self) -> bool;
/// Efface le cache des stations (force re-découverte)
fn clear_radiofrance_station_cache(&self) -> Result<()>;
// ========================================================================
// High-level helpers
// ========================================================================
/// Récupère les stations, en utilisant le cache si valide
///
/// Cette méthode est un helper qui :
/// 1. Vérifie le cache
/// 2. Si valide, retourne les stations du cache
/// 3. Si invalide, retourne None (l'appelant doit découvrir et mettre en cache)
///
/// # Example
///
/// ```no_run
/// # use pmoconfig::get_config;
/// # use pmoradiofrance::{RadioFranceConfigExt, RadioFranceClient};
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
/// let config = get_config();
/// let stations = if let Some(cached) = config.get_radiofrance_stations_cached()? {
/// cached
/// } else {
/// let client = RadioFranceClient::new().await?;
/// let discovered = client.discover_all_stations().await?;
/// config.set_radiofrance_cached_stations(&discovered)?;
/// discovered
/// };
/// # Ok(())
/// # }
/// ```
fn get_radiofrance_stations_cached(&self) -> Result<Option<Vec<Station>>>;
}
impl RadioFranceConfigExt for Config {
fn get_radiofrance_enabled(&self) -> Result<bool> {
match self.get_value(&["sources", "radiofrance", "enabled"]) {
Ok(Value::Bool(b)) => Ok(b),
_ => {
// Default: enabled
self.set_radiofrance_enabled(true)?;
Ok(true)
}
}
}
fn set_radiofrance_enabled(&self, enabled: bool) -> Result<()> {
self.set_value(&["sources", "radiofrance", "enabled"], Value::Bool(enabled))
}
fn get_radiofrance_cached_stations(&self) -> Result<Option<CachedStationList>> {
let ttl = self.get_radiofrance_station_cache_ttl()?;
match self.get_value(&["sources", "radiofrance", "station_cache"]) {
Ok(value) => {
// Try to deserialize the cached data
let cached: CachedStationList = serde_yaml::from_value(value)?;
// Check validity
if cached.is_valid(ttl) {
Ok(Some(cached))
} else {
// Cache expired or version mismatch
Ok(None)
}
}
Err(_) => Ok(None),
}
}
fn set_radiofrance_cached_stations(&self, stations: &[Station]) -> Result<()> {
let cached = CachedStationList::new(stations.to_vec());
let value = serde_yaml::to_value(&cached)?;
self.set_value(&["sources", "radiofrance", "station_cache"], value)
}
fn get_radiofrance_station_cache_ttl(&self) -> Result<u64> {
match self.get_value(&["sources", "radiofrance", "station_cache_ttl_secs"]) {
Ok(Value::Number(n)) => {
if let Some(ttl) = n.as_u64() {
Ok(ttl)
} else {
// Invalid number, use default
self.set_radiofrance_station_cache_ttl(DEFAULT_STATION_CACHE_TTL_SECS)?;
Ok(DEFAULT_STATION_CACHE_TTL_SECS)
}
}
_ => {
// Not set, use default and persist
self.set_radiofrance_station_cache_ttl(DEFAULT_STATION_CACHE_TTL_SECS)?;
Ok(DEFAULT_STATION_CACHE_TTL_SECS)
}
}
}
fn set_radiofrance_station_cache_ttl(&self, ttl_secs: u64) -> Result<()> {
self.set_value(
&["sources", "radiofrance", "station_cache_ttl_secs"],
Value::Number(serde_yaml::Number::from(ttl_secs)),
)
}
fn is_radiofrance_station_cache_valid(&self) -> bool {
self.get_radiofrance_cached_stations()
.ok()
.flatten()
.is_some()
}
fn clear_radiofrance_station_cache(&self) -> Result<()> {
// Set to null to clear
self.set_value(&["sources", "radiofrance", "station_cache"], Value::Null)
}
fn get_radiofrance_stations_cached(&self) -> Result<Option<Vec<Station>>> {
Ok(self
.get_radiofrance_cached_stations()?
.map(|cached| cached.stations))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_ttl() {
// 7 days in seconds
assert_eq!(DEFAULT_STATION_CACHE_TTL_SECS, 7 * 24 * 3600);
}
}