mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
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:
@@ -6,3 +6,4 @@
|
||||
|
||||
pub mod logging;
|
||||
pub mod ports;
|
||||
pub mod user_agent;
|
||||
|
||||
@@ -1,49 +1,36 @@
|
||||
//! Cross-crate port traits.
|
||||
//! Cross-crate port handles.
|
||||
//!
|
||||
//! These traits exist purely to break dependency cycles that would otherwise
|
||||
//! force `psysonic-audio` ↔ `psysonic-analysis` to depend on each other in
|
||||
//! both directions. Implementers register themselves as Tauri State on app
|
||||
//! bootstrap; consumers look them up via `app.try_state::<Arc<dyn _>>()`.
|
||||
//! Exists to break the one back-edge in the audio↔analysis dependency:
|
||||
//! `psysonic-analysis` needs to ask "is this track currently playing?", but
|
||||
//! must not depend on `psysonic-audio` (which has the real dep on analysis,
|
||||
//! not the other way around).
|
||||
//!
|
||||
//! Layout:
|
||||
//! - `AnalysisOrchestrator` — implemented in `psysonic-analysis`, called from
|
||||
//! `psysonic-audio` to enqueue full-buffer loudness/waveform analysis.
|
||||
//! - `PlaybackQuery` — implemented in `psysonic-audio` on `AudioEngine`,
|
||||
//! called from `psysonic-analysis` to ask "is this track currently playing?".
|
||||
//! Implementation note: ports are exposed as **closure handles** rather than
|
||||
//! `Arc<dyn Trait>` — this avoids forcing every existing `State<AudioEngine>`
|
||||
//! callsite to switch to `State<Arc<AudioEngine>>` (which Tauri State requires
|
||||
//! for trait-object registration). The shell crate creates the handle by
|
||||
//! capturing an `AppHandle` and looking up the audio engine at call time.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Read-only queries about the live playback session. Implemented on
|
||||
/// `AudioEngine` in `psysonic-audio`.
|
||||
///
|
||||
/// Registered as `Arc<dyn PlaybackQuery>` in Tauri State so non-audio crates
|
||||
/// can query without taking a hard dep on the audio crate.
|
||||
pub trait PlaybackQuery: Send + Sync + 'static {
|
||||
/// `true` if `track_id` is the track currently being decoded/played.
|
||||
fn is_track_currently_playing(&self, track_id: &str) -> bool;
|
||||
/// "Is this track currently being decoded/played?" — implementation lives in
|
||||
/// `psysonic-audio` (`AudioEngine::analysis_track_id_is_current_playback`).
|
||||
/// The shell crate registers an instance constructed via [`PlaybackQueryHandle::new`]
|
||||
/// as Tauri State; consumers in `psysonic-analysis` look it up.
|
||||
#[derive(Clone)]
|
||||
pub struct PlaybackQueryHandle {
|
||||
inner: Arc<dyn Fn(&str) -> bool + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
/// Triggers full-buffer analysis on already-captured track bytes. Implemented
|
||||
/// on `AnalysisRuntime` in `psysonic-analysis`.
|
||||
///
|
||||
/// Registered as `Arc<dyn AnalysisOrchestrator>` in Tauri State so audio
|
||||
/// callsites (`stream::ranged_http`, `stream::track_stream`, `helpers`,
|
||||
/// `play_input`, `preload_commands`) can submit without depending on
|
||||
/// `psysonic-analysis`.
|
||||
pub trait AnalysisOrchestrator: Send + Sync + 'static {
|
||||
/// Enqueue a CPU-seed analysis pass for `track_id` over the given byte
|
||||
/// buffer. `high_priority` mirrors the HTTP-backfill head-insertion
|
||||
/// behaviour for the currently playing track.
|
||||
///
|
||||
/// Errors are stringified — audio callers only care about pass/fail.
|
||||
fn submit_cpu_seed<'a>(
|
||||
&'a self,
|
||||
app: AppHandle,
|
||||
track_id: String,
|
||||
bytes: Vec<u8>,
|
||||
high_priority: bool,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
|
||||
impl PlaybackQueryHandle {
|
||||
pub fn new<F>(f: F) -> Self
|
||||
where
|
||||
F: Fn(&str) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
Self { inner: Arc::new(f) }
|
||||
}
|
||||
|
||||
pub fn is_track_currently_playing(&self, track_id: &str) -> bool {
|
||||
(self.inner)(track_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Process-global outbound User-Agent for Rust-side HTTP.
|
||||
//!
|
||||
//! Initialised to `psysonic/<version>` (workspace package version) and then
|
||||
//! overridden from the main WebView's `navigator.userAgent` once the frontend
|
||||
//! reports it during startup. Every Rust HTTP client (`reqwest::Client`,
|
||||
//! handcrafted `header::USER_AGENT`) reads the current value via
|
||||
//! [`subsonic_wire_user_agent`] so a single switch keeps server-side
|
||||
//! request-fingerprints consistent.
|
||||
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
pub fn default_subsonic_wire_user_agent() -> String {
|
||||
format!("psysonic/{}", env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
pub 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()))
|
||||
}
|
||||
|
||||
pub fn subsonic_wire_user_agent() -> String {
|
||||
runtime_subsonic_wire_user_agent()
|
||||
.read()
|
||||
.map(|ua| ua.clone())
|
||||
.unwrap_or_else(|_| default_subsonic_wire_user_agent())
|
||||
}
|
||||
Reference in New Issue
Block a user