mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
9417d522f3
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.
58 lines
2.2 KiB
Rust
58 lines
2.2 KiB
Rust
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
/// 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
|
|
/// downloads with progress events), build the client inline.
|
|
pub fn subsonic_http_client(timeout: Duration) -> Result<reqwest::Client, String> {
|
|
reqwest::Client::builder()
|
|
.user_agent(psysonic_core::user_agent::subsonic_wire_user_agent())
|
|
.timeout(timeout)
|
|
.build()
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Streams an HTTP response body to `dest_path` in chunks. Never buffers the full
|
|
/// file in memory — keeps RAM flat regardless of file size.
|
|
pub async fn stream_to_file(
|
|
response: reqwest::Response,
|
|
dest_path: &Path,
|
|
) -> Result<(), String> {
|
|
use futures_util::StreamExt;
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
let mut file = tokio::fs::File::create(dest_path)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
let mut stream = response.bytes_stream();
|
|
while let Some(chunk) = stream.next().await {
|
|
let chunk = chunk.map_err(|e| e.to_string())?;
|
|
file.write_all(&chunk).await.map_err(|e| e.to_string())?;
|
|
}
|
|
file.flush().await.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Streams `response` to `part_path`, then renames `part_path` → `dest_path`.
|
|
/// On any failure the partial `.part` file is best-effort removed so it does
|
|
/// not linger on disk. Caller must ensure `dest_path.parent()` exists.
|
|
///
|
|
/// Note vs. previous inline implementations: the offline/device single-track
|
|
/// 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.
|
|
pub async fn finalize_streamed_download(
|
|
response: reqwest::Response,
|
|
dest_path: &Path,
|
|
part_path: &Path,
|
|
) -> Result<(), String> {
|
|
if let Err(e) = stream_to_file(response, part_path).await {
|
|
let _ = tokio::fs::remove_file(part_path).await;
|
|
return Err(e);
|
|
}
|
|
if let Err(e) = tokio::fs::rename(part_path, dest_path).await {
|
|
let _ = tokio::fs::remove_file(part_path).await;
|
|
return Err(e.to_string());
|
|
}
|
|
Ok(())
|
|
}
|