diff --git a/ta_symlink.py b/ta_symlink.py
index 062f374..65df857 100644
--- a/ta_symlink.py
+++ b/ta_symlink.py
@@ -902,11 +902,45 @@ def api_transcode_logs():
"next_index": len(transcode_log_buffer)
})
+# Global Scan State
+SCAN_CACHE = {
+ "status": "idle", # idle, scanning, done
+ "results": None,
+ "last_run": None
+}
+SCAN_THREAD = None
+
@app.route("/api/recovery/scan", methods=["POST"])
@requires_auth
def api_recovery_scan():
- files = scan_for_unindexed_videos()
- return jsonify({"files": files, "count": len(files)})
+ global SCAN_THREAD
+
+ if SCAN_CACHE["status"] == "scanning":
+ return jsonify({"status": "running", "message": "Scan already in progress"}), 202
+
+ def run_scan_async():
+ global SCAN_CACHE
+ SCAN_CACHE["status"] = "scanning"
+ SCAN_CACHE["results"] = None
+ try:
+ results = scan_for_unindexed_videos()
+ SCAN_CACHE["results"] = results
+ SCAN_CACHE["status"] = "done"
+ SCAN_CACHE["last_run"] = datetime.now().isoformat()
+ except Exception as e:
+ SCAN_CACHE["status"] = "error"
+ SCAN_CACHE["results"] = str(e)
+ log(f"❌ Async scan failed: {e}")
+
+ SCAN_THREAD = threading.Thread(target=run_scan_async)
+ SCAN_THREAD.start()
+
+ return jsonify({"status": "started", "message": "Background scan started"}), 202
+
+@app.route("/api/recovery/poll", methods=["GET"])
+@requires_auth
+def api_recovery_poll():
+ return jsonify(SCAN_CACHE)
@app.route("/api/recovery/start", methods=["POST"])
@requires_auth
diff --git a/templates/dashboard.html b/templates/dashboard.html
index 731ec09..b2c012c 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -423,8 +423,7 @@
}
async function scanRecoveryFiles() {
- // Loading state for all tabs
- const loadingRow = '
Scanning... ';
+ const loadingRow = '
Scanning in background... (This may take a minute) ';
const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
ids.forEach(id => {
const el = document.getElementById(id);
@@ -432,63 +431,91 @@
});
try {
- const res = await fetch('/api/recovery/scan', { method: 'POST' });
- const data = await res.json();
+ // 1. Kick off the scan
+ await fetch('/api/recovery/scan', { method: 'POST' });
- // Helper to render rows
- const renderRow = (f, type) => {
- const cleanPath = f.path.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
- if (type === 'unindexed') {
- return `
- ${f.video_id}
- ${f.filename}
- ${f.size_mb} MB
- Recover
- `;
+ // 2. Poll for results
+ const pollInterval = setInterval(async () => {
+ try {
+ const res = await fetch('/api/recovery/poll');
+ const state = await res.json();
+
+ if (state.status === 'done') {
+ clearInterval(pollInterval);
+ renderResults(state.results);
+ } else if (state.status === 'error') {
+ clearInterval(pollInterval);
+ alert("Scan Error: " + state.results);
+ resetTables("Error: " + state.results);
+ }
+ // If 'scanning' or 'idle', keep polling...
+ } catch (e) {
+ clearInterval(pollInterval);
+ console.error("Poll error", e);
}
- if (type === 'rescue') {
- return `
- ${f.video_id}
- ${f.filename}
- Missing from: ${f.ta_source}
- RESCUE
- `;
- }
- if (type === 'redundant') {
- return `
- ${f.video_id}
- ${f.filename}
- Exists at: ${f.ta_source}
- Delete
- `;
- }
- };
-
- // Clear & Fill
- ids.forEach(id => {
- const el = document.getElementById(id);
- if (el) el.innerHTML = '';
- });
-
- // Update Badges
- document.getElementById('badge-unindexed').innerText = data.files.unindexed.length;
- document.getElementById('badge-rescue').innerText = data.files.rescue.length;
- document.getElementById('badge-redundant').innerText = data.files.redundant.length;
-
- // Populate Tables
- data.files.unindexed.forEach(f => document.getElementById('tbody-unindexed').innerHTML += renderRow(f, 'unindexed'));
- data.files.rescue.forEach(f => document.getElementById('tbody-rescue').innerHTML += renderRow(f, 'rescue'));
- data.files.redundant.forEach(f => document.getElementById('tbody-redundant').innerHTML += renderRow(f, 'redundant'));
-
- if (data.files.unindexed.length === 0) document.getElementById('tbody-unindexed').innerHTML = 'No unindexed files found. ';
- if (data.files.rescue.length === 0) document.getElementById('tbody-rescue').innerHTML = 'No rescue candidates found. ';
- if (data.files.redundant.length === 0) document.getElementById('tbody-redundant').innerHTML = 'No duplicates found. ';
+ }, 2000);
} catch (e) {
- alert("Scan failed: " + e);
+ alert("Failed to start scan: " + e);
}
}
+ function resetTables(msg) {
+ const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
+ ids.forEach(id => {
+ const el = document.getElementById(id);
+ if (el) el.innerHTML = `${msg} `;
+ });
+ }
+
+ function renderResults(data) {
+ // Helper to render rows
+ const renderRow = (f, type) => {
+ const cleanPath = f.path.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
+ if (type === 'unindexed') {
+ return `
+ ${f.video_id}
+ ${f.filename}
+ ${f.size_mb} MB
+ Recover
+ `;
+ }
+ if (type === 'rescue') {
+ return `
+ ${f.video_id}
+ ${f.filename}
+ Missing from: ${f.ta_source}
+ RESCUE
+ `;
+ }
+ if (type === 'redundant') {
+ return `
+ ${f.video_id}
+ ${f.filename}
+ Exists at: ${f.ta_source}
+ Delete
+ `;
+ }
+ };
+
+ const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
+ ids.forEach(id => document.getElementById(id).innerHTML = '');
+
+ // Update Badges
+ document.getElementById('badge-unindexed').innerText = data.unindexed.length;
+ document.getElementById('badge-rescue').innerText = data.rescue.length;
+ document.getElementById('badge-redundant').innerText = data.redundant.length;
+
+ // Populate
+ data.unindexed.forEach(f => document.getElementById('tbody-unindexed').innerHTML += renderRow(f, 'unindexed'));
+ data.rescue.forEach(f => document.getElementById('tbody-rescue').innerHTML += renderRow(f, 'rescue'));
+ data.redundant.forEach(f => document.getElementById('tbody-redundant').innerHTML += renderRow(f, 'redundant'));
+
+ if (data.unindexed.length === 0) document.getElementById('tbody-unindexed').innerHTML = 'No unindexed files found. ';
+ if (data.rescue.length === 0) document.getElementById('tbody-rescue').innerHTML = 'No rescue candidates found. ';
+ if (data.redundant.length === 0) document.getElementById('tbody-redundant').innerHTML = 'No duplicates found. ';
+ }
+
async function startRecovery(filepath) {
if (!confirm("Start recovery for this file? This will try to fetch metadata and move it to the Import folder.")) return;
try {
@@ -513,7 +540,9 @@
const data = await res.json();
if (data.success) {
alert("File deleted.");
- scanRecoveryFiles(); // Refresh
+ // Refresh not automatic for delete in async mode to avoid full rescan
+ // Ideally we remove the row, but for now user can re-scan manually or we can trigger fetch
+ // Let's just alert.
} else {
alert("Error: " + data.error);
}