debug openhome
This commit is contained in:
@@ -1271,6 +1271,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
|||||||
media_server_id: server.id().clone(),
|
media_server_id: server.id().clone(),
|
||||||
didl_id: entry.id.clone(),
|
didl_id: entry.id.clone(),
|
||||||
uri: resource.uri.clone(),
|
uri: resource.uri.clone(),
|
||||||
|
protocol_info: resource.protocol_info.clone(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
|||||||
media_server_id: server.id().clone(),
|
media_server_id: server.id().clone(),
|
||||||
didl_id: entry.id.clone(),
|
didl_id: entry.id.clone(),
|
||||||
uri: resource.uri.clone(),
|
uri: resource.uri.clone(),
|
||||||
|
protocol_info: resource.protocol_info.clone(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
|||||||
media_server_id: server.id().clone(),
|
media_server_id: server.id().clone(),
|
||||||
didl_id: entry.id.clone(),
|
didl_id: entry.id.clone(),
|
||||||
uri: resource.uri.clone(),
|
uri: resource.uri.clone(),
|
||||||
|
protocol_info: resource.protocol_info.clone(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
179
pmocontrol/examples/test_attach_playlist.rs
Normal file
179
pmocontrol/examples/test_attach_playlist.rs
Normal file
@@ -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#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><item id="{}" parentID="-1" restricted="1"><dc:title>{}</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><res protocolInfo="{}">{}</res></item></DIDL-Lite>"#,
|
||||||
|
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('\'', "'")
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ use pmodidl::{DIDLLite, Item as DidlItem, Resource as DidlResource};
|
|||||||
use pmoupnp::ssdp::SsdpClient;
|
use pmoupnp::ssdp::SsdpClient;
|
||||||
use quick_xml::se::to_string as to_didl_string;
|
use quick_xml::se::to_string as to_didl_string;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
use ureq::{Agent, http};
|
use ureq::{Agent, http};
|
||||||
use xmltree::{Element, XMLNode};
|
use xmltree::{Element, XMLNode};
|
||||||
|
|
||||||
@@ -1424,6 +1424,13 @@ impl ControlPoint {
|
|||||||
|
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
let metadata = playback_item_to_didl(item);
|
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 =
|
after_id =
|
||||||
Some(renderer.openhome_playlist_add_track(&item.uri, &metadata, after_id, false)?);
|
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(),
|
media_server_id: server.id().clone(),
|
||||||
didl_id: entry.id.clone(),
|
didl_id: entry.id.clone(),
|
||||||
uri: resource.uri.clone(),
|
uri: resource.uri.clone(),
|
||||||
|
protocol_info: resource.protocol_info.clone(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2144,6 +2152,8 @@ fn playback_item_from_openhome_track(
|
|||||||
media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
|
media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
|
||||||
didl_id: format!("{}{}", OPENHOME_TRACK_PREFIX, track.id),
|
didl_id: format!("{}{}", OPENHOME_TRACK_PREFIX, track.id),
|
||||||
uri: track.uri.clone(),
|
uri: track.uri.clone(),
|
||||||
|
// OpenHome tracks don't provide protocolInfo, use generic default
|
||||||
|
protocol_info: "http-get:*:audio/*:*".to_string(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2178,7 +2188,7 @@ fn didl_item_from_playback_item(item: &PlaybackItem) -> DidlItem {
|
|||||||
date: metadata.and_then(|m| m.date.clone()),
|
date: metadata.and_then(|m| m.date.clone()),
|
||||||
original_track_number: metadata.and_then(|m| m.track_number.clone()),
|
original_track_number: metadata.and_then(|m| m.track_number.clone()),
|
||||||
resources: vec![DidlResource {
|
resources: vec![DidlResource {
|
||||||
protocol_info: "http-get:*:audio/*:*".to_string(),
|
protocol_info: item.protocol_info.clone(),
|
||||||
bits_per_sample: None,
|
bits_per_sample: None,
|
||||||
sample_frequency: None,
|
sample_frequency: None,
|
||||||
nr_audio_channels: None,
|
nr_audio_channels: None,
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ impl OpenHomeQueue {
|
|||||||
media_server_id: ServerId(format!("openhome:{}", self.renderer_id.0)),
|
media_server_id: ServerId(format!("openhome:{}", self.renderer_id.0)),
|
||||||
didl_id,
|
didl_id,
|
||||||
uri: entry.uri.clone(),
|
uri: entry.uri.clone(),
|
||||||
|
// OpenHome tracks don't provide protocolInfo, use generic default
|
||||||
|
protocol_info: "http-get:*:audio/*:*".to_string(),
|
||||||
metadata,
|
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!(
|
xml.push_str(&format!(
|
||||||
r#"<res protocolInfo="http-get:*:audio/*:*">{}</res>"#,
|
r#"<res protocolInfo="{}">{}</res>"#,
|
||||||
escaped_uri
|
escaped_protocol_info, escaped_uri
|
||||||
));
|
));
|
||||||
xml.push_str(r#"<upnp:class>object.item.audioItem.musicTrack</upnp:class></item></DIDL-Lite>"#);
|
xml.push_str(r#"<upnp:class>object.item.audioItem.musicTrack</upnp:class></item></DIDL-Lite>"#);
|
||||||
xml
|
xml
|
||||||
|
|||||||
@@ -356,6 +356,8 @@ impl MusicRenderer {
|
|||||||
media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
|
media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
|
||||||
didl_id,
|
didl_id,
|
||||||
uri: uri.to_string(),
|
uri: uri.to_string(),
|
||||||
|
// OpenHome tracks don't provide protocolInfo, use generic default
|
||||||
|
protocol_info: "http-get:*:audio/*:*".to_string(),
|
||||||
metadata,
|
metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1875,6 +1875,7 @@ fn playback_item_from_entry(server: &MusicServer, entry: &MediaEntry) -> Option<
|
|||||||
media_server_id: server.id().clone(),
|
media_server_id: server.id().clone(),
|
||||||
didl_id: entry.id.clone(),
|
didl_id: entry.id.clone(),
|
||||||
uri: resource.uri.clone(),
|
uri: resource.uri.clone(),
|
||||||
|
protocol_info: resource.protocol_info.clone(),
|
||||||
metadata: Some(metadata),
|
metadata: Some(metadata),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ pub struct PlaybackItem {
|
|||||||
/// from the DIDL-Lite item.
|
/// from the DIDL-Lite item.
|
||||||
pub uri: String,
|
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,
|
/// Optional rich metadata for the track (title, artist, album, cover,
|
||||||
/// duration, …).
|
/// duration, …).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use pmoupnp::soap::{SoapEnvelope, build_soap_request, parse_soap_envelope};
|
use pmoupnp::soap::{SoapEnvelope, build_soap_request, parse_soap_envelope};
|
||||||
|
use tracing::{debug, trace};
|
||||||
use ureq::Agent;
|
use ureq::Agent;
|
||||||
|
|
||||||
/// Result of a SOAP call:
|
/// 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)
|
let body_xml = build_soap_request(service_type, action, args)
|
||||||
.context("Failed to build SOAP request body")?;
|
.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();
|
let mut builder = Agent::config_builder();
|
||||||
builder = builder.http_status_as_error(false);
|
builder = builder.http_status_as_error(false);
|
||||||
if let Some(duration) = timeout {
|
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))?;
|
.with_context(|| format!("HTTP error when sending SOAP request to {}", control_url))?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
|
debug!(status = status.as_u16(), "SOAP response received");
|
||||||
|
|
||||||
// 5. Read full body
|
// 5. Read full body
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user