download marking in place

This commit is contained in:
2026-02-16 16:08:53 -06:00
parent 6fe294f19b
commit 26a79ecb7e
2 changed files with 134 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
use std::{
collections::VecDeque,
collections::{VecDeque, HashSet},
fs,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
@@ -92,6 +92,89 @@ impl DownloadManager {
// ---------- directory helpers ----------
const DOWNLOAD_MARKER_EXT: &str = "cj-jftui.json";
fn marker_path_for_media(media_path: &Path) -> PathBuf {
// Example:
// Movie.mkv -> Movie.mkv.cj-jftui.json
let file_name = media_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("download");
media_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!("{file_name}.{DOWNLOAD_MARKER_EXT}"))
}
fn write_download_marker(media_path: &Path, item_id: &str) -> Result<(), String> {
let marker = marker_path_for_media(media_path);
let payload = json!({
"item_id": item_id,
"saved_at_unix": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
.unwrap_or(0),
"media_file": media_path.file_name().and_then(|s| s.to_str()).unwrap_or(""),
});
fs::write(&marker, payload.to_string())
.map_err(|e| format!("write marker failed: {e}"))?;
Ok(())
}
fn scan_dir_for_markers(dir: &Path, out: &mut HashSet<String>) -> Result<(), String> {
if !dir.exists() {
return Ok(());
}
for ent in fs::read_dir(dir).map_err(|e| format!("read_dir failed: {e}"))? {
let p = ent.map_err(|e| format!("dir entry failed: {e}"))?.path();
if p.is_dir() {
scan_dir_for_markers(&p, out)?;
continue;
}
// Look for *.cj-jftui.json
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if !name.ends_with(DOWNLOAD_MARKER_EXT) {
continue;
}
let raw = match fs::read_to_string(&p) {
Ok(s) => s,
Err(_) => continue,
};
let v: Value = match serde_json::from_str(raw.trim()) {
Ok(v) => v,
Err(_) => continue,
};
if let Some(id) = v.get("item_id").and_then(|x| x.as_str()) {
if !id.trim().is_empty() {
out.insert(id.to_string());
}
}
}
Ok(())
}
/// Public: scan the downloads root and return set of downloaded Jellyfin item ids.
pub fn scan_downloaded_ids() -> Result<HashSet<String>, String> {
let root = downloads_root()?;
let mut out = HashSet::new();
scan_dir_for_markers(&root, &mut out)?;
Ok(out)
}
pub fn downloads_root() -> Result<PathBuf, String> {
let dir = paths::app_config_dir().ok_or_else(|| "no config dir".to_string())?;
Ok(dir.join("downloads"))
@@ -719,8 +802,6 @@ fn poll_progress_for_front(state: &Arc<Mutex<DownloadState>>) {
}
fn finalize_front(state: &Arc<Mutex<DownloadState>>) -> Result<(), String> {
let (it, tmp_dir, final_abs_watch) = {
let st = state.lock().map_err(|_| "lock poisoned".to_string())?;
@@ -741,8 +822,8 @@ fn finalize_front(state: &Arc<Mutex<DownloadState>>) -> Result<(), String> {
let real_file = find_real_downloaded_file(&tmp_dir)?;
// Build final filename:
// Your final_rel_path ends in "...something.watch" or "...something.listen".
// We keep the same STEM and replace extension with the actual downloaded extension.
// final_rel_path ends in "...something.watch/.listen"
// Keep stem, replace extension with actual media extension
let real_ext = real_file
.extension()
.and_then(|s| s.to_str())
@@ -756,7 +837,6 @@ fn finalize_front(state: &Arc<Mutex<DownloadState>>) -> Result<(), String> {
.to_string();
let final_abs = if real_ext.is_empty() {
// fallback: no extension detected; keep .watch path as-is
final_abs_watch.clone()
} else {
final_parent.join(format!("{stem}.{real_ext}"))
@@ -765,6 +845,10 @@ fn finalize_front(state: &Arc<Mutex<DownloadState>>) -> Result<(), String> {
// Move file into place
fs::rename(&real_file, &final_abs).map_err(|e| format!("move to final failed: {e}"))?;
// ✅ NEW: write marker so we can detect "downloaded" later
// (If marker fails, we still consider download successful.)
let _ = write_download_marker(&final_abs, &it.item_id);
// Cleanup temp folder fully
let _ = fs::remove_dir_all(&tmp_dir);