Files
Psychotoxical-psysonic/src-tauri/src/lib.rs
T
Psychotoxical 6d63365c2a fix(windows): show app name in media controls via AppUserModelID (#1102)
The Windows system media controls (Quick Settings media tile, lock screen,
third-party media flyouts) labelled playback as "Unknown application" with no
icon, because souvlaki creates the SMTC via GetForWindow and Windows resolves
the source name from the process AppUserModelID, which was never set.

Call SetCurrentProcessExplicitAppUserModelID early in run() so the process has
an explicit identity that matches the installer shortcut's AppUserModelID;
Windows then resolves the name and icon to Psysonic.

(cherry picked from commit 4fd85f2dd4)
2026-06-17 01:30:23 +03:00

913 lines
46 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 mod theme_animation;
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();
}
}
/// Windows: associate this process with an explicit AppUserModelID. Windows uses
/// it to name the app in taskbar grouping and the SMTC media controls; without it
/// the media tile reads "Unknown application". Must match the AppUserModelID the
/// installer sets on the Start-menu shortcut so the name/icon resolve.
#[cfg(target_os = "windows")]
fn set_app_user_model_id() {
use windows::core::w;
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
// SAFETY: a Win32 call with a static wide string; errors are non-fatal.
unsafe {
let _ = SetCurrentProcessExplicitAppUserModelID(w!("dev.psysonic.player"));
}
}
pub fn run() {
// Windows: bind this process to an explicit AppUserModelID before any window
// or the SMTC media controls are created, so the OS can resolve the app
// name/icon for taskbar grouping and the media tile (#1102 follow-up: the
// Quick-Settings / lock-screen media tile showed "Unknown application").
#[cfg(target_os = "windows")]
set_app_user_model_id();
// 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_state_flags(
tauri_plugin_window_state::StateFlags::all()
& !tauri_plugin_window_state::StateFlags::VISIBLE,
)
.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)");
}
// ── Dev: `--theme-watch <theme.css>` live theme reload ─────────
// Poll a local theme.css and push it into the running app on save,
// so theme authors get a live loop without re-importing a zip. The
// frontend (dev only) installs it under the id in its
// `[data-theme='<id>']` selector and applies it. Dev-builds only.
#[cfg(debug_assertions)]
{
let args: Vec<String> = std::env::args().collect();
if let Some(i) = args.iter().position(|a| a == "--theme-watch") {
match args.get(i + 1).cloned() {
Some(path) => {
eprintln!("[theme-watch] watching {path}");
let handle = app.handle().clone();
std::thread::spawn(move || {
let p = std::path::PathBuf::from(&path);
let mut last_css = String::new();
loop {
if let Ok(css) = std::fs::read_to_string(&p) {
if css != last_css {
last_css = css.clone();
let _ = handle.emit("theme-watch:css", css);
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
});
}
None => eprintln!("[theme-watch] usage: --theme-watch <path/to/theme.css>"),
}
}
}
// ── 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);
// Suppress WebKit's own MPRIS player so radio (HTML <audio>)
// doesn't duplicate the souvlaki one (issue #1048).
let _ = linux_webkit_disable_media_session(&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 {
// Keep Play/Pause distinct from Toggle: the OS
// (notably macOS on audio-route changes, e.g. a
// headphone disconnect) sends an explicit Pause,
// and collapsing all three into a toggle would
// resume paused playback on the new device (#1094).
MediaControlEvent::Toggle => {
let _ = app_handle.emit("media:play-pause", ());
}
MediaControlEvent::Play => {
let _ = app_handle.emit("media:play", ());
}
MediaControlEvent::Pause => {
let _ = app_handle.emit("media: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());
audio::start_stream_idle_watcher(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();
// All platforms: pause rendering, then let JS decide hide-to-tray
// vs exit based on the minimizeToTray setting. macOS previously
// always force-quit on the red close button, ignoring the setting
// (#1103). The tray "Exit" item still emits app:force-quit for an
// unconditional quit.
if let Some(w) = window.app_handle().get_webview_window("main") {
let _ = w.eval(PAUSE_RENDERING_JS);
}
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();
}
}
}
})
.on_page_load(|webview, payload| {
if webview.label() != "main" {
return;
}
match payload.event() {
tauri::webview::PageLoadEvent::Started => {
let window = webview.window().clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(48));
let _ = window.show();
});
}
tauri::webview::PageLoadEvent::Finished => {
let _ = webview.window().show();
}
}
})
.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,
theme_animation_risk,
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::audioscrobbler_request,
psysonic_integration::remote::listenbrainz_request,
psysonic_integration::remote::maloja_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_genre_tags_inspect,
psysonic_library::commands::library_genre_tags_run,
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");
}