Implémentation du client stateful pour Radio France
Ajout de l'implémentation du client stateful `RadioFranceStatefulClient` avec gestion du cache des stations et des métadonnées live, ainsi que la construction de playlists UPnP. Intégration avec `SourceCacheManager` pour les covers. Ajout des structures `StationGroups`, `StationGroup` et `StationPlaylist` pour l'organisation hiérarchique des stations et le mapping API → UPnP.
This commit is contained in:
@@ -62,3 +62,439 @@ Sous le folder principal Radio France, On doit avoir une playlist par station. L
|
|||||||
Je pense que les playlists doivent être des playlists volatiles. Les covers doivent être cachés dans [@pmocovers](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/pmocovers) Par contre les URL doivent être passées telles qu'elles, C'est du pur stream, on ne va pas les cacher dans le PMOaudiocache.
|
Je pense que les playlists doivent être des playlists volatiles. Les covers doivent être cachés dans [@pmocovers](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/pmocovers) Par contre les URL doivent être passées telles qu'elles, C'est du pur stream, on ne va pas les cacher dans le PMOaudiocache.
|
||||||
|
|
||||||
étend le document de réflexion pour proposer une architecture: [@api_radiofrance_complete.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md)
|
étend le document de réflexion pour proposer une architecture: [@api_radiofrance_complete.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/ToThinkAbout/api_radiofrance_complete.md)
|
||||||
|
|
||||||
|
## Round 4bis : Implémentation du Client Stateful
|
||||||
|
|
||||||
|
**Crate concernée** : `pmoradiofrance`
|
||||||
|
|
||||||
|
**Objectif** : Implémenter le client stateful et les structures de support pour Radio France selon l'architecture définie dans le Round 4 de `api_radiofrance_complete.md`.
|
||||||
|
|
||||||
|
### Fichiers à créer/modifier
|
||||||
|
|
||||||
|
#### 1. `pmoradiofrance/src/stateful_client.rs` (NOUVEAU)
|
||||||
|
|
||||||
|
Implémenter le client stateful `RadioFranceStatefulClient` qui :
|
||||||
|
|
||||||
|
**Responsabilités** :
|
||||||
|
- Gère le cache des stations découvertes (via pmoconfig)
|
||||||
|
- Gère le cache mémoire des métadonnées live (HashMap avec TTL)
|
||||||
|
- Expose des méthodes de haut niveau pour la découverte et l'organisation des stations
|
||||||
|
- Construit des playlists UPnP à partir des métadonnées live
|
||||||
|
- Intègre avec `SourceCacheManager` pour les covers
|
||||||
|
|
||||||
|
**Structure principale** :
|
||||||
|
```rust
|
||||||
|
pub struct RadioFranceStatefulClient {
|
||||||
|
client: RadioFranceClient,
|
||||||
|
config: Arc<Config>,
|
||||||
|
metadata_cache: Arc<RwLock<HashMap<String, LiveMetadataCache>>>,
|
||||||
|
cache_manager: SourceCacheManager,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Méthodes à implémenter** :
|
||||||
|
|
||||||
|
**Discovery avec cache** :
|
||||||
|
- `new()` - Créer depuis config par défaut
|
||||||
|
- `with_client_and_config()` - Créer avec client et config personnalisés
|
||||||
|
- `get_all_stations()` - Retourne toutes les stations (cache si valide, sinon découverte)
|
||||||
|
- `refresh_stations()` - Force la redécouverte (ignore cache)
|
||||||
|
- `get_main_stations()` - Filtre les stations principales
|
||||||
|
- `get_webradios(parent)` - Filtre les webradios d'une station parent
|
||||||
|
- `get_local_radios()` - Filtre les radios locales ICI
|
||||||
|
|
||||||
|
**Organisation hiérarchique** :
|
||||||
|
- `get_stations_by_group()` - Retourne `StationGroups` avec regroupement logique
|
||||||
|
- `standalone` : Stations sans webradios (France Culture, France Inter, France Info)
|
||||||
|
- `with_webradios` : Groupes FIP et France Musique avec leurs webradios
|
||||||
|
- `local_radios` : Toutes les radios ICI (France Bleu)
|
||||||
|
|
||||||
|
**Métadonnées live** :
|
||||||
|
- `get_live_metadata(station)` - Retourne métadonnées (cache court terme si valide)
|
||||||
|
- `refresh_live_metadata(station)` - Force le rafraîchissement (ignore cache)
|
||||||
|
|
||||||
|
**Construction de playlists** :
|
||||||
|
- `build_station_playlist(station)` - Construit une playlist UPnP complète pour une station
|
||||||
|
- `update_playlist_metadata(station, playlist)` - Met à jour les métadonnées volatiles
|
||||||
|
- `get_stream_url(station)` - Retourne l'URL du stream HiFi
|
||||||
|
|
||||||
|
**Helpers** :
|
||||||
|
- `is_station_cache_valid()` - Vérifie validité du cache des stations
|
||||||
|
- `station_cache_age_secs()` - Retourne l'âge du cache en secondes
|
||||||
|
|
||||||
|
**Implémentation du cache mémoire** :
|
||||||
|
```rust
|
||||||
|
struct LiveMetadataCache {
|
||||||
|
metadata: LiveResponse,
|
||||||
|
fetched_at: SystemTime,
|
||||||
|
valid_until: SystemTime,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logique de cache des métadonnées live** :
|
||||||
|
1. Vérifier HashMap en mémoire
|
||||||
|
2. Si présent et `SystemTime::now() < valid_until` : retourner cache
|
||||||
|
3. Sinon : appeler `client.live_metadata()`, calculer `valid_until` depuis `delayToRefresh`, stocker, retourner
|
||||||
|
|
||||||
|
#### 2. `pmoradiofrance/src/playlist.rs` (NOUVEAU)
|
||||||
|
|
||||||
|
Structures et helpers pour la construction de playlists UPnP.
|
||||||
|
|
||||||
|
**Important** : Utiliser les structures `Item` et `Resource` de `pmodidl` pour représenter les playlists UPnP.
|
||||||
|
|
||||||
|
**Structures à définir** :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use pmodidl::{Item, Resource};
|
||||||
|
|
||||||
|
/// Groupes de stations organisés hiérarchiquement
|
||||||
|
pub struct StationGroups {
|
||||||
|
pub standalone: Vec<Station>,
|
||||||
|
pub with_webradios: Vec<StationGroup>,
|
||||||
|
pub local_radios: Vec<Station>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Groupe station principale + webradios
|
||||||
|
pub struct StationGroup {
|
||||||
|
pub main: Station,
|
||||||
|
pub webradios: Vec<Station>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Playlist UPnP pour une station (volatiles)
|
||||||
|
///
|
||||||
|
/// Contient UN SEUL item pmodidl::Item représentant le stream.
|
||||||
|
/// Les métadonnées de l'item changent au fil du temps (émissions, morceaux)
|
||||||
|
/// mais l'URL du stream reste identique.
|
||||||
|
pub struct StationPlaylist {
|
||||||
|
/// ID de la playlist (ex: "radiofrance:franceculture")
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// Station source
|
||||||
|
pub station: Station,
|
||||||
|
|
||||||
|
/// Item UPnP unique représentant le stream
|
||||||
|
/// Utilise pmodidl::Item avec toutes les métadonnées volatiles
|
||||||
|
pub stream_item: Item,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helpers à implémenter** :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl StationPlaylist {
|
||||||
|
/// Construire une playlist depuis les métadonnées live
|
||||||
|
///
|
||||||
|
/// Crée un pmodidl::Item avec :
|
||||||
|
/// - id : "radiofrance:{station_slug}:stream"
|
||||||
|
/// - parent_id : "radiofrance:{station_slug}"
|
||||||
|
/// - title, artist, album, genre selon le mapping API → UPnP
|
||||||
|
/// - resource unique avec l'URL du stream HiFi
|
||||||
|
/// - album_art pointant vers la cover cachée
|
||||||
|
pub async fn from_live_metadata(
|
||||||
|
station: Station,
|
||||||
|
metadata: LiveResponse,
|
||||||
|
cache_manager: &SourceCacheManager,
|
||||||
|
) -> Result<Self>;
|
||||||
|
|
||||||
|
/// Mettre à jour les métadonnées volatiles de l'item
|
||||||
|
///
|
||||||
|
/// Met à jour uniquement les champs volatiles :
|
||||||
|
/// - title, artist, album (depuis nouvelles métadonnées)
|
||||||
|
/// - album_art / album_art_pk (si nouvelle cover)
|
||||||
|
///
|
||||||
|
/// L'URL du stream (resource.url) ne change JAMAIS.
|
||||||
|
pub async fn update_metadata(
|
||||||
|
&mut self,
|
||||||
|
metadata: &LiveResponse,
|
||||||
|
cache_manager: &SourceCacheManager,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Construire un pmodidl::Item depuis les métadonnées live
|
||||||
|
async fn build_item_from_metadata(
|
||||||
|
station: &Station,
|
||||||
|
metadata: &LiveResponse,
|
||||||
|
cache_manager: &SourceCacheManager,
|
||||||
|
) -> Result<Item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StationGroups {
|
||||||
|
/// Organiser une liste de stations en groupes
|
||||||
|
pub fn from_stations(stations: Vec<Station>) -> Self;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping API → UPnP (avec pmodidl::Item)** :
|
||||||
|
|
||||||
|
**Construction du pmodidl::Item** :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use pmodidl::{Item, Resource};
|
||||||
|
|
||||||
|
// Champs communs à tous les items
|
||||||
|
let item = Item {
|
||||||
|
id: format!("radiofrance:{}:stream", station.slug),
|
||||||
|
parent_id: format!("radiofrance:{}", station.slug),
|
||||||
|
restricted: Some("1".to_string()),
|
||||||
|
class: "object.item.audioItem.audioBroadcast".to_string(),
|
||||||
|
|
||||||
|
// Métadonnées volatiles (varient selon radio parlée/musicale)
|
||||||
|
title: ...,
|
||||||
|
creator: ...,
|
||||||
|
artist: ...,
|
||||||
|
album: ...,
|
||||||
|
genre: ...,
|
||||||
|
|
||||||
|
// Cover
|
||||||
|
album_art: Some(cover_url),
|
||||||
|
album_art_pk: Some(cover_pk),
|
||||||
|
|
||||||
|
// Resource unique (stream)
|
||||||
|
resources: vec![Resource {
|
||||||
|
protocol_info: "http-get:*:audio/aac:*".to_string(),
|
||||||
|
bits_per_sample: None,
|
||||||
|
sample_frequency: Some("48000".to_string()),
|
||||||
|
nr_audio_channels: Some("2".to_string()),
|
||||||
|
duration: None, // Pas de durée pour un stream live
|
||||||
|
url: stream_url,
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Pas de descriptions pour les streams Radio France
|
||||||
|
descriptions: vec![],
|
||||||
|
|
||||||
|
// Autres champs optionnels
|
||||||
|
date: None,
|
||||||
|
original_track_number: None,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour **radios parlées** (France Culture, France Inter, France Info) :
|
||||||
|
- `title` = `now.firstLine.title` + " • " + `now.secondLine.title`
|
||||||
|
- `creator` = `now.producer`
|
||||||
|
- `artist` = `now.producer`
|
||||||
|
- `album` = `now.firstLine.title` (nom de l'émission)
|
||||||
|
- `genre` = Some("Talk Radio")
|
||||||
|
- `class` = "object.item.audioItem.audioBroadcast"
|
||||||
|
|
||||||
|
Pour **radios musicales** (FIP, France Musique) :
|
||||||
|
- Si `now.song` présent :
|
||||||
|
- `title` = `now.firstLine.title` (titre du morceau)
|
||||||
|
- `creator` = `now.song.interpreters.join(", ")`
|
||||||
|
- `artist` = `now.song.interpreters.join(", ")`
|
||||||
|
- `album` = `now.song.release.title`
|
||||||
|
- `genre` = Some("Music")
|
||||||
|
- `class` = "object.item.audioItem.musicTrack"
|
||||||
|
- Sinon (talk segment) : utiliser mapping radio parlée
|
||||||
|
|
||||||
|
**Cache des covers** :
|
||||||
|
- Extraire UUID depuis `now.visualBackground.src` ou image du `now.song`
|
||||||
|
- Construire URL haute résolution : `ImageSize::XLarge` ou `ImageSize::Large`
|
||||||
|
- Cacher via `cache_manager.cache_cover(url)`
|
||||||
|
- Stocker PK : `RADIOFRANCE:{uuid}`
|
||||||
|
- Dans l'item :
|
||||||
|
- `album_art` = URL publique de la cover (via serveur HTTP)
|
||||||
|
- `album_art_pk` = PK du cache (pour retrouver le fichier)
|
||||||
|
|
||||||
|
**Resource (URL du stream)** :
|
||||||
|
- Priorité : AAC 192 kbps > HLS > AAC 128 kbps > MP3 128 kbps
|
||||||
|
- Utiliser `metadata.now.media.best_hifi_stream()`
|
||||||
|
- `protocol_info` :
|
||||||
|
- AAC : `"http-get:*:audio/aac:*"`
|
||||||
|
- HLS : `"http-get:*:application/vnd.apple.mpegurl:*"`
|
||||||
|
- MP3 : `"http-get:*:audio/mpeg:*"`
|
||||||
|
- `sample_frequency` = `"48000"` pour AAC, None pour HLS
|
||||||
|
- `nr_audio_channels` = `"2"` pour AAC, None pour HLS
|
||||||
|
- `duration` = None (stream infini)
|
||||||
|
- `url` = URL complète du stream
|
||||||
|
|
||||||
|
#### 3. `pmoradiofrance/src/config_ext.rs` (MODIFIER/COMPLÉTER)
|
||||||
|
|
||||||
|
Étendre le trait `RadioFranceConfigExt` avec toutes les méthodes nécessaires.
|
||||||
|
|
||||||
|
**Constantes** :
|
||||||
|
```rust
|
||||||
|
const DEFAULT_RADIOFRANCE_BASE_URL: &str = "https://www.radiofrance.fr";
|
||||||
|
const DEFAULT_RADIOFRANCE_TIMEOUT_SECS: u64 = 30;
|
||||||
|
const DEFAULT_RADIOFRANCE_CACHE_TTL_DAYS: u64 = 7;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trait complet** :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait RadioFranceConfigExt {
|
||||||
|
// Activation
|
||||||
|
fn get_radiofrance_enabled(&self) -> Result<bool>;
|
||||||
|
fn set_radiofrance_enabled(&self, enabled: bool) -> Result<()>;
|
||||||
|
|
||||||
|
// Configuration 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<()>;
|
||||||
|
|
||||||
|
// 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<()>;
|
||||||
|
|
||||||
|
// Factory method
|
||||||
|
fn create_radiofrance_client(&self) -> Result<RadioFranceStatefulClient>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
|
||||||
|
Chemins dans la config :
|
||||||
|
- `["sources", "radiofrance", "enabled"]` - bool
|
||||||
|
- `["sources", "radiofrance", "base_url"]` - string
|
||||||
|
- `["sources", "radiofrance", "timeout_secs"]` - u64
|
||||||
|
- `["sources", "radiofrance", "cache_ttl_days"]` - u64
|
||||||
|
- `["sources", "radiofrance", "stations_cache"]` - serde_yaml::Value (structure `CachedStationList`)
|
||||||
|
|
||||||
|
**Auto-persistence** :
|
||||||
|
- `get_radiofrance_enabled()` : Persister `true` par défaut si absent
|
||||||
|
- `get_radiofrance_base_url()` : Persister `DEFAULT_RADIOFRANCE_BASE_URL` si absent
|
||||||
|
- `get_radiofrance_timeout_secs()` : Persister `DEFAULT_RADIOFRANCE_TIMEOUT_SECS` si absent
|
||||||
|
- `get_radiofrance_cache_ttl_days()` : Persister `DEFAULT_RADIOFRANCE_CACHE_TTL_DAYS` si absent
|
||||||
|
|
||||||
|
**Factory method** :
|
||||||
|
```rust
|
||||||
|
fn create_radiofrance_client(&self) -> Result<RadioFranceStatefulClient> {
|
||||||
|
let base_url = self.get_radiofrance_base_url()?;
|
||||||
|
let timeout_secs = self.get_radiofrance_timeout_secs()?;
|
||||||
|
|
||||||
|
let http_client = RadioFranceClient::builder()
|
||||||
|
.base_url(base_url)
|
||||||
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let config = get_config();
|
||||||
|
let cache_manager = SourceCacheManager::from_registry("radiofrance".to_string())?;
|
||||||
|
|
||||||
|
Ok(RadioFranceStatefulClient::with_client_and_config(
|
||||||
|
http_client,
|
||||||
|
config,
|
||||||
|
cache_manager,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. `pmoradiofrance/src/models.rs` (MODIFIER)
|
||||||
|
|
||||||
|
Ajouter la structure `CachedStationList` si pas déjà présente (elle est définie dans le Round 3) :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CachedStationList {
|
||||||
|
pub stations: Vec<Station>,
|
||||||
|
pub last_updated: u64,
|
||||||
|
pub version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CachedStationList {
|
||||||
|
pub const CURRENT_VERSION: u32 = 1;
|
||||||
|
pub const DEFAULT_TTL_SECS: u64 = 7 * 24 * 3600;
|
||||||
|
|
||||||
|
pub fn new(stations: Vec<Station>) -> Self;
|
||||||
|
pub fn is_valid(&self, ttl_secs: u64) -> bool;
|
||||||
|
pub fn is_valid_default(&self) -> bool;
|
||||||
|
pub fn age_secs(&self) -> u64;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. `pmoradiofrance/src/lib.rs` (MODIFIER)
|
||||||
|
|
||||||
|
Ajouter les exports :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub mod stateful_client;
|
||||||
|
pub mod playlist;
|
||||||
|
|
||||||
|
pub use stateful_client::RadioFranceStatefulClient;
|
||||||
|
pub use playlist::{StationGroups, StationGroup, StationPlaylist};
|
||||||
|
|
||||||
|
// Ré-exporter pmodidl::Item pour faciliter l'utilisation
|
||||||
|
pub use pmodidl::Item as RadioItem;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. `pmoradiofrance/Cargo.toml` (MODIFIER)
|
||||||
|
|
||||||
|
Vérifier que les dépendances sont présentes :
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
# ... dépendances existantes du Round 3 ...
|
||||||
|
|
||||||
|
# Nouvelles pour Round 4
|
||||||
|
pmoconfig = { path = "../pmoconfig" }
|
||||||
|
pmocovers = { path = "../pmocovers" }
|
||||||
|
pmosource = { path = "../pmosource" } # Pour SourceCacheManager
|
||||||
|
pmodidl = { path = "../pmodidl" } # Pour Item et Resource
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["pmoconfig"]
|
||||||
|
pmoconfig = ["dep:pmoconfig"]
|
||||||
|
cache = ["dep:pmocovers", "pmosource/server"]
|
||||||
|
server = ["pmoconfig", "cache"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Règles métier importantes
|
||||||
|
|
||||||
|
1. **Renommage France Bleu → ICI** :
|
||||||
|
- Slug conservé : `francebleu`, `francebleu_alsace`, etc.
|
||||||
|
- Label affiché : "ICI", "ICI Alsace", etc.
|
||||||
|
- Implémenter dans `Station::display_name()` ou helper similaire
|
||||||
|
|
||||||
|
2. **Organisation des stations** :
|
||||||
|
- Standalone : France Culture, France Inter, France Info, Mouv'
|
||||||
|
- Avec webradios : FIP (9 webradios), France Musique (5+ webradios)
|
||||||
|
- Locales : ~40 radios ICI (France Bleu)
|
||||||
|
|
||||||
|
3. **Playlists volatiles** :
|
||||||
|
- Un seul item par playlist (le stream)
|
||||||
|
- URL du stream ne change JAMAIS
|
||||||
|
- Métadonnées changent toutes les 2-5 minutes
|
||||||
|
- Cover cachée dans `pmocovers`, PAS dans `pmoaudiocache`
|
||||||
|
|
||||||
|
4. **Stream HiFi** :
|
||||||
|
- Toujours présenter le meilleur stream disponible
|
||||||
|
- Ordre : AAC 192kbps > HLS > AAC 128kbps > MP3 128kbps
|
||||||
|
|
||||||
|
5. **Cache des stations** :
|
||||||
|
- TTL : 7 jours par défaut (configurable)
|
||||||
|
- Stockage : YAML dans pmoconfig
|
||||||
|
- Versionnement : Invalider si version change
|
||||||
|
|
||||||
|
6. **Cache des métadonnées live** :
|
||||||
|
- TTL : Dynamique (champ `delayToRefresh` de l'API)
|
||||||
|
- Stockage : Mémoire (HashMap)
|
||||||
|
- Par station
|
||||||
|
|
||||||
|
### Tests à ajouter
|
||||||
|
|
||||||
|
**Tests unitaires** (`stateful_client.rs`) :
|
||||||
|
- Validation du cache (TTL, version)
|
||||||
|
- Organisation des stations en groupes
|
||||||
|
- Mapping métadonnées API → UPnP
|
||||||
|
|
||||||
|
**Tests d'intégration** (avec `#[ignore]`) :
|
||||||
|
- Découverte et cache des stations
|
||||||
|
- Récupération métadonnées live avec cache
|
||||||
|
- Construction de playlists complètes
|
||||||
|
- Mise à jour métadonnées volatiles
|
||||||
|
|
||||||
|
### Résultat attendu
|
||||||
|
|
||||||
|
À la fin du Round 4bis, on doit avoir :
|
||||||
|
|
||||||
|
1. ✅ Client stateful fonctionnel avec cache intelligent
|
||||||
|
2. ✅ Structures de playlists UPnP complètes
|
||||||
|
3. ✅ Extension pmoconfig complète
|
||||||
|
4. ✅ Organisation hiérarchique des stations
|
||||||
|
5. ✅ Mapping API → UPnP pour radios parlées et musicales
|
||||||
|
6. ✅ Cache des covers via pmocovers
|
||||||
|
7. ✅ Tests unitaires et d'intégration
|
||||||
|
|
||||||
|
Le Round 5 pourra alors implémenter la `MusicSource` qui utilisera ce client stateful.
|
||||||
|
|||||||
2559
metadata.txt
Normal file
2559
metadata.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user