mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +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:
Generated
+18
@@ -3608,6 +3608,7 @@ dependencies = [
|
||||
"libc",
|
||||
"lofty",
|
||||
"md5",
|
||||
"psysonic-analysis",
|
||||
"psysonic-core",
|
||||
"reqwest",
|
||||
"ringbuf",
|
||||
@@ -3638,6 +3639,23 @@ dependencies = [
|
||||
"zbus 5.15.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psysonic-analysis"
|
||||
version = "1.46.0-dev"
|
||||
dependencies = [
|
||||
"ebur128",
|
||||
"futures-util",
|
||||
"md5",
|
||||
"psysonic-core",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"symphonia",
|
||||
"tauri",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psysonic-core"
|
||||
version = "1.46.0-dev"
|
||||
|
||||
@@ -31,6 +31,7 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
psysonic-core = { path = "crates/psysonic-core" }
|
||||
psysonic-analysis = { path = "crates/psysonic-analysis" }
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "psysonic-analysis"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
psysonic-core = { path = "../psysonic-core" }
|
||||
|
||||
tauri = { version = "2" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "time", "sync"] }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "rustls"] }
|
||||
futures-util = "0.3"
|
||||
ebur128 = "0.1"
|
||||
md5 = "0.8"
|
||||
rusqlite = { version = "0.39", features = ["bundled"] }
|
||||
symphonia = { version = "0.5", default-features = false, features = ["flac", "mp3", "pcm", "aac", "alac", "isomp4", "vorbis", "ogg", "wav", "adpcm"] }
|
||||
+61
-23
@@ -3,21 +3,21 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
use super::analysis_cache;
|
||||
use super::lib_commands::*;
|
||||
use super::subsonic_wire_user_agent;
|
||||
use psysonic_core::user_agent::subsonic_wire_user_agent;
|
||||
|
||||
use crate::analysis_cache;
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct WaveformUpdatedPayload {
|
||||
pub(crate) track_id: String,
|
||||
pub(crate) is_partial: bool,
|
||||
pub struct WaveformUpdatedPayload {
|
||||
pub track_id: String,
|
||||
pub is_partial: bool,
|
||||
}
|
||||
|
||||
// ─── HTTP backfill queue: download tracks + seed analysis cache ──────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum AnalysisBackfillEnqueueKind {
|
||||
pub enum AnalysisBackfillEnqueueKind {
|
||||
/// New job at the tail of the queue.
|
||||
NewBack,
|
||||
/// New job for the currently playing track (head).
|
||||
@@ -31,10 +31,10 @@ pub(crate) enum AnalysisBackfillEnqueueKind {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AnalysisBackfillQueueState {
|
||||
pub(crate) deque: VecDeque<(String, String)>,
|
||||
pub struct AnalysisBackfillQueueState {
|
||||
pub deque: VecDeque<(String, String)>,
|
||||
/// Set while this `track_id` is inside `analysis_backfill_download_and_seed` (not in deque).
|
||||
pub(crate) in_progress: Option<String>,
|
||||
pub in_progress: Option<String>,
|
||||
}
|
||||
|
||||
impl AnalysisBackfillQueueState {
|
||||
@@ -55,7 +55,7 @@ impl AnalysisBackfillQueueState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enqueue(
|
||||
pub fn enqueue(
|
||||
&mut self,
|
||||
tid: String,
|
||||
url: String,
|
||||
@@ -82,7 +82,7 @@ impl AnalysisBackfillQueueState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prune_queued_not_in(&mut self, keep_track_ids: &HashSet<&str>) -> usize {
|
||||
pub fn prune_queued_not_in(&mut self, keep_track_ids: &HashSet<&str>) -> usize {
|
||||
let before = self.deque.len();
|
||||
self.deque
|
||||
.retain(|(track_id, _)| keep_track_ids.contains(track_id.as_str()));
|
||||
@@ -90,13 +90,13 @@ impl AnalysisBackfillQueueState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AnalysisBackfillShared {
|
||||
pub(crate) state: Mutex<AnalysisBackfillQueueState>,
|
||||
pub struct AnalysisBackfillShared {
|
||||
pub state: Mutex<AnalysisBackfillQueueState>,
|
||||
wake_tx: tokio::sync::mpsc::UnboundedSender<()>,
|
||||
}
|
||||
|
||||
impl AnalysisBackfillShared {
|
||||
pub(crate) fn ping_worker(&self) {
|
||||
pub fn ping_worker(&self) {
|
||||
let _ = self.wake_tx.send(());
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl AnalysisBackfillShared {
|
||||
static ANALYSIS_BACKFILL: OnceLock<Arc<AnalysisBackfillShared>> = OnceLock::new();
|
||||
|
||||
/// Lazily spawns the single backfill worker (first caller supplies `AppHandle`).
|
||||
pub(crate) fn analysis_backfill_shared(app: &tauri::AppHandle) -> Arc<AnalysisBackfillShared> {
|
||||
pub fn analysis_backfill_shared(app: &tauri::AppHandle) -> Arc<AnalysisBackfillShared> {
|
||||
ANALYSIS_BACKFILL
|
||||
.get_or_init(|| {
|
||||
let (wake_tx, wake_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
@@ -120,6 +120,44 @@ pub(crate) fn analysis_backfill_shared(app: &tauri::AppHandle) -> Arc<AnalysisBa
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Decode `bytes` for `track_id` via the cpu-seed queue. Returns `Ok(true)` when
|
||||
/// a loudness row exists in the cache after the seed (cache-hit short-circuits as
|
||||
/// well as fresh decode hits).
|
||||
pub async fn enqueue_analysis_seed(
|
||||
app: &tauri::AppHandle,
|
||||
track_id: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<bool, String> {
|
||||
if let Some(cache) = app.try_state::<analysis_cache::AnalysisCache>() {
|
||||
if cache.cpu_seed_redundant_for_track(track_id).unwrap_or(false) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
let high = analysis_backfill_is_current_track(app, track_id);
|
||||
let outcome = submit_analysis_cpu_seed(
|
||||
app.clone(),
|
||||
track_id.to_string(),
|
||||
bytes.to_vec(),
|
||||
high,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::app_eprintln!("[analysis] failed to seed {}: {}", track_id, e);
|
||||
e
|
||||
})?;
|
||||
let has_loudness = app
|
||||
.try_state::<analysis_cache::AnalysisCache>()
|
||||
.and_then(|cache| cache.get_latest_loudness_for_track(track_id).ok().flatten())
|
||||
.is_some();
|
||||
crate::app_deprintln!(
|
||||
"[analysis] seed result track_id={} bytes={} has_loudness={} outcome={outcome:?}",
|
||||
track_id,
|
||||
bytes.len(),
|
||||
has_loudness
|
||||
);
|
||||
Ok(has_loudness)
|
||||
}
|
||||
|
||||
async fn analysis_backfill_download_and_seed(
|
||||
app: &tauri::AppHandle,
|
||||
track_id: &str,
|
||||
@@ -176,9 +214,9 @@ async fn analysis_backfill_worker_loop(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn analysis_backfill_is_current_track(app: &tauri::AppHandle, track_id: &str) -> bool {
|
||||
app.try_state::<crate::audio::AudioEngine>()
|
||||
.is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id))
|
||||
pub fn analysis_backfill_is_current_track(app: &tauri::AppHandle, track_id: &str) -> bool {
|
||||
app.try_state::<psysonic_core::ports::PlaybackQueryHandle>()
|
||||
.is_some_and(|p| p.is_track_currently_playing(track_id))
|
||||
}
|
||||
|
||||
// ─── Full-track waveform + loudness: single CPU worker (mirrors HTTP backfill queue) ─
|
||||
@@ -187,7 +225,7 @@ pub(crate) fn analysis_backfill_is_current_track(app: &tauri::AppHandle, track_i
|
||||
// submitters attach to `running` followers so they all get the same outcome.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum AnalysisCpuSeedEnqueueKind {
|
||||
pub enum AnalysisCpuSeedEnqueueKind {
|
||||
NewBack,
|
||||
NewFront,
|
||||
ReorderedFront,
|
||||
@@ -362,7 +400,7 @@ fn emit_analysis_queue_snapshot_line() {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn analysis_queue_snapshot_loop() {
|
||||
pub async fn analysis_queue_snapshot_loop() {
|
||||
emit_analysis_queue_snapshot_line();
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
@@ -432,7 +470,7 @@ async fn analysis_cpu_seed_worker_loop(
|
||||
///
|
||||
/// Returns `(http_removed, cpu_removed_jobs, cpu_removed_waiters)`. Either
|
||||
/// queue may not have been initialized yet — those slots return 0.
|
||||
pub(crate) fn prune_analysis_queues(
|
||||
pub fn prune_analysis_queues(
|
||||
keep_track_ids: &HashSet<&str>,
|
||||
) -> Result<(usize, usize, usize), String> {
|
||||
let http_removed = if let Some(shared) = ANALYSIS_BACKFILL.get() {
|
||||
@@ -464,7 +502,7 @@ pub(crate) fn prune_analysis_queues(
|
||||
/// Emits `analysis:waveform-updated` when analysis **wrote** new waveform data (`Upserted`).
|
||||
/// Cache-hit skips (`SkippedWaveformCacheHit`) omit the event so the frontend does not
|
||||
/// re-run loudness refresh / waveform IPC for rows that were already current.
|
||||
pub(crate) async fn submit_analysis_cpu_seed(
|
||||
pub async fn submit_analysis_cpu_seed(
|
||||
app: tauri::AppHandle,
|
||||
track_id: String,
|
||||
bytes: Vec<u8>,
|
||||
@@ -0,0 +1,13 @@
|
||||
//! `psysonic-analysis` — loudness/waveform analysis cache and the runtime
|
||||
//! that drives HTTP backfill + CPU-seed work queues.
|
||||
//!
|
||||
//! Submodules mirror the original layout in the top crate:
|
||||
//! - `analysis_cache` — SQLite-backed loudness/waveform store + compute helpers
|
||||
//! - `analysis_runtime` — backfill queue, CPU-seed queue, queue snapshot loop
|
||||
|
||||
pub mod analysis_cache;
|
||||
pub mod analysis_runtime;
|
||||
|
||||
// Re-export logging facade so submodules can write `crate::app_eprintln!()`
|
||||
// the same way they did when they lived in the top crate.
|
||||
pub use psysonic_core::{app_deprintln, app_eprintln, logging};
|
||||
@@ -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())
|
||||
}
|
||||
+23
-24
@@ -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
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
use crate::analysis_runtime::enqueue_analysis_seed;
|
||||
use crate::audio;
|
||||
use crate::subsonic_wire_user_agent;
|
||||
|
||||
use super::super::file_transfer::stream_to_file;
|
||||
use super::downloads::{resolve_hot_cache_root, HotCacheDownloadResult};
|
||||
use super::offline::{enqueue_analysis_seed, enqueue_analysis_seed_from_file};
|
||||
use super::offline::enqueue_analysis_seed_from_file;
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn download_track_hot_cache(
|
||||
|
||||
-2
@@ -15,5 +15,3 @@ pub(crate) use downloads::{
|
||||
check_arch_linux, download_update, download_zip, fetch_netease_lyrics, get_embedded_lyrics,
|
||||
open_folder,
|
||||
};
|
||||
// Internal helper consumed by analysis_runtime (used as analysis_runtime depends on it):
|
||||
pub(crate) use offline::enqueue_analysis_seed;
|
||||
|
||||
+1
-32
@@ -1,44 +1,13 @@
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::analysis_cache;
|
||||
use crate::analysis_runtime::{analysis_backfill_is_current_track, submit_analysis_cpu_seed};
|
||||
use crate::analysis_runtime::enqueue_analysis_seed;
|
||||
use crate::DownloadSemaphore;
|
||||
|
||||
use super::super::file_transfer::{finalize_streamed_download, subsonic_http_client};
|
||||
|
||||
// ─── Offline Track Cache ──────────────────────────────────────────────────────
|
||||
|
||||
pub(crate) async fn enqueue_analysis_seed(app: &tauri::AppHandle, track_id: &str, bytes: &[u8]) -> Result<bool, String> {
|
||||
if let Some(cache) = app.try_state::<analysis_cache::AnalysisCache>() {
|
||||
if cache.cpu_seed_redundant_for_track(track_id).unwrap_or(false) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
let high = analysis_backfill_is_current_track(app, track_id);
|
||||
let outcome = submit_analysis_cpu_seed(
|
||||
app.clone(),
|
||||
track_id.to_string(),
|
||||
bytes.to_vec(),
|
||||
high,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
crate::app_eprintln!("[analysis] failed to seed {}: {}", track_id, e);
|
||||
e
|
||||
})?;
|
||||
let has_loudness = app
|
||||
.try_state::<analysis_cache::AnalysisCache>()
|
||||
.and_then(|cache| cache.get_latest_loudness_for_track(track_id).ok().flatten())
|
||||
.is_some();
|
||||
crate::app_deprintln!(
|
||||
"[analysis] seed result track_id={} bytes={} has_loudness={} outcome={outcome:?}",
|
||||
track_id,
|
||||
bytes.len(),
|
||||
has_loudness
|
||||
);
|
||||
Ok(has_loudness)
|
||||
}
|
||||
|
||||
pub(crate) async fn enqueue_analysis_seed_from_file(
|
||||
app: &tauri::AppHandle,
|
||||
track_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user