Add pmoradiofrance module and improve track duration handling
This commit adds the pmoradiofrance module to the build, updates the version to 0.3.14, and enhances the music renderer to fetch track duration from DIDL metadata when the renderer doesn't provide it directly. It also improves metadata caching for Radio France, including dynamic duration calculation and better station name handling.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
dependencies = [
|
||||
"axum 0.8.7",
|
||||
"console-subscriber",
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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<PlaybackPositionInfo, ControlPointError> {
|
||||
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 <res> 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<String> {
|
||||
// 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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.3.13
|
||||
0.3.14
|
||||
|
||||
Reference in New Issue
Block a user