Revue de code et refactoring

This commit is contained in:
2025-10-06 20:24:04 +02:00
parent adeb37ce83
commit adb6555aa6
20 changed files with 445 additions and 440 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

81
Cargo.lock generated
View File

@@ -469,6 +469,15 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "deranged"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.4.2" version = "1.4.2"
@@ -1323,6 +1332,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -1397,6 +1412,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_info"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3"
dependencies = [
"log",
"plist",
"serde",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.4" version = "0.12.4"
@@ -1444,10 +1471,24 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plist"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64",
"indexmap",
"quick-xml",
"serde",
"time",
]
[[package]] [[package]]
name = "pmoapp" name = "pmoapp"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"pmoserver",
"rust-embed", "rust-embed",
] ]
@@ -1489,7 +1530,6 @@ dependencies = [
"futures", "futures",
"futures-util", "futures-util",
"pmoconfig", "pmoconfig",
"pmoupnp",
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json", "serde_json",
@@ -1515,6 +1555,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pmoconfig", "pmoconfig",
"pmodidl", "pmodidl",
"pmoserver",
"pmoutils", "pmoutils",
"reqwest", "reqwest",
"serde", "serde",
@@ -1532,6 +1573,7 @@ name = "pmoutils"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"get_if_addrs", "get_if_addrs",
"os_info",
] ]
[[package]] [[package]]
@@ -1558,6 +1600,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.101" version = "1.0.101"
@@ -2106,6 +2154,37 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"

View File

@@ -7,7 +7,7 @@ edition = "2024"
pmoconfig = { path = "../pmoconfig" } pmoconfig = { path = "../pmoconfig" }
pmoupnp = { path = "../pmoupnp"} pmoupnp = { path = "../pmoupnp"}
pmoserver = { path = "../pmoserver" } pmoserver = { path = "../pmoserver" }
pmoapp = { path = "../pmoapp" } pmoapp = { path = "../pmoapp", features = ["pmoserver"] }
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] }

View File

@@ -1,34 +1,24 @@
use pmoupnp::{ use pmoupnp::{
mediarenderer::MEDIA_RENDERER, mediarenderer::MEDIA_RENDERER,
ssdp::{SsdpDevice, SsdpServer}, ssdp::SsdpServer,
UpnpServer,
UpnpModel, UpnpModel,
}; };
use pmoserver::{ use pmoserver::{
logs::{log_dump, log_sse, LogState, SseLayer}, logs::LoggingOptions,
ServerBuilder ServerBuilder
}; };
use pmoapp::Webapp; use pmoapp::{Webapp, WebAppExt};
use tracing_subscriber::Registry;
use tracing_subscriber::prelude::*;
use tracing::info; use tracing::info;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Initialiser le logging d'abord
let log_state = LogState::new(1000);
let subscriber = Registry::default()
.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_ansi(true),
)
.with(SseLayer::new(log_state.clone()));
tracing::subscriber::set_global_default(subscriber).unwrap();
// Créer le serveur // Créer le serveur
let mut server = ServerBuilder::new_configured().build(); let mut server = ServerBuilder::new_configured().build();
// Initialiser le logging et enregistrer les routes de logs
server.init_logging(LoggingOptions::default()).await;
// Routes de base // Routes de base
server server
.add_route("/info", || async { .add_route("/info", || async {
@@ -36,33 +26,12 @@ async fn main() {
}) })
.await; .await;
server.add_spa::<Webapp>("/app").await; // Ajouter la webapp via le trait WebAppExt
info!("📡 Registering Web application...");
server.add_webapp_with_redirect::<Webapp>("/app").await;
// Routes de logging info!("📡 Registering MediaRenderer...");
server let renderer_instance = server.register_device(MEDIA_RENDERER.clone())
.add_handler_with_state("/log-sse", log_sse, log_state.clone())
.await;
server
.add_handler_with_state("/log-dump", log_dump, log_state.clone())
.await;
server.add_redirect("/", "/app").await;
// Créer et enregistrer le MediaRenderer
info!("🎵 Creating MediaRenderer instance...");
let renderer_instance = MEDIA_RENDERER.create_instance();
// Créer et ajouter les instances de services
for service in MEDIA_RENDERER.services().iter() {
let service_instance = service.create_instance();
renderer_instance
.add_service(service_instance)
.expect("Failed to add service to renderer");
}
info!("📡 Registering MediaRenderer routes...");
renderer_instance
.register_urls(&mut server)
.await .await
.expect("Failed to register MediaRenderer routes"); .expect("Failed to register MediaRenderer routes");
@@ -76,25 +45,9 @@ async fn main() {
let mut ssdp_server = SsdpServer::new(); let mut ssdp_server = SsdpServer::new();
ssdp_server.start().expect("Failed to start SSDP server"); ssdp_server.start().expect("Failed to start SSDP server");
// Créer le device SSDP pour le MediaRenderer // Créer et enregistrer le device SSDP pour le MediaRenderer
let location = format!("{}{}", let ssdp_device = renderer_instance
renderer_instance.base_url(), .to_ssdp_device("PMOMusic", "1.0");
renderer_instance.description_route()
);
let mut ssdp_device = SsdpDevice::new(
renderer_instance.udn().to_string(),
MEDIA_RENDERER.device_type(),
location,
format!("Linux/5.0 UPnP/1.1 PMOMusic/1.0"),
);
// Ajouter les types de notification pour chaque service
for service in renderer_instance.services() {
ssdp_device.add_notification_type(service.service_type());
}
// Enregistrer le device et envoyer les annonces SSDP
ssdp_server.add_device(ssdp_device); ssdp_server.add_device(ssdp_device);
info!("✅ SSDP announcements sent for MediaRenderer"); info!("✅ SSDP announcements sent for MediaRenderer");

View File

@@ -5,3 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
rust-embed = "8.5.0" rust-embed = "8.5.0"
[dependencies.pmoserver]
path = "../pmoserver"
optional = true
[features]
default = []
pmoserver = ["dep:pmoserver"]

View File

@@ -239,6 +239,8 @@
//! - [Vite Documentation](https://vitejs.dev/) //! - [Vite Documentation](https://vitejs.dev/)
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::future::Future;
use std::pin::Pin;
/// Structure représentant l'application web embarquée. /// Structure représentant l'application web embarquée.
/// ///
@@ -248,16 +250,72 @@ use rust_embed::RustEmbed;
/// ## Exemple /// ## Exemple
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pmoapp::Webapp; /// use pmoapp::{Webapp, WebAppExt};
/// use pmoserver::ServerBuilder; /// use pmoserver::ServerBuilder;
/// ///
/// # async fn example() { /// # async fn example() {
/// let mut server = ServerBuilder::new("MyApp").build(); /// let mut server = ServerBuilder::new("MyApp").build();
/// ///
/// // Ajouter la webapp comme SPA sur le chemin /app /// // Ajouter la webapp via le trait WebAppExt
/// server.add_spa::<Webapp>("/app").await; /// server.add_webapp::<Webapp>("/app").await;
/// # } /// # }
/// ``` /// ```
#[derive(RustEmbed, Clone)] #[derive(RustEmbed, Clone)]
#[folder = "webapp/dist"] #[folder = "webapp/dist"]
pub struct Webapp; pub struct Webapp;
/// Trait pour étendre un serveur HTTP avec des fonctionnalités webapp.
///
/// Ce trait permet à `pmoapp` d'ajouter des méthodes d'extension sur des types
/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmoapp`.
///
/// # Architecture
///
/// Similaire au pattern utilisé par `pmoupnp` pour `UpnpServer`, ce trait permet
/// une extension propre et découplée :
///
/// - `pmoserver` définit un serveur HTTP générique
/// - `pmoapp` étend ce serveur avec des méthodes webapp via ce trait
/// - Le serveur n'a pas besoin de connaître `pmoapp`
///
/// # Exemple d'implémentation
///
/// ```ignore
/// impl WebAppExt for pmoserver::Server {
/// fn add_webapp<W: RustEmbed>(&mut self, path: &str) -> ... {
/// // Délègue à la méthode interne add_spa
/// self.add_spa::<W>(path)
/// }
/// }
/// ```
pub trait WebAppExt {
/// Ajoute une Single Page Application au serveur.
///
/// # Arguments
///
/// * `path` - Le chemin où monter la webapp (ex: "/app")
///
/// # Type Parameter
///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp
fn add_webapp<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
W: RustEmbed + Clone + Send + Sync + 'static;
/// Ajoute une webapp avec une redirection automatique depuis la racine.
///
/// # Arguments
///
/// * `path` - Le chemin où monter la webapp (ex: "/app")
///
/// # Type Parameter
///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp
fn add_webapp_with_redirect<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
W: RustEmbed + Clone + Send + Sync + 'static;
}
// Implémentation du trait pour pmoserver::Server (feature-gated)
#[cfg(feature = "pmoserver")]
mod pmoserver_impl;

View File

@@ -0,0 +1,57 @@
//! Implémentation du trait WebAppExt pour le serveur pmoserver
//!
//! Ce module enrichit `pmoserver::Server` avec les fonctionnalités webapp en
//! implémentant le trait [`WebAppExt`](crate::WebAppExt). Cette implémentation
//! permet d'enregistrer facilement des webapps embarquées sur le serveur.
//!
//! ## Architecture
//!
//! `pmoapp` étend `pmoserver::Server` sans que `pmoserver` connaisse `pmoapp`.
//! C'est le pattern d'extension : `pmoapp` ajoute des fonctionnalités à un type
//! externe via un trait, similaire au pattern utilisé par `pmoupnp` pour `UpnpServer`.
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoapp::{Webapp, WebAppExt};
//! use pmoserver::ServerBuilder;
//!
//! # async fn example() {
//! let mut server = ServerBuilder::new("MyApp").build();
//!
//! // Le trait WebAppExt est automatiquement disponible
//! server.add_webapp::<Webapp>("/app").await;
//!
//! // Ou avec redirection
//! server.add_webapp_with_redirect::<Webapp>("/app").await;
//! # }
//! ```
use crate::WebAppExt;
use pmoserver::Server;
use rust_embed::RustEmbed;
use std::future::Future;
use std::pin::Pin;
impl WebAppExt for Server {
fn add_webapp<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
W: RustEmbed + Clone + Send + Sync + 'static,
{
let path = path.to_string();
Box::pin(async move {
self.add_spa::<W>(&path).await;
})
}
fn add_webapp_with_redirect<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
W: RustEmbed + Clone + Send + Sync + 'static,
{
let path = path.to_string();
Box::pin(async move {
self.add_spa::<W>(&path).await;
self.add_redirect("/", &path).await;
})
}
}

View File

@@ -21,7 +21,3 @@ axum-embed = "0.1.0"
rust-embed = "8.7.2" rust-embed = "8.7.2"
utoipa = { version = "5.4.0", features = ["axum_extras"] } utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
[dependencies.pmoupnp]
path = "../pmoupnp"
default-features = false

View File

@@ -20,7 +20,6 @@
//! //!
//! - [`server`] : Implémentation du serveur principal et du builder //! - [`server`] : Implémentation du serveur principal et du builder
//! - [`logs`] : Système de logs SSE pour monitoring en temps réel //! - [`logs`] : Système de logs SSE pour monitoring en temps réel
//! - `upnp_impl` : Implémentation du trait `pmoupnp::UpnpServer` (privé)
//! //!
//! ## Exemple d'utilisation //! ## Exemple d'utilisation
//! //!
@@ -53,27 +52,25 @@
//! //!
//! ## Intégration UPnP //! ## Intégration UPnP
//! //!
//! Le serveur implémente automatiquement le trait `pmoupnp::UpnpServer`, permettant //! Le serveur peut être étendu avec UPnP via le trait `pmoupnp::UpnpServer`.
//! de connecter des devices UPnP : //! L'implémentation est fournie par `pmoupnp` (feature `pmoserver`), permettant
//! de connecter des devices UPnP sans que `pmoserver` dépende de `pmoupnp` :
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER}; //! use pmoupnp::{UpnpServer, mediarenderer::MEDIA_RENDERER};
//! use pmoupnp::devices::DeviceInstance;
//! use pmoserver::ServerBuilder; //! use pmoserver::ServerBuilder;
//! use std::sync::Arc;
//! //!
//! # async fn example() { //! # async fn example() {
//! let mut server = ServerBuilder::new("MediaRenderer").build(); //! let mut server = ServerBuilder::new("MediaRenderer").build();
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER)); //! let device = MEDIA_RENDERER.create_instance();
//! //!
//! // Le device enregistre automatiquement ses routes //! // Le trait UpnpServer est automatiquement disponible (implémenté dans pmoupnp)
//! device.register_urls(&mut server).await; //! device.register_urls(&mut server).await;
//! # } //! # }
//! ``` //! ```
pub mod server; pub mod server;
pub mod logs; pub mod logs;
mod upnp_impl;
pub use server::{Server, ServerBuilder, ServerInfo}; pub use server::{Server, ServerBuilder, ServerInfo};
pub use logs::{LogState, SseLayer, log_sse, log_dump}; pub use logs::{LogState, SseLayer, log_sse, log_dump, init_logging, LoggingOptions};

View File

@@ -19,6 +19,7 @@ use axum::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing_subscriber::{Registry, layer::SubscriberExt};
/// Représente une entrée de log /// Représente une entrée de log
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -157,3 +158,60 @@ fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool {
allowed allowed
} }
/// Options d'initialisation du système de logging
#[derive(Debug, Clone)]
pub struct LoggingOptions {
/// Capacité du buffer circulaire (nombre d'entrées conservées)
pub buffer_capacity: usize,
/// Activer la sortie vers stderr/stdout
pub enable_console: bool,
}
impl Default for LoggingOptions {
fn default() -> Self {
Self {
buffer_capacity: 1000,
enable_console: true,
}
}
}
/// Initialise le système de logging avec SSE et optionnellement la console
///
/// # Arguments
/// * `options` - Options de configuration du logging
///
/// # Retourne
/// Le `LogState` qui peut être utilisé pour ajouter les routes de logging au serveur
///
/// # Exemple
/// ```rust,no_run
/// use pmoserver::logs::{init_logging, LoggingOptions};
///
/// let log_state = init_logging(LoggingOptions {
/// buffer_capacity: 1000,
/// enable_console: true,
/// });
/// ```
pub fn init_logging(options: LoggingOptions) -> LogState {
let log_state = LogState::new(options.buffer_capacity);
let subscriber = Registry::default().with(SseLayer::new(log_state.clone()));
if options.enable_console {
let subscriber = subscriber.with(
tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.with_ansi(true),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set global default subscriber");
} else {
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set global default subscriber");
}
log_state
}

View File

@@ -25,6 +25,7 @@ use std::{net::SocketAddr, sync::Arc};
use tokio::{signal, sync::RwLock, task::JoinHandle}; use tokio::{signal, sync::RwLock, task::JoinHandle};
use tracing::info; use tracing::info;
use utoipa_swagger_ui::SwaggerUi; use utoipa_swagger_ui::SwaggerUi;
use crate::logs::{LogState, LoggingOptions, init_logging, log_sse, log_dump};
/// Info serveur sérialisable /// Info serveur sérialisable
#[derive(Clone, Serialize, utoipa::ToSchema)] #[derive(Clone, Serialize, utoipa::ToSchema)]
@@ -45,6 +46,7 @@ pub struct Server {
router: Arc<RwLock<Router>>, router: Arc<RwLock<Router>>,
api_router: Arc<RwLock<Option<Router>>>, api_router: Arc<RwLock<Option<Router>>>,
join_handle: Option<JoinHandle<()>>, join_handle: Option<JoinHandle<()>>,
log_state: Option<LogState>,
} }
impl Server { impl Server {
@@ -70,6 +72,7 @@ impl Server {
router: Arc::new(RwLock::new(Router::new())), router: Arc::new(RwLock::new(Router::new())),
api_router: Arc::new(RwLock::new(None)), api_router: Arc::new(RwLock::new(None)),
join_handle: None, join_handle: None,
log_state: None,
} }
} }
@@ -508,6 +511,45 @@ impl Server {
http_port: self.http_port, http_port: self.http_port,
} }
} }
/// Initialise le système de logging et enregistre les routes de logs
///
/// Cette méthode configure le système de tracing avec SSE et optionnellement la console,
/// puis enregistre automatiquement les routes `/log-sse` et `/log-dump`.
///
/// # Arguments
///
/// * `options` - Options de configuration du logging
///
/// # Exemple
///
/// ```rust,no_run
/// # use pmoserver::{ServerBuilder, logs::LoggingOptions};
/// # #[tokio::main]
/// # async fn main() {
/// let mut server = ServerBuilder::new_configured().build();
///
/// // Initialiser les logs avec console
/// server.init_logging(LoggingOptions::default()).await;
///
/// // Ou sans console
/// server.init_logging(LoggingOptions {
/// buffer_capacity: 1000,
/// enable_console: false,
/// }).await;
///
/// server.start().await;
/// # }
/// ```
pub async fn init_logging(&mut self, options: LoggingOptions) {
let log_state = init_logging(options);
// Enregistrer automatiquement les routes de logging
self.add_handler_with_state("/log-sse", log_sse, log_state.clone()).await;
self.add_handler_with_state("/log-dump", log_dump, log_state.clone()).await;
self.log_state = Some(log_state);
}
} }
/// Builder pattern /// Builder pattern

View File

@@ -1,94 +0,0 @@
//! Implémentation du trait UpnpServer pour le serveur pmoserver
//!
//! Ce module fournit l'implémentation du trait [`pmoupnp::UpnpServer`] pour
//! le [`Server`](crate::server::Server) de pmoserver, permettant aux devices
//! et services UPnP d'enregistrer automatiquement leurs endpoints HTTP.
//!
//! ## Architecture
//!
//! L'implémentation fait le pont entre :
//! - Les pointeurs de fonction du trait `UpnpServer` (agnostiques du framework web)
//! - Les handlers Axum (spécifiques à l'implémentation `pmoserver`)
//!
//! Chaque méthode du trait crée un wrapper qui :
//! 1. Convertit les pointeurs de fonction en closures compatibles Axum
//! 2. Délègue l'enregistrement aux méthodes internes du `Server`
//! 3. Retourne une future qui se résout une fois le handler enregistré
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER};
//! use pmoupnp::devices::DeviceInstance;
//! use pmoserver::ServerBuilder;
//! use std::sync::Arc;
//!
//! # async fn example() {
//! let mut server = ServerBuilder::new("MyRenderer").build();
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER));
//!
//! // Le trait UpnpServer est automatiquement disponible
//! device.register_urls(&mut server).await;
//! # }
//! ```
use crate::server::Server;
use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
use std::future::Future;
use std::pin::Pin;
use axum::extract::State;
impl UpnpServer for Server {
fn add_handler<F, Fut>(&mut self, path: &str, handler: F) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
F: Fn() -> Fut + Send + Sync + 'static + Clone,
Fut: Future<Output = Response> + Send + 'static,
{
let path = path.to_string();
Box::pin(async move {
Self::add_handler(self, &path, handler).await;
})
}
fn add_post_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static,
{
let path = path.to_string();
// Créer un wrapper qui convertit le fn pointer en handler Axum
let wrapper = move |State(s): State<S>, body: String| -> Pin<Box<dyn Future<Output = Response> + Send>> {
handler(State(s), body)
};
Box::pin(async move {
Self::add_post_handler_with_state(self, &path, wrapper, state).await;
})
}
fn add_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(State<S>, HeaderMap, Request) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static,
{
let path = path.to_string();
// Créer un wrapper qui convertit le fn pointer en handler Axum
let wrapper = move |State(s): State<S>, headers: HeaderMap, req: Request| -> Pin<Box<dyn Future<Output = Response> + Send>> {
handler(State(s), headers, req)
};
Box::pin(async move {
Self::add_handler_with_state(self, &path, wrapper, state).await;
})
}
}

View File

@@ -7,6 +7,7 @@ edition = "2024"
pmoconfig = { path = "../pmoconfig" } pmoconfig = { path = "../pmoconfig" }
pmodidl = { path = "../pmodidl"} pmodidl = { path = "../pmodidl"}
pmoutils = { path = "../pmoutils" } pmoutils = { path = "../pmoutils" }
pmoserver = { path = "../pmoserver" }
url = "2.5.7" url = "2.5.7"
uuid = "1.18.1" uuid = "1.18.1"

View File

@@ -251,7 +251,7 @@ impl DeviceInstance {
} }
/// Enregistre toutes les URLs du device et de ses services dans le serveur. /// Enregistre toutes les URLs du device et de ses services dans le serveur.
pub fn register_urls<'a, S: crate::UpnpServer + ?Sized>(&'a self, server: &'a mut S) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> { pub fn register_urls<'a>(&'a self, server: &'a mut pmoserver::Server) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
Box::pin(async move { Box::pin(async move {
info!( info!(
"✅ Device description for {} available at: {}{}", "✅ Device description for {} available at: {}{}",
@@ -330,4 +330,45 @@ impl DeviceInstance {
xml, xml,
).into_response() ).into_response()
} }
/// 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
}
} }

View File

@@ -1,10 +1,11 @@
//! Implémentation des traits UPnP pour Device. //! Implémentation des traits UPnP pour Device.
use std::sync::Arc;
use xmltree::{Element, XMLNode}; use xmltree::{Element, XMLNode};
use crate::{ use crate::{
devices::{Device, DeviceInstance}, devices::{Device, DeviceInstance},
UpnpObject, UpnpModel, UpnpObject, UpnpModel, UpnpInstance,
}; };
impl UpnpObject for Device { impl UpnpObject for Device {
@@ -115,4 +116,19 @@ impl UpnpObject for Device {
impl UpnpModel for Device { impl UpnpModel for Device {
type Instance = DeviceInstance; type Instance = DeviceInstance;
/// Crée une instance du device avec ses services déjà instanciés.
///
/// Les services sont créés dans DeviceInstance::new(), cette méthode
/// établit uniquement les liens bidirectionnels parent-enfant.
fn create_instance(&self) -> Arc<DeviceInstance> {
let instance = Arc::new(DeviceInstance::new(self));
// Établir le lien parent pour chaque service
for service in instance.services() {
service.set_device(Arc::clone(&instance));
}
instance
}
} }

View File

@@ -1,10 +1,10 @@
mod object_trait; mod object_trait;
mod object_set; mod object_set;
mod server;
pub mod actions; pub mod actions;
pub mod devices; pub mod devices;
pub mod mediarenderer; pub mod mediarenderer;
pub mod server;
pub mod services; pub mod services;
pub mod soap; pub mod soap;
pub mod ssdp; pub mod ssdp;
@@ -12,8 +12,7 @@ pub mod state_variables;
pub mod value_ranges; pub mod value_ranges;
pub mod variable_types; pub mod variable_types;
// Re-exports
pub use server::UpnpServer;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
@@ -21,6 +20,7 @@ use std::{collections::HashMap, sync::Arc};
use std::sync::RwLock; use std::sync::RwLock;
pub use crate::object_trait::*; pub use crate::object_trait::*;
pub use crate::server::UpnpServer;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UpnpObjectType { pub struct UpnpObjectType {

View File

@@ -1,262 +1,22 @@
//! Trait pour les serveurs UPnP use std::sync::Arc;
//!
//! Ce module définit le trait [`UpnpServer`] qui permet de connecter
//! des devices UPnP à n'importe quelle implémentation de serveur web.
//!
//! ## Architecture
//!
//! Le trait `UpnpServer` définit une interface minimale permettant aux devices
//! et services UPnP d'enregistrer leurs endpoints HTTP sans dépendre d'une
//! implémentation de serveur spécifique.
//!
//! ## Séparation des responsabilités
//!
//! - **pmoupnp** : Définit le trait `UpnpServer` et l'utilise via des contraintes génériques
//! - **pmoserver** : Fournit une implémentation concrète basée sur Axum
//! - **Autres crates** : Peuvent fournir leurs propres implémentations (actix-web, warp, etc.)
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmoupnp::{UpnpServer, devices::{Device, DeviceInstance}};
//! use std::sync::Arc;
//!
//! # async fn example<S: UpnpServer>(mut server: S) {
//! // Créer un device
//! let device = Device::new(
//! "MyDevice".to_string(),
//! "MyDeviceType".to_string(),
//! "Friendly Name".to_string(),
//! );
//! let device_instance = Arc::new(DeviceInstance::new(&device));
//!
//! // Le device enregistre automatiquement ses routes UPnP
//! device_instance.register_urls(&mut server).await;
//! # }
//! ```
//!
//! ## Implémentation
//!
//! Pour implémenter ce trait, votre serveur doit fournir trois méthodes
//! pour enregistrer des handlers HTTP asynchrones :
//!
//! ```rust,no_run
//! use pmoupnp::UpnpServer;
//! use std::future::Future;
//! use std::pin::Pin;
//!
//! struct MyServer {
//! // votre implémentation
//! }
//!
//! impl UpnpServer for MyServer {
//! fn add_handler<F, Fut>(&mut self, path: &str, handler: F)
//! -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
//! where
//! F: Fn() -> Fut + Send + Sync + 'static + Clone,
//! Fut: Future<Output = pmoupnp::server::Response> + Send + 'static,
//! {
//! // Enregistrer le handler pour GET requests
//! # todo!()
//! }
//!
//! fn add_post_handler_with_state<S>(
//! &mut self,
//! path: &str,
//! handler: fn(axum::extract::State<S>, String)
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
//! state: S,
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
//! where
//! S: Clone + Send + Sync + 'static,
//! {
//! // Enregistrer le handler pour POST avec body
//! # todo!()
//! }
//!
//! fn add_handler_with_state<S>(
//! &mut self,
//! path: &str,
//! handler: fn(axum::extract::State<S>,
//! pmoupnp::server::HeaderMap,
//! pmoupnp::server::Request)
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
//! state: S,
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
//! where
//! S: Clone + Send + Sync + 'static,
//! {
//! // Enregistrer le handler avec accès complet à la requête
//! # todo!()
//! }
//! }
//! ```
use std::future::Future; use pmoserver::Server;
use std::pin::Pin;
/// Type alias pour la réponse HTTP (basé sur Axum). use crate::devices::errors::DeviceError;
/// use crate::devices::{Device, DeviceInstance};
/// Utilisé pour éviter une dépendance directe sur axum dans les signatures de trait, use crate::UpnpModel;
/// tout en restant compatible avec les types Axum.
pub type Response = axum::response::Response;
/// Type alias pour les en-têtes HTTP (basé sur Axum). pub trait UpnpServer {
pub type HeaderMap = axum::http::HeaderMap; async fn register_device(&mut self, device: Arc<Device>) -> Result<Arc<DeviceInstance>,DeviceError> ;
/// Type alias pour la requête HTTP (basé sur Axum).
pub type Request = axum::extract::Request<axum::body::Body>;
/// Trait pour les serveurs compatibles UPnP.
///
/// Ce trait définit l'interface minimale qu'un serveur web doit implémenter
/// pour supporter l'enregistrement automatique des endpoints UPnP par les
/// [`DeviceInstance`](crate::devices::DeviceInstance) et
/// [`ServiceInstance`](crate::services::ServiceInstance).
///
/// ## Contraintes
///
/// - `Send + Sync` : Le serveur doit être partageable entre threads
///
/// ## Méthodes
///
/// Les trois méthodes permettent d'enregistrer différents types de handlers :
///
/// 1. **`add_handler`** : Handler GET simple sans état
/// 2. **`add_post_handler_with_state`** : Handler POST avec état et body texte (pour SOAP)
/// 3. **`add_handler_with_state`** : Handler générique avec accès complet (pour SUBSCRIBE/UNSUBSCRIBE)
///
/// ## Implémentations
///
/// - **pmoserver::Server** : Implémentation basée sur Axum (fournie par la crate `pmoserver`)
pub trait UpnpServer: Send + Sync {
/// Ajoute un handler GET pour un chemin donné.
///
/// Utilisé principalement pour servir les descripteurs XML des devices et services.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/device/MediaRenderer/description.xml`)
/// * `handler` - Une closure asynchrone qui génère la réponse
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::UpnpServer;
/// use axum::response::IntoResponse;
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_handler("/description.xml", || async {
/// "<?xml version=\"1.0\"?><root></root>".into_response()
/// }).await;
/// # }
/// ```
fn add_handler<F, Fut>(&mut self, path: &str, handler: F) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
F: Fn() -> Fut + Send + Sync + 'static + Clone,
Fut: Future<Output = Response> + Send + 'static;
/// Ajoute un handler POST avec état pour un chemin donné.
///
/// Utilisé pour les endpoints de contrôle SOAP des services UPnP.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/control`)
/// * `handler` - Un pointeur de fonction qui traite la requête SOAP
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::{UpnpServer, server::Response};
/// use axum::extract::State;
/// use std::pin::Pin;
/// use std::future::Future;
///
/// fn soap_handler(
/// State(service): State<String>,
/// body: String,
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
/// Box::pin(async move {
/// // Traiter la requête SOAP
/// axum::response::Response::default()
/// })
/// }
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_post_handler_with_state(
/// "/control",
/// soap_handler,
/// "ServiceName".to_string(),
/// ).await;
/// # }
/// ```
fn add_post_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(axum::extract::State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static;
/// Ajoute un handler avec état et accès complet à la requête.
///
/// Utilisé pour les endpoints d'événements (SUBSCRIBE/UNSUBSCRIBE) qui nécessitent
/// un accès aux en-têtes HTTP et à la méthode HTTP.
///
/// # Arguments
///
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/event`)
/// * `handler` - Un pointeur de fonction avec accès complet à la requête
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
///
/// # Retour
///
/// Une future qui se résout quand le handler est enregistré.
///
/// # Exemple
///
/// ```rust,no_run
/// use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
/// use axum::extract::State;
/// use std::pin::Pin;
/// use std::future::Future;
///
/// fn event_handler(
/// State(service): State<String>,
/// headers: HeaderMap,
/// req: Request,
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
/// Box::pin(async move {
/// // Traiter SUBSCRIBE/UNSUBSCRIBE
/// axum::response::Response::default()
/// })
/// }
///
/// # async fn example<S: UpnpServer>(mut server: S) {
/// server.add_handler_with_state(
/// "/event",
/// event_handler,
/// "ServiceName".to_string(),
/// ).await;
/// # }
/// ```
fn add_handler_with_state<S>(
&mut self,
path: &str,
handler: fn(axum::extract::State<S>, HeaderMap, Request) -> Pin<Box<dyn Future<Output = Response> + Send>>,
state: S,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
where
S: Clone + Send + Sync + 'static;
} }
impl UpnpServer for Server {
async fn register_device(&mut self, device: Arc<Device>) -> Result<Arc<DeviceInstance>,DeviceError> {
let di = device.create_instance();
di.register_urls(self).await?;
Ok(di)
}
}

View File

@@ -304,7 +304,7 @@ impl ServiceInstance {
/// # Errors /// # Errors
/// ///
/// Retourne une erreur si l'enregistrement des routes échoue. /// Retourne une erreur si l'enregistrement des routes échoue.
pub async fn register_urls<S: crate::UpnpServer + ?Sized>(&self, server: &mut S) -> Result<(), ServiceError> { pub async fn register_urls(&self, server: &mut pmoserver::Server) -> Result<(), ServiceError> {
let device = self.device.read().unwrap(); let device = self.device.read().unwrap();
let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string()); let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string());
let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default(); let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default();

View File

@@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
get_if_addrs = "0.5.3" get_if_addrs = "0.5.3"
os_info = "3.8"

View File

@@ -17,4 +17,36 @@
/// ``` /// ```
mod ip_utils; mod ip_utils;
pub use ip_utils::guess_local_ip; pub use ip_utils::guess_local_ip;
/// Retourne une chaîne décrivant le système d'exploitation et sa version.
///
/// Utilise la crate `os_info` pour obtenir de manière portable et fiable
/// les informations sur le système d'exploitation courant.
///
/// # Format
/// - macOS: "macOS/15.1" ou "Mac OS/10.15.7"
/// - Linux: "Linux/6.5.0" ou "Ubuntu/22.04"
/// - Windows: "Windows/10.0.19045"
/// - Autre: "{OS}/Unknown"
///
/// # Exemples
///
/// ```
/// use pmoutils::get_os_string;
///
/// let os = get_os_string();
/// println!("OS: {}", os); // Ex: "Linux/6.5.0"
/// ```
pub fn get_os_string() -> String {
let info = os_info::get();
let os_type = format!("{:?}", info.os_type());
// Obtenir la version si disponible
let version = info.version();
if version != &os_info::Version::Unknown {
format!("{}/{}", os_type, version)
} else {
format!("{}/Unknown", os_type)
}
}