push-pruvwtuysnnn #66
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user