mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
refactor: move analysis admin commands into psysonic-analysis (M6/7)
Reframed M6 from "extract psysonic-commands" to "place each domain's
Tauri commands in its own domain crate" — the original psysonic-commands
proposal would have been a thin shell with no clear domain ownership
because each prior milestone already kept its own commands inline:
audio_*_commands in psysonic-audio
cache/sync commands in psysonic-syncfs
navidrome/discord/etc in psysonic-integration
The leftover analysis admin commands (7 of them) logically belong to
the analysis domain. So they move there:
src/lib_commands/app_api/analysis.rs →
crates/psysonic-analysis/src/commands.rs
WaveformCachePayload + LoudnessCachePayload moved out of top-crate
lib.rs into commands.rs
PlaybackQueryHandle gets a second closure (`should_defer_backfill`) so
analysis_enqueue_seed_from_url can ask "is a ranged playback already
going to seed this track?" without depending on psysonic-audio.
Top crate keeps the shell-flavored commands (window/tray/mini-player,
greet/exit_app, mpris/global-shortcuts/check_dir_accessible, perf,
cli_bridge) for M7 to clean up.
Behaviour preserving. Cargo check + clippy --workspace clean.
This commit is contained in:
+45
-17
@@ -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<u8>,
|
||||
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<Option<WaveformCachePayload>, 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<f64>,
|
||||
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<u64, String> {
|
||||
@@ -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<u64, String> {
|
||||
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<bool>,
|
||||
@@ -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::<crate::audio::AudioEngine>() {
|
||||
if crate::audio::ranged_loudness_backfill_should_defer(&engine, &track_id) {
|
||||
if let Some(playback) = app.try_state::<PlaybackQueryHandle>() {
|
||||
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<String>,
|
||||
) -> Result<AnalysisPrunePendingResult, String> {
|
||||
let mut normalized: Vec<String> = Vec::with_capacity(track_ids.len());
|
||||
@@ -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.
|
||||
|
||||
@@ -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<dyn Fn(&str) -> bool + Send + Sync + 'static>,
|
||||
is_playing: Arc<dyn Fn(&str) -> bool + Send + Sync + 'static>,
|
||||
should_defer_backfill: Arc<dyn Fn(&str) -> bool + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl PlaybackQueryHandle {
|
||||
pub fn new<F>(f: F) -> Self
|
||||
pub fn new<P, D>(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)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-38
@@ -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<Option<souvlaki::MediaControls>>;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WaveformCachePayload {
|
||||
bins: Vec<u8>,
|
||||
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::<crate::audio::AudioEngine>()
|
||||
.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::<crate::audio::AudioEngine>()
|
||||
.is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id))
|
||||
},
|
||||
move |track_id| {
|
||||
app_defer
|
||||
.try_state::<crate::audio::AudioEngine>()
|
||||
.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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user