Optimize Recovery Scan: Implement Async Backend & UI Polling for large libraries
This commit is contained in:
parent
55fb0447eb
commit
2f5017d13b
2 changed files with 118 additions and 55 deletions
|
|
@ -902,11 +902,45 @@ def api_transcode_logs():
|
||||||
"next_index": len(transcode_log_buffer)
|
"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"])
|
@app.route("/api/recovery/scan", methods=["POST"])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
def api_recovery_scan():
|
def api_recovery_scan():
|
||||||
files = scan_for_unindexed_videos()
|
global SCAN_THREAD
|
||||||
return jsonify({"files": files, "count": len(files)})
|
|
||||||
|
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"])
|
@app.route("/api/recovery/start", methods=["POST"])
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
|
|
||||||
|
|
@ -423,8 +423,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scanRecoveryFiles() {
|
async function scanRecoveryFiles() {
|
||||||
// Loading state for all tabs
|
const loadingRow = '<tr><td colspan="4" class="text-center"><div class="spinner-border text-primary" role="status"></div> Scanning in background... (This may take a minute)</td></tr>';
|
||||||
const loadingRow = '<tr><td colspan="4" class="text-center"><div class="spinner-border text-primary" role="status"></div> Scanning...</td></tr>';
|
|
||||||
const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
|
const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
|
||||||
ids.forEach(id => {
|
ids.forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
|
|
@ -432,9 +431,44 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/recovery/scan', { method: 'POST' });
|
// 1. Kick off the scan
|
||||||
const data = await res.json();
|
await fetch('/api/recovery/scan', { method: 'POST' });
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (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 = `<tr><td colspan="4" class="text-center text-muted">${msg}</td></tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(data) {
|
||||||
// Helper to render rows
|
// Helper to render rows
|
||||||
const renderRow = (f, type) => {
|
const renderRow = (f, type) => {
|
||||||
const cleanPath = f.path.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
const cleanPath = f.path.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||||
|
|
@ -464,29 +498,22 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear & Fill
|
const ids = ['tbody-unindexed', 'tbody-rescue', 'tbody-redundant'];
|
||||||
ids.forEach(id => {
|
ids.forEach(id => document.getElementById(id).innerHTML = '');
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) el.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update Badges
|
// Update Badges
|
||||||
document.getElementById('badge-unindexed').innerText = data.files.unindexed.length;
|
document.getElementById('badge-unindexed').innerText = data.unindexed.length;
|
||||||
document.getElementById('badge-rescue').innerText = data.files.rescue.length;
|
document.getElementById('badge-rescue').innerText = data.rescue.length;
|
||||||
document.getElementById('badge-redundant').innerText = data.files.redundant.length;
|
document.getElementById('badge-redundant').innerText = data.redundant.length;
|
||||||
|
|
||||||
// Populate Tables
|
// Populate
|
||||||
data.files.unindexed.forEach(f => document.getElementById('tbody-unindexed').innerHTML += renderRow(f, 'unindexed'));
|
data.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.rescue.forEach(f => document.getElementById('tbody-rescue').innerHTML += renderRow(f, 'rescue'));
|
||||||
data.files.redundant.forEach(f => document.getElementById('tbody-redundant').innerHTML += renderRow(f, 'redundant'));
|
data.redundant.forEach(f => document.getElementById('tbody-redundant').innerHTML += renderRow(f, 'redundant'));
|
||||||
|
|
||||||
if (data.files.unindexed.length === 0) document.getElementById('tbody-unindexed').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No unindexed files found.</td></tr>';
|
if (data.unindexed.length === 0) document.getElementById('tbody-unindexed').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No unindexed files found.</td></tr>';
|
||||||
if (data.files.rescue.length === 0) document.getElementById('tbody-rescue').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No rescue candidates found.</td></tr>';
|
if (data.rescue.length === 0) document.getElementById('tbody-rescue').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No rescue candidates found.</td></tr>';
|
||||||
if (data.files.redundant.length === 0) document.getElementById('tbody-redundant').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No duplicates found.</td></tr>';
|
if (data.redundant.length === 0) document.getElementById('tbody-redundant').innerHTML = '<tr><td colspan="4" class="text-center text-muted">No duplicates found.</td></tr>';
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
alert("Scan failed: " + e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRecovery(filepath) {
|
async function startRecovery(filepath) {
|
||||||
|
|
@ -513,7 +540,9 @@
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert("File deleted.");
|
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 {
|
} else {
|
||||||
alert("Error: " + data.error);
|
alert("Error: " + data.error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue