Files
pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md
Eric Coissac e10f217385 feat(pmoradiofrance): implémentation complète du client Radio France
Cette mise à jour implémente complètement le client Radio France avec :

- Découverte dynamique des stations (principales, webradios et locales)
- Accès aux métadonnées live via l'API publique
- Gestion des flux audio HiFi (AAC 192 kbps, HLS)
- Support du cache des stations avec TTL configurable
- Extension de configuration pour pmoconfig
- Exemples d'utilisation et tests d'intégration

Les stations découvertes incluent : France Inter, France Info, France Culture, France Musique, FIP, Mouv', France Bleu (locales), et leurs variantes webradios respectives.

Les fonctionnalités incluent :
- Récupération des métadonnées live (émission en cours, producteur, visuels)
- Accès aux flux audio HiFi
- Gestion intelligente des rafraîchissements via delayToRefresh
- Cache des listes de stations avec TTL configurable (7 jours par défaut)

Les tests d'intégration couvrent : découverte des stations, métadonnées live, flux audio, cache, et gestion des erreurs.
2026-01-22 22:00:40 +01:00

37 KiB

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

{
  "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

{
  "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 :

{
  "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

async fn fetch_live_metadata(station: &str) -> Result<LiveMetadata> {
    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 :

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

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<ShowMetadata>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShowMetadata {
    pub start_time: Option<u64>,
    pub end_time: Option<u64>,
    pub producer: Option<String>,
    pub first_line: Line,
    pub second_line: Line,
    pub third_line: Option<Line>,
    pub intro: Option<String>,
    pub song: Option<Song>,
    pub media: Media,
}

#[derive(Debug, Deserialize)]
pub struct Line {
    pub title: Option<String>,
    pub id: Option<String>,
    pub path: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct Song {
    pub id: String,
    pub year: Option<u32>,
    pub interpreters: Vec<String>,
    pub release: Release,
}

#[derive(Debug, Deserialize)]
pub struct Release {
    pub label: Option<String>,
    pub title: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct Media {
    pub sources: Vec<Source>,
}

#[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<LiveResponse, reqwest::Error> {
    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 :

# 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 :

struct LiveMetadataCache {
    metadata: LiveResponse,
    fetched_at: SystemTime,
    valid_until: SystemTime,
}

// Pseudo-code
fn get_live_metadata(station: &str) -> Result<LiveResponse> {
    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

// 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 :

// Pseudo-code
async fn cache_cover_from_metadata(metadata: &ShowMetadata) -> Option<String> {
    // 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 :

/// Client stateful pour Radio France avec cache et gestion d'état
pub struct RadioFranceStatefulClient {
    client: RadioFranceClient,          // Client HTTP basique
    config: Arc<Config>,                 // Configuration pmoconfig
    metadata_cache: Arc<RwLock<HashMap<String, LiveMetadataCache>>>,
    cache_manager: SourceCacheManager,   // Pour covers
}

impl RadioFranceStatefulClient {
    /// Créer un nouveau client stateful
    pub async fn new() -> Result<Self>;
    
    /// Créer avec un client HTTP personnalisé
    pub fn with_client_and_config(
        client: RadioFranceClient,
        config: Arc<Config>,
    ) -> Self;
    
    // ========================================================================
    // Station Discovery (avec cache)
    // ========================================================================
    
    /// Obtenir toutes les stations (depuis cache si valide, sinon découverte)
    pub async fn get_all_stations(&self) -> Result<Vec<Station>>;
    
    /// Forcer la redécouverte des stations (ignore le cache)
    pub async fn refresh_stations(&self) -> Result<Vec<Station>>;
    
    /// Obtenir les stations principales uniquement
    pub async fn get_main_stations(&self) -> Result<Vec<Station>>;
    
    /// Obtenir les webradios d'une station (ex: FIP Rock, FIP Jazz)
    pub async fn get_webradios(&self, parent_station: &str) -> Result<Vec<Station>>;
    
    /// Obtenir les radios locales ICI (France Bleu)
    pub async fn get_local_radios(&self) -> Result<Vec<Station>>;
    
    // ========================================================================
    // Organisation hiérarchique
    // ========================================================================
    
    /// Obtenir les stations organisées par groupe
    pub async fn get_stations_by_group(&self) -> Result<StationGroups>;
    
    // ========================================================================
    // 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<LiveResponse>;
    
    /// Forcer le rafraîchissement des métadonnées (ignore le cache)
    pub async fn refresh_live_metadata(&self, station: &str) -> Result<LiveResponse>;
    
    // ========================================================================
    // Construction de playlists
    // ========================================================================
    
    /// Construire une playlist UPnP pour une station
    pub async fn build_station_playlist(&self, station: &str) -> Result<StationPlaylist>;
    
    /// 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<String>;
    
    /// 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<u64>;
}

/// Groupes de stations organisés hiérarchiquement
pub struct StationGroups {
    /// Stations principales sans webradios (France Culture, France Inter, etc.)
    pub standalone: Vec<Station>,
    
    /// Stations avec webradios (FIP, France Musique)
    pub with_webradios: Vec<StationGroup>,
    
    /// Radios locales ICI (France Bleu)
    pub local_radios: Vec<Station>,
}

/// Groupe de stations (principale + webradios)
pub struct StationGroup {
    /// Station principale
    pub main: Station,
    
    /// Webradios associées (triées alphabétiquement)
    pub webradios: Vec<Station>,
}

/// 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<String>, // Émission en cours
    pub album: Option<String>,  // Nom de la station (répété)
    pub cover_pk: Option<String>, // Cover cachée
}

/// Item de stream
pub struct StreamItem {
    pub id: String,
    pub title: String,          // Titre de l'émission/morceau
    pub artist: Option<String>, // Producteur/artiste
    pub album: Option<String>,  // Nom de l'émission/album
    pub genre: Option<String>,
    pub url: String,            // URL du stream (AAC HiFi ou HLS)
    pub protocol_info: String,
    pub bitrate: Option<u32>,
    pub sample_rate: Option<u32>,
    pub channels: Option<u8>,
    pub cover_pk: Option<String>,
}

6. Extension de configuration (config_ext.rs)

Trait d'extension pour pmoconfig :

pub trait RadioFranceConfigExt {
    // ========================================================================
    // Activation de la source
    // ========================================================================
    
    fn get_radiofrance_enabled(&self) -> Result<bool>;
    fn set_radiofrance_enabled(&self, enabled: bool) -> Result<()>;
    
    // ========================================================================
    // Cache des stations
    // ========================================================================
    
    fn get_radiofrance_stations_cache(&self) -> Result<Option<CachedStationList>>;
    fn set_radiofrance_stations_cache(&self, cache: &CachedStationList) -> Result<()>;
    fn clear_radiofrance_stations_cache(&self) -> Result<()>;
    
    fn get_radiofrance_cache_ttl_days(&self) -> Result<u64>;
    fn set_radiofrance_cache_ttl_days(&self, days: u64) -> Result<()>;
    
    // ========================================================================
    // Configuration client HTTP
    // ========================================================================
    
    fn get_radiofrance_base_url(&self) -> Result<String>;
    fn set_radiofrance_base_url(&self, url: String) -> Result<()>;
    
    fn get_radiofrance_timeout_secs(&self) -> Result<u64>;
    fn set_radiofrance_timeout_secs(&self, secs: u64) -> Result<()>;
    
    // ========================================================================
    // Factory method
    // ========================================================================
    
    fn create_radiofrance_client(&self) -> Result<RadioFranceStatefulClient>;
}

Chemins de configuration :

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

[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.) :

pub struct RadioFranceStatefulClient {
    client: RadioFranceClient,                    // Clone cheap (Arc interne)
    config: Arc<Config>,                          // Partagé
    metadata_cache: Arc<RwLock<HashMap<...>>>,   // 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