push-pruvwtuysnnn #66

Merged
eric merged 13 commits from push-pruvwtuysnnn into main 2026-01-23 23:15:29 +01:00
3 changed files with 143 additions and 26 deletions
Showing only changes of commit aad1cc772f - Show all commits

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