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:
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user