# API Radio France - Documentation complète ## Vue d'ensemble Radio France expose plusieurs APIs publiques **sans authentification** pour accéder aux métadonnées des émissions en direct et aux flux audio. **Date d'analyse :** 2026-01-22 **Sources :** Analyse de fichiers HAR + tests directs --- ## 1. API Live par station ### Format général ``` https://www.radiofrance.fr/{station}/api/live? ``` ### Stations disponibles | Station | Endpoint | Status | |---------|----------|--------| | France Inter | `/franceinter/api/live?` | ✅ Fonctionne | | France Info | `/franceinfo/api/live?` | ✅ Fonctionne | | France Culture | `/franceculture/api/live?` | ✅ Fonctionne | | France Musique | `/francemusique/api/live?` | ✅ Fonctionne | | FIP | `/fip/api/live?` | ✅ Fonctionne | | Mouv' | `/mouv/api/live?` | ✅ Fonctionne | | France Bleu (national) | `/francebleu/api/live?` | ✅ Fonctionne | | Mon Petit France Inter | `/monpetitfranceinter/api/live?` | ✅ Fonctionne | ### Structure de réponse ```json { "stationName": "franceculture", "delayToRefresh": 262000, "migrated": true, "now": { "printProgMusic": true, "startTime": 1769108400, "endTime": 1769110122, "producer": "Nom du producteur", "firstLine": { "title": "Nom de l'émission", "id": "uuid-emission", "path": "franceculture/podcasts/emission" }, "secondLine": { "title": "Titre de l'épisode/chronique", "id": "uuid-episode", "path": "franceculture/podcasts/emission/episode" }, "thirdLine": { "title": "Sous-titre éventuel", "id": "uuid", "path": null }, "intro": "Description de l'émission...", "reactAvailable": false, "visualBackground": { "model": "EmbedImage", "src": "https://www.radiofrance.fr/pikapi/images/uuid", "width": 4000, "height": 1000, "dominant": "#c8e8f8", "copyright": "Radio France" }, "song": { "id": "uuid-morceau", "year": 2024, "interpreters": ["Artiste"], "release": { "label": "Label", "title": "Album", "reference": null } }, "media": { "sources": [ { "url": "https://icecast.radiofrance.fr/franceculture-lofi.mp3?id=radiofrance", "broadcastType": "live", "format": "mp3", "bitrate": 32 }, { "url": "https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance", "broadcastType": "live", "format": "hls", "bitrate": 0 }, { "url": "https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance", "broadcastType": "live", "format": "aac", "bitrate": 192 }, { "url": "https://icecast.radiofrance.fr/franceculture-midfi.aac?id=radiofrance", "broadcastType": "live", "format": "aac", "bitrate": 128 }, { "url": "https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance", "broadcastType": "timeshift", "format": "hls", "bitrate": 0 } ] }, "localRadios": [], "visuals": { "card": { /* Image pour la carte */ }, "player": { /* Image pour le player */ } } }, "next": { /* Même structure pour l'émission suivante */ } } ``` ### Champs importants - **`delayToRefresh`** : Temps en millisecondes avant le prochain rafraîchissement recommandé - **`now.song`** : Présent si c'est une musique (FIP, France Musique) - **`now.media.sources`** : Liste de tous les flux disponibles avec formats et bitrates - **`localRadios`** : Liste des radios locales (pour France Bleu) --- ## 2. API LiveMeta (ancienne API, toujours fonctionnelle) ### Format ``` https://api.radiofrance.fr/livemeta/live/{id}/transistor_{station}_player ``` ### IDs connus | Station | ID | Endpoint | |---------|-----|----------| | France Culture | 5 | `/livemeta/live/5/transistor_culture_player` | ### Exemple de réponse ```json { "prev": [{ "firstLine": "Le direct", "secondLine": "France Culture, l'esprit d'ouverture", "cover": "uuid-image", "startTime": null, "endTime": null }], "now": { "firstLine": "La Série fiction", "firstLineUuid": "uuid", "firstLinePath": "franceculture/podcasts/emission", "secondLine": "Titre de l'épisode", "cover": "uuid-image", "startTime": 1769108400, "endTime": 1769110122 }, "next": [{ /* émission suivante */ }], "delayToRefresh": 742000 } ``` **Note :** Cette API retourne moins de détails que `/api/live?` mais fonctionne toujours. --- ## 3. Flux audio ### Format des URLs #### HLS (recommandé) ``` https://stream.radiofrance.fr/{station}/{station}.m3u8?id=radiofrance ``` #### Icecast (AAC et MP3) ``` https://icecast.radiofrance.fr/{station}-{qualite}.{format}?id=radiofrance ``` ### Qualités disponibles | Qualité | Bitrate AAC | Bitrate MP3 | Utilisation | |---------|-------------|-------------|-------------| | `lofi` | 32 kbps | 32 kbps | Connexions lentes | | `midfi` | 128 kbps | 128 kbps | Standard | | `hifi` | 192 kbps | - | Haute qualité | ### Exemples d'URLs **France Culture :** ``` https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance https://icecast.radiofrance.fr/franceculture-midfi.aac?id=radiofrance https://icecast.radiofrance.fr/franceculture-midfi.mp3?id=radiofrance https://icecast.radiofrance.fr/franceculture-lofi.aac?id=radiofrance https://icecast.radiofrance.fr/franceculture-lofi.mp3?id=radiofrance ``` **France Inter :** ``` https://stream.radiofrance.fr/franceinter/franceinter.m3u8?id=radiofrance https://icecast.radiofrance.fr/franceinter-hifi.aac?id=radiofrance https://icecast.radiofrance.fr/franceinter-midfi.aac?id=radiofrance https://icecast.radiofrance.fr/franceinter-midfi.mp3?id=radiofrance https://icecast.radiofrance.fr/franceinter-lofi.aac?id=radiofrance https://icecast.radiofrance.fr/franceinter-lofi.mp3?id=radiofrance ``` --- ## 4. Webradios thématiques ### FIP Webradios FIP propose plusieurs webradios thématiques. Format des URLs : ``` https://icecast.radiofrance.fr/fip{variant}-{qualite}.aac?id=radiofrance ``` #### Variantes disponibles (confirmées) | Variante | URL | Status | |----------|-----|--------| | FIP principale | `fip-hifi.aac` | ✅ | | FIP Rock | `fiprock-hifi.aac` | ✅ | | FIP Jazz | `fipjazz-hifi.aac` | ✅ | | FIP Groove | `fipgroove-hifi.aac` | ✅ | | FIP Reggae | `fipreggae-hifi.aac` | ✅ | | FIP Electro | `fipelectro-hifi.aac` | ✅ | | FIP Metal | `fipmetal-hifi.aac` | ✅ | | FIP Nouveautés | `fipnouveautes-hifi.aac` | ✅ | | FIP Pop | `fippop-hifi.aac` | ✅ | **Exemples :** ``` https://icecast.radiofrance.fr/fiprock-hifi.aac?id=radiofrance https://icecast.radiofrance.fr/fipjazz-midfi.aac?id=radiofrance https://icecast.radiofrance.fr/fipgroove-lofi.aac?id=radiofrance ``` ### France Musique Webradios Format similaire : ``` https://icecast.radiofrance.fr/francemusique{variant}-{qualite}.aac?id=radiofrance ``` #### Variantes disponibles (confirmées) | Variante | URL | Status | |----------|-----|--------| | France Musique principale | `francemusique-hifi.aac` | ✅ | | La Jazz | `francemusiquelajazz-hifi.aac` | ✅ | | La Contemporaine | `francemusiquelacontemporaine-hifi.aac` | ✅ | | Baroque | `francemusiquebaroque-hifi.aac` | ✅ | | Opéra | `francemusiqueopera-hifi.aac` | ✅ | **Exemples :** ``` https://icecast.radiofrance.fr/francemusiquelajazz-hifi.aac?id=radiofrance https://icecast.radiofrance.fr/francemusiquebaroque-midfi.aac?id=radiofrance ``` --- ## 5. France Bleu - Radios locales ### API ``` https://www.radiofrance.fr/francebleu/api/live? ``` ### Structure spécifique Le champ `localRadios` contient la liste de toutes les radios locales : ```json { "stationName": "francebleu", "delayToRefresh": 2090000, "now": { /* ... */ }, "localRadios": [ { "id": 12, "title": "ICI Alsace", "name": "francebleu_alsace", "isOnAir": true }, { "id": 13, "title": "ICI Armorique", "name": "francebleu_armorique", "isOnAir": true } // ... ~40 radios locales ] } ``` ### Format des flux locaux **Hypothèse (à confirmer) :** ``` https://icecast.radiofrance.fr/fb{nom}-hifi.aac?id=radiofrance ``` Exemple : ``` https://icecast.radiofrance.fr/fbalsace-hifi.aac?id=radiofrance ``` --- ## 6. API Pikapi (Images) ### Format ``` https://www.radiofrance.fr/pikapi/images/{uuid}/{taille} ``` ### Tailles disponibles Basé sur l'analyse des réponses, plusieurs tailles semblent disponibles : - `88x88` - Miniature - `200x200` - Petite - `420x720` - Moyenne portrait - `560x960` - Grande portrait - `1200x680` - Grande paysage - `raw` - Taille originale **Exemples :** ``` https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39/200x200 https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39/1200x680 ``` --- ## 7. Autres endpoints (configuration) D'après l'analyse du fichier HAR, voici d'autres endpoints internes utilisés : ### Endpoints de configuration (dans `__data.json`) - **`https://kirby.radiofrance.fr`** - CMS Kirby - **`https://www.radiofrance.fr/pikapi`** - API images - **`https://www.radiofrance.fr/transistor`** - API Transistor - **`https://api.radiofrance.fr/livemeta/live`** - API LiveMeta - **`https://preroll.radiofrance.fr`** - Publicités pre-roll ### API Expressions (contenu éditorial) ``` https://www.radiofrance.fr/api/expressions?variant=vertical&limit=36&ids={uuid,uuid,...} ``` Retourne des contenus éditoriaux par UUIDs. --- ## 8. Résumé pour PMOMusic ### Recommandations d'implémentation #### Pour les métadonnées live **Option 1 (recommandée) :** API `/api/live?` par station ```rust async fn fetch_live_metadata(station: &str) -> Result { let url = format!("https://www.radiofrance.fr/{}/api/live?", station); reqwest::get(&url).await?.json().await } ``` **Avantages :** - ✅ Données complètes (émission, producteur, intro, visuels) - ✅ Flux audio inclus dans la réponse - ✅ `delayToRefresh` pour polling intelligent - ✅ Support des radios locales (France Bleu) #### Pour les flux audio **Priorisation recommandée :** 1. **HLS** (format moderne, adaptatif) 2. **AAC hifi** (192 kbps, meilleure qualité) 3. **AAC midfi** (128 kbps, bon compromis) 4. **MP3 midfi** (128 kbps, compatibilité maximale) 5. **AAC/MP3 lofi** (32 kbps, fallback) #### Polling intelligent Utiliser le champ `delayToRefresh` pour optimiser : ```rust loop { let metadata = fetch_live_metadata("franceculture").await?; // Afficher/utiliser les métadonnées println!("{} - {}", metadata.now.first_line.title, metadata.now.second_line.title ); // Attendre le temps recommandé tokio::time::sleep( Duration::from_millis(metadata.delay_to_refresh) ).await; } ``` ### Liste complète des stations à supporter **Stations principales :** - France Inter - France Info - France Culture - France Musique - FIP - Mouv' - Mon Petit France Inter **Webradios FIP (9):** - FIP principale - FIP Rock, Jazz, Groove, Reggae, Electro, Metal, Nouveautés, Pop **Webradios France Musique (5+):** - France Musique principale - La Jazz, La Contemporaine, Baroque, Opéra **Radios locales France Bleu (~40):** - À récupérer dynamiquement via `/francebleu/api/live?` --- ## 9. Points d'attention ### Rate limiting - Pas de limite documentée observée - Utiliser `delayToRefresh` pour respecter les recommandations - Éviter les requêtes inutiles (cache local) ### User-Agent Pour un projet open-source, utiliser un User-Agent identifiable : ``` PMOMusic/0.3.10 (https://github.com/votre-repo) ``` ### Gestion d'erreurs - Les APIs peuvent retourner des données vides (`null`) - Le champ `song` n'existe que pour les radios musicales - `localRadios` n'existe que pour France Bleu ### Respect des CGU - Ces APIs sont utilisées par le site officiel - Usage pour un projet open-source personnel/non-commercial - Ne pas redistribuer les flux audio commercialement --- ## 10. Annexes ### Exemple complet en Rust ```rust use serde::{Deserialize, Serialize}; use reqwest; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LiveResponse { pub station_name: String, pub delay_to_refresh: u64, pub migrated: bool, pub now: ShowMetadata, pub next: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShowMetadata { pub start_time: Option, pub end_time: Option, pub producer: Option, pub first_line: Line, pub second_line: Line, pub third_line: Option, pub intro: Option, pub song: Option, pub media: Media, } #[derive(Debug, Deserialize)] pub struct Line { pub title: Option, pub id: Option, pub path: Option, } #[derive(Debug, Deserialize)] pub struct Song { pub id: String, pub year: Option, pub interpreters: Vec, pub release: Release, } #[derive(Debug, Deserialize)] pub struct Release { pub label: Option, pub title: Option, } #[derive(Debug, Deserialize)] pub struct Media { pub sources: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Source { pub url: String, pub broadcast_type: String, pub format: String, pub bitrate: u32, } pub async fn get_live_metadata(station: &str) -> Result { let url = format!("https://www.radiofrance.fr/{}/api/live?", station); reqwest::Client::new() .get(&url) .header("User-Agent", "PMOMusic/0.3.10") .send() .await? .json() .await } ``` ### Stations complètes testées | Station | API Live | Flux HLS | Flux AAC | Flux MP3 | |---------|----------|----------|----------|----------| | France Inter | ✅ | ✅ | ✅ | ✅ | | France Info | ✅ | ✅ | ✅ | ✅ | | France Culture | ✅ | ✅ | ✅ | ✅ | | France Musique | ✅ | ✅ | ✅ | ✅ | | FIP | ✅ | ✅ | ✅ | ✅ | | Mouv' | ✅ | ✅ | ✅ | ✅ | | France Bleu | ✅ | ✅ | ✅ | ✅ | | Mon Petit France Inter | ✅ | ✅ (à tester) | ✅ (à tester) | ✅ (à tester) | --- **Dernière mise à jour :** 2026-01-22 **Méthode d'analyse :** Capture HAR + tests directs des endpoints **Statut :** Toutes les APIs sont publiques et fonctionnelles sans authentification --- # Round 4 : Architecture Client Stateful pour PMORadioFrance ## Date : 2026-01-22 ## Contexte Le Round 3 a permis d'implémenter un client HTTP basique (`RadioFranceClient`) pour interroger l'API Radio France. Ce client est **stateless** : il ne gère pas de cache, ne maintient pas d'état, et doit interroger l'API à chaque requête. Le Round 4 vise à construire la couche suivante : un **client stateful** qui : - Gère un cache des stations découvertes avec TTL - Expose des méthodes de haut niveau pour obtenir des listes de radios - Prépare les données pour la construction de la source UPnP - Intègre avec pmoconfig pour stocker les informations persistantes ## Objectifs du client stateful ### 1. Cache intelligent des stations **Problématique** : La découverte de toutes les stations (méthode `discover_all_stations()`) fait ~10 requêtes HTTP et prend 3-5 secondes. Les stations Radio France ne changent que très rarement (nouvelles webradios ~1-2 fois par an, nouvelles stations locales jamais). **Solution** : Utiliser pmoconfig pour stocker la liste des stations avec un timestamp, et ne rafraîchir que si le TTL est dépassé (ou sur requête forcée). **Stratégie de cache** : ```yaml # Dans .pmomusic/config.yaml sources: radiofrance: stations_cache: version: 1 # Version du schéma de découverte last_updated: 1737565200 # Unix timestamp ttl_days: 7 # TTL par défaut : 7 jours stations: - slug: "franceculture" name: "France Culture" type: "main" - slug: "fip" name: "FIP" type: "main" - slug: "fip_rock" name: "FIP Rock" type: "webradio" parent: "fip" - slug: "francebleu_alsace" name: "ICI Alsace" type: "local" region: "Alsace" id: 12 # ... ~50+ stations au total ``` **Logique de rafraîchissement** : 1. Lire le cache depuis la config 2. Vérifier `version` (invalide si ancienne version de découverte) 3. Vérifier TTL : `now - last_updated < ttl_days * 86400` 4. Si valide : retourner le cache 5. Si invalide ou absent : appeler `discover_all_stations()` et mettre à jour la config ### 2. Organisation des stations Les stations doivent être organisées logiquement pour la navigation UPnP : ``` Radio France (racine) ├── France Culture ├── France Inter ├── France Info ├── France Musique ├── FIP │ ├── FIP (principale) │ ├── FIP Rock │ ├── FIP Jazz │ ├── FIP Groove │ ├── FIP Reggae │ ├── FIP Electro │ ├── FIP Metal │ ├── FIP Nouveautés │ └── FIP Pop ├── Mouv' └── ICI (France Bleu renommé) ├── ICI Alsace ├── ICI Armorique ├── ICI Auxerre ├── ... (~40 radios locales) └── ICI Vaucluse ``` **Règles de regroupement** : - **Stations principales** : Une entrée par station principale (France Culture, France Inter, etc.) - **Stations avec webradios** (FIP, France Musique) : Un folder contenant : 1. La station principale en premier 2. Les webradios triées alphabétiquement - **France Bleu** : Renommé "ICI" avec toutes les radios locales dedans **Changement de label** : - API retourne : `"France Bleu"` → Affichage : `"ICI"` - API retourne : `"ICI Alsace"` → Affichage : `"ICI Alsace"` (inchangé) - Slugs conservés tels quels : `francebleu_alsace`, `francebleu`, etc. ### 3. Métadonnées live avec rafraîchissement intelligent Pour chaque station, on doit pouvoir obtenir les métadonnées live avec cache court terme : **Cache de métadonnées live** : - Durée : Utiliser le champ `delayToRefresh` de l'API (généralement 2-5 minutes) - Stockage : En mémoire uniquement (pas dans pmoconfig) - Invalidation : Automatique après `delayToRefresh` millisecondes **Stratégie** : ```rust struct LiveMetadataCache { metadata: LiveResponse, fetched_at: SystemTime, valid_until: SystemTime, } // Pseudo-code fn get_live_metadata(station: &str) -> Result { if let Some(cached) = memory_cache.get(station) { if SystemTime::now() < cached.valid_until { return Ok(cached.metadata.clone()); } } let metadata = client.live_metadata(station).await?; let delay = Duration::from_millis(metadata.delay_to_refresh); memory_cache.insert(station, LiveMetadataCache { metadata: metadata.clone(), fetched_at: SystemTime::now(), valid_until: SystemTime::now() + delay, }); Ok(metadata) } ``` ### 4. Construction de playlists pour la source UPnP Chaque station doit être exposée comme une **playlist volatile** contenant un seul item : le stream de plus haute qualité. **Règles métier pour les playlists** : #### Format des playlists ```rust // Pseudo-structure d'une playlist de station PMOPlaylist { id: "radiofrance:franceculture", role: PlaylistRole::Radio, volatile: true, // Les métadonnées changent, pas le contenu // Métadonnées de la playlist (= métadonnées de la station) title: "France Culture", // Nom de la station artist: "Les Matins", // Nom de l'émission en cours (now.firstLine.title) album: "France Culture", // Nom de la station (répété) cover_pk: "COVER_PK", // Cover de l'émission en cours (now.visualBackground) // Contenu : UN SEUL ITEM items: [ PMOItem { id: "radiofrance:franceculture:stream", title: "Le Journal de l'éco • Le jouet profite...", // now.secondLine.title artist: "Guillaume Erner", // now.producer ou show producer album: "Les Matins", // now.firstLine.title (émission) genre: "Talk Radio", // Type de station // Stream URL (AAC 192 kbps ou HLS) url: "https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance", // Métadonnées techniques protocol_info: "http-get:*:audio/aac:*", bitrate: 192000, sample_rate: 48000, channels: 2, // Cover de l'émission/morceau cover_pk: "COVER_PK", } ] } ``` #### Mapping des métadonnées API → UPnP **Pour les radios parlées (France Culture, France Inter, France Info)** : | Champ UPnP | Source API | Exemple | |------------|------------|---------| | Playlist Title | Station name | "France Culture" | | Playlist Artist | `now.firstLine.title` | "Les Matins" | | Playlist Cover | `now.visualBackground` → cache | UUID de cover | | Item Title | `now.firstLine.title` + `now.secondLine.title` | "Les Matins • Le Journal de l'éco" | | Item Artist | `now.producer` | "Guillaume Erner" | | Item Album | `now.firstLine.title` | "Les Matins" | | Item Cover | `now.visualBackground` → cache | UUID de cover | | Item Genre | "Talk Radio" | Fixe | **Pour les radios musicales (FIP, France Musique)** : | Champ UPnP | Source API | Exemple | |------------|------------|---------| | Playlist Title | Station name | "FIP Rock" | | Playlist Artist | `now.song.artists` OU `now.firstLine.title` | "The Rolling Stones" | | Playlist Cover | `now.song` image OU `now.visualBackground` | UUID de cover | | Item Title | `now.song.title` OU `now.firstLine.title` | "Paint It Black" | | Item Artist | `now.song.artists` | "The Rolling Stones" | | Item Album | `now.song.release.title` | "Aftermath" | | Item Cover | `now.song` image OU `now.visualBackground` | UUID de cover | | Item Genre | "Music" OU genre spécifique | "Rock" | **Note importante** : Les métadonnées changent régulièrement (toutes les 2-5 minutes), mais l'URL du stream reste la même. C'est pour cela que les playlists sont **volatiles** : on ne change pas leur contenu (toujours 1 item), mais on met à jour les métadonnées de cet item. #### Gestion des covers **Stratégie de cache** : - Les covers doivent être cachées dans `pmocovers` - URL source : `now.visualBackground.src` ou image de `now.song` - Extraction UUID : Parser l'URL Pikapi pour extraire l'UUID - Transformation : Télécharger et convertir en WebP si nécessaire - Stockage : Cache avec le PK = `RADIOFRANCE:{uuid}` **Workflow de cache de cover** : ```rust // Pseudo-code async fn cache_cover_from_metadata(metadata: &ShowMetadata) -> Option { // 1. Extraire l'URL de l'image let image_url = metadata.visual_background.as_ref()?.src.clone(); // 2. Extraire UUID let uuid = extract_uuid_from_url(&image_url)?; // 3. Construire URL en haute résolution let hires_url = ImageSize::XLarge.build_url(&uuid); // 4. Cacher avec pmocovers let cover_pk = cache_manager.cache_cover(&hires_url).await.ok()?; Some(cover_pk) } ``` **Tailles de cover** : - Pour les métadonnées UPnP : Utiliser `ImageSize::XLarge` (1200x680) ou `ImageSize::Large` (560x960) - Pikapi supporte plusieurs tailles, on choisit la plus grande disponible #### URL des streams **Règle métier** : Ne présenter que le stream de **plus haute résolution** disponible. **Priorité de sélection** : 1. AAC 192 kbps (HiFi) : `https://icecast.radiofrance.fr/{station}-hifi.aac?id=radiofrance` 2. HLS adaptatif : `https://stream.radiofrance.fr/{station}/{station}.m3u8?id=radiofrance` 3. AAC 128 kbps (MidFi) : Fallback si HiFi indisponible 4. MP3 128 kbps : Fallback ultime **Pas de cache audio** : Les streams sont des flux en direct, on ne les cache JAMAIS dans `pmoaudiocache`. Les URLs sont passées telles quelles au renderer. ### 5. Interface du client stateful **Proposition d'API publique** : ```rust /// Client stateful pour Radio France avec cache et gestion d'état pub struct RadioFranceStatefulClient { client: RadioFranceClient, // Client HTTP basique config: Arc, // Configuration pmoconfig metadata_cache: Arc>>, cache_manager: SourceCacheManager, // Pour covers } impl RadioFranceStatefulClient { /// Créer un nouveau client stateful pub async fn new() -> Result; /// Créer avec un client HTTP personnalisé pub fn with_client_and_config( client: RadioFranceClient, config: Arc, ) -> Self; // ======================================================================== // Station Discovery (avec cache) // ======================================================================== /// Obtenir toutes les stations (depuis cache si valide, sinon découverte) pub async fn get_all_stations(&self) -> Result>; /// Forcer la redécouverte des stations (ignore le cache) pub async fn refresh_stations(&self) -> Result>; /// Obtenir les stations principales uniquement pub async fn get_main_stations(&self) -> Result>; /// Obtenir les webradios d'une station (ex: FIP Rock, FIP Jazz) pub async fn get_webradios(&self, parent_station: &str) -> Result>; /// Obtenir les radios locales ICI (France Bleu) pub async fn get_local_radios(&self) -> Result>; // ======================================================================== // Organisation hiérarchique // ======================================================================== /// Obtenir les stations organisées par groupe pub async fn get_stations_by_group(&self) -> Result; // ======================================================================== // Métadonnées live (avec cache court terme) // ======================================================================== /// Obtenir les métadonnées live d'une station (cache 2-5 min) pub async fn get_live_metadata(&self, station: &str) -> Result; /// Forcer le rafraîchissement des métadonnées (ignore le cache) pub async fn refresh_live_metadata(&self, station: &str) -> Result; // ======================================================================== // Construction de playlists // ======================================================================== /// Construire une playlist UPnP pour une station pub async fn build_station_playlist(&self, station: &str) -> Result; /// Mettre à jour les métadonnées d'une playlist existante pub async fn update_playlist_metadata( &self, station: &str, playlist: &mut StationPlaylist, ) -> Result<()>; // ======================================================================== // Helpers // ======================================================================== /// Obtenir l'URL du stream HiFi pour une station pub async fn get_stream_url(&self, station: &str) -> Result; /// Vérifier si le cache des stations est valide pub fn is_station_cache_valid(&self) -> bool; /// Obtenir l'âge du cache des stations (en secondes) pub fn station_cache_age_secs(&self) -> Option; } /// Groupes de stations organisés hiérarchiquement pub struct StationGroups { /// Stations principales sans webradios (France Culture, France Inter, etc.) pub standalone: Vec, /// Stations avec webradios (FIP, France Musique) pub with_webradios: Vec, /// Radios locales ICI (France Bleu) pub local_radios: Vec, } /// Groupe de stations (principale + webradios) pub struct StationGroup { /// Station principale pub main: Station, /// Webradios associées (triées alphabétiquement) pub webradios: Vec, } /// Playlist UPnP pour une station pub struct StationPlaylist { /// ID de la playlist pub id: String, /// Station source pub station: Station, /// Métadonnées de la playlist (changent avec les émissions) pub metadata: PlaylistMetadata, /// Item unique (stream) pub stream_item: StreamItem, } /// Métadonnées de playlist (volatiles) pub struct PlaylistMetadata { pub title: String, // Nom de la station pub artist: Option, // Émission en cours pub album: Option, // Nom de la station (répété) pub cover_pk: Option, // Cover cachée } /// Item de stream pub struct StreamItem { pub id: String, pub title: String, // Titre de l'émission/morceau pub artist: Option, // Producteur/artiste pub album: Option, // Nom de l'émission/album pub genre: Option, pub url: String, // URL du stream (AAC HiFi ou HLS) pub protocol_info: String, pub bitrate: Option, pub sample_rate: Option, pub channels: Option, pub cover_pk: Option, } ``` ### 6. Extension de configuration (config_ext.rs) **Trait d'extension pour pmoconfig** : ```rust pub trait RadioFranceConfigExt { // ======================================================================== // Activation de la source // ======================================================================== fn get_radiofrance_enabled(&self) -> Result; fn set_radiofrance_enabled(&self, enabled: bool) -> Result<()>; // ======================================================================== // Cache des stations // ======================================================================== fn get_radiofrance_stations_cache(&self) -> Result>; fn set_radiofrance_stations_cache(&self, cache: &CachedStationList) -> Result<()>; fn clear_radiofrance_stations_cache(&self) -> Result<()>; fn get_radiofrance_cache_ttl_days(&self) -> Result; fn set_radiofrance_cache_ttl_days(&self, days: u64) -> Result<()>; // ======================================================================== // Configuration client HTTP // ======================================================================== fn get_radiofrance_base_url(&self) -> Result; fn set_radiofrance_base_url(&self, url: String) -> Result<()>; fn get_radiofrance_timeout_secs(&self) -> Result; fn set_radiofrance_timeout_secs(&self, secs: u64) -> Result<()>; // ======================================================================== // Factory method // ======================================================================== fn create_radiofrance_client(&self) -> Result; } ``` **Chemins de configuration** : ```yaml sources: radiofrance: enabled: true # Activation de la source base_url: "https://www.radiofrance.fr" timeout_secs: 30 cache_ttl_days: 7 # TTL du cache des stations stations_cache: # Cache des stations découvertes version: 1 last_updated: 1737565200 stations: - slug: "franceculture" name: "France Culture" type: "main" # ... reste des stations ``` ## Workflow de mise à jour des métadonnées ### Scénario 1 : Première utilisation 1. Utilisateur ouvre la source Radio France dans son client UPnP 2. `RadioFranceSource::browse("radiofrance")` est appelé 3. Source appelle `stateful_client.get_all_stations()` 4. Cache vide → Appel `discover_all_stations()` (~3-5 secondes) 5. Résultat stocké dans config avec timestamp 6. Retour de la liste des stations ### Scénario 2 : Utilisation ultérieure (cache valide) 1. Utilisateur ouvre la source Radio France 2. Source appelle `stateful_client.get_all_stations()` 3. Cache présent et valide (< 7 jours) → Retour immédiat depuis config 4. Pas d'appel réseau ### Scénario 3 : Lecture d'une station 1. Utilisateur sélectionne "France Culture" et lance la lecture 2. Source appelle `stateful_client.build_station_playlist("franceculture")` 3. Stateful client : - Appelle `get_live_metadata("franceculture")` (cache 2-5 min si présent) - Extrait les métadonnées de l'émission en cours - Cache la cover de l'émission via `pmocovers` - Construit la playlist avec 1 item (stream HiFi) 4. Retour de la playlist au renderer ### Scénario 4 : Mise à jour des métadonnées pendant la lecture 1. Renderer lit le stream depuis 3 minutes 2. Control point demande les métadonnées à jour 3. Source appelle `stateful_client.update_playlist_metadata()` 4. Stateful client : - Vérifie le cache des métadonnées live - Si expiré (> `delayToRefresh` ms) : appelle l'API - Met à jour les métadonnées de la playlist - Cache la nouvelle cover si différente 5. Control point reçoit les nouvelles métadonnées **Important** : L'URL du stream ne change JAMAIS pendant la lecture. Seules les métadonnées (titre, artiste, cover) changent. ## Architecture des fichiers ``` pmoradiofrance/ ├── src/ │ ├── lib.rs # Exports publics │ ├── client.rs # Client HTTP basique (Round 3) ✅ │ ├── models.rs # Structures de données (Round 3) ✅ │ ├── error.rs # Types d'erreur ✅ │ ├── stateful_client.rs # Client stateful (Round 4) 🆕 │ ├── playlist.rs # Construction de playlists (Round 4) 🆕 │ ├── config_ext.rs # Extension pmoconfig (Round 4) 🆕 │ └── source.rs # Implémentation MusicSource (Round 5) ├── assets/ │ └── default.webp # Logo Radio France 300x300px ├── Cargo.toml └── README.md ``` ## Dépendances supplémentaires ```toml [dependencies] # Déjà présentes (Round 3) reqwest = { version = "0.12", features = ["json"] } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } chrono = { workspace = true } async-trait = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } url = "2.5" scraper = "0.22" regex = "1.11" pmosource = { path = "../pmosource" } # Nouvelles (Round 4) pmoconfig = { path = "../pmoconfig" } # Configuration persistante pmocovers = { path = "../pmocovers" } # Cache de covers # pmoaudiocache NON utilisé (pas de cache audio pour les streams live) [features] default = ["pmoconfig"] pmoconfig = ["dep:pmoconfig"] cache = ["dep:pmocovers"] logging = [] server = ["pmosource/server", "pmoconfig", "cache"] full = ["server", "logging"] ``` ## Considérations d'implémentation ### Thread safety Le client stateful doit être thread-safe car il sera partagé entre plusieurs threads (ContentDirectory, AVTransport, etc.) : ```rust pub struct RadioFranceStatefulClient { client: RadioFranceClient, // Clone cheap (Arc interne) config: Arc, // Partagé metadata_cache: Arc>>, // Cache mémoire protégé cache_manager: SourceCacheManager, // Thread-safe } impl Clone for RadioFranceStatefulClient { fn clone(&self) -> Self { // Clone cheap : tous les champs sont Arc ou Clone Self { client: self.client.clone(), config: self.config.clone(), metadata_cache: self.metadata_cache.clone(), cache_manager: self.cache_manager.clone(), } } } ``` ### Performances **Cache des stations** : - Stockage : YAML dans config (~10-20 KB pour ~50 stations) - Lecture : Désérialisation YAML (~1-2 ms) - TTL : 7 jours (configurable) **Cache des métadonnées live** : - Stockage : Mémoire (HashMap) - Taille : ~5-10 KB par station - TTL : 2-5 minutes (champ `delayToRefresh` de l'API) - Limite : ~100 stations max = ~1 MB max **Cache des covers** : - Via `pmocovers` (LRU disk cache) - Taille moyenne : 50-200 KB par cover WebP - Limite : Configurable via `pmocovers` (défaut : 2000 items) ### Gestion d'erreurs **Stratégie de fallback** : 1. **Cache des stations invalide ou absent** → Redécouverte (erreur propagée si échec) 2. **Métadonnées live indisponibles** → Utiliser cache expiré si présent, sinon erreur 3. **Cover indisponible** → Utiliser cover par défaut de la source 4. **Stream HiFi indisponible** → Fallback sur HLS puis AAC MidFi ### Logging Utiliser `tracing` pour logger : - Découverte des stations (nombre, durée) - Hits/miss du cache - Rafraîchissement des métadonnées - Erreurs réseau ## Tests ### Tests unitaires - Validation du cache (TTL, version, invalidation) - Parsing des métadonnées - Construction des playlists - Mapping API → UPnP ### Tests d'intégration - Découverte réelle des stations - Récupération des métadonnées live - Cache et invalidation - Construction de playlists complètes ## Prochaines étapes (Round 5) Le Round 5 implémentera la `MusicSource` finale qui : - Utilise le `RadioFranceStatefulClient` - Implémente le trait `MusicSource` de `pmosource` - Expose l'arborescence UPnP ContentDirectory - Gère les playlists volatiles via `pmoplaylist` - Notifie les changements de métadonnées --- **Fin du Round 4**