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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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![],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user