✨ 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:
@@ -38,6 +38,8 @@ async-trait = { version = "0.1", optional = true }
|
||||
tokio-stream = { version = "0.1", features = ["sync"], optional = true }
|
||||
async-stream = { version = "0.3", optional = true }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
url = { version = "2", optional = true }
|
||||
urlencoding = { version = "2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
percent-encoding = "2.3"
|
||||
@@ -45,4 +47,4 @@ percent-encoding = "2.3"
|
||||
[features]
|
||||
default = []
|
||||
# Active l'API REST pmoserver
|
||||
pmoserver = ["dep:pmoserver", "dep:utoipa", "dep:axum", "dep:tokio", "dep:tokio-util", "dep:async-trait", "dep:tokio-stream", "dep:async-stream"]
|
||||
pmoserver = ["dep:pmoserver", "dep:utoipa", "dep:axum", "dep:tokio", "dep:tokio-util", "dep:async-trait", "dep:tokio-stream", "dep:async-stream", "dep:url", "dep:urlencoding"]
|
||||
|
||||
@@ -28,7 +28,7 @@ use async_trait::async_trait;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
http::{StatusCode, header::HeaderMap},
|
||||
routing::{get, post},
|
||||
};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
@@ -2081,7 +2081,9 @@ async fn browse_container(
|
||||
State(state): State<ControlPointState>,
|
||||
Path((server_id, container_id)): Path<(String, String)>,
|
||||
Query(params): Query<BrowseParams>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<BrowseResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let base_url = pmoserver::get_base_url_from_request(&headers);
|
||||
let sid = DeviceId(server_id.clone());
|
||||
|
||||
let server = state.control_point.media_server(&sid).ok_or_else(|| {
|
||||
@@ -2170,7 +2172,7 @@ async fn browse_container(
|
||||
child_count: None,
|
||||
artist: e.artist,
|
||||
album: e.album,
|
||||
album_art_uri: e.album_art_uri,
|
||||
album_art_uri: transform_cover_url(e.album_art_uri.as_deref(), &base_url),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -2204,6 +2206,41 @@ fn map_snapshot_error(
|
||||
)
|
||||
}
|
||||
|
||||
/// Transforme une URL de cover externe LAN en URL de proxy local
|
||||
fn transform_cover_url(url: Option<&str>, base_url: &str) -> Option<String> {
|
||||
let url = url?;
|
||||
|
||||
// Si c'est déjà une URL locale de notre instance, ne pas transformer
|
||||
if url.starts_with(base_url) {
|
||||
return Some(url.to_string());
|
||||
}
|
||||
|
||||
// Vérifier si c'est une URL LAN externe à proxyfier
|
||||
if should_proxy_cover_url(url) {
|
||||
let encoded = urlencoding::encode(url);
|
||||
return Some(format!("/covers/proxy?url={}", encoded));
|
||||
}
|
||||
|
||||
//URL publique ou autre - laisser telle quelle
|
||||
Some(url.to_string())
|
||||
}
|
||||
|
||||
/// Vérifie si l'URL doit être proxyfiée (URL LAN externe)
|
||||
fn should_proxy_cover_url(url: &str) -> bool {
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
return host.ends_with(".local") || host == "localhost";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Helper to fetch playback items from a media server object (container or item).
|
||||
///
|
||||
/// This function browses the server to get the entries and converts them to PlaybackItem.
|
||||
|
||||
Reference in New Issue
Block a user