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.
This commit is contained in:
206
Blackboard/Report/Construire_pmoradiofrance.md
Normal file
206
Blackboard/Report/Construire_pmoradiofrance.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Rapport : Implémentation de pmoradiofrance (Round 3)
|
||||
|
||||
**Date** : 2026-01-22
|
||||
**Crate** : `pmoradiofrance`
|
||||
**Statut** : Implémentation initiale complète
|
||||
|
||||
---
|
||||
|
||||
## Résumé
|
||||
|
||||
Création de la crate `pmoradiofrance` qui fournit un client Rust pour accéder aux APIs publiques de Radio France. Le client permet :
|
||||
|
||||
- La découverte dynamique de toutes les stations (~70+)
|
||||
- La récupération des métadonnées live (émission en cours, producteur, visuels)
|
||||
- L'accès aux flux audio HiFi (AAC 192 kbps, HLS)
|
||||
- Le cache de la liste des stations avec TTL configurable
|
||||
|
||||
---
|
||||
|
||||
## Fichiers créés
|
||||
|
||||
| Fichier | Description |
|
||||
|---------|-------------|
|
||||
| `pmoradiofrance/Cargo.toml` | Configuration de la crate avec dépendances |
|
||||
| `pmoradiofrance/src/lib.rs` | Point d'entrée et exports publics |
|
||||
| `pmoradiofrance/src/error.rs` | Types d'erreurs (`Error`, `Result`) |
|
||||
| `pmoradiofrance/src/models.rs` | Structures de données pour l'API |
|
||||
| `pmoradiofrance/src/client.rs` | `RadioFranceClient` et `ClientBuilder` |
|
||||
| `pmoradiofrance/src/config_ext.rs` | Extension `RadioFranceConfigExt` pour pmoconfig |
|
||||
| `pmoradiofrance/examples/discover_stations.rs` | Exemple de découverte |
|
||||
| `pmoradiofrance/examples/live_metadata.rs` | Exemple de métadonnées live |
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
| Fichier | Modification |
|
||||
|---------|--------------|
|
||||
| `Cargo.toml` (racine) | Ajout de `pmoradiofrance` au workspace |
|
||||
|
||||
---
|
||||
|
||||
## Architecture du client
|
||||
|
||||
### `RadioFranceClient`
|
||||
|
||||
Client HTTP stateless pour interroger les APIs Radio France :
|
||||
|
||||
```rust
|
||||
// Création
|
||||
let client = RadioFranceClient::new().await?;
|
||||
|
||||
// Découverte des stations
|
||||
let stations = client.discover_all_stations().await?;
|
||||
|
||||
// Métadonnées live
|
||||
let metadata = client.live_metadata("franceculture").await?;
|
||||
|
||||
// URL du flux HiFi
|
||||
let stream_url = client.get_hifi_stream_url("fip_rock").await?;
|
||||
```
|
||||
|
||||
### Découverte des stations
|
||||
|
||||
Le client découvre dynamiquement :
|
||||
|
||||
1. **Stations principales** (7) : France Inter, France Info, France Culture, France Musique, FIP, Mouv', France Bleu
|
||||
2. **Webradios** (~15-20) : FIP Rock, FIP Jazz, France Musique Baroque, etc.
|
||||
3. **Radios locales France Bleu** (~44) : via le champ `now.localRadios` de l'API
|
||||
|
||||
### Gestion des webradios
|
||||
|
||||
Le parsing des slugs gère automatiquement les webradios :
|
||||
|
||||
| Slug | Base station | Paramètre webradio |
|
||||
|------|--------------|-------------------|
|
||||
| `fip` | `fip` | - |
|
||||
| `fip_rock` | `fip` | `?webradio=fip_rock` |
|
||||
| `francemusique_jazz` | `francemusique` | `?webradio=francemusique_jazz` |
|
||||
| `francebleu_alsace` | `francebleu_alsace` | - (slug direct) |
|
||||
|
||||
---
|
||||
|
||||
## Extension de configuration
|
||||
|
||||
Le trait `RadioFranceConfigExt` permet de cacher la liste des stations :
|
||||
|
||||
```rust
|
||||
use pmoconfig::get_config;
|
||||
use pmoradiofrance::RadioFranceConfigExt;
|
||||
|
||||
let config = get_config();
|
||||
|
||||
// Vérifier le cache (TTL par défaut : 7 jours)
|
||||
if let Some(stations) = config.get_radiofrance_stations_cached()? {
|
||||
// Utiliser les stations du cache
|
||||
} else {
|
||||
// Découvrir et mettre en cache
|
||||
let client = RadioFranceClient::new().await?;
|
||||
let stations = client.discover_all_stations().await?;
|
||||
config.set_radiofrance_cached_stations(&stations)?;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration YAML générée
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
radiofrance:
|
||||
enabled: true
|
||||
station_cache_ttl_secs: 604800 # 7 jours
|
||||
station_cache:
|
||||
stations: [...]
|
||||
last_updated: 1769112000
|
||||
version: 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests d'intégration
|
||||
|
||||
17 tests d'intégration qui appellent la vraie API Radio France :
|
||||
|
||||
```bash
|
||||
# Exécuter tous les tests d'intégration
|
||||
cargo test -p pmoradiofrance -- --ignored
|
||||
|
||||
# Avec output visible
|
||||
cargo test -p pmoradiofrance -- --ignored --nocapture
|
||||
```
|
||||
|
||||
| Test | Description | Status |
|
||||
|------|-------------|--------|
|
||||
| `test_client_creation` | Création du client | ✅ |
|
||||
| `test_live_metadata_franceculture` | Métadonnées France Culture | ✅ |
|
||||
| `test_live_metadata_franceinter` | Métadonnées France Inter | ✅ |
|
||||
| `test_live_metadata_fip` | Métadonnées FIP (avec chanson) | ✅ |
|
||||
| `test_live_metadata_fip_rock` | Métadonnées webradio FIP Rock | ✅ |
|
||||
| `test_live_metadata_francemusique` | Métadonnées France Musique | ✅ |
|
||||
| `test_live_metadata_francebleu` | Métadonnées + radios locales | ✅ |
|
||||
| `test_live_metadata_mouv` | Métadonnées Mouv' | ✅ |
|
||||
| `test_get_hifi_stream_url` | URLs des flux HiFi | ✅ |
|
||||
| `test_get_available_streams` | Liste des flux disponibles | ✅ |
|
||||
| `test_discover_main_stations` | Découverte stations principales | ✅ |
|
||||
| `test_discover_fip_webradios` | Découverte webradios FIP | ✅ |
|
||||
| `test_discover_francemusique_webradios` | Découverte webradios FM | ✅ |
|
||||
| `test_discover_local_radios` | Découverte radios locales | ✅ |
|
||||
| `test_discover_all_stations` | Découverte complète | ✅ |
|
||||
| `test_invalid_station` | Gestion d'erreur | ✅ |
|
||||
| `test_refresh_delay` | Calcul délai refresh | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Correction effectuée pendant l'implémentation
|
||||
|
||||
**Bug découvert** : Le champ `localRadios` de l'API France Bleu est dans `now.localRadios` (imbriqué dans `ShowMetadata`), pas au niveau racine de `LiveResponse`.
|
||||
|
||||
**Correction** :
|
||||
1. Déplacé le champ `local_radios` de `LiveResponse` vers `ShowMetadata`
|
||||
2. Ajouté une méthode helper `LiveResponse::local_radios()` pour accès simplifié
|
||||
3. Mis à jour le client et les tests
|
||||
|
||||
---
|
||||
|
||||
## Features Cargo
|
||||
|
||||
| Feature | Description | Dépendances |
|
||||
|---------|-------------|-------------|
|
||||
| `default` | Configuration de base | `pmoconfig` |
|
||||
| `pmoconfig` | Support extension config | `dep:pmoconfig` |
|
||||
| `cache` | Support cache audio/covers | `pmocovers`, `pmoaudiocache` |
|
||||
| `playlist` | Support playlists FIFO | `pmoplaylist` |
|
||||
| `logging` | Logs avec tracing | - |
|
||||
| `server` | Support serveur complet | Toutes les features |
|
||||
| `full` | Toutes les features | `server`, `logging` |
|
||||
|
||||
---
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
1. **Implémenter `source.rs`** : Trait `MusicSource` pour intégration UPnP
|
||||
2. **Ajouter support FIFO** : Pour les radios musicales (FIP, France Musique)
|
||||
3. **Cache des métadonnées live** : Respecter `delayToRefresh`
|
||||
4. **Intégration serveur** : Routes API REST via pmoserver
|
||||
|
||||
---
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Découverte des stations
|
||||
|
||||
```bash
|
||||
cargo run -p pmoradiofrance --example discover_stations
|
||||
```
|
||||
|
||||
### Métadonnées live
|
||||
|
||||
```bash
|
||||
# Station par défaut (France Culture)
|
||||
cargo run -p pmoradiofrance --example live_metadata
|
||||
|
||||
# Station spécifique
|
||||
cargo run -p pmoradiofrance --example live_metadata -- fip_rock
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Fin du rapport**
|
||||
64
Blackboard/ToDiscuss/Construire_pmoradiofrance.md
Normal file
64
Blackboard/ToDiscuss/Construire_pmoradiofrance.md
Normal file
@@ -0,0 +1,64 @@
|
||||
** Tu dois suivre scrupuleusement les règles définies dans le fichier [@Rules.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Rules.md) **
|
||||
|
||||
|
||||
** Cette tâche n'est pas une tâche de codage, c'est une tâche de réflexion. Elle doit conduire à la rédaction d'un rapport dans le répertoire [@ToThinkAbout](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout) **
|
||||
|
||||
à parir de la page [web](https://www.radiofrance.fr/franceculture) peux-tu comprendre comment elle obtient les informations prsésenté ci-dessous:
|
||||
|
||||
```html
|
||||
<div role="heading" aria-level="1" slot="title" class="CoverRadio-title qg-tt3 svelte-1thibul"><!----><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" data-testid="link" class="svelte-1t7i9vq underline-hover"><!----><!---->Le Journal de l'éco • Le jouet profite de la morosité ambiante<!----></a><!----></span><!----></div>
|
||||
```
|
||||
|
||||
et
|
||||
|
||||
```html
|
||||
<p class="CoverRadio-subtitle qg-tt5 qg-focus-container svelte-1thibul" slot="subtitle"><!----><!----><!----><a href="/franceculture/podcasts/les-matins" data-testid="link" class="svelte-1t7i9vq"><!---->Les Matins<!----></a><!----> <span class="CoverRadio-producer qg-tx1 svelte-qz676b">par Guillaume Erner</span><!----><!----></p>
|
||||
```
|
||||
|
||||
## Round 2:
|
||||
|
||||
À partir de [@api_radiofrance_complete.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md) et de [@music_source.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Architecture/music_source.md)
|
||||
|
||||
Nous allons commencer l'implémentation de pmoradiofrance en implémentant dans un fichier client.rs Une API de requêtes sur Radio France. Pour les fonctionnalités, il faut effectivement se reporter au fichier `api_radiofrance_complete.md` Et pour l'architecture, il est possible de s'inspirer de [@client.rs](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/pmoparadise/src/client.rs)
|
||||
|
||||
## Round 3
|
||||
|
||||
A partir des découvertes faites durant le round 2, Tu as maintenant le droit d'écrire du code. Tu peux donc écrire le fichier: client.rs de la nouvelle crate pmoradiofrance. Pour cela, tu devras t'appuyer sur les rapports [@api_radiofrance_complete.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md) et de [@music_source.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Architecture/music_source.md)
|
||||
|
||||
Il faut absolument cacher les réponses de l'API, Afin de limiter au maximum les requêtes inutiles, Notamment, on sait que les chaînes et les web radios ne changent que très rarement. On peut peut-être penser à faire une extension de configuration [@pmoconfig_ext.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Architecture/pmoconfig_ext.md) Pour stocker les informations principales ainsi qu'un timestamp et se dire que sauf requêtes forcées on ne va pas mettre à jour cette liste plus d'une fois par semaine.
|
||||
|
||||
## Round 4
|
||||
|
||||
Je pense qu'il faut maintenant implémenter le deuxième niveau de la Crate Radio France, En implémentant un client Stateful qui peut retourner facilement des listes de radio, des playlists, qui charge ces informations soit à part de la configuration, soit à partir de l'API, suivant que le TTL est dépassé ou pas. Le tout pour préparer la construction de la source Radio France.
|
||||
|
||||
Pensez comme rêgle métier à renommer les choses qui apparaissent comme `France Bleue` en `ICI` Au niveau des labels d'affichage, pas des slugs évidemment.
|
||||
|
||||
Voilà le type d'arborescence de browsing qu'on pourrait avoir.
|
||||
On ne présente à chaque fois qu'un lien vers le flux de plus haute résolution.
|
||||
|
||||
|
||||
```
|
||||
Radio France
|
||||
├── France Culture
|
||||
├── FIP
|
||||
│ ├── FIP
|
||||
│ ├── FIP Cultes
|
||||
│ ├── FIP Nouveautes
|
||||
│ ├── FIP ...
|
||||
│ └── FIP Pop
|
||||
├── Mouv'
|
||||
├── ...
|
||||
└── ICI
|
||||
├── ICI Alsace
|
||||
├── ICI Armorique
|
||||
├── ICI Auxerre
|
||||
├── ...
|
||||
└── ICI Vaucluse
|
||||
```
|
||||
|
||||
Sous le folder principal Radio France, On doit avoir une playlist par station. Les stations qui n'ont qu'une seule chaîne ne contiennent que cette chaîne dans leur PMOplaylist, Les autres, si elles ont une station principale comme FIP, commencent par cette station principale puis leur station annexe.
|
||||
Évidemment, normalement, le contenu en titre des playlists ne doit pas évoluer. Mais les métadonnées si régulièrement Si l'on met le titre de la station comme équivalent d'un titre d'album, Le nom de l'émission pourrait être l'auteur. Et le titre de l'émission du jour, le titre du morceau. Ainsi, au fur et à mesure du temps, on fait évoluer des métadonnées pour changer la couverture, le nom de l'émission, le titre de l'émissions du jour.
|
||||
|
||||
Je pense que les playlists doivent être des playlists volatiles. Les covers doivent être cachés dans [@pmocovers](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/pmocovers) Par contre les URL doivent être passées telles qu'elles, C'est du pur stream, on ne va pas les cacher dans le PMOaudiocache.
|
||||
|
||||
étend le document de réflexion pour proposer une architecture: [@api_radiofrance_complete.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md)
|
||||
474
Blackboard/ToThinkAbout/analyse_metadonnees_franceculture.md
Normal file
474
Blackboard/ToThinkAbout/analyse_metadonnees_franceculture.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# 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 :**
|
||||
```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=<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"` :
|
||||
|
||||
```html
|
||||
<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)
|
||||
|
||||
```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** :
|
||||
```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<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.
|
||||
1227
Blackboard/ToThinkAbout/api_radiofrance_complete.md
Normal file
1227
Blackboard/ToThinkAbout/api_radiofrance_complete.md
Normal file
File diff suppressed because it is too large
Load Diff
831
Blackboard/ToThinkAbout/client_radiofrance_architecture.md
Normal file
831
Blackboard/ToThinkAbout/client_radiofrance_architecture.md
Normal file
@@ -0,0 +1,831 @@
|
||||
# Architecture du client Radio France (client.rs)
|
||||
|
||||
**Date** : 2026-01-22
|
||||
**Objectif** : Conception d'une API Rust pour interroger les métadonnées live et flux audio de Radio France
|
||||
**Référence** : Architecture inspirée de `pmoparadise/src/client.rs`
|
||||
|
||||
---
|
||||
|
||||
## Table des matières
|
||||
|
||||
1. [Vue d'ensemble](#vue-densemble)
|
||||
2. [Découverte dynamique des stations](#découverte-dynamique-des-stations)
|
||||
3. [Architecture du client](#architecture-du-client)
|
||||
4. [Structures de données](#structures-de-données)
|
||||
5. [Méthodes principales](#méthodes-principales)
|
||||
6. [Exemple d'utilisation](#exemple-dutilisation)
|
||||
7. [Points d'attention](#points-dattention)
|
||||
|
||||
---
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le client Radio France doit permettre :
|
||||
- **Découverte dynamique** de ~73 stations/webradios (scraping HTML)
|
||||
- **Métadonnées live** via `/api/live?` avec polling intelligent
|
||||
- **Flux audio** en qualité maximale uniquement (AAC 192 kbps + HLS)
|
||||
- **Un seul client** pour toutes les stations (pas un client par station)
|
||||
|
||||
### Philosophie
|
||||
|
||||
- **Pas de hardcoding** : Toutes les stations sont découvertes dynamiquement
|
||||
- **Qualité maximale uniquement** : AAC 192 kbps (hifi) + HLS, pas de choix lofi/midfi
|
||||
- **Architecture simple** : Un client unique, les stations sont des paramètres
|
||||
|
||||
---
|
||||
|
||||
## Découverte dynamique des stations
|
||||
|
||||
### Stratégie complète
|
||||
|
||||
Radio France n'expose **pas d'API centralisée** listant toutes les stations. La découverte se fait par **scraping HTML** des pages principales :
|
||||
|
||||
#### 1. Stations principales (8)
|
||||
|
||||
**Source** : `https://www.radiofrance.fr/`
|
||||
|
||||
**Méthode** : Scraper le HTML et extraire tous les slugs via regex `(franceinter|franceinfo|franceculture|francemusique|fip|mouv|francebleu|monpetit)`
|
||||
|
||||
**Résultat attendu** :
|
||||
```
|
||||
franceinter
|
||||
franceinfo
|
||||
franceculture
|
||||
francemusique
|
||||
fip
|
||||
mouv
|
||||
francebleu
|
||||
monpetitfranceinter
|
||||
```
|
||||
|
||||
#### 2. Webradios de chaque station (nombre variable)
|
||||
|
||||
**Principe** : **TOUTES les stations** peuvent avoir des webradios, pas seulement FIP et France Musique.
|
||||
|
||||
**Méthode** : Pour chaque station principale découverte, scraper sa page `https://www.radiofrance.fr/{station}` et extraire les identifiants via regex `{station}_[a-z_]+`
|
||||
|
||||
**Exemples découverts** :
|
||||
|
||||
**FIP** (`https://www.radiofrance.fr/fip`) :
|
||||
```
|
||||
fip_cultes
|
||||
fip_electro
|
||||
fip_groove
|
||||
fip_hiphop
|
||||
fip_jazz
|
||||
fip_metal
|
||||
fip_nouveautes
|
||||
fip_pop
|
||||
fip_reggae
|
||||
fip_rock
|
||||
fip_sacre_francais
|
||||
fip_world
|
||||
```
|
||||
|
||||
**France Musique** (`https://www.radiofrance.fr/francemusique`) :
|
||||
```
|
||||
francemusique_baroque
|
||||
francemusique_classique_easy
|
||||
francemusique_classique_love
|
||||
francemusique_classique_plus
|
||||
francemusique_concert_rf
|
||||
francemusique_evenementielle
|
||||
francemusique_la_contemporaine
|
||||
francemusique_la_jazz
|
||||
francemusique_ocora_monde
|
||||
francemusique_opera
|
||||
francemusique_piano_zen
|
||||
```
|
||||
|
||||
**Autres stations** : À découvrir dynamiquement (France Inter, Mouv, etc. pourraient avoir des webradios futures)
|
||||
|
||||
#### 3. Radios locales France Bleu (~40)
|
||||
|
||||
**Source** : API `/francebleu/api/live?` → champ `localRadios[]`
|
||||
|
||||
**Méthode** : Appel API et extraction du tableau JSON
|
||||
|
||||
**Exemple de structure** :
|
||||
```json
|
||||
{
|
||||
"localRadios": [
|
||||
{"id": 12, "title": "ICI Alsace", "name": "francebleu_alsace", "isOnAir": true},
|
||||
{"id": 13, "title": "ICI Armorique", "name": "francebleu_armorique", "isOnAir": true},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Total découvert
|
||||
|
||||
- **8** stations principales
|
||||
- **~23** webradios (12 FIP + 11 France Musique + possibles autres)
|
||||
- **~40** radios locales France Bleu
|
||||
- **= ~71+ stations au total** (extensible automatiquement si nouvelles webradios)
|
||||
|
||||
---
|
||||
|
||||
## Architecture du client
|
||||
|
||||
### Client unique
|
||||
|
||||
Contrairement à une approche "un client par station", nous utilisons **un seul client** avec les stations comme **paramètres de méthode**.
|
||||
|
||||
```rust
|
||||
pub struct RadioFranceClient {
|
||||
client: reqwest::Client,
|
||||
timeout: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
### Pas de cache interne
|
||||
|
||||
Le client est **stateless** et ne cache rien. La gestion du cache (métadonnées, images) sera faite par les couches supérieures (`SourceCacheManager`).
|
||||
|
||||
### Builder pattern
|
||||
|
||||
Pour permettre la configuration :
|
||||
|
||||
```rust
|
||||
pub struct ClientBuilder {
|
||||
client: Option<reqwest::Client>,
|
||||
timeout: Duration,
|
||||
user_agent: String,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structures de données
|
||||
|
||||
### 1. Station découverte
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Station {
|
||||
pub slug: String, // "fip_rock", "franceinter"
|
||||
pub name: String, // "FIP Rock", "France Inter"
|
||||
pub station_type: StationType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum StationType {
|
||||
Main, // Station principale
|
||||
Webradio { // Webradio de n'importe quelle station
|
||||
parent_station: String, // "fip", "francemusique", "mouv", etc.
|
||||
},
|
||||
LocalRadio { region: String }, // Radio locale France Bleu
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Réponse API Live
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveResponse {
|
||||
pub station_name: String,
|
||||
pub delay_to_refresh: u64, // millisecondes
|
||||
pub migrated: bool,
|
||||
pub now: ShowMetadata,
|
||||
pub next: Option<ShowMetadata>,
|
||||
pub local_radios: Option<Vec<LocalRadio>>, // France Bleu uniquement
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Métadonnées d'émission
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, 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, // Titre émission
|
||||
pub second_line: Line, // Titre épisode/chronique
|
||||
pub third_line: Option<Line>, // Sous-titre
|
||||
pub intro: Option<String>, // Description
|
||||
pub song: Option<Song>, // Pour radios musicales (FIP, France Musique)
|
||||
pub media: Media, // Flux audio disponibles
|
||||
pub visual_background: Option<EmbedImage>,
|
||||
pub visuals: Option<Visuals>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Line {
|
||||
pub title: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub path: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Morceau musical (FIP, France Musique)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Song {
|
||||
pub id: String,
|
||||
pub year: Option<u32>,
|
||||
pub interpreters: Vec<String>,
|
||||
pub release: Release,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Release {
|
||||
pub label: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub reference: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Flux audio
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Media {
|
||||
pub sources: Vec<StreamSource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamSource {
|
||||
pub url: String,
|
||||
pub broadcast_type: BroadcastType,
|
||||
pub format: StreamFormat,
|
||||
pub bitrate: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BroadcastType {
|
||||
Live,
|
||||
Timeshift,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StreamFormat {
|
||||
Mp3,
|
||||
Aac,
|
||||
Hls,
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Images
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmbedImage {
|
||||
pub model: String,
|
||||
pub src: String,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub dominant: Option<String>,
|
||||
pub copyright: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Visuals {
|
||||
pub card: Option<EmbedImage>,
|
||||
pub player: Option<EmbedImage>,
|
||||
}
|
||||
|
||||
pub enum ImageSize {
|
||||
Tiny, // 88x88
|
||||
Small, // 200x200
|
||||
Medium, // 420x720
|
||||
Large, // 560x960
|
||||
XLarge, // 1200x680
|
||||
Raw, // Taille originale
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Radios locales
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalRadio {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub name: String,
|
||||
pub is_on_air: bool,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Méthodes principales
|
||||
|
||||
### 1. Création du client
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Créer un nouveau client avec settings par défaut
|
||||
pub async fn new() -> Result<Self> {
|
||||
Self::builder().build().await
|
||||
}
|
||||
|
||||
/// Créer un builder pour configuration avancée
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::default()
|
||||
}
|
||||
|
||||
/// Créer avec un reqwest::Client existant
|
||||
pub fn with_client(client: reqwest::Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Découverte des stations
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Découvrir toutes les stations disponibles (scraping + API)
|
||||
pub async fn discover_all_stations(&self) -> Result<Vec<Station>> {
|
||||
let mut stations = Vec::new();
|
||||
|
||||
// 1. Découvrir les stations principales
|
||||
let main_stations = self.scrape_main_stations().await?;
|
||||
|
||||
// 2. Pour CHAQUE station principale, découvrir ses webradios éventuelles
|
||||
for main_station in main_stations {
|
||||
// Ajouter la station principale
|
||||
stations.push(main_station.clone());
|
||||
|
||||
// Découvrir ses webradios (peut retourner 0 si aucune)
|
||||
if let Ok(webradios) = self.scrape_station_webradios(&main_station.slug).await {
|
||||
stations.extend(webradios);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Cas spécial : radios locales France Bleu (via API)
|
||||
if let Ok(locals) = self.discover_local_radios().await {
|
||||
stations.extend(locals);
|
||||
}
|
||||
|
||||
Ok(stations)
|
||||
}
|
||||
|
||||
/// Scraper les stations principales depuis homepage
|
||||
async fn scrape_main_stations(&self) -> Result<Vec<Station>> {
|
||||
let html = self.client
|
||||
.get("https://www.radiofrance.fr/")
|
||||
.timeout(self.timeout)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let re = regex::Regex::new(
|
||||
r"(franceinter|franceinfo|franceculture|francemusique|fip|mouv|francebleu|monpetit)"
|
||||
)?;
|
||||
|
||||
let mut slugs = std::collections::HashSet::new();
|
||||
for cap in re.captures_iter(&html) {
|
||||
slugs.insert(cap[0].to_string());
|
||||
}
|
||||
|
||||
Ok(slugs.into_iter().map(|slug| Station {
|
||||
slug: slug.clone(),
|
||||
name: Self::slug_to_name(&slug),
|
||||
station_type: StationType::Main,
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Scraper les webradios d'une station donnée
|
||||
///
|
||||
/// Fonctionne pour n'importe quelle station (fip, francemusique, mouv, etc.)
|
||||
/// Retourne un Vec vide si aucune webradio n'est trouvée.
|
||||
async fn scrape_station_webradios(&self, station: &str) -> Result<Vec<Station>> {
|
||||
let url = format!("https://www.radiofrance.fr/{}", station);
|
||||
let html = self.client
|
||||
.get(&url)
|
||||
.timeout(self.timeout)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
// Pattern générique : {station}_[a-z_]+
|
||||
let pattern = format!(r"{}_[a-z_]+", station);
|
||||
let re = regex::Regex::new(&pattern)?;
|
||||
|
||||
let mut slugs = std::collections::HashSet::new();
|
||||
for cap in re.captures_iter(&html) {
|
||||
slugs.insert(cap[0].to_string());
|
||||
}
|
||||
|
||||
Ok(slugs.into_iter().map(|slug| Station {
|
||||
slug: slug.clone(),
|
||||
name: Self::slug_to_name(&slug),
|
||||
station_type: StationType::Webradio {
|
||||
parent_station: station.to_string(),
|
||||
},
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Découvrir les radios locales France Bleu via API
|
||||
async fn discover_local_radios(&self) -> Result<Vec<Station>> {
|
||||
let response = self.live_metadata("francebleu").await?;
|
||||
|
||||
Ok(response.local_radios
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|local| Station {
|
||||
slug: local.name,
|
||||
name: local.title,
|
||||
station_type: StationType::LocalRadio {
|
||||
region: local.title.replace("ICI ", ""),
|
||||
},
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Convertir slug en nom lisible (heuristique simple)
|
||||
fn slug_to_name(slug: &str) -> String {
|
||||
// Transformations basiques, à améliorer
|
||||
slug.replace('_', " ")
|
||||
.split_whitespace()
|
||||
.map(|w| {
|
||||
let mut c = w.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Métadonnées live
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Récupérer les métadonnées live d'une station
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `station` - Slug de la station (ex: "franceculture", "fip_rock")
|
||||
///
|
||||
/// # Webradios
|
||||
/// Pour les webradios FIP/France Musique, utiliser le format :
|
||||
/// - Principales : "fip", "francemusique"
|
||||
/// - Webradios : "fip_rock", "francemusique_jazz"
|
||||
///
|
||||
/// L'API utilise le paramètre `?webradio=` automatiquement si nécessaire.
|
||||
pub async fn live_metadata(&self, station: &str) -> Result<LiveResponse> {
|
||||
let (base_station, webradio) = Self::parse_station_slug(station);
|
||||
|
||||
let mut url = url::Url::parse(&format!(
|
||||
"https://www.radiofrance.fr/{}/api/live?",
|
||||
base_station
|
||||
))?;
|
||||
|
||||
// Ajouter le paramètre webradio si nécessaire
|
||||
if let Some(wr) = webradio {
|
||||
url.query_pairs_mut().append_pair("webradio", wr);
|
||||
}
|
||||
|
||||
let response = self.client
|
||||
.get(url)
|
||||
.timeout(self.timeout)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::ApiError(format!(
|
||||
"API returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Parser le slug pour extraire station de base et webradio
|
||||
///
|
||||
/// Exemples :
|
||||
/// - "fip" → ("fip", None)
|
||||
/// - "fip_rock" → ("fip", Some("fip_rock"))
|
||||
/// - "francemusique_jazz" → ("francemusique", Some("francemusique_jazz"))
|
||||
/// - "franceinter" → ("franceinter", None)
|
||||
fn parse_station_slug(slug: &str) -> (&str, Option<&str>) {
|
||||
if slug.starts_with("fip_") {
|
||||
("fip", Some(slug))
|
||||
} else if slug.starts_with("francemusique_") {
|
||||
("francemusique", Some(slug))
|
||||
} else if slug.starts_with("francebleu_") {
|
||||
// Radios locales : pas de paramètre webradio, slug direct
|
||||
(slug, None)
|
||||
} else {
|
||||
// Stations principales
|
||||
(slug, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer uniquement les métadonnées de l'émission actuelle
|
||||
pub async fn now_playing(&self, station: &str) -> Result<ShowMetadata> {
|
||||
let response = self.live_metadata(station).await?;
|
||||
Ok(response.now)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Flux audio (qualité maximale uniquement)
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Récupérer l'URL du flux audio en qualité maximale
|
||||
///
|
||||
/// Priorité : AAC 192 kbps (hifi) > HLS
|
||||
pub async fn get_hifi_stream_url(&self, station: &str) -> Result<String> {
|
||||
let metadata = self.live_metadata(station).await?;
|
||||
|
||||
// Chercher AAC hifi (192 kbps)
|
||||
if let Some(source) = metadata.now.media.sources.iter().find(|s| {
|
||||
s.format == StreamFormat::Aac
|
||||
&& s.broadcast_type == BroadcastType::Live
|
||||
&& s.bitrate == 192
|
||||
}) {
|
||||
return Ok(source.url.clone());
|
||||
}
|
||||
|
||||
// Fallback HLS
|
||||
if let Some(source) = metadata.now.media.sources.iter().find(|s| {
|
||||
s.format == StreamFormat::Hls
|
||||
&& s.broadcast_type == BroadcastType::Live
|
||||
}) {
|
||||
return Ok(source.url.clone());
|
||||
}
|
||||
|
||||
Err(Error::NoHifiStream(format!(
|
||||
"No HiFi stream found for station: {}",
|
||||
station
|
||||
)))
|
||||
}
|
||||
|
||||
/// Lister tous les flux disponibles pour une station
|
||||
pub async fn get_available_streams(&self, station: &str) -> Result<Vec<StreamSource>> {
|
||||
let metadata = self.live_metadata(station).await?;
|
||||
Ok(metadata.now.media.sources)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Images (Pikapi)
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Construire l'URL d'une image Pikapi
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `uuid` - UUID de l'image (extrait des métadonnées)
|
||||
/// * `size` - Taille souhaitée
|
||||
pub fn get_image_url(uuid: &str, size: ImageSize) -> String {
|
||||
let size_str = match size {
|
||||
ImageSize::Tiny => "88x88",
|
||||
ImageSize::Small => "200x200",
|
||||
ImageSize::Medium => "420x720",
|
||||
ImageSize::Large => "560x960",
|
||||
ImageSize::XLarge => "1200x680",
|
||||
ImageSize::Raw => "raw",
|
||||
};
|
||||
|
||||
format!("https://www.radiofrance.fr/pikapi/images/{}/{}", uuid, size_str)
|
||||
}
|
||||
|
||||
/// Extraire l'UUID d'une URL Pikapi existante
|
||||
pub fn extract_image_uuid(url: &str) -> Option<String> {
|
||||
let re = regex::Regex::new(r"/pikapi/images/([a-f0-9-]+)").ok()?;
|
||||
re.captures(url)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Polling intelligent
|
||||
|
||||
```rust
|
||||
impl RadioFranceClient {
|
||||
/// Calculer le délai avant le prochain refresh recommandé
|
||||
pub fn next_refresh_delay(metadata: &LiveResponse) -> Duration {
|
||||
Duration::from_millis(metadata.delay_to_refresh)
|
||||
}
|
||||
|
||||
/// Calculer le délai en tenant compte du temps écoulé
|
||||
pub fn adjusted_refresh_delay(
|
||||
metadata: &LiveResponse,
|
||||
fetched_at: std::time::SystemTime,
|
||||
) -> Duration {
|
||||
let base_delay = Duration::from_millis(metadata.delay_to_refresh);
|
||||
let elapsed = fetched_at.elapsed().unwrap_or(Duration::ZERO);
|
||||
|
||||
base_delay.saturating_sub(elapsed)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemple d'utilisation
|
||||
|
||||
### Découverte et affichage de toutes les stations
|
||||
|
||||
```rust
|
||||
use pmoradiofrance::RadioFranceClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = RadioFranceClient::new().await?;
|
||||
|
||||
println!("Découverte des stations...");
|
||||
let stations = client.discover_all_stations().await?;
|
||||
|
||||
println!("Trouvé {} stations :", stations.len());
|
||||
for station in &stations {
|
||||
println!(" - {} ({})", station.name, station.slug);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Récupération des métadonnées live
|
||||
|
||||
```rust
|
||||
use pmoradiofrance::RadioFranceClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = RadioFranceClient::new().await?;
|
||||
|
||||
// Station principale
|
||||
let fc_live = client.live_metadata("franceculture").await?;
|
||||
println!("France Culture : {} - {}",
|
||||
fc_live.now.first_line.title.unwrap_or_default(),
|
||||
fc_live.now.second_line.title.unwrap_or_default()
|
||||
);
|
||||
|
||||
// Webradio FIP
|
||||
let fip_rock_live = client.live_metadata("fip_rock").await?;
|
||||
if let Some(song) = &fip_rock_live.now.song {
|
||||
println!("FIP Rock : {} - {}",
|
||||
song.interpreters.join(", "),
|
||||
fip_rock_live.now.first_line.title.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Polling avec délai intelligent
|
||||
|
||||
```rust
|
||||
use pmoradiofrance::RadioFranceClient;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = RadioFranceClient::new().await?;
|
||||
|
||||
loop {
|
||||
let fetched_at = SystemTime::now();
|
||||
let metadata = client.live_metadata("fip").await?;
|
||||
|
||||
println!("Now: {} - {}",
|
||||
metadata.now.second_line.title.unwrap_or_default(),
|
||||
metadata.now.first_line.title.unwrap_or_default()
|
||||
);
|
||||
|
||||
// Attendre le délai recommandé
|
||||
let delay = RadioFranceClient::adjusted_refresh_delay(&metadata, fetched_at);
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Récupération du flux HiFi
|
||||
|
||||
```rust
|
||||
use pmoradiofrance::RadioFranceClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = RadioFranceClient::new().await?;
|
||||
|
||||
let stream_url = client.get_hifi_stream_url("franceculture").await?;
|
||||
println!("Stream HiFi : {}", stream_url);
|
||||
// Exemple : https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Points d'attention
|
||||
|
||||
### 1. Rate limiting
|
||||
|
||||
- Pas de limite documentée observée
|
||||
- **Toujours** respecter `delayToRefresh` pour éviter les requêtes inutiles
|
||||
- Mettre en cache les résultats de `discover_all_stations()` (TTL : 24h recommandé)
|
||||
|
||||
### 2. User-Agent
|
||||
|
||||
Pour un projet open-source, utiliser un User-Agent identifiable :
|
||||
|
||||
```rust
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user_agent: "PMOMusic/0.3.10 (https://github.com/votre-repo)".to_string(),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Gestion d'erreurs
|
||||
|
||||
Les APIs peuvent retourner :
|
||||
- **Données vides** (`null`) pour certains champs
|
||||
- **`song`** absent pour radios non-musicales (France Inter, France Info, France Culture)
|
||||
- **`localRadios`** uniquement pour France Bleu
|
||||
- **`visual_background`** parfois absent
|
||||
|
||||
Toujours utiliser `Option<>` et gérer les cas manquants.
|
||||
|
||||
### 4. Webradios et paramètre `?webradio=`
|
||||
|
||||
- **Stations principales** : `/franceinter/api/live?`
|
||||
- **Webradios FIP** : `/fip/api/live?webradio=fip_rock`
|
||||
- **Webradios France Musique** : `/francemusique/api/live?webradio=francemusique_jazz`
|
||||
- **Radios locales** : `/francebleu_alsace/api/live?` (slug direct, pas de paramètre)
|
||||
|
||||
### 5. Images Pikapi
|
||||
|
||||
Les URLs dans les réponses API utilisent parfois des chemins complets, parfois juste l'UUID :
|
||||
|
||||
```json
|
||||
"src": "https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39"
|
||||
```
|
||||
|
||||
Toujours normaliser en extrayant l'UUID et en reconstruisant l'URL avec la taille souhaitée.
|
||||
|
||||
### 6. Scraping HTML
|
||||
|
||||
Le scraping HTML est **fragile** par nature. Recommandations :
|
||||
|
||||
- **Cache agressif** : Stocker les résultats de découverte (TTL 24h minimum)
|
||||
- **Fallback** : Avoir une liste de base hardcodée si le scraping échoue
|
||||
- **Validation optionnelle** : Tester chaque station découverte avec `/api/live?` avant de l'ajouter (peut être lent)
|
||||
- **Monitoring** : Logger les échecs de découverte
|
||||
|
||||
### 7. Performance
|
||||
|
||||
Pour découvrir ~70 stations :
|
||||
- **Scraping** : 1 homepage + 8 pages stations (une par station principale)
|
||||
- **Validation France Bleu** : 1 requête API
|
||||
- **Total** : ~10 requêtes HTTP
|
||||
|
||||
Temps estimé : 3-5 secondes avec timeout 30s (parallélisable pour réduire à ~1-2s).
|
||||
|
||||
### 8. Respect des CGU
|
||||
|
||||
- APIs publiques utilisées par le site officiel
|
||||
- Usage acceptable pour un projet open-source personnel/non-commercial
|
||||
- **Ne pas redistribuer** les flux audio commercialement
|
||||
- **Ne pas surcharger** les serveurs (respecter `delayToRefresh`)
|
||||
|
||||
---
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
1. **Implémenter `client.rs`** avec l'architecture décrite
|
||||
2. **Ajouter les tests** :
|
||||
- Tests unitaires pour parsing de slugs
|
||||
- Tests d'intégration pour découverte
|
||||
- Tests d'API live (avec captures VCR)
|
||||
3. **Intégrer avec `pmosource`** :
|
||||
- Implémenter le trait `MusicSource`
|
||||
- Gérer le cache via `SourceCacheManager`
|
||||
- Support FIFO pour radios musicales (FIP)
|
||||
4. **Documenter les limitations** :
|
||||
- Stations non accessibles
|
||||
- Cas d'erreur connus
|
||||
- Métriques de fiabilité
|
||||
|
||||
---
|
||||
|
||||
**Fin du rapport d'architecture client.rs**
|
||||
Reference in New Issue
Block a user