/// 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::{EnvFilter, fmt, prelude::*}; #[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('\'', "'") }