From ff515e22bd78f200ca52c4a305992e2b22791c6d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 18 Oct 2025 09:58:39 +0200 Subject: [PATCH] nouveau mediarenderer --- Cargo.lock | 1 + PMOMusic/src/main.rs | 4 +- pmoaudiocache/src/cache.rs | 25 +++++++-- pmoaudiocache/src/lib.rs | 3 +- pmocache/src/cache.rs | 20 +------ pmocache/src/cache_trait.rs | 1 - pmocovers/src/cache.rs | 25 +++++++-- pmocovers/src/lib.rs | 3 +- pmomediaserver/Cargo.toml | 4 +- pmomediaserver/src/sources.rs | 30 +++------- pmomediaserver/src/sources_api.rs | 53 +++++++++++++----- pmoparadise/Cargo.toml | 2 + pmoparadise/src/source.rs | 67 +++++++++++++++++----- pmoqobuz/Cargo.toml | 2 + pmoqobuz/src/source.rs | 32 +++++++++-- pmosource/Cargo.toml | 3 +- pmosource/src/cache.rs | 73 +++++++++++++++++++----- pmoupnp/src/cache_registry.rs | 92 +++++++++++++++++++++++++++++++ pmoupnp/src/upnp_server.rs | 24 +++++--- 19 files changed, 353 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7984a2f..e425e043 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2473,6 +2473,7 @@ dependencies = [ "pmodidl", "pmoplaylist", "pmoserver", + "pmoupnp", "serde", "serde_json", "thiserror 1.0.69", diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index 223b4d80..b2aace02 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -22,7 +22,7 @@ async fn main() { .await .expect("Cannot initialise the image cache"); - info!("✅ Cover cache ready at {}", covercache.cache_dir(),); + info!("✅ Cover cache ready at {}", covercache.cache_dir().display()); info!("📡 Registering the audio cache..."); let audiocache = server @@ -30,7 +30,7 @@ async fn main() { .await .expect("Cannot initialise the audio cache"); - info!("✅ Audio cache ready at {}", audiocache.cache_dir(),); + info!("✅ Audio cache ready at {}", audiocache.cache_dir().display()); // Routes de base server diff --git a/pmoaudiocache/src/cache.rs b/pmoaudiocache/src/cache.rs index 480916df..8d0085f2 100644 --- a/pmoaudiocache/src/cache.rs +++ b/pmoaudiocache/src/cache.rs @@ -83,7 +83,6 @@ fn create_flac_transformer() -> StreamTransformer { /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre de pistes) -/// * `base_url` - URL de base pour la génération d'URLs /// /// # Returns /// @@ -94,11 +93,11 @@ fn create_flac_transformer() -> StreamTransformer { /// ```rust,no_run /// use pmoaudiocache::cache; /// -/// let cache = cache::new_cache("./audio_cache", 1000, "http://localhost:8080").unwrap(); +/// let cache = cache::new_cache("./audio_cache", 1000).unwrap(); /// ``` -pub fn new_cache(dir: &str, limit: usize, base_url: &str) -> Result { +pub fn new_cache(dir: &str, limit: usize) -> Result { let transformer_factory = Arc::new(|| create_flac_transformer()); - Cache::with_transformer(dir, limit, base_url, Some(transformer_factory)) + Cache::with_transformer(dir, limit, Some(transformer_factory)) } /// Ajoute une piste audio depuis une URL avec extraction et stockage des métadonnées @@ -201,3 +200,21 @@ pub fn get_metadata(cache: &Cache, pk: &str) -> Result) -> String { + if let Some(p) = param { + format!("/audio/tracks/{}/{}", pk, p) + } else { + format!("/audio/tracks/{}", pk) + } +} diff --git a/pmoaudiocache/src/lib.rs b/pmoaudiocache/src/lib.rs index 6799c2ca..4715666c 100644 --- a/pmoaudiocache/src/lib.rs +++ b/pmoaudiocache/src/lib.rs @@ -179,8 +179,7 @@ use utoipa::OpenApi; #[cfg(feature = "pmoserver")] impl AudioCacheExt for pmoserver::Server { async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result> { - let base_url = self.info().base_url; - let cache = Arc::new(crate::cache::new_cache(cache_dir, limit, &base_url)?); + let cache = Arc::new(crate::cache::new_cache(cache_dir, limit)?); // Router de fichiers pour servir les pistes FLAC // Routes: GET /audio/tracks/{pk} et GET /audio/tracks/{pk}/{param} diff --git a/pmocache/src/cache.rs b/pmocache/src/cache.rs index 2f5582fc..63e45d8c 100644 --- a/pmocache/src/cache.rs +++ b/pmocache/src/cache.rs @@ -52,8 +52,6 @@ pub struct Cache { dir: PathBuf, /// Limite de taille du cache (nombre d'éléments) limit: usize, - /// URL de base pour la génération d'URLs - base_url: String, /// Base de données SQLite pub db: Arc, /// Map des downloads en cours (pk -> Download) @@ -71,9 +69,8 @@ impl Cache { /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre d'éléments) - /// * `base_url` - URL de base pour la génération d'URLs - pub fn new(dir: &str, limit: usize, base_url: &str) -> Result { - Self::with_transformer(dir, limit, base_url, None) + pub fn new(dir: &str, limit: usize) -> Result { + Self::with_transformer(dir, limit, None) } /// Crée un nouveau cache avec un transformer optionnel @@ -82,7 +79,6 @@ impl Cache { /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre d'éléments) - /// * `base_url` - URL de base pour la génération d'URLs /// * `transformer_factory` - Factory pour créer des transformers à chaque téléchargement /// /// # Exemple @@ -109,14 +105,12 @@ impl Cache { /// let cache = Cache::::with_transformer( /// "./cache", /// 1000, - /// "http://localhost:8080", /// Some(transformer_factory) /// ).unwrap(); /// ``` pub fn with_transformer( dir: &str, limit: usize, - base_url: &str, transformer_factory: Option StreamTransformer + Send + Sync>>, ) -> Result { let directory = PathBuf::from(dir); @@ -126,7 +120,6 @@ impl Cache { Ok(Self { dir: directory, limit, - base_url: base_url.to_string(), db: Arc::new(db), downloads: Arc::new(RwLock::new(HashMap::new())), transformer_factory, @@ -456,11 +449,6 @@ impl Cache { &self.dir } - /// Retourne l'URL de base - pub fn get_base_url(&self) -> &str { - &self.base_url - } - /// Construit le chemin complet d'un fichier dans le cache avec le param par défaut /// /// Format: `{pk}.{default_param}.{extension}` @@ -546,10 +534,6 @@ impl FileCache for Cache { self.db.clone() } - fn get_base_url(&self) -> &str { - &self.base_url - } - fn validate_data(&self, data: &[u8]) -> Result> { // Le cache générique accepte toutes les données Ok(data.to_vec()) diff --git a/pmocache/src/cache_trait.rs b/pmocache/src/cache_trait.rs index a6845115..60c212b9 100644 --- a/pmocache/src/cache_trait.rs +++ b/pmocache/src/cache_trait.rs @@ -11,7 +11,6 @@ pub trait FileCache: Send + Sync { fn get_cache_dir(&self) -> &Path; fn get_database(&self) -> Arc; - fn get_base_url(&self) -> &str; /// Valide les données avant de les stocker dans le cache /// diff --git a/pmocovers/src/cache.rs b/pmocovers/src/cache.rs index 44a7f112..aaf8ba42 100644 --- a/pmocovers/src/cache.rs +++ b/pmocovers/src/cache.rs @@ -63,7 +63,6 @@ fn create_webp_transformer() -> StreamTransformer { /// /// * `dir` - Répertoire de stockage du cache /// * `limit` - Limite de taille du cache (nombre d'images) -/// * `base_url` - URL de base pour la génération d'URLs /// /// # Returns /// @@ -74,9 +73,27 @@ fn create_webp_transformer() -> StreamTransformer { /// ```rust,no_run /// use pmocovers::cache; /// -/// let cache = cache::new_cache("./cache", 1000, "http://localhost:8080").unwrap(); +/// let cache = cache::new_cache("./cache", 1000).unwrap(); /// ``` -pub fn new_cache(dir: &str, limit: usize, base_url: &str) -> Result { +pub fn new_cache(dir: &str, limit: usize) -> Result { let transformer_factory = Arc::new(|| create_webp_transformer()); - Cache::with_transformer(dir, limit, base_url, Some(transformer_factory)) + Cache::with_transformer(dir, limit, Some(transformer_factory)) +} + +/// Retourne la route relative pour accéder à une couverture +/// +/// # Arguments +/// +/// * `pk` - Clé primaire de l'image +/// * `size` - Taille optionnelle de l'image +/// +/// # Returns +/// +/// Route relative (ex: "/covers/images/abc123" ou "/covers/images/abc123/300") +pub fn route_for(pk: &str, size: Option) -> String { + if let Some(s) = size { + format!("/covers/images/{}/{}", pk, s) + } else { + format!("/covers/images/{}", pk) + } } diff --git a/pmocovers/src/lib.rs b/pmocovers/src/lib.rs index 91331667..b99e8485 100644 --- a/pmocovers/src/lib.rs +++ b/pmocovers/src/lib.rs @@ -114,8 +114,7 @@ impl CoverCacheExt for pmoserver::Server { -> anyhow::Result> { use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router}; - let base_url = self.info().base_url; - let cache = Arc::new(cache::new_cache(cache_dir, limit, &base_url)?); + let cache = Arc::new(cache::new_cache(cache_dir, limit)?); // Router de fichiers avec génération de variantes // Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size} diff --git a/pmomediaserver/Cargo.toml b/pmomediaserver/Cargo.toml index f4f57c77..eb376dbf 100644 --- a/pmomediaserver/Cargo.toml +++ b/pmomediaserver/Cargo.toml @@ -31,6 +31,6 @@ default = ["pmosource/server"] # Feature pour activer l'API REST de gestion des sources api = ["dep:axum", "dep:utoipa", "pmosource/server"] # Feature pour activer le support Qobuz configuré -qobuz = ["api", "dep:pmoqobuz", "dep:pmoconfig", "pmoqobuz/cache"] +qobuz = ["api", "dep:pmoqobuz", "dep:pmoconfig", "pmoqobuz/server"] # Feature pour activer le support Radio Paradise -paradise = ["api", "dep:pmoparadise"] +paradise = ["api", "dep:pmoparadise", "pmoparadise/server"] diff --git a/pmomediaserver/src/sources.rs b/pmomediaserver/src/sources.rs index 04532a34..9ee02b13 100644 --- a/pmomediaserver/src/sources.rs +++ b/pmomediaserver/src/sources.rs @@ -128,13 +128,9 @@ impl SourcesExt for Server { .await .map_err(|e| SourceInitError::QobuzError(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 - let source = QobuzSource::new(client, &base_url); + // Créer la source depuis le registry + let source = QobuzSource::from_registry(client) + .map_err(|e| SourceInitError::QobuzError(format!("Failed to create source: {}", e)))?; // Enregistrer la source self.register_music_source(Arc::new(source)).await; @@ -155,13 +151,9 @@ impl SourcesExt for Server { .await .map_err(|e| SourceInitError::QobuzError(format!("Failed to authenticate: {}", 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 - let source = QobuzSource::new(client, &base_url); + // Créer la source depuis le registry + let source = QobuzSource::from_registry(client) + .map_err(|e| SourceInitError::QobuzError(format!("Failed to create source: {}", e)))?; // Enregistrer la source self.register_music_source(Arc::new(source)).await; @@ -182,13 +174,9 @@ impl SourcesExt for Server { .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); + // Créer la source depuis le registry avec capacité FIFO par défaut + let source = RadioParadiseSource::from_registry_default(client) + .map_err(|e| SourceInitError::ParadiseError(format!("Failed to create source: {}", e)))?; // Enregistrer la source self.register_music_source(Arc::new(source)).await; diff --git a/pmomediaserver/src/sources_api.rs b/pmomediaserver/src/sources_api.rs index ecb98ae0..d4231191 100644 --- a/pmomediaserver/src/sources_api.rs +++ b/pmomediaserver/src/sources_api.rs @@ -97,13 +97,19 @@ async fn register_qobuz(Json(creds): Json) -> impl IntoRespons } }; - // 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 = Arc::new(QobuzSource::new(client, &base_url)); + // Créer et enregistrer la source depuis le registry + let source = match QobuzSource::from_registry(client) { + Ok(s) => Arc::new(s), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Failed to create source: {}", e), + }), + ) + .into_response(); + } + }; let source_id = source.as_ref().id().to_string(); register_source(source).await; @@ -150,16 +156,33 @@ async fn register_paradise(Json(params): Json) -> impl IntoRespo } }; - // 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 + // Créer et enregistrer la source depuis le registry let source = if let Some(capacity) = params.fifo_capacity { - Arc::new(RadioParadiseSource::new(client, &base_url, capacity)) + match RadioParadiseSource::from_registry(client, capacity) { + Ok(s) => Arc::new(s), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Failed to create source: {}", e), + }), + ) + .into_response(); + } + } } else { - Arc::new(RadioParadiseSource::new_default(client, &base_url)) + match RadioParadiseSource::from_registry_default(client) { + Ok(s) => Arc::new(s), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Failed to create source: {}", e), + }), + ) + .into_response(); + } + } }; let source_id = source.as_ref().id().to_string(); diff --git a/pmoparadise/Cargo.toml b/pmoparadise/Cargo.toml index c4d3cf8a..a3a1a562 100644 --- a/pmoparadise/Cargo.toml +++ b/pmoparadise/Cargo.toml @@ -63,6 +63,8 @@ metadata-only = [] per-track = ["dep:claxon", "dep:hound", "dep:tempfile"] # Active le media server UPnP mediaserver = ["dep:pmoupnp", "dep:pmoserver", "dep:pmodidl", "dep:uuid"] +# Feature pour activer le support serveur (cache registry) +server = ["pmosource/server"] # Feature cache (deprecated - toujours actif maintenant) cache = [] diff --git a/pmoparadise/src/source.rs b/pmoparadise/src/source.rs index e1fa9d78..bb929800 100644 --- a/pmoparadise/src/source.rs +++ b/pmoparadise/src/source.rs @@ -79,18 +79,56 @@ impl std::fmt::Debug for RadioParadiseSource { } impl RadioParadiseSource { - /// Create a new Radio Paradise source with caches + /// Create a new Radio Paradise source from the cache registry + /// + /// This is the recommended way to create a source when using the UPnP server. + /// The caches are automatically retrieved from the global registry. + /// + /// # Arguments + /// + /// * `client` - Radio Paradise API client + /// * `fifo_capacity` - Maximum number of tracks in the FIFO + /// + /// # Errors + /// + /// Returns an error if the caches are not initialized in the registry + #[cfg(feature = "server")] + pub fn from_registry(client: RadioParadiseClient, fifo_capacity: usize) -> Result { + let playlist = FifoPlaylist::new( + "radio-paradise".to_string(), + "Radio Paradise".to_string(), + fifo_capacity, + DEFAULT_IMAGE, + ); + + let cache_manager = SourceCacheManager::from_registry("radio-paradise".to_string())?; + + Ok(Self { + inner: Arc::new(RadioParadiseSourceInner { + client, + playlist, + cache_manager, + blocks: tokio::sync::RwLock::new(std::collections::HashMap::new()), + }), + }) + } + + /// Create with default FIFO capacity from the cache registry + #[cfg(feature = "server")] + pub fn from_registry_default(client: RadioParadiseClient) -> Result { + Self::from_registry(client, DEFAULT_FIFO_CAPACITY) + } + + /// Create a new Radio Paradise source with explicit caches (for tests) /// /// # Arguments /// /// * `client` - Radio Paradise API client - /// * `cache_base_url` - Base URL for the cache server (e.g., "http://localhost:8080") /// * `fifo_capacity` - Maximum number of tracks in the FIFO /// * `cover_cache` - Cover image cache (required) /// * `audio_cache` - Audio cache (required) pub fn new( client: RadioParadiseClient, - cache_base_url: impl Into, fifo_capacity: usize, cover_cache: Arc, audio_cache: Arc, @@ -102,9 +140,7 @@ impl RadioParadiseSource { DEFAULT_IMAGE, ); - let cache_base_url = cache_base_url.into(); let cache_manager = SourceCacheManager::new( - cache_base_url.clone(), "radio-paradise".to_string(), cover_cache, audio_cache, @@ -120,14 +156,13 @@ impl RadioParadiseSource { } } - /// Create with default FIFO capacity + /// Create with default FIFO capacity (for tests) pub fn new_default( client: RadioParadiseClient, - cache_base_url: impl Into, cover_cache: Arc, audio_cache: Arc, ) -> Self { - Self::new(client, cache_base_url, DEFAULT_FIFO_CAPACITY, cover_cache, audio_cache) + Self::new(client, DEFAULT_FIFO_CAPACITY, cover_cache, audio_cache) } /// Add a track from a Radio Paradise song and block @@ -160,9 +195,17 @@ impl RadioParadiseSource { match self.inner.cache_manager.cache_cover(&image_url).await { Ok(pk) => { // Use the cached cover URL - let cached_url = self.inner.cache_manager.cover_url(&pk, None); - track = track.with_image(cached_url); - Some(pk) + match self.inner.cache_manager.cover_url(&pk, None) { + Ok(cached_url) => { + track = track.with_image(cached_url); + Some(pk) + } + Err(e) => { + tracing::warn!("Failed to build cover URL for {}: {}", pk, e); + track = track.with_image(image_url); + Some(pk) + } + } } Err(e) => { tracing::warn!("Failed to cache cover image {}: {}", image_url, e); @@ -583,7 +626,6 @@ mod tests { let (cover_cache, audio_cache) = create_test_caches().await; let source = RadioParadiseSource::new_default( client, - "http://localhost:8080", cover_cache, audio_cache ); @@ -610,7 +652,6 @@ mod tests { let (cover_cache, audio_cache) = create_test_caches().await; let source = RadioParadiseSource::new_default( client, - "http://localhost:8080", cover_cache, audio_cache ); diff --git a/pmoqobuz/Cargo.toml b/pmoqobuz/Cargo.toml index 314b344f..f162da36 100644 --- a/pmoqobuz/Cargo.toml +++ b/pmoqobuz/Cargo.toml @@ -57,6 +57,8 @@ pmosource = { path = "../pmosource" } default = [] # Feature pour activer les extensions pmoserver pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"] +# Feature pour activer le support serveur (cache registry) +server = ["pmosource/server"] # Feature cache (deprecated - toujours actif maintenant) cache = [] diff --git a/pmoqobuz/src/source.rs b/pmoqobuz/src/source.rs index a794871a..aecd47df 100644 --- a/pmoqobuz/src/source.rs +++ b/pmoqobuz/src/source.rs @@ -82,23 +82,45 @@ impl std::fmt::Debug for QobuzSource { } impl QobuzSource { - /// Create a new Qobuz source with caches + /// Create a new Qobuz source from the cache registry + /// + /// This is the recommended way to create a source when using the UPnP server. + /// The caches are automatically retrieved from the global registry. + /// + /// # Arguments + /// + /// * `client` - Authenticated Qobuz API client + /// + /// # Errors + /// + /// Returns an error if the caches are not initialized in the registry + #[cfg(feature = "server")] + pub fn from_registry(client: QobuzClient) -> Result { + let cache_manager = SourceCacheManager::from_registry("qobuz".to_string())?; + + Ok(Self { + inner: Arc::new(QobuzSourceInner { + client, + cache_manager, + update_counter: tokio::sync::RwLock::new(0), + last_change: tokio::sync::RwLock::new(SystemTime::now()), + }), + }) + } + + /// Create a new Qobuz source with explicit caches (for tests) /// /// # Arguments /// /// * `client` - Authenticated Qobuz API client - /// * `cache_base_url` - Base URL for the cache server (e.g., "http://localhost:8080") /// * `cover_cache` - Cover image cache (required) /// * `audio_cache` - Audio cache (required) pub fn new( client: QobuzClient, - cache_base_url: impl Into, cover_cache: Arc, audio_cache: Arc, ) -> Self { - let cache_base_url = cache_base_url.into(); let cache_manager = SourceCacheManager::new( - cache_base_url.clone(), "qobuz".to_string(), cover_cache, audio_cache, diff --git a/pmosource/Cargo.toml b/pmosource/Cargo.toml index 39b8425b..1ccc05b9 100644 --- a/pmosource/Cargo.toml +++ b/pmosource/Cargo.toml @@ -33,6 +33,7 @@ pmocovers = { path = "../pmocovers", optional = true } # Server extension (optional) pmoserver = { path = "../pmoserver", optional = true } pmoconfig = { path = "../pmoconfig", optional = true } +pmoupnp = { path = "../pmoupnp", optional = true } # Web framework for API (optional) axum = { version = "0.8", optional = true } @@ -45,4 +46,4 @@ lazy_static = { version = "1.4", optional = true } [features] default = ["cache"] cache = ["pmoaudiocache", "pmocovers"] -server = ["pmoserver", "pmoconfig", "axum", "serde", "serde_json", "utoipa", "tracing", "lazy_static"] +server = ["pmoserver", "pmoconfig", "pmoupnp", "axum", "serde", "serde_json", "utoipa", "tracing", "lazy_static"] diff --git a/pmosource/src/cache.rs b/pmosource/src/cache.rs index e4b65ff6..630783e4 100644 --- a/pmosource/src/cache.rs +++ b/pmosource/src/cache.rs @@ -47,9 +47,6 @@ pub struct SourceCacheManager { /// Métadonnées des pistes (track_id → metadata) track_cache: RwLock>, - /// URL de base du serveur - cache_base_url: String, - /// ID de collection pour cette source (ex: "radio-paradise", "qobuz") collection_id: String, @@ -61,23 +58,56 @@ pub struct SourceCacheManager { } impl SourceCacheManager { - /// Créer un nouveau manager + /// Créer un nouveau manager depuis le registre de caches + /// + /// Cette méthode utilise le registre global de caches (`CACHE_REGISTRY`) + /// pour récupérer les caches centralisés du serveur. + /// + /// # Arguments + /// + /// * `collection_id` - ID de collection pour cette source (ex: "radio-paradise", "qobuz") + /// + /// # Returns + /// + /// Un nouveau `SourceCacheManager` configuré avec les caches centralisés + /// + /// # Errors + /// + /// Retourne une erreur si les caches ne sont pas encore initialisés dans le registre + #[cfg(feature = "server")] + pub fn from_registry(collection_id: String) -> Result { + let cover_cache = pmoupnp::cache_registry::get_cover_cache() + .ok_or_else(|| MusicSourceError::CacheError( + "Cover cache not initialized in registry".to_string() + ))?; + + let audio_cache = pmoupnp::cache_registry::get_audio_cache() + .ok_or_else(|| MusicSourceError::CacheError( + "Audio cache not initialized in registry".to_string() + ))?; + + Ok(Self { + track_cache: RwLock::new(HashMap::new()), + collection_id, + cover_cache, + audio_cache, + }) + } + + /// Créer un nouveau manager (ancien constructeur pour tests) /// /// # Arguments /// - /// * `cache_base_url` - URL de base du serveur /// * `collection_id` - ID de collection (source ID) /// * `cover_cache` - Cache de couvertures centralisé /// * `audio_cache` - Cache audio centralisé pub fn new( - cache_base_url: String, collection_id: String, cover_cache: Arc, audio_cache: Arc, ) -> Self { Self { track_cache: RwLock::new(HashMap::new()), - cache_base_url, collection_id, cover_cache, audio_cache, @@ -93,7 +123,18 @@ impl SourceCacheManager { if let Some(metadata) = cache.get(object_id) { if let Some(ref pk) = metadata.cached_audio_pk { - return Ok(format!("{}/audio/tracks/{}/stream", self.cache_base_url, pk)); + #[cfg(feature = "server")] + { + let url = pmoupnp::cache_registry::build_audio_url(pk, Some("stream")) + .map_err(|e| MusicSourceError::CacheError(e.to_string()))?; + return Ok(url); + } + #[cfg(not(feature = "server"))] + { + return Err(MusicSourceError::CacheError( + "Server feature not enabled".to_string() + )); + } } return Ok(metadata.original_uri.clone()); } @@ -140,11 +181,17 @@ impl SourceCacheManager { /// # Returns /// /// L'URL complète de l'image - pub fn cover_url(&self, pk: &str, size: Option) -> String { - if let Some(s) = size { - format!("{}/covers/images/{}/{}", self.cache_base_url, pk, s) - } else { - format!("{}/covers/images/{}", self.cache_base_url, pk) + pub fn cover_url(&self, pk: &str, size: Option) -> Result { + #[cfg(feature = "server")] + { + pmoupnp::cache_registry::build_cover_url(pk, size) + .map_err(|e| MusicSourceError::CacheError(e.to_string())) + } + #[cfg(not(feature = "server"))] + { + Err(MusicSourceError::CacheError( + "Server feature not enabled - cannot build cover URL".to_string() + )) } } diff --git a/pmoupnp/src/cache_registry.rs b/pmoupnp/src/cache_registry.rs index 94c126c0..88c648aa 100644 --- a/pmoupnp/src/cache_registry.rs +++ b/pmoupnp/src/cache_registry.rs @@ -18,6 +18,9 @@ use pmoaudiocache::Cache as AudioCache; /// Contient les instances partagées des caches de couvertures et audio. /// Ces caches sont uniques et partagés entre toutes les sources musicales. pub struct CacheRegistry { + /// URL de base du serveur (ex: "http://localhost:8080") + base_url: Option, + /// Cache de couvertures (WebP) cover_cache: Option>, @@ -29,11 +32,22 @@ impl CacheRegistry { /// Créer un nouveau registre vide pub fn new() -> Self { Self { + base_url: None, cover_cache: None, audio_cache: None, } } + /// Définir l'URL de base du serveur + pub fn set_base_url(&mut self, url: String) { + self.base_url = Some(url); + } + + /// Récupérer l'URL de base du serveur + pub fn base_url(&self) -> Option<&str> { + self.base_url.as_deref() + } + /// Enregistrer le cache de couvertures pub fn set_cover_cache(&mut self, cache: Arc) { self.cover_cache = Some(cache); @@ -53,6 +67,42 @@ impl CacheRegistry { pub fn audio_cache(&self) -> Option> { self.audio_cache.clone() } + + /// Construit l'URL complète pour une couverture + /// + /// # Arguments + /// + /// * `pk` - Clé primaire de la couverture + /// * `size` - Taille optionnelle de l'image + /// + /// # Returns + /// + /// URL complète (ex: "http://localhost:8080/covers/images/abc123/300") + pub fn build_cover_url(&self, pk: &str, size: Option) -> anyhow::Result { + let base_url = self.base_url + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?; + let route = pmocovers::cache::route_for(pk, size); + Ok(format!("{}{}", base_url, route)) + } + + /// Construit l'URL complète pour une piste audio + /// + /// # Arguments + /// + /// * `pk` - Clé primaire de la piste + /// * `param` - Paramètre optionnel (ex: "orig", "128k") + /// + /// # Returns + /// + /// URL complète (ex: "http://localhost:8080/audio/tracks/abc123/orig") + pub fn build_audio_url(&self, pk: &str, param: Option<&str>) -> anyhow::Result { + let base_url = self.base_url + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?; + let route = pmoaudiocache::cache::route_for(pk, param); + Ok(format!("{}{}", base_url, route)) + } } impl Default for CacheRegistry { @@ -99,6 +149,48 @@ pub fn get_audio_cache() -> Option> { CACHE_REGISTRY.read().unwrap().audio_cache() } +/// Construit l'URL complète pour une couverture +/// +/// Fonction globale qui utilise le registre de caches pour construire l'URL. +/// +/// # Arguments +/// +/// * `pk` - Clé primaire de la couverture +/// * `size` - Taille optionnelle de l'image +/// +/// # Examples +/// +/// ```rust,ignore +/// use pmoupnp::cache_registry::build_cover_url; +/// +/// let url = build_cover_url("abc123", Some(300))?; +/// // url = "http://localhost:8080/covers/images/abc123/300" +/// ``` +pub fn build_cover_url(pk: &str, size: Option) -> anyhow::Result { + CACHE_REGISTRY.read().unwrap().build_cover_url(pk, size) +} + +/// Construit l'URL complète pour une piste audio +/// +/// Fonction globale qui utilise le registre de caches pour construire l'URL. +/// +/// # Arguments +/// +/// * `pk` - Clé primaire de la piste +/// * `param` - Paramètre optionnel (ex: "orig", "128k") +/// +/// # Examples +/// +/// ```rust,ignore +/// use pmoupnp::cache_registry::build_audio_url; +/// +/// let url = build_audio_url("abc123", Some("orig"))?; +/// // url = "http://localhost:8080/audio/tracks/abc123/orig" +/// ``` +pub fn build_audio_url(pk: &str, param: Option<&str>) -> anyhow::Result { + CACHE_REGISTRY.read().unwrap().build_audio_url(pk, param) +} + #[cfg(test)] mod tests { use super::*; diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index 3ddf53dd..baf1ab6e 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -186,8 +186,8 @@ impl UpnpServerExt for Server { use pmocovers::new_cache; use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router}; - let base_url = self.info().base_url; - let cache = Arc::new(new_cache(cache_dir, limit, &base_url)?); + let base_url = self.info().base_url.clone(); + let cache = Arc::new(new_cache(cache_dir, limit)?); // Routes de fichiers avec génération de variantes // Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size} @@ -220,8 +220,12 @@ impl UpnpServerExt for Server { let openapi = pmocovers::ApiDoc::openapi(); self.add_openapi(api_router, openapi, "covers").await; - // Enregistrer dans le registre global - CACHE_REGISTRY.write().unwrap().set_cover_cache(cache.clone()); + // Enregistrer base_url et cache dans le registre global + { + let mut registry = CACHE_REGISTRY.write().unwrap(); + registry.set_base_url(base_url); + registry.set_cover_cache(cache.clone()); + } Ok(cache) } @@ -231,8 +235,8 @@ impl UpnpServerExt for Server { use pmoaudiocache::new_cache; use pmocache::pmoserver_ext::{create_file_router, create_api_router}; - let base_url = self.info().base_url; - let cache = Arc::new(new_cache(cache_dir, limit, &base_url)?); + let base_url = self.info().base_url.clone(); + let cache = Arc::new(new_cache(cache_dir, limit)?); // Routes de fichiers pour servir les pistes FLAC let file_router = create_file_router(cache.clone(), "audio/flac"); @@ -243,8 +247,12 @@ impl UpnpServerExt for Server { let openapi = pmoaudiocache::ApiDoc::openapi(); self.add_openapi(api_router, openapi, "audio").await; - // Enregistrer dans le registre global - CACHE_REGISTRY.write().unwrap().set_audio_cache(cache.clone()); + // Enregistrer base_url et cache dans le registre global + { + let mut registry = CACHE_REGISTRY.write().unwrap(); + registry.set_base_url(base_url); + registry.set_audio_cache(cache.clone()); + } Ok(cache) }