Files
pmomusic/Blackboard/ToThinkAbout/analyse_metadonnees_franceculture.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

16 KiB

Analyse : Récupération des métadonnées France Culture

Objectif

Comprendre comment le site web de France Culture (https://www.radiofrance.fr/franceculture) obtient et affiche les informations sur l'émission en cours.

Architecture du site

Framework utilisé

SvelteKit avec Server-Side Rendering (SSR)

Le site utilise SvelteKit, comme en témoignent :

  • L'attribut data-sveltekit-preload-data="hover" sur le <body>
  • Les classes CSS préfixées par svelte- (ex: svelte-1thibul, svelte-qz676b)
  • Les chemins vers les assets : /client/immutable/assets/

Rendu des données

SSR (Server-Side Rendering) - Les données sont déjà présentes dans le HTML initial

Méthode de récupération des informations

API publique JSON découverte !

Après analyse du trafic réseau (fichier HAR), l'API officielle existe et est OUVERTE :

API LiveMeta (métadonnées en temps réel)

https://api.radiofrance.fr/livemeta/live/5/transistor_culture_player

Caractéristiques :

  • Aucune authentification requise (pas de token)
  • Endpoint officiel utilisé par le site web
  • JSON structuré avec émission en cours, précédente et suivante
  • Timestamps précis de début et fin d'émission
  • UUIDs des émissions pour récupérer plus de détails
  • Indicateur de rafraîchissement (delayToRefresh en millisecondes)

Exemple de réponse :

{
  "prev": [{
    "firstLine": "Le direct",
    "firstLineUuid": null,
    "firstLinePath": null,
    "secondLine": "France Culture, l'esprit d'ouverture",
    "cover": "4e9fba8d-7675-409d-86a0-fce40f0cd4a6",
    "startTime": null,
    "endTime": null
  }],
  "now": {
    "firstLine": "La Série fiction",
    "firstLineUuid": "69cf4362-6bfb-48d1-89cf-9d11202f9938",
    "firstLineExpressionUuid": "69cf4362-6bfb-48d1-89cf-9d11202f9938",
    "firstLinePath": "franceculture/podcasts/fictions-le-feuilleton",
    "firstLinePathUuid": "3c1c2e55-41a0-11e5-9fe0-005056a87c89",
    "secondLine": "\"Ségou\" de Maryse Condé 9/10 : Deuil et pénitence",
    "secondLineExpressionUuid": "69cf4362-6bfb-48d1-89cf-9d11202f9938",
    "cover": "436430f7-5b2b-43f2-9f3c-28f2ad6cae39",
    "startTime": 1769108400,
    "endTime": 1769110122
  },
  "next": [{
    "firstLine": "L'Instant poésie",
    "firstLinePath": "franceculture/podcasts/l-instant-poesie",
    "firstLineUuid": "06fe22c7-144c-41b8-983d-ec956595b694",
    "secondLine": "L'Instant poésie d'Abd al Malik 14/20 : \"Roman inachevé\" de Louis Aragon, une main tendue",
    "cover": "a18a392b-f7d5-41bd-972a-e64451f35213",
    "startTime": 1769110200,
    "endTime": 1769110555
  }],
  "delayToRefresh": 742000
}

Paramètres optionnels :

  • ?date=<timestamp> : Récupérer les métadonnées à un moment donné (historique)

API Pikapi (images de couverture)

https://www.radiofrance.fr/pikapi/images/{uuid}/{taille}

Exemples :

  • https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39/200x200
  • Autres tailles disponibles (à tester)

Anciennes tentatives (pour référence historique)

Les tentatives d'accès aux endpoints suivants ont échoué :

  • https://www.radiofrance.fr/api/v2.1/stations/franceculture → retourne du HTML
  • https://www.radiofrance.fr/api/v2.1/stations/franceculture/live → retourne du HTML
  • https://openapi.radiofrance.fr/v1/graphql → nécessite un header x-token

Données embarquées dans le HTML (SSR)

Les informations sont également directement rendues dans le HTML par le serveur SvelteKit (méthode de fallback).

Structure HTML des métadonnées

Zone principale : CoverRadio

Les informations de l'émission en cours se trouvent dans la section class="CoverRadio" :

<div class="CoverRadio-infoContainer">
    
    <!-- Titre de l'émission/segment -->
    <div class="CoverRadio-title qg-tt3 svelte-1thibul" role="heading" aria-level="1">
        <span class="truncate qg-focus-container svelte-1t7i9vq">
            <a href="/franceculture/podcasts/le-journal-de-l-eco/le-jouet-profite-de-la-morosite-ambiante-4949584" 
               aria-label="Le Journal de l'éco • Le jouet profite de la morosité ambiante">
                Le Journal de l'éco • Le jouet profite de la morosité ambiante
            </a>
        </span>
    </div>
    
    <!-- Nom de l'émission parente + producteur -->
    <p class="CoverRadio-subtitle qg-tt5 qg-focus-container svelte-1thibul">
        <a href="/franceculture/podcasts/les-matins">Les Matins</a>
        <span class="CoverRadio-producer qg-tx1 svelte-qz676b">par Guillaume Erner</span>
    </p>
    
    <!-- Indicateur de direct -->
    <div class="CoverRadio-ctaTop">
        <p class="direct qg-st6 CoverRadio-labelDirect dark default svelte-12tsplm">
            En direct
        </p>
    </div>
    
</div>

Classes CSS identifiées

Classe CSS Contenu Utilité
CoverRadio-title Titre du segment/chronique en cours Titre principal
CoverRadio-subtitle Nom de l'émission parente Contexte de diffusion
CoverRadio-producer Nom du producteur/animateur Crédit
CoverRadio-labelDirect Badge "En direct" Statut de diffusion

Stratégies d'extraction

Option 1 : Scraping HTML simple

Récupérer la page HTML et extraire les données via :

  • Parsing HTML (BeautifulSoup en Python, scraper en Rust)
  • Regex ciblées sur les classes CSS

Avantages :

  • Pas de token nécessaire
  • Données toujours présentes dans le HTML
  • Méthode robuste

Inconvénients :

  • Dépendant de la structure HTML
  • Risque de cassure si le site change
  • Parsing HTML plus lourd

Option 2 : API GraphQL avec token

L'API GraphQL existe (https://openapi.radiofrance.fr/v1/graphql) mais nécessite un x-token.

Étapes :

  1. Analyser le code JavaScript du site pour trouver comment le token est généré
  2. Extraire ou reproduire la logique de génération de token
  3. Utiliser l'API GraphQL

Avantages :

  • API structurée et officielle
  • Données JSON propres
  • Moins de risque de changement

Inconvénients :

  • Nécessite un token (non documenté publiquement)
  • Potentiellement bloqué/limité en débit
  • Reverse engineering requis

Option 3 : API interne SvelteKit

SvelteKit utilise des endpoints /__data.json pour l'hydratation client.

À explorer :

  • https://www.radiofrance.fr/franceculture/__data.json
  • Endpoints de données internes

Recommandation

Pour un projet comme PMOMusic (pmoradiofrance)

Approche hybride recommandée :

  1. Court terme : Scraping HTML

    • Implémenter un parser HTML en Rust
    • Cibler les classes CSS CoverRadio-*
    • Parser avec scraper ou select en Rust
  2. Moyen terme : Investigation API

    • Analyser le code JavaScript pour trouver le token
    • Tenter d'utiliser l'API GraphQL si possible
  3. Mise en cache et rafraîchissement

    • Rafraîchir les métadonnées toutes les 1-5 minutes
    • Mettre en cache pour éviter les requêtes excessives

Exemple de code conceptuel (Rust)

use scraper::{Html, Selector};

async fn fetch_current_show() -> Result<ShowInfo, Error> {
    let html = reqwest::get("https://www.radiofrance.fr/franceculture")
        .await?
        .text()
        .await?;
    
    let document = Html::parse_document(&html);
    
    // Sélecteurs CSS
    let title_selector = Selector::parse(".CoverRadio-title a").unwrap();
    let subtitle_selector = Selector::parse(".CoverRadio-subtitle a").unwrap();
    let producer_selector = Selector::parse(".CoverRadio-producer").unwrap();
    
    let title = document
        .select(&title_selector)
        .next()
        .map(|e| e.inner_html())
        .unwrap_or_default();
    
    let show_name = document
        .select(&subtitle_selector)
        .next()
        .map(|e| e.inner_html())
        .unwrap_or_default();
    
    let producer = document
        .select(&producer_selector)
        .next()
        .map(|e| e.inner_html().replace("par ", ""))
        .unwrap_or_default();
    
    Ok(ShowInfo {
        title,
        show_name,
        producer,
    })
}

Points d'attention

  1. Rate limiting : Ne pas surcharger le site avec des requêtes trop fréquentes
  2. User-Agent : Utiliser un User-Agent identifiable pour un projet open-source
  3. Gestion d'erreurs : Le site peut être temporairement indisponible
  4. Structure HTML : Peut changer sans préavis
  5. Respect des CGU : Vérifier les conditions d'utilisation de Radio France

Mise à jour de la page côté client

Comment la page se rafraîchit-elle ?

Réponse : La page ne se met PAS à jour automatiquement côté client.

Après analyse :

  1. Pas de polling/WebSocket : Aucun mécanisme de setInterval, setTimeout, WebSocket ou Server-Sent Events (SSE) détecté dans le HTML
  2. Pas de JavaScript de mise à jour : Le DOM n'est pas modifié dynamiquement pour les métadonnées CoverRadio-*
  3. Navigation SvelteKit : Les mises à jour se font via la navigation SPA de SvelteKit

Mécanisme de navigation SvelteKit

SvelteKit utilise le preloading et les endpoints __data.json :

https://www.radiofrance.fr/franceculture/__data.json

Cet endpoint retourne un JSON structuré contenant toutes les données de la page, incluant :

  • Métadonnées de l'émission en cours
  • Configuration du site
  • Contenu de la page

Format de données :

{
  "type": "data",
  "nodes": [
    {
      "metadata": { ... },
      "context": { ... },
      "mainStationLive": { ... }
    }
  ]
}

Stratégie de rafraîchissement

Pour un utilisateur sur le site :

  1. Chargement initial : SSR complet avec HTML
  2. Navigation ultérieure : SvelteKit charge __data.json en AJAX
  3. Rechargement manuel : L'utilisateur doit recharger la page (F5) pour voir les nouvelles métadonnées

Il n'y a pas de mise à jour automatique en temps réel.

Recommandation mise à jour

🏆 Option privilégiée : API LiveMeta officielle (DÉCOUVERTE !)

URL : https://api.radiofrance.fr/livemeta/live/5/transistor_culture_player

Avantages :

  • API officielle Radio France : Endpoint public et documenté
  • Aucune authentification : Pas de token, pas de restriction
  • JSON léger et structuré : Format simple et prévisible
  • Données optimales : Juste ce qu'il faut (prev/now/next)
  • Polling intelligent : delayToRefresh indique quand rafraîchir
  • Stable : API de production utilisée par le site officiel
  • Support historique : Paramètre ?date= pour l'historique
  • UUIDs : Références pour récupérer plus de détails si besoin

Inconvénients :

  • Aucun majeur identifié

Code Rust recommandé :

use serde::{Deserialize, Serialize};
use reqwest;

#[derive(Debug, Deserialize, Serialize)]
struct LiveMetadata {
    prev: Vec<ShowInfo>,
    now: ShowInfo,
    next: Vec<ShowInfo>,
    #[serde(rename = "delayToRefresh")]
    delay_to_refresh: u64,
}

#[derive(Debug, Deserialize, Serialize)]
struct ShowInfo {
    #[serde(rename = "firstLine")]
    first_line: String,
    #[serde(rename = "firstLineUuid")]
    first_line_uuid: Option<String>,
    #[serde(rename = "firstLinePath")]
    first_line_path: Option<String>,
    #[serde(rename = "secondLine")]
    second_line: String,
    cover: String,
    #[serde(rename = "startTime")]
    start_time: Option<u64>,
    #[serde(rename = "endTime")]
    end_time: Option<u64>,
}

async fn fetch_franceculture_live() -> Result<LiveMetadata, reqwest::Error> {
    let url = "https://api.radiofrance.fr/livemeta/live/5/transistor_culture_player";
    
    reqwest::get(url)
        .await?
        .json::<LiveMetadata>()
        .await
}

// Utilisation avec polling intelligent
async fn monitor_live() {
    loop {
        match fetch_franceculture_live().await {
            Ok(metadata) => {
                println!("En cours : {} - {}", 
                    metadata.now.first_line, 
                    metadata.now.second_line
                );
                
                // Attendre le temps recommandé avant de rafraîchir
                tokio::time::sleep(
                    tokio::time::Duration::from_millis(metadata.delay_to_refresh)
                ).await;
            }
            Err(e) => {
                eprintln!("Erreur : {}", e);
                // Fallback : attendre 60 secondes
                tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
            }
        }
    }
}

Hiérarchie des options (mise à jour)

  1. 🥇 Premier choix : API LiveMeta - API officielle Radio France
  2. 🥈 Fallback niveau 1 : __data.json - Endpoint SvelteKit si LiveMeta indisponible
  3. 🥉 Fallback niveau 2 : Scraping HTML - Si les API JSON sont toutes indisponibles
  4. 💭 Exploration future : API GraphQL - Si un token public devient disponible

Conclusion

Pour la mise à jour côté serveur (PMOMusic) :

  • Utiliser l'API LiveMeta officielle : https://api.radiofrance.fr/livemeta/live/5/transistor_culture_player
  • Polling intelligent : Utiliser delayToRefresh pour optimiser les appels
  • Récupération des images : Via Pikapi avec l'UUID de cover
  • Gestion d'erreur : Fallback sur __data.json puis HTML si nécessaire

Pour la page web elle-même :

  • Aucune mise à jour automatique : L'utilisateur doit recharger la page manuellement
  • Navigation SPA via SvelteKit charge __data.json en AJAX
  • Le SSR initial contient déjà toutes les données dans le HTML

URLs de flux audio découvertes

Flux HLS (recommandé)

Master playlist :

https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance

Qualités disponibles :

  • lofi : 105 kbps (BANDWIDTH=107000) - franceculture_lofi.m3u8?id=radiofrance
  • midfi : 178 kbps (BANDWIDTH=185000) - franceculture_midfi.m3u8?id=radiofrance
  • hifi : 268 kbps (BANDWIDTH=280000) - franceculture_hifi.m3u8?id=radiofrance

Codec : mp4a.40.2 (AAC-LC)

Flux Icecast (à confirmer)

D'après RF_old.json, ces URLs devraient exister (non observées dans le HAR car le player web utilise HLS) :

MP3 :

https://icecast.radiofrance.fr/franceculture-lofi.mp3?id=radiofrance
https://icecast.radiofrance.fr/franceculture-midfi.mp3?id=radiofrance
https://icecast.radiofrance.fr/franceculture-hifi.mp3?id=radiofrance

AAC :

https://icecast.radiofrance.fr/franceculture-lofi.aac?id=radiofrance
https://icecast.radiofrance.fr/franceculture-midfi.aac?id=radiofrance
https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance

Mapping des stations Radio France

D'après l'analyse du fichier HAR et RF_old.json, voici le mapping des IDs de stations :

Station ID Station Endpoint LiveMeta
France Culture 5 /livemeta/live/5/transistor_culture_player
France Inter ? À découvrir
France Musique ? À découvrir
FIP ? À découvrir
Mouv' ? À découvrir
France Bleu (national) ? À découvrir

Note : Les IDs des autres stations peuvent être découverts en analysant le HAR de leurs pages respectives ou en testant des valeurs séquentielles (1, 2, 3, 4, 6, 7...).

Prochaines étapes recommandées

  1. Implémenter le client LiveMeta en Rust avec les structures proposées
  2. 🔍 Découvrir les IDs des autres stations Radio France
  3. 🔍 Tester les URLs Icecast pour confirmer leur disponibilité
  4. 📋 Documenter l'API complète dans le code PMOMusic
  5. 🧪 Tester le paramètre ?date= pour l'accès historique
  6. 🎨 Tester les tailles d'images Pikapi disponibles (200x200, 400x400, etc.)

Annexe : Analyse du fichier HAR

Source : www.radiofrance.fr.har
Date de capture : 2026-01-22
Page analysée : https://www.radiofrance.fr/franceculture

Découvertes principales :

  • API LiveMeta accessible et ouverte
  • Aucune authentification requise
  • Polling intelligent via delayToRefresh
  • Support HLS multi-bitrate
  • API Pikapi pour les images

Cette analyse confirme que Radio France expose des APIs publiques utilisables pour des projets comme PMOMusic.