diff --git a/pmomediaserver/src/contentdirectory/actions/browse.rs b/pmomediaserver/src/contentdirectory/actions/browse.rs index 0177e20b..419d0da4 100644 --- a/pmomediaserver/src/contentdirectory/actions/browse.rs +++ b/pmomediaserver/src/contentdirectory/actions/browse.rs @@ -4,6 +4,7 @@ use crate::contentdirectory::variables::{ A_ARG_TYPE_RESULT, A_ARG_TYPE_UPDATEID, }; use pmoupnp::define_action; +use crate::contentdirectory::handlers; define_action! { pub static BROWSE = "Browse" { @@ -18,4 +19,5 @@ define_action! { out "TotalMatches" => A_ARG_TYPE_COUNT, out "UpdateID" => A_ARG_TYPE_UPDATEID, } + with handler handlers::browse_handler() } diff --git a/pmomediaserver/src/contentdirectory/actions/getsearchcapabilities.rs b/pmomediaserver/src/contentdirectory/actions/getsearchcapabilities.rs index 9da4d5d0..6842cb73 100644 --- a/pmomediaserver/src/contentdirectory/actions/getsearchcapabilities.rs +++ b/pmomediaserver/src/contentdirectory/actions/getsearchcapabilities.rs @@ -1,8 +1,10 @@ use crate::contentdirectory::variables::SEARCHCAPABILITIES; use pmoupnp::define_action; +use crate::contentdirectory::handlers; define_action! { pub static GETSEARCHCAPABILITIES = "GetSearchCapabilities" { out "SearchCaps" => SEARCHCAPABILITIES, } + with handler handlers::get_search_capabilities_handler() } diff --git a/pmomediaserver/src/contentdirectory/actions/getsortcapabilities.rs b/pmomediaserver/src/contentdirectory/actions/getsortcapabilities.rs index dbad6c74..0a30f7b9 100644 --- a/pmomediaserver/src/contentdirectory/actions/getsortcapabilities.rs +++ b/pmomediaserver/src/contentdirectory/actions/getsortcapabilities.rs @@ -1,8 +1,10 @@ use crate::contentdirectory::variables::SORTCAPABILITIES; use pmoupnp::define_action; +use crate::contentdirectory::handlers; define_action! { pub static GETSORTCAPABILITIES = "GetSortCapabilities" { out "SortCaps" => SORTCAPABILITIES, } + with handler handlers::get_sort_capabilities_handler() } diff --git a/pmomediaserver/src/contentdirectory/actions/getsystemupdateid.rs b/pmomediaserver/src/contentdirectory/actions/getsystemupdateid.rs index 76db9134..b0d82eb2 100644 --- a/pmomediaserver/src/contentdirectory/actions/getsystemupdateid.rs +++ b/pmomediaserver/src/contentdirectory/actions/getsystemupdateid.rs @@ -1,8 +1,10 @@ use crate::contentdirectory::variables::SYSTEMUPDATEID; use pmoupnp::define_action; +use crate::contentdirectory::handlers; define_action! { pub static GETSYSTEMUPDATEID = "GetSystemUpdateID" { out "Id" => SYSTEMUPDATEID, } + with handler handlers::get_system_update_id_handler() } diff --git a/pmomediaserver/src/contentdirectory/actions/search.rs b/pmomediaserver/src/contentdirectory/actions/search.rs index ea1c0159..826a687a 100644 --- a/pmomediaserver/src/contentdirectory/actions/search.rs +++ b/pmomediaserver/src/contentdirectory/actions/search.rs @@ -4,6 +4,7 @@ use crate::contentdirectory::variables::{ A_ARG_TYPE_RESULT, A_ARG_TYPE_UPDATEID, }; use pmoupnp::define_action; +use crate::contentdirectory::handlers; define_action! { pub static SEARCH = "Search" { @@ -18,4 +19,5 @@ define_action! { out "TotalMatches" => A_ARG_TYPE_COUNT, out "UpdateID" => A_ARG_TYPE_UPDATEID, } + with handler handlers::search_handler() } diff --git a/pmomediaserver/src/contentdirectory/handlers.rs b/pmomediaserver/src/contentdirectory/handlers.rs new file mode 100644 index 00000000..bf9b5efb --- /dev/null +++ b/pmomediaserver/src/contentdirectory/handlers.rs @@ -0,0 +1,309 @@ +//! # Handlers pour les actions ContentDirectory +//! +//! Ce module implémente les handlers UPnP pour les actions du service ContentDirectory. +//! Chaque handler fait le pont entre l'API UPnP et la logique métier dans [`ContentHandler`]. +//! +//! ## Architecture +//! +//! ```text +//! UPnP Action (XML) +//! ↓ +//! Handler (ce module) - extraction des paramètres +//! ↓ +//! ContentHandler - logique métier +//! ↓ +//! Sources musicales +//! ``` +//! +//! ## Handlers implémentés +//! +//! - [`browse_handler`] : Navigation dans la hiérarchie de contenu +//! - [`search_handler`] : Recherche dans les sources +//! - [`get_search_capabilities_handler`] : Capacités de recherche supportées +//! - [`get_sort_capabilities_handler`] : Capacités de tri supportées +//! - [`get_system_update_id_handler`] : ID de mise à jour du système + +use pmoupnp::action_handler; +use pmoupnp::actions::{ActionHandler, ActionError}; +use pmoupnp::variable_types::StateValue; +use crate::content_handler::ContentHandler; +use tracing::{debug, error}; + +/// Handler pour l'action Browse. +/// +/// Navigue dans la hiérarchie de contenu (containers et items). +/// +/// # Arguments UPnP +/// +/// - `ObjectID` : ID de l'objet à parcourir ("0" pour la racine) +/// - `BrowseFlag` : "BrowseMetadata" ou "BrowseDirectChildren" +/// - `Filter` : Filtre de propriétés (non utilisé actuellement) +/// - `StartingIndex` : Index de départ pour la pagination +/// - `RequestedCount` : Nombre d'éléments demandés (0 = tous) +/// - `SortCriteria` : Critères de tri (non utilisé actuellement) +/// +/// # Retours UPnP +/// +/// - `Result` : XML DIDL-Lite contenant les résultats +/// - `NumberReturned` : Nombre d'éléments retournés +/// - `TotalMatches` : Nombre total d'éléments +/// - `UpdateID` : ID de mise à jour +pub fn browse_handler() -> ActionHandler { + action_handler!(|instance| { + debug!("📂 Browse handler called"); + + let handler = ContentHandler::new(); + + // Extraire les arguments d'entrée + let object_id = match instance + .argument("ObjectID") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("ObjectID not found".to_string()))? + .value() + { + StateValue::String(s) => s, + _ => return Err(ActionError::ArgumentError("ObjectID must be a string".to_string())), + }; + + let browse_flag = match instance + .argument("BrowseFlag") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("BrowseFlag not found".to_string()))? + .value() + { + StateValue::String(s) => s, + _ => return Err(ActionError::ArgumentError("BrowseFlag must be a string".to_string())), + }; + + let starting_index = match instance + .argument("StartingIndex") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("StartingIndex not found".to_string()))? + .value() + { + StateValue::UI4(n) => n, + _ => return Err(ActionError::ArgumentError("StartingIndex must be ui4".to_string())), + }; + + let requested_count = match instance + .argument("RequestedCount") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("RequestedCount not found".to_string()))? + .value() + { + StateValue::UI4(n) => n, + _ => return Err(ActionError::ArgumentError("RequestedCount must be ui4".to_string())), + }; + + // Appeler la logique métier + let (didl, returned, total, update_id) = handler + .browse(&object_id, &browse_flag, starting_index, requested_count) + .await + .map_err(|e| { + error!("Browse failed: {}", e); + ActionError::GeneralError(e) + })?; + + // Définir les arguments de sortie + if let Some(arg) = instance.argument("Result") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::String(didl)).await; + } + } + + if let Some(arg) = instance.argument("NumberReturned") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(returned)).await; + } + } + + if let Some(arg) = instance.argument("TotalMatches") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(total)).await; + } + } + + if let Some(arg) = instance.argument("UpdateID") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(update_id)).await; + } + } + + debug!("✅ Browse completed: returned={}, total={}", returned, total); + Ok(()) + }) +} + +/// Handler pour l'action Search. +/// +/// Recherche du contenu dans les sources qui supportent la recherche. +/// +/// # Arguments UPnP +/// +/// - `ContainerID` : ID du container dans lequel rechercher +/// - `SearchCriteria` : Critères de recherche UPnP +/// - `Filter` : Filtre de propriétés (non utilisé) +/// - `StartingIndex` : Index de départ +/// - `RequestedCount` : Nombre demandé +/// - `SortCriteria` : Critères de tri (non utilisé) +/// +/// # Retours UPnP +/// +/// - `Result` : XML DIDL-Lite +/// - `NumberReturned` : Nombre retourné +/// - `TotalMatches` : Total +/// - `UpdateID` : ID de mise à jour +pub fn search_handler() -> ActionHandler { + action_handler!(|instance| { + debug!("🔍 Search handler called"); + + let handler = ContentHandler::new(); + + let container_id = match instance + .argument("ContainerID") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("ContainerID not found".to_string()))? + .value() + { + StateValue::String(s) => s, + _ => return Err(ActionError::ArgumentError("ContainerID must be a string".to_string())), + }; + + let search_criteria = match instance + .argument("SearchCriteria") + .and_then(|arg| arg.get_variable_instance()) + .ok_or_else(|| ActionError::ArgumentError("SearchCriteria not found".to_string()))? + .value() + { + StateValue::String(s) => s, + _ => return Err(ActionError::ArgumentError("SearchCriteria must be a string".to_string())), + }; + + let (didl, returned, total, update_id) = handler + .search(&container_id, &search_criteria) + .await + .map_err(|e| { + error!("Search failed: {}", e); + ActionError::GeneralError(e) + })?; + + // Définir les sorties + if let Some(arg) = instance.argument("Result") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::String(didl)).await; + } + } + + if let Some(arg) = instance.argument("NumberReturned") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(returned)).await; + } + } + + if let Some(arg) = instance.argument("TotalMatches") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(total)).await; + } + } + + if let Some(arg) = instance.argument("UpdateID") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(update_id)).await; + } + } + + debug!("✅ Search completed: returned={}, total={}", returned, total); + Ok(()) + }) +} + +/// Handler pour GetSearchCapabilities. +/// +/// Retourne les capacités de recherche supportées. +/// +/// # Retours UPnP +/// +/// - `SearchCaps` : Chaîne de capacités séparées par virgules +pub fn get_search_capabilities_handler() -> ActionHandler { + action_handler!(|instance| { + debug!("🔍 GetSearchCapabilities handler called"); + + let handler = ContentHandler::new(); + let capabilities = handler.get_search_capabilities().await; + + if let Some(arg) = instance.argument("SearchCaps") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::String(capabilities.clone())).await; + } + } + + debug!("✅ SearchCapabilities: {}", capabilities); + Ok(()) + }) +} + +/// Handler pour GetSortCapabilities. +/// +/// Retourne les capacités de tri supportées. +/// +/// # Retours UPnP +/// +/// - `SortCaps` : Chaîne de capacités séparées par virgules +pub fn get_sort_capabilities_handler() -> ActionHandler { + action_handler!(|instance| { + debug!("📊 GetSortCapabilities handler called"); + + let handler = ContentHandler::new(); + let capabilities = handler.get_sort_capabilities().await; + + if let Some(arg) = instance.argument("SortCaps") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::String(capabilities.clone())).await; + } + } + + debug!("✅ SortCapabilities: {}", capabilities); + Ok(()) + }) +} + +/// Handler pour GetSystemUpdateID. +/// +/// Retourne l'ID de mise à jour global du système. +/// Cet ID change quand le contenu disponible change. +/// +/// # Retours UPnP +/// +/// - `Id` : ID de mise à jour (entier non signé) +pub fn get_system_update_id_handler() -> ActionHandler { + action_handler!(|instance| { + debug!("🔄 GetSystemUpdateID handler called"); + + let handler = ContentHandler::new(); + let update_id = handler.get_system_update_id().await; + + if let Some(arg) = instance.argument("Id") { + if let Some(var) = arg.get_variable_instance() { + var.set_value(StateValue::UI4(update_id)).await; + } + } + + debug!("✅ SystemUpdateID: {}", update_id); + Ok(()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_handlers_creation() { + // Vérifier que tous les handlers se créent sans erreur + let _ = browse_handler(); + let _ = search_handler(); + let _ = get_search_capabilities_handler(); + let _ = get_sort_capabilities_handler(); + let _ = get_system_update_id_handler(); + } +} diff --git a/pmomediaserver/src/contentdirectory/mod.rs b/pmomediaserver/src/contentdirectory/mod.rs index 4ac8a3fe..e95d770f 100644 --- a/pmomediaserver/src/contentdirectory/mod.rs +++ b/pmomediaserver/src/contentdirectory/mod.rs @@ -76,6 +76,7 @@ use pmoupnp::define_service; pub mod variables; pub mod actions; +pub mod handlers; use actions::{ BROWSE, SEARCH, GETSEARCHCAPABILITIES, GETSORTCAPABILITIES, GETSYSTEMUPDATEID diff --git a/pmomediaserver/src/contentdirectory/variables/a_arg_type_browseflag.rs b/pmomediaserver/src/contentdirectory/variables/a_arg_type_browseflag.rs index 145fc95a..8e6077ca 100644 --- a/pmomediaserver/src/contentdirectory/variables/a_arg_type_browseflag.rs +++ b/pmomediaserver/src/contentdirectory/variables/a_arg_type_browseflag.rs @@ -2,6 +2,7 @@ use pmoupnp::define_variable; define_variable! { pub static A_ARG_TYPE_BROWSEFLAG: String = "A_ARG_TYPE_BrowseFlag" { + default: "BrowseDirectChildren", allowed: ["BrowseMetadata", "BrowseDirectChildren"], evented: false, } diff --git a/pmomediaserver/src/contentdirectory/variables/a_arg_type_filter.rs b/pmomediaserver/src/contentdirectory/variables/a_arg_type_filter.rs index 0ef8dafa..c923abbf 100644 --- a/pmomediaserver/src/contentdirectory/variables/a_arg_type_filter.rs +++ b/pmomediaserver/src/contentdirectory/variables/a_arg_type_filter.rs @@ -2,6 +2,7 @@ use pmoupnp::define_variable; define_variable! { pub static A_ARG_TYPE_FILTER: String = "A_ARG_TYPE_Filter" { + default: "*", evented: false, } } diff --git a/pmomediaserver/src/contentdirectory/variables/a_arg_type_index.rs b/pmomediaserver/src/contentdirectory/variables/a_arg_type_index.rs index 7ae6419c..cb9429f1 100644 --- a/pmomediaserver/src/contentdirectory/variables/a_arg_type_index.rs +++ b/pmomediaserver/src/contentdirectory/variables/a_arg_type_index.rs @@ -2,6 +2,7 @@ use pmoupnp::define_variable; define_variable! { pub static A_ARG_TYPE_INDEX: UI4 = "A_ARG_TYPE_Index" { + default: 0, evented: false, } } diff --git a/pmomediaserver/src/contentdirectory/variables/a_arg_type_objectid.rs b/pmomediaserver/src/contentdirectory/variables/a_arg_type_objectid.rs index 40d06426..53e1f6da 100644 --- a/pmomediaserver/src/contentdirectory/variables/a_arg_type_objectid.rs +++ b/pmomediaserver/src/contentdirectory/variables/a_arg_type_objectid.rs @@ -2,6 +2,7 @@ use pmoupnp::define_variable; define_variable! { pub static A_ARG_TYPE_OBJECTID: String = "A_ARG_TYPE_ObjectID" { + default: "0", evented: false, } } diff --git a/pmomediaserver/src/contentdirectory/variables/a_arg_type_sortcriteria.rs b/pmomediaserver/src/contentdirectory/variables/a_arg_type_sortcriteria.rs index f7654bc0..86ab3bd7 100644 --- a/pmomediaserver/src/contentdirectory/variables/a_arg_type_sortcriteria.rs +++ b/pmomediaserver/src/contentdirectory/variables/a_arg_type_sortcriteria.rs @@ -2,6 +2,7 @@ use pmoupnp::define_variable; define_variable! { pub static A_ARG_TYPE_SORTCRITERIA: String = "A_ARG_TYPE_SortCriteria" { + default: "", evented: false, } } diff --git a/pmoupnp/src/actions/action_instance.rs b/pmoupnp/src/actions/action_instance.rs index 65e5cb78..4ac85e46 100644 --- a/pmoupnp/src/actions/action_instance.rs +++ b/pmoupnp/src/actions/action_instance.rs @@ -243,7 +243,7 @@ impl ActionInstance { if arg_model.is_in() { if let Some(value) = data.get(arg_inst.get_name()) { if let Some(var_inst) = arg_inst.get_variable_instance() { - var_inst.set_value(value.clone()); + var_inst.set_value(value.clone()).await.ok(); trace!(" IN {} = {:?}", arg_inst.get_name(), value); } } diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs index f28763af..b9cc4624 100644 --- a/pmoupnp/src/services/service_instance.rs +++ b/pmoupnp/src/services/service_instance.rs @@ -1269,6 +1269,7 @@ async fn control_handler(State(instance): State>, body: Str }; debug!("🎬 Received SOAP action: {}", soap_action.name); + debug!("🎬 SOAP arguments: {:?}", soap_action.args); // Trouver l'action correspondante dans l'instance let action_instance = match instance.action(&soap_action.name) { @@ -1290,8 +1291,22 @@ async fn control_handler(State(instance): State>, body: Str }; // Convertir les arguments SOAP (String) en ActionData (StateValue) + // D'abord, initialiser tous les arguments IN avec leurs valeurs par défaut let mut action_data = HashMap::new(); + for arg_inst in action_instance.arguments_set().all() { + let arg_model = arg_inst.as_ref().get_model(); + if arg_model.is_in() { + if let Some(var_inst) = arg_inst.get_variable_instance() { + // Utiliser la valeur par défaut de la variable + let default_value = var_inst.value(); + action_data.insert(arg_inst.get_name().to_string(), default_value); + } + } + } + + // Puis, écraser avec les valeurs fournies dans le SOAP for (arg_name, arg_value) in soap_action.args { + debug!("🔍 Processing SOAP arg: {} = '{}'", arg_name, arg_value); // Trouver l'argument correspondant pour obtenir son type if let Some(arg_inst) = action_instance.argument(&arg_name) { if let Some(var_inst) = arg_inst.get_variable_instance() { @@ -1299,6 +1314,7 @@ async fn control_handler(State(instance): State>, body: Str // Parser la valeur selon le type de la variable match StateValue::from_string(&arg_value, &var_model.as_state_var_type()) { Ok(value) => { + debug!("✅ Parsed {} = {:?}", arg_name, value); action_data.insert(arg_name, value); } Err(e) => {