From 5f7ffe1bbe12a1e73d01e3b014c6030a32137a5b Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Wed, 17 Dec 2025 20:45:51 +0100 Subject: [PATCH] debug openhome --- .../examples/full_control_point_demo.rs | 1 + pmocontrol/examples/live_pmomusic_demo.rs | 1 + pmocontrol/examples/queue_pmomusic_demo.rs | 1 + pmocontrol/examples/test_attach_playlist.rs | 179 ++++++++++++++++++ pmocontrol/src/control_point.rs | 14 +- .../src/control_point/openhome_queue.rs | 7 +- pmocontrol/src/music_renderer.rs | 2 + pmocontrol/src/pmoserver_ext.rs | 1 + pmocontrol/src/queue_backend.rs | 7 + pmocontrol/src/soap_client.rs | 11 ++ 10 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 pmocontrol/examples/test_attach_playlist.rs diff --git a/pmocontrol/examples/full_control_point_demo.rs b/pmocontrol/examples/full_control_point_demo.rs index 388dc525..1d22a4d1 100644 --- a/pmocontrol/examples/full_control_point_demo.rs +++ b/pmocontrol/examples/full_control_point_demo.rs @@ -1271,6 +1271,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option< media_server_id: server.id().clone(), didl_id: entry.id.clone(), uri: resource.uri.clone(), + protocol_info: resource.protocol_info.clone(), metadata: Some(metadata), }) } diff --git a/pmocontrol/examples/live_pmomusic_demo.rs b/pmocontrol/examples/live_pmomusic_demo.rs index 127870ae..73df75fc 100644 --- a/pmocontrol/examples/live_pmomusic_demo.rs +++ b/pmocontrol/examples/live_pmomusic_demo.rs @@ -552,6 +552,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option< media_server_id: server.id().clone(), didl_id: entry.id.clone(), uri: resource.uri.clone(), + protocol_info: resource.protocol_info.clone(), metadata: Some(metadata), }) } diff --git a/pmocontrol/examples/queue_pmomusic_demo.rs b/pmocontrol/examples/queue_pmomusic_demo.rs index 0beb5df1..276cb059 100644 --- a/pmocontrol/examples/queue_pmomusic_demo.rs +++ b/pmocontrol/examples/queue_pmomusic_demo.rs @@ -483,6 +483,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option< media_server_id: server.id().clone(), didl_id: entry.id.clone(), uri: resource.uri.clone(), + protocol_info: resource.protocol_info.clone(), metadata: Some(metadata), }) } diff --git a/pmocontrol/examples/test_attach_playlist.rs b/pmocontrol/examples/test_attach_playlist.rs new file mode 100644 index 00000000..e4cef051 --- /dev/null +++ b/pmocontrol/examples/test_attach_playlist.rs @@ -0,0 +1,179 @@ +/// Test example to debug playlist attachment issues +/// +/// This example tests each step of attaching a playlist to an OpenHome renderer: +/// 1. Browse the MediaServer for playlist items +/// 2. Insert each item into the OpenHome renderer's playlist +/// 3. Verify the operation succeeded +/// +/// Usage: +/// cargo run --example test_attach_playlist + +use anyhow::{Context, Result}; +use pmocontrol::{ + media_server::{MediaBrowser, MediaEntry, MediaServer, ServerId}, + openhome_client::OhPlaylistClient, +}; +use tracing::{debug, error, info, warn}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize logging + tracing_subscriber::registry() + .with(fmt::layer()) + .with( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info,pmocontrol=debug")), + ) + .init(); + + info!("๐Ÿงช Starting playlist attachment test"); + + // Configuration - adjust these for your setup + let media_server_url = "http://192.168.0.138:8080"; + let media_server_id = "uuid:17fe2ea6-8908-4e30-bc52-b28ea4cab3e4"; + let playlist_container_id = "radio-paradise:channel:mellow:liveplaylist"; + + let renderer_playlist_url = "http://192.168.0.200:49152/Playlist"; + let renderer_service_type = "urn:av-openhome-org:service:Playlist:1"; + + info!("Configuration:"); + info!(" MediaServer: {}", media_server_url); + info!(" Playlist: {}", playlist_container_id); + info!(" Renderer: {}", renderer_playlist_url); + info!(""); + + // Step 1: Create MediaServer client + info!("๐Ÿ“ก Step 1: Connecting to MediaServer"); + let content_directory_url = format!( + "{}/device/{}/service/ContentDirectory/control", + media_server_url, media_server_id.replace("uuid:", "") + ); + + let server = MediaServer::new( + ServerId(media_server_id.to_string()), + "PMOMusic Test".to_string(), + content_directory_url, + ); + + debug!("MediaServer client created"); + + // Step 2: Browse the playlist container + info!("๐Ÿ“‚ Step 2: Browsing playlist container"); + info!(" Container ID: {}", playlist_container_id); + + let entries = match server.browse_children(playlist_container_id, 0, 10) { + Ok(entries) => { + info!("โœ… Browse succeeded: {} items found", entries.len()); + entries + } + Err(e) => { + error!("โŒ Browse failed: {}", e); + error!(" This is where the error occurs!"); + return Err(e); + } + }; + + if entries.is_empty() { + warn!("โš ๏ธ Playlist is empty, nothing to insert"); + return Ok(()); + } + + // Display the first few items + info!("๐Ÿ“‹ First items in playlist:"); + for (idx, entry) in entries.iter().take(3).enumerate() { + info!(" [{}] {} - {}", + idx, + entry.title, + entry.artist.as_deref().unwrap_or("Unknown") + ); + if let Some(res) = entry.resources.first() { + debug!(" URI: {}", res.uri); + debug!(" protocolInfo: {}", res.protocol_info); + } + } + info!(""); + + // Step 3: Connect to OpenHome renderer + info!("๐ŸŽต Step 3: Connecting to OpenHome renderer"); + let oh_client = OhPlaylistClient::new( + renderer_playlist_url.to_string(), + renderer_service_type.to_string(), + ); + debug!("OpenHome client created"); + + // Step 4: Clear existing playlist + info!("๐Ÿ—‘๏ธ Step 4: Clearing existing playlist"); + match oh_client.delete_all() { + Ok(_) => info!("โœ… Playlist cleared"), + Err(e) => { + warn!("โš ๏ธ Could not clear playlist: {}", e); + } + } + info!(""); + + // Step 5: Insert items one by one + info!("โž• Step 5: Inserting items into renderer"); + let mut after_id = 0u32; + let mut inserted_count = 0; + + for (idx, entry) in entries.iter().enumerate() { + if entry.is_container { + debug!("Skipping container: {}", entry.title); + continue; + } + + let resource = match entry.resources.iter().find(|r| r.is_audio()) { + Some(r) => r, + None => { + warn!("No audio resource found for: {}", entry.title); + continue; + } + }; + + info!(" [{}] Inserting: {}", idx, entry.title); + debug!(" URI: {}", resource.uri); + debug!(" protocolInfo: {}", resource.protocol_info); + + // Build simple DIDL-Lite metadata + let didl_metadata = format!( + r#"{}object.item.audioItem.musicTrack{}"#, + entry.id, + xmlescape(&entry.title), + resource.protocol_info, + xmlescape(&resource.uri) + ); + + trace!("DIDL metadata: {}", didl_metadata); + + match oh_client.insert(after_id, &resource.uri, &didl_metadata) { + Ok(new_id) => { + debug!(" โœ… Inserted with ID: {}", new_id); + after_id = new_id; + inserted_count += 1; + } + Err(e) => { + error!(" โŒ Insert failed: {}", e); + error!(" This is the UPnP 501 error location!"); + + // Continue with next item instead of failing + warn!(" Continuing with next item..."); + } + } + } + + info!(""); + info!("โœ… Test completed: {}/{} items inserted successfully", + inserted_count, entries.len()); + + Ok(()) +} + +/// Simple XML escaping +fn xmlescape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index baa532b5..11d8077d 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -14,7 +14,7 @@ use pmodidl::{DIDLLite, Item as DidlItem, Resource as DidlResource}; use pmoupnp::ssdp::SsdpClient; use quick_xml::se::to_string as to_didl_string; use thiserror::Error; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, trace, warn}; use ureq::{Agent, http}; use xmltree::{Element, XMLNode}; @@ -1424,6 +1424,13 @@ impl ControlPoint { for item in items.iter() { let metadata = playback_item_to_didl(item); + debug!( + uri = item.uri.as_str(), + protocol_info = item.protocol_info.as_str(), + metadata_len = metadata.len(), + "Inserting track to OpenHome playlist" + ); + trace!(metadata = metadata.as_str(), "DIDL-Lite metadata"); after_id = Some(renderer.openhome_playlist_add_track(&item.uri, &metadata, after_id, false)?); } @@ -2119,6 +2126,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option< media_server_id: server.id().clone(), didl_id: entry.id.clone(), uri: resource.uri.clone(), + protocol_info: resource.protocol_info.clone(), metadata: Some(metadata), }) } @@ -2144,6 +2152,8 @@ fn playback_item_from_openhome_track( media_server_id: ServerId(format!("openhome:{}", renderer_id.0)), didl_id: format!("{}{}", OPENHOME_TRACK_PREFIX, track.id), uri: track.uri.clone(), + // OpenHome tracks don't provide protocolInfo, use generic default + protocol_info: "http-get:*:audio/*:*".to_string(), metadata: Some(metadata), } } @@ -2178,7 +2188,7 @@ fn didl_item_from_playback_item(item: &PlaybackItem) -> DidlItem { date: metadata.and_then(|m| m.date.clone()), original_track_number: metadata.and_then(|m| m.track_number.clone()), resources: vec![DidlResource { - protocol_info: "http-get:*:audio/*:*".to_string(), + protocol_info: item.protocol_info.clone(), bits_per_sample: None, sample_frequency: None, nr_audio_channels: None, diff --git a/pmocontrol/src/control_point/openhome_queue.rs b/pmocontrol/src/control_point/openhome_queue.rs index c410f807..167a12d5 100644 --- a/pmocontrol/src/control_point/openhome_queue.rs +++ b/pmocontrol/src/control_point/openhome_queue.rs @@ -181,6 +181,8 @@ impl OpenHomeQueue { media_server_id: ServerId(format!("openhome:{}", self.renderer_id.0)), didl_id, uri: entry.uri.clone(), + // OpenHome tracks don't provide protocolInfo, use generic default + protocol_info: "http-get:*:audio/*:*".to_string(), metadata, } } @@ -261,9 +263,10 @@ fn build_metadata_xml(item: &PlaybackItem) -> String { } } + let escaped_protocol_info = escape(item.protocol_info.as_str()); xml.push_str(&format!( - r#"{}"#, - escaped_uri + r#"{}"#, + escaped_protocol_info, escaped_uri )); xml.push_str(r#"object.item.audioItem.musicTrack"#); xml diff --git a/pmocontrol/src/music_renderer.rs b/pmocontrol/src/music_renderer.rs index 140320d3..6c3a4bcb 100644 --- a/pmocontrol/src/music_renderer.rs +++ b/pmocontrol/src/music_renderer.rs @@ -356,6 +356,8 @@ impl MusicRenderer { media_server_id: ServerId(format!("openhome:{}", renderer_id.0)), didl_id, uri: uri.to_string(), + // OpenHome tracks don't provide protocolInfo, use generic default + protocol_info: "http-get:*:audio/*:*".to_string(), metadata, }) } diff --git a/pmocontrol/src/pmoserver_ext.rs b/pmocontrol/src/pmoserver_ext.rs index a2abb583..310763c3 100644 --- a/pmocontrol/src/pmoserver_ext.rs +++ b/pmocontrol/src/pmoserver_ext.rs @@ -1875,6 +1875,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option< media_server_id: server.id().clone(), didl_id: entry.id.clone(), uri: resource.uri.clone(), + protocol_info: resource.protocol_info.clone(), metadata: Some(metadata), }) } diff --git a/pmocontrol/src/queue_backend.rs b/pmocontrol/src/queue_backend.rs index cc5d34da..70c1cdca 100644 --- a/pmocontrol/src/queue_backend.rs +++ b/pmocontrol/src/queue_backend.rs @@ -66,6 +66,13 @@ pub struct PlaybackItem { /// from the DIDL-Lite item. pub uri: String, + /// UPnP protocolInfo string for the resource (e.g., "http-get:*:audio/flac:*"). + /// + /// This string describes the protocol, network, MIME type, and additional + /// info about the media resource. It's required for proper UPnP/OpenHome + /// renderer compatibility. + pub protocol_info: String, + /// Optional rich metadata for the track (title, artist, album, cover, /// duration, โ€ฆ). /// diff --git a/pmocontrol/src/soap_client.rs b/pmocontrol/src/soap_client.rs index 77c0a5a3..e20d0003 100644 --- a/pmocontrol/src/soap_client.rs +++ b/pmocontrol/src/soap_client.rs @@ -2,6 +2,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use pmoupnp::soap::{SoapEnvelope, build_soap_request, parse_soap_envelope}; +use tracing::{debug, trace}; use ureq::Agent; /// Result of a SOAP call: @@ -39,6 +40,15 @@ pub fn invoke_upnp_action_with_timeout( let body_xml = build_soap_request(service_type, action, args) .context("Failed to build SOAP request body")?; + debug!( + url = control_url, + action = action, + service_type = service_type, + "Sending SOAP request" + ); + + trace!(body = body_xml.as_str(), "SOAP request body"); + let mut builder = Agent::config_builder(); builder = builder.http_status_as_error(false); if let Some(duration) = timeout { @@ -60,6 +70,7 @@ pub fn invoke_upnp_action_with_timeout( .with_context(|| format!("HTTP error when sending SOAP request to {}", control_url))?; let status = response.status(); + debug!(status = status.as_u16(), "SOAP response received"); // 5. Read full body //