Refactorisation complète de pmoradiofrance : cache centralisé avec système d'événements

Refactorisation complète de la crate `pmoradiofrance` pour simplifier l'architecture autour d'un cache de métadonnées centralisé avec système d'événements.

## Objectifs

1. Simplifier les structures de stations (supprimer StationType)
2. Créer un cache de métadonnées in-memory avec TTL basé sur `end_time`
3. Maintenir le cache de stations persistant (pmoconfig, TTL 1 semaine)
4. Implémenter un système d'événements pour la synchronisation GENA
5. Unifier les méthodes `to_didl()` pour retourner des Containers DIDL
6. Gérer automatiquement le cache des covers via pmocovers

## Changements architecturaux majeurs

### 1. Nouveau fichier: metadata_cache.rs

**Créé**: `pmoradiofrance/src/metadata_cache.rs`

Contient deux structures principales:

- **CachedMetadata**: Stocke uniquement les données nécessaires au DIDL (titre, artiste, album, cover, stream URL, etc.)
- **MetadataCache**: Gère le cache in-memory avec TTL + cache persistant des stations + système d'événements

**Fonctionnalités**:
- TTL basé sur `end_time` de l'API Radio France
- Gestion automatique du cache de covers via pmocovers
- Système subscribe/notify pour les événements
- Graceful degradation si API Radio France down
- Méthode `to_didl()` retournant une playlist à un item avec métadonnées identiques

### 2. Suppression: stateful_client.rs

**Supprimé**: `pmoradiofrance/src/stateful_client.rs`

Raison: Complètement redondant avec `MetadataCache`. Toute la logique a été déplacée dans le nouveau module.

### 3. Simplification: models.rs

**Modifications**:
- Supprimé `StationType` enum
- Simplifié `Station` struct (juste `slug` + `name`)
- Supprimé méthodes `is_main()`, `is_webradio()`, `is_local_radio()`, `base_station()`
- Conservé structures d'API (`LiveResponse`, `ShowMetadata`, etc.)

### 4. Simplification: playlist.rs

**Modifications**:
- Supprimé `StationPlaylist` complètement
- Simplifié `StationGroup` et `StationGroups`
- **Important**: `to_didl()` retourne `Container` (pas `Vec<Container>`)
- Logique unifiée: ICI fonctionne comme FIP (plus de traitement spécial)
- Préservé les règles de mapping RF → UPnP existantes

### 5. Refactoring: source.rs

**Modifications**:
- Utilise uniquement `MetadataCache` (plus de `stateful_client`)
- Simplifié `browse()` en 3 cas simples
- Abonnement aux événements du cache pour GENA
- Retourne des `Container` (cohérence avec to_didl)

### 6. Adaptation: config_ext.rs

**Modifications**:
- Format simplifié: `Vec<Station>` au lieu de `CachedStationList`
- TTL reste à 7 jours (1 semaine)

### 7. Mise à jour: lib.rs

**Modifications**:
- Ajouté `pub mod metadata_cache;`
- Supprimé export de `stateful_client`
- Ajouté exports: `MetadataCache`, `CachedMetadata`

## Hiérarchie de browse

**Niveau 0**: `radiofrance`
- Retourne UN Container contenant les containers de groupes
- Exemple: Container "FIP", Container "France Culture", Container "ICI"

**Niveau 1**: `radiofrance:group:fip` ou `radiofrance:ici`
- Si 1 station: retourne directement la playlist (Container playlistContainer)
- Si plusieurs stations: retourne un container contenant les playlists

**Niveau 2**: `radiofrance:fip`
- Retourne Container playlistContainer avec 1 item
- Métadonnées identiques entre playlist et item

## Règles de mapping préservées

Les règles existantes de transformation RF → UPnP ont été préservées:
- Radio musicale avec song → métadonnées du morceau
- Radio parlée → agrégation émission/producteur
- Éviter duplications du nom de station
- Calcul de duration depuis end_time

## Système d'événements

**Flux**:
1. `MetadataCache` rafraîchit les métadonnées d'un slug
2. Notifie tous les abonnés via `notify(slug)`
3. `RadioFranceSource` reçoit l'événement
4. Émet un événement GENA UPnP pour la playlist `radiofrance:{slug}`
5. Le Control Point reçoit la notification et peut se mettre à jour

## Fichiers modifiés

### Créés
- `pmoradiofrance/src/metadata_cache.rs`

### Supprimés
- `pmoradiofrance/src/stateful_client.rs`

### Modifiés
- `pmoradiofrance/src/models.rs`
- `pmoradiofrance/src/playlist.rs`
- `pmoradiofrance/src/source.rs`
- `pmoradiofrance/src/config_ext.rs`
- `pmoradiofrance/src/lib.rs`

### Inchangés
- `pmoradiofrance/src/client.rs`
- `pmoradiofrance/src/error.rs`

## Points de vigilance

1. **Migration**: Le cache pmoconfig existant sera invalidé (nouveau format)
2. **Covers**: Nécessite que pmocovers soit initialisé via cache_registry
3. **Thread safety**: Utilisation d'Arc<RwLock> pour la sécurité thread
4. **Graceful degradation**: Retourne cache expiré si API Radio France down

## Prochaines étapes

1. Tester le cache de métadonnées (TTL, refresh, graceful degradation)
2. Tester le système d'événements
3. Tester le browse sur les 3 niveaux
4. Vérifier les événements GENA
5. Vérifier que les covers sont correctement cachées
This commit is contained in:
2026-01-24 14:15:26 +01:00
parent 9bb4f00650
commit 8ba627aee5
18 changed files with 1370 additions and 2377 deletions

View File

@@ -1,436 +1,146 @@
# Rapport : Refonte du cache Radio France avec système d'événements
# Rapport: Simplification de pmoradiofrance
## Résumé
Ce rapport documente le plan d'implémentation validé pour la refonte complète du système de cache de métadonnées Radio France dans la crate **pmoradiofrance**.
**Objectif** : Éliminer la duplication des métadonnées et créer une architecture événementielle avec un seul cache de métadonnées (source unique de vérité), permettant des notifications GENA cohérentes pour les Control Points UPnP.
**Crate concernée** : `pmoradiofrance`
## Architecture cible
### Principe fondamental
**Une seule source de vérité** : Le cache HTTP avec TTL dans `RadioFranceStatefulClient`.
```mermaid
flowchart TB
API[Radio France API]
Cache[MetadataCache<br/>TTL automatique<br/>Événements]
Source[RadioFranceSource<br/>MusicSource trait]
Browse[Browse/DIDL]
Refresh[Refresh thread]
GENA[Notifications GENA UPnP]
CP[Control Point]
API -->|Fetch quand TTL expiré| Cache
Cache -->|Événement: slug modifié| Source
Cache -->|get_metadata| Browse
Browse -->|Reconstruit containers + playlists| DIDL[DIDL]
Refresh -->|get_metadata chaque seconde| Cache
Source -->|S'abonne aux événements| Cache
Source -->|Notifie changements| GENA
CP -->|Subscribe à playlist| GENA
style Cache fill:#90EE90
style Source fill:#FFB6C1
style Refresh fill:#87CEEB
```
### Hiérarchie UPnP : Concept de playlist à 1 item
**Innovation** : Chaque slug Radio France est exposé comme une **playlist contenant 1 item** (et non comme un simple item). Cela permet au Control Point de s'abonner aux changements de la playlist via GENA.
```
radiofrance/ (container root)
├─ Stations standalone (containers de playlists)
│ ├─ radiofrance:franceculture/ (container → playlist)
│ │ └─ radiofrance:franceculture:stream (item unique dans la playlist)
│ └─ radiofrance:franceinter/ (container → playlist)
│ └─ radiofrance:franceinter:stream (item unique)
├─ Groupes avec webradios (containers de containers)
│ ├─ radiofrance:group:fip/ (container de groupe)
│ │ ├─ radiofrance:fip/ (container → playlist)
│ │ │ └─ radiofrance:fip:stream (item)
│ │ ├─ radiofrance:fip_rock/ (container → playlist)
│ │ │ └─ radiofrance:fip_rock:stream (item)
│ │ └─ ...
└─ radiofrance:ici/ (container de groupe)
├─ radiofrance:francebleu_alsace/ (container → playlist)
│ └─ radiofrance:francebleu_alsace:stream (item)
└─ ...
```
## Plan d'implémentation
### Phase 1 : Système d'événements dans le cache
**Fichier** : `pmoradiofrance/src/stateful_client.rs`
#### 1.1 Ajouter le système de callback
```rust
// Type de callback pour notifications
pub type MetadataUpdateCallback = Arc<dyn Fn(&str) + Send + Sync>;
pub struct RadioFranceStatefulClient {
client: RadioFranceClient,
config: Arc<Config>,
metadata_cache: Arc<RwLock<HashMap<String, LiveMetadataCache>>>,
// NOUVEAU : Liste des callbacks abonnés
update_callbacks: Arc<RwLock<Vec<MetadataUpdateCallback>>>,
}
```
#### 1.2 Méthodes d'abonnement et notification
```rust
impl RadioFranceStatefulClient {
/// S'abonner aux mises à jour de métadonnées
pub fn subscribe_to_updates(&self, callback: MetadataUpdateCallback) {
let mut callbacks = self.update_callbacks.write().unwrap();
callbacks.push(callback);
}
/// Notifier tous les abonnés d'une mise à jour
fn notify_update(&self, slug: &str) {
let callbacks = self.update_callbacks.read().unwrap();
for callback in callbacks.iter() {
callback(slug);
}
}
}
```
#### 1.3 Refactorisation de get_live_metadata()
**Principe** : Une seule méthode qui gère tout automatiquement :
- Si cache valide → retour immédiat
- Si cache expiré → fetch API + mise à jour cache + notification événements
```rust
pub async fn get_live_metadata(&self, station: &str) -> Result<LiveResponse> {
// Check cache first
{
let cache = self.metadata_cache.read().unwrap();
if let Some(entry) = cache.get(station) {
if entry.is_valid() {
return Ok(entry.metadata.clone());
}
}
}
// Cache miss or expired - fetch from API
let metadata = tokio::time::timeout(
Duration::from_secs(5),
self.client.live_metadata(station),
).await??;
// Update cache
{
let mut cache = self.metadata_cache.write().unwrap();
cache.insert(station.to_string(), LiveMetadataCache::new(metadata.clone()));
}
// Notify subscribers
self.notify_update(station);
Ok(metadata)
}
```
**SUPPRIMER** : `refresh_live_metadata()` - Redondant, le TTL gère tout.
### Phase 2 : Hiérarchie UPnP avec playlists
**Fichier** : `pmoradiofrance/src/source.rs`
#### 2.1 Supprimer le cache d'items
```rust
pub struct RadioFranceSource {
pub(crate) client: RadioFranceStatefulClient,
// SUPPRIMER : playlists: Arc<RwLock<HashMap<String, StationPlaylist>>>,
refresh_handles: Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
// ... reste inchangé
}
```
#### 2.2 Construction du container de playlist avec item
**Principe clé** : Un seul appel au cache, métadonnées cohérentes container/item.
```rust
/// Construit le container de playlist avec son unique item (métadonnées cohérentes)
async fn build_station_playlist(&self, station: &Station) -> Result<Container> {
// UN SEUL appel cache - garantit cohérence
let metadata = self.client.get_live_metadata(&station.slug).await?;
// Build l'item avec pmoDidl
let mut item = StationPlaylist::build_item_from_metadata(
station,
&metadata,
self.cover_cache.as_ref(),
self.server_base_url.as_deref(),
).await?;
// Parent_id de l'item = le container de playlist
let playlist_id = format!("radiofrance:{}", station.slug);
item.parent_id = playlist_id.clone();
// Container avec MÊMES métadonnées que l'item
let container = Container {
id: playlist_id,
parent_id: self.get_parent_id_for_station(station),
restricted: Some("1".to_string()),
child_count: Some(1),
searchable: Some("0".to_string()),
// Métadonnées identiques à l'item
title: item.title.clone(),
artist: item.artist.clone(),
album_art: item.album_art.clone(),
class: "object.container.playlistContainer".to_string(),
containers: vec![],
items: vec![item], // Item inclus dans le container
};
Ok(container)
}
```
#### 2.3 Refactorisation du browse
Le browse est simplifié car les containers contiennent déjà leurs items.
```rust
async fn browse(&self, object_id: &str) -> pmosource::Result<BrowseResult> {
match object_id {
"radiofrance" => {
let container = self.build_container_tree().await?;
Ok(BrowseResult::Containers(container.containers))
}
id if id.starts_with("radiofrance:group:") => {
// Retourne des containers de playlists
let slug = id.strip_prefix("radiofrance:group:")?;
let stations = self.get_group_stations(slug).await?;
let mut containers = Vec::new();
for station in stations {
containers.push(self.build_station_playlist(&station).await?);
}
Ok(BrowseResult::Containers(containers))
}
id if id.starts_with("radiofrance:") && !id.contains(":stream") => {
// Browse de playlist - retourne le container qui contient l'item
let slug = id.strip_prefix("radiofrance:")?;
let station = self.get_station_by_slug(slug).await?;
let container = self.build_station_playlist(&station).await?;
Ok(BrowseResult::Containers(vec![container]))
}
// ... autres cas
}
}
```
### Phase 3 : Notifications GENA événementielles
**Fichier** : `pmoradiofrance/src/source.rs`
#### 3.1 Abonnement aux événements du cache
Dans le constructeur `new()`, s'abonner aux événements :
```rust
pub async fn new(config: Arc<Config>) -> Result<Self> {
let client = RadioFranceStatefulClient::new(config).await?;
let source = Self {
client,
refresh_handles: Arc::new(RwLock::new(HashMap::new())),
// ...
};
// S'abonner aux événements du cache
let container_notifier = source.container_notifier.clone();
let update_id = source.update_id.clone();
let last_change = source.last_change.clone();
source.client.subscribe_to_updates(Arc::new(move |slug: &str| {
let slug = slug.to_string();
let update_id = update_id.clone();
let last_change = last_change.clone();
let container_notifier = container_notifier.clone();
// Spawn async car callback n'est pas async
tokio::spawn(async move {
*update_id.write().await += 1;
*last_change.write().await = Some(SystemTime::now());
if let Some(ref notifier) = container_notifier {
// Notifier le container de PLAYLIST (pas l'item)
notifier(&[format!("radiofrance:{}", slug)]);
}
});
}));
Ok(source)
}
```
#### 3.2 Simplification du refresh thread
**Ultra-simple** : Appeler `get_live_metadata()` toutes les secondes.
```rust
pub async fn start_metadata_refresh(&self, station_slug: &str) -> Result<()> {
let mut handles = self.refresh_handles.write().await;
if handles.contains_key(station_slug) {
return Ok(()); // Already running
}
let client = self.client.clone();
let slug = station_slug.to_string();
let handle = tokio::spawn(async move {
loop {
// Appel simple - le cache + TTL + événements gèrent tout
let _ = client.get_live_metadata(&slug).await;
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
handles.insert(station_slug.to_string(), handle);
Ok(())
}
```
### Phase 4 : Pas de modification
**Fichier** : `pmoradiofrance/src/config_ext.rs`
Le cache des stations avec TTL de 7 jours est déjà correct, pas de changement.
### Phase 5 : Synchronisation async
**Fichier** : `pmoradiofrance/src/source.rs`
Remplacer `std::sync::RwLock` par `tokio::sync::RwLock` pour les champs utilisés dans le callback :
```rust
pub struct RadioFranceSource {
// ...
update_id: Arc<tokio::sync::RwLock<u32>>,
last_change: Arc<tokio::sync::RwLock<Option<SystemTime>>>,
// ...
}
```
### Phase 6 : Nettoyage du code obsolète
**Fichier** : `pmoradiofrance/src/playlist.rs`
Supprimer les méthodes obsolètes :
- `StationPlaylist::update_metadata()`
- `StationPlaylist::update_metadata_no_cache()`
- `StationPlaylist::from_live_metadata()` → Garder uniquement `build_item_from_metadata()`
- `StationPlaylist::from_live_metadata_no_cache()` → Garder uniquement `build_item_from_metadata_sync()`
La structure devient un ensemble de méthodes statiques pour construire des items DIDL.
## Fichiers à modifier
### 1. pmoradiofrance/src/stateful_client.rs
- Ajouter `update_callbacks: Arc<RwLock<Vec<MetadataUpdateCallback>>>`
- Ajouter `subscribe_to_updates()` et `notify_update()`
- Refactoriser `get_live_metadata()` pour gérer cache + TTL + notifications
- **SUPPRIMER** `refresh_live_metadata()` (redondant)
### 2. pmoradiofrance/src/source.rs
- Supprimer le champ `playlists`
- Ajouter `build_station_playlist()` (container + item, 1 seul appel cache)
- Ajouter `get_parent_id_for_station()`
- Modifier `browse()` pour utiliser `build_station_playlist()` partout
- S'abonner aux événements du cache dans `new()`
- Simplifier `start_metadata_refresh()` (appel `get_live_metadata()` toutes les secondes)
- Changer `update_id` et `last_change` vers `tokio::sync::RwLock`
### 3. pmoradiofrance/src/playlist.rs
- Supprimer `update_metadata()` et `update_metadata_no_cache()`
- Conserver uniquement `build_item_from_metadata()` et `build_item_from_metadata_sync()`
- Simplifier la structure (méthodes statiques uniquement)
## Tests de vérification
### 1. Browse de la racine
- Ouvrir `radiofrance/` dans l'interface
- Vérifier des **containers** (groupes + stations standalone)
- **PAS** d'items directs à la racine
### 2. Browse d'un groupe (ex: FIP)
- Ouvrir `radiofrance:group:fip/`
- Vérifier des **containers** (FIP, FIP Rock, FIP Jazz, etc.)
- Classe UPnP : `object.container.playlistContainer`
- Child count : `1` pour chaque
### 3. Browse d'une playlist (ex: FIP)
- Ouvrir `radiofrance:fip/` (le container de playlist)
- Vérifier **1 seul item** : `radiofrance:fip:stream`
- Métadonnées à jour (titre, artiste, album du morceau en cours)
- Re-browse immédiat : devrait utiliser le cache HTTP (rapide)
### 4. Lecture d'un stream
- Lancer la lecture via la playlist `radiofrance:fip/`
- Vérifier que le stream démarre
- Attendre 2-5 minutes (délai de refresh)
- Vérifier dans les logs que le refresh a lieu
- Re-browser la playlist : les métadonnées doivent avoir changé
### 5. Vérification des événements GENA
- Avec un Control Point UPnP supportant l'abonnement aux playlists
- S'abonner à la playlist `radiofrance:fip`
- Lancer le stream
- Vérifier que les notifications GENA arrivent à chaque refresh
- Le Control Point doit re-browse automatiquement et voir les nouvelles métadonnées
### Logs à surveiller
```
DEBUG RadioFranceStatefulClient: Using cached metadata for fip (TTL: XXms)
DEBUG RadioFranceStatefulClient: Fetching live metadata for fip
DEBUG RadioFranceSource: Notifying UPnP container update: radiofrance:fip
```
## Avantages de cette architecture
1. **Source unique de vérité** : Le cache HTTP du `RadioFranceStatefulClient`
2. **Métadonnées toujours à jour** : TTL automatique + événements
3. **Pas de duplication** : Les items ne sont jamais stockés, reconstruits à chaque browse
4. **GENA cohérent** : Notifications envoyées uniquement lors de vraies mises à jour
5. **Code simplifié** : Moins de gestion de cache, moins de bugs possibles
6. **Performance** : Cache HTTP rapide (< 50ms pour browse complet avec cache chaud)
## Ordre d'implémentation
1. Phase 1 : Système d'événements dans le cache
2. Phase 5 : Corriger les RwLock (prérequis pour Phase 3)
3. Phase 3 : Refondre le refresh avec abonnement
4. Phase 2 : Modifier la hiérarchie UPnP et supprimer le cache d'items
5. Phase 6 : Nettoyer le code obsolète
6. Tests et validation
## Notes techniques
### Thread safety
Le callback d'événement n'est pas async. Solution : spawn une tâche async depuis le callback (déjà implémenté dans Phase 3.1).
### Performance
Reconstruction DIDL à chaque browse mais :
- Cache HTTP évite les appels réseau
- Construction DIDL légère (structures en mémoire)
- Métadonnées toujours fraîches
- Performance attendue : < 50ms avec cache chaud
Refactoring complet de la crate `pmoradiofrance` pour simplifier l'architecture autour d'un cache de métadonnées centralisé avec système d'événements.
## Objectifs
1. Simplifier les structures de stations (supprimer StationType)
2. Créer un cache de métadonnées in-memory avec TTL basé sur `end_time`
3. Maintenir le cache de stations persistant (pmoconfig, TTL 1 semaine)
4. Implémenter un système d'événements pour la synchronisation GENA
5. Unifier les méthodes `to_didl()` pour retourner des Containers DIDL
6. Gérer automatiquement le cache des covers via pmocovers
## Changements architecturaux majeurs
### 1. Nouveau fichier: metadata_cache.rs
**Créé**: `pmoradiofrance/src/metadata_cache.rs`
Contient deux structures principales:
- **CachedMetadata**: Stocke uniquement les données nécessaires au DIDL (titre, artiste, album, cover, stream URL, etc.)
- **MetadataCache**: Gère le cache in-memory avec TTL + cache persistant des stations + système d'événements
**Fonctionnalités**:
- TTL basé sur `end_time` de l'API Radio France
- Gestion automatique du cache de covers via pmocovers
- Système subscribe/notify pour les événements
- Graceful degradation si API Radio France down
- Méthode `to_didl()` retournant une playlist à un item avec métadonnées identiques
### 2. Suppression: stateful_client.rs
**Supprimé**: `pmoradiofrance/src/stateful_client.rs`
Raison: Complètement redondant avec `MetadataCache`. Toute la logique a été déplacée dans le nouveau module.
### 3. Simplification: models.rs
**Modifications**:
- Supprimé `StationType` enum
- Simplifié `Station` struct (juste `slug` + `name`)
- Supprimé méthodes `is_main()`, `is_webradio()`, `is_local_radio()`, `base_station()`
- Conservé structures d'API (`LiveResponse`, `ShowMetadata`, etc.)
### 4. Simplification: playlist.rs
**Modifications**:
- Supprimé `StationPlaylist` complètement
- Simplifié `StationGroup` et `StationGroups`
- **Important**: `to_didl()` retourne `Container` (pas `Vec<Container>`)
- Logique unifiée: ICI fonctionne comme FIP (plus de traitement spécial)
- Préservé les règles de mapping RF → UPnP existantes
### 5. Refactoring: source.rs
**Modifications**:
- Utilise uniquement `MetadataCache` (plus de `stateful_client`)
- Simplifié `browse()` en 3 cas simples
- Abonnement aux événements du cache pour GENA
- Retourne des `Container` (cohérence avec to_didl)
### 6. Adaptation: config_ext.rs
**Modifications**:
- Format simplifié: `Vec<Station>` au lieu de `CachedStationList`
- TTL reste à 7 jours (1 semaine)
### 7. Mise à jour: lib.rs
**Modifications**:
- Ajouté `pub mod metadata_cache;`
- Supprimé export de `stateful_client`
- Ajouté exports: `MetadataCache`, `CachedMetadata`
## Hiérarchie de browse
**Niveau 0**: `radiofrance`
- Retourne UN Container contenant les containers de groupes
- Exemple: Container "FIP", Container "France Culture", Container "ICI"
**Niveau 1**: `radiofrance:group:fip` ou `radiofrance:ici`
- Si 1 station: retourne directement la playlist (Container playlistContainer)
- Si plusieurs stations: retourne un container contenant les playlists
**Niveau 2**: `radiofrance:fip`
- Retourne Container playlistContainer avec 1 item
- Métadonnées identiques entre playlist et item
## Règles de mapping préservées
Les règles existantes de transformation RF → UPnP ont été préservées:
- Radio musicale avec song → métadonnées du morceau
- Radio parlée → agrégation émission/producteur
- Éviter duplications du nom de station
- Calcul de duration depuis end_time
## Système d'événements
**Flux**:
1. `MetadataCache` rafraîchit les métadonnées d'un slug
2. Notifie tous les abonnés via `notify(slug)`
3. `RadioFranceSource` reçoit l'événement
4. Émet un événement GENA UPnP pour la playlist `radiofrance:{slug}`
5. Le Control Point reçoit la notification et peut se mettre à jour
## Fichiers modifiés
### Créés
- `pmoradiofrance/src/metadata_cache.rs`
### Supprimés
- `pmoradiofrance/src/stateful_client.rs`
### Modifiés
- `pmoradiofrance/src/models.rs`
- `pmoradiofrance/src/playlist.rs`
- `pmoradiofrance/src/source.rs`
- `pmoradiofrance/src/config_ext.rs`
- `pmoradiofrance/src/lib.rs`
### Inchangés
- `pmoradiofrance/src/client.rs`
- `pmoradiofrance/src/error.rs`
## Points de vigilance
1. **Migration**: Le cache pmoconfig existant sera invalidé (nouveau format)
2. **Covers**: Nécessite que pmocovers soit initialisé via cache_registry
3. **Thread safety**: Utilisation d'Arc<RwLock> pour la sécurité thread
4. **Graceful degradation**: Retourne cache expiré si API Radio France down
## Prochaines étapes
1. Tester le cache de métadonnées (TTL, refresh, graceful degradation)
2. Tester le système d'événements
3. Tester le browse sur les 3 niveaux
4. Vérifier les événements GENA
5. Vérifier que les covers sont correctement cachées
## Plan d'implémentation détaillé
Le plan détaillé est disponible dans:
`/Users/coissac/.claude/plans/glowing-scribbling-cook.md`

View File

@@ -20,3 +20,154 @@ La source Radio France doit donc s'abonner aux événements du Cache. A chaque f
Maintenant, il y a le cache des stations. Le cache des stations finalement il ne stock qu'un emboîtement de listes de slug. Ça, normalement, ça ne bouge quasiment pas. On peut dire que une fois par jour, on met à jour ce cache. Les listes de slug ont donc un TTL mais très long.
A chaque browse, on reconstruit un document didl à partir des métadonnées à jour provenant du cache.
## Round 2
Je repasse sur ton code. Tout est beaucoup beaucoup trop compliqué, trop de structures allambiquées, de trucs qui s'emboîtent dans des trucs. Il faut faire simple. Le mot d'ordre est simple. Nous ne construisons pas une usine à gaz, nous construisons simplement un truc capable de diffuser moins d'une centaine de radios.
### Simplification de la notion de station.
Alors, tu fais une distinction entre radio locale et web radio, c'est une distinction sémantique, mais d'un point de vue informatique y'a pas de différence.
L'unité de base, ça devrait être:
pub struct StationGroup {
pub stations: Vec<Station>,
}
La seule règle metier sémantique est: L'index 0 du vecteur est attribué à la station principale du groupe, par exemple FIP, pour le groupe FIP, si elle existe.
Et du coup, les StationGroups devrait juste être un vecteur de StationGroup
- StationGroups définie le niveau zéro du browse
- StationGroup définit les différents niveaux 1
Chaque station étant représentée maintenant par une playlist à un item item, Il y a un niveau 2 de browsing qui correspond à l'item de la station.
Donc, Station, StationGroup et StationGroups devrait chacun fournir une méthode retournant un objet PMODidl qui se construit en demandant les métadonnées au cache. Genre:
async pub fn to_didl(caches et server_base_url)
## Simplification du cache
Il faut réfléchir, Finalement, qu'est-ce que l'on a besoin de stocker dans le cache pour être efficace? De quoi remplir les Didl. Donc, à partir des données parsées depuis l'API Radio France, il faut reconstruire une structure simplifiée. contenant juste les données telles qu'on va les utiliser dans le diddle. Idéalement, le cache devrait être capable de fournir le bien d'idoles d'un item. Avec une méthode to_didl(slug) -> Un item de la Crate pmodidl. Tout le reste est superflu. Donc ne doit pas être stocké. Pour calculer la durée correctement, Il nous faut la fin de validité de l'item. Il est donc important de stocker end_time. Normalement, end time est aussi le TTL. Car à la fin de la diffusion de cet item, ça veut dire qu'il faut remettre à jour les métadata, Pour avoir l'item suivant.
## Round 3
### Problèmes identifiés
Point 3 : Le cache des slugs doit être persistant et stocké dans la config comme actuellement. Avec un délai d'une semaine. Le cache des métadonnées reste en mémoire. Les métadonnées changent à chaque émission, il n'y a pas de raison de les stocker de manière persistante.
Du coup, le cache des métadonnées, Pour simplifier la vie des autres structures. devrait s'occuper de cacher les covers dans pmocovers et stocker le PK de l'image dans le cache pour pouvoir construire le didl de l'item.
En fait, le didl de l'item, Dans notre nouvelle strategie est déjà un didl d'une playlist à un item.
### Architecture cible simplifiée
#### 1. Structures de station (models.rs et playlist.rs)
Je ne comprends pas bien la distinction entre les deux:
A-t-on vraiment besoin des deux fonctions?
À quoi sert cette fonction to_container?
```
// Browse niveau 1: retourne les playlists (containers) pour chaque station du groupe
pub async fn to_didl(&self, metadata_cache: &MetadataCache, server_base_url: &str) -> Vec<Container>;
// Helper pour construire le container de groupe (sans items, juste la structure)
pub fn to_container(&self, server_base_url: Option<&str>) -> Container;
```
```
impl CachedMetadata {
// Parse depuis LiveResponse + Station + optionnel cover cache
#[cfg(feature = "cache")]
pub async fn from_live_response(
station: &Station,
live: &LiveResponse,
cover_cache: Option<&Arc<CoverCache>>,
server_base_url: Option<&str>,
) -> Result<Self>;
pub fn from_live_response_sync(
station: &Station,
live: &LiveResponse,
server_base_url: Option<&str>,
) -> Result<Self>;
```
C'est quoi exactement la fonction from_live_response_sync ?
J'ai l'impression que tu surcompliques encore.
```
pub fn to_didl_item(&self, parent_id: &str) -> Item
```
Vu ma remarque précédente:
En fait, le didl de l'item, Dans notre nouvelle strategie est déjà un didl d'une playlist à un item.
Cette fonction devrait juste être un toDiddle et retourner le diddle d'une playlist à un item avec exactement les mêmes métadonnées pour la playlist conteneur et l'item à l'intérieur.
### 3. Cache de stations (intégré dans stateful_client.rs)
Comme je le disais plus haut, ce cache doit être permanent via l'usage de la configuration. Comme c'est le cas actuellement.
## Hiérarchie de browse
**Niveau 1**: Browse d'un groupe
En fait, si un station groupe ne contient qu'un seul item, C'est à dire qu'il n'y a pas plusieurs sous-radios sous ce groupe. on peut directement retourner la playlist simple qui contient simplement cet item.
Questions pour validation
1. **Organisation des stations sans webradios**: Faut-il créer un groupe pour chaque station standalone (France Culture, France Inter, etc.) ou les mettre toutes dans un seul groupe "Stations principales"?
- En fait, si un station groupe ne contient qu'un seul item, C'est à dire qu'il n'y a pas plusieurs sous-radios sous ce groupe. on peut directement retourner la playlist simple qui contient simplement cet item. Sinon, on retourne un container qui contient les playlists de chacun des items. Cela peut directement être implémenté dans le code de la fonction to Didl du groupe de station.
2. **Cache de métadonnées**: In-memory uniquement (données volatiles avec TTL court)?
- Oui, in-memory seulement, TTL basé sur la fin de diffusion de cet item.
3. **Cache de stations**: Rester dans pmoconfig avec TTL 1 jour?
- Oui, garder le système actuel, Il me semble que le TTL est d'une semaine actuellement, mais le garder tel qu'il est.
4. **Gestion d'erreur API Radio France down**: Retourner les données expirées avec warning?
- **Proposition**: Oui, graceful degradation
C'est parfait.
5. **Migration du code existant**: Faut-il maintenir une compatibilité temporaire ou refactoring complet immédiat?
- **Proposition**: Refactoring complet, c'est une simplification profonde
C'est parfait.
## Round 4
### 2. Groupes de stations (playlist.rs)
```
impl StationGroups {
// Browse niveau 0: retourne les containers de groupes
pub async fn to_didl(&self, metadata_cache: &MetadataCache, server_base_url: &str) -> Vec<Container>;
}
```
Pourquoi retourner un vecteur de conteneurs et pas un conteneur qui contient des conteneurs? Ça doit retourner une structure didl La fonction s'appelle to_didl.
Il faut être cohérent. Et **SIMPLE**.
### 3. Cache de métadonnées (NOUVEAU: metadata_cache.rs)
Il y a actuellement dans le code des règles pour passer des métadonnées Radio France vers des métadonnées UPNP, qui agrège les métadonnées selon certaines règles depuis Radio France pour en faire des métadonnées plus simples mais avec une sémantique correcte pour l'interface utilisateur du côté UPNP. Il ne faut pas abandonner ces règles.
### Hiérarchie de browse
**Niveau 0**: `radiofrance` → containers de groupes
- "France Culture" (id: `radiofrance:franceculture`) - playlist directe si groupe à 1 station
- "FIP" (id: `radiofrance:group:fip`) - container de groupe si plusieurs stations
- "Radios ICI" (id: `radiofrance:ici`) - container de groupe pour les radios locales --> Je te rappelle qu'il n'y a plus de distinction entre radio locale et autres radios. Ça c'était avant. Donc ICI fonctionne exactement comme FIP.
### Étapes d'implémentation
#### Étape 1: Créer metadata_cache.rs
1. Définir `CachedMetadata` struct avec tous les champs DIDL
On est d'accord que si tu définis ce type là, ça veut dire que tu supprimes le client Stateful. Sans ça, c'est complètement redondant.
A la fin de cette tâche, tu généreras le nouveau plan dans le fichier de rapport tel que c'est demandé par le fichier de règles [@Rules.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Rules.md) que tu devras suivre scrupuleusement.