✨ Add cover proxy for external LAN media servers
- Implement new /covers/proxy endpoint to cache and rewrite external LAN cover URLs - Add url/urlencoding dependencies for URL parsing/escaping in pmocontrol and pmocovers Cargo.toml - Transform album_art_uri fields to use proxy endpoint for LAN URLs in REST and SSE handlers (browse_container, MetadataChanged) - Add URL validation logic to detect LAN vs public URLs and avoid self-caching - Update Cargo.lock with new dependencies
This commit is contained in:
@@ -18,6 +18,7 @@ reqwest = { version = "0.12", features = ["blocking"] }
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = "2"
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
use crate::cache;
|
||||
use crate::Cache;
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use pmocache::api::{AddItemRequest, AddItemResponse, ErrorResponse};
|
||||
use pmocache::covers_route_for;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -84,3 +91,104 @@ pub async fn add_cover_item(
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Proxy pour covers LAN externes
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CoverProxyParams {
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CoverProxyResponse {
|
||||
pub cached_url: String,
|
||||
pub pk: String,
|
||||
}
|
||||
|
||||
/// 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
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub async fn cover_proxy_handler(
|
||||
Query(params): Query<CoverProxyParams>,
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Extension(base_url): Extension<pmoserver::BaseUrl>,
|
||||
) -> impl IntoResponse {
|
||||
let external_url = ¶ms.url;
|
||||
|
||||
// Ignorer si déjà une URL de NOTRE instance pmomusic (ne pas se cacher soi-même)
|
||||
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(),
|
||||
}),
|
||||
)
|
||||
.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)
|
||||
match cache.add_from_url(external_url, Some("external-covers")).await {
|
||||
Ok(pk) => {
|
||||
// Retourner l'URL locale
|
||||
let local_url = base_url.url_for(&covers_route_for(&pk, None));
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(CoverProxyResponse {
|
||||
cached_url: local_url,
|
||||
pk,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(ErrorResponse {
|
||||
error: "CACHE_ERROR".to_string(),
|
||||
message: format!("Failed to cache external cover: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si l'URL est déjà une cover locale de NOTRE instance pmomusic
|
||||
/// Note: Les covers d'autres instances pmomusic sur le LAN DEVRAIENT être proxyfiées
|
||||
fn is_local_cover_url(url: &str, base_url: &pmoserver::BaseUrl) -> bool {
|
||||
url.starts_with(&base_url.0)
|
||||
}
|
||||
|
||||
/// Vérifie si l'URL doit être proxyfiée (URL LAN externe)
|
||||
fn should_proxy_url(url: &str) -> bool {
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
// Proxy uniquement les URLs LAN (pas les URLs publiques)
|
||||
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||
return match ip {
|
||||
std::net::IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_loopback(),
|
||||
std::net::IpAddr::V6(ipv6) => ipv6.is_loopback(),
|
||||
};
|
||||
}
|
||||
// aussi les .local
|
||||
return host.ends_with(".local") || host == "localhost";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -382,6 +382,10 @@ impl CoverCacheExt for pmoserver::Server {
|
||||
"/consolidate",
|
||||
axum::routing::post(pmocache::api::consolidate_cache::<CoversConfig>),
|
||||
)
|
||||
.route(
|
||||
"/proxy",
|
||||
axum::routing::get(crate::api::cover_proxy_handler),
|
||||
)
|
||||
.with_state(cache.clone());
|
||||
|
||||
let openapi = crate::ApiDoc::openapi();
|
||||
|
||||
Reference in New Issue
Block a user