Add pmocovers integration for external cover URL proxying

- Introduce optional `pmocover` dependency in pmocontrol
- Add cover URL transformation logic for both REST and SSE endpoints using `pmocovers::proxy_cover_url`/sync
- Refactor `/covers/proxy?...=` handler to accept any external URL and return local cached route
- Implement helper functions `transform_cover_url` (async) & sync variant for consistent cover URL normalization
- Update Cargo.lock to include `pmocovers`
This commit is contained in:
2026-04-04 12:04:40 +02:00
parent bdaee820ad
commit ea5936717a
6 changed files with 224 additions and 61 deletions

View File

@@ -109,9 +109,9 @@ pub struct CoverProxyResponse {
/// GET /covers/proxy?url=<encoded_url>
/// Proxy transparent qui :
/// 1. Détecte si l'URL est une URL LAN externe (pas déjà locale)
/// 2. Ajoute à cache via add_from_url (déduplication automatique)
/// 3. Retourne l'URL locale du cache
/// 1. Ajoute l'URL au cache (add_from_url gère déduplication)
/// 2. Retourne l'URL locale du cache
/// Note: Si l'URL est déjà une cover locale de notre instance, on retourne directement l'URL
#[cfg(feature = "pmoserver")]
pub async fn cover_proxy_handler(
Query(params): Query<CoverProxyParams>,
@@ -120,31 +120,19 @@ pub async fn cover_proxy_handler(
) -> impl IntoResponse {
let external_url = &params.url;
// Ignorer si déjà une URL de NOTRE instance pmomusic (ne pas se cacher soi-même)
// Si c'est déjà une URL locale de NOTRE instance pmomusic, la retourner directement
if is_local_cover_url(external_url, &base_url) {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "INVALID_REQUEST".to_string(),
message: "URL is already a local cover from this instance".to_string(),
StatusCode::OK,
Json(CoverProxyResponse {
cached_url: external_url.clone(),
pk: String::new(),
}),
)
.into_response();
}
// Vérifier si c'est une URL LAN à proxyfier
if !should_proxy_url(external_url) {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: "INVALID_REQUEST".to_string(),
message: "URL is not a LAN URL requiring proxy".to_string(),
}),
)
.into_response();
}
// Ajouter au cache (add_from_url gère la déduplication)
// Ajouter au cache (add_from_url gère la déduplication et le download)
match cache.add_from_url(external_url, Some("external-covers")).await {
Ok(pk) => {
// Retourner l'URL locale

View File

@@ -135,6 +135,77 @@ pub fn get_cover_cache() -> Option<Arc<Cache>> {
COVER_CACHE.get().cloned()
}
// ============================================================================
// Helper pour proxyfier les URLs de covers externes
// ============================================================================
/// Transforme une URL de cover externe en URL locale du cache.
///
/// Si l'URL est déjà une route locale de notre cache, la retourne directement.
/// Sinon, ajoute l'URL au cache (download si nécessaire) et retourne l'URL locale.
///
/// Usage :
/// ```rust
/// let local_url = pmocovers::proxy_cover_url("https://example.com/cover.jpg").await?;
/// ```
#[cfg(feature = "pmoserver")]
pub async fn proxy_cover_url(url: &str, base_url: &pmoserver::BaseUrl) -> anyhow::Result<String> {
proxy_cover_url_sync_impl(url, base_url).await
}
/// Version synchrone de proxy_cover_url.
/// Utilise un runtime tokio temporaire pour exécuter add_from_url.
///
/// Usage :
/// ```rust
/// let local_url = pmocovers::proxy_cover_url_sync("https://example.com/cover.jpg", base_url);
/// ```
#[cfg(feature = "pmoserver")]
pub fn proxy_cover_url_sync(url: &str, base_url: &pmoserver::BaseUrl) -> anyhow::Result<String> {
// Si c'est déjà une route locale de notre cache, retourner directement
if url.starts_with("/covers/") {
return Ok(url.to_string());
}
// Si c'est déjà une URL de notre instance, la retourner directement
if url.starts_with(&base_url.0) {
return Ok(url.to_string());
}
// Ajouter au cache en utilisant un runtime temporaire
let cache = get_cover_cache()
.ok_or_else(|| anyhow::anyhow!("Cover cache not initialized"))?;
let runtime = tokio::runtime::Runtime::new()?;
let pk = runtime.block_on(async move {
cache.add_from_url(url, Some("external-covers")).await
})?;
let route = pmocache::covers_route_for(&pk, None);
Ok(base_url.url_for(&route))
}
#[cfg(feature = "pmoserver")]
async fn proxy_cover_url_sync_impl(url: &str, base_url: &pmoserver::BaseUrl) -> anyhow::Result<String> {
// Si c'est déjà une route locale de notre cache, retourner directement
if url.starts_with("/covers/") {
return Ok(url.to_string());
}
// Si c'est déjà une URL de notre instance, la retourner directement
if url.starts_with(&base_url.0) {
return Ok(url.to_string());
}
// Ajouter au cache (add_from_url gère déduplication et download)
let cache = get_cover_cache()
.ok_or_else(|| anyhow::anyhow!("Cover cache not initialized"))?;
let pk = cache.add_from_url(url, Some("external-covers")).await?;
let route = pmocache::covers_route_for(&pk, None);
Ok(base_url.url_for(&route))
}
// ============================================================================
// Extension pmoserver
// ============================================================================