- Replace old /api/live endpoint with new livemeta/pull API (api.radiofrance.fr) - Add hardcoded station ID/stream mappings with runtime-updatable mapping - Implement station rediscovery on unknown slug or stream failure - Add persistent caching for station mappings (30-day TTL) via pmoconfig - Update live_metadata to use numeric IDs and handle ID mismatches - Add station validation (API + stream accessibility check) - Refactor metadata caching with fetched_at and fallback TTL - Update discover_local_radios to scrape SvelteKit page data - Add new models: PullResponse, PullStep, EmbedImage for livemeta format - Improve live_metadata.rs example to safely truncate intros - Enable parallel DIDL generation for playlist groups
351 lines
12 KiB
Rust
351 lines
12 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::Station;
|
|
use anyhow::Result;
|
|
use pmoconfig::Config;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_yaml::Value;
|
|
use std::collections::HashMap;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
/// Default TTL for station list cache (7 days in seconds)
|
|
pub const DEFAULT_STATION_CACHE_TTL_SECS: u64 = 7 * 24 * 3600;
|
|
|
|
/// Default TTL for station mapping cache (30 days — IDs and stream URLs are very stable)
|
|
pub const DEFAULT_STATION_MAPPING_TTL_SECS: u64 = 30 * 24 * 3600;
|
|
|
|
/// Informations d'une station : ID numérique et URL du stream HiFi
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct StationInfo {
|
|
/// ID numérique pour l'API livemeta/pull
|
|
pub station_id: u32,
|
|
/// URL du stream HiFi (icecast AAC ou MP3)
|
|
pub stream_url: String,
|
|
}
|
|
|
|
/// Cached station list (simplifié)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct CachedStations {
|
|
stations: Vec<Station>,
|
|
last_updated: u64, // Unix timestamp
|
|
}
|
|
|
|
/// Mapping slug → StationInfo avec timestamp de mise à jour
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct CachedStationMapping {
|
|
/// slug → { station_id, stream_url }
|
|
mapping: HashMap<String, StationInfo>,
|
|
last_updated: u64, // Unix timestamp
|
|
}
|
|
|
|
/// 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(Vec<Station>)` 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)
|
|
fn get_radiofrance_stations_cached(&self) -> Result<Option<Vec<Station>>>;
|
|
|
|
/// 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<()>;
|
|
|
|
/// Efface le cache des stations (force re-découverte)
|
|
fn clear_radiofrance_station_cache(&self) -> Result<()>;
|
|
|
|
// ========================================================================
|
|
// Station Mapping Cache (slug → station_id + stream_url)
|
|
// ========================================================================
|
|
|
|
/// Récupère le mapping slug → StationInfo depuis le cache (si valide)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// - `Some(HashMap)` si le cache existe et n'est pas expiré
|
|
/// - `None` si absent ou TTL dépassé → il faut re-découvrir
|
|
fn get_radiofrance_station_mapping(&self) -> Result<Option<HashMap<String, StationInfo>>>;
|
|
|
|
/// Enregistre le mapping slug → StationInfo en cache
|
|
fn set_radiofrance_station_mapping(&self, mapping: &HashMap<String, StationInfo>)
|
|
-> Result<()>;
|
|
|
|
/// Récupère le TTL du mapping (en secondes, défaut 30 jours)
|
|
fn get_radiofrance_mapping_ttl(&self) -> Result<u64>;
|
|
|
|
/// Définit le TTL du mapping (en secondes)
|
|
fn set_radiofrance_mapping_ttl(&self, ttl_secs: u64) -> Result<()>;
|
|
|
|
/// Efface le cache du mapping (force re-découverte au prochain accès)
|
|
fn clear_radiofrance_station_mapping(&self) -> Result<()>;
|
|
|
|
/// Met à jour une seule entrée dans le mapping (sans invalider tout le cache)
|
|
///
|
|
/// Utilisé pour la mise à jour incrémentale lors d'une re-découverte unitaire.
|
|
fn upsert_radiofrance_station_info(&self, slug: &str, info: StationInfo) -> Result<()>;
|
|
}
|
|
|
|
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_stations_cached(&self) -> Result<Option<Vec<Station>>> {
|
|
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: CachedStations = serde_yaml::from_value(value)?;
|
|
|
|
// Check validity (TTL)
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
|
|
if now - cached.last_updated < ttl {
|
|
Ok(Some(cached.stations))
|
|
} else {
|
|
// Cache expired
|
|
Ok(None)
|
|
}
|
|
}
|
|
Err(_) => Ok(None),
|
|
}
|
|
}
|
|
|
|
fn set_radiofrance_cached_stations(&self, stations: &[Station]) -> Result<()> {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
|
|
let cached = CachedStations {
|
|
stations: stations.to_vec(),
|
|
last_updated: now,
|
|
};
|
|
|
|
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 clear_radiofrance_station_cache(&self) -> Result<()> {
|
|
// Set to null to clear
|
|
self.set_value(&["sources", "radiofrance", "station_cache"], Value::Null)
|
|
}
|
|
|
|
fn get_radiofrance_station_mapping(&self) -> Result<Option<HashMap<String, StationInfo>>> {
|
|
let ttl = self.get_radiofrance_mapping_ttl()?;
|
|
|
|
match self.get_value(&["sources", "radiofrance", "station_mapping"]) {
|
|
Ok(value) => {
|
|
let cached: CachedStationMapping = serde_yaml::from_value(value)?;
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
|
|
if now - cached.last_updated < ttl {
|
|
Ok(Some(cached.mapping))
|
|
} else {
|
|
Ok(None) // Expiré
|
|
}
|
|
}
|
|
Err(_) => Ok(None), // Absent
|
|
}
|
|
}
|
|
|
|
fn set_radiofrance_station_mapping(
|
|
&self,
|
|
mapping: &HashMap<String, StationInfo>,
|
|
) -> Result<()> {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
|
|
let cached = CachedStationMapping {
|
|
mapping: mapping.clone(),
|
|
last_updated: now,
|
|
};
|
|
|
|
let value = serde_yaml::to_value(&cached)?;
|
|
self.set_value(&["sources", "radiofrance", "station_mapping"], value)
|
|
}
|
|
|
|
fn get_radiofrance_mapping_ttl(&self) -> Result<u64> {
|
|
match self.get_value(&["sources", "radiofrance", "station_mapping_ttl_secs"]) {
|
|
Ok(Value::Number(n)) => {
|
|
if let Some(ttl) = n.as_u64() {
|
|
Ok(ttl)
|
|
} else {
|
|
self.set_radiofrance_mapping_ttl(DEFAULT_STATION_MAPPING_TTL_SECS)?;
|
|
Ok(DEFAULT_STATION_MAPPING_TTL_SECS)
|
|
}
|
|
}
|
|
_ => {
|
|
self.set_radiofrance_mapping_ttl(DEFAULT_STATION_MAPPING_TTL_SECS)?;
|
|
Ok(DEFAULT_STATION_MAPPING_TTL_SECS)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_radiofrance_mapping_ttl(&self, ttl_secs: u64) -> Result<()> {
|
|
self.set_value(
|
|
&["sources", "radiofrance", "station_mapping_ttl_secs"],
|
|
Value::Number(serde_yaml::Number::from(ttl_secs)),
|
|
)
|
|
}
|
|
|
|
fn clear_radiofrance_station_mapping(&self) -> Result<()> {
|
|
self.set_value(
|
|
&["sources", "radiofrance", "station_mapping"],
|
|
Value::Null,
|
|
)
|
|
}
|
|
|
|
fn upsert_radiofrance_station_info(&self, slug: &str, info: StationInfo) -> Result<()> {
|
|
// Charger le mapping existant (même expiré) pour mise à jour incrémentale
|
|
let mut mapping = match self.get_value(&["sources", "radiofrance", "station_mapping"]) {
|
|
Ok(value) => serde_yaml::from_value::<CachedStationMapping>(value)
|
|
.map(|c| c.mapping)
|
|
.unwrap_or_default(),
|
|
Err(_) => HashMap::new(),
|
|
};
|
|
|
|
mapping.insert(slug.to_string(), info);
|
|
|
|
self.set_radiofrance_station_mapping(&mapping)
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|