diff --git a/.gitignore b/.gitignore index a239b6f5..6c6f3612 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ xxx xx all.txt pmo_src.txt -upmpdcli/ \ No newline at end of file +upmpdcli/ +/*.xml diff --git a/headers.txt b/headers.txt new file mode 100644 index 00000000..8398ae15 --- /dev/null +++ b/headers.txt @@ -0,0 +1,5 @@ +HTTP/1.1 500 Internal Server Error +content-type: text/xml; charset="utf-8" +content-length: 597 +date: Sun, 19 Oct 2025 19:06:41 GMT + diff --git a/pmocovers/src/db.rs b/pmocovers/src/db.rs new file mode 100644 index 00000000..64767b9d --- /dev/null +++ b/pmocovers/src/db.rs @@ -0,0 +1,7 @@ +//! Module de compatibilité pour l'ancien module db +//! +//! Ce module réexporte les types de `pmocache::db` pour maintenir +//! la compatibilité avec l'API existante. + +// Réexporter les types de pmocache +pub use pmocache::db::{CacheEntry, DB}; diff --git a/pmomediaserver/src/content_handler.rs b/pmomediaserver/src/content_handler.rs index d7a9c434..f1ea8f2a 100644 --- a/pmomediaserver/src/content_handler.rs +++ b/pmomediaserver/src/content_handler.rs @@ -12,7 +12,7 @@ use pmodidl::{Container, DIDLLite}; use pmosource::api::{get_source as get_source_from_registry, list_all_sources}; -use pmosource::{BrowseResult, MusicSource}; +use pmosource::{BrowseResult, MusicSource, MusicSourceError}; use std::sync::Arc; /// Convertit des containers et items en XML DIDL-Lite @@ -113,40 +113,52 @@ impl ContentHandler { } // Sinon, chercher dans les sources + let mut non_not_found_error: Option = None; for source in list_all_sources().await { - if let Ok(result) = source.browse(object_id).await { - // L'objet a été trouvé, retourner ses métadonnées - match result { - BrowseResult::Containers(containers) => { - if let Some(container) = containers.first() { - let didl = to_didl_lite(&[container.clone()], &[])?; - let update_id = source.update_id().await; - return Ok((didl, 1, 1, update_id)); - } - } - BrowseResult::Items(items) => { - if let Some(item) = items.first() { - let didl = to_didl_lite(&[], &[item.clone()])?; - let update_id = source.update_id().await; - return Ok((didl, 1, 1, update_id)); - } - } - BrowseResult::Mixed { containers, items } => { - if let Some(container) = containers.first() { - let didl = to_didl_lite(&[container.clone()], &[])?; - let update_id = source.update_id().await; - return Ok((didl, 1, 1, update_id)); - } else if let Some(item) = items.first() { - let didl = to_didl_lite(&[], &[item.clone()])?; - let update_id = source.update_id().await; - return Ok((didl, 1, 1, update_id)); + match source.browse(object_id).await { + Ok(result) => { + // L'objet a été trouvé, retourner ses métadonnées + match result { + BrowseResult::Containers(containers) => { + if let Some(container) = containers.first() { + let didl = to_didl_lite(&[container.clone()], &[])?; + let update_id = source.update_id().await; + return Ok((didl, 1, 1, update_id)); + } + } + BrowseResult::Items(items) => { + if let Some(item) = items.first() { + let didl = to_didl_lite(&[], &[item.clone()])?; + let update_id = source.update_id().await; + return Ok((didl, 1, 1, update_id)); + } + } + BrowseResult::Mixed { containers, items } => { + if let Some(container) = containers.first() { + let didl = to_didl_lite(&[container.clone()], &[])?; + let update_id = source.update_id().await; + return Ok((didl, 1, 1, update_id)); + } else if let Some(item) = items.first() { + let didl = to_didl_lite(&[], &[item.clone()])?; + let update_id = source.update_id().await; + return Ok((didl, 1, 1, update_id)); + } } } } + Err(MusicSourceError::ObjectNotFound(_)) => continue, + Err(e) => { + non_not_found_error = Some(e.to_string()); + break; + } } } - Err(format!("Object not found: {}", object_id)) + if let Some(err) = non_not_found_error { + Err(format!("Browse failed: {}", err)) + } else { + Err(format!("Object not found: {}", object_id)) + } } } @@ -170,15 +182,27 @@ impl ContentHandler { } // Sinon, chercher dans les sources + let mut non_not_found_error: Option = None; for source in list_all_sources().await { - if let Ok(result) = source.browse(object_id).await { - return self - .browse_result_to_didl(result, source, starting_index, requested_count) - .await; + match source.browse(object_id).await { + Ok(result) => { + return self + .browse_result_to_didl(result, source, starting_index, requested_count) + .await; + } + Err(MusicSourceError::ObjectNotFound(_)) => continue, + Err(e) => { + non_not_found_error = Some(e.to_string()); + break; + } } } - Err(format!("Container not found: {}", object_id)) + if let Some(err) = non_not_found_error { + Err(format!("Browse failed: {}", err)) + } else { + Err(format!("Container not found: {}", object_id)) + } } /// Browse la racine (liste toutes les sources) diff --git a/pmomediaserver/src/contentdirectory/handlers.rs b/pmomediaserver/src/contentdirectory/handlers.rs index b80d307a..2d98ffc9 100644 --- a/pmomediaserver/src/contentdirectory/handlers.rs +++ b/pmomediaserver/src/contentdirectory/handlers.rs @@ -56,10 +56,34 @@ pub fn browse_handler() -> ActionHandler { let object_id: String = get!(&data, "ObjectID", String); let browse_flag: String = get!(&data, "BrowseFlag", String); - let starting_index: u32 = get!(&data, "StartingIndex", u32); - let requested_count: u32 = get!(&data, "RequestedCount", u32); - let _filter: String = get!(&data, "Filter", String); - let _sort_criteria: String = get!(&data, "SortCriteria", String); + + let starting_index: u32 = get!( + &data, + "StartingIndex", + u32, + "ContentDirectory::Browse misconfigured: 'StartingIndex' missing or not bound" + ); + + let requested_count: u32 = get!( + &data, + "RequestedCount", + u32, + "ContentDirectory::Browse misconfigured: 'RequestedCount' missing or not bound" + ); + + let _filter: String = get!( + &data, + "Filter", + String, + "ContentDirectory::Browse misconfigured: 'Filter' missing or not bound" + ); + + let _sort_criteria: String = get!( + &data, + "SortCriteria", + String, + "ContentDirectory::Browse misconfigured: 'SortCriteria' missing or not bound" + ); info!( "📂 Browse requested: object_id={} flag={} start={} count={}", diff --git a/pmoparadise/src/client.rs b/pmoparadise/src/client.rs index b84c7260..a2f9b0c2 100644 --- a/pmoparadise/src/client.rs +++ b/pmoparadise/src/client.rs @@ -105,6 +105,21 @@ impl RadioParadiseClient { cloned } + /// Clone the client with a different bitrate while preserving other settings. + pub fn clone_with_bitrate(&self, bitrate: Bitrate) -> Self { + let mut cloned = self.clone(); + cloned.bitrate = bitrate; + cloned.next_block_url = None; + cloned + } + + /// Clone the client with an updated channel and bitrate. + pub fn clone_with_channel_and_bitrate(&self, channel: u8, bitrate: Bitrate) -> Self { + let mut cloned = self.clone_with_channel(channel); + cloned.bitrate = bitrate; + cloned + } + /// Get a block by event ID /// /// If `event` is None, returns the current block. diff --git a/pmoparadise/src/pmoserver_ext.rs b/pmoparadise/src/pmoserver_ext.rs index a559676d..051f25c6 100644 --- a/pmoparadise/src/pmoserver_ext.rs +++ b/pmoparadise/src/pmoserver_ext.rs @@ -3,9 +3,9 @@ //! Ce module fournit un trait d'extension pour ajouter facilement l'API Radio Paradise //! à un serveur pmoserver. -use crate::{Block, NowPlaying, RadioParadiseClient}; +use crate::{models::Bitrate, Block, NowPlaying, RadioParadiseClient}; use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, routing::get, Json, Router, @@ -21,6 +21,15 @@ pub struct RadioParadiseState { client: Arc>, } +const MAX_CHANNEL_ID: u8 = 3; + +#[derive(Debug, Default, Deserialize)] +#[serde(default)] +struct ParadiseQuery { + channel: Option, + bitrate: Option, +} + impl RadioParadiseState { pub async fn new() -> anyhow::Result { let client = RadioParadiseClient::new() @@ -30,6 +39,36 @@ impl RadioParadiseState { client: Arc::new(RwLock::new(client)), }) } + + async fn client_for_params( + &self, + params: &ParadiseQuery, + ) -> Result { + let base_client = { + let client_guard = self.client.read().await; + client_guard.clone() + }; + + let mut client = base_client; + + if let Some(channel) = params.channel { + if channel > MAX_CHANNEL_ID { + tracing::warn!("Invalid Radio Paradise channel requested: {}", channel); + return Err(StatusCode::BAD_REQUEST); + } + client = client.clone_with_channel(channel); + } + + if let Some(bitrate_id) = params.bitrate { + let bitrate = Bitrate::from_u8(bitrate_id).map_err(|e| { + tracing::warn!("Invalid Radio Paradise bitrate requested: {}", e); + StatusCode::BAD_REQUEST + })?; + client = client.clone_with_bitrate(bitrate); + } + + Ok(client) + } } /// Information sur un canal Radio Paradise @@ -186,8 +225,9 @@ impl From for NowPlayingResponse { )] async fn get_now_playing( State(state): State, + Query(params): Query, ) -> Result, StatusCode> { - let client = state.client.read().await; + let client = state.client_for_params(¶ms).await?; let now_playing = client.now_playing().await.map_err(|e| { tracing::error!("Failed to fetch now playing from Radio Paradise: {}", e); StatusCode::INTERNAL_SERVER_ERROR @@ -208,8 +248,9 @@ async fn get_now_playing( )] async fn get_current_block( State(state): State, + Query(params): Query, ) -> Result, StatusCode> { - let client = state.client.read().await; + let client = state.client_for_params(¶ms).await?; let block = client.get_block(None).await.map_err(|e| { tracing::error!("Failed to fetch current block from Radio Paradise: {}", e); StatusCode::INTERNAL_SERVER_ERROR @@ -234,8 +275,9 @@ async fn get_current_block( async fn get_block_by_id( State(state): State, Path(event_id): Path, + Query(params): Query, ) -> Result, StatusCode> { - let client = state.client.read().await; + let client = state.client_for_params(¶ms).await?; let block = client.get_block(Some(event_id)).await.map_err(|e| { tracing::error!( "Failed to fetch block {} from Radio Paradise: {}", diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs index 2abbcff3..733accbe 100644 --- a/pmoupnp/src/actions/action_instance.rs +++ b/pmoupnp/src/actions/action_instance.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + env::var, sync::Arc, }; @@ -153,8 +154,13 @@ impl ActionInstance { for (arg_name, state_value) in soap_data.iter() { if let Some(arg_inst) = self.argument(arg_name) { - if arg_inst.get_model().is_in() { - action_data.insert(arg_name.clone(), state_value.to_reflect()); + if arg_inst.is_in() { + if let Some(var_inst) = arg_inst.get_variable_instance() { + action_data + .insert(arg_name.clone(), var_inst.parse_value(state_value.clone())); + } else { + action_data.insert(arg_name.clone(), state_value.to_reflect()); + } updated.insert(arg_name.clone()); } } diff --git a/pmoupnp/src/actions/action_methods.rs b/pmoupnp/src/actions/action_methods.rs index ff60b6f1..236c336c 100644 --- a/pmoupnp/src/actions/action_methods.rs +++ b/pmoupnp/src/actions/action_methods.rs @@ -56,16 +56,16 @@ impl Action { /// Il peut être remplacé via [`set_handler`](Self::set_handler). fn default_handler() -> ActionHandler { action_handler!(|data| { - info!("🎬 Action called with default handler"); - + let mut s = String::new(); // Logger les arguments for (key, value) in data.iter() { - trace!( - " {} = {}", + s.push_str(&format![ + "- {} = {}\n", key, crate::actions::reflect_to_string(value.as_ref()) - ); + ]); } + info!("🎬 Action called with default handler\n\n{}", s); // Retourner les données telles quelles Ok(data) diff --git a/pmoupnp/src/actions/arg_instance_methods.rs b/pmoupnp/src/actions/arg_instance_methods.rs index 390d9225..73429929 100644 --- a/pmoupnp/src/actions/arg_instance_methods.rs +++ b/pmoupnp/src/actions/arg_instance_methods.rs @@ -247,6 +247,10 @@ impl ArgumentInstance { pub fn get_variable_instance(&self) -> Option> { self.variable_instance.read().unwrap().clone() } + + pub fn is_in(&self) -> bool { + self.model.is_in() + } } impl UpnpInstance for ActionInstanceSet { diff --git a/pmoupnp/src/actions/handler_helpers.rs b/pmoupnp/src/actions/handler_helpers.rs index a42e9ed0..fa3d2658 100644 --- a/pmoupnp/src/actions/handler_helpers.rs +++ b/pmoupnp/src/actions/handler_helpers.rs @@ -195,6 +195,16 @@ macro_rules! get { ($data:expr, $key:expr, $type:ty) => { $crate::actions::get_value::<$type>($data, $key)? }; + ($data:expr, $key:expr, $type:ty, $($msg:tt)+) => {{ + match $crate::actions::get_value::<$type>($data, $key) { + Ok(value) => value, + Err(_) => { + let message = format!($($msg)+); + tracing::error!("{}", message); + return Err($crate::actions::ActionError::ArgumentError(message)); + } + } + }}; } /// Macro pour insérer facilement une valeur dans ActionData. diff --git a/pmoupnp/src/ssdp/server.rs b/pmoupnp/src/ssdp/server.rs index 7df4bf43..2982634f 100644 --- a/pmoupnp/src/ssdp/server.rs +++ b/pmoupnp/src/ssdp/server.rs @@ -312,10 +312,9 @@ impl SsdpServer { ); match socket.send_to(resp.as_bytes(), src) { Ok(_) => { - info!("📡 M-SEARCH response sent to {} with ST={}", src, nt); debug!( - "📡 M-SEARCH response payload\n
\n\n```\n{}\n```\n
\n", - resp + "📡 M-SEARCH response sent to {} with ST={}\n\n### payload\n\n
\n\n```\n{}\n```\n
\n", + src, nt, resp ); } Err(e) => warn!("❌ Failed to send M-SEARCH response to {}: {}", src, e), diff --git a/pmoupnp/src/state_variables/instance_methods.rs b/pmoupnp/src/state_variables/instance_methods.rs index dab1176b..009afaf6 100644 --- a/pmoupnp/src/state_variables/instance_methods.rs +++ b/pmoupnp/src/state_variables/instance_methods.rs @@ -243,13 +243,16 @@ impl StateVarInstance { /// /// Un `Box` contenant la valeur actuelle pub fn to_reflect(&self) -> Box { - use crate::variable_types::StateVarType; - let current_value = self.value.read().unwrap().clone(); + self.parse_value(current_value) + } + + pub fn parse_value(&self, value: StateValue) -> Box { + use crate::variable_types::StateVarType; // Parser uniquement pour les String if self.as_state_var_type() == StateVarType::String { - if let StateValue::String(ref s) = current_value { + if let StateValue::String(ref s) = value { if let Some(ref parser) = self.model.parse { match parser(s) { Ok(reflected) => return reflected, @@ -266,10 +269,8 @@ impl StateVarInstance { } } - // Conversion standard pour tous les autres types - current_value.to_reflect() + value.to_reflect() } - /// Définit la valeur depuis Box /// /// - Si type String ET marshal défini : utilise le marshal