diff --git a/pmoradiofrance/src/api_rest.rs b/pmoradiofrance/src/api_rest.rs index 7507c7e4..95f6eefa 100644 --- a/pmoradiofrance/src/api_rest.rs +++ b/pmoradiofrance/src/api_rest.rs @@ -50,6 +50,7 @@ pub fn create_router(state: RadioFranceState) -> Router { .route("/stations", get(get_stations)) .route("/{slug}/metadata", get(get_metadata)) .route("/{slug}/stream", get(proxy_stream)) + .route("/default-logo", get(get_default_logo)) .with_state(state) } @@ -124,3 +125,15 @@ async fn proxy_stream( Ok((headers, body).into_response()) } + +/// GET /api/radiofrance/default-logo +/// Returns the default Radio France logo (embedded in binary) +async fn get_default_logo() -> impl IntoResponse { + use crate::source::RADIOFRANCE_DEFAULT_IMAGE; + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "image/webp".parse().unwrap()); + headers.insert("Cache-Control", "public, max-age=86400".parse().unwrap()); + + (headers, RADIOFRANCE_DEFAULT_IMAGE).into_response() +} diff --git a/pmoradiofrance/src/playlist.rs b/pmoradiofrance/src/playlist.rs index 42f273fd..292c2100 100644 --- a/pmoradiofrance/src/playlist.rs +++ b/pmoradiofrance/src/playlist.rs @@ -87,7 +87,11 @@ impl StationGroups { for station in stations { match &station.station_type { StationType::Main => { - main_stations.insert(station.slug.clone(), station); + // Filtrer France Bleu : ce n'est pas une vraie radio mais le nom générique + // pour toutes les radios locales ICI (ex-France Bleu) + if station.slug != "francebleu" { + main_stations.insert(station.slug.clone(), station); + } } StationType::Webradio { parent_station } => { webradios_by_parent @@ -235,9 +239,13 @@ impl StationPlaylist { } /// Construit une playlist sans cache de covers - pub fn from_live_metadata_no_cache(station: Station, metadata: &LiveResponse) -> Result { + pub fn from_live_metadata_no_cache( + station: Station, + metadata: &LiveResponse, + server_base_url: Option<&str>, + ) -> Result { let id = format!("radiofrance:{}", station.slug); - let stream_item = Self::build_item_from_metadata_sync(&station, metadata, None)?; + let stream_item = Self::build_item_from_metadata_sync(&station, metadata, server_base_url)?; Ok(Self { id, @@ -285,7 +293,11 @@ impl StationPlaylist { } /// Met à jour les métadonnées sans cache - pub fn update_metadata_no_cache(&mut self, metadata: &LiveResponse) -> Result<()> { + pub fn update_metadata_no_cache( + &mut self, + metadata: &LiveResponse, + server_base_url: Option<&str>, + ) -> Result<()> { let old_url = self .stream_item .resources @@ -293,7 +305,8 @@ impl StationPlaylist { .map(|r| r.url.clone()) .unwrap_or_default(); - let mut new_item = Self::build_item_from_metadata_sync(&self.station, metadata, None)?; + let mut new_item = + Self::build_item_from_metadata_sync(&self.station, metadata, server_base_url)?; if let Some(res) = new_item.resources.first_mut() { if !old_url.is_empty() { @@ -320,7 +333,7 @@ impl StationPlaylist { let (album_art, album_art_pk) = if let Some(cache) = cover_cache { Self::cache_cover(metadata, cache, server_base_url).await } else { - Self::extract_cover_url(metadata) + Self::extract_cover_url(metadata, server_base_url) }; // Construction de la ressource (stream) @@ -349,12 +362,12 @@ impl StationPlaylist { fn build_item_from_metadata_sync( station: &Station, metadata: &LiveResponse, - _cover_url_override: Option, + server_base_url: Option<&str>, ) -> Result { let (title, creator, artist, album, genre, class) = Self::extract_metadata_fields(station, metadata); - let (album_art, album_art_pk) = Self::extract_cover_url(metadata); + let (album_art, album_art_pk) = Self::extract_cover_url(metadata, server_base_url); let resource = Self::build_stream_resource(metadata); Ok(Item { @@ -410,20 +423,32 @@ impl StationPlaylist { let first = now.first_line.title_or_default(); let second = now.second_line.title_or_default(); + // Construire le titre en évitant les duplications let title = if !first.is_empty() && !second.is_empty() { - format!("{} • {}", first, second) + // Si first contient déjà second, utiliser seulement first + if first.contains(second) { + first.to_string() + } else { + format!("{} • {}", first, second) + } } else if !first.is_empty() { first.to_string() } else { station.display_name().to_string() }; - let creator = now.producer.clone(); - let artist = now.producer.clone(); + // Artist/Creator = "{Station} - {Subtitle}" + let artist = if !second.is_empty() { + Some(format!("{} - {}", station.name, second)) + } else { + Some(station.name.clone()) + }; + let creator = artist.clone(); + // Album = nom de l'émission principale let album = if !first.is_empty() { Some(first.to_string()) } else { - None + Some(station.name.clone()) }; let genre = Some("Talk Radio".to_string()); let class = "object.item.audioItem.audioBroadcast".to_string(); @@ -433,8 +458,13 @@ impl StationPlaylist { } /// Extrait l'URL de cover depuis les métadonnées (sans cache) - fn extract_cover_url(metadata: &LiveResponse) -> (Option, Option) { - // Priorité : visual_background > song cover + fn extract_cover_url( + metadata: &LiveResponse, + server_base_url: Option<&str>, + ) -> (Option, Option) { + // Priorité : visual_background > visuals.card > visuals.player > logo par défaut + + // 1. visual_background if let Some(ref visual) = metadata.now.visual_background { if let Some(uuid) = visual.extract_uuid() { let url = ImageSize::Large.build_url(&uuid); @@ -442,7 +472,34 @@ impl StationPlaylist { } } - // Pas de cover trouvée + // 2. visuals.card + if let Some(ref visuals) = metadata.now.visuals { + if let Some(ref card) = visuals.card { + if let Some(uuid) = card.extract_uuid() { + let url = ImageSize::Large.build_url(&uuid); + return (Some(url), None); + } + } + + // 3. visuals.player + if let Some(ref player) = visuals.player { + if let Some(uuid) = player.extract_uuid() { + let url = ImageSize::Large.build_url(&uuid); + return (Some(url), None); + } + } + } + + // Fallback sur le logo par défaut via l'API REST + if let Some(base) = server_base_url { + let logo_url = format!( + "{}/api/radiofrance/default-logo", + base.trim_end_matches('/') + ); + return (Some(logo_url), None); + } + + // Pas de cover trouvée et pas de serveur configuré (None, None) } @@ -453,16 +510,35 @@ impl StationPlaylist { cache: &Arc, server_base_url: Option<&str>, ) -> (Option, Option) { - // Extraire l'UUID de la cover + // Extraire l'UUID de la cover (priorité : visual_background > visuals.card > visuals.player) let uuid = metadata .now .visual_background .as_ref() - .and_then(|v| v.extract_uuid()); + .and_then(|v| v.extract_uuid()) + .or_else(|| { + metadata.now.visuals.as_ref().and_then(|visuals| { + visuals + .card + .as_ref() + .and_then(|c| c.extract_uuid()) + .or_else(|| visuals.player.as_ref().and_then(|p| p.extract_uuid())) + }) + }); let uuid = match uuid { Some(u) => u, - None => return (None, None), + None => { + // Fallback sur le logo par défaut via l'API REST + if let Some(base) = server_base_url { + let logo_url = format!( + "{}/api/radiofrance/default-logo", + base.trim_end_matches('/') + ); + return (Some(logo_url), None); + } + return (None, None); + } }; // URL haute résolution @@ -479,7 +555,16 @@ impl StationPlaylist { } Err(e) => { tracing::warn!("Failed to cache Radio France cover: {}", e); - (Some(cover_url), None) + // Fallback sur le logo par défaut via l'API REST en cas d'erreur + if let Some(base) = server_base_url { + let logo_url = format!( + "{}/api/radiofrance/default-logo", + base.trim_end_matches('/') + ); + (Some(logo_url), None) + } else { + (Some(cover_url), None) + } } } } diff --git a/pmoradiofrance/src/source.rs b/pmoradiofrance/src/source.rs index 0024811b..78fefeaa 100644 --- a/pmoradiofrance/src/source.rs +++ b/pmoradiofrance/src/source.rs @@ -22,8 +22,8 @@ use pmocovers::Cache as CoverCache; #[cfg(feature = "server")] use pmoupnp; -/// Default image for Radio France source -const RADIOFRANCE_DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/radiofrance-logo.webp"); +/// Default image for Radio France source (embedded in binary) +pub const RADIOFRANCE_DEFAULT_IMAGE: &[u8] = include_bytes!("../assets/radiofrance-logo.webp"); /// Radio France music source /// @@ -153,7 +153,7 @@ impl RadioFranceSource { #[cfg(feature = "cache")] let cover_cache = self.cover_cache.clone(); - #[cfg(feature = "cache")] + let server_base_url = self.server_base_url.clone(); let handle = tokio::spawn(async move { @@ -185,7 +185,10 @@ impl RadioFranceSource { { let mut pls = playlists.write().await; if let Some(playlist) = pls.get_mut(&slug) { - let _: () = playlist.update_metadata_no_cache(&metadata); + let _: Result<()> = playlist.update_metadata_no_cache( + &metadata, + server_base_url.as_deref(), + ); // Update change tracking *update_id.write().await = update_id.read().await.wrapping_add(1); @@ -372,7 +375,11 @@ impl RadioFranceSource { .await?; #[cfg(not(feature = "cache"))] - let playlist = StationPlaylist::from_live_metadata_no_cache(station.clone(), &metadata)?; + let playlist = StationPlaylist::from_live_metadata_no_cache( + station.clone(), + &metadata, + self.server_base_url.as_deref(), + )?; // Cache it let mut playlists_write = self.playlists.write().await; @@ -382,6 +389,14 @@ impl RadioFranceSource { // Note: We don't start metadata refresh here to avoid blocking during browse. // Refresh will be started in resolve_uri() when the stream is actually played. + #[cfg(feature = "logging")] + tracing::debug!( + "Built item for {}: {} resources, album_art: {:?}", + station.slug, + playlist.stream_item.resources.len(), + playlist.stream_item.album_art.is_some() + ); + Ok(playlist.stream_item) } } @@ -568,8 +583,12 @@ impl MusicSource for RadioFranceSource { .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?; #[cfg(not(feature = "cache"))] - let playlist = StationPlaylist::from_live_metadata_no_cache(station.clone(), &metadata) - .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?; + let playlist = StationPlaylist::from_live_metadata_no_cache( + station.clone(), + &metadata, + self.server_base_url.as_deref(), + ) + .map_err(|e| MusicSourceError::BrowseError(e.to_string()))?; let mut playlists_write = self.playlists.write().await; playlists_write.insert(slug.to_string(), playlist);