refactor: extract psysonic-syncfs crate (M4/7)

Moves all on-disk cache + device-sync code out of the top crate:

  crates/psysonic-syncfs/                      new lib crate
    src/cache/{offline,hot,downloads,fs_utils} unchanged behaviour
    src/sync/{batch,device}                    (no tray.rs — that's UI)
    src/file_transfer.rs                       shared HTTP helpers
    src/lib.rs                                 + DownloadSemaphore type
                                               + sync_cancel_flags fn

The shell crate keeps `lib_commands/sync/tray.rs` (OS tray icon — UI
concern, will move to shell-tauri at M7) but drops the rest of
`lib_commands/cache/` and the syncfs sibling files.

Tauri command quirk surfaced and resolved: `#[tauri::command]` puts its
`__cmd__*` and `__tauri_command_name_*` helper macros at the *exact*
module of the function, and `pub use` doesn't carry them across module
boundaries. invoke_handler! in lib.rs now references each syncfs
command via its full deepest path
(`psysonic_syncfs::cache::offline::download_track_offline`, etc.) so
Tauri's macros resolve at the right scope. Same approach the audio
crate already uses (`audio::commands::audio_play`).

Cross-crate ref migrations applied via batch sed:
  crate::audio::*               → psysonic_audio::*
  crate::analysis_runtime::*    → psysonic_analysis::analysis_runtime::*
  crate::analysis_cache::*      → psysonic_analysis::analysis_cache::*
  crate::subsonic_wire_user_agent → psysonic_core::user_agent::*
  super::super::file_transfer:: → crate::file_transfer::

Behaviour preserving. Cargo check + clippy --workspace clean.
This commit is contained in:
Psychotoxical
2026-05-09 13:45:53 +02:00
parent 41e75663f1
commit 9417d522f3
17 changed files with 211 additions and 158 deletions
+23
View File
@@ -3611,6 +3611,7 @@ dependencies = [
"psysonic-analysis", "psysonic-analysis",
"psysonic-audio", "psysonic-audio",
"psysonic-core", "psysonic-core",
"psysonic-syncfs",
"reqwest", "reqwest",
"ringbuf", "ringbuf",
"rodio", "rodio",
@@ -3694,6 +3695,28 @@ dependencies = [
"tauri", "tauri",
] ]
[[package]]
name = "psysonic-syncfs"
version = "1.46.0-dev"
dependencies = [
"futures-util",
"id3",
"lofty",
"md5",
"psysonic-analysis",
"psysonic-audio",
"psysonic-core",
"reqwest",
"serde",
"serde_json",
"sysinfo",
"tauri",
"tauri-plugin-process",
"tauri-plugin-shell",
"tokio",
"url",
]
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.29" version = "0.1.29"
+1
View File
@@ -33,6 +33,7 @@ tauri-build = { version = "2", features = [] }
psysonic-core = { path = "crates/psysonic-core" } psysonic-core = { path = "crates/psysonic-core" }
psysonic-analysis = { path = "crates/psysonic-analysis" } psysonic-analysis = { path = "crates/psysonic-analysis" }
psysonic-audio = { path = "crates/psysonic-audio" } psysonic-audio = { path = "crates/psysonic-audio" }
psysonic-syncfs = { path = "crates/psysonic-syncfs" }
tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-single-instance = "2" tauri-plugin-single-instance = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
@@ -0,0 +1,25 @@
[package]
name = "psysonic-syncfs"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = false
[dependencies]
psysonic-core = { path = "../psysonic-core" }
psysonic-analysis = { path = "../psysonic-analysis" }
psysonic-audio = { path = "../psysonic-audio" }
tauri = { version = "2" }
tauri-plugin-shell = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt", "time", "sync"] }
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "rustls", "blocking"] }
futures-util = "0.3"
sysinfo = { version = "0.38", default-features = false, features = ["disk"] }
url = "2"
md5 = "0.8"
lofty = "0.24"
id3 = "1.16.4"
@@ -1,8 +1,8 @@
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
use crate::subsonic_wire_user_agent; use psysonic_core::user_agent::subsonic_wire_user_agent;
pub(crate) fn resolve_hot_cache_root( pub fn resolve_hot_cache_root(
custom_dir: Option<String>, custom_dir: Option<String>,
app: &tauri::AppHandle, app: &tauri::AppHandle,
) -> Result<std::path::PathBuf, String> { ) -> Result<std::path::PathBuf, String> {
@@ -24,7 +24,7 @@ pub(crate) fn resolve_hot_cache_root(
/// Returns true if the current Linux system is Arch-based /// Returns true if the current Linux system is Arch-based
/// (checks /etc/arch-release and /etc/os-release). /// (checks /etc/arch-release and /etc/os-release).
#[tauri::command] #[tauri::command]
pub(crate) fn check_arch_linux() -> bool { pub fn check_arch_linux() -> bool {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
if std::path::Path::new("/etc/arch-release").exists() { if std::path::Path::new("/etc/arch-release").exists() {
@@ -46,7 +46,7 @@ pub(crate) fn check_arch_linux() -> bool {
/// Progress payload emitted during an update binary download. /// Progress payload emitted during an update binary download.
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct UpdateDownloadProgress { pub struct UpdateDownloadProgress {
bytes: u64, bytes: u64,
total: Option<u64>, total: Option<u64>,
} }
@@ -55,7 +55,7 @@ pub(crate) struct UpdateDownloadProgress {
/// Emits `update:download:progress` events with `{ bytes, total }` every 250 ms. /// Emits `update:download:progress` events with `{ bytes, total }` every 250 ms.
/// Returns the final absolute file path on success. /// Returns the final absolute file path on success.
#[tauri::command] #[tauri::command]
pub(crate) async fn download_update(url: String, filename: String, app: tauri::AppHandle) -> Result<String, String> { pub async fn download_update(url: String, filename: String, app: tauri::AppHandle) -> Result<String, String> {
use futures_util::StreamExt; use futures_util::StreamExt;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
@@ -128,7 +128,7 @@ pub(crate) async fn download_update(url: String, filename: String, app: tauri::A
/// Performs a track search, then fetches the LRC string for the best match. /// Performs a track search, then fetches the LRC string for the best match.
/// Returns `None` if no match or no lyrics are found. /// Returns `None` if no match or no lyrics are found.
#[tauri::command] #[tauri::command]
pub(crate) async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> { pub async fn fetch_netease_lyrics(artist: String, title: String) -> Result<Option<String>, String> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(subsonic_wire_user_agent()) .user_agent(subsonic_wire_user_agent())
.timeout(std::time::Duration::from_secs(8)) .timeout(std::time::Duration::from_secs(8))
@@ -181,7 +181,7 @@ pub(crate) async fn fetch_netease_lyrics(artist: String, title: String) -> Resul
/// Errors are silenced and mapped to `None` so the frontend falls through to the /// Errors are silenced and mapped to `None` so the frontend falls through to the
/// next lyrics source without crashing. /// next lyrics source without crashing.
#[tauri::command] #[tauri::command]
pub(crate) fn get_embedded_lyrics(path: String) -> Option<String> { pub fn get_embedded_lyrics(path: String) -> Option<String> {
use lofty::file::FileType; use lofty::file::FileType;
use lofty::prelude::*; use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
@@ -198,7 +198,7 @@ pub(crate) fn get_embedded_lyrics(path: String) -> Option<String> {
let file_type = probe.file_type(); let file_type = probe.file_type();
// ── MP3 / MPEG: use the `id3` crate for SYLT / USLT ───────────────────── // ── MP3 / MPEG: use the `id3` crate for SYLT / USLT ─────────────────────
// lofty's MpegFile::id3v2_tag field is pub(crate) — not accessible here. // lofty's MpegFile::id3v2_tag field is pub — not accessible here.
// The `id3` crate exposes a clean public API for typed ID3v2 frames. // The `id3` crate exposes a clean public API for typed ID3v2 frames.
if matches!(file_type, Some(FileType::Mpeg)) { if matches!(file_type, Some(FileType::Mpeg)) {
use id3::{Content, Tag as Id3Tag}; use id3::{Content, Tag as Id3Tag};
@@ -280,7 +280,7 @@ pub(crate) fn get_embedded_lyrics(path: String) -> Option<String> {
/// Uses platform-specific process spawning — tauri-plugin-shell's open() only /// Uses platform-specific process spawning — tauri-plugin-shell's open() only
/// allows https:// URLs per the capability scope and fails silently for paths. /// allows https:// URLs per the capability scope and fails silently for paths.
#[tauri::command] #[tauri::command]
pub(crate) fn open_folder(path: String) -> Result<(), String> { pub fn open_folder(path: String) -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
std::process::Command::new("explorer.exe") std::process::Command::new("explorer.exe")
@@ -310,7 +310,7 @@ pub(crate) fn open_folder(path: String) -> Result<(), String> {
/// (Navidrome on-the-fly ZIPs). /// (Navidrome on-the-fly ZIPs).
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct ZipProgress { pub struct ZipProgress {
id: String, id: String,
bytes: u64, bytes: u64,
total: Option<u64>, total: Option<u64>,
@@ -321,7 +321,7 @@ pub(crate) struct ZipProgress {
/// live MB-counter without holding any binary data in the WebView process. /// live MB-counter without holding any binary data in the WebView process.
/// Returns the final destination path on success. /// Returns the final destination path on success.
#[tauri::command] #[tauri::command]
pub(crate) async fn download_zip( pub async fn download_zip(
id: String, id: String,
url: String, url: String,
dest_path: String, dest_path: String,
@@ -398,7 +398,7 @@ pub(crate) async fn download_zip(
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct HotCacheDownloadResult { pub struct HotCacheDownloadResult {
pub(crate) path: String, pub path: String,
pub(crate) size: u64, pub size: u64,
} }
@@ -2,7 +2,7 @@ use std::path::Path;
/// Recursively sums the size of all files under `root`. /// Recursively sums the size of all files under `root`.
/// Missing roots, unreadable directories, and unreadable files are silently skipped. /// Missing roots, unreadable directories, and unreadable files are silently skipped.
pub(crate) fn dir_size_recursive(root: &Path) -> u64 { pub fn dir_size_recursive(root: &Path) -> u64 {
if !root.exists() { if !root.exists() {
return 0; return 0;
} }
@@ -31,7 +31,7 @@ pub(crate) fn dir_size_recursive(root: &Path) -> u64 {
/// ///
/// `boundary` is never removed and is treated as a hard stop. If `start_dir` is /// `boundary` is never removed and is treated as a hard stop. If `start_dir` is
/// not under `boundary`, the function is a no-op. /// not under `boundary`, the function is a no-op.
pub(crate) fn prune_empty_dirs_up_to(start_dir: &Path, boundary: &Path) { pub fn prune_empty_dirs_up_to(start_dir: &Path, boundary: &Path) {
let mut current = Some(start_dir.to_path_buf()); let mut current = Some(start_dir.to_path_buf());
while let Some(dir) = current { while let Some(dir) = current {
if dir == boundary || !dir.starts_with(boundary) { if dir == boundary || !dir.starts_with(boundary) {
@@ -1,13 +1,13 @@
use crate::analysis_runtime::enqueue_analysis_seed; use psysonic_analysis::analysis_runtime::enqueue_analysis_seed;
use crate::audio; use psysonic_audio as audio;
use crate::subsonic_wire_user_agent; use psysonic_core::user_agent::subsonic_wire_user_agent;
use super::super::file_transfer::stream_to_file; use crate::file_transfer::stream_to_file;
use super::downloads::{resolve_hot_cache_root, HotCacheDownloadResult}; use super::downloads::{resolve_hot_cache_root, HotCacheDownloadResult};
use super::offline::enqueue_analysis_seed_from_file; use super::offline::enqueue_analysis_seed_from_file;
#[tauri::command] #[tauri::command]
pub(crate) async fn download_track_hot_cache( pub async fn download_track_hot_cache(
track_id: String, track_id: String,
server_id: String, server_id: String,
url: String, url: String,
@@ -103,7 +103,7 @@ pub(crate) async fn download_track_hot_cache(
/// Promotes bytes captured by the manual streaming path into hot cache on disk. /// 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. /// Returns `Ok(None)` when no completed stream cache is available for this URL.
#[tauri::command] #[tauri::command]
pub(crate) async fn promote_stream_cache_to_hot_cache( pub async fn promote_stream_cache_to_hot_cache(
track_id: String, track_id: String,
server_id: String, server_id: String,
url: String, url: String,
@@ -177,14 +177,14 @@ pub(crate) async fn promote_stream_cache_to_hot_cache(
} }
#[tauri::command] #[tauri::command]
pub(crate) async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 { pub async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
resolve_hot_cache_root(custom_dir, &app) resolve_hot_cache_root(custom_dir, &app)
.map(|root| super::fs_utils::dir_size_recursive(&root)) .map(|root| super::fs_utils::dir_size_recursive(&root))
.unwrap_or(0) .unwrap_or(0)
} }
#[tauri::command] #[tauri::command]
pub(crate) async fn delete_hot_cache_track( pub async fn delete_hot_cache_track(
local_path: String, local_path: String,
custom_dir: Option<String>, custom_dir: Option<String>,
app: tauri::AppHandle, app: tauri::AppHandle,
@@ -215,7 +215,7 @@ pub(crate) async fn delete_hot_cache_track(
/// Removes the entire hot cache root (`psysonic-hot-cache` for the active location). /// Removes the entire hot cache root (`psysonic-hot-cache` for the active location).
#[tauri::command] #[tauri::command]
pub(crate) async fn purge_hot_cache(custom_dir: Option<String>, app: tauri::AppHandle) -> Result<(), String> { pub async fn purge_hot_cache(custom_dir: Option<String>, app: tauri::AppHandle) -> Result<(), String> {
let root = resolve_hot_cache_root(custom_dir, &app)?; let root = resolve_hot_cache_root(custom_dir, &app)?;
if !root.exists() { if !root.exists() {
return Ok(()); return Ok(());
+4
View File
@@ -0,0 +1,4 @@
mod fs_utils;
pub mod offline;
pub mod downloads;
pub mod hot;
@@ -1,14 +1,14 @@
use tauri::Manager; use tauri::Manager;
use crate::analysis_cache; use psysonic_analysis::analysis_cache;
use crate::analysis_runtime::enqueue_analysis_seed; use psysonic_analysis::analysis_runtime::enqueue_analysis_seed;
use crate::DownloadSemaphore; use crate::DownloadSemaphore;
use super::super::file_transfer::{finalize_streamed_download, subsonic_http_client}; use crate::file_transfer::{finalize_streamed_download, subsonic_http_client};
// ─── Offline Track Cache ────────────────────────────────────────────────────── // ─── Offline Track Cache ──────────────────────────────────────────────────────
pub(crate) async fn enqueue_analysis_seed_from_file( pub async fn enqueue_analysis_seed_from_file(
app: &tauri::AppHandle, app: &tauri::AppHandle,
track_id: &str, track_id: &str,
file_path: &std::path::Path, file_path: &std::path::Path,
@@ -32,7 +32,7 @@ pub(crate) async fn enqueue_analysis_seed_from_file(
/// Returns the absolute file path so TypeScript can store it and later /// Returns the absolute file path so TypeScript can store it and later
/// construct a `psysonic-local://<path>` URL for the audio engine. /// construct a `psysonic-local://<path>` URL for the audio engine.
#[tauri::command] #[tauri::command]
pub(crate) async fn download_track_offline( pub async fn download_track_offline(
track_id: String, track_id: String,
server_id: String, server_id: String,
url: String, url: String,
@@ -90,7 +90,7 @@ pub(crate) async fn download_track_offline(
/// Returns the total size in bytes of all files in the offline cache directory (and optional custom dir). /// Returns the total size in bytes of all files in the offline cache directory (and optional custom dir).
#[tauri::command] #[tauri::command]
pub(crate) async fn get_offline_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 { pub async fn get_offline_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
let default_dir = match app.path().app_data_dir() { let default_dir = match app.path().app_data_dir() {
Ok(d) => d.join("psysonic-offline"), Ok(d) => d.join("psysonic-offline"),
Err(_) => return 0, Err(_) => return 0,
@@ -111,7 +111,7 @@ pub(crate) async fn get_offline_cache_size(custom_dir: Option<String>, app: taur
/// After deleting the file, empty parent directories up to (but not including) /// After deleting the file, empty parent directories up to (but not including)
/// `base_dir` are pruned using `remove_dir` (never `remove_dir_all`). /// `base_dir` are pruned using `remove_dir` (never `remove_dir_all`).
#[tauri::command] #[tauri::command]
pub(crate) async fn delete_offline_track( pub async fn delete_offline_track(
local_path: String, local_path: String,
base_dir: Option<String>, base_dir: Option<String>,
app: tauri::AppHandle, app: tauri::AppHandle,
@@ -4,9 +4,9 @@ use std::time::Duration;
/// Build a reqwest client with the standard Subsonic UA and a single overall timeout. /// Build a reqwest client with the standard Subsonic UA and a single overall timeout.
/// For flows that need separate connect + read timeouts (long-running update/zip /// For flows that need separate connect + read timeouts (long-running update/zip
/// downloads with progress events), build the client inline. /// downloads with progress events), build the client inline.
pub(crate) fn subsonic_http_client(timeout: Duration) -> Result<reqwest::Client, String> { pub fn subsonic_http_client(timeout: Duration) -> Result<reqwest::Client, String> {
reqwest::Client::builder() reqwest::Client::builder()
.user_agent(crate::subsonic_wire_user_agent()) .user_agent(psysonic_core::user_agent::subsonic_wire_user_agent())
.timeout(timeout) .timeout(timeout)
.build() .build()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@@ -14,7 +14,7 @@ pub(crate) fn subsonic_http_client(timeout: Duration) -> Result<reqwest::Client,
/// Streams an HTTP response body to `dest_path` in chunks. Never buffers the full /// Streams an HTTP response body to `dest_path` in chunks. Never buffers the full
/// file in memory — keeps RAM flat regardless of file size. /// file in memory — keeps RAM flat regardless of file size.
pub(crate) async fn stream_to_file( pub async fn stream_to_file(
response: reqwest::Response, response: reqwest::Response,
dest_path: &Path, dest_path: &Path,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -40,7 +40,7 @@ pub(crate) async fn stream_to_file(
/// Note vs. previous inline implementations: the offline/device single-track /// Note vs. previous inline implementations: the offline/device single-track
/// flows used to leave a `.part` orphan if the final rename failed. This helper /// flows used to leave a `.part` orphan if the final rename failed. This helper
/// always cleans up, matching the batch-sync flow that already did. /// always cleans up, matching the batch-sync flow that already did.
pub(crate) async fn finalize_streamed_download( pub async fn finalize_streamed_download(
response: reqwest::Response, response: reqwest::Response,
dest_path: &Path, dest_path: &Path,
part_path: &Path, part_path: &Path,
@@ -0,0 +1,28 @@
//! `psysonic-syncfs` — offline / hot-cache, device sync, and the shared HTTP
//! download helpers used by both.
//!
//! This crate hosts the Tauri commands that read/write the on-disk caches
//! (`offline_*`, `hot_cache_*`) and that copy tracks to mounted USB / SD-card
//! devices (`sync_*`).
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, OnceLock};
// Re-export logging facade so submodules can keep `crate::app_eprintln!()`.
pub use psysonic_core::{app_deprintln, app_eprintln, logging};
pub mod cache;
pub mod file_transfer;
pub mod sync;
/// Shared semaphore that caps simultaneous `download_track_offline` executions.
pub type DownloadSemaphore = Arc<tokio::sync::Semaphore>;
/// Per-job cancellation flags for `sync_batch_to_device`.
/// Each running sync registers an `Arc<AtomicBool>` here; `cancel_device_sync`
/// flips it.
pub fn sync_cancel_flags() -> &'static Mutex<HashMap<String, Arc<AtomicBool>>> {
static FLAGS: OnceLock<Mutex<HashMap<String, Arc<AtomicBool>>>> = OnceLock::new();
FLAGS.get_or_init(|| Mutex::new(HashMap::new()))
}
@@ -5,14 +5,14 @@ use tauri::Emitter;
use crate::sync_cancel_flags; use crate::sync_cancel_flags;
use super::super::file_transfer::{finalize_streamed_download, subsonic_http_client}; use crate::file_transfer::{finalize_streamed_download, subsonic_http_client};
use super::device::{ use super::device::{
build_track_path, get_removable_drives, is_path_on_mounted_volume, SyncBatchResult, build_track_path, get_removable_drives, is_path_on_mounted_volume, SyncBatchResult,
TrackSyncInfo, TrackSyncInfo,
}; };
#[tauri::command] #[tauri::command]
pub(crate) async fn list_device_dir_files(dir: String) -> Result<Vec<String>, String> { pub async fn list_device_dir_files(dir: String) -> Result<Vec<String>, String> {
let root = std::path::PathBuf::from(&dir); let root = std::path::PathBuf::from(&dir);
if !root.exists() { if !root.exists() {
return Err("VOLUME_NOT_FOUND".to_string()); return Err("VOLUME_NOT_FOUND".to_string());
@@ -45,7 +45,7 @@ pub(crate) async fn list_device_dir_files(dir: String) -> Result<Vec<String>, St
/// Deletes a file from the device and prunes empty parent directories /// Deletes a file from the device and prunes empty parent directories
/// (up to 2 levels: album folder, then artist folder). /// (up to 2 levels: album folder, then artist folder).
#[tauri::command] #[tauri::command]
pub(crate) async fn delete_device_file(path: String) -> Result<(), String> { pub async fn delete_device_file(path: String) -> Result<(), String> {
let p = std::path::PathBuf::from(&path); let p = std::path::PathBuf::from(&path);
if p.exists() { if p.exists() {
tokio::fs::remove_file(&p).await.map_err(|e| e.to_string())?; tokio::fs::remove_file(&p).await.map_err(|e| e.to_string())?;
@@ -55,7 +55,7 @@ pub(crate) async fn delete_device_file(path: String) -> Result<(), String> {
} }
/// Prune empty parent directories up to `levels` levels above `file_path`. /// Prune empty parent directories up to `levels` levels above `file_path`.
pub(crate) async fn prune_empty_parents(file_path: &std::path::Path, levels: usize) { pub async fn prune_empty_parents(file_path: &std::path::Path, levels: usize) {
let mut current = file_path.parent().map(|d| d.to_path_buf()); let mut current = file_path.parent().map(|d| d.to_path_buf());
for _ in 0..levels { for _ in 0..levels {
let Some(dir) = current else { break }; let Some(dir) = current else { break };
@@ -73,7 +73,7 @@ pub(crate) async fn prune_empty_parents(file_path: &std::path::Path, levels: usi
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct SubsonicAuthPayload { pub struct SubsonicAuthPayload {
base_url: String, base_url: String,
u: String, u: String,
t: String, t: String,
@@ -84,7 +84,7 @@ pub(crate) struct SubsonicAuthPayload {
} }
#[derive(serde::Deserialize, Clone)] #[derive(serde::Deserialize, Clone)]
pub(crate) struct DeviceSyncSourcePayload { pub struct DeviceSyncSourcePayload {
#[serde(rename = "type")] #[serde(rename = "type")]
source_type: String, source_type: String,
id: String, id: String,
@@ -96,7 +96,7 @@ pub(crate) struct DeviceSyncSourcePayload {
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct SyncDeltaResult { pub struct SyncDeltaResult {
add_bytes: u64, add_bytes: u64,
add_count: u32, add_count: u32,
del_bytes: u64, del_bytes: u64,
@@ -105,7 +105,7 @@ pub(crate) struct SyncDeltaResult {
tracks: Vec<serde_json::Value>, tracks: Vec<serde_json::Value>,
} }
pub(crate) async fn fetch_subsonic_songs( pub async fn fetch_subsonic_songs(
client: &reqwest::Client, client: &reqwest::Client,
auth: &SubsonicAuthPayload, auth: &SubsonicAuthPayload,
endpoint: &str, endpoint: &str,
@@ -142,7 +142,7 @@ pub(crate) async fn fetch_subsonic_songs(
} }
#[tauri::command] #[tauri::command]
pub(crate) async fn calculate_sync_payload( pub async fn calculate_sync_payload(
sources: Vec<DeviceSyncSourcePayload>, sources: Vec<DeviceSyncSourcePayload>,
deletion_ids: Vec<String>, deletion_ids: Vec<String>,
auth: SubsonicAuthPayload, auth: SubsonicAuthPayload,
@@ -314,7 +314,7 @@ pub(crate) async fn calculate_sync_payload(
/// Signals a running `sync_batch_to_device` job to stop after its current tracks finish. /// Signals a running `sync_batch_to_device` job to stop after its current tracks finish.
#[tauri::command] #[tauri::command]
pub(crate) fn cancel_device_sync(job_id: String, app: tauri::AppHandle) { pub fn cancel_device_sync(job_id: String, app: tauri::AppHandle) {
if let Ok(flags) = sync_cancel_flags().lock() { if let Ok(flags) = sync_cancel_flags().lock() {
if let Some(flag) = flags.get(&job_id) { if let Some(flag) = flags.get(&job_id) {
flag.store(true, Ordering::Relaxed); flag.store(true, Ordering::Relaxed);
@@ -328,7 +328,7 @@ pub(crate) fn cancel_device_sync(job_id: String, app: tauri::AppHandle) {
/// Emits throttled `device:sync:progress` events (max once per 500ms) and a /// Emits throttled `device:sync:progress` events (max once per 500ms) and a
/// final `device:sync:complete` event with the summary. /// final `device:sync:complete` event with the summary.
#[tauri::command] #[tauri::command]
pub(crate) async fn sync_batch_to_device( pub async fn sync_batch_to_device(
tracks: Vec<TrackSyncInfo>, tracks: Vec<TrackSyncInfo>,
dest_dir: String, dest_dir: String,
job_id: String, job_id: String,
@@ -517,7 +517,7 @@ pub(crate) async fn sync_batch_to_device(
/// Deletes multiple files from the device in one call and prunes empty parent /// Deletes multiple files from the device in one call and prunes empty parent
/// directories. Returns the number of files successfully deleted. /// directories. Returns the number of files successfully deleted.
#[tauri::command] #[tauri::command]
pub(crate) async fn delete_device_files(paths: Vec<String>) -> Result<u32, String> { pub async fn delete_device_files(paths: Vec<String>) -> Result<u32, String> {
let mut deleted: u32 = 0; let mut deleted: u32 = 0;
for path in &paths { for path in &paths {
let p = std::path::PathBuf::from(path); let p = std::path::PathBuf::from(path);
@@ -1,25 +1,25 @@
use tauri::Emitter; use tauri::Emitter;
use super::super::file_transfer::{finalize_streamed_download, subsonic_http_client}; use crate::file_transfer::{finalize_streamed_download, subsonic_http_client};
// ─── Device Sync ───────────────────────────────────────────────────────────── // ─── Device Sync ─────────────────────────────────────────────────────────────
/// Information about a single mounted removable drive. /// Information about a single mounted removable drive.
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
pub(crate) struct RemovableDrive { pub struct RemovableDrive {
pub(crate) name: String, pub name: String,
pub(crate) mount_point: String, pub mount_point: String,
pub(crate) available_space: u64, pub available_space: u64,
pub(crate) total_space: u64, pub total_space: u64,
pub(crate) file_system: String, pub file_system: String,
pub(crate) is_removable: bool, pub is_removable: bool,
} }
/// Returns all currently mounted removable drives. /// Returns all currently mounted removable drives.
/// On Linux these are typically USB sticks / SD cards under /media or /run/media. /// On Linux these are typically USB sticks / SD cards under /media or /run/media.
/// On macOS they appear under /Volumes. On Windows they are separate drive letters. /// On macOS they appear under /Volumes. On Windows they are separate drive letters.
#[tauri::command] #[tauri::command]
pub(crate) fn get_removable_drives() -> Vec<RemovableDrive> { pub fn get_removable_drives() -> Vec<RemovableDrive> {
use sysinfo::Disks; use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list(); let disks = Disks::new_with_refreshed_list();
disks disks
@@ -41,7 +41,7 @@ pub(crate) fn get_removable_drives() -> Vec<RemovableDrive> {
/// The file records which sources (albums/playlists/artists) are synced to this /// The file records which sources (albums/playlists/artists) are synced to this
/// device so that another machine can pick them up without relying on localStorage. /// device so that another machine can pick them up without relying on localStorage.
#[tauri::command] #[tauri::command]
pub(crate) fn write_device_manifest(dest_dir: String, sources: serde_json::Value) -> Result<(), String> { pub fn write_device_manifest(dest_dir: String, sources: serde_json::Value) -> Result<(), String> {
let path = std::path::Path::new(&dest_dir).join("psysonic-sync.json"); let path = std::path::Path::new(&dest_dir).join("psysonic-sync.json");
// Manifest v2: fixed "{AlbumArtist}/{Album}/{TrackNum} - {Title}.{ext}" schema, // Manifest v2: fixed "{AlbumArtist}/{Album}/{TrackNum} - {Title}.{ext}" schema,
// no user-configurable filename template. Readers still accept v1 manifests. // no user-configurable filename template. Readers still accept v1 manifests.
@@ -57,7 +57,7 @@ pub(crate) fn write_device_manifest(dest_dir: String, sources: serde_json::Value
/// Reads `psysonic-sync.json` from the target directory. /// Reads `psysonic-sync.json` from the target directory.
/// Returns the parsed JSON value, or null if the file doesn't exist. /// Returns the parsed JSON value, or null if the file doesn't exist.
#[tauri::command] #[tauri::command]
pub(crate) fn read_device_manifest(dest_dir: String) -> Option<serde_json::Value> { pub fn read_device_manifest(dest_dir: String) -> Option<serde_json::Value> {
let path = std::path::Path::new(&dest_dir).join("psysonic-sync.json"); let path = std::path::Path::new(&dest_dir).join("psysonic-sync.json");
let content = std::fs::read_to_string(&path).ok()?; let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok() serde_json::from_str(&content).ok()
@@ -65,7 +65,7 @@ pub(crate) fn read_device_manifest(dest_dir: String) -> Option<serde_json::Value
/// Per-entry result for `rename_device_files`. /// Per-entry result for `rename_device_files`.
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub(crate) struct RenameResult { pub struct RenameResult {
#[serde(rename = "oldPath")] #[serde(rename = "oldPath")]
old_path: String, old_path: String,
#[serde(rename = "newPath")] #[serde(rename = "newPath")]
@@ -85,7 +85,7 @@ pub(crate) struct RenameResult {
/// and which failed. Does not roll back on partial failure — each `fs::rename` /// and which failed. Does not roll back on partial failure — each `fs::rename`
/// is atomic, so nothing can be half-renamed. /// is atomic, so nothing can be half-renamed.
#[tauri::command] #[tauri::command]
pub(crate) fn rename_device_files( pub fn rename_device_files(
target_dir: String, target_dir: String,
pairs: Vec<(String, String)>, pairs: Vec<(String, String)>,
) -> Result<Vec<RenameResult>, String> { ) -> Result<Vec<RenameResult>, String> {
@@ -170,7 +170,7 @@ pub(crate) fn rename_device_files(
/// playlist is self-contained — moving/copying the folder anywhere keeps it /// playlist is self-contained — moving/copying the folder anywhere keeps it
/// working. Tracks are expected to be in playlist order (index starts at 1). /// working. Tracks are expected to be in playlist order (index starts at 1).
#[tauri::command] #[tauri::command]
pub(crate) fn write_playlist_m3u8( pub fn write_playlist_m3u8(
dest_dir: String, dest_dir: String,
playlist_name: String, playlist_name: String,
tracks: Vec<TrackSyncInfo>, tracks: Vec<TrackSyncInfo>,
@@ -199,7 +199,7 @@ pub(crate) fn write_playlist_m3u8(
/// filesystem). This prevents accidentally writing to `/media/usb` after the /// filesystem). This prevents accidentally writing to `/media/usb` after the
/// USB drive has been unmounted — at that point the path would fall through to `/` /// USB drive has been unmounted — at that point the path would fall through to `/`
/// and fill the root partition. /// and fill the root partition.
pub(crate) fn is_path_on_mounted_volume(path: &std::path::Path) -> bool { pub fn is_path_on_mounted_volume(path: &std::path::Path) -> bool {
use sysinfo::Disks; use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list(); let disks = Disks::new_with_refreshed_list();
let canonical = match path.canonicalize() { let canonical = match path.canonicalize() {
@@ -232,53 +232,53 @@ pub(crate) fn is_path_on_mounted_volume(path: &std::path::Path) -> bool {
} }
#[derive(serde::Deserialize, Clone)] #[derive(serde::Deserialize, Clone)]
pub(crate) struct TrackSyncInfo { pub struct TrackSyncInfo {
pub(crate) id: String, pub id: String,
pub(crate) url: String, pub url: String,
pub(crate) suffix: String, pub suffix: String,
/// Track artist — used in Extended M3U (#EXTINF) entries so playlists display /// Track artist — used in Extended M3U (#EXTINF) entries so playlists display
/// the actual performer rather than the album artist. /// the actual performer rather than the album artist.
pub(crate) artist: String, pub artist: String,
/// Album artist — used for the top-level folder so compilation albums stay together. /// Album artist — used for the top-level folder so compilation albums stay together.
/// Falls back to `artist` in the frontend when the server has no albumArtist tag. /// Falls back to `artist` in the frontend when the server has no albumArtist tag.
#[serde(rename = "albumArtist")] #[serde(rename = "albumArtist")]
pub(crate) album_artist: String, pub album_artist: String,
pub(crate) album: String, pub album: String,
pub(crate) title: String, pub title: String,
#[serde(rename = "trackNumber")] #[serde(rename = "trackNumber")]
pub(crate) track_number: Option<u32>, pub track_number: Option<u32>,
/// Duration in seconds — needed for Extended M3U (#EXTINF) playlist entries. /// Duration in seconds — needed for Extended M3U (#EXTINF) playlist entries.
#[serde(default)] #[serde(default)]
pub(crate) duration: Option<u32>, pub duration: Option<u32>,
/// When set, the track belongs to a playlist source and is placed under /// When set, the track belongs to a playlist source and is placed under
/// `Playlists/{name}/` with `playlist_index` as its filename prefix. /// `Playlists/{name}/` with `playlist_index` as its filename prefix.
/// Same track synced from both an album and a playlist source ends up twice /// Same track synced from both an album and a playlist source ends up twice
/// on the device — once in the album tree, once in the playlist folder. /// on the device — once in the album tree, once in the playlist folder.
#[serde(default, rename = "playlistName")] #[serde(default, rename = "playlistName")]
pub(crate) playlist_name: Option<String>, pub playlist_name: Option<String>,
#[serde(default, rename = "playlistIndex")] #[serde(default, rename = "playlistIndex")]
pub(crate) playlist_index: Option<u32>, pub playlist_index: Option<u32>,
} }
/// Summary returned by `sync_batch_to_device` after all tracks are processed. /// Summary returned by `sync_batch_to_device` after all tracks are processed.
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
pub(crate) struct SyncBatchResult { pub struct SyncBatchResult {
pub(crate) done: u32, pub done: u32,
pub(crate) skipped: u32, pub skipped: u32,
pub(crate) failed: u32, pub failed: u32,
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub(crate) struct SyncTrackResult { pub struct SyncTrackResult {
pub(crate) path: String, pub path: String,
pub(crate) skipped: bool, pub skipped: bool,
} }
/// Replaces characters that are invalid in file/directory names on Windows and /// Replaces characters that are invalid in file/directory names on Windows and
/// most Unix filesystems with an underscore, and trims leading/trailing dots and /// most Unix filesystems with an underscore, and trims leading/trailing dots and
/// spaces which cause issues on Windows. Underscore (not deletion) so that "AC/DC" /// spaces which cause issues on Windows. Underscore (not deletion) so that "AC/DC"
/// and "ACDC" don't collapse into the same folder. /// and "ACDC" don't collapse into the same folder.
pub(crate) fn sanitize_path_component(s: &str) -> String { pub fn sanitize_path_component(s: &str) -> String {
const INVALID: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|']; const INVALID: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
let sanitized: String = s let sanitized: String = s
.chars() .chars()
@@ -289,7 +289,7 @@ pub(crate) fn sanitize_path_component(s: &str) -> String {
/// Sanitize and replace empty results with a placeholder — prevents paths like /// Sanitize and replace empty results with a placeholder — prevents paths like
/// `//01 - .flac` when metadata is missing. /// `//01 - .flac` when metadata is missing.
pub(crate) fn sanitize_or(s: &str, fallback: &str) -> String { pub fn sanitize_or(s: &str, fallback: &str) -> String {
let cleaned = sanitize_path_component(s); let cleaned = sanitize_path_component(s);
if cleaned.is_empty() { fallback.to_string() } else { cleaned } if cleaned.is_empty() { fallback.to_string() } else { cleaned }
} }
@@ -299,7 +299,7 @@ pub(crate) fn sanitize_or(s: &str, fallback: &str) -> String {
/// ///
/// Album-tree: `{AlbumArtist}/{Album}/{TrackNum:02d} - {Title}.{ext}` /// Album-tree: `{AlbumArtist}/{Album}/{TrackNum:02d} - {Title}.{ext}`
/// Playlist: `Playlists/{PlaylistName}/{PlaylistIndex:02d} - {Artist} - {Title}.{ext}` /// Playlist: `Playlists/{PlaylistName}/{PlaylistIndex:02d} - {Artist} - {Title}.{ext}`
pub(crate) fn build_track_path(track: &TrackSyncInfo) -> String { pub fn build_track_path(track: &TrackSyncInfo) -> String {
let relative = match (&track.playlist_name, track.playlist_index) { let relative = match (&track.playlist_name, track.playlist_index) {
(Some(name), Some(idx)) => { (Some(name), Some(idx)) => {
let playlist = sanitize_or(name, "Unnamed Playlist"); let playlist = sanitize_or(name, "Unnamed Playlist");
@@ -323,7 +323,7 @@ pub(crate) fn build_track_path(track: &TrackSyncInfo) -> String {
/// Downloads a single track to a USB/SD device using the configured filename template. /// Downloads a single track to a USB/SD device using the configured filename template.
/// Emits `device:sync:progress` events with `{ jobId, trackId, status, path? }`. /// Emits `device:sync:progress` events with `{ jobId, trackId, status, path? }`.
#[tauri::command] #[tauri::command]
pub(crate) async fn sync_track_to_device( pub async fn sync_track_to_device(
track: TrackSyncInfo, track: TrackSyncInfo,
dest_dir: String, dest_dir: String,
job_id: String, job_id: String,
@@ -375,7 +375,7 @@ pub(crate) async fn sync_track_to_device(
/// Computes the expected file paths for a batch of tracks under the fixed schema. /// Computes the expected file paths for a batch of tracks under the fixed schema.
/// Used by the cleanup flow to find orphans. /// Used by the cleanup flow to find orphans.
#[tauri::command] #[tauri::command]
pub(crate) fn compute_sync_paths(tracks: Vec<TrackSyncInfo>, dest_dir: String) -> Vec<String> { pub fn compute_sync_paths(tracks: Vec<TrackSyncInfo>, dest_dir: String) -> Vec<String> {
tracks.iter().map(|track| { tracks.iter().map(|track| {
let relative = build_track_path(track); let relative = build_track_path(track);
let file_name = format!("{}.{}", relative, track.suffix); let file_name = format!("{}.{}", relative, track.suffix);
@@ -0,0 +1,2 @@
pub mod batch;
pub mod device;
+29 -39
View File
@@ -12,6 +12,7 @@ pub use psysonic_core::user_agent::{
}; };
pub use psysonic_analysis::{analysis_cache, analysis_runtime}; pub use psysonic_analysis::{analysis_cache, analysis_runtime};
pub use psysonic_audio as audio; pub use psysonic_audio as audio;
pub use psysonic_syncfs::{sync_cancel_flags, DownloadSemaphore};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod taskbar_win; mod taskbar_win;
mod tray_runtime; mod tray_runtime;
@@ -19,8 +20,7 @@ mod tray_runtime;
pub(crate) use tray_runtime::*; pub(crate) use tray_runtime::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, Mutex};
use std::sync::atomic::AtomicBool;
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
use lib_commands::*; use lib_commands::*;
@@ -33,16 +33,6 @@ type ShortcutMap = Mutex<HashMap<String, String>>;
/// The frontend queues more tasks than this; Rust is the real throttle. /// The frontend queues more tasks than this; Rust is the real throttle.
const MAX_DL_CONCURRENCY: usize = 4; const MAX_DL_CONCURRENCY: usize = 4;
/// Shared semaphore that caps simultaneous `download_track_offline` executions.
type DownloadSemaphore = Arc<tokio::sync::Semaphore>;
/// Per-job cancellation flags for `sync_batch_to_device`.
/// Each running sync registers an `Arc<AtomicBool>` here; `cancel_device_sync` flips it.
fn sync_cancel_flags() -> &'static Mutex<HashMap<String, Arc<AtomicBool>>> {
static FLAGS: OnceLock<Mutex<HashMap<String, Arc<AtomicBool>>>> = OnceLock::new();
FLAGS.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Shared handle to OS media controls (MPRIS2 on Linux, Now Playing on macOS, SMTC on Windows). /// 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). /// `None` if souvlaki failed to initialize (e.g. no D-Bus session on Linux).
type MprisControls = Mutex<Option<souvlaki::MediaControls>>; type MprisControls = Mutex<Option<souvlaki::MediaControls>>;
@@ -350,7 +340,7 @@ pub fn run() {
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
greet, greet,
calculate_sync_payload, psysonic_syncfs::sync::batch::calculate_sync_payload,
exit_app, exit_app,
cli_publish_player_snapshot, cli_publish_player_snapshot,
cli_publish_library_list, cli_publish_library_list,
@@ -437,36 +427,36 @@ pub fn run() {
analysis_delete_all_waveforms, analysis_delete_all_waveforms,
analysis_enqueue_seed_from_url, analysis_enqueue_seed_from_url,
analysis_prune_pending_to_track_ids, analysis_prune_pending_to_track_ids,
download_track_offline, psysonic_syncfs::cache::offline::download_track_offline,
delete_offline_track, psysonic_syncfs::cache::offline::delete_offline_track,
get_offline_cache_size, psysonic_syncfs::cache::offline::get_offline_cache_size,
download_track_hot_cache, psysonic_syncfs::cache::hot::download_track_hot_cache,
promote_stream_cache_to_hot_cache, psysonic_syncfs::cache::hot::promote_stream_cache_to_hot_cache,
get_hot_cache_size, psysonic_syncfs::cache::hot::get_hot_cache_size,
delete_hot_cache_track, psysonic_syncfs::cache::hot::delete_hot_cache_track,
purge_hot_cache, psysonic_syncfs::cache::hot::purge_hot_cache,
sync_track_to_device, psysonic_syncfs::sync::device::sync_track_to_device,
sync_batch_to_device, psysonic_syncfs::sync::batch::sync_batch_to_device,
cancel_device_sync, psysonic_syncfs::sync::batch::cancel_device_sync,
compute_sync_paths, psysonic_syncfs::sync::device::compute_sync_paths,
list_device_dir_files, psysonic_syncfs::sync::batch::list_device_dir_files,
delete_device_file, psysonic_syncfs::sync::batch::delete_device_file,
delete_device_files, psysonic_syncfs::sync::batch::delete_device_files,
get_removable_drives, psysonic_syncfs::sync::device::get_removable_drives,
write_device_manifest, psysonic_syncfs::sync::device::write_device_manifest,
read_device_manifest, psysonic_syncfs::sync::device::read_device_manifest,
write_playlist_m3u8, psysonic_syncfs::sync::device::write_playlist_m3u8,
rename_device_files, psysonic_syncfs::sync::device::rename_device_files,
toggle_tray_icon, toggle_tray_icon,
set_tray_tooltip, set_tray_tooltip,
set_tray_menu_labels, set_tray_menu_labels,
check_dir_accessible, check_dir_accessible,
download_zip, psysonic_syncfs::cache::downloads::download_zip,
check_arch_linux, psysonic_syncfs::cache::downloads::check_arch_linux,
download_update, psysonic_syncfs::cache::downloads::download_update,
open_folder, psysonic_syncfs::cache::downloads::open_folder,
get_embedded_lyrics, psysonic_syncfs::cache::downloads::get_embedded_lyrics,
fetch_netease_lyrics, psysonic_syncfs::cache::downloads::fetch_netease_lyrics,
fetch_bandsintown_events, fetch_bandsintown_events,
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
taskbar_win::update_taskbar_icon, taskbar_win::update_taskbar_icon,
-17
View File
@@ -1,17 +0,0 @@
mod fs_utils;
mod offline;
mod downloads;
mod hot;
// Tauri commands re-exported for the lib.rs invoke_handler:
pub(crate) use offline::{
delete_offline_track, download_track_offline, get_offline_cache_size,
};
pub(crate) use hot::{
delete_hot_cache_track, download_track_hot_cache, get_hot_cache_size,
promote_stream_cache_to_hot_cache, purge_hot_cache,
};
pub(crate) use downloads::{
check_arch_linux, download_update, download_zip, fetch_netease_lyrics, get_embedded_lyrics,
open_folder,
};
+6 -3
View File
@@ -1,6 +1,4 @@
pub(crate) mod app_api; pub(crate) mod app_api;
pub(crate) mod cache;
pub(crate) mod file_transfer;
pub(crate) mod sync; pub(crate) mod sync;
pub(crate) mod ui; pub(crate) mod ui;
@@ -8,6 +6,11 @@ pub(crate) mod ui;
// more level here so `lib.rs`'s `use lib_commands::*;` lands every // more level here so `lib.rs`'s `use lib_commands::*;` lands every
// invoke-handler-registered name at lib.rs scope. // invoke-handler-registered name at lib.rs scope.
pub(crate) use app_api::*; pub(crate) use app_api::*;
pub(crate) use cache::*;
pub(crate) use sync::*; pub(crate) use sync::*;
pub(crate) use ui::*; pub(crate) use ui::*;
// Cache + file_transfer + sync commands now live in psysonic_syncfs.
// invoke_handler! in lib.rs registers them with their full paths
// (`psysonic_syncfs::cache::*` / `psysonic_syncfs::sync::*`) so Tauri's
// `__cmd__*` magic macros resolve across the crate boundary. Nothing to
// re-export at this level.
+6 -12
View File
@@ -1,19 +1,13 @@
mod device;
mod batch;
mod tray; mod tray;
// Tauri commands re-exported for the lib.rs invoke_handler:
pub(crate) use device::{
get_removable_drives, read_device_manifest, rename_device_files, sync_track_to_device,
write_device_manifest, write_playlist_m3u8, compute_sync_paths,
};
pub(crate) use batch::{
calculate_sync_payload, cancel_device_sync, delete_device_file, delete_device_files,
list_device_dir_files, sync_batch_to_device,
};
pub(crate) use tray::{ pub(crate) use tray::{
is_tiling_wm_cmd, no_compositing_mode, set_tray_menu_labels, set_tray_tooltip, is_tiling_wm_cmd, no_compositing_mode, set_tray_menu_labels, set_tray_tooltip,
toggle_tray_icon, toggle_tray_icon,
}; };
// Internal helpers consumed elsewhere in the crate: // Internal helpers consumed elsewhere in the shell crate:
pub(crate) use tray::{is_tiling_wm, stop_audio_engine, try_build_tray_icon}; pub(crate) use tray::{is_tiling_wm, stop_audio_engine, try_build_tray_icon};
// Sync commands now live in `psysonic_syncfs::sync::*` — invoke_handler!
// in lib.rs registers them with the full path so Tauri's `__cmd__*` magic
// macros resolve correctly across the crate boundary; nothing to re-export
// here.