The reconcile heals rows whose file is still in the library; it deliberately leaves ORPHANS — history rows whose file is gone (deleted / replaced / re-downloaded elsewhere). Those can never be healed (no file left to confirm) and linger in the Unverified list forever. This adds an explicit, user-initiated cleanup for them. - core/downloads/orphan_history.py: pure, tested rule. A row is an orphan when its file resolves nowhere; flags `suspicious` when EVERY reviewed file is unreachable (the mount-down signature) so the caller refuses rather than mass-delete a healthy log during an outage. - POST /api/verification/clean-orphans (admin-only): runs it against _resolve_history_audio_path (raw path -> prefix-swap resolver -> tracks-table title fallback), refuses on the suspicious signature, and deletes only history ROWS — never a file (the files are already gone). - UI: "🧹 Clean orphaned" button in the Unverified bulk-actions row, with a confirm dialog spelling out that it removes log rows only and refuses if the library looks offline. NEVER automatic / never at boot — a filesystem check during a mount outage would otherwise wipe good history. 5 pure-rule tests + safety-gate coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWJk7EuM7YktQeNyqQwTZY
45 lines
1.9 KiB
Python
45 lines
1.9 KiB
Python
"""Identify dead review-queue history rows whose file is gone (#934 follow-up).
|
|
|
|
The Unverified/Quarantine review queue is fed from ``library_history`` — an
|
|
append-only log that is never pruned. When a file is deleted, replaced, or
|
|
re-downloaded elsewhere, its old ``unverified`` row lingers forever and can
|
|
never be healed (there's no file left to confirm). Those are *orphans*.
|
|
|
|
This decides which rows are orphans, given a ``resolve(row) -> path | None``
|
|
the caller wires to the real filesystem lookup. Pure (no DB, no filesystem) so
|
|
the rules — including the safety gate — are unit-testable.
|
|
|
|
Safety gate: a filesystem check mass-false-positives when the library mount is
|
|
down (every file looks missing). So if EVERY reviewed file is unreachable and
|
|
there are enough rows to judge, we flag it ``suspicious`` and the caller refuses
|
|
to delete — better to clean nothing than to wipe a healthy log during an outage.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Sequence
|
|
|
|
|
|
def find_orphan_history_ids(
|
|
rows: Sequence[dict],
|
|
resolve: Callable[[dict], Any],
|
|
*,
|
|
min_for_safety: int = 5,
|
|
) -> dict:
|
|
"""Return ``{'orphan_ids', 'checked', 'suspicious'}``.
|
|
|
|
A row is an orphan when it has a non-empty ``file_path`` but ``resolve`` can
|
|
find no file for it. ``suspicious`` is True when every checked row is
|
|
missing and there are at least ``min_for_safety`` of them — the mount-down
|
|
signature; the caller should refuse to delete in that case.
|
|
"""
|
|
orphan_ids = []
|
|
checked = 0
|
|
for row in rows:
|
|
if not str((row.get('file_path') or '')).strip():
|
|
continue
|
|
checked += 1
|
|
if resolve(row) is None:
|
|
orphan_ids.append(row.get('id'))
|
|
suspicious = checked >= min_for_safety and len(orphan_ids) == checked
|
|
return {'orphan_ids': orphan_ids, 'checked': checked, 'suspicious': suspicious}
|