refactor: extract psysonic-analysis crate (M2/7)

Moves analysis_cache + analysis_runtime out of the top crate into a new
psysonic-analysis crate, plus the runtime user-agent facade into
psysonic-core. The audio↔analysis dependency cycle is broken via a
PlaybackQueryHandle port registered as Tauri State.

  crates/psysonic-analysis/                new lib crate
    src/analysis_cache/{mod,store,compute} unchanged behaviour
    src/analysis_runtime.rs                + enqueue_analysis_seed (was
                                           in lib_commands/cache/offline)

  crates/psysonic-core/src/
    user_agent.rs                          subsonic_wire_user_agent +
                                           runtime/default helpers
                                           (was in top lib.rs)
    ports.rs                               PlaybackQueryHandle: closure
                                           wrapper, not Arc<dyn Trait>,
                                           so existing State<AudioEngine>
                                           callsites stay unchanged

The shell setup hook registers the real PlaybackQueryHandle once the
AppHandle is available; the closure captures it and re-resolves
AudioEngine via try_state at each call.

Top crate keeps `crate::analysis_cache`, `crate::analysis_runtime`,
`crate::subsonic_wire_user_agent`, and `crate::submit_analysis_cpu_seed`
working via re-exports — no audio/lib_commands callsite needed editing.

Behaviour preserving. Cargo check + clippy --workspace clean (only
pre-existing warnings carry over).
This commit is contained in:
Psychotoxical
2026-05-09 13:25:04 +02:00
parent 7718ac3ee5
commit ff456dd823
15 changed files with 195 additions and 124 deletions
+23 -24
View File
@@ -2,23 +2,27 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod audio;
mod analysis_cache;
mod analysis_runtime;
pub mod cli;
mod discord;
mod lib_commands;
pub use psysonic_core::logging;
pub use psysonic_core::{app_eprintln, app_deprintln};
pub use psysonic_core::user_agent::{
default_subsonic_wire_user_agent, runtime_subsonic_wire_user_agent, subsonic_wire_user_agent,
};
pub use psysonic_analysis::{analysis_cache, analysis_runtime};
// `crate::submit_analysis_cpu_seed` shorthand kept so the audio modules don't
// have to spell out `psysonic_analysis::analysis_runtime::...` until M3.
pub(crate) use psysonic_analysis::analysis_runtime::submit_analysis_cpu_seed;
#[cfg(target_os = "windows")]
mod taskbar_win;
mod tray_runtime;
pub(crate) use analysis_runtime::*;
pub(crate) use tray_runtime::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::sync::{Arc, Mutex, OnceLock};
use std::sync::atomic::AtomicBool;
use tauri::{Emitter, Manager};
@@ -32,25 +36,6 @@ type ShortcutMap = Mutex<HashMap<String, String>>;
/// The frontend queues more tasks than this; Rust is the real throttle.
const MAX_DL_CONCURRENCY: usize = 4;
fn default_subsonic_wire_user_agent() -> String {
format!("psysonic/{}", env!("CARGO_PKG_VERSION"))
}
fn runtime_subsonic_wire_user_agent() -> &'static RwLock<String> {
static UA: OnceLock<RwLock<String>> = OnceLock::new();
UA.get_or_init(|| RwLock::new(default_subsonic_wire_user_agent()))
}
/// Unified outbound User-Agent for all Rust-side HTTP requests.
/// It is initialized with `psysonic/<version>` and then overridden from
/// the main WebView `navigator.userAgent` at app startup.
pub(crate) fn subsonic_wire_user_agent() -> String {
runtime_subsonic_wire_user_agent()
.read()
.map(|ua| ua.clone())
.unwrap_or_else(|_| default_subsonic_wire_user_agent())
}
/// Shared semaphore that caps simultaneous `download_track_offline` executions.
type DownloadSemaphore = Arc<tokio::sync::Semaphore>;
@@ -151,8 +136,22 @@ pub fn run() {
app.manage(cache);
}
// ── Playback-query port (analysis → audio back-edge) ──────────
// Replace the placeholder registered above with a real handle
// that has access to the AppHandle, so analysis_runtime can ask
// AudioEngine if a track is currently playing.
{
let app_for_query = app.handle().clone();
let real_handle = psysonic_core::ports::PlaybackQueryHandle::new(move |track_id| {
app_for_query
.try_state::<crate::audio::AudioEngine>()
.is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id))
});
app.manage(real_handle);
}
// Periodic analysis queue sizes (debug logging mode only).
tauri::async_runtime::spawn(analysis_queue_snapshot_loop());
tauri::async_runtime::spawn(psysonic_analysis::analysis_runtime::analysis_queue_snapshot_loop());
// ── Custom title bar on Linux ─────────────────────────────────
// Remove OS window decorations on all Linux so the React TitleBar