Ajoute la source pmoparadise au mediaserver

This commit is contained in:
2025-10-17 12:01:36 +02:00
parent 622c1d952e
commit 218e527e24
6 changed files with 204 additions and 23 deletions

1
Cargo.lock generated
View File

@@ -2355,6 +2355,7 @@ dependencies = [
"once_cell", "once_cell",
"pmoconfig", "pmoconfig",
"pmodidl", "pmodidl",
"pmoparadise",
"pmoqobuz", "pmoqobuz",
"pmoserver", "pmoserver",
"pmosource", "pmosource",

View File

@@ -7,7 +7,7 @@ edition = "2024"
pmoconfig = { path = "../pmoconfig" } pmoconfig = { path = "../pmoconfig" }
pmoupnp = { path = "../pmoupnp"} pmoupnp = { path = "../pmoupnp"}
pmomediarenderer = { path = "../pmomediarenderer" } pmomediarenderer = { path = "../pmomediarenderer" }
pmomediaserver = { path = "../pmomediaserver", features = ["qobuz", "api"] } pmomediaserver = { path = "../pmomediaserver", features = ["qobuz", "paradise", "api"] }
pmoserver = { path = "../pmoserver" } pmoserver = { path = "../pmoserver" }
pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmocovers = { path = "../pmocovers", features = ["pmoserver"] }
pmoapp = { path = "../pmoapp", features = ["pmoserver"] } pmoapp = { path = "../pmoapp", features = ["pmoserver"] }

View File

@@ -2,7 +2,7 @@ use pmoapp::{WebAppExt, Webapp};
use pmocovers::CoverCacheExt; use pmocovers::CoverCacheExt;
use pmomediarenderer::MEDIA_RENDERER; use pmomediarenderer::MEDIA_RENDERER;
use pmomediaserver::{MEDIA_SERVER, sources::SourcesExt, MediaServerExt, sources_api_router}; 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 pmoupnp::{UpnpServer, ssdp::SsdpServer, upnp_api::UpnpApiExt};
use tracing::info; use tracing::info;
@@ -55,11 +55,16 @@ async fn main() {
// Enregistrer les sources musicales // Enregistrer les sources musicales
info!("📡 Registering music sources..."); info!("📡 Registering music sources...");
// Enregistrer Qobuz depuis la configuration // Enregistrer Qobuz
if let Err(e) = server.register_qobuz_from_config().await { if let Err(e) = server.register_qobuz().await {
tracing::warn!("Failed to register Qobuz: {}", e); 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 // Lister toutes les sources enregistrées
let sources = server.list_music_sources().await; let sources = server.list_music_sources().await;
info!("✅ {} music source(s) registered", sources.len()); info!("✅ {} music source(s) registered", sources.len());

View File

@@ -23,6 +23,7 @@ serde_json = "1.0"
axum = { version = "0.8", optional = true } axum = { version = "0.8", optional = true }
utoipa = { version = "5.3", optional = true } utoipa = { version = "5.3", optional = true }
pmoqobuz = { path = "../pmoqobuz", optional = true } pmoqobuz = { path = "../pmoqobuz", optional = true }
pmoparadise = { path = "../pmoparadise", optional = true }
pmoconfig = { path = "../pmoconfig", optional = true } pmoconfig = { path = "../pmoconfig", optional = true }
[features] [features]
@@ -31,3 +32,5 @@ default = []
api = ["dep:axum", "dep:utoipa"] api = ["dep:axum", "dep:utoipa"]
# Feature pour activer le support Qobuz configuré # Feature pour activer le support Qobuz configuré
qobuz = ["dep:pmoqobuz", "dep:pmoconfig", "pmoqobuz/cache"] qobuz = ["dep:pmoqobuz", "dep:pmoconfig", "pmoqobuz/cache"]
# Feature pour activer le support Radio Paradise
paradise = ["dep:pmoparadise"]

View File

@@ -14,6 +14,10 @@ pub enum SourceInitError {
#[error("Failed to initialize Qobuz: {0}")] #[error("Failed to initialize Qobuz: {0}")]
QobuzError(String), QobuzError(String),
#[cfg(feature = "paradise")]
#[error("Failed to initialize Radio Paradise: {0}")]
ParadiseError(String),
#[error("Configuration error: {0}")] #[error("Configuration error: {0}")]
ConfigError(String), ConfigError(String),
@@ -46,7 +50,7 @@ pub type Result<T> = std::result::Result<T, SourceInitError>;
/// ``` /// ```
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait SourcesExt { 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 /// Cette méthode lit les credentials Qobuz depuis `pmoconfig` et crée
/// automatiquement un `QobuzSource` avec cache activé. /// automatiquement un `QobuzSource` avec cache activé.
@@ -71,10 +75,10 @@ pub trait SourcesExt {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// server.register_qobuz_from_config().await?; /// server.register_qobuz().await?;
/// ``` /// ```
#[cfg(feature = "qobuz")] #[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 /// Enregistre la source Qobuz avec des credentials explicites
/// ///
@@ -86,19 +90,38 @@ pub trait SourcesExt {
/// # Examples /// # Examples
/// ///
/// ```ignore /// ```ignore
/// server.register_qobuz("user@example.com", "password").await?; /// server.register_qobuz_with_credentials("user@example.com", "password").await?;
/// ``` /// ```
#[cfg(feature = "qobuz")] #[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] #[async_trait::async_trait]
impl SourcesExt for Server { impl SourcesExt for Server {
#[cfg(feature = "qobuz")] #[cfg(feature = "qobuz")]
async fn register_qobuz_from_config(&mut self) -> Result<()> { async fn register_qobuz(&mut self) -> Result<()> {
use pmoqobuz::{QobuzClient, QobuzSource}; use pmoqobuz::{QobuzClient, QobuzSource};
tracing::info!("Initializing Qobuz source from configuration..."); tracing::info!("Initializing Qobuz source...");
// Créer le client depuis la config // Créer le client depuis la config
let client = QobuzClient::from_config() let client = QobuzClient::from_config()
@@ -122,7 +145,7 @@ impl SourcesExt for Server {
} }
#[cfg(feature = "qobuz")] #[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}; use pmoqobuz::{QobuzClient, QobuzSource};
tracing::info!("Initializing Qobuz source with explicit credentials..."); tracing::info!("Initializing Qobuz source with explicit credentials...");
@@ -147,12 +170,34 @@ impl SourcesExt for Server {
Ok(()) Ok(())
} }
}
// Placeholder pour d'autres sources #[cfg(feature = "paradise")]
// TODO: Ajouter Radio Paradise lorsque la crate sera disponible async fn register_paradise(&mut self) -> Result<()> {
// #[cfg(feature = "radioparadise")] use pmoparadise::{RadioParadiseClient, RadioParadiseSource};
// async fn register_radioparadise_from_config(&mut self) -> Result<()> { ... }
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)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -65,6 +65,15 @@ pub struct QobuzCredentials {
pub password: Option<String>, pub password: Option<String>,
} }
/// 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<usize>,
}
/// Réponse d'enregistrement de source /// Réponse d'enregistrement de source
#[derive(Debug, Serialize, utoipa::ToSchema)] #[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct SourceRegisteredResponse { pub struct SourceRegisteredResponse {
@@ -226,6 +235,65 @@ async fn register_qobuz(Json(creds): Json<QobuzCredentials>) -> impl IntoRespons
.into_response() .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<ParadiseParams>) -> 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 /// Désenregistre une source musicale
/// ///
/// Supprime une source du registre par son ID. /// Supprime une source du registre par son ID.
@@ -281,19 +349,52 @@ async fn unregister_source(Path(id): Path<String>) -> impl IntoResponse {
pub fn sources_api_router() -> Router { pub fn sources_api_router() -> Router {
let mut router = Router::new() let mut router = Router::new()
.route("/sources", get(list_sources)) .route("/sources", get(list_sources))
.route("/sources/:id", get(get_source)) .route("/sources/{id}", get(get_source))
.route("/sources/:id", delete(unregister_source)); .route("/sources/{id}", delete(unregister_source));
#[cfg(feature = "qobuz")] #[cfg(feature = "qobuz")]
{ {
router = router.route("/sources/qobuz", post(register_qobuz)); router = router.route("/sources/qobuz", post(register_qobuz));
} }
#[cfg(feature = "paradise")]
{
router = router.route("/sources/paradise", post(register_paradise));
}
router router
} }
/// Structure pour la documentation OpenAPI /// Structure pour la documentation OpenAPI (Qobuz + Paradise)
#[cfg(feature = "qobuz")] #[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)] #[derive(utoipa::OpenApi)]
#[openapi( #[openapi(
paths( paths(
@@ -318,8 +419,34 @@ pub fn sources_api_router() -> Router {
)] )]
pub struct SourcesApiDoc; pub struct SourcesApiDoc;
/// Structure pour la documentation OpenAPI (sans Qobuz) /// Structure pour la documentation OpenAPI (Paradise uniquement)
#[cfg(not(feature = "qobuz"))] #[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)] #[derive(utoipa::OpenApi)]
#[openapi( #[openapi(
paths( paths(