Ajout d'une route pour le logo par défaut et amélioration de la gestion des covers

Ajout d'une nouvelle route /api/radiofrance/default-logo pour retourner le logo par défaut de Radio France.

Modification de la gestion des covers pour:
- Filtrer France Bleu des stations principales
- Prioriser les images de fond, puis les cartes et les joueurs
- Ajouter un fallback sur le logo par défaut via l'API REST
- Passer l'URL de base du serveur aux fonctions de gestion des covers

Correction de la construction du titre et des métadonnées (artist, album) pour éviter les duplications et améliorer la cohérence.
This commit is contained in:
2026-01-23 16:55:29 +01:00
parent 418b0b7cbe
commit aad1cc772f
3 changed files with 143 additions and 26 deletions

View File

@@ -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()
}

View File

@@ -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<Self> {
pub fn from_live_metadata_no_cache(
station: Station,
metadata: &LiveResponse,
server_base_url: Option<&str>,
) -> Result<Self> {
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<String>,
server_base_url: Option<&str>,
) -> Result<Item> {
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<String>, Option<String>) {
// Priorité : visual_background > song cover
fn extract_cover_url(
metadata: &LiveResponse,
server_base_url: Option<&str>,
) -> (Option<String>, Option<String>) {
// 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<CoverCache>,
server_base_url: Option<&str>,
) -> (Option<String>, Option<String>) {
// 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)
}
}
}
}

View File

@@ -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);