Files
psysonic/src-tauri/src/lib.rs
T
Psychotoxical b3573e7107 feat(themes): import a theme from a local .zip (#1012)
* feat(themes): validate and extract locally imported theme packages

Add the backend + validation half of local theme import: users will be
able to load a theme packaged as a .zip (manifest.json + theme.css).

- `import_theme_zip` Tauri command unpacks only manifest.json + theme.css
  from the archive, outside the webview, with an archive-size cap,
  per-entry uncompressed caps, and path-traversal rejection.
- `validateThemePackage` runs the full theme-store contract in-app: the
  manifest schema (field patterns copied from the repo schema), the CSS
  token whitelist with all core tokens required, color-scheme matching
  the declared mode, data-URI restricted to the arrow token, and a guard
  against ids that collide with built-in themes. The existing
  `validateThemeCss` containment guard is reused and runs again at
  injection time. The contract token list is a byte-identical copy of the
  themes repo's allowed-tokens.json.

Covered by validateThemePackage tests for every rejection class.

* feat(themes): add the Import a theme section to the Themes tab

A dedicated section between the theme scheduler and the Theme Store lets
users import a theme from a local .zip. After the package validates, a
confirmation dialog names the theme and its author before it is installed.

A rejected import shows a plain-language explanation aimed at end users;
the raw contract diagnostics (token names, missing fields) are tucked
into a collapsible "Technical details" block for theme authors. Fully
localised across all nine languages.

* docs(themes): changelog + credits for local theme import

Fold the local .zip import (and the store pagination / refresh-scroll
polish) into the still-unreleased Theme Store entry rather than a new
section, and extend the credits line. PRs #1011 and #1012.
2026-06-07 11:35:52 +02:00

828 lines
42 KiB
Rust

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod cli;
mod cover_cache;
mod library_analysis_backfill;
mod lib_commands;
mod theme_import;
pub use psysonic_integration::discord;
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};
pub use psysonic_audio as audio;
pub use psysonic_syncfs::{sync_cancel_flags, DownloadSemaphore};
#[cfg(target_os = "windows")]
mod taskbar_win;
mod tray_runtime;
pub(crate) use tray_runtime::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tauri::{Emitter, Manager};
use lib_commands::*;
/// Tracks which user-configured shortcuts are currently registered (shortcut_str → action).
/// Prevents on_shortcut() accumulating duplicate handlers across JS reloads (HMR / StrictMode).
type ShortcutMap = Mutex<HashMap<String, String>>;
/// Maximum number of offline track downloads that can run concurrently.
/// The frontend queues more tasks than this; Rust is the real throttle.
const MAX_DL_CONCURRENCY: usize = 4;
/// Shared handle to OS media controls (MPRIS2 on Linux, Now Playing on macOS, SMTC on Windows).
/// `None` if souvlaki failed to initialize (e.g. no D-Bus session on Linux).
type MprisControls = Mutex<Option<souvlaki::MediaControls>>;
/// Release builds only: focus or CLI-hand off when a second instance is launched.
#[cfg(not(debug_assertions))]
fn on_second_instance<R: tauri::Runtime>(
app: &tauri::AppHandle<R>,
argv: Vec<String>,
_cwd: String,
) {
if !crate::cli::handle_cli_on_primary_instance(app, &argv) {
let window = app.get_webview_window("main").expect("no main window");
// The window may have been hidden via the close-to-tray path,
// which injects PAUSE_RENDERING_JS (sets `__psyHidden=true`,
// pauses CSS animations). Tray-icon restore mirrors this with
// RESUME_RENDERING_JS — second-launch restore must do the same,
// otherwise the webview comes back with rendering still paused
// and navigation looks blank (issue #497).
let _ = window.eval(RESUME_RENDERING_JS);
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
pub fn run() {
// Linux: second `psysonic --player …` forwards over D-Bus before heavy startup.
#[cfg(target_os = "linux")]
{
let argv: Vec<String> = std::env::args().collect();
if crate::cli::parse_cli_command(&argv).is_some() {
match crate::cli::linux_try_forward_player_cli_secondary(&argv) {
Ok(crate::cli::LinuxPlayerForwardResult::Forwarded) => std::process::exit(0),
Ok(crate::cli::LinuxPlayerForwardResult::ContinueStartup) => {}
Err(msg) => {
crate::app_eprintln!("NOT OK: {msg}");
std::process::exit(1);
}
}
}
}
let (audio_engine, _audio_thread) = audio::create_engine();
let builder = tauri::Builder::default()
.manage(audio_engine)
.manage(ShortcutMap::default())
.manage(discord::DiscordState::new())
.manage(Arc::new(tokio::sync::Semaphore::new(MAX_DL_CONCURRENCY)) as DownloadSemaphore)
.manage(TrayState::default())
.manage(TrayTooltip::default())
.manage(TrayPlaybackState::default())
.manage(TrayMenuItemsState::default())
.manage(TrayMenuLabelsState::default())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_denylist(&["mini"])
.build()
)
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init());
#[cfg(not(debug_assertions))]
let builder = builder.plugin(tauri_plugin_single_instance::init(on_second_instance));
builder
.setup(|app| {
#[cfg(debug_assertions)]
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_title("Psysonic (Dev)");
}
// ── Analysis cache (SQLite) ───────────────────────────────────
{
let cache = analysis_cache::AnalysisCache::init(app.handle())
.map_err(|e| format!("analysis cache init failed: {e}"))?;
app.manage(cache);
}
cover_cache::init_cover_cache(app.handle())
.map_err(|e| format!("cover cache init failed: {e}"))?;
library_analysis_backfill::init_library_analysis_backfill(app.handle())
.map_err(|e| format!("library analysis backfill init failed: {e}"))?;
// ── Library track store (psysonic-library, PR-5a + PR-5b) ─────
// PR-5a brought up the read-only Tauri surface + LibraryRuntime.
// PR-5b adds the mutating commands, sync session map, current-job
// tracker, and the 30-second background scheduler tick task below
// — which sweeps every bound session through
// `BackgroundScheduler::tick` while honouring the runtime's
// `scheduler_cancel` flag.
{
let store = psysonic_library::store::LibraryStore::init(app.handle())
.map_err(|e| format!("library store init failed: {e}"))?;
let runtime = psysonic_library::LibraryRuntime::new(std::sync::Arc::new(store));
app.manage(runtime);
let app_for_sched = app.handle().clone();
tauri::async_runtime::spawn(async move {
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::time::MissedTickBehavior;
let mut interval = tokio::time::interval(Duration::from_secs(30));
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
interval.tick().await;
let Some(state) = app_for_sched
.try_state::<psysonic_library::LibraryRuntime>()
else {
break;
};
if state.scheduler_cancel.load(Ordering::SeqCst) {
break;
}
let sessions = state.snapshot_sessions();
if sessions.is_empty() {
continue;
}
let hint = state.current_playback_hint();
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis().min(i64::MAX as u128) as i64)
.unwrap_or(0);
for session in sessions {
let scope = session.library_scope.clone().unwrap_or_default();
let flags_bits = psysonic_library::repos::SyncStateRepository::new(
&state.store,
)
.get_capability_flags(&session.server_id, &scope)
.ok()
.flatten()
.unwrap_or(0);
let flags = psysonic_library::sync::capability::CapabilityFlags::new(
flags_bits,
);
let subsonic = psysonic_integration::subsonic::SubsonicClient::new(
session.base_url.clone(),
session.username.clone(),
session.password.clone(),
);
let mut sched =
psysonic_library::sync::scheduler::BackgroundScheduler::new(
&state.store,
&subsonic,
session.server_id.clone(),
scope.clone(),
flags,
)
.with_playback_hint(hint);
if let Some(tok) = session.navidrome_token.clone() {
sched = sched.with_navidrome_credentials(
psysonic_library::sync::capability::NavidromeProbeCredentials {
server_url: session.base_url.clone(),
bearer_token: tok,
},
);
}
let foreground_job = state
.current_job()
.is_some_and(|j| j.server_id == session.server_id);
if foreground_job {
sched = sched.with_foreground_sync_job_active(true);
}
let _ = sched.tick(now_ms).await;
// Background ticks stay silent in PR-5b — Tauri
// emit for the scheduler path lands when the
// Settings panel needs it (PR-5c). Manual
// `library_sync_start` already emits via its
// own orchestrator.
}
}
});
}
audio::cleanup_orphan_stream_spill_dir(app.handle());
// ── Playback-query port (analysis → audio back-edge) ──────────
// Two closures, each capturing an AppHandle, so analysis_runtime
// can ask AudioEngine playback questions without depending on the
// audio crate.
{
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);
}
app.manage(psysonic_analysis::analysis_runtime::PlaybackPriorityHints::default());
// ── Content-hash sink (analysis → library E2 back-edge) ───────
// After a seed the analysis pipeline records the playback-derived
// md5_16kb as `track.content_hash` so id-remap can rebind a track
// when the server reassigns ids. Decoupled from psysonic-library
// via a psysonic-core port; a no-op when the library has no row for
// the (server_id, track_id) — i.e. the index is off for that server.
{
let app_for_hash = app.handle().clone();
let sink = psysonic_core::ports::ContentHashSink::new(
move |server_id: &str, track_id: &str, md5: &str| {
if let Some(runtime) =
app_for_hash.try_state::<psysonic_library::LibraryRuntime>()
{
let _ = psysonic_library::commands::patch_content_hash(
&runtime, server_id, track_id, md5,
);
}
},
);
app.manage(sink);
}
// ── Analysis-readiness query (library → analysis E3 back-edge) ──
// `library_get_track` enrichment asks whether waveform/loudness are
// cached for (server_id, track_id, content_hash). Read-only probe:
// exact key then legacy '' fallback, no re-tag. Decoupled from
// psysonic-analysis via a psysonic-core port.
{
let app_for_readiness = app.handle().clone();
let query = psysonic_core::ports::AnalysisReadinessQuery::new(
move |server_id: &str, track_id: &str, md5: &str| {
let Some(cache) = app_for_readiness
.try_state::<analysis_cache::AnalysisCache>()
else {
return (false, false);
};
let probe = |sid: &str| {
let key = analysis_cache::TrackKey {
server_id: sid.to_string(),
track_id: track_id.to_string(),
md5_16kb: md5.to_string(),
};
let wf = cache.get_waveform(&key).ok().flatten().is_some();
let ld = cache.loudness_row_exists_for_key(&key).unwrap_or(false);
(wf, ld)
};
let (wf, ld) = probe(server_id);
// Legacy '' fallback for rows analysed before E1 wiring.
let wf = wf || (!server_id.is_empty() && probe("").0);
let ld = ld || (!server_id.is_empty() && probe("").1);
(wf, ld)
},
);
app.manage(query);
}
// ── Analysis needs-work probe (library → analysis batch scan) ──
{
use psysonic_core::ports::TrackAnalysisNeedsWorkQuery;
let app_for_needs_work = app.handle().clone();
let needs_work = TrackAnalysisNeedsWorkQuery::new(
move |server_id: &str, track_id: &str| {
psysonic_analysis::analysis_runtime::track_analysis_needs_work(
&app_for_needs_work,
server_id,
track_id,
)
},
);
app.manage(needs_work);
}
// ── Track enrichment port (analysis → library facts) ───────────
{
use psysonic_core::track_enrichment::{TrackEnrichmentPlan, TrackEnrichmentPort};
use std::time::{SystemTime, UNIX_EPOCH};
fn enrichment_now_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
let app_for_enrichment_plan = app.handle().clone();
let app_for_enrichment_store = app.handle().clone();
let port = TrackEnrichmentPort::new(
move |server_id: &str, track_id: &str, content_hash: &str| {
let Some(runtime) =
app_for_enrichment_plan.try_state::<psysonic_library::LibraryRuntime>()
else {
return TrackEnrichmentPlan::default();
};
match psysonic_library::enrichment::plan_track_enrichment(
&runtime.store,
server_id,
track_id,
content_hash,
enrichment_now_unix_ms(),
) {
Ok(plan) => plan,
Err(e) => {
eprintln!(
"[enrichment] plan failed server_id={server_id} track_id={track_id}: {e}"
);
TrackEnrichmentPlan {
need_bpm: true,
need_valence: true,
need_arousal: true,
need_moods: true,
}
}
}
},
move |server_id: &str,
track_id: &str,
content_hash: &str,
facts: &psysonic_core::track_enrichment::TrackEnrichmentFacts| {
let Some(runtime) =
app_for_enrichment_store.try_state::<psysonic_library::LibraryRuntime>()
else {
return Err("library runtime unavailable".into());
};
psysonic_library::enrichment::store_track_enrichment_facts(
&runtime.store,
server_id,
track_id,
content_hash,
facts,
enrichment_now_unix_ms(),
)
},
);
app.manage(port);
}
// Periodic analysis queue sizes (debug logging mode only).
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
// can take over. The frontend checks is_tiling_wm() to decide
// whether to actually render the TitleBar (hidden on tiling WMs).
#[cfg(target_os = "linux")]
{
use tauri::Manager;
let handle = app.handle().clone();
sync_wayland_text_profile_cache_from_disk(&handle);
if let Some(win) = app.get_webview_window("main") {
let _ = win.set_decorations(false);
let _ = linux_webkit_apply_wayland_gpu_font_tuning(&win);
let _ = linux_webkit_reapply_cached_wayland_text_render_profile(&win);
}
}
// ── System tray ───────────────────────────────────────────────
// Always build on startup when possible; the frontend calls toggle_tray_icon(false)
// immediately after load if the user has disabled the tray icon.
// May be skipped if Ayatana/AppIndicator libraries are missing (no panic).
{
if let Some(tray) = try_build_tray_icon(app.handle()) {
*app.state::<TrayState>().lock().unwrap() = Some(tray);
}
}
// ── MPRIS2 / OS media controls via souvlaki ──────────────────
// Release only: debug builds share the D-Bus name / SMTC slot with prod.
#[cfg(not(debug_assertions))]
{
use souvlaki::{MediaControlEvent, MediaControls, PlatformConfig};
// Collect pre-conditions and the platform-specific HWND.
// Returns None early (with a log) on any unrecoverable condition
// so app.manage() always executes exactly once at the bottom.
let maybe_controls: Option<MediaControls> = (|| {
// Linux: requires a live D-Bus session.
#[cfg(target_os = "linux")]
{
let dbus_ok = std::env::var("DBUS_SESSION_BUS_ADDRESS")
.map(|v| !v.is_empty())
.unwrap_or(false);
if !dbus_ok {
crate::app_eprintln!("[Psysonic] No D-Bus session — MPRIS media controls disabled");
return None;
}
}
// Windows: souvlaki SMTC must hook into the existing Win32
// message loop rather than spinning up its own. Pass the
// main window's HWND so it can do so. If we can't get one,
// skip init (no crash, just no media overlay).
#[cfg(target_os = "windows")]
let hwnd = {
use tauri::Manager;
let h = app.get_webview_window("main")
.and_then(|w| w.hwnd().ok())
.map(|h| h.0);
if h.is_none() {
crate::app_eprintln!("[Psysonic] Could not get HWND — Windows media controls disabled");
return None;
}
h
};
#[cfg(not(target_os = "windows"))]
let hwnd: Option<*mut std::ffi::c_void> = None;
let config = PlatformConfig {
dbus_name: "psysonic",
display_name: "Psysonic",
hwnd,
};
match MediaControls::new(config) {
Ok(mut controls) => {
let app_handle = app.handle().clone();
if let Err(e) = controls.attach(move |event: MediaControlEvent| {
match event {
MediaControlEvent::Toggle
| MediaControlEvent::Play
| MediaControlEvent::Pause => {
let _ = app_handle.emit("media:play-pause", ());
}
MediaControlEvent::Next => {
let _ = app_handle.emit("media:next", ());
}
MediaControlEvent::Previous => {
let _ = app_handle.emit("media:prev", ());
}
MediaControlEvent::Seek(direction) => {
use souvlaki::SeekDirection;
let delta: f64 = match direction {
SeekDirection::Forward => 5.0,
SeekDirection::Backward => -5.0,
};
let _ = app_handle.emit("media:seek-relative", delta);
}
MediaControlEvent::SetPosition(pos) => {
let secs = pos.0.as_secs_f64();
let _ = app_handle.emit("media:seek-absolute", secs);
}
_ => {}
}
}) {
crate::app_eprintln!("[Psysonic] Failed to attach media controls: {e:?}");
}
Some(controls)
}
Err(e) => {
crate::app_eprintln!("[Psysonic] Could not create media controls: {e:?}");
None
}
}
})();
app.manage(MprisControls::new(maybe_controls));
}
#[cfg(debug_assertions)]
{
app.manage(MprisControls::new(None));
}
// ── Windows Taskbar Thumbnail Toolbar ────────────────────────
#[cfg(all(target_os = "windows", not(debug_assertions)))]
{
use tauri::Manager;
if let Some(w) = app.get_webview_window("main") {
if let Ok(hwnd) = w.hwnd() {
taskbar_win::init(app.handle(), hwnd.0 as isize);
}
}
}
// ── Audio device-change watcher ───────────────────────────────
{
use tauri::Manager;
let engine = app.state::<audio::AudioEngine>();
audio::start_device_watcher(&engine, app.handle().clone());
}
// ── Reopen output after system sleep/resume (WASAPI / PipeWire etc.)
audio::register_post_sleep_audio_recovery(app.handle().clone());
// ── Pre-create mini player window (Windows) ──────────────────
// Creating the second WebView2 webview lazily from an invoke
// handler on Windows reliably stalls the Tauri event loop —
// the mini shows a blank white window, neither main nor mini
// can be closed, and the user has to kill the process via
// Task Manager. Building it at startup (hidden) avoids the
// runtime-creation code path entirely; later `open_mini_player`
// calls are pure show/hide.
#[cfg(target_os = "windows")]
{
if let Err(e) = build_mini_player_window(app.handle(), false) {
crate::app_eprintln!("[psysonic] Failed to pre-create mini window: {e}");
}
}
// Cold start with `--player …`: defer emit so the webview can register listeners.
crate::cli::spawn_deferred_cli_argv_handler(app.handle());
Ok(())
})
.on_window_event(|window, event| {
// Persist mini player position whenever the user drags it.
if window.label() == "mini" {
if let tauri::WindowEvent::Moved(pos) = event {
persist_mini_pos_throttled(window.app_handle(), pos.x, pos.y);
}
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if window.label() == "main" {
api.prevent_close();
#[cfg(target_os = "macos")]
{
// On macOS the red close button quits the app entirely.
// Route through JS so playback position + Orbit state get
// flushed; exit_app on the way back stops the audio engine.
let _ = window.emit("app:force-quit", ());
}
#[cfg(not(target_os = "macos"))]
{
// Pause rendering before JS decides whether to hide to tray or exit.
if let Some(w) = window.app_handle().get_webview_window("main") {
let _ = w.eval(PAUSE_RENDERING_JS);
}
// Let JS decide: minimize to tray or exit, based on user setting.
let _ = window.emit("window:close-requested", ());
}
} else if window.label() == "mini" {
// Native close on the mini: hide instead of destroying so
// state is preserved, and restore the main window.
api.prevent_close();
if let Some(w) = window.app_handle().get_webview_window("mini") {
let _ = w.eval(PAUSE_RENDERING_JS);
}
let _ = window.hide();
if let Some(main) = window.app_handle().get_webview_window("main") {
let _ = main.unminimize();
let _ = main.show();
let _ = main.set_focus();
}
}
}
})
.invoke_handler(tauri::generate_handler![
greet,
theme_import::import_theme_zip,
backup_export_library_db,
backup_import_library_db,
backup_export_full,
backup_import_full,
migration_inspect,
migration_run,
resolve_host_addresses,
psysonic_syncfs::sync::batch::calculate_sync_payload,
exit_app,
cli_publish_player_snapshot,
cli_publish_library_list,
cli_publish_server_list,
cli_publish_search_results,
set_window_decorations,
set_linux_webkit_smooth_scrolling,
linux_wayland_gpu_font_tuning_active,
linux_wayland_text_render_settings_available,
set_linux_wayland_text_render_profile,
set_logging_mode,
get_logging_mode,
tail_runtime_logs,
export_runtime_logs,
frontend_debug_log,
performance_cpu_snapshot,
set_subsonic_wire_user_agent,
no_compositing_mode,
linux_xdg_session_type,
is_tiling_wm_cmd,
open_mini_player,
preload_mini_player,
close_mini_player,
set_mini_player_always_on_top,
resize_mini_player,
show_main_window,
pause_rendering,
resume_rendering,
register_global_shortcut,
unregister_global_shortcut,
mpris_set_metadata,
mpris_set_playback,
audio::commands::audio_play,
audio::transport_commands::audio_pause,
audio::transport_commands::audio_resume,
audio::transport_commands::audio_stop,
audio::transport_commands::audio_seek,
audio::mix_commands::audio_set_volume,
audio::mix_commands::audio_update_replay_gain,
audio::mix_commands::audio_set_eq,
audio::mix_commands::audio_set_playback_rate,
audio::autoeq_commands::autoeq_entries,
audio::autoeq_commands::autoeq_fetch_profile,
audio::preload_commands::audio_preload,
audio::radio_commands::audio_play_radio,
audio::preview::audio_preview_play,
audio::preview::audio_preview_stop,
audio::preview::audio_preview_stop_silent,
audio::preview::audio_preview_set_volume,
audio::mix_commands::audio_set_crossfade,
audio::mix_commands::audio_set_gapless,
audio::mix_commands::audio_set_normalization,
audio::device_commands::audio_list_devices,
audio::device_commands::audio_canonicalize_selected_device,
audio::device_commands::audio_default_output_device_name,
audio::device_commands::audio_set_device,
audio::commands::audio_chain_preload,
psysonic_integration::discord::discord_update_presence,
psysonic_integration::discord::discord_clear_presence,
psysonic_integration::remote::lastfm_request,
psysonic_integration::navidrome::covers::upload_playlist_cover,
psysonic_integration::navidrome::covers::upload_radio_cover,
psysonic_integration::navidrome::covers::upload_artist_image,
psysonic_integration::navidrome::covers::delete_radio_cover,
psysonic_integration::navidrome::users::navidrome_login,
psysonic_integration::navidrome::users::nd_list_users,
psysonic_integration::navidrome::users::nd_create_user,
psysonic_integration::navidrome::users::nd_update_user,
psysonic_integration::navidrome::users::nd_delete_user,
psysonic_integration::navidrome::queries::nd_list_libraries,
psysonic_integration::navidrome::queries::nd_list_songs,
psysonic_integration::navidrome::queries::nd_list_artists_by_role,
psysonic_integration::navidrome::queries::nd_list_albums_by_artist_role,
psysonic_integration::navidrome::queries::nd_set_user_libraries,
psysonic_integration::navidrome::playlists::nd_list_playlists,
psysonic_integration::navidrome::playlists::nd_create_playlist,
psysonic_integration::navidrome::playlists::nd_update_playlist,
psysonic_integration::navidrome::playlists::nd_get_playlist,
psysonic_integration::navidrome::playlists::nd_delete_playlist,
psysonic_integration::navidrome::queries::nd_get_song_path,
psysonic_integration::remote::search_radio_browser,
psysonic_integration::remote::get_top_radio_stations,
psysonic_integration::remote::fetch_url_bytes,
psysonic_integration::remote::fetch_json_url,
psysonic_integration::remote::fetch_icy_metadata,
psysonic_integration::remote::resolve_stream_url,
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_waveform_for_track,
psysonic_analysis::commands::analysis_delete_all_waveforms,
psysonic_analysis::commands::analysis_delete_all_for_server,
psysonic_analysis::commands::analysis_get_failed_track_count,
psysonic_analysis::commands::analysis_list_failed_tracks,
psysonic_analysis::commands::analysis_clear_failed_tracks,
psysonic_analysis::commands::analysis_migrate_server_index_keys,
psysonic_analysis::commands::analysis_enqueue_seed_from_url,
psysonic_analysis::commands::analysis_set_playback_priority_hints,
psysonic_analysis::commands::analysis_set_pipeline_parallelism,
psysonic_analysis::commands::analysis_get_pipeline_queue_stats,
psysonic_analysis::commands::analysis_get_backfill_queue_stats,
psysonic_analysis::commands::analysis_prune_pending_to_track_ids,
psysonic_library::commands::library_get_status,
psysonic_library::commands::library_search,
psysonic_library::commands::library_live_search,
psysonic_library::commands::library_advanced_search,
psysonic_library::commands::library_list_lossless_albums,
psysonic_library::commands::library_list_albums_by_genre,
psysonic_library::commands::library_get_artist_lossless_browse,
psysonic_library::commands::library_search_cross_server,
psysonic_library::commands::library_get_track,
psysonic_library::commands::library_get_tracks_batch,
psysonic_library::commands::library_get_tracks_by_album,
psysonic_library::commands::library_upsert_songs_from_api,
psysonic_library::commands::library_get_artifact,
psysonic_library::commands::library_get_facts,
psysonic_library::commands::library_get_offline_path,
psysonic_library::commands::library_analysis_progress,
psysonic_library::commands::library_count_live_tracks,
psysonic_library::commands::library_sync_bind_session,
psysonic_library::commands::library_sync_clear_session,
psysonic_library::commands::library_set_playback_hint,
psysonic_library::commands::library_get_playback_hint,
psysonic_library::commands::library_sync_start,
psysonic_library::commands::library_sync_verify_integrity,
psysonic_library::commands::library_sync_cancel,
psysonic_library::commands::library_patch_track,
psysonic_library::browse_support::library_reconcile_album_stars,
psysonic_library::browse_support::library_get_catalog_year_bounds,
psysonic_library::browse_support::library_get_genre_album_counts,
psysonic_library::commands::library_put_artifact,
psysonic_library::commands::library_put_fact,
psysonic_library::commands::library_record_play_session,
psysonic_library::commands::library_get_player_stats_year_summary,
psysonic_library::commands::library_get_player_stats_heatmap,
psysonic_library::commands::library_get_player_stats_day_detail,
psysonic_library::commands::library_get_player_stats_year_bounds,
psysonic_library::commands::library_get_player_stats_recent_days,
psysonic_library::commands::library_purge_server,
psysonic_library::commands::library_migrate_server_index_keys,
psysonic_library::commands::library_delete_server_data,
psysonic_library::commands::library_analysis_backfill_batch,
library_analysis_backfill::library_analysis_backfill_configure,
psysonic_library::commands::library_resolve_cover_entry,
cover_cache::cover_cache_peek_batch,
cover_cache::cover_cache_ensure,
cover_cache::cover_cache_ensure_batch,
cover_cache::cover_cache_stats,
cover_cache::cover_cache_evict_tick,
cover_cache::cover_cache_configure,
cover_cache::cover_cache_clear,
cover_cache::cover_cache_clear_server,
cover_cache::cover_cache_rename_server_bucket,
cover_cache::cover_cache_stats_server,
cover_cache::cover_cache_get_pipeline_queue_stats,
cover_cache::library_cover_backfill_batch,
cover_cache::library_cover_progress,
cover_cache::library_cover_catalog_size,
cover_cache::library_cover_clear_fetch_failures,
cover_cache::library_cover_backfill_configure,
cover_cache::library_cover_backfill_set_base_url,
cover_cache::library_cover_backfill_pulse,
cover_cache::library_cover_backfill_reset_cursor,
cover_cache::library_cover_backfill_set_ui_priority,
cover_cache::library_cover_backfill_set_parallel,
cover_cache::library_cover_backfill_run_full_pass,
cover_cache::cover_revalidate_enqueue,
cover_cache::cover_revalidate_tick,
cover_cache::cover_revalidate_batch,
psysonic_syncfs::cache::offline::download_track_offline,
psysonic_syncfs::cache::offline::cancel_offline_downloads,
psysonic_syncfs::cache::offline::clear_offline_cancel,
psysonic_syncfs::cache::offline::delete_offline_track,
psysonic_syncfs::cache::offline::get_offline_cache_size,
psysonic_syncfs::cache::local::download_track_local,
psysonic_syncfs::cache::local::probe_library_track_local,
psysonic_syncfs::cache::local::discover_library_tier_on_disk,
psysonic_syncfs::cache::local::prune_orphan_library_tier_files,
psysonic_syncfs::cache::local::prune_orphan_ephemeral_cache_files,
psysonic_syncfs::cache::local::evict_ephemeral_cache_orphans_to_fit,
psysonic_syncfs::cache::local::probe_media_files,
psysonic_syncfs::cache::local::get_media_tier_size,
psysonic_syncfs::cache::local::purge_media_tier,
psysonic_syncfs::cache::local::delete_media_file,
psysonic_syncfs::cache::local::prune_empty_media_tier_dirs,
psysonic_syncfs::cache::local::promote_stream_cache_to_local,
psysonic_syncfs::cache::local::migrate_legacy_offline_disk,
psysonic_syncfs::cache::hot::download_track_hot_cache,
psysonic_syncfs::cache::hot::promote_stream_cache_to_hot_cache,
psysonic_syncfs::cache::hot::get_hot_cache_size,
psysonic_syncfs::cache::hot::delete_hot_cache_track,
psysonic_syncfs::cache::hot::purge_hot_cache,
psysonic_syncfs::sync::device::sync_track_to_device,
psysonic_syncfs::sync::batch::sync_batch_to_device,
psysonic_syncfs::sync::batch::cancel_device_sync,
psysonic_syncfs::sync::device::compute_sync_paths,
psysonic_syncfs::sync::batch::list_device_dir_files,
psysonic_syncfs::sync::batch::delete_device_file,
psysonic_syncfs::sync::batch::delete_device_files,
psysonic_syncfs::sync::device::get_removable_drives,
psysonic_syncfs::sync::device::write_device_manifest,
psysonic_syncfs::sync::device::read_device_manifest,
psysonic_syncfs::sync::device::write_playlist_m3u8,
psysonic_syncfs::sync::device::rename_device_files,
toggle_tray_icon,
set_tray_tooltip,
set_tray_menu_labels,
check_dir_accessible,
psysonic_syncfs::cache::downloads::download_zip,
psysonic_syncfs::cache::downloads::check_arch_linux,
psysonic_syncfs::cache::downloads::download_update,
psysonic_syncfs::cache::downloads::open_folder,
psysonic_syncfs::cache::downloads::get_embedded_lyrics,
psysonic_syncfs::cache::downloads::fetch_netease_lyrics,
psysonic_integration::bandsintown::fetch_bandsintown_events,
#[cfg(target_os = "windows")]
taskbar_win::update_taskbar_icon,
])
.run(tauri::generate_context!())
.expect("error while running Psysonic");
}