# 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 `` - 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 :** ```json { "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=` : 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"` : ```html

Les Matins par Guillaume Erner

En direct

``` ### 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) ```rust use scraper::{Html, Selector}; async fn fetch_current_show() -> Result { 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** : ```json { "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é :** ```rust use serde::{Deserialize, Serialize}; use reqwest; #[derive(Debug, Deserialize, Serialize)] struct LiveMetadata { prev: Vec, now: ShowInfo, next: Vec, #[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, #[serde(rename = "firstLinePath")] first_line_path: Option, #[serde(rename = "secondLine")] second_line: String, cover: String, #[serde(rename = "startTime")] start_time: Option, #[serde(rename = "endTime")] end_time: Option, } async fn fetch_franceculture_live() -> Result { let url = "https://api.radiofrance.fr/livemeta/live/5/transistor_culture_player"; reqwest::get(url) .await? .json::() .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.