nouveau mediarenderer

This commit is contained in:
2025-10-18 09:58:39 +02:00
parent d86cfe46df
commit ff515e22bd
19 changed files with 353 additions and 111 deletions

1
Cargo.lock generated
View File

@@ -2473,6 +2473,7 @@ dependencies = [
"pmodidl", "pmodidl",
"pmoplaylist", "pmoplaylist",
"pmoserver", "pmoserver",
"pmoupnp",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 1.0.69", "thiserror 1.0.69",

View File

@@ -22,7 +22,7 @@ async fn main() {
.await .await
.expect("Cannot initialise the image cache"); .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..."); info!("📡 Registering the audio cache...");
let audiocache = server let audiocache = server
@@ -30,7 +30,7 @@ async fn main() {
.await .await
.expect("Cannot initialise the audio cache"); .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 // Routes de base
server server

View File

@@ -83,7 +83,6 @@ fn create_flac_transformer() -> StreamTransformer {
/// ///
/// * `dir` - Répertoire de stockage du cache /// * `dir` - Répertoire de stockage du cache
/// * `limit` - Limite de taille du cache (nombre de pistes) /// * `limit` - Limite de taille du cache (nombre de pistes)
/// * `base_url` - URL de base pour la génération d'URLs
/// ///
/// # Returns /// # Returns
/// ///
@@ -94,11 +93,11 @@ fn create_flac_transformer() -> StreamTransformer {
/// ```rust,no_run /// ```rust,no_run
/// use pmoaudiocache::cache; /// 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<Cache> { pub fn new_cache(dir: &str, limit: usize) -> Result<Cache> {
let transformer_factory = Arc::new(|| create_flac_transformer()); 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 /// 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<crate::metadata::AudioMet
Ok(metadata) Ok(metadata)
} }
/// Retourne la route relative pour accéder à une piste audio
///
/// # Arguments
///
/// * `pk` - Clé primaire de la piste
/// * `param` - Paramètre optionnel (ex: "orig", "128k", etc.)
///
/// # Returns
///
/// Route relative (ex: "/audio/tracks/abc123" ou "/audio/tracks/abc123/orig")
pub fn route_for(pk: &str, param: Option<&str>) -> String {
if let Some(p) = param {
format!("/audio/tracks/{}/{}", pk, p)
} else {
format!("/audio/tracks/{}", pk)
}
}

View File

@@ -179,8 +179,7 @@ use utoipa::OpenApi;
#[cfg(feature = "pmoserver")] #[cfg(feature = "pmoserver")]
impl AudioCacheExt for pmoserver::Server { impl AudioCacheExt for pmoserver::Server {
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<Arc<Cache>> { async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<Arc<Cache>> {
let base_url = self.info().base_url; let cache = Arc::new(crate::cache::new_cache(cache_dir, limit)?);
let cache = Arc::new(crate::cache::new_cache(cache_dir, limit, &base_url)?);
// Router de fichiers pour servir les pistes FLAC // Router de fichiers pour servir les pistes FLAC
// Routes: GET /audio/tracks/{pk} et GET /audio/tracks/{pk}/{param} // Routes: GET /audio/tracks/{pk} et GET /audio/tracks/{pk}/{param}

View File

@@ -52,8 +52,6 @@ pub struct Cache<C: CacheConfig> {
dir: PathBuf, dir: PathBuf,
/// Limite de taille du cache (nombre d'éléments) /// Limite de taille du cache (nombre d'éléments)
limit: usize, limit: usize,
/// URL de base pour la génération d'URLs
base_url: String,
/// Base de données SQLite /// Base de données SQLite
pub db: Arc<DB>, pub db: Arc<DB>,
/// Map des downloads en cours (pk -> Download) /// Map des downloads en cours (pk -> Download)
@@ -71,9 +69,8 @@ impl<C: CacheConfig> Cache<C> {
/// ///
/// * `dir` - Répertoire de stockage du cache /// * `dir` - Répertoire de stockage du cache
/// * `limit` - Limite de taille du cache (nombre d'éléments) /// * `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) -> Result<Self> {
pub fn new(dir: &str, limit: usize, base_url: &str) -> Result<Self> { Self::with_transformer(dir, limit, None)
Self::with_transformer(dir, limit, base_url, None)
} }
/// Crée un nouveau cache avec un transformer optionnel /// Crée un nouveau cache avec un transformer optionnel
@@ -82,7 +79,6 @@ impl<C: CacheConfig> Cache<C> {
/// ///
/// * `dir` - Répertoire de stockage du cache /// * `dir` - Répertoire de stockage du cache
/// * `limit` - Limite de taille du cache (nombre d'éléments) /// * `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 /// * `transformer_factory` - Factory pour créer des transformers à chaque téléchargement
/// ///
/// # Exemple /// # Exemple
@@ -109,14 +105,12 @@ impl<C: CacheConfig> Cache<C> {
/// let cache = Cache::<MyConfig>::with_transformer( /// let cache = Cache::<MyConfig>::with_transformer(
/// "./cache", /// "./cache",
/// 1000, /// 1000,
/// "http://localhost:8080",
/// Some(transformer_factory) /// Some(transformer_factory)
/// ).unwrap(); /// ).unwrap();
/// ``` /// ```
pub fn with_transformer( pub fn with_transformer(
dir: &str, dir: &str,
limit: usize, limit: usize,
base_url: &str,
transformer_factory: Option<Arc<dyn Fn() -> StreamTransformer + Send + Sync>>, transformer_factory: Option<Arc<dyn Fn() -> StreamTransformer + Send + Sync>>,
) -> Result<Self> { ) -> Result<Self> {
let directory = PathBuf::from(dir); let directory = PathBuf::from(dir);
@@ -126,7 +120,6 @@ impl<C: CacheConfig> Cache<C> {
Ok(Self { Ok(Self {
dir: directory, dir: directory,
limit, limit,
base_url: base_url.to_string(),
db: Arc::new(db), db: Arc::new(db),
downloads: Arc::new(RwLock::new(HashMap::new())), downloads: Arc::new(RwLock::new(HashMap::new())),
transformer_factory, transformer_factory,
@@ -456,11 +449,6 @@ impl<C: CacheConfig> Cache<C> {
&self.dir &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 /// Construit le chemin complet d'un fichier dans le cache avec le param par défaut
/// ///
/// Format: `{pk}.{default_param}.{extension}` /// Format: `{pk}.{default_param}.{extension}`
@@ -546,10 +534,6 @@ impl<C: CacheConfig> FileCache<C> for Cache<C> {
self.db.clone() self.db.clone()
} }
fn get_base_url(&self) -> &str {
&self.base_url
}
fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> { fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> {
// Le cache générique accepte toutes les données // Le cache générique accepte toutes les données
Ok(data.to_vec()) Ok(data.to_vec())

View File

@@ -11,7 +11,6 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
fn get_cache_dir(&self) -> &Path; fn get_cache_dir(&self) -> &Path;
fn get_database(&self) -> Arc<DB>; fn get_database(&self) -> Arc<DB>;
fn get_base_url(&self) -> &str;
/// Valide les données avant de les stocker dans le cache /// Valide les données avant de les stocker dans le cache
/// ///

View File

@@ -63,7 +63,6 @@ fn create_webp_transformer() -> StreamTransformer {
/// ///
/// * `dir` - Répertoire de stockage du cache /// * `dir` - Répertoire de stockage du cache
/// * `limit` - Limite de taille du cache (nombre d'images) /// * `limit` - Limite de taille du cache (nombre d'images)
/// * `base_url` - URL de base pour la génération d'URLs
/// ///
/// # Returns /// # Returns
/// ///
@@ -74,9 +73,27 @@ fn create_webp_transformer() -> StreamTransformer {
/// ```rust,no_run /// ```rust,no_run
/// use pmocovers::cache; /// 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<Cache> { pub fn new_cache(dir: &str, limit: usize) -> Result<Cache> {
let transformer_factory = Arc::new(|| create_webp_transformer()); 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<usize>) -> String {
if let Some(s) = size {
format!("/covers/images/{}/{}", pk, s)
} else {
format!("/covers/images/{}", pk)
}
} }

View File

@@ -114,8 +114,7 @@ impl CoverCacheExt for pmoserver::Server {
-> anyhow::Result<Arc<Cache>> { -> anyhow::Result<Arc<Cache>> {
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router}; 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)?);
let cache = Arc::new(cache::new_cache(cache_dir, limit, &base_url)?);
// Router de fichiers avec génération de variantes // Router de fichiers avec génération de variantes
// Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size} // Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size}

View File

@@ -31,6 +31,6 @@ default = ["pmosource/server"]
# Feature pour activer l'API REST de gestion des sources # Feature pour activer l'API REST de gestion des sources
api = ["dep:axum", "dep:utoipa", "pmosource/server"] api = ["dep:axum", "dep:utoipa", "pmosource/server"]
# Feature pour activer le support Qobuz configuré # 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 # Feature pour activer le support Radio Paradise
paradise = ["api", "dep:pmoparadise"] paradise = ["api", "dep:pmoparadise", "pmoparadise/server"]

View File

@@ -128,13 +128,9 @@ impl SourcesExt for Server {
.await .await
.map_err(|e| SourceInitError::QobuzError(format!("Failed to create client: {}", e)))?; .map_err(|e| SourceInitError::QobuzError(format!("Failed to create client: {}", e)))?;
// Récupérer l'URL de base du serveur depuis la config // Créer la source depuis le registry
let config = pmoconfig::get_config(); let source = QobuzSource::from_registry(client)
let port = config.get_http_port(); .map_err(|e| SourceInitError::QobuzError(format!("Failed to create source: {}", e)))?;
let base_url = format!("http://localhost:{}", port);
// Créer la source
let source = QobuzSource::new(client, &base_url);
// Enregistrer la source // Enregistrer la source
self.register_music_source(Arc::new(source)).await; self.register_music_source(Arc::new(source)).await;
@@ -155,13 +151,9 @@ impl SourcesExt for Server {
.await .await
.map_err(|e| SourceInitError::QobuzError(format!("Failed to authenticate: {}", e)))?; .map_err(|e| SourceInitError::QobuzError(format!("Failed to authenticate: {}", e)))?;
// Récupérer l'URL de base du serveur depuis la config // Créer la source depuis le registry
let config = pmoconfig::get_config(); let source = QobuzSource::from_registry(client)
let port = config.get_http_port(); .map_err(|e| SourceInitError::QobuzError(format!("Failed to create source: {}", e)))?;
let base_url = format!("http://localhost:{}", port);
// Créer la source
let source = QobuzSource::new(client, &base_url);
// Enregistrer la source // Enregistrer la source
self.register_music_source(Arc::new(source)).await; self.register_music_source(Arc::new(source)).await;
@@ -182,13 +174,9 @@ impl SourcesExt for Server {
.await .await
.map_err(|e| SourceInitError::ParadiseError(format!("Failed to create client: {}", e)))?; .map_err(|e| SourceInitError::ParadiseError(format!("Failed to create client: {}", e)))?;
// Récupérer l'URL de base du serveur depuis la config // Créer la source depuis le registry avec capacité FIFO par défaut
let config = pmoconfig::get_config(); let source = RadioParadiseSource::from_registry_default(client)
let port = config.get_http_port(); .map_err(|e| SourceInitError::ParadiseError(format!("Failed to create source: {}", e)))?;
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 // Enregistrer la source
self.register_music_source(Arc::new(source)).await; self.register_music_source(Arc::new(source)).await;

View File

@@ -97,13 +97,19 @@ async fn register_qobuz(Json(creds): Json<QobuzCredentials>) -> impl IntoRespons
} }
}; };
// Récupérer l'URL de base du serveur depuis la config // Créer et enregistrer la source depuis le registry
let config = pmoconfig::get_config(); let source = match QobuzSource::from_registry(client) {
let port = config.get_http_port(); Ok(s) => Arc::new(s),
let base_url = format!("http://localhost:{}", port); Err(e) => {
return (
// Créer et enregistrer la source StatusCode::INTERNAL_SERVER_ERROR,
let source = Arc::new(QobuzSource::new(client, &base_url)); Json(ErrorResponse {
error: format!("Failed to create source: {}", e),
}),
)
.into_response();
}
};
let source_id = source.as_ref().id().to_string(); let source_id = source.as_ref().id().to_string();
register_source(source).await; register_source(source).await;
@@ -150,16 +156,33 @@ async fn register_paradise(Json(params): Json<ParadiseParams>) -> impl IntoRespo
} }
}; };
// Récupérer l'URL de base du serveur depuis la config // Créer et enregistrer la source depuis le registry
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 { 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 { } 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(); let source_id = source.as_ref().id().to_string();

View File

@@ -63,6 +63,8 @@ metadata-only = []
per-track = ["dep:claxon", "dep:hound", "dep:tempfile"] per-track = ["dep:claxon", "dep:hound", "dep:tempfile"]
# Active le media server UPnP # Active le media server UPnP
mediaserver = ["dep:pmoupnp", "dep:pmoserver", "dep:pmodidl", "dep:uuid"] 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) # Feature cache (deprecated - toujours actif maintenant)
cache = [] cache = []

View File

@@ -79,18 +79,56 @@ impl std::fmt::Debug for RadioParadiseSource {
} }
impl 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<Self> {
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> {
Self::from_registry(client, DEFAULT_FIFO_CAPACITY)
}
/// Create a new Radio Paradise source with explicit caches (for tests)
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `client` - Radio Paradise API client /// * `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 /// * `fifo_capacity` - Maximum number of tracks in the FIFO
/// * `cover_cache` - Cover image cache (required) /// * `cover_cache` - Cover image cache (required)
/// * `audio_cache` - Audio cache (required) /// * `audio_cache` - Audio cache (required)
pub fn new( pub fn new(
client: RadioParadiseClient, client: RadioParadiseClient,
cache_base_url: impl Into<String>,
fifo_capacity: usize, fifo_capacity: usize,
cover_cache: Arc<CoverCache>, cover_cache: Arc<CoverCache>,
audio_cache: Arc<AudioCache>, audio_cache: Arc<AudioCache>,
@@ -102,9 +140,7 @@ impl RadioParadiseSource {
DEFAULT_IMAGE, DEFAULT_IMAGE,
); );
let cache_base_url = cache_base_url.into();
let cache_manager = SourceCacheManager::new( let cache_manager = SourceCacheManager::new(
cache_base_url.clone(),
"radio-paradise".to_string(), "radio-paradise".to_string(),
cover_cache, cover_cache,
audio_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( pub fn new_default(
client: RadioParadiseClient, client: RadioParadiseClient,
cache_base_url: impl Into<String>,
cover_cache: Arc<CoverCache>, cover_cache: Arc<CoverCache>,
audio_cache: Arc<AudioCache>, audio_cache: Arc<AudioCache>,
) -> Self { ) -> 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 /// 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 { match self.inner.cache_manager.cache_cover(&image_url).await {
Ok(pk) => { Ok(pk) => {
// Use the cached cover URL // Use the cached cover URL
let cached_url = self.inner.cache_manager.cover_url(&pk, None); match self.inner.cache_manager.cover_url(&pk, None) {
track = track.with_image(cached_url); Ok(cached_url) => {
Some(pk) 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) => { Err(e) => {
tracing::warn!("Failed to cache cover image {}: {}", image_url, 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 (cover_cache, audio_cache) = create_test_caches().await;
let source = RadioParadiseSource::new_default( let source = RadioParadiseSource::new_default(
client, client,
"http://localhost:8080",
cover_cache, cover_cache,
audio_cache audio_cache
); );
@@ -610,7 +652,6 @@ mod tests {
let (cover_cache, audio_cache) = create_test_caches().await; let (cover_cache, audio_cache) = create_test_caches().await;
let source = RadioParadiseSource::new_default( let source = RadioParadiseSource::new_default(
client, client,
"http://localhost:8080",
cover_cache, cover_cache,
audio_cache audio_cache
); );

View File

@@ -57,6 +57,8 @@ pmosource = { path = "../pmosource" }
default = [] default = []
# Feature pour activer les extensions pmoserver # Feature pour activer les extensions pmoserver
pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"] pmoserver = ["dep:pmoserver", "dep:axum", "dep:utoipa"]
# Feature pour activer le support serveur (cache registry)
server = ["pmosource/server"]
# Feature cache (deprecated - toujours actif maintenant) # Feature cache (deprecated - toujours actif maintenant)
cache = [] cache = []

View File

@@ -82,23 +82,45 @@ impl std::fmt::Debug for QobuzSource {
} }
impl 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<Self> {
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 /// # Arguments
/// ///
/// * `client` - Authenticated Qobuz API client /// * `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) /// * `cover_cache` - Cover image cache (required)
/// * `audio_cache` - Audio cache (required) /// * `audio_cache` - Audio cache (required)
pub fn new( pub fn new(
client: QobuzClient, client: QobuzClient,
cache_base_url: impl Into<String>,
cover_cache: Arc<CoverCache>, cover_cache: Arc<CoverCache>,
audio_cache: Arc<AudioCache>, audio_cache: Arc<AudioCache>,
) -> Self { ) -> Self {
let cache_base_url = cache_base_url.into();
let cache_manager = SourceCacheManager::new( let cache_manager = SourceCacheManager::new(
cache_base_url.clone(),
"qobuz".to_string(), "qobuz".to_string(),
cover_cache, cover_cache,
audio_cache, audio_cache,

View File

@@ -33,6 +33,7 @@ pmocovers = { path = "../pmocovers", optional = true }
# Server extension (optional) # Server extension (optional)
pmoserver = { path = "../pmoserver", optional = true } pmoserver = { path = "../pmoserver", optional = true }
pmoconfig = { path = "../pmoconfig", optional = true } pmoconfig = { path = "../pmoconfig", optional = true }
pmoupnp = { path = "../pmoupnp", optional = true }
# Web framework for API (optional) # Web framework for API (optional)
axum = { version = "0.8", optional = true } axum = { version = "0.8", optional = true }
@@ -45,4 +46,4 @@ lazy_static = { version = "1.4", optional = true }
[features] [features]
default = ["cache"] default = ["cache"]
cache = ["pmoaudiocache", "pmocovers"] 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"]

View File

@@ -47,9 +47,6 @@ pub struct SourceCacheManager {
/// Métadonnées des pistes (track_id → metadata) /// Métadonnées des pistes (track_id → metadata)
track_cache: RwLock<HashMap<String, TrackMetadata>>, track_cache: RwLock<HashMap<String, TrackMetadata>>,
/// URL de base du serveur
cache_base_url: String,
/// ID de collection pour cette source (ex: "radio-paradise", "qobuz") /// ID de collection pour cette source (ex: "radio-paradise", "qobuz")
collection_id: String, collection_id: String,
@@ -61,23 +58,56 @@ pub struct SourceCacheManager {
} }
impl 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<Self> {
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 /// # Arguments
/// ///
/// * `cache_base_url` - URL de base du serveur
/// * `collection_id` - ID de collection (source ID) /// * `collection_id` - ID de collection (source ID)
/// * `cover_cache` - Cache de couvertures centralisé /// * `cover_cache` - Cache de couvertures centralisé
/// * `audio_cache` - Cache audio centralisé /// * `audio_cache` - Cache audio centralisé
pub fn new( pub fn new(
cache_base_url: String,
collection_id: String, collection_id: String,
cover_cache: Arc<CoverCache>, cover_cache: Arc<CoverCache>,
audio_cache: Arc<AudioCache>, audio_cache: Arc<AudioCache>,
) -> Self { ) -> Self {
Self { Self {
track_cache: RwLock::new(HashMap::new()), track_cache: RwLock::new(HashMap::new()),
cache_base_url,
collection_id, collection_id,
cover_cache, cover_cache,
audio_cache, audio_cache,
@@ -93,7 +123,18 @@ impl SourceCacheManager {
if let Some(metadata) = cache.get(object_id) { if let Some(metadata) = cache.get(object_id) {
if let Some(ref pk) = metadata.cached_audio_pk { 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()); return Ok(metadata.original_uri.clone());
} }
@@ -140,11 +181,17 @@ impl SourceCacheManager {
/// # Returns /// # Returns
/// ///
/// L'URL complète de l'image /// L'URL complète de l'image
pub fn cover_url(&self, pk: &str, size: Option<usize>) -> String { pub fn cover_url(&self, pk: &str, size: Option<usize>) -> Result<String> {
if let Some(s) = size { #[cfg(feature = "server")]
format!("{}/covers/images/{}/{}", self.cache_base_url, pk, s) {
} else { pmoupnp::cache_registry::build_cover_url(pk, size)
format!("{}/covers/images/{}", self.cache_base_url, pk) .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()
))
} }
} }

View File

@@ -18,6 +18,9 @@ use pmoaudiocache::Cache as AudioCache;
/// Contient les instances partagées des caches de couvertures et audio. /// Contient les instances partagées des caches de couvertures et audio.
/// Ces caches sont uniques et partagés entre toutes les sources musicales. /// Ces caches sont uniques et partagés entre toutes les sources musicales.
pub struct CacheRegistry { pub struct CacheRegistry {
/// URL de base du serveur (ex: "http://localhost:8080")
base_url: Option<String>,
/// Cache de couvertures (WebP) /// Cache de couvertures (WebP)
cover_cache: Option<Arc<CoverCache>>, cover_cache: Option<Arc<CoverCache>>,
@@ -29,11 +32,22 @@ impl CacheRegistry {
/// Créer un nouveau registre vide /// Créer un nouveau registre vide
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
base_url: None,
cover_cache: None, cover_cache: None,
audio_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 /// Enregistrer le cache de couvertures
pub fn set_cover_cache(&mut self, cache: Arc<CoverCache>) { pub fn set_cover_cache(&mut self, cache: Arc<CoverCache>) {
self.cover_cache = Some(cache); self.cover_cache = Some(cache);
@@ -53,6 +67,42 @@ impl CacheRegistry {
pub fn audio_cache(&self) -> Option<Arc<AudioCache>> { pub fn audio_cache(&self) -> Option<Arc<AudioCache>> {
self.audio_cache.clone() 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 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<String> {
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 { impl Default for CacheRegistry {
@@ -99,6 +149,48 @@ pub fn get_audio_cache() -> Option<Arc<AudioCache>> {
CACHE_REGISTRY.read().unwrap().audio_cache() 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<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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -186,8 +186,8 @@ impl UpnpServerExt for Server {
use pmocovers::new_cache; use pmocovers::new_cache;
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router}; use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router};
let base_url = self.info().base_url; let base_url = self.info().base_url.clone();
let cache = Arc::new(new_cache(cache_dir, limit, &base_url)?); let cache = Arc::new(new_cache(cache_dir, limit)?);
// Routes de fichiers avec génération de variantes // Routes de fichiers avec génération de variantes
// Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size} // Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size}
@@ -220,8 +220,12 @@ impl UpnpServerExt for Server {
let openapi = pmocovers::ApiDoc::openapi(); let openapi = pmocovers::ApiDoc::openapi();
self.add_openapi(api_router, openapi, "covers").await; self.add_openapi(api_router, openapi, "covers").await;
// Enregistrer dans le registre global // Enregistrer base_url et cache dans le registre global
CACHE_REGISTRY.write().unwrap().set_cover_cache(cache.clone()); {
let mut registry = CACHE_REGISTRY.write().unwrap();
registry.set_base_url(base_url);
registry.set_cover_cache(cache.clone());
}
Ok(cache) Ok(cache)
} }
@@ -231,8 +235,8 @@ impl UpnpServerExt for Server {
use pmoaudiocache::new_cache; use pmoaudiocache::new_cache;
use pmocache::pmoserver_ext::{create_file_router, create_api_router}; use pmocache::pmoserver_ext::{create_file_router, create_api_router};
let base_url = self.info().base_url; let base_url = self.info().base_url.clone();
let cache = Arc::new(new_cache(cache_dir, limit, &base_url)?); let cache = Arc::new(new_cache(cache_dir, limit)?);
// Routes de fichiers pour servir les pistes FLAC // Routes de fichiers pour servir les pistes FLAC
let file_router = create_file_router(cache.clone(), "audio/flac"); let file_router = create_file_router(cache.clone(), "audio/flac");
@@ -243,8 +247,12 @@ impl UpnpServerExt for Server {
let openapi = pmoaudiocache::ApiDoc::openapi(); let openapi = pmoaudiocache::ApiDoc::openapi();
self.add_openapi(api_router, openapi, "audio").await; self.add_openapi(api_router, openapi, "audio").await;
// Enregistrer dans le registre global // Enregistrer base_url et cache dans le registre global
CACHE_REGISTRY.write().unwrap().set_audio_cache(cache.clone()); {
let mut registry = CACHE_REGISTRY.write().unwrap();
registry.set_base_url(base_url);
registry.set_audio_cache(cache.clone());
}
Ok(cache) Ok(cache)
} }