Files
pmomusic/pmomediaserver/src/contentdirectory/handlers.rs
Eric Coissac 8179c0e239 refactor: rework search implementation and fix navigation bug
Replaces raw string queries with structured `SearchQuery` types across the `MusicSource` trait and UPnP handlers. Adds dedicated paginated endpoints, explicit caching, and type-specific filtering for the Qobuz source. Fixes a frontend navigation bug by aligning state management with UPnP browse semantics and properly routing virtual container IDs. Also updates the Makefile for dynamic `RUST_LOG` configuration, standardizes logging with `tracing`, and adds the dependency lockfile.
2026-06-11 22:02:38 +02:00

257 lines
8.0 KiB
Rust

//! # 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 crate::content_handler::ContentHandler;
use pmoupnp::actions::{ActionError, ActionHandler};
use pmoupnp::{action_handler, get, set};
use tracing::{debug, error, info};
/// 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!(|data| {
let mut data = data;
tracing::warn!("━━━ BROWSE ━━━");
let handler = ContentHandler::new();
let object_id: String = get!(&data, "ObjectID", String);
let browse_flag: String = get!(&data, "BrowseFlag", 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={}",
object_id, browse_flag, starting_index, requested_count
);
// 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
tracing::warn!(object_id, returned, total, didl_preview = &didl[..didl.len().min(300)], "━━━ BROWSE DIDL ━━━");
set!(&mut data, "Result", didl);
set!(&mut data, "NumberReturned", returned);
set!(&mut data, "TotalMatches", total);
set!(&mut data, "UpdateID", update_id);
debug!(
"✅ Browse completed: returned={}, total={}",
returned, total
);
info!(
"📂 Browse completed: object_id={} returned={} total={}",
object_id, returned, total
);
Ok(data)
})
}
/// 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!(|data| {
let mut data = data;
tracing::warn!("━━━ SEARCH ━━━");
let handler = ContentHandler::new();
let container_id: String = get!(&data, "ContainerID", String);
let search_criteria: String = get!(&data, "SearchCriteria", String);
let _filter: String = get!(&data, "Filter", String);
let _starting_index: u32 = get!(&data, "StartingIndex", u32);
let _requested_count: u32 = get!(&data, "RequestedCount", u32);
let _sort_criteria: String = get!(&data, "SortCriteria", 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
set!(&mut data, "Result", didl);
set!(&mut data, "NumberReturned", returned);
set!(&mut data, "TotalMatches", total);
set!(&mut data, "UpdateID", update_id);
debug!(
"✅ Search completed: returned={}, total={}",
returned, total
);
Ok(data)
})
}
/// 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!(|data| {
let mut data = data;
debug!("🔍 GetSearchCapabilities handler called");
let handler = ContentHandler::new();
let capabilities = handler.get_search_capabilities().await;
set!(&mut data, "SearchCaps", capabilities.clone());
debug!("✅ SearchCapabilities: {}", capabilities);
Ok(data)
})
}
/// 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!(|data| {
let mut data = data;
debug!("📊 GetSortCapabilities handler called");
let handler = ContentHandler::new();
let capabilities = handler.get_sort_capabilities().await;
set!(&mut data, "SortCaps", capabilities.clone());
debug!("✅ SortCapabilities: {}", capabilities);
Ok(data)
})
}
/// 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!(|data| {
let mut data = data;
debug!("🔄 GetSystemUpdateID handler called");
let handler = ContentHandler::new();
let update_id = handler.get_system_update_id().await;
set!(&mut data, "Id", update_id);
debug!("✅ SystemUpdateID: {}", update_id);
Ok(data)
})
}
#[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();
}
}