diff --git a/Cargo.lock b/Cargo.lock index d8b919d6..867c2113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2355,6 +2355,7 @@ dependencies = [ "once_cell", "pmoconfig", "pmodidl", + "pmoparadise", "pmoqobuz", "pmoserver", "pmosource", diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index c3be8e51..52f44081 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" pmoconfig = { path = "../pmoconfig" } pmoupnp = { path = "../pmoupnp"} pmomediarenderer = { path = "../pmomediarenderer" } -pmomediaserver = { path = "../pmomediaserver", features = ["qobuz", "api"] } +pmomediaserver = { path = "../pmomediaserver", features = ["qobuz", "paradise", "api"] } pmoserver = { path = "../pmoserver" } pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmoapp = { path = "../pmoapp", features = ["pmoserver"] } diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index 28affaab..a149ed5c 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -2,7 +2,7 @@ use pmoapp::{WebAppExt, Webapp}; use pmocovers::CoverCacheExt; use pmomediarenderer::MEDIA_RENDERER; use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt, MediaServerExt, sources_api_router}; -use pmoserver::{ServerBuilder, logs::LoggingOptions}; +use pmoserver::ServerBuilder; use pmoupnp::{UpnpServer, ssdp::SsdpServer, upnp_api::UpnpApiExt}; use tracing::info; @@ -55,11 +55,16 @@ async fn main() { // Enregistrer les sources musicales info!("📡 Registering music sources..."); - // Enregistrer Qobuz depuis la configuration - if let Err(e) = server.register_qobuz_from_config().await { + // 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); + } + // Lister toutes les sources enregistrées let sources = server.list_music_sources().await; info!("✅ {} music source(s) registered", sources.len()); diff --git a/pmomediaserver/Cargo.toml b/pmomediaserver/Cargo.toml index a001d651..49658bf3 100644 --- a/pmomediaserver/Cargo.toml +++ b/pmomediaserver/Cargo.toml @@ -23,6 +23,7 @@ serde_json = "1.0" axum = { version = "0.8", optional = true } utoipa = { version = "5.3", optional = true } pmoqobuz = { path = "../pmoqobuz", optional = true } +pmoparadise = { path = "../pmoparadise", optional = true } pmoconfig = { path = "../pmoconfig", optional = true } [features] @@ -31,3 +32,5 @@ default = [] api = ["dep:axum", "dep:utoipa"] # Feature pour activer le support Qobuz configuré qobuz = ["dep:pmoqobuz", "dep:pmoconfig", "pmoqobuz/cache"] +# Feature pour activer le support Radio Paradise +paradise = ["dep:pmoparadise"] diff --git a/pmomediaserver/src/sources.rs b/pmomediaserver/src/sources.rs index 652f84ce..a3511a7a 100644 --- a/pmomediaserver/src/sources.rs +++ b/pmomediaserver/src/sources.rs @@ -14,6 +14,10 @@ pub enum SourceInitError { #[error("Failed to initialize Qobuz: {0}")] QobuzError(String), + #[cfg(feature = "paradise")] + #[error("Failed to initialize Radio Paradise: {0}")] + ParadiseError(String), + #[error("Configuration error: {0}")] ConfigError(String), @@ -46,7 +50,7 @@ pub type Result = std::result::Result; /// ``` #[async_trait::async_trait] pub trait SourcesExt { - /// Enregistre la source Qobuz depuis la configuration + /// Enregistre la source Qobuz /// /// Cette méthode lit les credentials Qobuz depuis `pmoconfig` et crée /// automatiquement un `QobuzSource` avec cache activé. @@ -71,10 +75,10 @@ pub trait SourcesExt { /// # Examples /// /// ```ignore - /// server.register_qobuz_from_config().await?; + /// server.register_qobuz().await?; /// ``` #[cfg(feature = "qobuz")] - async fn register_qobuz_from_config(&mut self) -> Result<()>; + async fn register_qobuz(&mut self) -> Result<()>; /// Enregistre la source Qobuz avec des credentials explicites /// @@ -86,19 +90,38 @@ pub trait SourcesExt { /// # Examples /// /// ```ignore - /// server.register_qobuz("user@example.com", "password").await?; + /// server.register_qobuz_with_credentials("user@example.com", "password").await?; /// ``` #[cfg(feature = "qobuz")] - async fn register_qobuz(&mut self, username: &str, password: &str) -> Result<()>; + async fn register_qobuz_with_credentials(&mut self, username: &str, password: &str) -> Result<()>; + + /// Enregistre la source Radio Paradise + /// + /// Cette méthode crée automatiquement un `RadioParadiseSource` avec cache activé. + /// Radio Paradise ne nécessite pas d'authentification. + /// + /// # Erreurs + /// + /// Retourne une erreur si : + /// - La connexion au client Radio Paradise échoue + /// - La feature "paradise" n'est pas activée + /// + /// # Examples + /// + /// ```ignore + /// server.register_paradise().await?; + /// ``` + #[cfg(feature = "paradise")] + async fn register_paradise(&mut self) -> Result<()>; } #[async_trait::async_trait] impl SourcesExt for Server { #[cfg(feature = "qobuz")] - async fn register_qobuz_from_config(&mut self) -> Result<()> { + async fn register_qobuz(&mut self) -> Result<()> { use pmoqobuz::{QobuzClient, QobuzSource}; - tracing::info!("Initializing Qobuz source from configuration..."); + tracing::info!("Initializing Qobuz source..."); // Créer le client depuis la config let client = QobuzClient::from_config() @@ -122,7 +145,7 @@ impl SourcesExt for Server { } #[cfg(feature = "qobuz")] - async fn register_qobuz(&mut self, username: &str, password: &str) -> Result<()> { + async fn register_qobuz_with_credentials(&mut self, username: &str, password: &str) -> Result<()> { use pmoqobuz::{QobuzClient, QobuzSource}; tracing::info!("Initializing Qobuz source with explicit credentials..."); @@ -147,12 +170,34 @@ impl SourcesExt for Server { Ok(()) } -} -// Placeholder pour d'autres sources -// TODO: Ajouter Radio Paradise lorsque la crate sera disponible -// #[cfg(feature = "radioparadise")] -// async fn register_radioparadise_from_config(&mut self) -> Result<()> { ... } + #[cfg(feature = "paradise")] + async fn register_paradise(&mut self) -> Result<()> { + use pmoparadise::{RadioParadiseClient, RadioParadiseSource}; + + tracing::info!("Initializing Radio Paradise source..."); + + // Créer le client (Radio Paradise ne nécessite pas d'authentification) + let client = RadioParadiseClient::new() + .await + .map_err(|e| SourceInitError::ParadiseError(format!("Failed to create client: {}", e)))?; + + // Récupérer l'URL de base du serveur depuis la config + let config = pmoconfig::get_config(); + let port = config.get_http_port(); + let base_url = format!("http://localhost:{}", port); + + // Créer la source avec capacité FIFO par défaut + let source = RadioParadiseSource::new_default(client, &base_url); + + // Enregistrer la source + self.register_music_source(Arc::new(source)).await; + + tracing::info!("✅ Radio Paradise source registered successfully"); + + Ok(()) + } +} #[cfg(test)] mod tests { diff --git a/pmomediaserver/src/sources_api.rs b/pmomediaserver/src/sources_api.rs index 80c552a2..bea37c21 100644 --- a/pmomediaserver/src/sources_api.rs +++ b/pmomediaserver/src/sources_api.rs @@ -65,6 +65,15 @@ pub struct QobuzCredentials { pub password: Option, } +/// Paramètres pour Radio Paradise (actuellement vide, mais peut être étendu) +#[cfg(feature = "paradise")] +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct ParadiseParams { + /// Capacité FIFO (optionnelle, 50 par défaut) + #[serde(default)] + pub fifo_capacity: Option, +} + /// Réponse d'enregistrement de source #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct SourceRegisteredResponse { @@ -226,6 +235,65 @@ async fn register_qobuz(Json(creds): Json) -> impl IntoRespons .into_response() } +/// Enregistre une source Radio Paradise +/// +/// Enregistre une nouvelle source Radio Paradise (ne nécessite pas d'authentification). +#[cfg(feature = "paradise")] +#[utoipa::path( + post, + path = "/sources/paradise", + request_body = ParadiseParams, + responses( + (status = 201, description = "Source enregistrée", body = SourceRegisteredResponse), + (status = 400, description = "Erreur d'enregistrement", body = ErrorResponse), + ), + tag = "sources" +)] +async fn register_paradise(Json(params): Json) -> impl IntoResponse { + use pmoparadise::{RadioParadiseClient, RadioParadiseSource}; + + let registry = get_source_registry().await; + + // Créer le client (Radio Paradise ne nécessite pas d'auth) + let client = match RadioParadiseClient::new().await { + Ok(c) => c, + Err(e) => { + return ( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + error: format!("Failed to create Radio Paradise client: {}", e), + }), + ) + .into_response(); + } + }; + + // Récupérer l'URL de base du serveur depuis la config + let config = pmoconfig::get_config(); + let port = config.get_http_port(); + let base_url = format!("http://localhost:{}", port); + + // Créer et enregistrer la source + let source = if let Some(capacity) = params.fifo_capacity { + Arc::new(RadioParadiseSource::new(client, &base_url, capacity)) + } else { + Arc::new(RadioParadiseSource::new_default(client, &base_url)) + }; + + let source_id = source.as_ref().id().to_string(); + + registry.register(source).await; + + ( + StatusCode::CREATED, + Json(SourceRegisteredResponse { + message: "Radio Paradise source registered successfully".to_string(), + source_id, + }), + ) + .into_response() +} + /// Désenregistre une source musicale /// /// Supprime une source du registre par son ID. @@ -281,19 +349,52 @@ async fn unregister_source(Path(id): Path) -> impl IntoResponse { pub fn sources_api_router() -> Router { let mut router = Router::new() .route("/sources", get(list_sources)) - .route("/sources/:id", get(get_source)) - .route("/sources/:id", delete(unregister_source)); + .route("/sources/{id}", get(get_source)) + .route("/sources/{id}", delete(unregister_source)); #[cfg(feature = "qobuz")] { router = router.route("/sources/qobuz", post(register_qobuz)); } + #[cfg(feature = "paradise")] + { + router = router.route("/sources/paradise", post(register_paradise)); + } + router } -/// Structure pour la documentation OpenAPI -#[cfg(feature = "qobuz")] +/// Structure pour la documentation OpenAPI (Qobuz + Paradise) +#[cfg(all(feature = "qobuz", feature = "paradise"))] +#[derive(utoipa::OpenApi)] +#[openapi( + paths( + list_sources, + get_source, + unregister_source, + register_qobuz, + register_paradise, + ), + components( + schemas( + SourceInfo, + SourceCapabilitiesInfo, + SourcesList, + SourceRegisteredResponse, + ErrorResponse, + QobuzCredentials, + ParadiseParams, + ) + ), + tags( + (name = "sources", description = "Gestion des sources musicales") + ) +)] +pub struct SourcesApiDoc; + +/// Structure pour la documentation OpenAPI (Qobuz uniquement) +#[cfg(all(feature = "qobuz", not(feature = "paradise")))] #[derive(utoipa::OpenApi)] #[openapi( paths( @@ -318,8 +419,34 @@ pub fn sources_api_router() -> Router { )] pub struct SourcesApiDoc; -/// Structure pour la documentation OpenAPI (sans Qobuz) -#[cfg(not(feature = "qobuz"))] +/// Structure pour la documentation OpenAPI (Paradise uniquement) +#[cfg(all(feature = "paradise", not(feature = "qobuz")))] +#[derive(utoipa::OpenApi)] +#[openapi( + paths( + list_sources, + get_source, + unregister_source, + register_paradise, + ), + components( + schemas( + SourceInfo, + SourceCapabilitiesInfo, + SourcesList, + SourceRegisteredResponse, + ErrorResponse, + ParadiseParams, + ) + ), + tags( + (name = "sources", description = "Gestion des sources musicales") + ) +)] +pub struct SourcesApiDoc; + +/// Structure pour la documentation OpenAPI (sans sources spécifiques) +#[cfg(not(any(feature = "qobuz", feature = "paradise")))] #[derive(utoipa::OpenApi)] #[openapi( paths(