Fix library filter, compact dashboard/credentials UI, add fuzzy audio dedup
- beets_service: filter params now use truthy checks instead of `is not None`, since a real <select> left on "(any)" submits an empty string, not an absent param -- the filter form silently matched zero rows for any real submission. Also match grouping tokens within "; "-joined multi-playlist values instead of requiring an exact string match. - dashboard: credential status renders as compact dot indicators instead of badge+text chips, so long scope names (telegram) stay on one line. - credentials page: two-column grid layout instead of one long column. - find-fuzzy-dupes.py: add --json/--only-paths flags matching dedup-library.sh's convention, so it can plug into the same review queue. - dedup_review_service: add scan_fuzzy() and route confirm_and_apply() to the correct underlying script (dedup-library.sh vs find-fuzzy-dupes.py) per candidate's pass_name, since tag-based passes miss duplicates whose tags differ even when the audio is identical (e.g. a remix credited to different artists between two copies). - dedup page: add a "Scan for audio duplicates" trigger alongside the existing tag-based scan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -141,8 +141,20 @@ def main() -> int:
|
||||
ap.add_argument("--duration-tol", type=int, default=DURATION_TOLERANCE)
|
||||
ap.add_argument("--no-cache", action="store_true",
|
||||
help="ignore the scan cache and re-fingerprint everything")
|
||||
ap.add_argument("--json", action="store_true",
|
||||
help="additionally emit one JSON line per candidate deletion to stdout, "
|
||||
"for dedup_review_service to parse -- same convention as "
|
||||
"dedup-library.sh --json. Purely additive.")
|
||||
ap.add_argument("--only-paths", metavar="FILE",
|
||||
help="in --apply mode, only delete a candidate if its path is listed "
|
||||
"(one per line) in FILE -- same convention as dedup-library.sh.")
|
||||
args = ap.parse_args()
|
||||
|
||||
only_paths = None
|
||||
if args.only_paths:
|
||||
with open(args.only_paths) as f:
|
||||
only_paths = {line.strip() for line in f if line.strip()}
|
||||
|
||||
if not os.path.exists(FINGERPRINT_DB):
|
||||
print(f"ERROR: fingerprint index missing at {FINGERPRINT_DB}", file=sys.stderr)
|
||||
print("Run build-fingerprint-index.py first.", file=sys.stderr)
|
||||
@@ -258,6 +270,14 @@ def main() -> int:
|
||||
continue
|
||||
print(f" DELETE [{loser_ext} {loser_size / 1024 / 1024:.1f}M sim={score:.3f}] {loser_path}")
|
||||
delete_targets.append((beets_id, loser_path))
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"pass": "fuzzy_audio",
|
||||
"keep_path": keeper_path,
|
||||
"delete_path": loser_path,
|
||||
"delete_size_bytes": loser_size,
|
||||
"similarity": round(score, 3),
|
||||
}))
|
||||
print()
|
||||
if skipped_transitive:
|
||||
print(f"[fuzzy-dupes] skipped {skipped_transitive} transitive false-positives "
|
||||
@@ -269,7 +289,12 @@ def main() -> int:
|
||||
|
||||
print(f"\n[fuzzy-dupes] APPLY: deleting {len(delete_targets)} files via beet remove -d -f")
|
||||
failed = 0
|
||||
skipped_unconfirmed = 0
|
||||
for beets_id, path in delete_targets:
|
||||
if only_paths is not None and path not in only_paths:
|
||||
print(f" SKIP (not in --only-paths confirm list) {path}")
|
||||
skipped_unconfirmed += 1
|
||||
continue
|
||||
# Escape regex metacharacters for path:: regex query
|
||||
escaped = re.escape(path)
|
||||
result = subprocess.run(
|
||||
@@ -279,7 +304,8 @@ def main() -> int:
|
||||
if result.returncode != 0:
|
||||
print(f" FAILED: {path} — {result.stderr.strip()}", file=sys.stderr)
|
||||
failed += 1
|
||||
print(f"[fuzzy-dupes] done. {len(delete_targets) - failed} deleted, {failed} failed.")
|
||||
deleted = len(delete_targets) - failed - skipped_unconfirmed
|
||||
print(f"[fuzzy-dupes] done. {deleted} deleted, {failed} failed, {skipped_unconfirmed} skipped (unconfirmed).")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user