feat(cache): centralize absolute URL generation with PMO_SERVER_URL
Replace hardcoded relative URLs and manual base_url concatenation with a unified absolute URL API via pmocache::covers_absolute_url_for() and CacheTrait::absolute_url_for(). - Add pmocache as a required dependency to pmoparadise - Introduce absolute_url_for() and covers_absolute_url_for() helpers using PMO_SERVER_URL env var (default: http://localhost:8080) - Update all callers to use absolute URLs for covers and audio in streaming, playlists, Qobuz, Radio France, UPnP, and server startup - Remove redundant route_for() usage in URL construction - Add pmocache to Cargo.lock
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -4147,6 +4147,7 @@ dependencies = [
|
|||||||
"pmoaudio",
|
"pmoaudio",
|
||||||
"pmoaudio-ext",
|
"pmoaudio-ext",
|
||||||
"pmoaudiocache",
|
"pmoaudiocache",
|
||||||
|
"pmocache",
|
||||||
"pmoconfig",
|
"pmoconfig",
|
||||||
"pmocovers",
|
"pmocovers",
|
||||||
"pmoflac",
|
"pmoflac",
|
||||||
|
|||||||
@@ -88,10 +88,8 @@ impl IcyClientStream {
|
|||||||
|
|
||||||
// Add cover URL if we have a cover_pk
|
// Add cover URL if we have a cover_pk
|
||||||
if let Some(pk) = &meta.cover_pk {
|
if let Some(pk) = &meta.cover_pk {
|
||||||
// Use relative URL /covers/image/{pk}/256
|
let cover_url = pmocache::covers_absolute_url_for(pk, None);
|
||||||
// This works when streaming from the same server that serves covers
|
metadata_str.push_str(&format!("StreamUrl='{}';", cover_url));
|
||||||
// VLC and other players will resolve relative URLs correctly
|
|
||||||
metadata_str.push_str(&format!("StreamUrl='/covers/image/{}/256';", pk));
|
|
||||||
} else if let Some(url) = &meta.cover_url {
|
} else if let Some(url) = &meta.cover_url {
|
||||||
// Fallback to external cover URL if no local pk
|
// Fallback to external cover URL if no local pk
|
||||||
metadata_str.push_str(&format!("StreamUrl='{}';", url));
|
metadata_str.push_str(&format!("StreamUrl='{}';", url));
|
||||||
|
|||||||
@@ -69,14 +69,7 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
|||||||
|
|
||||||
/// Retourne la route relative pour accéder à un item du cache
|
/// Retourne la route relative pour accéder à un item du cache
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// Format: `/{cache_name}/{cache_type}/{pk}[/{param}]`
|
||||||
///
|
|
||||||
/// * `pk` - Clé primaire de la piste
|
|
||||||
/// * `param` - Paramètre optionnel (ex: "orig", "128k", etc.)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// Route relative (ex: "/audio/flac/abc123" ou "/audio/tracks/abc123/orig")
|
|
||||||
fn route_for(&self, pk: &str, param: Option<&str>) -> String {
|
fn route_for(&self, pk: &str, param: Option<&str>) -> String {
|
||||||
if let Some(p) = param {
|
if let Some(p) = param {
|
||||||
format!("/{}/{}/{}/{}", C::cache_name(), C::cache_type(), pk, p)
|
format!("/{}/{}/{}/{}", C::cache_name(), C::cache_type(), pk, p)
|
||||||
@@ -85,6 +78,16 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retourne l'URL absolue pour accéder à un item du cache
|
||||||
|
///
|
||||||
|
/// Utilise la variable d'environnement `PMO_SERVER_URL` comme base,
|
||||||
|
/// avec `http://localhost:8080` comme valeur par défaut.
|
||||||
|
fn absolute_url_for(&self, pk: &str, param: Option<&str>) -> String {
|
||||||
|
let base = std::env::var("PMO_SERVER_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||||
|
format!("{}{}", base.trim_end_matches('/'), self.route_for(pk, param))
|
||||||
|
}
|
||||||
|
|
||||||
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -144,6 +144,22 @@ pub use cache::{
|
|||||||
CacheSubscription,
|
CacheSubscription,
|
||||||
};
|
};
|
||||||
pub use cache_trait::{pk_from_content_header, FileCache};
|
pub use cache_trait::{pk_from_content_header, FileCache};
|
||||||
|
|
||||||
|
/// Retourne la route relative pour une cover: `/covers/image/{pk}[/{param}]`
|
||||||
|
pub fn covers_route_for(pk: &str, param: Option<&str>) -> String {
|
||||||
|
if let Some(p) = param {
|
||||||
|
format!("/covers/image/{}/{}", pk, p)
|
||||||
|
} else {
|
||||||
|
format!("/covers/image/{}", pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne l'URL absolue pour une cover via `PMO_SERVER_URL`
|
||||||
|
pub fn covers_absolute_url_for(pk: &str, param: Option<&str>) -> String {
|
||||||
|
let base = std::env::var("PMO_SERVER_URL")
|
||||||
|
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||||
|
format!("{}{}", base.trim_end_matches('/'), covers_route_for(pk, param))
|
||||||
|
}
|
||||||
pub use db::{CacheEntry, DB};
|
pub use db::{CacheEntry, DB};
|
||||||
pub use download::{
|
pub use download::{
|
||||||
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
download, download_with_transformer, ingest_with_transformer, peek_header, peek_reader_header,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ pmoplaylist = { path = "../pmoplaylist" }
|
|||||||
pmoconfig = { path = "../pmoconfig", optional = true }
|
pmoconfig = { path = "../pmoconfig", optional = true }
|
||||||
|
|
||||||
# Cache support (OBLIGATOIRE - architecture refactorisée)
|
# Cache support (OBLIGATOIRE - architecture refactorisée)
|
||||||
|
pmocache = { path = "../pmocache" }
|
||||||
pmocovers = { path = "../pmocovers" }
|
pmocovers = { path = "../pmocovers" }
|
||||||
pmoaudiocache = { path = "../pmoaudiocache" }
|
pmoaudiocache = { path = "../pmoaudiocache" }
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ impl RadioParadiseSource {
|
|||||||
let cover_pk = json["cover_pk"].as_str().map(|s| s.to_string());
|
let cover_pk = json["cover_pk"].as_str().map(|s| s.to_string());
|
||||||
let cover_url = cover_pk
|
let cover_url = cover_pk
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|pk| format!("{}/covers/jpeg/{}", self.base_url, pk))
|
.map(|pk| pmocache::covers_absolute_url_for(pk, None))
|
||||||
.or_else(|| json["cover_url"].as_str().map(|s| s.to_string()))
|
.or_else(|| json["cover_url"].as_str().map(|s| s.to_string()))
|
||||||
.or_else(|| Some(self.default_cover_url()));
|
.or_else(|| Some(self.default_cover_url()));
|
||||||
|
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ fn playlist_track_to_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cover_url_from_pk(pk: &str) -> String {
|
fn cover_url_from_pk(pk: &str) -> String {
|
||||||
format!("/covers/image/{}/256", pk)
|
pmocache::covers_absolute_url_for(pk, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_cover_pk(input: Option<String>) -> Option<String> {
|
fn normalize_cover_pk(input: Option<String>) -> Option<String> {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl ReadHandle {
|
|||||||
let _remaining = self.remaining().await?;
|
let _remaining = self.remaining().await?;
|
||||||
|
|
||||||
// Convertir cover_pk en URL si présent
|
// Convertir cover_pk en URL si présent
|
||||||
let album_art = cover_pk.map(|pk| format!("/cover/{}", pk));
|
let album_art = cover_pk.map(|pk| pmocache::covers_absolute_url_for(&pk, None));
|
||||||
|
|
||||||
Ok(Container {
|
Ok(Container {
|
||||||
id: self.playlist.id.clone(),
|
id: self.playlist.id.clone(),
|
||||||
@@ -253,7 +253,7 @@ impl ReadHandle {
|
|||||||
let track_number = meta.get_track_number().await.ok().flatten();
|
let track_number = meta.get_track_number().await.ok().flatten();
|
||||||
let cover_pk = meta.get_cover_pk().await.ok().flatten();
|
let cover_pk = meta.get_cover_pk().await.ok().flatten();
|
||||||
let cover_url = if let Some(pk) = cover_pk.as_ref() {
|
let cover_url = if let Some(pk) = cover_pk.as_ref() {
|
||||||
Some(format!("/covers/jpeg/{}/256", pk))
|
Some(pmocache::covers_absolute_url_for(pk, None))
|
||||||
} else {
|
} else {
|
||||||
meta.get_cover_url().await.ok().flatten()
|
meta.get_cover_url().await.ok().flatten()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ async fn cache_album_image(mut album: Album, cover_cache: &Arc<pmocovers::Cache>
|
|||||||
if let Some(ref image_url) = album.image {
|
if let Some(ref image_url) = album.image {
|
||||||
match cover_cache.add_from_url(image_url, None).await {
|
match cover_cache.add_from_url(image_url, None).await {
|
||||||
Ok(pk) => {
|
Ok(pk) => {
|
||||||
album.image_cached = Some(format!("/covers/images/{}", pk));
|
album.image_cached = Some(pmocache::covers_absolute_url_for(&pk, None));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Failed to cache album image: {}", e);
|
tracing::warn!("Failed to cache album image: {}", e);
|
||||||
|
|||||||
@@ -251,15 +251,13 @@ impl CachedMetadata {
|
|||||||
// Note: add_from_url() lance le téléchargement complet en arrière-plan.
|
// Note: add_from_url() lance le téléchargement complet en arrière-plan.
|
||||||
// L'URL est valide immédiatement — si le fichier n'est pas encore prêt,
|
// L'URL est valide immédiatement — si le fichier n'est pas encore prêt,
|
||||||
// le client web doit réessayer (retry avec backoff).
|
// le client web doit réessayer (retry avec backoff).
|
||||||
let route = cache.route_for(&pk, None);
|
let public_url = pmocache::covers_absolute_url_for(&pk, None);
|
||||||
let public_url = format!("{}{}", server_base_url.trim_end_matches('/'), route);
|
|
||||||
|
|
||||||
#[cfg(feature = "logging")]
|
#[cfg(feature = "logging")]
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Cached cover - UUID: {}, PK: {}, route: {}, public_url: {}",
|
"Cached cover - UUID: {}, PK: {}, public_url: {}",
|
||||||
uuid,
|
uuid,
|
||||||
pk,
|
pk,
|
||||||
route,
|
|
||||||
public_url
|
public_url
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ impl Server {
|
|||||||
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
|
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
|
||||||
let api_registry = Arc::new(RwLock::new(Vec::new()));
|
let api_registry = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
|
||||||
|
let base_url = base_url.into();
|
||||||
|
|
||||||
|
// Initialiser PMO_SERVER_URL pour que tous les caches puissent construire des URLs absolues
|
||||||
|
// sans avoir besoin de propager base_url manuellement.
|
||||||
|
// SAFETY: appelé une seule fois au démarrage du serveur, avant tout thread concurrent.
|
||||||
|
unsafe { std::env::set_var("PMO_SERVER_URL", &base_url) };
|
||||||
|
|
||||||
// Créer le router initial avec l'endpoint de registre
|
// Créer le router initial avec l'endpoint de registre
|
||||||
let registry_route = Router::new()
|
let registry_route = Router::new()
|
||||||
.route("/api/registry", get(get_api_registry))
|
.route("/api/registry", get(get_api_registry))
|
||||||
@@ -121,7 +128,7 @@ impl Server {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
base_url: base_url.into(),
|
base_url,
|
||||||
http_port,
|
http_port,
|
||||||
router: Arc::new(RwLock::new(registry_route)),
|
router: Arc::new(RwLock::new(registry_route)),
|
||||||
api_router: Arc::new(RwLock::new(None)),
|
api_router: Arc::new(RwLock::new(None)),
|
||||||
|
|||||||
@@ -54,18 +54,10 @@ pub fn get_audio_cache() -> Option<Arc<AudioCache>> {
|
|||||||
/// // url = "http://localhost:8080/covers/images/abc123/300"
|
/// // url = "http://localhost:8080/covers/images/abc123/300"
|
||||||
/// ```
|
/// ```
|
||||||
pub fn build_cover_url(pk: &str, size: Option<usize>) -> anyhow::Result<String> {
|
pub fn build_cover_url(pk: &str, size: Option<usize>) -> anyhow::Result<String> {
|
||||||
// Récupérer l'URL de base depuis la variable d'environnement ou une config
|
Ok(pmocache::covers_absolute_url_for(
|
||||||
let base_url =
|
pk,
|
||||||
std::env::var("PMO_SERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
|
size.map(|s| s.to_string()).as_deref(),
|
||||||
|
))
|
||||||
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
|
/// Construit l'URL complète pour une piste audio
|
||||||
@@ -84,12 +76,6 @@ pub fn build_cover_url(pk: &str, size: Option<usize>) -> anyhow::Result<String>
|
|||||||
/// // url = "http://localhost:8080/audio/tracks/abc123/stream"
|
/// // url = "http://localhost:8080/audio/tracks/abc123/stream"
|
||||||
/// ```
|
/// ```
|
||||||
pub fn build_audio_url(pk: &str, param: Option<&str>) -> anyhow::Result<String> {
|
pub fn build_audio_url(pk: &str, param: Option<&str>) -> anyhow::Result<String> {
|
||||||
// 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 cache = get_audio_cache().ok_or_else(|| anyhow::anyhow!("No registered audio cache"))?;
|
||||||
|
Ok(cache.absolute_url_for(pk, param))
|
||||||
let route = cache.route_for(pk, param);
|
|
||||||
Ok(format!("{}{}", base_url, route))
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user