mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
This commit is contained in:
@@ -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(®istry)));
|
||||
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(®istry)));
|
||||
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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user