diff --git a/ta_symlink.py b/ta_symlink.py index 133f819..d614daf 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -320,7 +320,8 @@ def fetch_all_metadata(): video_map[vid_id] = { "title": title, "channel_name": channel_name, - "published": published + "published": published, + "filesystem_path": video.get("path") or video.get("filesystem_path") } # Check pagination to see if we are done @@ -1135,6 +1136,67 @@ def api_recovery_start(): "status": "completed" if success else "failed" }) +@app.route("/api/recovery/delete-batch", methods=["POST"]) +@requires_auth +def api_recovery_delete_batch(): + data = request.get_json() + filepaths = data.get('filepaths', []) + destruct_mode = data.get('destruct_mode', False) + + if not filepaths: + return jsonify({"error": "No filepaths provided"}), 400 + + results = [] + # Refresh metadata to ensure we have latest paths for destruct mode + video_map = fetch_all_metadata() if destruct_mode else {} + + for filepath in filepaths: + p = Path(filepath) + if not p.exists(): + results.append({"path": filepath, "success": False, "error": "File not found"}) + continue + + try: + vid_id = extract_id_from_filename(p.name) + + # DESTRUCT MODE: Delete source too + if destruct_mode and vid_id: + meta = video_map.get(vid_id) + if meta and meta.get('filesystem_path'): + source_path = Path(meta['filesystem_path']) + if source_path.exists(): + source_path.unlink() + log(f" [DESTRUCT] Deleted source: {source_path}") + + # Also check lost_media table + with get_db() as conn: + conn.execute("DELETE FROM lost_media WHERE video_id = ?", (vid_id,)) + conn.commit() + + # DELETE TARGET (Symlink/Resource) + p.unlink() + + # Clean up empty parent folder if it's a video folder + parent = p.parent + if parent not in [TARGET_DIR, HIDDEN_DIR, SOURCE_DIR] and parent.name != "source": + try: + if not any(parent.iterdir()): + parent.rmdir() + log(f" [CLEANUP] Removed empty dir: {parent}") + except: + pass + + results.append({"path": filepath, "success": True}) + except Exception as e: + results.append({"path": filepath, "success": False, "error": str(e)}) + log(f"❌ Failed to delete {filepath}: {e}") + + return jsonify({ + "results": results, + "success_count": len([r for r in results if r["success"]]), + "fail_count": len([r for r in results if not r["success"]]) + }) + @app.route("/api/recovery/delete", methods=["POST"]) @requires_auth def api_recovery_delete(): diff --git a/ui/src/components/RecoveryModal.svelte b/ui/src/components/RecoveryModal.svelte index 23a975d..6203a62 100644 --- a/ui/src/components/RecoveryModal.svelte +++ b/ui/src/components/RecoveryModal.svelte @@ -7,6 +7,10 @@ let status = "idle"; let results: any = { unindexed: [], rescue: [], redundant: [], lost: [] }; let pollInterval: ReturnType; + + // New State for Destruct Mode & Multi-selection + let destructMode = false; + let selectedPaths = new Set(); async function startScan() { scanning = true; @@ -48,7 +52,6 @@ }); async function recoverFile(path: string, isBatch = false) { - // Implementation mirrors existing JS logic if (!isBatch && !confirm("Recover this file?")) return; try { const res = await fetch("/api/recovery/start", { @@ -60,29 +63,89 @@ if (!isBatch) { alert(d.message); startScan(); - } // Refresh + } } catch (e) { alert(e); } } async function deleteFile(path: string) { - if (!confirm("Delete file? This cannot be undone.")) return; + let msg = "Delete file? This cannot be undone."; + if (destructMode) { + msg = "☢️ DESTRUCT MODE ACTIVE ☢️\n\nThis will delete BOTH the target folder AND the source file in your archive.\n\nAre you absolutely sure?"; + } + + if (!confirm(msg)) return; + + if (destructMode && !confirm("FINAL WARNING: This is IRREVERSIBLE. Delete source file now?")) return; + try { - const res = await fetch("/api/recovery/delete", { + const res = await fetch("/api/recovery/delete-batch", { method: "POST", - body: JSON.stringify({ filepath: path }), + body: JSON.stringify({ + filepaths: [path], + destruct_mode: destructMode + }), headers: { "Content-Type": "application/json" }, }); const d = await res.json(); - if (d.success) { + if (d.success_count > 0) { alert("Deleted."); startScan(); - } else alert("Error: " + d.error); + } else alert("Error deleting file."); } catch (e) { alert(e); } } + + async function deleteSelected() { + if (selectedPaths.size === 0) return; + + let msg = `Delete ${selectedPaths.size} selected items?`; + if (destructMode) { + msg = `☢️ DESTRUCT MODE ACTIVE ☢️\n\nYou are about to delete ${selectedPaths.size} items from BOTH Target and Source.\n\nAre you sure you want to proceed?`; + } + + if (!confirm(msg)) return; + if (destructMode && !confirm("FINAL WARNING: This will permanently delete SOURCE FILES. Continue?")) return; + + try { + const res = await fetch("/api/recovery/delete-batch", { + method: "POST", + body: JSON.stringify({ + filepaths: Array.from(selectedPaths), + destruct_mode: destructMode + }), + headers: { "Content-Type": "application/json" }, + }); + const d = await res.json(); + alert(`Batch complete. Success: ${d.success_count}, Failed: ${d.fail_count}`); + selectedPaths = new Set(); + startScan(); + } catch (e) { + alert("Batch delete failed: " + e); + } + } + + function toggleSelect(path: string) { + if (selectedPaths.has(path)) { + selectedPaths.delete(path); + } else { + selectedPaths.add(path); + } + selectedPaths = selectedPaths; // Trigger Svelte reactivity + } + + function toggleAll() { + const items = results[activeTab] || []; + if (selectedPaths.size === items.length && items.length > 0) { + selectedPaths = new Set(); + } else { + selectedPaths = new Set(items.map((i: any) => i.path)); + } + } + + $: allSelected = results[activeTab]?.length > 0 && selectedPaths.size === results[activeTab]?.length;

Advanced Recovery + {#if destructMode} + DESTRUCT MODE ACTIVE + {/if}

-
- -
Status: {status}
+
+
+ + + {#if selectedPaths.size > 0} + + {/if} +
+ +
+ + + +
Status: {status}
+
@@ -125,7 +215,7 @@ {activeTab === tab ? 'border-neon-cyan text-neon-cyan' : 'border-transparent text-gray-500 hover:text-gray-300'}" - on:click={() => (activeTab = tab as any)} + on:click={() => { activeTab = tab as any; selectedPaths = new Set(); }} > {tab} {#if scanning && (!results[activeTab] || results[activeTab].length === 0)}
- Scanning... +
+
SYSTEM WIDE SCAN IN PROGRESS...
{:else} - - +
+ + @@ -156,15 +255,25 @@ {#each results[activeTab] || [] as item} - + + {item.filename || item.path} + {/each} {#if !results[activeTab]?.length} + + Nothing detected in this category. + {/if} @@ -221,3 +335,9 @@ + +
+ + Video ID Filename / Path Size/Info
+ toggleSelect(item.path)} + class="accent-neon-cyan" + > + {item.video_id} {item.filename || item.path} {item.size_mb @@ -172,46 +281,51 @@ : item.ta_source || "-"} - {#if activeTab === "unindexed"} - - {:else if activeTab === "redundant"} - - {:else if activeTab === "lost"} - - - {:else} - - {/if} +
+ {#if activeTab === "unindexed"} + + {:else if activeTab === "redundant"} + + {:else if activeTab === "lost"} + + + {:else} + + {/if} +
No items found.