Deletion on and offline restored.
This commit is contained in:
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
This is the first actually useful programming project I have completed.
|
||||||
|
I still can't write in rust because of generative AI.
|
||||||
@@ -1227,3 +1227,138 @@ pub fn local_media_path_for_item_id(item_id: &str) -> Option<PathBuf> {
|
|||||||
let root = downloads_root().ok()?;
|
let root = downloads_root().ok()?;
|
||||||
walk(&root, item_id)
|
walk(&root, item_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Delete ALL local copies of an item by scanning *.cj-jftui.json markers recursively.
|
||||||
|
// Returns how many media files we attempted to remove (count of matched markers).
|
||||||
|
pub fn delete_local_by_item_id(item_id: &str) -> Result<usize, String> {
|
||||||
|
let root = downloads_root()?;
|
||||||
|
let mut deleted = 0usize;
|
||||||
|
delete_in_dir_by_item_id(&root, item_id, &mut deleted)?;
|
||||||
|
Ok(deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete local content based on a concrete local file path.
|
||||||
|
// If there is a sibling marker, we read item_id and delete ALL copies (same as online behavior).
|
||||||
|
// If there is no marker, we delete just that file + sidecars.
|
||||||
|
pub fn delete_local_by_path(path: &Path) -> Result<usize, String> {
|
||||||
|
// If there is a sibling marker, prefer item_id-based deletion (removes hardlinks elsewhere too).
|
||||||
|
if let Some(item_id) = item_id_for_media_path(path)? {
|
||||||
|
return delete_local_by_item_id(&item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No marker => delete just this file + any obvious sidecars.
|
||||||
|
delete_file_and_sidecars(path)?;
|
||||||
|
Ok(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Internal helpers
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
fn delete_in_dir_by_item_id(dir: &Path, item_id: &str, deleted: &mut usize) -> 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() {
|
||||||
|
delete_in_dir_by_item_id(&p, item_id, deleted)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// marker files only
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mid = match v.get("item_id").and_then(|x| x.as_str()) {
|
||||||
|
Some(s) => s.trim(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if mid != item_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we matched the item_id, delete:
|
||||||
|
// - media file referenced by marker (if present)
|
||||||
|
// - marker itself
|
||||||
|
// - media sidecars (.aria2/.part)
|
||||||
|
if let Some(media_name) = v.get("media_file").and_then(|x| x.as_str()) {
|
||||||
|
let media_path = p
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(dir)
|
||||||
|
.join(media_name);
|
||||||
|
|
||||||
|
let _ = delete_file_and_sidecars(&media_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&p); // remove marker
|
||||||
|
*deleted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads sibling marker "<mediafilename>.cj-jftui.json" and returns item_id if present.
|
||||||
|
fn item_id_for_media_path(media_path: &Path) -> Result<Option<String>, String> {
|
||||||
|
let fname = match media_path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let marker = media_path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.join(format!("{fname}.{DOWNLOAD_MARKER_EXT}"));
|
||||||
|
|
||||||
|
if !marker.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = fs::read_to_string(&marker).map_err(|e| format!("read marker failed: {e}"))?;
|
||||||
|
let v: Value = serde_json::from_str(raw.trim()).map_err(|e| format!("marker json parse failed: {e}"))?;
|
||||||
|
|
||||||
|
Ok(v.get("item_id").and_then(|x| x.as_str()).map(|s| s.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_file_and_sidecars(path: &Path) -> Result<(), String> {
|
||||||
|
// delete main file
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
// delete sibling marker (if any)
|
||||||
|
if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
let marker = path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.join(format!("{fname}.{DOWNLOAD_MARKER_EXT}"));
|
||||||
|
let _ = fs::remove_file(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete obvious aria2 artifacts
|
||||||
|
if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
|
||||||
|
let aria2 = parent.join(format!("{fname}.aria2"));
|
||||||
|
let part = parent.join(format!("{fname}.part"));
|
||||||
|
|
||||||
|
let _ = fs::remove_file(aria2);
|
||||||
|
let _ = fs::remove_file(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
51
src/main.rs
51
src/main.rs
@@ -668,6 +668,54 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capital X: delete local copy/copies for hovered item (works same online/offline)
|
||||||
|
if matches!(code, KeyCode::Char('X')) {
|
||||||
|
let result = match app.current_view() {
|
||||||
|
View::Library { items, selected, .. } if !items.is_empty() => {
|
||||||
|
let id = items[*selected].id.clone();
|
||||||
|
crate::core::downloads::delete_local_by_item_id(&id)
|
||||||
|
}
|
||||||
|
View::OfflineLibrary { entries, selected, .. } if !entries.is_empty() => {
|
||||||
|
let e = &entries[*selected];
|
||||||
|
if e.is_dir {
|
||||||
|
app.status = "X: select a file (not a folder)".into();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crate::core::downloads::delete_local_by_path(&e.path)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app.status = "X: nothing deletable here".into();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(n) => {
|
||||||
|
// refresh downloaded-id index so 💾 updates immediately
|
||||||
|
app.downloaded_ids = crate::core::downloads::scan_downloaded_ids().unwrap_or_default();
|
||||||
|
|
||||||
|
// if we’re in OfflineLibrary, refresh the directory listing so the file disappears
|
||||||
|
if let Some(View::OfflineLibrary { dir, entries, selected, .. }) = app.view_stack.last_mut() {
|
||||||
|
*entries = offline_list_dir(dir);
|
||||||
|
if !entries.is_empty() {
|
||||||
|
*selected = (*selected).min(entries.len() - 1);
|
||||||
|
} else {
|
||||||
|
*selected = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.status = format!("Deleted local copies: {n}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.status = format!("Delete failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Mark watched (w) / Unmark watched (W)
|
// Mark watched (w) / Unmark watched (W)
|
||||||
if matches!(code, KeyCode::Char('w') | KeyCode::Char('W')) {
|
if matches!(code, KeyCode::Char('w') | KeyCode::Char('W')) {
|
||||||
let unmark = matches!(code, KeyCode::Char('W'));
|
let unmark = matches!(code, KeyCode::Char('W'));
|
||||||
@@ -1199,7 +1247,6 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
KeyCode::Char('q') => app.should_exit = true,
|
KeyCode::Char('q') => app.should_exit = true,
|
||||||
|
|
||||||
//Enter code Block
|
//Enter code Block
|
||||||
@@ -1903,7 +1950,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
.unwrap_or("-");
|
.unwrap_or("-");
|
||||||
|
|
||||||
let right_text = format!(
|
let right_text = format!(
|
||||||
"{connected}\n\np/P: play/autoplay
|
"{connected}\np/P: play/autoplay
|
||||||
r/R: resume/autoplay
|
r/R: resume/autoplay
|
||||||
d: download / cancel
|
d: download / cancel
|
||||||
q: quit
|
q: quit
|
||||||
|
|||||||
Reference in New Issue
Block a user