download marking in place
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user