diff --git a/Cargo.lock b/Cargo.lock index de470c04..f8f0516f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "PMOMusic" -version = "0.3.13" +version = "0.3.14" dependencies = [ "axum 0.8.7", "console-subscriber", diff --git a/Dockerfile b/Dockerfile index 706f4da9..6ef70136 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,7 @@ COPY pmoaudiocache/ ./pmoaudiocache/ COPY pmoaudio/ ./pmoaudio/ COPY pmoqobuz/ ./pmoqobuz/ COPY pmoparadise/ ./pmoparadise/ +COPY pmoradiofrance/ ./pmoradiofrance/ COPY pmosource/ ./pmosource/ COPY pmoplaylist/ ./pmoplaylist/ COPY pmoflac/ ./pmoflac/ diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index bf06997e..4ab7721d 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "PMOMusic" -version = "0.3.13" +version = "0.3.14" edition = "2024" [dependencies] diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index fe801cd8..36955bb5 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time::SystemTime; +use pmodidl::{DIDLLite, MediaMetadataParser}; use tracing::{debug, error}; use crate::errors::ControlPointError; @@ -755,8 +756,30 @@ impl MusicRenderer { /// Get playback position pub fn playback_position(&self) -> Result { - self.lock_backend_for("playback_position") - .playback_position() + let mut position_info = self + .lock_backend_for("playback_position") + .playback_position()?; + + // Si track_duration est absent ou invalide, essayer de le parser depuis le DIDL metadata + let needs_duration_fix = position_info + .track_duration + .as_ref() + .map(|d| d == "00:00:00" || d == "0:00:00") + .unwrap_or(true); // None = true + + if needs_duration_fix { + if let Some(ref metadata_xml) = position_info.track_metadata { + if let Some(duration) = parse_didl_duration(metadata_xml) { + tracing::debug!( + "MusicRenderer: Corrected track_duration from DIDL metadata: {}", + duration + ); + position_info.track_duration = Some(duration); + } + } + } + + Ok(position_info) } /// Sets the playlist binding for this renderer. @@ -1437,6 +1460,36 @@ impl RendererFromMediaRendererInfo for MusicRendererBackend { } } +/// Parse duration from DIDL-Lite metadata XML. +/// +/// Extracts the duration attribute from the element in DIDL metadata. +/// This is used as a fallback when the renderer doesn't provide track_duration +/// in GetPositionInfo or similar calls. +fn parse_didl_duration(didl_xml: &str) -> Option { + // Parse DIDL-Lite XML properly using pmodidl + let didl = match DIDLLite::parse(didl_xml) { + Ok(d) => d, + Err(e) => { + tracing::trace!("Failed to parse DIDL metadata: {}", e); + return None; + } + }; + + // Extract duration from the first item's first resource + let duration = didl.items.first()?.resources.first()?.duration.clone(); + + if let Some(ref dur) = duration { + tracing::debug!( + "MusicRenderer: Extracted duration from DIDL metadata: {}", + dur + ); + } else { + tracing::trace!("No duration attribute found in DIDL metadata"); + } + + duration +} + /// Transport control façade that dispatches to whichever backend can fulfill /// the request, returning a standardized error if the backend lacks support. impl TransportControl for MusicRendererBackend { diff --git a/pmoradiofrance/src/metadata_cache.rs b/pmoradiofrance/src/metadata_cache.rs index c664ac12..14fa67c8 100644 --- a/pmoradiofrance/src/metadata_cache.rs +++ b/pmoradiofrance/src/metadata_cache.rs @@ -238,17 +238,23 @@ impl CachedMetadata { match cache.add_from_url(&cover_url, Some("radiofrance")).await { Ok(pk) => { // Construire l'URL publique - let public_url = format!( - "{}{}", - server_base_url.trim_end_matches('/'), - cache.route_for(&pk, None) + let route = cache.route_for(&pk, None); + let public_url = format!("{}{}", server_base_url.trim_end_matches('/'), route); + + #[cfg(feature = "logging")] + tracing::debug!( + "Cached cover - UUID: {}, PK: {}, route: {}, public_url: {}", + uuid, + pk, + route, + public_url ); (Some(public_url), Some(pk)) } Err(e) => { #[cfg(feature = "logging")] - tracing::warn!("Failed to cache Radio France cover: {}", e); + tracing::warn!("Failed to cache Radio France cover UUID {}: {}", uuid, e); // Fallback sur le logo par défaut en cas d'erreur let logo_url = format!( "{}/api/radiofrance/default-logo", @@ -344,6 +350,47 @@ impl CachedMetadata { /// /// La playlist et l'item ont EXACTEMENT les mêmes métadonnées pub fn to_didl(&self, playlist_id: &str, parent_id: &str) -> Container { + // Calculer la duration dynamiquement (temps restant jusqu'à end_time) + let duration = if let Some(end) = self.end_time { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if end > now { + let duration_secs = end - now; + let hours = duration_secs / 3600; + let minutes = (duration_secs % 3600) / 60; + let seconds = duration_secs % 60; + let dur = format!("{}:{:02}:{:02}", hours, minutes, seconds); + + #[cfg(feature = "logging")] + tracing::debug!( + "Duration calculated for {}: {} (end_time: {}, now: {}, remaining: {}s)", + self.slug, + dur, + end, + now, + duration_secs + ); + + Some(dur) + } else { + #[cfg(feature = "logging")] + tracing::warn!( + "Duration expired for {}: end_time {} < now {}", + self.slug, + end, + now + ); + None + } + } else { + #[cfg(feature = "logging")] + tracing::warn!("No end_time for {}, duration will be None", self.slug); + None + }; + let item = Item { id: format!("{}:stream", playlist_id), parent_id: playlist_id.to_string(), @@ -363,7 +410,7 @@ impl CachedMetadata { bits_per_sample: None, sample_frequency: self.sample_frequency.clone(), nr_audio_channels: self.nr_audio_channels.clone(), - duration: self.duration.clone(), + duration, url: self.stream_url.clone(), }], descriptions: vec![], @@ -493,11 +540,21 @@ impl MetadataCache { } }; - // 3. Parse LiveResponse -> CachedMetadata + // 3. Récupérer le nom de la station depuis la liste des stations + let station_name = { + let stations = self.get_stations().await.unwrap_or_default(); + stations + .iter() + .find(|s| s.slug == slug) + .map(|s| s.name.clone()) + .unwrap_or_else(|| slug.to_string()) + }; + + // 4. Parse LiveResponse -> CachedMetadata let metadata = CachedMetadata::from_live_response( &Station { slug: slug.to_string(), - name: slug.to_string(), + name: station_name, }, &live_response, &self.cover_cache, diff --git a/version.txt b/version.txt index e4737652..0b69c00c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.13 +0.3.14