// 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>; /// 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>; /// Release builds only: focus or CLI-hand off when a second instance is launched. #[cfg(not(debug_assertions))] fn on_second_instance( app: &tauri::AppHandle, argv: Vec, _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 = 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::() 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::() .is_some_and(|e| crate::audio::analysis_track_id_is_current_playback(&e, track_id)) }, move |track_id| { app_defer .try_state::() .is_some_and(|e| crate::audio::ranged_loudness_backfill_should_defer(&e, track_id)) }, ); app.manage(handle); } 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::() { 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::() 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::() 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::() 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::().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 = (|| { // 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::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"); }