Fix stuck dedup candidate, enrich timeout/starvation, and qobuz status error
1. enrich-buy-url.py re-queried every no-match track on every daily run (external, rate-limited lookups over the whole backlog), which blew past its 30-min timeout. Cache "tried, no match" with a BUY_URL_TRIED marker tag and skip those for 30 days (--force still re-checks). This is what made "Look up buy links" fail with TIMEOUT after 1800s. 2. Move maintenance:enrich_buy_url to run last (09:45) in the 9am block, after the fingerprint index (09:25) and status report (09:30). enrich starting at 09:10 and holding the shared lock is what left "Rebuild fingerprint index" skipped_lock. 3. pipeline-status.sh: guard the qobuz/app_id read with a file-exists check. `< missing 2>/dev/null` still leaks the shell's redirection error (opened before 2>/dev/null applies), so the daily report logged "qobuz/app_id: No such file or directory" every run. 4. dedup confirm_and_apply: a candidate whose delete target no longer exists (already removed by an earlier dedup/upgrade/hand) was filtered out and the apply silently no-op'd, leaving it stuck in the queue with no way to delete or clear it (the 100 gecs case). Now mark such candidates applied so a Delete click clears them. Covered by tests/test_dedup_review.py. Tests: 46 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""confirm_and_apply: a candidate whose delete target is already gone must be
|
||||
marked applied (cleared from the queue), not silently no-op'd and left stuck.
|
||||
This is the 'can't delete this one' case that stranded a stale fuzzy candidate."""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from app.db import SessionLocal
|
||||
from app.models import DedupCandidate, DedupRun
|
||||
from app.services import dedup_review_service as d
|
||||
|
||||
|
||||
def _make_candidate(keep_path: str, delete_path: str) -> int:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
run = DedupRun(started_at=time.time(), mode="dry_run")
|
||||
db.add(run)
|
||||
db.commit()
|
||||
db.refresh(run)
|
||||
c = DedupCandidate(
|
||||
dedup_run_id=run.id, pass_name="fuzzy_audio",
|
||||
keep_path=keep_path, delete_path=delete_path, delete_id=None,
|
||||
)
|
||||
db.add(c)
|
||||
db.commit()
|
||||
db.refresh(c)
|
||||
return c.id
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_already_gone_delete_path_is_resolved(tmp_path):
|
||||
keep = tmp_path / "keep.flac"
|
||||
keep.write_bytes(b"x") # keep still exists
|
||||
gone = tmp_path / "gone.mp3" # delete target already removed
|
||||
cid = _make_candidate(str(keep), str(gone))
|
||||
|
||||
# No actionable file to delete -> no subprocess/lock; the async call resolves
|
||||
# the stale candidate directly.
|
||||
result = asyncio.run(d.confirm_and_apply([cid], "tester"))
|
||||
|
||||
assert result is not None # a run row was produced (not a silent no-op)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
c = db.get(DedupCandidate, cid)
|
||||
assert c.applied is True # cleared from the pending queue
|
||||
finally:
|
||||
db.close()
|
||||
# and it no longer appears in the pending list
|
||||
assert cid not in [c.id for c in d.list_pending_candidates()]
|
||||
|
||||
|
||||
def test_ignore_then_unignore(tmp_path):
|
||||
cid = _make_candidate(str(tmp_path / "a.flac"), str(tmp_path / "b.mp3"))
|
||||
assert d.ignore_candidate(cid, "tester").ignored is True
|
||||
assert cid not in [c.id for c in d.list_pending_candidates()]
|
||||
assert d.unignore_candidate(cid).ignored is False
|
||||
assert cid in [c.id for c in d.list_pending_candidates()]
|
||||
Reference in New Issue
Block a user