refactor(ipc): complete tauri-specta typed-IPC cutover, contract guards, and layering cleanup (#1230)

This commit is contained in:
Psychotoxical
2026-07-04 16:00:31 +02:00
committed by GitHub
parent fdbb9deac6
commit 77ab95170d
199 changed files with 3691 additions and 1252 deletions
+7 -1
View File
@@ -26,6 +26,7 @@ pub fn resolve_hot_cache_root(
/// Returns true if the current Linux system is Arch-based
/// (checks /etc/arch-release and /etc/os-release).
#[tauri::command]
#[specta::specta]
pub fn check_arch_linux() -> bool {
#[cfg(target_os = "linux")]
{
@@ -57,6 +58,7 @@ pub struct UpdateDownloadProgress {
/// Emits `update:download:progress` events with `{ bytes, total }` every 250 ms.
/// Returns the final absolute file path on success.
#[tauri::command]
#[specta::specta]
pub async fn download_update(url: String, filename: String, app: tauri::AppHandle) -> Result<String, String> {
use futures_util::StreamExt;
use std::time::{Duration, Instant};
@@ -130,6 +132,7 @@ pub async fn download_update(url: String, filename: String, app: tauri::AppHandl
/// Performs a track search, then fetches the LRC string for the best match.
/// Returns `None` if no match or no lyrics are found.
#[tauri::command]
#[specta::specta]
pub async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> {
let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent())
@@ -183,6 +186,7 @@ pub async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Optio
/// Errors are silenced and mapped to `None` so the frontend falls through to the
/// next lyrics source without crashing.
#[tauri::command]
#[specta::specta]
pub fn get_embedded_lyrics(path: String) -> Option<String> {
use lofty::file::FileType;
use lofty::prelude::*;
@@ -282,6 +286,7 @@ pub fn get_embedded_lyrics(path: String) -> Option<String> {
/// Uses platform-specific process spawning — tauri-plugin-shell's open() only
/// allows https:// URLs per the capability scope and fails silently for paths.
#[tauri::command]
#[specta::specta]
pub fn open_folder(path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
@@ -323,6 +328,7 @@ pub struct ZipProgress {
/// live MB-counter without holding any binary data in the WebView process.
/// Returns the final destination path on success.
#[tauri::command]
#[specta::specta]
pub async fn download_zip(
id: String,
url: String,
@@ -409,7 +415,7 @@ pub async fn download_zip(
}
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct HotCacheDownloadResult {
pub path: String,
+5
View File
@@ -10,6 +10,7 @@ use super::downloads::{resolve_hot_cache_root, HotCacheDownloadResult};
use super::offline::enqueue_analysis_seed_from_file;
#[tauri::command]
#[specta::specta]
pub async fn download_track_hot_cache(
track_id: String,
server_id: String,
@@ -129,6 +130,7 @@ pub async fn download_track_hot_cache(
/// Promotes bytes captured by the manual streaming path into hot cache on disk.
/// Returns `Ok(None)` when no completed stream cache is available for this URL.
#[tauri::command]
#[specta::specta]
pub async fn promote_stream_cache_to_hot_cache(
track_id: String,
server_id: String,
@@ -244,6 +246,7 @@ pub async fn promote_stream_cache_to_hot_cache(
}
#[tauri::command]
#[specta::specta]
pub async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
resolve_hot_cache_root(custom_dir, &app)
.map(|root| super::fs_utils::dir_size_recursive(&root))
@@ -251,6 +254,7 @@ pub async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandl
}
#[tauri::command]
#[specta::specta]
pub async fn delete_hot_cache_track(
local_path: String,
custom_dir: Option<String>,
@@ -282,6 +286,7 @@ pub async fn delete_hot_cache_track(
/// Removes the entire hot cache root (`psysonic-hot-cache` for the active location).
#[tauri::command]
#[specta::specta]
pub async fn purge_hot_cache(custom_dir: Option<String>, app: tauri::AppHandle) -> Result<(), String> {
let root = resolve_hot_cache_root(custom_dir, &app)?;
if !root.exists() {
+17 -4
View File
@@ -44,7 +44,7 @@ pub fn resolve_media_dir(custom_media_dir: Option<&str>, app: &AppHandle) -> Res
}
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct LocalTrackDownloadResult {
pub path: String,
@@ -52,7 +52,7 @@ pub struct LocalTrackDownloadResult {
pub layout_fingerprint: String,
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct LibraryTrackProbeResult {
pub path: String,
@@ -61,7 +61,7 @@ pub struct LibraryTrackProbeResult {
pub exists: bool,
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct LibraryTierDiskHit {
pub track_id: String,
@@ -230,6 +230,7 @@ async fn local_track_hit_if_exists(
/// `TRACK_NOT_INDEXED` when the row is missing. Disk scope uses `server_index_key`;
/// SQL lookup uses `library_server_id`.
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub async fn download_track_local(
tier: String,
@@ -409,6 +410,7 @@ pub async fn download_track_local(
/// Scan library-tier bytes on disk and match them to known candidates only
/// (`track_offline.local_path` + canonical paths for `candidate_track_ids`).
#[tauri::command]
#[specta::specta]
pub async fn discover_library_tier_on_disk(
server_index_key: String,
library_server_id: String,
@@ -517,6 +519,7 @@ pub async fn discover_library_tier_on_disk(
/// Resolve the canonical `library/` path for a track and report on-disk presence only
/// (no download, no analysis seed).
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub async fn probe_library_track_local(
track_id: String,
@@ -580,6 +583,7 @@ async fn prune_orphan_files_under_root(root: &Path, keep_paths: &[String]) -> Ve
/// Remove library-tier files under `{server_index_key}` that are not listed in `keep_paths`.
#[tauri::command]
#[specta::specta]
pub async fn prune_orphan_library_tier_files(
server_index_key: String,
keep_paths: Vec<String>,
@@ -654,6 +658,7 @@ async fn evict_orphan_files_under_root_to_fit(
/// Evict unindexed ephemeral cache files (oldest first) until tier size ≤ `max_bytes`.
#[tauri::command]
#[specta::specta]
pub async fn evict_ephemeral_cache_orphans_to_fit(
keep_paths: Vec<String>,
max_bytes: u64,
@@ -667,6 +672,7 @@ pub async fn evict_ephemeral_cache_orphans_to_fit(
/// Remove ephemeral-tier files under `{media}/cache/` not listed in `keep_paths`.
#[tauri::command]
#[specta::specta]
pub async fn prune_orphan_ephemeral_cache_files(
keep_paths: Vec<String>,
media_dir: Option<String>,
@@ -679,6 +685,7 @@ pub async fn prune_orphan_ephemeral_cache_files(
/// Batch existence probe for reconcile (index rows without on-disk bytes).
#[tauri::command]
#[specta::specta]
pub fn probe_media_files(local_paths: Vec<String>) -> Vec<bool> {
local_paths
.iter()
@@ -696,6 +703,7 @@ fn resolve_media_tier_root(
/// Recursive byte size under `{media}/{cache|library}/`.
#[tauri::command]
#[specta::specta]
pub async fn get_media_tier_size(
tier: String,
media_dir: Option<String>,
@@ -712,6 +720,7 @@ pub async fn get_media_tier_size(
/// Deletes the entire `{cache|library}/` subtree under the media root.
#[tauri::command]
#[specta::specta]
pub async fn purge_media_tier(
tier: String,
media_dir: Option<String>,
@@ -749,6 +758,7 @@ fn prune_parents_after_media_file_delete(
/// Deletes one media file and prunes empty parents up to the tier root.
#[tauri::command]
#[specta::specta]
pub async fn delete_media_file(
local_path: String,
media_dir: Option<String>,
@@ -766,6 +776,7 @@ pub async fn delete_media_file(
/// Removes empty directories under `{media}/{cache|library}/` (post-eviction sweep).
#[tauri::command]
#[specta::specta]
pub async fn prune_empty_media_tier_dirs(
tier: String,
media_dir: Option<String>,
@@ -780,6 +791,7 @@ pub async fn prune_empty_media_tier_dirs(
/// Promotes stream-cache bytes into `{media}/cache/…` using library-index paths.
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)]
pub async fn promote_stream_cache_to_local(
track_id: String,
@@ -905,7 +917,7 @@ pub async fn promote_stream_cache_to_local(
}))
}
#[derive(Debug, Clone, serde::Serialize)]
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct LegacyOfflineMigrationResult {
pub track_id: String,
@@ -1228,6 +1240,7 @@ async fn relocate_legacy_track_file(
/// Scan `psysonic-offline/{segment}/{trackId}.ext`, verify each id in the library
/// index, and relocate live tracks into `{media}/library/…`.
#[tauri::command]
#[specta::specta]
pub async fn migrate_legacy_offline_disk(
media_dir: Option<String>,
custom_offline_dir: Option<String>,
+5
View File
@@ -88,6 +88,7 @@ pub(crate) fn resolve_offline_cache_dir(
/// Returns the absolute file path so TypeScript can store it and later
/// construct a `psysonic-local://<path>` URL for the audio engine.
#[tauri::command]
#[specta::specta]
#[allow(clippy::too_many_arguments)] // Tauri command surface — args map 1:1 to the JS call.
pub async fn download_track_offline(
track_id: String,
@@ -160,6 +161,7 @@ pub async fn download_track_offline(
/// boundary; ones still parked on the download semaphore bail as soon as they
/// acquire a slot. Mirrors `cancel_device_sync` for the device-sync side.
#[tauri::command]
#[specta::specta]
pub fn cancel_offline_downloads(download_ids: Vec<String>) {
if let Ok(mut flags) = offline_cancel_flags().lock() {
for id in download_ids {
@@ -175,6 +177,7 @@ pub fn cancel_offline_downloads(download_ids: Vec<String>) {
/// across a long session. The frontend calls this once an album/playlist
/// download settles (completed or cancelled).
#[tauri::command]
#[specta::specta]
pub fn clear_offline_cancel(download_id: String) {
if let Ok(mut flags) = offline_cancel_flags().lock() {
flags.remove(&download_id);
@@ -435,6 +438,7 @@ mod tests {
/// Returns the total size in bytes of all files in the offline cache directory (and optional custom dir).
#[tauri::command]
#[specta::specta]
pub async fn get_offline_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
let default_dir = match app.path().app_data_dir() {
Ok(d) => d.join("psysonic-offline"),
@@ -474,6 +478,7 @@ pub(crate) async fn delete_offline_track_with_boundary(
/// After deleting the file, empty parent directories up to (but not including)
/// `base_dir` are pruned using `remove_dir` (never `remove_dir_all`).
#[tauri::command]
#[specta::specta]
pub async fn delete_offline_track(
local_path: String,
base_dir: Option<String>,