Amélioration du chargement des métadonnées et filtrage de francebleu

### Fichiers modifiés :
- `pmoradiofrance/src/playlist.rs`
- `pmoradiofrance/src/source.rs`
- `pmoradiofrance/src/client.rs`

### Changements structurels :

6. **Chargement parallèle des métadonnées** :
   - Utilisation de `futures::stream::buffer_unordered(5)` pour charger jusqu'à 5 stations en parallèle
   - Décalage de 50ms entre chaque requête pour éviter de surcharger l'API Radio France
   - Amélioration significative du temps de chargement des groupes multi-stations

7. **Filtrage de francebleu générique** :
   - Application de la règle métier : `francebleu` (sans suffixe) n'est pas une vraie station
   - Filtrage dans `discover_all_stations()` pour éviter qu'elle soit ajoutée à la liste
   - Utilisation de `group.slug()` au lieu de `stations[0].slug` dans `source.rs` pour gérer correctement le groupe ICI

### Résultat :
- **Chargement plus rapide** : les métadonnées sont récupérées en parallèle au lieu de séquentiellement
- Code plus simple et sans logique spéciale dispersée
- Interface prévisible : les stations sont toujours au même endroit
This commit is contained in:
2026-01-24 20:59:33 +01:00
parent 7d55b5c3f0
commit 3817fda38e
4 changed files with 81 additions and 22 deletions

View File

@@ -12,8 +12,10 @@ Refactoring complet pour supprimer la notion de "station virtuelle" et utiliser
### Modifications ### Modifications
**Fichier modifié :** **Fichiers modifiés :**
- `pmoradiofrance/src/playlist.rs` - `pmoradiofrance/src/playlist.rs`
- `pmoradiofrance/src/source.rs`
- `pmoradiofrance/src/client.rs`
### Changements structurels ### Changements structurels
@@ -39,12 +41,23 @@ Refactoring complet pour supprimer la notion de "station virtuelle" et utiliser
- Groupe ICI : toutes les stations triées par nom (pas de station principale) - Groupe ICI : toutes les stations triées par nom (pas de station principale)
- Les stations se retrouvent toujours à la même position dans la liste - Les stations se retrouvent toujours à la même position dans la liste
6. **Chargement parallèle des métadonnées** :
- Utilisation de `futures::stream::buffer_unordered(5)` pour charger jusqu'à 5 stations en parallèle
- Décalage de 50ms entre chaque requête pour éviter de surcharger l'API Radio France
- Amélioration significative du temps de chargement des groupes multi-stations
7. **Filtrage de francebleu générique** :
- Application de la règle métier : `francebleu` (sans suffixe) n'est pas une vraie station
- Filtrage dans `discover_all_stations()` pour éviter qu'elle soit ajoutée à la liste
- Utilisation de `group.slug()` au lieu de `stations[0].slug` dans `source.rs` pour gérer correctement le groupe ICI
### Résultat ### Résultat
- **Groupes avec radio principale** (FIP, France Inter, etc.) : la radio principale apparaît à l'index 0, suivie des webradios triées alphabétiquement - **Groupes avec radio principale** (FIP, France Inter, etc.) : la radio principale apparaît à l'index 0, suivie des webradios triées alphabétiquement
- **Groupe ICI** : accessible et affiche toutes les radios locales triées alphabétiquement - **Groupe ICI** : accessible et affiche toutes les radios locales triées alphabétiquement
- Code plus simple et sans logique spéciale dispersée - Code plus simple et sans logique spéciale dispersée
- Interface prévisible : les stations sont toujours au même endroit - Interface prévisible : les stations sont toujours au même endroit
- **Chargement plus rapide** : les métadonnées sont récupérées en parallèle au lieu de séquentiellement
## Statut ## Statut

View File

@@ -134,13 +134,13 @@ impl RadioFranceClient {
// Note: Skip francebleu because its "webradios" are actually local radios // Note: Skip francebleu because its "webradios" are actually local radios
// which we get from the API with proper "ICI" names // which we get from the API with proper "ICI" names
for main_station in &main_stations { for main_station in &main_stations {
stations.push(main_station.clone()); // RÈGLE MÉTIER: Filtrer francebleu (portail générique, pas une vraie station)
// Skip francebleu - its local radios are discovered via API below
if main_station.slug == "francebleu" { if main_station.slug == "francebleu" {
continue; continue;
} }
stations.push(main_station.clone());
// Try to discover webradios (may return empty for some stations) // Try to discover webradios (may return empty for some stations)
if let Ok(webradios) = self.discover_station_webradios(&main_station.slug).await { if let Ok(webradios) = self.discover_station_webradios(&main_station.slug).await {
stations.extend(webradios); stations.extend(webradios);
@@ -434,10 +434,10 @@ impl RadioFranceClient {
return ("mouv", Some(slug)); return ("mouv", Some(slug));
} }
// France Bleu local radios use their slug directly, no webradio param // France Bleu local radios are webradios of francebleu
// e.g., francebleu_alsace → francebleu_alsace/api/live // e.g., francebleu_alsace → francebleu/api/live?webradio=francebleu-alsace
if slug.starts_with("francebleu_") { if slug.starts_with("francebleu_") {
return (slug, None); return ("francebleu", Some(slug));
} }
// Main stations // Main stations

View File

@@ -150,7 +150,7 @@ impl StationGroup {
} }
/// Retourne le slug du groupe (personnalisé ou slug de la première station) /// Retourne le slug du groupe (personnalisé ou slug de la première station)
fn slug(&self) -> &str { pub fn slug(&self) -> &str {
self.group_slug.as_deref().unwrap_or(&self.stations[0].slug) self.group_slug.as_deref().unwrap_or(&self.stations[0].slug)
} }
@@ -207,13 +207,33 @@ impl StationGroup {
.await .await
} else { } else {
// Groupe multi-stations : retourner un container avec toutes les playlists en stub // Groupe multi-stations : retourner un container avec toutes les playlists en stub
let mut containers = Vec::new(); // Chargement en parallèle avec décalage pour éviter de surcharger l'API
use futures::stream::{self, StreamExt};
for station in &self.stations { let futures: Vec<_> = self
// Appeler to_stub() sur chaque station .stations
let playlist_stub = station.to_stub(metadata_cache, server_base_url).await?; .iter()
containers.push(playlist_stub); .enumerate()
} .map(|(i, station)| async move {
// Décalage de 50ms entre chaque requête
if i > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
station.to_stub(metadata_cache, server_base_url).await
})
.collect();
let results: Vec<Result<Container>> = stream::iter(futures)
.buffer_unordered(5) // Maximum 5 requêtes en parallèle
.collect()
.await;
// to_stub() ne devrait plus échouer (utilise des valeurs par défaut)
// mais on gère quand même les erreurs par sécurité
let containers: Vec<Container> = results
.into_iter()
.filter_map(|result| result.ok())
.collect();
let album_art = Some(format!( let album_art = Some(format!(
"{}/api/radiofrance/default-logo", "{}/api/radiofrance/default-logo",
@@ -244,17 +264,43 @@ impl StationGroup {
impl Station { impl Station {
/// Niveau 2: to_stub() retourne comment cette station apparaît dans la liste d'un groupe /// Niveau 2: to_stub() retourne comment cette station apparaît dans la liste d'un groupe
/// ///
/// Retourne un container de playlist vide avec métadonnées live /// Retourne un container de playlist vide avec métadonnées live ou par défaut
pub async fn to_stub( pub async fn to_stub(
&self, &self,
metadata_cache: &MetadataCache, metadata_cache: &MetadataCache,
_server_base_url: &str, server_base_url: &str,
) -> Result<Container> { ) -> Result<Container> {
let playlist_id = format!("radiofrance:{}", self.slug); let playlist_id = format!("radiofrance:{}", self.slug);
let parent_id = self.compute_parent_id(); let parent_id = self.compute_parent_id();
// Récupérer les métadonnées du cache // Essayer de récupérer les métadonnées du cache
let cached_metadata = metadata_cache.get(&self.slug).await?; let (title, artist, album_art) = match metadata_cache.get(&self.slug).await {
Ok(cached) => (
cached.title.clone(),
cached.artist.clone(),
cached.album_art.clone(),
),
Err(e) => {
// Graceful degradation: utiliser des métadonnées par défaut
#[cfg(feature = "logging")]
tracing::warn!(
"Failed to get metadata for {}, using defaults: {}",
self.slug,
e
);
let default_art = Some(format!(
"{}/api/radiofrance/default-logo",
server_base_url.trim_end_matches('/')
));
(
self.name.clone(),
Some("Radio France".to_string()),
default_art,
)
}
};
// Construire juste le container de playlist (sans l'item stream) // Construire juste le container de playlist (sans l'item stream)
Ok(Container { Ok(Container {
@@ -263,10 +309,10 @@ impl Station {
restricted: Some("1".to_string()), restricted: Some("1".to_string()),
child_count: Some("1".to_string()), child_count: Some("1".to_string()),
searchable: Some("0".to_string()), searchable: Some("0".to_string()),
title: cached_metadata.title.clone(), title,
class: "object.container.playlistContainer".to_string(), class: "object.container.playlistContainer".to_string(),
artist: cached_metadata.artist.clone(), artist,
album_art: cached_metadata.album_art.clone(), album_art,
containers: vec![], containers: vec![],
items: vec![], items: vec![],
}) })

View File

@@ -268,7 +268,7 @@ impl MusicSource for RadioFranceSource {
let group = groups let group = groups
.groups .groups
.iter() .iter()
.find(|g| g.stations[0].slug == slug) .find(|g| g.slug() == slug)
.ok_or_else(|| MusicSourceError::ObjectNotFound(id.to_string()))?; .ok_or_else(|| MusicSourceError::ObjectNotFound(id.to_string()))?;
let container = group let container = group