From 4b29d128590d9b96a9d7dc21b2415f655fb15451 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 26 Oct 2025 07:41:03 +0100 Subject: [PATCH] retire le mediaserver de pmoparadise --- Cargo.lock | 3 - pmoparadise/Cargo.toml | 16 +- pmoparadise/examples/upnp_mediaserver.rs | 70 ---- pmoparadise/src/lib.rs | 12 +- .../src/mediaserver/connection_manager.rs | 168 --------- .../src/mediaserver/content_directory.rs | 349 ------------------ pmoparadise/src/mediaserver/mod.rs | 56 --- pmoparadise/src/mediaserver/server.rs | 188 ---------- 8 files changed, 5 insertions(+), 857 deletions(-) delete mode 100644 pmoparadise/examples/upnp_mediaserver.rs delete mode 100644 pmoparadise/src/mediaserver/connection_manager.rs delete mode 100644 pmoparadise/src/mediaserver/content_directory.rs delete mode 100644 pmoparadise/src/mediaserver/mod.rs delete mode 100644 pmoparadise/src/mediaserver/server.rs diff --git a/Cargo.lock b/Cargo.lock index f895175d..747121c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2504,11 +2504,9 @@ dependencies = [ "pmoaudiocache", "pmoconfig", "pmocovers", - "pmodidl", "pmoplaylist", "pmoserver", "pmosource", - "pmoupnp", "reqwest", "rusqlite", "serde", @@ -2525,7 +2523,6 @@ dependencies = [ "tracing-subscriber", "url", "utoipa", - "uuid", "wiremock", ] diff --git a/pmoparadise/Cargo.toml b/pmoparadise/Cargo.toml index 424e320f..14e0d83b 100644 --- a/pmoparadise/Cargo.toml +++ b/pmoparadise/Cargo.toml @@ -55,12 +55,6 @@ claxon = "0.4" hound = { version = "3.5", optional = true } tempfile = { version = "3.8", optional = true } -# UPnP Media Server dependencies -pmoupnp = { path = "../pmoupnp", optional = true } -pmoserver = { path = "../pmoserver", optional = true } -pmodidl = { path = "../pmodidl", optional = true } -uuid = { version = "1.18", optional = true } - # Common music source traits pmosource = { path = "../pmosource" } @@ -72,7 +66,8 @@ pmoconfig = { path = "../pmoconfig" } pmocovers = { path = "../pmocovers" } pmoaudiocache = { path = "../pmoaudiocache" } -# OpenAPI/Swagger support (pour pmoserver extension) +# pmoserver extension support +pmoserver = { path = "../pmoserver", optional = true } utoipa = { version = "5.4.0", optional = true } axum = { version = "0.8.4", optional = true } @@ -84,8 +79,6 @@ metadata-only = [] per-track = ["dep:hound", "dep:tempfile"] # Active l'API REST pmoserver pmoserver = ["dep:pmoserver", "dep:utoipa", "dep:axum", "server"] -# Active le media server UPnP (includes pmoserver) -mediaserver = ["dep:pmoupnp", "dep:pmodidl", "dep:uuid", "pmoserver"] # Feature pour activer le support serveur (cache registry) server = ["pmosource/server"] # Feature cache (deprecated - toujours actif maintenant) @@ -114,11 +107,6 @@ name = "extract_track" path = "examples/extract_track.rs" required-features = ["per-track"] -[[example]] -name = "upnp_mediaserver" -path = "examples/upnp_mediaserver.rs" -required-features = ["mediaserver"] - [[example]] name = "with_cache" path = "examples/with_cache.rs" diff --git a/pmoparadise/examples/upnp_mediaserver.rs b/pmoparadise/examples/upnp_mediaserver.rs deleted file mode 100644 index b8fb9ed1..00000000 --- a/pmoparadise/examples/upnp_mediaserver.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Example: Run a UPnP/DLNA Media Server for Radio Paradise -//! -//! This example demonstrates: -//! - Creating a UPnP Media Server -//! - Exposing Radio Paradise blocks and songs -//! - SSDP discovery and announcements -//! - ContentDirectory and ConnectionManager services -//! -//! Run with: cargo run --example upnp_mediaserver --features mediaserver -//! -//! The server will be discoverable by DLNA/UPnP clients on your network. - -#[cfg(feature = "mediaserver")] -use pmoparadise::mediaserver::RadioParadiseMediaServer; -#[cfg(feature = "mediaserver")] -use pmoparadise::Bitrate; - -#[cfg(feature = "mediaserver")] -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize logging - #[cfg(feature = "logging")] - tracing_subscriber::fmt::init(); - - println!("Radio Paradise UPnP Media Server"); - println!("=================================\n"); - - // Create the media server - println!("Creating media server..."); - let server = RadioParadiseMediaServer::builder() - .with_friendly_name("Radio Paradise FLAC") - .with_manufacturer("PMOMusic") - .with_model_name("Radio Paradise Adapter v0.1") - .with_bitrate(Bitrate::Flac) - .with_channel(0) // Main mix - .with_port(8080) - .build() - .await?; - - println!("Media Server created!"); - println!(" UDN: {}", server.udn()); - println!(" Port: 8080"); - println!(" Quality: FLAC Lossless"); - println!(" Channel: Main Mix (0)"); - println!(); - - println!("Server is now discoverable on your network."); - println!("Look for 'Radio Paradise FLAC' in your DLNA/UPnP clients."); - println!(); - println!("ContentDirectory service available at:"); - println!( - " http://localhost:8080/upnp/device/{}/service/ContentDirectory", - server.udn() - ); - println!(); - println!("Press Ctrl+C to stop the server."); - println!(); - - // Run the server - server.run().await?; - - Ok(()) -} - -#[cfg(not(feature = "mediaserver"))] -fn main() { - eprintln!("ERROR: This example requires the 'mediaserver' feature."); - eprintln!("Run with: cargo run --example upnp_mediaserver --features mediaserver"); - std::process::exit(1); -} diff --git a/pmoparadise/src/lib.rs b/pmoparadise/src/lib.rs index bbff376f..052075c3 100644 --- a/pmoparadise/src/lib.rs +++ b/pmoparadise/src/lib.rs @@ -209,9 +209,9 @@ //! //! - `default = ["metadata-only"]`: Standard metadata and streaming (no FLAC decoding) //! - `per-track`: Enable FLAC decoding and per-track extraction (adds `claxon`, `hound`, `tempfile`) -//! - `logging`: Enable tracing logs for debugging -//! - `mediaserver`: Enable UPnP/DLNA Media Server (adds `pmoupnp`, `pmoserver`, `pmodidl`) -//! - `cache`: Enable cover and audio caching support (adds `pmocovers`, `pmoaudiocache`, enables `logging`) +//! - `pmoserver`: Enable REST API extension for pmoserver integration (adds `utoipa`, `axum`) +//! - `server`: Enable server-side features (cache registry integration) +//! - `cache`: Enable cover and audio caching support (adds `pmocovers`, `pmoaudiocache`) //! //! ## See Also //! @@ -229,9 +229,6 @@ pub mod streaming; #[cfg(feature = "per-track")] pub mod track; -#[cfg(feature = "mediaserver")] -pub mod mediaserver; - #[cfg(feature = "pmoserver")] pub mod pmoserver_ext; @@ -245,9 +242,6 @@ pub use stream::BlockStream; #[cfg(feature = "per-track")] pub use track::{TrackMetadata, TrackStream}; -#[cfg(feature = "mediaserver")] -pub use mediaserver::{MediaServerBuilder, RadioParadiseMediaServer}; - #[cfg(feature = "pmoserver")] pub use pmoserver_ext::{ create_api_router, RadioParadiseApiDoc, RadioParadiseExt, RadioParadiseState, diff --git a/pmoparadise/src/mediaserver/connection_manager.rs b/pmoparadise/src/mediaserver/connection_manager.rs deleted file mode 100644 index 4608f6c4..00000000 --- a/pmoparadise/src/mediaserver/connection_manager.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! ConnectionManager service implementation - -use pmoupnp::actions::Action; -use pmoupnp::services::Service; -use pmoupnp::state_variables::StateVariable; -use std::sync::Arc; - -/// Create a ConnectionManager service -/// -/// The ConnectionManager service provides information about supported -/// protocols and connections. -pub fn create_connection_manager_service() -> Service { - let mut service = Service::new("ConnectionManager".to_string()); - service.set_service_type("urn:schemas-upnp-org:service:ConnectionManager:1".to_string()); - service.set_service_id("urn:upnp-org:serviceId:ConnectionManager".to_string()); - - // State variables - let source_protocol_info = - StateVariable::new("SourceProtocolInfo".to_string(), "string".to_string()) - .with_send_events(true) - .with_default_value(get_protocol_info()); - - let sink_protocol_info = - StateVariable::new("SinkProtocolInfo".to_string(), "string".to_string()) - .with_send_events(true) - .with_default_value("".to_string()); - - let current_connection_ids = - StateVariable::new("CurrentConnectionIDs".to_string(), "string".to_string()) - .with_send_events(true) - .with_default_value("0".to_string()); - - service.add_state_variable(Arc::new(source_protocol_info)); - service.add_state_variable(Arc::new(sink_protocol_info)); - service.add_state_variable(Arc::new(current_connection_ids)); - - // GetProtocolInfo action - let mut get_protocol_info = Action::new("GetProtocolInfo".to_string()); - get_protocol_info.add_output_argument("Source".to_string(), "SourceProtocolInfo".to_string()); - get_protocol_info.add_output_argument("Sink".to_string(), "SinkProtocolInfo".to_string()); - service.add_action(Arc::new(get_protocol_info)); - - // GetCurrentConnectionIDs action - let mut get_connection_ids = Action::new("GetCurrentConnectionIDs".to_string()); - get_connection_ids.add_output_argument( - "ConnectionIDs".to_string(), - "CurrentConnectionIDs".to_string(), - ); - service.add_action(Arc::new(get_connection_ids)); - - // GetCurrentConnectionInfo action - let mut get_connection_info = Action::new("GetCurrentConnectionInfo".to_string()); - get_connection_info.add_input_argument( - "ConnectionID".to_string(), - "A_ARG_TYPE_ConnectionID".to_string(), - ); - get_connection_info.add_output_argument("RcsID".to_string(), "A_ARG_TYPE_RcsID".to_string()); - get_connection_info.add_output_argument( - "AVTransportID".to_string(), - "A_ARG_TYPE_AVTransportID".to_string(), - ); - get_connection_info.add_output_argument( - "ProtocolInfo".to_string(), - "A_ARG_TYPE_ProtocolInfo".to_string(), - ); - get_connection_info.add_output_argument( - "PeerConnectionManager".to_string(), - "A_ARG_TYPE_ConnectionManager".to_string(), - ); - get_connection_info.add_output_argument( - "PeerConnectionID".to_string(), - "A_ARG_TYPE_ConnectionID".to_string(), - ); - get_connection_info - .add_output_argument("Direction".to_string(), "A_ARG_TYPE_Direction".to_string()); - get_connection_info.add_output_argument( - "Status".to_string(), - "A_ARG_TYPE_ConnectionStatus".to_string(), - ); - service.add_action(Arc::new(get_connection_info)); - - // Additional state variables for arguments - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_ConnectionID".to_string(), - "i4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_RcsID".to_string(), - "i4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_AVTransportID".to_string(), - "i4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_ProtocolInfo".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_ConnectionManager".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new( - StateVariable::new("A_ARG_TYPE_Direction".to_string(), "string".to_string()) - .with_allowed_values(vec!["Input".to_string(), "Output".to_string()]), - )); - service.add_state_variable(Arc::new( - StateVariable::new( - "A_ARG_TYPE_ConnectionStatus".to_string(), - "string".to_string(), - ) - .with_allowed_values(vec![ - "OK".to_string(), - "ContentFormatMismatch".to_string(), - "InsufficientBandwidth".to_string(), - "UnreliableChannel".to_string(), - "Unknown".to_string(), - ]), - )); - - service -} - -/// Get the protocol info string -/// -/// Lists all supported protocols for Radio Paradise streaming. -fn get_protocol_info() -> String { - vec![ - // HTTP FLAC - "http-get:*:audio/flac:*", - "http-get:*:audio/x-flac:*", - // HTTP AAC - "http-get:*:audio/aac:*", - "http-get:*:audio/aacp:*", - "http-get:*:audio/x-aac:*", - // HTTP MP3 - "http-get:*:audio/mpeg:*", - "http-get:*:audio/mp3:*", - "http-get:*:audio/x-mp3:*", - ] - .join(",") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_connection_manager() { - let service = create_connection_manager_service(); - assert_eq!( - service.service_type(), - "urn:schemas-upnp-org:service:ConnectionManager:1" - ); - assert_eq!( - service.service_id(), - "urn:upnp-org:serviceId:ConnectionManager" - ); - } - - #[test] - fn test_protocol_info() { - let info = get_protocol_info(); - assert!(info.contains("audio/flac")); - assert!(info.contains("audio/aac")); - assert!(info.contains("audio/mpeg")); - } -} diff --git a/pmoparadise/src/mediaserver/content_directory.rs b/pmoparadise/src/mediaserver/content_directory.rs deleted file mode 100644 index e7433c18..00000000 --- a/pmoparadise/src/mediaserver/content_directory.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! ContentDirectory service implementation - -use crate::RadioParadiseClient; -use pmodidl::{DIDLContainer, DIDLItem, DIDLObject, Resource}; -use pmoupnp::actions::Action; -use pmoupnp::services::Service; -use pmoupnp::state_variables::StateVariable; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Create a ContentDirectory service for Radio Paradise -/// -/// The ContentDirectory service allows browsing Radio Paradise blocks and songs. -pub fn create_content_directory_service(client: Arc>) -> Service { - let mut service = Service::new("ContentDirectory".to_string()); - service.set_service_type("urn:schemas-upnp-org:service:ContentDirectory:1".to_string()); - service.set_service_id("urn:upnp-org:serviceId:ContentDirectory".to_string()); - - // State variables - let system_update_id = StateVariable::new("SystemUpdateID".to_string(), "ui4".to_string()) - .with_send_events(true) - .with_default_value("0".to_string()); - - let container_update_ids = - StateVariable::new("ContainerUpdateIDs".to_string(), "string".to_string()) - .with_send_events(true) - .with_default_value("".to_string()); - - service.add_state_variable(Arc::new(system_update_id)); - service.add_state_variable(Arc::new(container_update_ids)); - - // Browse action - let mut browse = Action::new("Browse".to_string()); - browse.add_input_argument("ObjectID".to_string(), "A_ARG_TYPE_ObjectID".to_string()); - browse.add_input_argument( - "BrowseFlag".to_string(), - "A_ARG_TYPE_BrowseFlag".to_string(), - ); - browse.add_input_argument("Filter".to_string(), "A_ARG_TYPE_Filter".to_string()); - browse.add_input_argument("StartingIndex".to_string(), "A_ARG_TYPE_Index".to_string()); - browse.add_input_argument("RequestedCount".to_string(), "A_ARG_TYPE_Count".to_string()); - browse.add_input_argument( - "SortCriteria".to_string(), - "A_ARG_TYPE_SortCriteria".to_string(), - ); - browse.add_output_argument("Result".to_string(), "A_ARG_TYPE_Result".to_string()); - browse.add_output_argument("NumberReturned".to_string(), "A_ARG_TYPE_Count".to_string()); - browse.add_output_argument("TotalMatches".to_string(), "A_ARG_TYPE_Count".to_string()); - browse.add_output_argument("UpdateID".to_string(), "A_ARG_TYPE_UpdateID".to_string()); - - // Store client reference for the action handler - let client_clone = client.clone(); - browse.set_handler(Box::new(move |args| { - let client = client_clone.clone(); - Box::pin(async move { handle_browse(client, args).await }) - })); - - service.add_action(Arc::new(browse)); - - // GetSearchCapabilities action - let mut get_search_caps = Action::new("GetSearchCapabilities".to_string()); - get_search_caps.add_output_argument( - "SearchCaps".to_string(), - "A_ARG_TYPE_SearchCaps".to_string(), - ); - get_search_caps.set_handler(Box::new(|_| { - Box::pin(async { - let mut result = std::collections::HashMap::new(); - result.insert("SearchCaps".to_string(), "".to_string()); - Ok(result) - }) - })); - service.add_action(Arc::new(get_search_caps)); - - // GetSortCapabilities action - let mut get_sort_caps = Action::new("GetSortCapabilities".to_string()); - get_sort_caps.add_output_argument("SortCaps".to_string(), "A_ARG_TYPE_SortCaps".to_string()); - get_sort_caps.set_handler(Box::new(|_| { - Box::pin(async { - let mut result = std::collections::HashMap::new(); - result.insert("SortCaps".to_string(), "dc:title".to_string()); - Ok(result) - }) - })); - service.add_action(Arc::new(get_sort_caps)); - - // GetSystemUpdateID action - let mut get_update_id = Action::new("GetSystemUpdateID".to_string()); - get_update_id.add_output_argument("Id".to_string(), "SystemUpdateID".to_string()); - get_update_id.set_handler(Box::new(|_| { - Box::pin(async { - let mut result = std::collections::HashMap::new(); - result.insert("Id".to_string(), "0".to_string()); - Ok(result) - }) - })); - service.add_action(Arc::new(get_update_id)); - - // Argument state variables - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_ObjectID".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new( - StateVariable::new("A_ARG_TYPE_BrowseFlag".to_string(), "string".to_string()) - .with_allowed_values(vec![ - "BrowseMetadata".to_string(), - "BrowseDirectChildren".to_string(), - ]), - )); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_Filter".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_Index".to_string(), - "ui4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_Count".to_string(), - "ui4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_SortCriteria".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_Result".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_UpdateID".to_string(), - "ui4".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_SearchCaps".to_string(), - "string".to_string(), - ))); - service.add_state_variable(Arc::new(StateVariable::new( - "A_ARG_TYPE_SortCaps".to_string(), - "string".to_string(), - ))); - - service -} - -/// Handle Browse action -async fn handle_browse( - client: Arc>, - args: std::collections::HashMap, -) -> Result, String> { - let object_id = args.get("ObjectID").ok_or("Missing ObjectID")?; - let browse_flag = args.get("BrowseFlag").ok_or("Missing BrowseFlag")?; - let starting_index: usize = args - .get("StartingIndex") - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - let requested_count: usize = args - .get("RequestedCount") - .and_then(|s| s.parse().ok()) - .unwrap_or(100); - - let client = client.read().await; - - let (didl_result, number_returned, total_matches) = match object_id.as_str() { - "0" => { - // Root container - show current block - if browse_flag == "BrowseMetadata" { - let root = create_root_container(); - (serialize_didl(&[root]), 1, 1) - } else { - // BrowseDirectChildren - show current block as a container - let block = client - .get_block(None) - .await - .map_err(|e| format!("Failed to get block: {}", e))?; - - let block_container = create_block_container(&block); - (serialize_didl(&[block_container]), 1, 1) - } - } - id if id.starts_with("block:") => { - // Browse songs in a block - let event_id: u64 = id - .strip_prefix("block:") - .and_then(|s| s.parse().ok()) - .ok_or("Invalid block ID")?; - - let block = client - .get_block(Some(event_id)) - .await - .map_err(|e| format!("Failed to get block: {}", e))?; - - if browse_flag == "BrowseMetadata" { - let container = create_block_container(&block); - (serialize_didl(&[container]), 1, 1) - } else { - // BrowseDirectChildren - show songs - let songs = block.songs_ordered(); - let total = songs.len(); - let songs_slice = songs - .iter() - .skip(starting_index) - .take(requested_count) - .collect::>(); - - let items: Vec = songs_slice - .iter() - .map(|(idx, song)| create_song_item(&block, *idx, song)) - .collect(); - - (serialize_didl(&items), items.len(), total) - } - } - _ => { - return Err(format!("Unknown ObjectID: {}", object_id)); - } - }; - - let mut result = std::collections::HashMap::new(); - result.insert("Result".to_string(), didl_result); - result.insert("NumberReturned".to_string(), number_returned.to_string()); - result.insert("TotalMatches".to_string(), total_matches.to_string()); - result.insert("UpdateID".to_string(), "0".to_string()); - - Ok(result) -} - -/// Create the root container -fn create_root_container() -> DIDLObject { - let mut container = DIDLContainer::new("0".to_string(), "-1".to_string()); - container.set_title("Radio Paradise".to_string()); - container.set_class("object.container.storageFolder".to_string()); - container.set_searchable(false); - container.set_child_count(Some(1)); - DIDLObject::Container(container) -} - -/// Create a container for a block -fn create_block_container(block: &crate::models::Block) -> DIDLObject { - let mut container = DIDLContainer::new(format!("block:{}", block.event), "0".to_string()); - container.set_title(format!( - "Block {} ({} songs)", - block.event, - block.song_count() - )); - container.set_class("object.container.album.musicAlbum".to_string()); - container.set_searchable(false); - container.set_child_count(Some(block.song_count())); - - // Add album art if available - if let Some(first_song) = block.get_song(0) { - if let Some(cover) = &first_song.cover { - if let Some(cover_url) = block.cover_url(cover) { - container.add_album_art_uri(cover_url); - } - } - } - - DIDLObject::Container(container) -} - -/// Create an item for a song -fn create_song_item( - block: &crate::models::Block, - index: usize, - song: &crate::models::Song, -) -> DIDLObject { - let mut item = DIDLItem::new( - format!("block:{}:song:{}", block.event, index), - format!("block:{}", block.event), - ); - - item.set_title(song.title.clone()); - item.set_class("object.item.audioItem.musicTrack".to_string()); - - // Add metadata - item.add_artist(song.artist.clone()); - if let Some(ref album) = song.album { - item.add_album(album.clone()); - } - - if let Some(year) = song.year { - item.set_date(format!("{}-01-01", year)); - } - - // Add album art - if let Some(cover) = &song.cover { - if let Some(cover_url) = block.cover_url(cover) { - item.add_album_art_uri(cover_url); - } - } - - // Add resource for streaming - let mut resource = Resource::new(block.url.clone()); - resource.set_protocol_info("http-get:*:audio/flac:*".to_string()); - resource.set_duration(format_duration(song.duration)); - resource.set_size(None); // Unknown size - - item.add_resource(resource); - - DIDLObject::Item(item) -} - -/// Format duration in H:MM:SS format -fn format_duration(duration_ms: u64) -> String { - let total_seconds = duration_ms / 1000; - let hours = total_seconds / 3600; - let minutes = (total_seconds % 3600) / 60; - let seconds = total_seconds % 60; - format!("{}:{:02}:{:02}", hours, minutes, seconds) -} - -/// Serialize DIDL objects to XML string -fn serialize_didl(objects: &[DIDLObject]) -> String { - let mut didl = String::from( - r#""#, - ); - - for obj in objects { - didl.push_str(&obj.to_didl()); - } - - didl.push_str(""); - didl -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_duration() { - assert_eq!(format_duration(0), "0:00:00"); - assert_eq!(format_duration(60000), "0:01:00"); - assert_eq!(format_duration(3661000), "1:01:01"); - } - - #[test] - fn test_create_root_container() { - let root = create_root_container(); - if let DIDLObject::Container(container) = root { - assert_eq!(container.id(), "0"); - assert_eq!(container.parent_id(), "-1"); - } else { - panic!("Expected Container"); - } - } -} diff --git a/pmoparadise/src/mediaserver/mod.rs b/pmoparadise/src/mediaserver/mod.rs deleted file mode 100644 index ed043c80..00000000 --- a/pmoparadise/src/mediaserver/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! UPnP Media Server for Radio Paradise -//! -//! This module provides a UPnP/DLNA Media Server implementation that exposes -//! Radio Paradise blocks and songs as a browsable media library. -//! -//! # Features -//! -//! - ContentDirectory service for browsing blocks and songs -//! - ConnectionManager service for protocol info -//! - DIDL-Lite metadata for songs -//! - Support for multiple quality levels -//! - Live streaming URLs -//! -//! # Architecture -//! -//! ```text -//! RadioParadiseMediaServer -//! └── Device (urn:schemas-upnp-org:device:MediaServer:1) -//! ├── ContentDirectory service -//! │ ├── Browse action -//! │ ├── Search action (optional) -//! │ └── GetSearchCapabilities -//! └── ConnectionManager service -//! ├── GetProtocolInfo -//! └── GetCurrentConnectionIDs -//! ``` -//! -//! # Example -//! -//! ```no_run -//! # #[cfg(feature = "mediaserver")] -//! # { -//! use pmoparadise::mediaserver::RadioParadiseMediaServer; -//! -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! let server = RadioParadiseMediaServer::builder() -//! .with_friendly_name("Radio Paradise FLAC") -//! .build() -//! .await?; -//! -//! server.run().await?; -//! Ok(()) -//! } -//! # } -//! ``` - -#[cfg(feature = "mediaserver")] -mod connection_manager; -#[cfg(feature = "mediaserver")] -mod content_directory; -#[cfg(feature = "mediaserver")] -mod server; - -#[cfg(feature = "mediaserver")] -pub use server::{MediaServerBuilder, RadioParadiseMediaServer}; diff --git a/pmoparadise/src/mediaserver/server.rs b/pmoparadise/src/mediaserver/server.rs deleted file mode 100644 index 9274ba2e..00000000 --- a/pmoparadise/src/mediaserver/server.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Radio Paradise UPnP Media Server implementation - -use crate::error::{Error, Result}; -use crate::RadioParadiseClient; -use pmoserver::Server; -use pmoupnp::devices::Device; -use pmoupnp::UpnpServer; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Radio Paradise UPnP Media Server -/// -/// Exposes Radio Paradise blocks and songs as a browsable UPnP media library. -pub struct RadioParadiseMediaServer { - server: Server, - client: Arc>, - device_udn: String, -} - -impl RadioParadiseMediaServer { - /// Create a new builder for the media server - pub fn builder() -> MediaServerBuilder { - MediaServerBuilder::default() - } - - /// Create a new media server with default settings - pub async fn new() -> Result { - Self::builder().build().await - } - - /// Run the media server - /// - /// This will start the HTTP server and SSDP announcements. - pub async fn run(self) -> Result<()> { - self.server - .run() - .await - .map_err(|e| Error::other(format!("Server error: {}", e))) - } - - /// Get the device UDN - pub fn udn(&self) -> &str { - &self.device_udn - } - - /// Get the Radio Paradise client - pub fn client(&self) -> Arc> { - self.client.clone() - } -} - -/// Builder for RadioParadiseMediaServer -pub struct MediaServerBuilder { - friendly_name: String, - manufacturer: String, - model_name: String, - channel: u8, - port: u16, -} - -impl Default for MediaServerBuilder { - fn default() -> Self { - Self { - friendly_name: "Radio Paradise Media Server".to_string(), - manufacturer: "PMOMusic".to_string(), - model_name: "Radio Paradise Adapter".to_string(), - channel: 0, - port: 8080, - } - } -} - -impl MediaServerBuilder { - /// Create a new builder with default settings - pub fn new() -> Self { - Self::default() - } - - /// Set the friendly name for the device - pub fn with_friendly_name(mut self, name: impl Into) -> Self { - self.friendly_name = name.into(); - self - } - - /// Set the manufacturer name - pub fn with_manufacturer(mut self, name: impl Into) -> Self { - self.manufacturer = name.into(); - self - } - - /// Set the model name - pub fn with_model_name(mut self, name: impl Into) -> Self { - self.model_name = name.into(); - self - } - - /// Set the Radio Paradise channel (0=main, 1=mellow, 2=rock, 3=world) - pub fn with_channel(mut self, channel: u8) -> Self { - self.channel = channel; - self - } - - /// Set the HTTP server port - pub fn with_port(mut self, port: u16) -> Self { - self.port = port; - self - } - - /// Build the media server - pub async fn build(self) -> Result { - // Create Radio Paradise client - let client = RadioParadiseClient::builder() - .channel(self.channel) - .build() - .await?; - - let client = Arc::new(RwLock::new(client)); - - // Create HTTP server - let mut server = pmoserver::ServerBuilder::new() - .with_port(self.port) - .build() - .map_err(|e| Error::other(format!("Failed to create server: {}", e)))?; - - // Create UPnP device - let device_udn = format!("uuid:{}", uuid::Uuid::new_v4()); - - let mut device = Device::new( - "MediaServer".to_string(), - "MediaServer".to_string(), - self.friendly_name.clone(), - ); - - device.set_manufacturer(self.manufacturer); - device.set_model_name(self.model_name); - device.set_udn(device_udn.clone()); - - // Add ContentDirectory service - let content_directory = - super::content_directory::create_content_directory_service(client.clone()); - device - .add_service(Arc::new(content_directory)) - .map_err(|e| Error::other(format!("Failed to add ContentDirectory: {:?}", e)))?; - - // Add ConnectionManager service - let connection_manager = super::connection_manager::create_connection_manager_service(); - device - .add_service(Arc::new(connection_manager)) - .map_err(|e| Error::other(format!("Failed to add ConnectionManager: {:?}", e)))?; - - // Register device with server - server - .register_device(Arc::new(device)) - .await - .map_err(|e| Error::other(format!("Failed to register device: {:?}", e)))?; - - Ok(RadioParadiseMediaServer { - server, - client, - device_udn, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_builder_defaults() { - let builder = MediaServerBuilder::default(); - assert_eq!(builder.friendly_name, "Radio Paradise Media Server"); - assert_eq!(builder.channel, 0); - assert_eq!(builder.port, 8080); - } - - #[test] - fn test_builder_customization() { - let builder = MediaServerBuilder::new() - .with_friendly_name("Custom Server") - .with_channel(1) - .with_port(9090); - - assert_eq!(builder.friendly_name, "Custom Server"); - assert_eq!(builder.channel, 1); - assert_eq!(builder.port, 9090); - } -}