From fb0394b427ad6e75409ec22991b523b53991b153 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 18 Oct 2025 20:25:56 +0200 Subject: [PATCH] Faire fonctionner le media server --- Cargo.lock | 1 + PMOMusic/src/main.rs | 99 ++++++++++-------------- pmoupnp/Cargo.toml | 1 + pmoupnp/src/upnp_server.rs | 149 +++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e425e043..6564ce9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2509,6 +2509,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", + "tracing-subscriber", "url", "utoipa", "uuid", diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index b2aace02..334cba88 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -1,51 +1,23 @@ use pmoapp::{WebAppExt, Webapp}; -use pmocovers::CoverCacheExt; -use pmoaudiocache::AudioCacheExt; use pmomediarenderer::MEDIA_RENDERER; use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt}; use pmosource::MusicSourceExt; -use pmoserver::ServerBuilder; -use pmoupnp::{UpnpServerExt, ssdp::SsdpServer, upnp_api::UpnpApiExt}; +use pmoserver::Server; +use pmoupnp::{UpnpServerExt, upnp_api::UpnpApiExt}; use tracing::info; #[tokio::main] -async fn main() { - // Créer le serveur - le trait UpnpServer étend automatiquement Server - let mut server = ServerBuilder::new_configured().build(); +async fn main() -> Result<(), Box> { + // ========== PHASE 1 : Infrastructure UPnP ========== + let mut server = Server::create_upnp_server().await?; - // Initialiser le logging et enregistrer les routes de logs - server.init_logging().await; - - info!("📡 Registering the cover cache..."); - let covercache = server - .init_cover_cache_configured() - .await - .expect("Cannot initialise the image cache"); - - info!("✅ Cover cache ready at {}", covercache.cache_dir().display()); - - info!("📡 Registering the audio cache..."); - let audiocache = server - .init_audio_cache_configured() - .await - .expect("Cannot initialise the audio cache"); - - info!("✅ Audio cache ready at {}", audiocache.cache_dir().display()); - - // Routes de base + // Routes personnalisées de l'application server .add_route("/info", || async { serde_json::json!({"version": "1.0.0"}) }) .await; - // Ajouter la webapp via le trait WebAppExt - info!("📡 Registering Web application..."); - server.add_webapp_with_redirect::("/app").await; - - // Enregistrer l'API d'introspection UPnP - server.register_upnp_api().await; - // Initialiser le système de gestion des sources musicales avec API REST info!("📡 Initializing music sources management system..."); server @@ -53,29 +25,19 @@ async fn main() { .await .expect("Failed to initialize music sources API"); - info!("📡 Registering MediaRenderer..."); - let renderer_instance = server - .register_device(MEDIA_RENDERER.clone()) - .await - .expect("Failed to register MediaRenderer routes"); - - info!( - "✅ MediaRenderer ready at {}{}", - renderer_instance.base_url(), - renderer_instance.description_route() - ); + // ========== PHASE 2 : Configuration métier ========== // Enregistrer les sources musicales - info!("📡 Registering music sources..."); + info!("🎵 Registering music sources..."); - // Enregistrer Qobuz - if let Err(e) = server.register_qobuz().await { - tracing::warn!("Failed to register Qobuz: {}", e); - } + // // Enregistrer Qobuz + // if let Err(e) = server.register_qobuz().await { + // tracing::warn!("⚠️ Failed to register Qobuz: {}", e); + // } // Enregistrer Radio Paradise if let Err(e) = server.register_paradise().await { - tracing::warn!("Failed to register Radio Paradise: {}", e); + tracing::warn!("⚠️ Failed to register Radio Paradise: {}", e); } // Lister toutes les sources enregistrées @@ -85,11 +47,24 @@ async fn main() { info!(" - {} ({})", source.name(), source.id()); } - info!("📡 Registering MediaServer..."); + // Enregistrer les devices UPnP (HTTP + SSDP automatique) + info!("📡 Registering UPnP devices..."); + + let renderer_instance = server + .register_device(MEDIA_RENDERER.clone()) + .await + .expect("Failed to register MediaRenderer"); + + info!( + "✅ MediaRenderer ready at {}{}", + renderer_instance.base_url(), + renderer_instance.description_route() + ); + let server_instance = server .register_device(MEDIA_SERVER.clone()) .await - .expect("Failed to register MediaServer routes"); + .expect("Failed to register MediaServer"); info!( "✅ MediaServer ready at {}{}", @@ -97,16 +72,18 @@ async fn main() { server_instance.description_route() ); - // Créer et démarrer le serveur SSDP - info!("📡 Starting SSDP discovery..."); - let mut ssdp_server = SsdpServer::new(); - ssdp_server.start().expect("Failed to start SSDP server"); + // Ajouter la webapp via le trait WebAppExt + info!("📡 Registering Web application..."); + server.add_webapp_with_redirect::("/app").await; - // Créer et enregistrer le device SSDP pour le MediaRenderer - let ssdp_device = renderer_instance.to_ssdp_device("PMOMusic", "1.0"); - ssdp_server.add_device(ssdp_device); - info!("✅ SSDP announcements sent for MediaRenderer"); + // ========== PHASE 3 : Démarrage du serveur ========== + info!("🌐 Starting HTTP server..."); server.start().await; + + info!("✅ PMOMusic is ready!"); + info!("Press Ctrl+C to stop..."); server.wait().await; + + Ok(()) } diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index 925f80d4..5891b8cf 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -28,6 +28,7 @@ chrono = { version = "0.4.42", features = ["serde"] } once_cell = "1.20" parking_lot = "0.12" tracing = "0.1" +tracing-subscriber = "0.3" bevy_reflect = "0.17.1" bevy_reflect_derive = "0.17.1" reqwest = "0.12.23" diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index baf1ab6e..220a3587 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -29,6 +29,8 @@ use crate::devices::errors::DeviceError; use crate::devices::{Device, DeviceInstance, DeviceRegistry}; use crate::UpnpModel; use crate::cache_registry::CACHE_REGISTRY; +use crate::ssdp::SsdpServer; +use crate::upnp_api::UpnpApiExt; use pmocovers::Cache as CoverCache; use pmoaudiocache::Cache as AudioCache; @@ -42,6 +44,14 @@ static DEVICE_REGISTRY: Lazy> = Lazy::new(|| { RwLock::new(DeviceRegistry::new()) }); +/// Serveur SSDP global et thread-safe. +/// +/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads. +/// Permet l'annonce automatique des devices UPnP sur le réseau. +static SSDP_SERVER: Lazy>> = Lazy::new(|| { + RwLock::new(None) +}); + /// Trait pour étendre un serveur avec des fonctionnalités UPnP. /// /// Ce trait ajoute : @@ -147,11 +157,70 @@ pub trait UpnpServerExt { /// Récupérer le cache audio fn audio_cache(&self) -> Option>; + + // ========= SSDP Management (NOUVEAU) ========= + + /// Initialise et démarre le serveur SSDP + /// + /// Cette méthode crée et démarre le serveur SSDP qui gère les annonces + /// UPnP sur le réseau (NOTIFY alive/byebye, réponses M-SEARCH). + /// + /// # Returns + /// + /// `Ok(())` si l'initialisation réussit, `Err` sinon. + /// + /// # Note + /// + /// Cette méthode peut être appelée plusieurs fois sans effet si SSDP + /// est déjà initialisé. + fn init_ssdp(&self) -> Result<(), std::io::Error>; + + /// Vérifie si le serveur SSDP est initialisé + /// + /// # Returns + /// + /// `true` si SSDP est actif, `false` sinon + fn ssdp_enabled(&self) -> bool; + + /// Crée et initialise un serveur UPnP complet (factory method) + /// + /// Cette méthode factory initialise l'infrastructure UPnP complète : + /// - Serveur HTTP (via pmoserver) + /// - Caches (couvertures + audio) + /// - Logging + /// - Serveur SSDP + /// + /// Après cette méthode, l'utilisateur doit : + /// - Enregistrer ses devices via `register_device()` + /// - Enregistrer ses sources musicales + /// - Appeler `wait()` pour attendre l'arrêt + /// + /// # Returns + /// + /// Un serveur UPnP prêt à l'emploi + /// + /// # Errors + /// + /// Retourne une erreur si l'initialisation échoue (config, caches, SSDP, etc.) + /// + /// # Examples + /// + /// ```ignore + /// use pmoupnp::UpnpServerExt; + /// use pmoserver::Server; + /// + /// let mut server = Server::create_upnp_server().await?; + /// server.register_device(my_device).await?; + /// server.wait().await; + /// ``` + async fn create_upnp_server() -> Result; } // Implémentation du trait UpnpServer pour pmoserver::Server impl UpnpServerExt for Server { async fn register_device(&mut self, device: Arc) -> Result, DeviceError> { + use tracing::info; + // Créer l'instance (retourne déjà un Arc) let di = device.create_instance(); @@ -164,6 +233,16 @@ impl UpnpServerExt for Server { .register(di.clone()) .map_err(|e| DeviceError::UrlRegistrationError(e))?; + // Annoncer via SSDP (si initialisé) + if self.ssdp_enabled() { + let ssdp_opt = SSDP_SERVER.read().unwrap(); + if let Some(ref ssdp) = *ssdp_opt { + let ssdp_device = di.to_ssdp_device("PMOMusic", "1.0"); + ssdp.add_device(ssdp_device); + info!("✅ SSDP announcement for {}", di.udn()); + } + } + Ok(di) } @@ -281,6 +360,76 @@ impl UpnpServerExt for Server { fn audio_cache(&self) -> Option> { crate::cache_registry::get_audio_cache() } + + // ========= SSDP Management Implementation ========= + + fn init_ssdp(&self) -> Result<(), std::io::Error> { + use tracing::info; + + let mut ssdp_opt = SSDP_SERVER.write().unwrap(); + if ssdp_opt.is_some() { + // Déjà initialisé + return Ok(()); + } + + let mut ssdp = SsdpServer::new(); + ssdp.start()?; + *ssdp_opt = Some(ssdp); + + info!("✅ SSDP server initialized"); + Ok(()) + } + + fn ssdp_enabled(&self) -> bool { + SSDP_SERVER.read().unwrap().is_some() + } + + async fn create_upnp_server() -> Result { + use pmoserver::ServerBuilder; + use tracing::{info, warn}; + + // 1. Créer le serveur depuis la config + info!("🔧 Creating UPnP server from configuration..."); + let mut server = ServerBuilder::new_configured().build(); + + // 2. Initialiser le logging HTTP (routes de logs + tracing) + info!("📝 Initializing logging..."); + server.init_logging().await; + + // 3. Initialiser les caches + info!("💾 Initializing caches..."); + match server.init_caches().await { + Ok(_) => { + info!("✅ Caches initialized"); + } + Err(e) => { + warn!("❌ Cache initialization failed: {}", e); + return Err(e); + } + } + + // 4. Le serveur HTTP n'est PAS encore démarré + // Il sera démarré après l'enregistrement des devices et routes + info!("🌐 HTTP server configured at {}", server.info().base_url); + + // 5. Enregistrer l'API d'introspection UPnP + info!("📡 Registering UPnP API..."); + server.register_upnp_api().await; + + // 6. Initialiser SSDP + info!("📡 Initializing SSDP discovery..."); + match server.init_ssdp() { + Ok(_) => info!("✅ SSDP server initialized"), + Err(e) => { + warn!("❌ SSDP initialization failed: {}", e); + return Err(e.into()); + } + } + + info!("🎉 UPnP server infrastructure ready"); + info!("📝 Next: Register devices and music sources"); + Ok(server) + } } /// Fonctions helper pour accéder au registre depuis les handlers.