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:
andrew
2026-07-08 16:25:52 -06:00
parent c87ddc2649
commit 02744f6654
8 changed files with 217 additions and 85 deletions
+63 -33
View File
@@ -34,6 +34,40 @@ def _row_to_dict(row: sqlite3.Row) -> dict:
return d
def _grouping_clause(grouping: str) -> tuple[str, list]:
"""grouping is a single tag OR a "; "-joined set (e.g. "techno; djstuff"
for a track that's in two playlists' libraries). Matching only the exact
combined string would silently exclude combo-tagged tracks from a
single-tag filter -- match the token in any position instead."""
return (
"(grouping = ? OR grouping LIKE ? OR grouping LIKE ? OR grouping LIKE ?)",
[grouping, f"{grouping}; %", f"%; {grouping}", f"%; {grouping}; %"],
)
def _build_where(grouping: str | None, search: str | None, format: str | None) -> tuple[str, list]:
where = []
params: list = []
# Truthy checks, not `is not None`: a <select> filter left on its
# "(any)" option submits an EMPTY STRING, not an absent param -- every
# real form submission includes it. `is not None` treated that as "filter
# to grouping/format == ''", which matches nothing and made every filter
# combination except an all-fields-filled one look completely broken.
if grouping:
clause, clause_params = _grouping_clause(grouping)
where.append(clause)
params.extend(clause_params)
if format:
where.append("format = ?")
params.append(format)
if search:
where.append("(artist LIKE ? OR title LIKE ? OR albumartist LIKE ?)")
like = f"%{search}%"
params.extend([like, like, like])
where_sql = f"WHERE {' AND '.join(where)}" if where else ""
return where_sql, params
def query_items(
grouping: str | None = None,
search: str | None = None,
@@ -48,23 +82,11 @@ def query_items(
if not db_exists():
return []
cols = ", ".join(_ITEM_COLUMNS)
where = []
params: list = []
if grouping is not None:
where.append("grouping = ?")
params.append(grouping)
if format is not None:
where.append("format = ?")
params.append(format)
if search:
where.append("(artist LIKE ? OR title LIKE ? OR albumartist LIKE ?)")
like = f"%{search}%"
params.extend([like, like, like])
where_sql = f"WHERE {' AND '.join(where)}" if where else ""
where_sql, params = _build_where(grouping, search, format)
limit_sql = ""
if limit is not None:
limit_sql = "LIMIT ? OFFSET ?"
params.extend([limit, offset])
params = params + [limit, offset]
conn = _connect()
try:
@@ -81,20 +103,7 @@ def count_items(grouping: str | None = None, search: str | None = None, format:
"""Matching row count for query_items()'s filters -- for pagination."""
if not db_exists():
return 0
where = []
params: list = []
if grouping is not None:
where.append("grouping = ?")
params.append(grouping)
if format is not None:
where.append("format = ?")
params.append(format)
if search:
where.append("(artist LIKE ? OR title LIKE ? OR albumartist LIKE ?)")
like = f"%{search}%"
params.extend([like, like, like])
where_sql = f"WHERE {' AND '.join(where)}" if where else ""
where_sql, params = _build_where(grouping, search, format)
conn = _connect()
try:
return conn.execute(f"SELECT COUNT(*) FROM items {where_sql}", params).fetchone()[0]
@@ -130,10 +139,6 @@ def stats() -> dict:
try:
total = conn.execute("SELECT COUNT(*) FROM items").fetchone()[0]
grouping_rows = conn.execute(
"SELECT DISTINCT grouping FROM items WHERE grouping IS NOT NULL AND grouping != ''"
).fetchall()
format_rows = conn.execute(
"SELECT format, COUNT(*) as n FROM items "
"WHERE format IS NOT NULL AND format != '' "
@@ -147,7 +152,7 @@ def stats() -> dict:
return {
"db_exists": True,
"total_tracks": total,
"groupings": sorted(r[0] for r in grouping_rows),
"groupings": distinct_groupings(conn),
"formats": [{"format": r["format"], "count": r["n"]} for r in format_rows],
"total_bytes": int(total_bytes),
}
@@ -155,6 +160,31 @@ def stats() -> dict:
conn.close()
def distinct_groupings(conn: sqlite3.Connection | None = None) -> list[str]:
"""Individual grouping tokens for the library page's filter dropdown --
split on "; " so a combo value like "techno; djstuff" contributes
"techno" and "djstuff" separately rather than cluttering the dropdown
with every raw combination that happens to exist in the DB."""
if not db_exists():
return []
owns_conn = conn is None
conn = conn or _connect()
try:
rows = conn.execute(
"SELECT DISTINCT grouping FROM items WHERE grouping IS NOT NULL AND grouping != ''"
).fetchall()
tokens = set()
for row in rows:
for part in row[0].split(";"):
part = part.strip()
if part:
tokens.add(part)
return sorted(tokens)
finally:
if owns_conn:
conn.close()
def distinct_formats() -> list[str]:
"""For the library page's format filter dropdown."""
if not db_exists():