Files
pmomusic/pmoupnp/src/devices/device_instance.rs

375 lines
12 KiB
Rust
Raw Normal View History

2025-10-05 21:58:36 +02:00
//! Implémentation de DeviceInstance.
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use tracing::info;
use xmltree::{Element, XMLNode, EmitterConfig};
use crate::{
devices::{Device, errors::DeviceError},
services::ServiceInstance,
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
};
/// Instance d'un device UPnP.
///
/// Représente une instance concrète d'un device UPnP, avec ses services instanciés
/// et son UDN unique.
pub struct DeviceInstance {
/// Métadonnées de l'objet
object: UpnpObjectType,
/// Référence vers le modèle
model: Arc<Device>,
/// UDN unique pour cette instance
udn: String,
/// URL de base du serveur
server_base_url: String,
/// Instances de services
services: RwLock<HashMap<String, Arc<ServiceInstance>>>,
/// Instances de sous-devices
devices: RwLock<HashMap<String, Arc<DeviceInstance>>>,
}
impl Clone for DeviceInstance {
fn clone(&self) -> Self {
Self {
object: self.object.clone(),
model: Arc::clone(&self.model),
udn: self.udn.clone(),
server_base_url: self.server_base_url.clone(),
services: RwLock::new(self.services.read().unwrap().clone()),
devices: RwLock::new(self.devices.read().unwrap().clone()),
}
}
}
impl std::fmt::Debug for DeviceInstance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceInstance")
.field("object", &self.object)
.field("udn", &self.udn)
.field("server_base_url", &self.server_base_url)
.field("services", &self.services)
.field("devices", &self.devices)
.finish()
}
}
impl UpnpTyped for DeviceInstance {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
&self.object
}
}
impl UpnpInstance for DeviceInstance {
type Model = Device;
fn new(model: &Device) -> Self {
// Obtenir ou créer un UDN persistant via la configuration
let device_name = model.get_name();
let udn = if let Ok(config_udn) = pmoconfig::get_config().get_device_udn("mediarenderer", device_name) {
config_udn
} else {
// Fallback : générer un UDN
tracing::warn!("Failed to get/save UDN from config, using generated UUID");
format!("uuid:{}_{}", model.udn_prefix(), uuid::Uuid::new_v4())
};
// Obtenir l'IP locale et le port depuis la configuration
let local_ip = pmoutils::guess_local_ip();
let port = pmoconfig::get_config().get_http_port();
let server_base_url = format!("http://{}:{}", local_ip, port);
2025-10-05 21:58:36 +02:00
Self {
object: UpnpObjectType {
name: model.get_name().to_string(),
object_type: "DeviceInstance".to_string(),
},
model: Arc::new(model.clone()),
udn,
server_base_url,
2025-10-05 21:58:36 +02:00
services: RwLock::new(HashMap::new()),
devices: RwLock::new(HashMap::new()),
}
}
}
impl UpnpTypedInstance for DeviceInstance {
fn get_model(&self) -> &Self::Model {
&self.model
}
}
impl UpnpObject for DeviceInstance {
fn to_xml_element(&self) -> Element {
let mut elem = Element::new("device");
// deviceType
let mut device_type = Element::new("deviceType");
device_type.children.push(XMLNode::Text(self.model.device_type()));
elem.children.push(XMLNode::Element(device_type));
// friendlyName
let mut friendly_name = Element::new("friendlyName");
friendly_name.children.push(XMLNode::Text(self.model.friendly_name().to_string()));
elem.children.push(XMLNode::Element(friendly_name));
// manufacturer
let mut manufacturer = Element::new("manufacturer");
manufacturer.children.push(XMLNode::Text(self.model.manufacturer().to_string()));
elem.children.push(XMLNode::Element(manufacturer));
// modelName
let mut model_name = Element::new("modelName");
model_name.children.push(XMLNode::Text(self.model.model_name().to_string()));
elem.children.push(XMLNode::Element(model_name));
// UDN
let mut udn = Element::new("UDN");
udn.children.push(XMLNode::Text(self.udn.clone()));
elem.children.push(XMLNode::Element(udn));
// serviceList
let services = self.services.read().unwrap();
if !services.is_empty() {
let mut service_list = Element::new("serviceList");
for service in services.values() {
service_list.children.push(XMLNode::Element(service.to_xml_element()));
}
elem.children.push(XMLNode::Element(service_list));
}
// deviceList (sous-devices)
let devices = self.devices.read().unwrap();
if !devices.is_empty() {
let mut device_list = Element::new("deviceList");
for device in devices.values() {
device_list.children.push(XMLNode::Element(device.to_xml_element()));
}
elem.children.push(XMLNode::Element(device_list));
}
// presentationURL
if let Some(url) = self.model.presentation_url() {
let mut presentation_url = Element::new("presentationURL");
presentation_url.children.push(XMLNode::Text(url.to_string()));
elem.children.push(XMLNode::Element(presentation_url));
}
elem
}
}
impl DeviceInstance {
/// Définit l'URL de base du serveur.
pub fn set_server_base_url(&mut self, url: String) {
self.server_base_url = url;
}
/// Retourne l'UDN du device.
pub fn udn(&self) -> &str {
&self.udn
}
/// Retourne l'URL de base du serveur (protocole + host + port).
pub fn base_url(&self) -> &str {
&self.server_base_url
}
/// Retourne la route du device (chemin relatif).
/// Utilise l'UDN pour garantir l'unicité si plusieurs devices du même type existent.
2025-10-05 21:58:36 +02:00
pub fn route(&self) -> String {
format!("/device/{}", self.udn())
2025-10-05 21:58:36 +02:00
}
/// Retourne la route de description du device.
pub fn description_route(&self) -> String {
format!("{}/desc.xml", self.route())
}
/// Ajoute une instance de service au device.
///
/// Cette méthode configure automatiquement le service pour qu'il connaisse
/// son device parent.
///
/// # Errors
///
/// Retourne une erreur si un service avec le même nom existe déjà.
pub fn add_service(self: &Arc<Self>, service: Arc<ServiceInstance>) -> Result<(), DeviceError> {
let mut services = self.services.write().unwrap();
let name = service.get_name().to_string();
if services.contains_key(&name) {
return Err(DeviceError::ServiceAlreadyExists(name));
}
// Configurer le service pour qu'il connaisse son device parent
service.set_device(Arc::clone(self));
services.insert(name, service);
Ok(())
}
/// Retourne tous les services.
pub fn services(&self) -> Vec<Arc<ServiceInstance>> {
self.services.read().unwrap().values().cloned().collect()
}
/// Retourne un service par nom.
pub fn get_service(&self, name: &str) -> Option<Arc<ServiceInstance>> {
self.services.read().unwrap().get(name).cloned()
}
/// Ajoute une instance de sous-device.
pub fn add_device(&self, device: Arc<DeviceInstance>) -> Result<(), DeviceError> {
let mut devices = self.devices.write().unwrap();
let name = device.get_name().to_string();
if devices.contains_key(&name) {
return Err(DeviceError::DeviceAlreadyExists(name));
}
devices.insert(name, device);
Ok(())
}
/// Retourne tous les sous-devices.
pub fn devices(&self) -> Vec<Arc<DeviceInstance>> {
self.devices.read().unwrap().values().cloned().collect()
}
/// Enregistre toutes les URLs du device et de ses services dans le serveur.
2025-10-06 20:24:04 +02:00
pub fn register_urls<'a>(&'a self, server: &'a mut pmoserver::Server) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
2025-10-05 21:58:36 +02:00
Box::pin(async move {
info!(
"✅ Device description for {} available at: {}{}",
self.get_name(),
self.base_url(),
self.description_route(),
);
// Handler pour la description du device
let instance_desc = self.clone();
server.add_handler(&self.description_route(), move || {
let instance = instance_desc.clone();
async move { instance.description_handler().await }
}).await;
// Enregistrer les services
for service in self.services() {
service.register_urls(server).await
.map_err(|e| DeviceError::UrlRegistrationError(e.to_string()))?;
}
// Enregistrer les sous-devices
for device in self.devices() {
device.register_urls(server).await?;
}
Ok(())
})
}
/// Génère l'élément XML de description du device.
pub fn description_element(&self) -> Element {
let mut root = Element::new("root");
root.attributes.insert(
"xmlns".to_string(),
"urn:schemas-upnp-org:device-1-0".to_string(),
);
// specVersion
let mut spec = Element::new("specVersion");
let mut major = Element::new("major");
major.children.push(XMLNode::Text("1".to_string()));
spec.children.push(XMLNode::Element(major));
let mut minor = Element::new("minor");
minor.children.push(XMLNode::Text("0".to_string()));
spec.children.push(XMLNode::Element(minor));
root.children.push(XMLNode::Element(spec));
// device
root.children.push(XMLNode::Element(self.to_xml_element()));
root
}
/// Handler HTTP pour la description du device.
async fn description_handler(&self) -> Response {
let elem = self.description_element();
let config = EmitterConfig::new()
.perform_indent(true)
.indent_string(" ");
let mut xml_output = Vec::new();
if let Err(e) = elem.write_with_config(&mut xml_output, config) {
tracing::error!("Failed to serialize device description XML: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
2025-10-06 11:54:32 +02:00
let xml = String::from_utf8_lossy(&xml_output).to_string();
2025-10-05 21:58:36 +02:00
(
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
xml,
).into_response()
}
2025-10-06 20:24:04 +02:00
/// Crée un SsdpDevice configuré pour ce device UPnP.
///
/// Cette méthode simplifie la création d'un device SSDP en configurant automatiquement :
/// - L'UDN du device
/// - Le type de device
/// - La location (URL de description)
/// - Le serveur (User-Agent avec OS/version détecté automatiquement)
/// - Les types de notification pour tous les services
///
/// # Arguments
///
/// * `app_name` - Nom de l'application (ex: "PMOMusic")
/// * `app_version` - Version de l'application (ex: "1.0")
///
/// # Exemple
///
/// ```ignore
/// let renderer_instance = MEDIA_RENDERER.create_instance();
/// let ssdp_device = renderer_instance.to_ssdp_device("PMOMusic", "1.0");
/// ssdp_server.add_device(ssdp_device);
/// ```
pub fn to_ssdp_device(&self, app_name: &str, app_version: &str) -> crate::ssdp::SsdpDevice {
let location = format!("{}{}", self.base_url(), self.description_route());
let os_string = pmoutils::get_os_string();
let server_string = format!("{} UPnP/1.1 {}/{}", os_string, app_name, app_version);
let mut ssdp_device = crate::ssdp::SsdpDevice::new(
self.udn().to_string(),
self.model.device_type(),
location,
server_string,
);
// Ajouter les types de notification pour chaque service
for service in self.services() {
ssdp_device.add_notification_type(service.service_type());
}
ssdp_device
}
2025-10-05 21:58:36 +02:00
}