Work on the contentdirectory action

This commit is contained in:
2025-10-19 02:28:22 +02:00
parent 13f0fa92b1
commit f3560fa4b9
14 changed files with 342 additions and 1 deletions

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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();
}
}

View File

@@ -76,6 +76,7 @@ use pmoupnp::define_service;
pub mod variables;
pub mod actions;
pub mod handlers;
use actions::{
BROWSE, SEARCH, GETSEARCHCAPABILITIES, GETSORTCAPABILITIES, GETSYSTEMUPDATEID

View File

@@ -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,
}

View File

@@ -2,6 +2,7 @@ use pmoupnp::define_variable;
define_variable! {
pub static A_ARG_TYPE_FILTER: String = "A_ARG_TYPE_Filter" {
default: "*",
evented: false,
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -2,6 +2,7 @@ use pmoupnp::define_variable;
define_variable! {
pub static A_ARG_TYPE_SORTCRITERIA: String = "A_ARG_TYPE_SortCriteria" {
default: "",
evented: false,
}
}

View File

@@ -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);
}
}

View File

@@ -1269,6 +1269,7 @@ async fn control_handler(State(instance): State<Arc<ServiceInstance>>, 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<Arc<ServiceInstance>>, 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<Arc<ServiceInstance>>, 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) => {