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:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user