mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
This commit is contained in:
@@ -63,13 +63,15 @@ pub fn build_fanart_url(mbid: &str, api_key: &str, client_key: Option<&str>) ->
|
||||
/// `Ok(None)` when the artist carries no MBID tag.
|
||||
pub async fn fetch_artist_tag_mbid(
|
||||
client: &Client,
|
||||
registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
|
||||
server_ref: Option<&str>,
|
||||
rest_base: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
artist_id: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
let url = build_artist_info2_url(rest_base, username, password, artist_id);
|
||||
let body = http_get_text(client, &url).await?;
|
||||
let body = http_get_text_scoped(client, registry, server_ref, &url).await?;
|
||||
let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
|
||||
let mbid = v
|
||||
.get("subsonic-response")
|
||||
@@ -218,8 +220,21 @@ fn classify_mb_releases(v: &serde_json::Value) -> MbResolution {
|
||||
}
|
||||
|
||||
/// Single GET → response text; any non-2xx is an error.
|
||||
async fn http_get_text(client: &Client, url: &str) -> Result<String, String> {
|
||||
let resp = client.get(url).send().await.map_err(|e| e.to_string())?;
|
||||
async fn http_get_text_scoped(
|
||||
client: &Client,
|
||||
registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
|
||||
server_ref: Option<&str>,
|
||||
url: &str,
|
||||
) -> Result<String, String> {
|
||||
let resp = psysonic_core::server_http::apply_optional_registry_headers(
|
||||
registry,
|
||||
server_ref,
|
||||
url,
|
||||
client.get(url),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("HTTP {status}"));
|
||||
|
||||
@@ -242,12 +242,18 @@ pub(super) async fn try_external_fanart(
|
||||
|
||||
let _permit = fanart_sem.clone().acquire_owned().await.ok()?;
|
||||
|
||||
let http_registry = app
|
||||
.try_state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>()
|
||||
.map(|s| Arc::clone(&*s));
|
||||
|
||||
// §23: resolve the tag MBID Rust-side via getArtistInfo2 — unless the cache
|
||||
// already carries one (skip the Navidrome round-trip).
|
||||
let (mbid, mbid_source) = match cached.as_ref().and_then(|r| r.mbid.clone()) {
|
||||
Some(m) => (m, cached.as_ref().and_then(|r| r.mbid_source.clone())),
|
||||
None => match external::fetch_artist_tag_mbid(
|
||||
client,
|
||||
http_registry.as_deref(),
|
||||
Some(server_id),
|
||||
&args.rest_base_url,
|
||||
&args.username,
|
||||
&args.password,
|
||||
@@ -344,7 +350,7 @@ pub(super) async fn try_external_fanart(
|
||||
}
|
||||
};
|
||||
|
||||
let bytes = match fetch::fetch_cover_bytes(client, &img_url).await {
|
||||
let bytes = match fetch::fetch_cover_bytes(client, &img_url, None, None).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("[fanart] download failed: {e}"); // transient — don't cache
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use reqwest::Client;
|
||||
use psysonic_core::server_http::ServerHttpRegistry;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use url::Url;
|
||||
|
||||
@@ -83,8 +84,21 @@ enum FetchAttempt {
|
||||
Permanent(String),
|
||||
}
|
||||
|
||||
async fn fetch_cover_once(client: &Client, url: &str) -> FetchAttempt {
|
||||
let resp = match client.get(url).send().await {
|
||||
async fn fetch_cover_once(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
registry: Option<&ServerHttpRegistry>,
|
||||
server_ref: Option<&str>,
|
||||
) -> FetchAttempt {
|
||||
let mut req = client.get(url);
|
||||
if let Some(reg) = registry {
|
||||
if let Some(sid) = server_ref.filter(|s| !s.is_empty()) {
|
||||
req = reg.apply_for_http_url(sid, url, req);
|
||||
} else if let Some(ctx) = reg.get_for_server_url(url) {
|
||||
req = psysonic_core::server_http::apply_server_headers_for_http_url(req, &ctx, url);
|
||||
}
|
||||
}
|
||||
let resp = match req.send().await {
|
||||
Ok(r) => r,
|
||||
// Connection reset / timeout / DNS — transient under server load.
|
||||
Err(e) => return FetchAttempt::Transient(e.to_string()),
|
||||
@@ -104,10 +118,15 @@ async fn fetch_cover_once(client: &Client, url: &str) -> FetchAttempt {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_cover_bytes(client: &Client, url: &str) -> Result<Vec<u8>, String> {
|
||||
pub async fn fetch_cover_bytes(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
registry: Option<&ServerHttpRegistry>,
|
||||
server_ref: Option<&str>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let mut last_err = String::from("cover fetch failed");
|
||||
for attempt in 0..COVER_FETCH_ATTEMPTS {
|
||||
match fetch_cover_once(client, url).await {
|
||||
match fetch_cover_once(client, url, registry, server_ref).await {
|
||||
FetchAttempt::Ok(bytes) => return Ok(bytes),
|
||||
FetchAttempt::Permanent(e) => return Err(e),
|
||||
FetchAttempt::Transient(e) => {
|
||||
|
||||
@@ -354,7 +354,10 @@ impl CoverCacheState {
|
||||
let source = if let Some(img) = load_image_from_disk(&dir) {
|
||||
CoverSource::Image(img)
|
||||
} else {
|
||||
match download_cover_payload(&dir, &client, &http_sem, args).await {
|
||||
let http_registry = app
|
||||
.try_state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>()
|
||||
.map(|s| Arc::clone(&*s));
|
||||
match download_cover_payload(&dir, &client, &http_sem, args, http_registry).await {
|
||||
Ok(bytes) => CoverSource::Bytes(bytes),
|
||||
Err(err) => {
|
||||
log_cover_fetch_failure(app, args, &err);
|
||||
@@ -532,6 +535,7 @@ async fn download_cover_payload(
|
||||
client: &Client,
|
||||
http_sem: &Semaphore,
|
||||
args: &CoverCacheEnsureArgs,
|
||||
registry: Option<Arc<psysonic_core::server_http::ServerHttpRegistry>>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let _permit = http_sem
|
||||
.acquire()
|
||||
@@ -549,7 +553,13 @@ async fn download_cover_payload(
|
||||
&args.cover_art_id,
|
||||
fetch_size,
|
||||
);
|
||||
fetch::fetch_cover_bytes(client, &url).await
|
||||
fetch::fetch_cover_bytes(
|
||||
client,
|
||||
&url,
|
||||
registry.as_deref(),
|
||||
Some(args.server_index_key.as_str()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn spawn_derive_remaining_tiers(
|
||||
|
||||
+10
-2
@@ -106,6 +106,7 @@ pub fn run() {
|
||||
|
||||
let builder = tauri::Builder::default()
|
||||
.manage(audio_engine)
|
||||
.manage(Arc::new(psysonic_core::server_http::ServerHttpRegistry::new()))
|
||||
.manage(ShortcutMap::default())
|
||||
.manage(discord::DiscordState::new())
|
||||
.manage(Arc::new(tokio::sync::Semaphore::new(MAX_DL_CONCURRENCY)) as DownloadSemaphore)
|
||||
@@ -238,7 +239,10 @@ pub fn run() {
|
||||
let flags = psysonic_library::sync::capability::CapabilityFlags::new(
|
||||
flags_bits,
|
||||
);
|
||||
let subsonic = psysonic_integration::subsonic::SubsonicClient::new(
|
||||
let registry = app_for_sched.state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>();
|
||||
let subsonic = psysonic_integration::subsonic::subsonic_client_with_registry(
|
||||
Some(registry.as_ref()),
|
||||
&session.server_id,
|
||||
session.base_url.clone(),
|
||||
session.username.clone(),
|
||||
session.password.clone(),
|
||||
@@ -251,7 +255,8 @@ pub fn run() {
|
||||
scope.clone(),
|
||||
flags,
|
||||
)
|
||||
.with_playback_hint(hint);
|
||||
.with_playback_hint(hint)
|
||||
.with_http_registry(Some(Arc::clone(®istry)));
|
||||
if let Some(tok) = session.navidrome_token.clone() {
|
||||
sched = sched.with_navidrome_credentials(
|
||||
psysonic_library::sync::capability::NavidromeProbeCredentials {
|
||||
@@ -682,6 +687,9 @@ pub fn run() {
|
||||
migration_inspect,
|
||||
migration_run,
|
||||
resolve_host_addresses,
|
||||
server_http_context_sync,
|
||||
server_http_context_sync_all,
|
||||
server_http_context_clear,
|
||||
psysonic_syncfs::sync::batch::calculate_sync_payload,
|
||||
exit_app,
|
||||
cli_publish_player_snapshot,
|
||||
|
||||
@@ -36,7 +36,10 @@ pub(crate) use integration::{
|
||||
unregister_global_shortcut,
|
||||
};
|
||||
pub(crate) use migration::{migration_inspect, migration_run};
|
||||
pub(crate) use network::resolve_host_addresses;
|
||||
pub(crate) use network::{
|
||||
resolve_host_addresses, server_http_context_clear, server_http_context_sync,
|
||||
server_http_context_sync_all,
|
||||
};
|
||||
|
||||
// Discord, Navidrome admin, last.fm + radio-browser + CORS proxy, bandsintown,
|
||||
// and analysis admin commands now live in their domain crates. invoke_handler!
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
//! dual-server-address add/edit form (UI hint only — not for connect).
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use psysonic_core::server_http::{ServerHttpContextSyncWire, ServerHttpRegistry};
|
||||
use tauri::State;
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
/// Resolve a hostname to a deduped list of IP address strings (IPv4 + IPv6).
|
||||
@@ -58,6 +63,34 @@ pub(crate) async fn resolve_host_addresses(hostname: String) -> Result<Vec<Strin
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn server_http_context_sync(
|
||||
registry: State<'_, Arc<ServerHttpRegistry>>,
|
||||
wire: ServerHttpContextSyncWire,
|
||||
) -> Result<(), String> {
|
||||
registry.sync(wire);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn server_http_context_sync_all(
|
||||
registry: State<'_, Arc<ServerHttpRegistry>>,
|
||||
entries: Vec<ServerHttpContextSyncWire>,
|
||||
) -> Result<(), String> {
|
||||
registry.sync_all(entries);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn server_http_context_clear(
|
||||
registry: State<'_, Arc<ServerHttpRegistry>>,
|
||||
server_id: String,
|
||||
app_server_id: String,
|
||||
) -> Result<(), String> {
|
||||
registry.remove(&server_id, &app_server_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Strip a `:port` suffix. Handles `host:port` and `[ipv6]:port`; leaves
|
||||
/// bracketed IPv6 with no port (`[::1]`) and bare hosts alone.
|
||||
fn strip_port(input: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user