Fix playlist status page, dedup delete-by-path bug, duplicate scan rows; add public IP dashboard card
Playlist detail page's status section (and the new two-column compare view) was never rendering: the router awaited status_service.playlist_status(), but that function is plain sync, so awaiting its dict return raised "object dict can't be used in 'await' expression" on every load, silently caught and shown as a generic fetch error. Removed the erroneous await. find-fuzzy-dupes.py's --apply and --apply-pairs both deleted by a `path::` regex query even though the beets id was already available in scope -- same mixed-path-storage issue (pre/post beets-2.11-upgrade items store absolute vs. library-relative paths) dedup-library.sh already worked around by deleting via id instead. This silently failed to match for most confirmed fuzzy-audio deletions (verified: 19 of 20 in one run). Added a delete_id column to dedup_candidates, threaded through the scan/apply pipeline, and switched both delete call sites to `id:`. Dedup scans also never checked whether a pair was already sitting in the pending list, so every re-scan (including the daily schedule) added a new row for the same unreviewed duplicate -- cleaned up 38 redundant rows already in production and added a check so future scans skip a pair that's already pending. Dashboard gets a Public IP stat card (cached 10 min, fetched via ipify) as a quick confidence check that outbound traffic is actually routed through gluetun's VPN and not the home connection.
This commit is contained in:
@@ -41,6 +41,22 @@ def _is_pair_ignored(db, path_a: str, path_b: str) -> bool:
|
||||
return db.execute(query).first() is not None
|
||||
|
||||
|
||||
def _is_pair_already_pending(db, keep_path: str, delete_path: str) -> bool:
|
||||
"""True if this exact keep/delete pair is already sitting in the
|
||||
pending list from an earlier scan. Without this, every re-scan of a
|
||||
duplicate the user hasn't reviewed yet (e.g. the daily scheduled scan)
|
||||
added a brand-new row for the same pair, so it piled up multiple
|
||||
identical entries in the table instead of just staying as one."""
|
||||
query = select(DedupCandidate.id).where(
|
||||
DedupCandidate.applied == False, # noqa: E712
|
||||
DedupCandidate.confirmed == False, # noqa: E712
|
||||
DedupCandidate.ignored == False, # noqa: E712
|
||||
DedupCandidate.keep_path == keep_path,
|
||||
DedupCandidate.delete_path == delete_path,
|
||||
).limit(1)
|
||||
return db.execute(query).first() is not None
|
||||
|
||||
|
||||
def _parse_json_lines(output: str) -> list[dict]:
|
||||
candidates = []
|
||||
for line in output.splitlines():
|
||||
@@ -77,23 +93,34 @@ async def _run_scan(job_key: str, script_name: str, triggered_by: str) -> DedupR
|
||||
db.refresh(dedup_run)
|
||||
|
||||
skipped_ignored = 0
|
||||
skipped_duplicate = 0
|
||||
for c in candidates:
|
||||
if _is_pair_ignored(db, c["keep_path"], c["delete_path"]):
|
||||
skipped_ignored += 1
|
||||
continue
|
||||
if _is_pair_already_pending(db, c["keep_path"], c["delete_path"]):
|
||||
skipped_duplicate += 1
|
||||
continue
|
||||
db.add(
|
||||
DedupCandidate(
|
||||
dedup_run_id=dedup_run.id,
|
||||
pass_name=c.get("pass", "unknown"),
|
||||
keep_path=c["keep_path"],
|
||||
delete_path=c["delete_path"],
|
||||
delete_id=c.get("delete_id"),
|
||||
delete_size_bytes=c.get("delete_size_bytes"),
|
||||
)
|
||||
)
|
||||
# Flush (not commit) so a duplicate pair emitted twice within
|
||||
# this same scan's own output -- e.g. two passes agreeing on
|
||||
# the same file -- is caught by the next iteration's check too,
|
||||
# not just duplicates from a previous scan's committed rows.
|
||||
db.flush()
|
||||
db.commit()
|
||||
db.refresh(dedup_run)
|
||||
if skipped_ignored:
|
||||
dedup_run.kept = (dedup_run.kept or 0) + skipped_ignored
|
||||
skipped_total = skipped_ignored + skipped_duplicate
|
||||
if skipped_total:
|
||||
dedup_run.kept = (dedup_run.kept or 0) + skipped_total
|
||||
db.commit()
|
||||
db.refresh(dedup_run)
|
||||
return dedup_run
|
||||
@@ -144,7 +171,8 @@ def _write_apply_args(script_name: str, script: str, group: list[DedupCandidate]
|
||||
pairs_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
pairs_file.write_text(
|
||||
"\n".join(
|
||||
json.dumps({"keep_path": c.keep_path, "delete_path": c.delete_path}) for c in group
|
||||
json.dumps({"keep_path": c.keep_path, "delete_path": c.delete_path, "delete_id": c.delete_id})
|
||||
for c in group
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user