mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
cfeec22c82
Split the large lib command module into nested app_api, cache, sync, and ui submodules while preserving command signatures and behavior. This keeps command responsibilities isolated and makes further extraction safer.
265 lines
8.0 KiB
Rust
265 lines
8.0 KiB
Rust
use super::*;
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn download_track_hot_cache(
|
|
track_id: String,
|
|
server_id: String,
|
|
url: String,
|
|
suffix: String,
|
|
custom_dir: Option<String>,
|
|
app: tauri::AppHandle,
|
|
) -> Result<HotCacheDownloadResult, String> {
|
|
let root = resolve_hot_cache_root(custom_dir, &app)?;
|
|
let cache_dir = root.join(&server_id);
|
|
|
|
tokio::fs::create_dir_all(&cache_dir)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let file_path = cache_dir.join(format!("{}.{}", track_id, suffix));
|
|
let path_str = file_path.to_string_lossy().to_string();
|
|
|
|
if file_path.exists() {
|
|
let size = tokio::fs::metadata(&file_path)
|
|
.await
|
|
.map(|m| m.len())
|
|
.unwrap_or(0);
|
|
crate::app_deprintln!(
|
|
"[hot-cache] download disk_hit track_id={} server_id={} bytes={}",
|
|
track_id,
|
|
server_id,
|
|
size
|
|
);
|
|
// Disk hit: still seed analysis, but do not block the command (full-file read); the
|
|
// prefetch worker runs invokes sequentially.
|
|
let app_seed = app.clone();
|
|
let tid = track_id.clone();
|
|
let fp = file_path.clone();
|
|
tokio::spawn(async move {
|
|
enqueue_analysis_seed_from_file(&app_seed, &tid, &fp).await;
|
|
});
|
|
return Ok(HotCacheDownloadResult {
|
|
path: path_str,
|
|
size,
|
|
});
|
|
}
|
|
|
|
crate::app_deprintln!(
|
|
"[hot-cache] download http_start track_id={} server_id={}",
|
|
track_id,
|
|
server_id
|
|
);
|
|
|
|
let client = reqwest::Client::builder()
|
|
.user_agent(subsonic_wire_user_agent())
|
|
.timeout(std::time::Duration::from_secs(120))
|
|
.build()
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let response = client.get(&url).send().await.map_err(|e| e.to_string())?;
|
|
if !response.status().is_success() {
|
|
return Err(format!("HTTP {}", response.status().as_u16()));
|
|
}
|
|
|
|
// Stream directly to a .part file; rename on success to avoid partial files.
|
|
let part_path = file_path.with_extension(format!("{suffix}.part"));
|
|
if let Err(e) = stream_to_file(response, &part_path).await {
|
|
let _ = tokio::fs::remove_file(&part_path).await;
|
|
return Err(e);
|
|
}
|
|
tokio::fs::rename(&part_path, &file_path)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let app_seed = app.clone();
|
|
let tid = track_id.clone();
|
|
let fp = file_path.clone();
|
|
tokio::spawn(async move {
|
|
enqueue_analysis_seed_from_file(&app_seed, &tid, &fp).await;
|
|
});
|
|
|
|
let size = tokio::fs::metadata(&file_path)
|
|
.await
|
|
.map(|m| m.len())
|
|
.unwrap_or(0);
|
|
crate::app_deprintln!(
|
|
"[hot-cache] download http_done track_id={} server_id={} bytes={}",
|
|
track_id,
|
|
server_id,
|
|
size
|
|
);
|
|
Ok(HotCacheDownloadResult {
|
|
path: path_str,
|
|
size,
|
|
})
|
|
}
|
|
|
|
/// 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]
|
|
pub(crate) async fn promote_stream_cache_to_hot_cache(
|
|
track_id: String,
|
|
server_id: String,
|
|
url: String,
|
|
suffix: String,
|
|
custom_dir: Option<String>,
|
|
app: tauri::AppHandle,
|
|
state: tauri::State<'_, audio::AudioEngine>,
|
|
) -> Result<Option<HotCacheDownloadResult>, String> {
|
|
let root = resolve_hot_cache_root(custom_dir, &app)?;
|
|
let cache_dir = root.join(&server_id);
|
|
tokio::fs::create_dir_all(&cache_dir)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let file_path = cache_dir.join(format!("{}.{}", track_id, suffix));
|
|
let path_str = file_path.to_string_lossy().to_string();
|
|
|
|
if file_path.exists() {
|
|
let size = tokio::fs::metadata(&file_path)
|
|
.await
|
|
.map(|m| m.len())
|
|
.unwrap_or(0);
|
|
crate::app_deprintln!(
|
|
"[hot-cache] promote disk_hit track_id={} server_id={} bytes={}",
|
|
track_id,
|
|
server_id,
|
|
size
|
|
);
|
|
let app_seed = app.clone();
|
|
let tid = track_id.clone();
|
|
let fp = file_path.clone();
|
|
tokio::spawn(async move {
|
|
enqueue_analysis_seed_from_file(&app_seed, &tid, &fp).await;
|
|
});
|
|
return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
|
|
}
|
|
|
|
let bytes = match audio::take_stream_completed_for_url(&state, &url) {
|
|
Some(b) => b,
|
|
None => {
|
|
crate::app_deprintln!(
|
|
"[hot-cache] promote skip track_id={} reason=no_completed_stream_for_url",
|
|
track_id
|
|
);
|
|
return Ok(None);
|
|
}
|
|
};
|
|
|
|
let part_path = file_path.with_extension(format!("{suffix}.part"));
|
|
if let Err(e) = tokio::fs::write(&part_path, &bytes).await {
|
|
let _ = tokio::fs::remove_file(&part_path).await;
|
|
return Err(e.to_string());
|
|
}
|
|
tokio::fs::rename(&part_path, &file_path)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
let _ = enqueue_analysis_seed(&app, &track_id, &bytes).await;
|
|
|
|
let size = tokio::fs::metadata(&file_path)
|
|
.await
|
|
.map(|m| m.len())
|
|
.unwrap_or(0);
|
|
crate::app_deprintln!(
|
|
"[hot-cache] promote from_stream track_id={} server_id={} bytes={}",
|
|
track_id,
|
|
server_id,
|
|
size
|
|
);
|
|
Ok(Some(HotCacheDownloadResult { path: path_str, size }))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
|
|
fn dir_size(root: std::path::PathBuf) -> u64 {
|
|
if !root.exists() {
|
|
return 0;
|
|
}
|
|
let mut total: u64 = 0;
|
|
let mut stack = vec![root];
|
|
while let Some(dir) = stack.pop() {
|
|
let rd = match std::fs::read_dir(&dir) {
|
|
Ok(r) => r,
|
|
Err(_) => continue,
|
|
};
|
|
for entry in rd.flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
stack.push(path);
|
|
} else if let Ok(meta) = std::fs::metadata(&path) {
|
|
total += meta.len();
|
|
}
|
|
}
|
|
}
|
|
total
|
|
}
|
|
|
|
resolve_hot_cache_root(custom_dir, &app)
|
|
.map(|root| dir_size(root))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn delete_hot_cache_track(
|
|
local_path: String,
|
|
custom_dir: Option<String>,
|
|
app: tauri::AppHandle,
|
|
) -> Result<(), String> {
|
|
let file_path = std::path::PathBuf::from(&local_path);
|
|
let existed = file_path.exists();
|
|
if existed {
|
|
tokio::fs::remove_file(&file_path)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
crate::app_deprintln!(
|
|
"[hot-cache] delete file existed={} path_suffix={}",
|
|
existed,
|
|
file_path
|
|
.file_name()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("?")
|
|
);
|
|
|
|
let boundary = resolve_hot_cache_root(custom_dir, &app)?;
|
|
|
|
let mut current = file_path.parent().map(|p| p.to_path_buf());
|
|
while let Some(dir) = current {
|
|
if dir == boundary || !dir.starts_with(&boundary) {
|
|
break;
|
|
}
|
|
match std::fs::read_dir(&dir) {
|
|
Ok(mut entries) => {
|
|
if entries.next().is_some() {
|
|
break;
|
|
}
|
|
if std::fs::remove_dir(&dir).is_err() {
|
|
break;
|
|
}
|
|
current = dir.parent().map(|p| p.to_path_buf());
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes the entire hot cache root (`psysonic-hot-cache` for the active location).
|
|
#[tauri::command]
|
|
pub(crate) 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() {
|
|
return Ok(());
|
|
}
|
|
tokio::fs::remove_dir_all(&root)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
crate::app_deprintln!(
|
|
"[hot-cache] purge root={} status=ok",
|
|
root.to_string_lossy()
|
|
);
|
|
Ok(())
|
|
}
|