diff --git a/src-tauri/src/lib_commands/app_api/analysis.rs b/src-tauri/crates/psysonic-analysis/src/commands.rs similarity index 81% rename from src-tauri/src/lib_commands/app_api/analysis.rs rename to src-tauri/crates/psysonic-analysis/src/commands.rs index 68bf6f99..efbf4dec 100644 --- a/src-tauri/src/lib_commands/app_api/analysis.rs +++ b/src-tauri/crates/psysonic-analysis/src/commands.rs @@ -1,16 +1,43 @@ +//! Tauri commands that read/write the analysis cache and steer the backfill +//! queue. Thin wrappers around `analysis_cache::*` and `analysis_runtime::*` +//! plus the playback-query port (for "is this track currently playing? / +//! is a ranged playback already going to seed it?"). + use std::collections::HashSet; use tauri::Manager; +use psysonic_core::ports::PlaybackQueryHandle; + use crate::analysis_cache; use crate::analysis_runtime::{ analysis_backfill_is_current_track, analysis_backfill_shared, prune_analysis_queues, AnalysisBackfillEnqueueKind, }; -use crate::{LoudnessCachePayload, WaveformCachePayload}; + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WaveformCachePayload { + pub bins: Vec, + pub bin_count: i64, + pub is_partial: bool, + pub known_until_sec: f64, + pub duration_sec: f64, + pub updated_at: i64, +} + +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoudnessCachePayload { + pub integrated_lufs: f64, + pub true_peak: f64, + pub recommended_gain_db: f64, + pub target_lufs: f64, + pub updated_at: i64, +} #[tauri::command] -pub(crate) fn analysis_get_waveform( +pub fn analysis_get_waveform( track_id: String, md5_16kb: String, cache: tauri::State<'_, analysis_cache::AnalysisCache>, @@ -50,7 +77,7 @@ pub(crate) fn analysis_get_waveform( } #[tauri::command] -pub(crate) fn analysis_get_waveform_for_track( +pub fn analysis_get_waveform_for_track( track_id: String, cache: tauri::State<'_, analysis_cache::AnalysisCache>, ) -> Result, String> { @@ -83,7 +110,7 @@ pub(crate) fn analysis_get_waveform_for_track( } #[tauri::command] -pub(crate) fn analysis_get_loudness_for_track( +pub fn analysis_get_loudness_for_track( track_id: String, target_lufs: Option, cache: tauri::State<'_, analysis_cache::AnalysisCache>, @@ -97,16 +124,17 @@ pub(crate) fn analysis_get_loudness_for_track( requested_target, ); LoudnessCachePayload { - integrated_lufs: v.integrated_lufs, - true_peak: v.true_peak, - recommended_gain_db, - target_lufs: requested_target, - updated_at: v.updated_at, - }})) + integrated_lufs: v.integrated_lufs, + true_peak: v.true_peak, + recommended_gain_db, + target_lufs: requested_target, + updated_at: v.updated_at, + } + })) } #[tauri::command] -pub(crate) fn analysis_delete_loudness_for_track( +pub fn analysis_delete_loudness_for_track( track_id: String, cache: tauri::State<'_, analysis_cache::AnalysisCache>, ) -> Result { @@ -114,14 +142,14 @@ pub(crate) fn analysis_delete_loudness_for_track( } #[tauri::command] -pub(crate) fn analysis_delete_all_waveforms( +pub fn analysis_delete_all_waveforms( cache: tauri::State<'_, analysis_cache::AnalysisCache>, ) -> Result { cache.delete_all_waveforms() } #[tauri::command] -pub(crate) fn analysis_enqueue_seed_from_url( +pub fn analysis_enqueue_seed_from_url( track_id: String, url: String, force: Option, @@ -132,8 +160,8 @@ pub(crate) fn analysis_enqueue_seed_from_url( } let force = force.unwrap_or(false); if !force { - if let Some(engine) = app.try_state::() { - if crate::audio::ranged_loudness_backfill_should_defer(&engine, &track_id) { + if let Some(playback) = app.try_state::() { + if playback.ranged_loudness_backfill_should_defer(&track_id) { crate::app_deprintln!( "[analysis] backfill skip track_id={} reason=ranged_playback_will_seed", track_id @@ -186,7 +214,7 @@ pub(crate) fn analysis_enqueue_seed_from_url( #[derive(Debug, Clone, serde::Serialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct AnalysisPrunePendingResult { +pub struct AnalysisPrunePendingResult { pub keep_count: usize, pub http_removed: usize, pub cpu_removed_jobs: usize, @@ -197,7 +225,7 @@ pub(crate) struct AnalysisPrunePendingResult { /// /// Keeps currently-running jobs untouched; only queued (not-yet-started) jobs are removed. #[tauri::command] -pub(crate) fn analysis_prune_pending_to_track_ids( +pub fn analysis_prune_pending_to_track_ids( track_ids: Vec, ) -> Result { let mut normalized: Vec = Vec::with_capacity(track_ids.len()); diff --git a/src-tauri/crates/psysonic-analysis/src/lib.rs b/src-tauri/crates/psysonic-analysis/src/lib.rs index 0f1e8a23..d69cf4ca 100644 --- a/src-tauri/crates/psysonic-analysis/src/lib.rs +++ b/src-tauri/crates/psysonic-analysis/src/lib.rs @@ -7,6 +7,7 @@ pub mod analysis_cache; pub mod analysis_runtime; +pub mod commands; // Re-export logging facade so submodules can write `crate::app_eprintln!()` // the same way they did when they lived in the top crate. diff --git a/src-tauri/crates/psysonic-core/src/ports.rs b/src-tauri/crates/psysonic-core/src/ports.rs index c3dce590..fc4f0610 100644 --- a/src-tauri/crates/psysonic-core/src/ports.rs +++ b/src-tauri/crates/psysonic-core/src/ports.rs @@ -13,24 +13,39 @@ use std::sync::Arc; -/// "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. +/// Read-only queries about the live playback session, used by analysis-side +/// code to break the analysis→audio back-edge. The shell crate constructs an +/// instance with two closures (each capturing an `AppHandle`) and registers it +/// as Tauri State; `psysonic-analysis` looks it up via `try_state::<…>()`. +/// +/// The closures are independent so each can be a no-op / always-false fallback +/// without coupling the other. #[derive(Clone)] pub struct PlaybackQueryHandle { - inner: Arc bool + Send + Sync + 'static>, + is_playing: Arc bool + Send + Sync + 'static>, + should_defer_backfill: Arc bool + Send + Sync + 'static>, } impl PlaybackQueryHandle { - pub fn new(f: F) -> Self + pub fn new(is_playing: P, should_defer_backfill: D) -> Self where - F: Fn(&str) -> bool + Send + Sync + 'static, + P: Fn(&str) -> bool + Send + Sync + 'static, + D: Fn(&str) -> bool + Send + Sync + 'static, { - Self { inner: Arc::new(f) } + Self { + is_playing: Arc::new(is_playing), + should_defer_backfill: Arc::new(should_defer_backfill), + } } + /// `true` if `track_id` is the track currently being decoded/played. pub fn is_track_currently_playing(&self, track_id: &str) -> bool { - (self.inner)(track_id) + (self.is_playing)(track_id) + } + + /// `true` if a ranged HTTP playback for `track_id` is mid-flight and will + /// seed analysis on completion — the backfill enqueue should defer. + pub fn ranged_loudness_backfill_should_defer(&self, track_id: &str) -> bool { + (self.should_defer_backfill)(track_id) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0bf06e4c..a1b6f21e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -38,27 +38,6 @@ const MAX_DL_CONCURRENCY: usize = 4; /// `None` if souvlaki failed to initialize (e.g. no D-Bus session on Linux). type MprisControls = Mutex>; -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct WaveformCachePayload { - bins: Vec, - bin_count: i64, - is_partial: bool, - known_until_sec: f64, - duration_sec: f64, - updated_at: i64, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LoudnessCachePayload { - integrated_lufs: f64, - true_peak: f64, - recommended_gain_db: f64, - target_lufs: f64, - updated_at: i64, -} - pub fn run() { // Linux: second `psysonic --player …` forwards over D-Bus before heavy startup. #[cfg(target_os = "linux")] @@ -125,17 +104,25 @@ pub fn run() { } // ── 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. + // Two closures, each capturing an AppHandle, so analysis_runtime + // can ask AudioEngine playback questions without depending on the + // audio crate. { - let app_for_query = app.handle().clone(); - let real_handle = psysonic_core::ports::PlaybackQueryHandle::new(move |track_id| { - app_for_query - .try_state::() - .is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id)) - }); - app.manage(real_handle); + let app_is_playing = app.handle().clone(); + let app_defer = app.handle().clone(); + let handle = psysonic_core::ports::PlaybackQueryHandle::new( + move |track_id| { + app_is_playing + .try_state::() + .is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id)) + }, + move |track_id| { + app_defer + .try_state::() + .is_some_and(|e| crate::audio::ranged_loudness_backfill_should_defer(&e, track_id)) + }, + ); + app.manage(handle); } // Periodic analysis queue sizes (debug logging mode only). @@ -421,13 +408,13 @@ pub fn run() { psysonic_integration::remote::fetch_json_url, psysonic_integration::remote::fetch_icy_metadata, psysonic_integration::remote::resolve_stream_url, - analysis_get_waveform, - analysis_get_waveform_for_track, - analysis_get_loudness_for_track, - analysis_delete_loudness_for_track, - analysis_delete_all_waveforms, - analysis_enqueue_seed_from_url, - analysis_prune_pending_to_track_ids, + psysonic_analysis::commands::analysis_get_waveform, + psysonic_analysis::commands::analysis_get_waveform_for_track, + psysonic_analysis::commands::analysis_get_loudness_for_track, + psysonic_analysis::commands::analysis_delete_loudness_for_track, + psysonic_analysis::commands::analysis_delete_all_waveforms, + psysonic_analysis::commands::analysis_enqueue_seed_from_url, + psysonic_analysis::commands::analysis_prune_pending_to_track_ids, psysonic_syncfs::cache::offline::download_track_offline, psysonic_syncfs::cache::offline::delete_offline_track, psysonic_syncfs::cache::offline::get_offline_cache_size, diff --git a/src-tauri/src/lib_commands/app_api/mod.rs b/src-tauri/src/lib_commands/app_api/mod.rs index 30820c11..204e3c91 100644 --- a/src-tauri/src/lib_commands/app_api/mod.rs +++ b/src-tauri/src/lib_commands/app_api/mod.rs @@ -1,4 +1,3 @@ -mod analysis; mod cli_bridge; mod core; mod integration; @@ -20,12 +19,7 @@ pub(crate) use integration::{ check_dir_accessible, mpris_set_metadata, mpris_set_playback, register_global_shortcut, unregister_global_shortcut, }; -pub(crate) use analysis::{ - analysis_delete_all_waveforms, analysis_delete_loudness_for_track, - analysis_enqueue_seed_from_url, analysis_get_loudness_for_track, analysis_get_waveform, - analysis_get_waveform_for_track, analysis_prune_pending_to_track_ids, -}; -// Discord, Navidrome admin, last.fm + radio-browser + CORS proxy, bandsintown -// now live in `psysonic_integration`. invoke_handler! in lib.rs registers -// them with their full paths so Tauri's `__cmd__*` macros resolve correctly. +// Discord, Navidrome admin, last.fm + radio-browser + CORS proxy, bandsintown, +// and analysis admin commands now live in their domain crates. invoke_handler! +// in lib.rs registers them with full paths so Tauri's `__cmd__*` macros resolve.