diff --git a/pmocache/src/cache_trait.rs b/pmocache/src/cache_trait.rs index a4d2486c..1036130c 100644 --- a/pmocache/src/cache_trait.rs +++ b/pmocache/src/cache_trait.rs @@ -138,8 +138,12 @@ pub trait FileCache: Send + Sync { /// /// # Returns /// - /// `true` si l'entrée existe en base de données et que le fichier est présent ET complet - /// (avec marker .complete) OU en cours de download + /// `true` si l'entrée existe en base de données et que le fichier est présent ET: + /// - SOIT le fichier est complet (marker .complete existe) + /// - SOIT le download est en cours (fichier récent sans marker) + /// + /// Ceci permet le progressive caching: les fichiers en cours de download sont acceptés + /// dès que le prebuffer est atteint, sans attendre le marker de completion. fn is_valid_pk(&self, pk: &str) -> bool { if self.get_database().get(pk, false).is_err() { tracing::debug!("is_valid_pk({}): DB entry not found", pk); @@ -152,22 +156,36 @@ pub trait FileCache: Send + Sync { return false; } - // Vérifier si le fichier est récent (modifié dans les 60 dernières secondes) - // Ceci détecte les downloads en cours même sans marker .complete - // Le marker sera vérifié plus tard lors de la lecture effective + // Vérifier d'abord si le marker de completion existe + let completion_marker = file_path.with_extension( + format!("{}.complete", C::file_extension()) + ); + + if completion_marker.exists() { + tracing::debug!("is_valid_pk({}): Completion marker found, file is complete", pk); + return true; + } + + // Pas de marker - vérifier si le download est en cours (fichier récent) + // Un fichier en cours de download aura une modification récente if let Ok(metadata) = file_path.metadata() { if let Ok(modified) = metadata.modified() { if let Ok(elapsed) = modified.elapsed() { let age_secs = elapsed.as_secs(); - let is_recent = age_secs < 60; - tracing::debug!("is_valid_pk({}): File age={}s, is_recent={}", pk, age_secs, is_recent); - return is_recent; + if age_secs < 60 { + tracing::debug!("is_valid_pk({}): No marker but file is recent ({}s), download in progress", pk, age_secs); + return true; + } else { + tracing::debug!("is_valid_pk({}): No marker and file is old ({}s), incomplete download", pk, age_secs); + return false; + } } } } - tracing::debug!("is_valid_pk({}): Could not check file age, accepting by default", pk); - true // Si on ne peut pas vérifier la date, on accepter par défaut + // Ne peut pas vérifier le statut - rejeter par sécurité + tracing::debug!("is_valid_pk({}): Could not check file status, rejecting", pk); + false } } diff --git a/pmoupnp/src/cache_registry.rs b/pmoupnp/src/cache_registry.rs new file mode 100644 index 00000000..ee5a2840 --- /dev/null +++ b/pmoupnp/src/cache_registry.rs @@ -0,0 +1,97 @@ +//! Registre centralisé des caches pour le serveur UPnP (couche de compatibilité) +//! +//! Ce module fournit une couche de compatibilité pour pmosource qui utilise +//! les singletons de pmoaudiocache et pmocovers pour accéder aux caches. + +use pmoaudiocache::Cache as AudioCache; +use pmocache::FileCache; +use pmocovers::Cache as CoverCache; +use std::sync::Arc; + +/// 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> { + pmocovers::get_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> { + pmoaudiocache::get_audio_cache() +} + +/// Construit l'URL complète pour une couverture +/// +/// # 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 { + // Récupérer l'URL de base depuis la variable d'environnement ou une config + let base_url = std::env::var("PMO_SERVER_URL") + .unwrap_or_else(|_| "http://localhost:8080".to_string()); + + let cache = get_cover_cache() + .ok_or_else(|| anyhow::anyhow!("No registered 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", "stream") +/// +/// # Examples +/// +/// ```rust,ignore +/// use pmoupnp::cache_registry::build_audio_url; +/// +/// let url = build_audio_url("abc123", Some("stream"))?; +/// // url = "http://localhost:8080/audio/tracks/abc123/stream" +/// ``` +pub fn build_audio_url(pk: &str, param: Option<&str>) -> anyhow::Result { + // Récupérer l'URL de base depuis la variable d'environnement ou une config + let base_url = std::env::var("PMO_SERVER_URL") + .unwrap_or_else(|_| "http://localhost:8080".to_string()); + + let cache = get_audio_cache() + .ok_or_else(|| anyhow::anyhow!("No registered audio cache"))?; + + let route = cache.route_for(pk, param); + Ok(format!("{}{}", base_url, route)) +} diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index 28e0cb93..67af0ac0 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -2,6 +2,7 @@ mod object_set; mod object_trait; pub mod actions; +pub mod cache_registry; pub mod devices; pub mod services; pub mod soap;