Déplacer cache registry dans les crates individuelles (pattern singleton)

Au lieu d'avoir un cache_registry centralisé dans pmoupnp qui créait
des dépendances circulaires, chaque crate a maintenant son propre
singleton global :

- pmoaudiocache : register_audio_cache() + get_audio_cache()
- pmocovers : register_cover_cache() + get_cover_cache()

Changes:
- Add singleton pattern to pmoaudiocache/src/lib.rs
- Add singleton pattern to pmocovers/src/lib.rs
- Add once_cell dependency to both crates
- Update pmoplaylist to use pmoaudiocache::get_audio_cache() as fallback
- Update pmoupnp/upnp_server.rs to use register_* functions
- Update pmoupnp/lib.rs to reexport only get_* (not register_*)
- Remove pmoupnp/src/cache_registry.rs (no longer needed)

pmoupnp réexporte get_audio_cache() et get_cover_cache() pour
compatibilité avec le code existant (pmosource, etc.).
This commit is contained in:
Claude
2025-11-05 20:48:52 +00:00
parent a6cb7ac5e1
commit e06a8d95df
9 changed files with 107 additions and 274 deletions

2
Cargo.lock generated
View File

@@ -2890,6 +2890,7 @@ dependencies = [
"chrono",
"futures-util",
"lofty",
"once_cell",
"paste",
"pmocache",
"pmoconfig",
@@ -2959,6 +2960,7 @@ dependencies = [
"anyhow",
"axum 0.8.6",
"image",
"once_cell",
"pmocache",
"pmoconfig",
"pmoserver",

View File

@@ -36,6 +36,9 @@ async-trait = "0.1"
# Async
tokio = { version = "1.0", features = ["full"] }
# Singleton
once_cell = "1.20"
# Serveur HTTP (optionnel pour l'extension)
pmoserver = { path = "../pmoserver", optional = true }
pmoconfig = { path = "../pmoconfig", optional = true }

View File

@@ -100,6 +100,48 @@ pub use config_ext::AudioCacheConfigExt;
#[cfg(feature = "pmoserver")]
pub use openapi::ApiDoc;
// ============================================================================
// Registre global singleton
// ============================================================================
use once_cell::sync::OnceCell;
use std::sync::Arc;
static AUDIO_CACHE: OnceCell<Arc<Cache>> = OnceCell::new();
/// Enregistre le cache audio global
///
/// Cette fonction doit être appelée au démarrage de l'application
/// pour rendre le cache audio disponible globalement.
///
/// # Examples
///
/// ```rust,ignore
/// use pmoaudiocache::{new_cache, register_audio_cache};
/// use std::sync::Arc;
///
/// let cache = Arc::new(new_cache("./cache", 1000)?);
/// register_audio_cache(cache);
/// ```
pub fn register_audio_cache(cache: Arc<Cache>) {
let _ = AUDIO_CACHE.set(cache);
}
/// Accès global au cache audio
///
/// # Examples
///
/// ```rust,ignore
/// use pmoaudiocache::get_audio_cache;
///
/// if let Some(cache) = get_audio_cache() {
/// // Utiliser le cache
/// }
/// ```
pub fn get_audio_cache() -> Option<Arc<Cache>> {
AUDIO_CACHE.get().cloned()
}
// ============================================================================
// Extension pmoserver (inline comme pmocovers)
// ============================================================================
@@ -132,8 +174,6 @@ pub trait AudioCacheExt {
#[cfg(feature = "pmoserver")]
use pmocache::pmoserver_ext::{create_api_router, create_file_router};
#[cfg(feature = "pmoserver")]
use std::sync::Arc;
#[cfg(feature = "pmoserver")]
use utoipa::OpenApi;
#[cfg(feature = "pmoserver")]

View File

@@ -21,6 +21,9 @@ serde = { version = "1.0", features = ["derive"] }
# Async
tokio = { version = "1.0", features = ["full"] }
# Singleton
once_cell = "1.20"
# Serveur HTTP (optionnel pour l'extension)
pmoserver = { path = "../pmoserver", optional = true }
pmoconfig = { path = "../pmoconfig", optional = true }

View File

@@ -68,8 +68,52 @@ pub use openapi::ApiDoc;
#[cfg(feature = "pmoconfig")]
pub use config_ext::CoverCacheConfigExt;
#[cfg(feature = "pmoserver")]
// ============================================================================
// Registre global singleton
// ============================================================================
use once_cell::sync::OnceCell;
use std::sync::Arc;
static COVER_CACHE: OnceCell<Arc<Cache>> = OnceCell::new();
/// Enregistre le cache de couvertures global
///
/// Cette fonction doit être appelée au démarrage de l'application
/// pour rendre le cache de couvertures disponible globalement.
///
/// # Examples
///
/// ```rust,ignore
/// use pmocovers::{new_cache, register_cover_cache};
/// use std::sync::Arc;
///
/// let cache = Arc::new(new_cache("./covers", 100)?);
/// register_cover_cache(cache);
/// ```
pub fn register_cover_cache(cache: Arc<Cache>) {
let _ = COVER_CACHE.set(cache);
}
/// Accès global au cache de couvertures
///
/// # Examples
///
/// ```rust,ignore
/// use pmocovers::get_cover_cache;
///
/// if let Some(cache) = get_cover_cache() {
/// // Utiliser le cache
/// }
/// ```
pub fn get_cover_cache() -> Option<Arc<Cache>> {
COVER_CACHE.get().cloned()
}
// ============================================================================
// Extension pmoserver
// ============================================================================
#[cfg(feature = "pmoserver")]
use utoipa::OpenApi;

View File

@@ -312,8 +312,8 @@ pub(crate) fn audio_cache() -> Result<Arc<pmoaudiocache::Cache>> {
.get()
.cloned()
.or_else(|| {
// Fallback: essayer pmoupnp si disponible
pmoupnp::get_audio_cache()
// Fallback: essayer le registre global de pmoaudiocache
pmoaudiocache::get_audio_cache()
})
.ok_or_else(|| crate::Error::ManagerNotInitialized)
}

View File

@@ -1,250 +0,0 @@
//! Registre centralisé des caches pour le serveur UPnP
//!
//! Ce module gère les caches partagés entre toutes les sources musicales :
//! - Cache de couvertures d'albums (WebP)
//! - Cache de pistes audio (FLAC)
//!
//! Les caches supportent les collections, permettant à chaque source
//! d'avoir sa propre collection dans le cache partagé.
use once_cell::sync::Lazy;
use pmoaudiocache::Cache as AudioCache;
use pmocache::FileCache;
use pmocovers::Cache as CoverCache;
use std::sync::Arc;
use std::sync::RwLock;
/// Registre global des caches
///
/// 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<String>,
/// Cache de couvertures (WebP)
cover_cache: Option<Arc<CoverCache>>,
/// Cache audio (FLAC)
audio_cache: Option<Arc<AudioCache>>,
}
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<CoverCache>) {
self.cover_cache = Some(cache);
}
/// Récupérer le cache de couvertures
pub fn cover_cache(&self) -> Option<Arc<CoverCache>> {
self.cover_cache.clone()
}
/// Enregistrer le cache audio
pub fn set_audio_cache(&mut self, cache: Arc<AudioCache>) {
self.audio_cache = Some(cache);
}
/// Récupérer le cache audio
pub fn audio_cache(&self) -> Option<Arc<AudioCache>> {
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<usize>) -> anyhow::Result<String> {
let base_url = self
.base_url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?;
let cache = get_cover_cache().ok_or_else(|| anyhow::anyhow!("No registred cover cache"))?;
let param = match size {
Some(size_) => Some(size_.to_string()),
None => None,
};
let route = cache.route_for(pk, param.as_deref());
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<String> {
let base_url = self
.base_url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Base URL not set in CacheRegistry"))?;
let cache = get_audio_cache().ok_or_else(|| anyhow::anyhow!("No registred audio cache"))?;
let route = cache.route_for(pk, param);
Ok(format!("{}{}", base_url, route))
}
}
impl Default for CacheRegistry {
fn default() -> Self {
Self::new()
}
}
/// Registre global thread-safe
///
/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads.
/// Permet aux handlers et aux sources d'accéder aux caches depuis n'importe où.
pub(crate) static CACHE_REGISTRY: Lazy<RwLock<CacheRegistry>> =
Lazy::new(|| RwLock::new(CacheRegistry::new()));
/// Accès global au cache de couvertures
///
/// # Examples
///
/// ```rust,ignore
/// use pmoupnp::cache_registry::get_cover_cache;
///
/// if let Some(cache) = get_cover_cache() {
/// let pk = cache.add_from_url("http://example.com/cover.jpg").await?;
/// }
/// ```
pub fn get_cover_cache() -> Option<Arc<CoverCache>> {
CACHE_REGISTRY.read().unwrap().cover_cache()
}
/// Accès global au cache audio
///
/// # Examples
///
/// ```rust,ignore
/// use pmoupnp::cache_registry::get_audio_cache;
///
/// if let Some(cache) = get_audio_cache() {
/// let (pk, _) = cache.add_from_url("http://example.com/track.flac", None).await?;
/// }
/// ```
pub fn get_audio_cache() -> Option<Arc<AudioCache>> {
CACHE_REGISTRY.read().unwrap().audio_cache()
}
/// Enregistre le cache audio global
///
/// Cette fonction doit être appelée au démarrage de l'application
/// pour rendre le cache audio disponible globalement.
///
/// # Examples
///
/// ```rust,ignore
/// use pmoupnp::cache_registry::register_audio_cache;
/// use pmoaudiocache::Cache as AudioCache;
/// use std::sync::Arc;
///
/// let audio_cache = Arc::new(AudioCache::new("./cache", 1000)?);
/// register_audio_cache(audio_cache);
/// ```
pub fn register_audio_cache(cache: Arc<AudioCache>) {
CACHE_REGISTRY.write().unwrap().set_audio_cache(cache);
}
/// Enregistre le cache de couvertures global
///
/// Cette fonction doit être appelée au démarrage de l'application
/// pour rendre le cache de couvertures disponible globalement.
///
/// # Examples
///
/// ```rust,ignore
/// use pmoupnp::cache_registry::register_cover_cache;
/// use pmocovers::Cache as CoverCache;
/// use std::sync::Arc;
///
/// let cover_cache = Arc::new(CoverCache::new("./covers", 100)?);
/// register_cover_cache(cover_cache);
/// ```
pub fn register_cover_cache(cache: Arc<CoverCache>) {
CACHE_REGISTRY.write().unwrap().set_cover_cache(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<usize>) -> anyhow::Result<String> {
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<String> {
CACHE_REGISTRY.read().unwrap().build_audio_url(pk, param)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_registry_empty() {
let registry = CacheRegistry::new();
assert!(registry.cover_cache().is_none());
assert!(registry.audio_cache().is_none());
}
}

View File

@@ -2,7 +2,6 @@ mod object_set;
mod object_trait;
pub mod actions;
pub mod cache_registry;
pub mod devices;
pub mod services;
pub mod soap;
@@ -16,9 +15,10 @@ pub mod variable_types;
use std::sync::RwLock;
use std::{collections::HashMap, sync::Arc};
pub use crate::cache_registry::{
get_audio_cache, get_cover_cache, register_audio_cache, register_cover_cache,
};
// Réexports pour compatibilité (seulement get_*, pas register_*)
pub use pmoaudiocache::get_audio_cache;
pub use pmocovers::get_cover_cache;
pub use crate::object_trait::*;
pub use crate::upnp_server::UpnpServerExt;

View File

@@ -26,7 +26,6 @@ use pmoserver::Server;
use utoipa::OpenApi;
use crate::UpnpModel;
use crate::cache_registry::CACHE_REGISTRY;
use crate::devices::errors::DeviceError;
use crate::devices::{Device, DeviceInstance, DeviceRegistry};
use crate::ssdp::SsdpServer;
@@ -330,12 +329,8 @@ impl UpnpServerExt for Server {
let openapi = pmocovers::ApiDoc::openapi();
self.add_openapi(api_router, openapi, "covers").await;
// 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());
}
// Enregistrer le cache dans le registre global
pmocovers::register_cover_cache(cache.clone());
Ok(cache)
}
@@ -360,12 +355,8 @@ impl UpnpServerExt for Server {
let openapi = pmoaudiocache::ApiDoc::openapi();
self.add_openapi(api_router, openapi, "audio").await;
// 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());
}
// Enregistrer le cache dans le registre global
pmoaudiocache::register_audio_cache(cache.clone());
Ok(cache)
}
@@ -388,11 +379,11 @@ impl UpnpServerExt for Server {
}
fn cover_cache(&self) -> Option<Arc<CoverCache>> {
crate::cache_registry::get_cover_cache()
pmocovers::get_cover_cache()
}
fn audio_cache(&self) -> Option<Arc<AudioCache>> {
crate::cache_registry::get_audio_cache()
pmoaudiocache::get_audio_cache()
}
// ========= SSDP Management Implementation =========