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.
1228 lines
37 KiB
Markdown
1228 lines
37 KiB
Markdown
# API Radio France - Documentation complète
|
|
|
|
## Vue d'ensemble
|
|
|
|
Radio France expose plusieurs APIs publiques **sans authentification** pour accéder aux métadonnées des émissions en direct et aux flux audio.
|
|
|
|
**Date d'analyse :** 2026-01-22
|
|
**Sources :** Analyse de fichiers HAR + tests directs
|
|
|
|
---
|
|
|
|
## 1. API Live par station
|
|
|
|
### Format général
|
|
```
|
|
https://www.radiofrance.fr/{station}/api/live?
|
|
```
|
|
|
|
### Stations disponibles
|
|
|
|
| Station | Endpoint | Status |
|
|
|---------|----------|--------|
|
|
| France Inter | `/franceinter/api/live?` | ✅ Fonctionne |
|
|
| France Info | `/franceinfo/api/live?` | ✅ Fonctionne |
|
|
| France Culture | `/franceculture/api/live?` | ✅ Fonctionne |
|
|
| France Musique | `/francemusique/api/live?` | ✅ Fonctionne |
|
|
| FIP | `/fip/api/live?` | ✅ Fonctionne |
|
|
| Mouv' | `/mouv/api/live?` | ✅ Fonctionne |
|
|
| France Bleu (national) | `/francebleu/api/live?` | ✅ Fonctionne |
|
|
| Mon Petit France Inter | `/monpetitfranceinter/api/live?` | ✅ Fonctionne |
|
|
|
|
### Structure de réponse
|
|
|
|
```json
|
|
{
|
|
"stationName": "franceculture",
|
|
"delayToRefresh": 262000,
|
|
"migrated": true,
|
|
"now": {
|
|
"printProgMusic": true,
|
|
"startTime": 1769108400,
|
|
"endTime": 1769110122,
|
|
"producer": "Nom du producteur",
|
|
"firstLine": {
|
|
"title": "Nom de l'émission",
|
|
"id": "uuid-emission",
|
|
"path": "franceculture/podcasts/emission"
|
|
},
|
|
"secondLine": {
|
|
"title": "Titre de l'épisode/chronique",
|
|
"id": "uuid-episode",
|
|
"path": "franceculture/podcasts/emission/episode"
|
|
},
|
|
"thirdLine": {
|
|
"title": "Sous-titre éventuel",
|
|
"id": "uuid",
|
|
"path": null
|
|
},
|
|
"intro": "Description de l'émission...",
|
|
"reactAvailable": false,
|
|
"visualBackground": {
|
|
"model": "EmbedImage",
|
|
"src": "https://www.radiofrance.fr/pikapi/images/uuid",
|
|
"width": 4000,
|
|
"height": 1000,
|
|
"dominant": "#c8e8f8",
|
|
"copyright": "Radio France"
|
|
},
|
|
"song": {
|
|
"id": "uuid-morceau",
|
|
"year": 2024,
|
|
"interpreters": ["Artiste"],
|
|
"release": {
|
|
"label": "Label",
|
|
"title": "Album",
|
|
"reference": null
|
|
}
|
|
},
|
|
"media": {
|
|
"sources": [
|
|
{
|
|
"url": "https://icecast.radiofrance.fr/franceculture-lofi.mp3?id=radiofrance",
|
|
"broadcastType": "live",
|
|
"format": "mp3",
|
|
"bitrate": 32
|
|
},
|
|
{
|
|
"url": "https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance",
|
|
"broadcastType": "live",
|
|
"format": "hls",
|
|
"bitrate": 0
|
|
},
|
|
{
|
|
"url": "https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance",
|
|
"broadcastType": "live",
|
|
"format": "aac",
|
|
"bitrate": 192
|
|
},
|
|
{
|
|
"url": "https://icecast.radiofrance.fr/franceculture-midfi.aac?id=radiofrance",
|
|
"broadcastType": "live",
|
|
"format": "aac",
|
|
"bitrate": 128
|
|
},
|
|
{
|
|
"url": "https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance",
|
|
"broadcastType": "timeshift",
|
|
"format": "hls",
|
|
"bitrate": 0
|
|
}
|
|
]
|
|
},
|
|
"localRadios": [],
|
|
"visuals": {
|
|
"card": { /* Image pour la carte */ },
|
|
"player": { /* Image pour le player */ }
|
|
}
|
|
},
|
|
"next": {
|
|
/* Même structure pour l'émission suivante */
|
|
}
|
|
}
|
|
```
|
|
|
|
### Champs importants
|
|
|
|
- **`delayToRefresh`** : Temps en millisecondes avant le prochain rafraîchissement recommandé
|
|
- **`now.song`** : Présent si c'est une musique (FIP, France Musique)
|
|
- **`now.media.sources`** : Liste de tous les flux disponibles avec formats et bitrates
|
|
- **`localRadios`** : Liste des radios locales (pour France Bleu)
|
|
|
|
---
|
|
|
|
## 2. API LiveMeta (ancienne API, toujours fonctionnelle)
|
|
|
|
### Format
|
|
```
|
|
https://api.radiofrance.fr/livemeta/live/{id}/transistor_{station}_player
|
|
```
|
|
|
|
### IDs connus
|
|
|
|
| Station | ID | Endpoint |
|
|
|---------|-----|----------|
|
|
| France Culture | 5 | `/livemeta/live/5/transistor_culture_player` |
|
|
|
|
### Exemple de réponse
|
|
|
|
```json
|
|
{
|
|
"prev": [{
|
|
"firstLine": "Le direct",
|
|
"secondLine": "France Culture, l'esprit d'ouverture",
|
|
"cover": "uuid-image",
|
|
"startTime": null,
|
|
"endTime": null
|
|
}],
|
|
"now": {
|
|
"firstLine": "La Série fiction",
|
|
"firstLineUuid": "uuid",
|
|
"firstLinePath": "franceculture/podcasts/emission",
|
|
"secondLine": "Titre de l'épisode",
|
|
"cover": "uuid-image",
|
|
"startTime": 1769108400,
|
|
"endTime": 1769110122
|
|
},
|
|
"next": [{ /* émission suivante */ }],
|
|
"delayToRefresh": 742000
|
|
}
|
|
```
|
|
|
|
**Note :** Cette API retourne moins de détails que `/api/live?` mais fonctionne toujours.
|
|
|
|
---
|
|
|
|
## 3. Flux audio
|
|
|
|
### Format des URLs
|
|
|
|
#### HLS (recommandé)
|
|
```
|
|
https://stream.radiofrance.fr/{station}/{station}.m3u8?id=radiofrance
|
|
```
|
|
|
|
#### Icecast (AAC et MP3)
|
|
```
|
|
https://icecast.radiofrance.fr/{station}-{qualite}.{format}?id=radiofrance
|
|
```
|
|
|
|
### Qualités disponibles
|
|
|
|
| Qualité | Bitrate AAC | Bitrate MP3 | Utilisation |
|
|
|---------|-------------|-------------|-------------|
|
|
| `lofi` | 32 kbps | 32 kbps | Connexions lentes |
|
|
| `midfi` | 128 kbps | 128 kbps | Standard |
|
|
| `hifi` | 192 kbps | - | Haute qualité |
|
|
|
|
### Exemples d'URLs
|
|
|
|
**France Culture :**
|
|
```
|
|
https://stream.radiofrance.fr/franceculture/franceculture.m3u8?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceculture-midfi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceculture-midfi.mp3?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceculture-lofi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceculture-lofi.mp3?id=radiofrance
|
|
```
|
|
|
|
**France Inter :**
|
|
```
|
|
https://stream.radiofrance.fr/franceinter/franceinter.m3u8?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceinter-hifi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceinter-midfi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceinter-midfi.mp3?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceinter-lofi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/franceinter-lofi.mp3?id=radiofrance
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Webradios thématiques
|
|
|
|
### FIP Webradios
|
|
|
|
FIP propose plusieurs webradios thématiques. Format des URLs :
|
|
|
|
```
|
|
https://icecast.radiofrance.fr/fip{variant}-{qualite}.aac?id=radiofrance
|
|
```
|
|
|
|
#### Variantes disponibles (confirmées)
|
|
|
|
| Variante | URL | Status |
|
|
|----------|-----|--------|
|
|
| FIP principale | `fip-hifi.aac` | ✅ |
|
|
| FIP Rock | `fiprock-hifi.aac` | ✅ |
|
|
| FIP Jazz | `fipjazz-hifi.aac` | ✅ |
|
|
| FIP Groove | `fipgroove-hifi.aac` | ✅ |
|
|
| FIP Reggae | `fipreggae-hifi.aac` | ✅ |
|
|
| FIP Electro | `fipelectro-hifi.aac` | ✅ |
|
|
| FIP Metal | `fipmetal-hifi.aac` | ✅ |
|
|
| FIP Nouveautés | `fipnouveautes-hifi.aac` | ✅ |
|
|
| FIP Pop | `fippop-hifi.aac` | ✅ |
|
|
|
|
**Exemples :**
|
|
```
|
|
https://icecast.radiofrance.fr/fiprock-hifi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/fipjazz-midfi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/fipgroove-lofi.aac?id=radiofrance
|
|
```
|
|
|
|
### France Musique Webradios
|
|
|
|
Format similaire :
|
|
|
|
```
|
|
https://icecast.radiofrance.fr/francemusique{variant}-{qualite}.aac?id=radiofrance
|
|
```
|
|
|
|
#### Variantes disponibles (confirmées)
|
|
|
|
| Variante | URL | Status |
|
|
|----------|-----|--------|
|
|
| France Musique principale | `francemusique-hifi.aac` | ✅ |
|
|
| La Jazz | `francemusiquelajazz-hifi.aac` | ✅ |
|
|
| La Contemporaine | `francemusiquelacontemporaine-hifi.aac` | ✅ |
|
|
| Baroque | `francemusiquebaroque-hifi.aac` | ✅ |
|
|
| Opéra | `francemusiqueopera-hifi.aac` | ✅ |
|
|
|
|
**Exemples :**
|
|
```
|
|
https://icecast.radiofrance.fr/francemusiquelajazz-hifi.aac?id=radiofrance
|
|
https://icecast.radiofrance.fr/francemusiquebaroque-midfi.aac?id=radiofrance
|
|
```
|
|
|
|
---
|
|
|
|
## 5. France Bleu - Radios locales
|
|
|
|
### API
|
|
```
|
|
https://www.radiofrance.fr/francebleu/api/live?
|
|
```
|
|
|
|
### Structure spécifique
|
|
|
|
Le champ `localRadios` contient la liste de toutes les radios locales :
|
|
|
|
```json
|
|
{
|
|
"stationName": "francebleu",
|
|
"delayToRefresh": 2090000,
|
|
"now": { /* ... */ },
|
|
"localRadios": [
|
|
{
|
|
"id": 12,
|
|
"title": "ICI Alsace",
|
|
"name": "francebleu_alsace",
|
|
"isOnAir": true
|
|
},
|
|
{
|
|
"id": 13,
|
|
"title": "ICI Armorique",
|
|
"name": "francebleu_armorique",
|
|
"isOnAir": true
|
|
}
|
|
// ... ~40 radios locales
|
|
]
|
|
}
|
|
```
|
|
|
|
### Format des flux locaux
|
|
|
|
**Hypothèse (à confirmer) :**
|
|
```
|
|
https://icecast.radiofrance.fr/fb{nom}-hifi.aac?id=radiofrance
|
|
```
|
|
|
|
Exemple :
|
|
```
|
|
https://icecast.radiofrance.fr/fbalsace-hifi.aac?id=radiofrance
|
|
```
|
|
|
|
---
|
|
|
|
## 6. API Pikapi (Images)
|
|
|
|
### Format
|
|
```
|
|
https://www.radiofrance.fr/pikapi/images/{uuid}/{taille}
|
|
```
|
|
|
|
### Tailles disponibles
|
|
|
|
Basé sur l'analyse des réponses, plusieurs tailles semblent disponibles :
|
|
|
|
- `88x88` - Miniature
|
|
- `200x200` - Petite
|
|
- `420x720` - Moyenne portrait
|
|
- `560x960` - Grande portrait
|
|
- `1200x680` - Grande paysage
|
|
- `raw` - Taille originale
|
|
|
|
**Exemples :**
|
|
```
|
|
https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39/200x200
|
|
https://www.radiofrance.fr/pikapi/images/436430f7-5b2b-43f2-9f3c-28f2ad6cae39/1200x680
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Autres endpoints (configuration)
|
|
|
|
D'après l'analyse du fichier HAR, voici d'autres endpoints internes utilisés :
|
|
|
|
### Endpoints de configuration (dans `__data.json`)
|
|
|
|
- **`https://kirby.radiofrance.fr`** - CMS Kirby
|
|
- **`https://www.radiofrance.fr/pikapi`** - API images
|
|
- **`https://www.radiofrance.fr/transistor`** - API Transistor
|
|
- **`https://api.radiofrance.fr/livemeta/live`** - API LiveMeta
|
|
- **`https://preroll.radiofrance.fr`** - Publicités pre-roll
|
|
|
|
### API Expressions (contenu éditorial)
|
|
|
|
```
|
|
https://www.radiofrance.fr/api/expressions?variant=vertical&limit=36&ids={uuid,uuid,...}
|
|
```
|
|
|
|
Retourne des contenus éditoriaux par UUIDs.
|
|
|
|
---
|
|
|
|
## 8. Résumé pour PMOMusic
|
|
|
|
### Recommandations d'implémentation
|
|
|
|
#### Pour les métadonnées live
|
|
|
|
**Option 1 (recommandée) :** API `/api/live?` par station
|
|
```rust
|
|
async fn fetch_live_metadata(station: &str) -> Result<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 :
|
|
|
|
```rust
|
|
loop {
|
|
let metadata = fetch_live_metadata("franceculture").await?;
|
|
|
|
// Afficher/utiliser les métadonnées
|
|
println!("{} - {}",
|
|
metadata.now.first_line.title,
|
|
metadata.now.second_line.title
|
|
);
|
|
|
|
// Attendre le temps recommandé
|
|
tokio::time::sleep(
|
|
Duration::from_millis(metadata.delay_to_refresh)
|
|
).await;
|
|
}
|
|
```
|
|
|
|
### Liste complète des stations à supporter
|
|
|
|
**Stations principales :**
|
|
- France Inter
|
|
- France Info
|
|
- France Culture
|
|
- France Musique
|
|
- FIP
|
|
- Mouv'
|
|
- Mon Petit France Inter
|
|
|
|
**Webradios FIP (9):**
|
|
- FIP principale
|
|
- FIP Rock, Jazz, Groove, Reggae, Electro, Metal, Nouveautés, Pop
|
|
|
|
**Webradios France Musique (5+):**
|
|
- France Musique principale
|
|
- La Jazz, La Contemporaine, Baroque, Opéra
|
|
|
|
**Radios locales France Bleu (~40):**
|
|
- À récupérer dynamiquement via `/francebleu/api/live?`
|
|
|
|
---
|
|
|
|
## 9. Points d'attention
|
|
|
|
### Rate limiting
|
|
- Pas de limite documentée observée
|
|
- Utiliser `delayToRefresh` pour respecter les recommandations
|
|
- Éviter les requêtes inutiles (cache local)
|
|
|
|
### User-Agent
|
|
Pour un projet open-source, utiliser un User-Agent identifiable :
|
|
```
|
|
PMOMusic/0.3.10 (https://github.com/votre-repo)
|
|
```
|
|
|
|
### Gestion d'erreurs
|
|
- Les APIs peuvent retourner des données vides (`null`)
|
|
- Le champ `song` n'existe que pour les radios musicales
|
|
- `localRadios` n'existe que pour France Bleu
|
|
|
|
### Respect des CGU
|
|
- Ces APIs sont utilisées par le site officiel
|
|
- Usage pour un projet open-source personnel/non-commercial
|
|
- Ne pas redistribuer les flux audio commercialement
|
|
|
|
---
|
|
|
|
## 10. Annexes
|
|
|
|
### Exemple complet en Rust
|
|
|
|
```rust
|
|
use serde::{Deserialize, Serialize};
|
|
use reqwest;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LiveResponse {
|
|
pub station_name: String,
|
|
pub delay_to_refresh: u64,
|
|
pub migrated: bool,
|
|
pub now: ShowMetadata,
|
|
pub next: Option<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** :
|
|
```yaml
|
|
# Dans .pmomusic/config.yaml
|
|
sources:
|
|
radiofrance:
|
|
stations_cache:
|
|
version: 1 # Version du schéma de découverte
|
|
last_updated: 1737565200 # Unix timestamp
|
|
ttl_days: 7 # TTL par défaut : 7 jours
|
|
stations:
|
|
- slug: "franceculture"
|
|
name: "France Culture"
|
|
type: "main"
|
|
- slug: "fip"
|
|
name: "FIP"
|
|
type: "main"
|
|
- slug: "fip_rock"
|
|
name: "FIP Rock"
|
|
type: "webradio"
|
|
parent: "fip"
|
|
- slug: "francebleu_alsace"
|
|
name: "ICI Alsace"
|
|
type: "local"
|
|
region: "Alsace"
|
|
id: 12
|
|
# ... ~50+ stations au total
|
|
```
|
|
|
|
**Logique de rafraîchissement** :
|
|
1. Lire le cache depuis la config
|
|
2. Vérifier `version` (invalide si ancienne version de découverte)
|
|
3. Vérifier TTL : `now - last_updated < ttl_days * 86400`
|
|
4. Si valide : retourner le cache
|
|
5. Si invalide ou absent : appeler `discover_all_stations()` et mettre à jour la config
|
|
|
|
### 2. Organisation des stations
|
|
|
|
Les stations doivent être organisées logiquement pour la navigation UPnP :
|
|
|
|
```
|
|
Radio France (racine)
|
|
├── France Culture
|
|
├── France Inter
|
|
├── France Info
|
|
├── France Musique
|
|
├── FIP
|
|
│ ├── FIP (principale)
|
|
│ ├── FIP Rock
|
|
│ ├── FIP Jazz
|
|
│ ├── FIP Groove
|
|
│ ├── FIP Reggae
|
|
│ ├── FIP Electro
|
|
│ ├── FIP Metal
|
|
│ ├── FIP Nouveautés
|
|
│ └── FIP Pop
|
|
├── Mouv'
|
|
└── ICI (France Bleu renommé)
|
|
├── ICI Alsace
|
|
├── ICI Armorique
|
|
├── ICI Auxerre
|
|
├── ... (~40 radios locales)
|
|
└── ICI Vaucluse
|
|
```
|
|
|
|
**Règles de regroupement** :
|
|
- **Stations principales** : Une entrée par station principale (France Culture, France Inter, etc.)
|
|
- **Stations avec webradios** (FIP, France Musique) : Un folder contenant :
|
|
1. La station principale en premier
|
|
2. Les webradios triées alphabétiquement
|
|
- **France Bleu** : Renommé "ICI" avec toutes les radios locales dedans
|
|
|
|
**Changement de label** :
|
|
- API retourne : `"France Bleu"` → Affichage : `"ICI"`
|
|
- API retourne : `"ICI Alsace"` → Affichage : `"ICI Alsace"` (inchangé)
|
|
- Slugs conservés tels quels : `francebleu_alsace`, `francebleu`, etc.
|
|
|
|
### 3. Métadonnées live avec rafraîchissement intelligent
|
|
|
|
Pour chaque station, on doit pouvoir obtenir les métadonnées live avec cache court terme :
|
|
|
|
**Cache de métadonnées live** :
|
|
- Durée : Utiliser le champ `delayToRefresh` de l'API (généralement 2-5 minutes)
|
|
- Stockage : En mémoire uniquement (pas dans pmoconfig)
|
|
- Invalidation : Automatique après `delayToRefresh` millisecondes
|
|
|
|
**Stratégie** :
|
|
```rust
|
|
struct LiveMetadataCache {
|
|
metadata: LiveResponse,
|
|
fetched_at: SystemTime,
|
|
valid_until: SystemTime,
|
|
}
|
|
|
|
// Pseudo-code
|
|
fn get_live_metadata(station: &str) -> Result<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
|
|
|
|
```rust
|
|
// Pseudo-structure d'une playlist de station
|
|
PMOPlaylist {
|
|
id: "radiofrance:franceculture",
|
|
role: PlaylistRole::Radio,
|
|
volatile: true, // Les métadonnées changent, pas le contenu
|
|
|
|
// Métadonnées de la playlist (= métadonnées de la station)
|
|
title: "France Culture", // Nom de la station
|
|
artist: "Les Matins", // Nom de l'émission en cours (now.firstLine.title)
|
|
album: "France Culture", // Nom de la station (répété)
|
|
cover_pk: "COVER_PK", // Cover de l'émission en cours (now.visualBackground)
|
|
|
|
// Contenu : UN SEUL ITEM
|
|
items: [
|
|
PMOItem {
|
|
id: "radiofrance:franceculture:stream",
|
|
title: "Le Journal de l'éco • Le jouet profite...", // now.secondLine.title
|
|
artist: "Guillaume Erner", // now.producer ou show producer
|
|
album: "Les Matins", // now.firstLine.title (émission)
|
|
genre: "Talk Radio", // Type de station
|
|
|
|
// Stream URL (AAC 192 kbps ou HLS)
|
|
url: "https://icecast.radiofrance.fr/franceculture-hifi.aac?id=radiofrance",
|
|
|
|
// Métadonnées techniques
|
|
protocol_info: "http-get:*:audio/aac:*",
|
|
bitrate: 192000,
|
|
sample_rate: 48000,
|
|
channels: 2,
|
|
|
|
// Cover de l'émission/morceau
|
|
cover_pk: "COVER_PK",
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Mapping des métadonnées API → UPnP
|
|
|
|
**Pour les radios parlées (France Culture, France Inter, France Info)** :
|
|
|
|
| Champ UPnP | Source API | Exemple |
|
|
|------------|------------|---------|
|
|
| Playlist Title | Station name | "France Culture" |
|
|
| Playlist Artist | `now.firstLine.title` | "Les Matins" |
|
|
| Playlist Cover | `now.visualBackground` → cache | UUID de cover |
|
|
| Item Title | `now.firstLine.title` + `now.secondLine.title` | "Les Matins • Le Journal de l'éco" |
|
|
| Item Artist | `now.producer` | "Guillaume Erner" |
|
|
| Item Album | `now.firstLine.title` | "Les Matins" |
|
|
| Item Cover | `now.visualBackground` → cache | UUID de cover |
|
|
| Item Genre | "Talk Radio" | Fixe |
|
|
|
|
**Pour les radios musicales (FIP, France Musique)** :
|
|
|
|
| Champ UPnP | Source API | Exemple |
|
|
|------------|------------|---------|
|
|
| Playlist Title | Station name | "FIP Rock" |
|
|
| Playlist Artist | `now.song.artists` OU `now.firstLine.title` | "The Rolling Stones" |
|
|
| Playlist Cover | `now.song` image OU `now.visualBackground` | UUID de cover |
|
|
| Item Title | `now.song.title` OU `now.firstLine.title` | "Paint It Black" |
|
|
| Item Artist | `now.song.artists` | "The Rolling Stones" |
|
|
| Item Album | `now.song.release.title` | "Aftermath" |
|
|
| Item Cover | `now.song` image OU `now.visualBackground` | UUID de cover |
|
|
| Item Genre | "Music" OU genre spécifique | "Rock" |
|
|
|
|
**Note importante** : Les métadonnées changent régulièrement (toutes les 2-5 minutes), mais l'URL du stream reste la même. C'est pour cela que les playlists sont **volatiles** : on ne change pas leur contenu (toujours 1 item), mais on met à jour les métadonnées de cet item.
|
|
|
|
#### Gestion des covers
|
|
|
|
**Stratégie de cache** :
|
|
- Les covers doivent être cachées dans `pmocovers`
|
|
- URL source : `now.visualBackground.src` ou image de `now.song`
|
|
- Extraction UUID : Parser l'URL Pikapi pour extraire l'UUID
|
|
- Transformation : Télécharger et convertir en WebP si nécessaire
|
|
- Stockage : Cache avec le PK = `RADIOFRANCE:{uuid}`
|
|
|
|
**Workflow de cache de cover** :
|
|
```rust
|
|
// Pseudo-code
|
|
async fn cache_cover_from_metadata(metadata: &ShowMetadata) -> Option<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** :
|
|
|
|
```rust
|
|
/// 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** :
|
|
|
|
```rust
|
|
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** :
|
|
|
|
```yaml
|
|
sources:
|
|
radiofrance:
|
|
enabled: true # Activation de la source
|
|
base_url: "https://www.radiofrance.fr"
|
|
timeout_secs: 30
|
|
cache_ttl_days: 7 # TTL du cache des stations
|
|
|
|
stations_cache: # Cache des stations découvertes
|
|
version: 1
|
|
last_updated: 1737565200
|
|
stations:
|
|
- slug: "franceculture"
|
|
name: "France Culture"
|
|
type: "main"
|
|
# ... reste des stations
|
|
```
|
|
|
|
## Workflow de mise à jour des métadonnées
|
|
|
|
### Scénario 1 : Première utilisation
|
|
|
|
1. Utilisateur ouvre la source Radio France dans son client UPnP
|
|
2. `RadioFranceSource::browse("radiofrance")` est appelé
|
|
3. Source appelle `stateful_client.get_all_stations()`
|
|
4. Cache vide → Appel `discover_all_stations()` (~3-5 secondes)
|
|
5. Résultat stocké dans config avec timestamp
|
|
6. Retour de la liste des stations
|
|
|
|
### Scénario 2 : Utilisation ultérieure (cache valide)
|
|
|
|
1. Utilisateur ouvre la source Radio France
|
|
2. Source appelle `stateful_client.get_all_stations()`
|
|
3. Cache présent et valide (< 7 jours) → Retour immédiat depuis config
|
|
4. Pas d'appel réseau
|
|
|
|
### Scénario 3 : Lecture d'une station
|
|
|
|
1. Utilisateur sélectionne "France Culture" et lance la lecture
|
|
2. Source appelle `stateful_client.build_station_playlist("franceculture")`
|
|
3. Stateful client :
|
|
- Appelle `get_live_metadata("franceculture")` (cache 2-5 min si présent)
|
|
- Extrait les métadonnées de l'émission en cours
|
|
- Cache la cover de l'émission via `pmocovers`
|
|
- Construit la playlist avec 1 item (stream HiFi)
|
|
4. Retour de la playlist au renderer
|
|
|
|
### Scénario 4 : Mise à jour des métadonnées pendant la lecture
|
|
|
|
1. Renderer lit le stream depuis 3 minutes
|
|
2. Control point demande les métadonnées à jour
|
|
3. Source appelle `stateful_client.update_playlist_metadata()`
|
|
4. Stateful client :
|
|
- Vérifie le cache des métadonnées live
|
|
- Si expiré (> `delayToRefresh` ms) : appelle l'API
|
|
- Met à jour les métadonnées de la playlist
|
|
- Cache la nouvelle cover si différente
|
|
5. Control point reçoit les nouvelles métadonnées
|
|
|
|
**Important** : L'URL du stream ne change JAMAIS pendant la lecture. Seules les métadonnées (titre, artiste, cover) changent.
|
|
|
|
## Architecture des fichiers
|
|
|
|
```
|
|
pmoradiofrance/
|
|
├── src/
|
|
│ ├── lib.rs # Exports publics
|
|
│ ├── client.rs # Client HTTP basique (Round 3) ✅
|
|
│ ├── models.rs # Structures de données (Round 3) ✅
|
|
│ ├── error.rs # Types d'erreur ✅
|
|
│ ├── stateful_client.rs # Client stateful (Round 4) 🆕
|
|
│ ├── playlist.rs # Construction de playlists (Round 4) 🆕
|
|
│ ├── config_ext.rs # Extension pmoconfig (Round 4) 🆕
|
|
│ └── source.rs # Implémentation MusicSource (Round 5)
|
|
├── assets/
|
|
│ └── default.webp # Logo Radio France 300x300px
|
|
├── Cargo.toml
|
|
└── README.md
|
|
```
|
|
|
|
## Dépendances supplémentaires
|
|
|
|
```toml
|
|
[dependencies]
|
|
# Déjà présentes (Round 3)
|
|
reqwest = { version = "0.12", features = ["json"] }
|
|
tokio = { workspace = true }
|
|
serde = { workspace = true }
|
|
serde_json = { workspace = true }
|
|
serde_yaml = { workspace = true }
|
|
chrono = { workspace = true }
|
|
async-trait = { workspace = true }
|
|
thiserror = { workspace = true }
|
|
anyhow = { workspace = true }
|
|
tracing = { workspace = true }
|
|
url = "2.5"
|
|
scraper = "0.22"
|
|
regex = "1.11"
|
|
pmosource = { path = "../pmosource" }
|
|
|
|
# Nouvelles (Round 4)
|
|
pmoconfig = { path = "../pmoconfig" } # Configuration persistante
|
|
pmocovers = { path = "../pmocovers" } # Cache de covers
|
|
# pmoaudiocache NON utilisé (pas de cache audio pour les streams live)
|
|
|
|
[features]
|
|
default = ["pmoconfig"]
|
|
pmoconfig = ["dep:pmoconfig"]
|
|
cache = ["dep:pmocovers"]
|
|
logging = []
|
|
server = ["pmosource/server", "pmoconfig", "cache"]
|
|
full = ["server", "logging"]
|
|
```
|
|
|
|
## Considérations d'implémentation
|
|
|
|
### Thread safety
|
|
|
|
Le client stateful doit être thread-safe car il sera partagé entre plusieurs threads (ContentDirectory, AVTransport, etc.) :
|
|
|
|
```rust
|
|
pub struct RadioFranceStatefulClient {
|
|
client: RadioFranceClient, // Clone cheap (Arc interne)
|
|
config: Arc<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**
|