Bump version to 0.3.20 and enhance metadata handling for Arylic and LinkPlay renderers

Update version from 0.3.19 to 0.3.20

- Enhanced metadata handling in ArylicTcpRenderer and LinkPlayRenderer to use queue metadata for consistent duration protection
- Added shared queue functionality for HybridUpnpArylic to synchronize metadata between UPnP and Arylic backends
- Improved error logging and debug information for playback position fetching
- Updated version in Cargo.toml and version.txt
This commit is contained in:
2026-01-31 23:02:54 +01:00
parent acfdf01dab
commit ecb8dc7e75
6 changed files with 104 additions and 8 deletions

2
Cargo.lock generated
View File

@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "PMOMusic"
version = "0.3.19"
version = "0.3.20"
dependencies = [
"axum 0.8.7",
"console-subscriber",

View File

@@ -1,6 +1,6 @@
[package]
name = "PMOMusic"
version = "0.3.19"
version = "0.3.20"
edition = "2024"
[dependencies]

View File

@@ -139,6 +139,27 @@ impl ArylicTcpRenderer {
pub fn is_continuous_stream(&self) -> bool {
*self.continuous_stream.lock().unwrap()
}
/// Create an ArylicTcpRenderer with a shared queue (for HybridUpnpArylic)
pub fn with_shared_queue(
info: &RendererInfo,
shared_queue: Arc<Mutex<MusicQueue>>,
) -> Result<Self, ControlPointError> {
let host = extract_linkplay_host(info.location()).ok_or_else(|| {
ControlPointError::ArilycTcpError(format!(
"Renderer {} has no valid LOCATION host",
info.udn()
))
})?;
Ok(Self {
host,
port: ARYLIC_TCP_PORT,
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
queue: shared_queue,
continuous_stream: Arc::new(Mutex::new(false)),
})
}
}
impl TransportControl for ArylicTcpRenderer {
@@ -222,8 +243,61 @@ impl PlaybackStatus for ArylicTcpRenderer {
impl PlaybackPosition for ArylicTcpRenderer {
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
let info = self.fetch_playback_info()?;
Ok(info.position_info())
let info = match self.fetch_playback_info() {
Ok(info) => {
tracing::debug!("ArylicTcp fetch_playback_info returned: {:?}", info);
info
}
Err(e) => {
tracing::warn!("ArylicTcp fetch_playback_info failed: {}", e);
return Err(e);
}
};
let mut position_info = info.position_info();
tracing::debug!(
"ArylicTcp position_info: track_duration={:?}, rel_time={:?}, track_metadata={:?}, track_uri={:?}",
position_info.track_duration,
position_info.rel_time,
position_info
.track_metadata
.as_ref()
.map(|s| &s[..s.len().min(100)]),
position_info.track_uri
);
// Récupérer les métadonnées depuis la queue (avec protection contre diminution de durée)
// Normalement current_index est toujours Some() si la queue n'est pas vide (règle métier)
let mut queue_guard = self.queue.lock().unwrap();
let queue_item = queue_guard.peek_current().ok().flatten();
if let Some((current_item, _)) = queue_item {
// Build DIDL metadata XML from cached/protected TrackMetadata
if let Some(ref metadata) = current_item.metadata {
tracing::debug!(
"ArylicTcp playback_position: using queue metadata - title={:?}, artist={:?}, duration={:?}, is_stream={}",
metadata.title,
metadata.artist,
metadata.duration,
metadata.is_continuous_stream
);
position_info.track_metadata = Some(
crate::music_renderer::musicrenderer::build_didl_lite_metadata(
metadata,
&current_item.uri,
&current_item.protocol_info,
),
);
} else {
tracing::warn!("ArylicTcp playback_position: queue item has no metadata");
}
position_info.track_uri = Some(current_item.uri.clone());
} else {
tracing::warn!("ArylicTcp playback_position: no current queue item");
}
drop(queue_guard);
Ok(position_info)
}
}

View File

@@ -155,7 +155,27 @@ impl PlaybackStatus for LinkPlayRenderer {
impl PlaybackPosition for LinkPlayRenderer {
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
Ok(self.fetch_status()?.position_info())
let mut position_info = self.fetch_status()?.position_info();
// Use queue metadata instead of direct status metadata to benefit from duration protection
let mut queue_guard = self.queue.lock().unwrap();
let queue_item = queue_guard.peek_current().ok().flatten();
if let Some((current_item, _)) = queue_item {
if let Some(ref metadata) = current_item.metadata {
position_info.track_metadata = Some(
crate::music_renderer::musicrenderer::build_didl_lite_metadata(
metadata,
&current_item.uri,
&current_item.protocol_info,
),
);
}
position_info.track_uri = Some(current_item.uri.clone());
}
drop(queue_guard);
Ok(position_info)
}
}

View File

@@ -1685,8 +1685,10 @@ impl RendererFromMediaRendererInfo for MusicRendererBackend {
if has_arylic && has_avtransport {
let upnp_backend = UpnpRenderer::build_from_renderer_info(info)?;
if let MusicRendererBackend::Upnp(upnp) = upnp_backend {
match ArylicTcpRenderer::build_from_renderer_info(info) {
Ok(MusicRendererBackend::ArylicTcp(arylic)) => {
// Share the UPnP queue with Arylic so metadata is consistent
let shared_queue = upnp.queue().clone();
match ArylicTcpRenderer::with_shared_queue(info, shared_queue) {
Ok(arylic) => {
return Ok(MusicRendererBackend::HybridUpnpArylic { upnp, arylic });
}
Err(err) => {

View File

@@ -1 +1 @@
0.3.19
0.3.20