feat(server): custom HTTP headers for reverse-proxy gates (#1095) (#1156)

This commit is contained in:
cucadmuh
2026-06-22 16:25:28 +03:00
committed by GitHub
parent 2c9b2eeb46
commit 15cecb5d7d
83 changed files with 2452 additions and 327 deletions
@@ -11,8 +11,9 @@ use rusqlite::params;
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager, State};
use psysonic_integration::navidrome::navidrome_token;
use psysonic_integration::subsonic::SubsonicClient;
use psysonic_core::server_http::ServerHttpRegistry;
use psysonic_integration::navidrome::navidrome_token_with_registry;
use psysonic_integration::subsonic::subsonic_client_with_registry;
use crate::advanced_search;
use crate::analysis_backfill::{self, LibraryAnalysisBackfillBatchDto, LibraryAnalysisProgressDto};
@@ -657,13 +658,14 @@ fn normalize_base_url(raw: &str) -> String {
/// caller falls back to a cached bearer / the Subsonic-only path. Never logs
/// the token or credentials.
async fn navidrome_token_with_retry(
registry: Option<&ServerHttpRegistry>,
base_url: &str,
username: &str,
password: &str,
) -> Option<String> {
const ATTEMPTS: u32 = 3;
for attempt in 1..=ATTEMPTS {
match navidrome_token(base_url, username, password).await {
match navidrome_token_with_registry(registry, base_url, username, password).await {
Ok(tok) => return Some(tok),
Err(_) if attempt < ATTEMPTS => {
tokio::time::sleep(Duration::from_millis(250 * attempt as u64)).await;
@@ -677,6 +679,7 @@ async fn navidrome_token_with_retry(
#[tauri::command]
pub async fn library_sync_bind_session(
runtime: State<'_, LibraryRuntime>,
http_registry: State<'_, Arc<ServerHttpRegistry>>,
server_id: String,
base_url: String,
username: String,
@@ -690,8 +693,13 @@ pub async fn library_sync_bind_session(
// keep a bearer cached from a prior bind rather than dropping to
// Subsonic-only — a transient miss must not strip an N1-capable server
// (R7-15 Q3). Non-Navidrome servers stay `None` and sync via Subsonic.
let navidrome_token_cached = match navidrome_token_with_retry(&base_url, &username, &password)
.await
let navidrome_token_cached = match navidrome_token_with_retry(
Some(http_registry.as_ref()),
&base_url,
&username,
&password,
)
.await
{
Some(tok) => Some(tok),
None => runtime.get_session(&server_id).and_then(|s| s.navidrome_token),
@@ -709,7 +717,13 @@ pub async fn library_sync_bind_session(
// Run the probe + persist capability flags. Failure to probe is a
// bind-time error — caller should fix credentials / URL.
let subsonic = SubsonicClient::new(base_url, username, password);
let subsonic = subsonic_client_with_registry(
Some(http_registry.as_ref()),
&server_id,
base_url,
username,
password,
);
let navidrome_creds = navidrome_token_cached.map(|tok| NavidromeProbeCredentials {
server_url: subsonic_base_url_from(&runtime, &server_id),
bearer_token: tok,
@@ -719,6 +733,7 @@ pub async fn library_sync_bind_session(
&runtime.store,
&subsonic,
navidrome_creds.as_ref(),
Some(http_registry.as_ref()),
&server_id,
scope,
)
@@ -872,8 +887,12 @@ async fn library_sync_start_inner(
let job_id_for_task = job_id.clone();
let parallelism = ParallelismBudget::resolve(runtime.current_playback_hint());
let app_for_runner = app.clone();
let runner_handle: tokio::task::JoinHandle<Result<(), String>> = tokio::task::spawn(async move {
let subsonic = SubsonicClient::new(
let registry = app_for_runner.state::<Arc<ServerHttpRegistry>>();
let subsonic = subsonic_client_with_registry(
Some(registry.as_ref()),
&session_clone.server_id,
session_clone.base_url.clone(),
session_clone.username.clone(),
session_clone.password.clone(),
@@ -895,7 +914,8 @@ async fn library_sync_start_inner(
)
.with_cancellation(Arc::clone(&cancel_for_task))
.with_progress(Arc::clone(&progress))
.with_parallelism_budget(parallelism);
.with_parallelism_budget(parallelism)
.with_http_registry(Some(Arc::clone(&registry)));
if let Some(creds) = navidrome_creds.clone() {
runner = runner.with_navidrome_credentials(creds);
}
@@ -919,7 +939,8 @@ async fn library_sync_start_inner(
capability_flags,
)
.with_cancellation(Arc::clone(&cancel_for_task))
.with_progress(Arc::clone(&progress));
.with_progress(Arc::clone(&progress))
.with_http_registry(Some(Arc::clone(&registry)));
if tombstone_budget > 0 {
runner = runner.with_tombstone_budget(tombstone_budget);
}
@@ -1647,7 +1668,7 @@ mod tests {
})))
.mount(&server)
.await;
let tok = navidrome_token_with_retry(&server.uri(), "user", "pw").await;
let tok = navidrome_token_with_retry(None, &server.uri(), "user", "pw").await;
assert_eq!(tok.as_deref(), Some("nd-tok"));
}
@@ -1664,7 +1685,7 @@ mod tests {
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.mount(&server)
.await;
let tok = navidrome_token_with_retry(&server.uri(), "user", "pw").await;
let tok = navidrome_token_with_retry(None, &server.uri(), "user", "pw").await;
assert!(tok.is_none());
}
}
@@ -91,6 +91,7 @@ pub async fn probe_and_persist(
store: &crate::store::LibraryStore,
subsonic: &psysonic_integration::subsonic::SubsonicClient,
navidrome: Option<&NavidromeProbeCredentials>,
http_registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
server_id: &str,
library_scope: &str,
) -> Result<CapabilityProbeResult, psysonic_integration::subsonic::SubsonicError> {
@@ -110,7 +111,7 @@ pub async fn probe_and_persist(
.map_err(psysonic_integration::subsonic::SubsonicError::Transport)?
.unwrap_or(0);
let mut result = CapabilityProbe::run(subsonic, navidrome).await?;
let mut result = CapabilityProbe::run(subsonic, navidrome, http_registry, Some(server_id)).await?;
// R7-15 Q3: a probe run without a Navidrome bearer can't test N1, so it
// must not drop a previously-learned NavidromeNativeBulk capability — the
@@ -173,6 +174,8 @@ impl CapabilityProbe {
pub async fn run(
subsonic: &SubsonicClient,
navidrome: Option<&NavidromeProbeCredentials>,
http_registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
server_id: Option<&str>,
) -> Result<CapabilityProbeResult, SubsonicError> {
let server_info = subsonic.server_info().await?;
@@ -207,7 +210,14 @@ impl CapabilityProbe {
}
if let Some(creds) = navidrome {
match native_bulk_available(&creds.server_url, &creds.bearer_token).await {
match native_bulk_available(
http_registry,
server_id,
&creds.server_url,
&creds.bearer_token,
)
.await
{
Ok(true) => flags.insert(CapabilityFlags::NAVIDROME_NATIVE_BULK),
Ok(false) => {}
Err(_) => {
@@ -345,7 +355,7 @@ mod tests {
let server = MockServer::start().await;
mount_subsonic_full_navidrome(&server).await;
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None)
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None, None, None)
.await
.unwrap();
assert!(result.flags.contains(CapabilityFlags::SUBSONIC_SEARCH3_BULK));
@@ -372,7 +382,7 @@ mod tests {
.mount(&server)
.await;
let err = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None)
let err = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None, None, None)
.await
.unwrap_err();
assert!(matches!(err, SubsonicError::Api { code: 40, .. }));
@@ -415,7 +425,7 @@ mod tests {
.mount(&server)
.await;
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None)
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), None, None, None)
.await
.unwrap();
assert!(result.flags.contains(CapabilityFlags::SUBSONIC_SEARCH3_BULK));
@@ -440,7 +450,7 @@ mod tests {
server_url: server.uri(),
bearer_token: "nd-tok".into(),
};
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), Some(&nav))
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), Some(&nav), None, None)
.await
.unwrap();
assert!(result.flags.contains(CapabilityFlags::NAVIDROME_NATIVE_BULK));
@@ -461,6 +471,7 @@ mod tests {
&store,
&test_subsonic_client(&server.uri()),
None,
None,
"s1",
"",
)
@@ -498,6 +509,7 @@ mod tests {
&store,
&test_subsonic_client(&server.uri()),
None,
None,
"s1",
"",
)
@@ -530,6 +542,7 @@ mod tests {
&store,
&test_subsonic_client(&server.uri()),
None,
None,
"s1",
"",
)
@@ -584,7 +597,7 @@ mod tests {
let store = LibraryStore::open_in_memory();
let result =
super::probe_and_persist(&store, &test_subsonic_client(&server.uri()), None, "s1", "")
super::probe_and_persist(&store, &test_subsonic_client(&server.uri()), None, None, "s1", "")
.await
.unwrap();
assert_eq!(result.server_track_count, Some(170_000));
@@ -617,6 +630,7 @@ mod tests {
&store,
&test_subsonic_client(&server.uri()),
None,
None,
"s1",
"",
)
@@ -647,7 +661,7 @@ mod tests {
mount_subsonic_full_navidrome(&server).await; // scanStatus has no count
let result =
super::probe_and_persist(&store, &test_subsonic_client(&server.uri()), None, "s1", "")
super::probe_and_persist(&store, &test_subsonic_client(&server.uri()), None, None, "s1", "")
.await
.unwrap();
assert_eq!(result.server_track_count, None);
@@ -672,7 +686,7 @@ mod tests {
server_url: server.uri(),
bearer_token: "nd-tok".into(),
};
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), Some(&nav))
let result = CapabilityProbe::run(&test_subsonic_client(&server.uri()), Some(&nav), None, None)
.await
.unwrap();
assert!(!result.flags.contains(CapabilityFlags::NAVIDROME_NATIVE_BULK));
@@ -20,6 +20,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use psysonic_core::server_http::ServerHttpRegistry;
use psysonic_integration::navidrome::queries::nd_list_songs_internal;
use psysonic_integration::subsonic::SubsonicClient;
use serde_json::Value;
@@ -71,6 +72,7 @@ pub struct DeltaSyncRunner<'a> {
store: &'a LibraryStore,
subsonic: &'a SubsonicClient,
navidrome: Option<NavidromeProbeCredentials>,
http_registry: Option<Arc<ServerHttpRegistry>>,
server_id: String,
library_scope: String,
capability_flags: CapabilityFlags,
@@ -95,6 +97,7 @@ impl<'a> DeltaSyncRunner<'a> {
store,
subsonic,
navidrome: None,
http_registry: None,
server_id: server_id.into(),
library_scope: library_scope.into(),
capability_flags,
@@ -111,6 +114,11 @@ impl<'a> DeltaSyncRunner<'a> {
self
}
pub fn with_http_registry(mut self, registry: Option<Arc<ServerHttpRegistry>>) -> Self {
self.http_registry = registry;
self
}
pub fn with_cancellation(mut self, flag: Arc<std::sync::atomic::AtomicBool>) -> Self {
self.cancel = Some(flag);
self
@@ -396,6 +404,8 @@ impl<'a> DeltaSyncRunner<'a> {
self,
|| {
nd_list_songs_internal(
self.http_registry.as_deref(),
Some(&self.server_id),
&creds.server_url,
&creds.bearer_token,
"updated_at",
@@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use psysonic_core::server_http::ServerHttpRegistry;
use psysonic_integration::navidrome::queries::nd_list_songs_internal;
use psysonic_integration::subsonic::SubsonicClient;
use serde_json::Value;
@@ -109,6 +110,7 @@ pub struct InitialSyncRunner<'a> {
store: &'a LibraryStore,
subsonic: &'a SubsonicClient,
navidrome: Option<NavidromeProbeCredentials>,
http_registry: Option<Arc<ServerHttpRegistry>>,
server_id: String,
library_scope: String,
capability_flags: CapabilityFlags,
@@ -132,6 +134,7 @@ impl<'a> InitialSyncRunner<'a> {
store,
subsonic,
navidrome: None,
http_registry: None,
server_id: server_id.into(),
library_scope: library_scope.into(),
capability_flags,
@@ -154,6 +157,11 @@ impl<'a> InitialSyncRunner<'a> {
self
}
pub fn with_http_registry(mut self, registry: Option<Arc<ServerHttpRegistry>>) -> Self {
self.http_registry = registry;
self
}
pub fn with_cancellation(mut self, flag: Arc<AtomicBool>) -> Self {
self.cancel = Some(flag);
self
@@ -580,6 +588,8 @@ impl<'a> InitialSyncRunner<'a> {
let cancel = self.cancel.clone();
let sleep_enabled = self.sleep_enabled;
let creds = creds.clone();
let http_registry = self.http_registry.clone();
let server_id = self.server_id.clone();
let mut queue = LinearPrefetchQueue::new(&budget, batch_size, offset);
loop {
@@ -590,6 +600,8 @@ impl<'a> InitialSyncRunner<'a> {
queue.pump(|| self.check_cancellation(), |off| {
let creds = creds.clone();
let cancel = cancel.clone();
let http_registry = http_registry.clone();
let server_id = server_id.clone();
tokio::spawn(async move {
retry_fetch(
sleep_enabled,
@@ -597,6 +609,8 @@ impl<'a> InitialSyncRunner<'a> {
|| async {
let end = off.saturating_add(batch_size);
let response = nd_list_songs_internal(
http_registry.as_deref(),
Some(&server_id),
&creds.server_url,
&creds.bearer_token,
"id",
@@ -671,6 +685,8 @@ impl<'a> InitialSyncRunner<'a> {
self,
|| {
nd_list_songs_internal(
self.http_registry.as_deref(),
Some(&self.server_id),
&creds.server_url,
&creds.bearer_token,
"id",
@@ -11,6 +11,7 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use psysonic_core::server_http::ServerHttpRegistry;
use psysonic_integration::subsonic::SubsonicClient;
use super::bandwidth::{ParallelismBudget, PlaybackHint};
@@ -45,6 +46,7 @@ pub struct BackgroundScheduler<'a> {
store: &'a LibraryStore,
subsonic: &'a SubsonicClient,
navidrome: Option<NavidromeProbeCredentials>,
http_registry: Option<Arc<ServerHttpRegistry>>,
server_id: String,
library_scope: String,
capability_flags: CapabilityFlags,
@@ -70,6 +72,7 @@ impl<'a> BackgroundScheduler<'a> {
store,
subsonic,
navidrome: None,
http_registry: None,
server_id: server_id.into(),
library_scope: library_scope.into(),
capability_flags,
@@ -87,6 +90,11 @@ impl<'a> BackgroundScheduler<'a> {
self
}
pub fn with_http_registry(mut self, registry: Option<Arc<ServerHttpRegistry>>) -> Self {
self.http_registry = registry;
self
}
pub fn with_playback_hint(mut self, hint: PlaybackHint) -> Self {
self.playback_hint = hint;
self
@@ -220,7 +228,8 @@ impl<'a> BackgroundScheduler<'a> {
&self.library_scope,
self.capability_flags,
)
.with_progress(Arc::clone(&self.progress));
.with_progress(Arc::clone(&self.progress))
.with_http_registry(self.http_registry.clone());
if let Some(creds) = &self.navidrome {
runner = runner.with_navidrome_credentials(creds.clone());
}