0.6.12: Redesign the dedup review table for faster, more accurate scanning
The keep/delete columns were raw paths in a fixed-width cell: ellipsis- truncated, full text only on hover. Slow to scan (mousing over every row to read the song name) and, once "fixed" with a tag-derived title/ artist header in a first pass of this change, actively worse for the thing dedup review actually needs -- confirming two files are really the same recording. Tags can be wrong; the filename on disk can't. New layout: each candidate gets a title row (song title, large, unclipped -- parsed from beets' known singleton path template, Artist/Album/Title.ext) followed by a compact detail row. The detail row shows a color-coded format pill (FLAC/MP3/etc, matching the same quality judgment dedup-library.sh's rank_file() already makes) and the full relative file path, always rendered as visible text -- never hover-only -- so a real difference in filename, album, or folder is still plainly visible even when tags line up. Shared artist collapses onto the title row so it isn't repeated per side; a colored left rail marks which side survives without having to read the words. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+68
-5
@@ -1,3 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@@ -30,16 +33,76 @@ def _display_path(path: str) -> str:
|
||||
return path[len(_LIBRARY_PREFIX):] if path.startswith(_LIBRARY_PREFIX) else path
|
||||
|
||||
|
||||
# Compilation-style import paths carry a "$track " prefix on the filename
|
||||
# (see pipeline/configs/beets config.yaml paths: comp/albumtype_soundtrack);
|
||||
# singleton paths (the vast majority of this library) don't. Strip it either
|
||||
# way so the title reads clean.
|
||||
_TRACK_PREFIX_RE = re.compile(r"^\d{1,3}[\s.\-]+")
|
||||
|
||||
# rank_file() in dedup-library.sh always ranks FLAC above every other format
|
||||
# and WAV below MP3 (a download glitch, never desirable) -- mirror that
|
||||
# judgment in the format pill's color so it reads as "this one's the keeper"
|
||||
# at a glance, not just a bare extension string.
|
||||
_FORMAT_BADGE = {"flac": "badge-success", "wav": "badge-warning"}
|
||||
|
||||
|
||||
def _split_display(path: str) -> dict:
|
||||
"""Break a library display path into (artist, album, title, ext).
|
||||
|
||||
Beets' singleton path template is always
|
||||
%the{$albumartist}/$album/$title (pipeline/configs/beets config.yaml,
|
||||
paths:) -- every track in this library lands at exactly that shape, so
|
||||
the last two segments are reliably album/filename. A path that doesn't
|
||||
fit (an unexpected root-level file) degrades to showing the raw display
|
||||
path as the title instead of guessing at structure that isn't there.
|
||||
"""
|
||||
display = _display_path(path)
|
||||
parts = display.split("/")
|
||||
if len(parts) >= 2:
|
||||
artist, album, filename = parts[0], parts[-2], parts[-1]
|
||||
title, ext = os.path.splitext(filename)
|
||||
title = _TRACK_PREFIX_RE.sub("", title)
|
||||
else:
|
||||
artist, album = "", ""
|
||||
title, ext = os.path.splitext(display)
|
||||
return {
|
||||
"path": path,
|
||||
"display": display,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"title": title,
|
||||
"ext": ext.lstrip(".").lower(),
|
||||
}
|
||||
|
||||
|
||||
def _file_size(path: str) -> int | None:
|
||||
try:
|
||||
return os.path.getsize(path)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _to_row(c) -> dict:
|
||||
keep = _split_display(c.keep_path)
|
||||
delete = _split_display(c.delete_path)
|
||||
keep["size_bytes"] = _file_size(c.keep_path)
|
||||
keep["badge"] = _FORMAT_BADGE.get(keep["ext"], "badge-muted")
|
||||
delete["size_bytes"] = c.delete_size_bytes if c.delete_size_bytes is not None else _file_size(c.delete_path)
|
||||
delete["badge"] = _FORMAT_BADGE.get(delete["ext"], "badge-muted")
|
||||
return {
|
||||
"id": c.id,
|
||||
"pass_name": c.pass_name,
|
||||
"pass_label": _pass_label(c.pass_name),
|
||||
"keep_path": c.keep_path,
|
||||
"delete_path": c.delete_path,
|
||||
"keep_display": _display_path(c.keep_path),
|
||||
"delete_display": _display_path(c.delete_path),
|
||||
"delete_size_bytes": c.delete_size_bytes,
|
||||
"keep": keep,
|
||||
"delete": delete,
|
||||
# Same-tag passes (numbered_sibling/case_insensitive/normalized) share
|
||||
# artist/title almost always; cross_album_fuzzy and fuzzy_audio can
|
||||
# legitimately differ (different credit, different album entirely) --
|
||||
# the template only collapses shared context onto one line when it's
|
||||
# actually shared, otherwise shows both sides in full.
|
||||
"same_title": keep["title"].lower() == delete["title"].lower(),
|
||||
"same_artist": keep["artist"].lower() == delete["artist"].lower(),
|
||||
"same_album": keep["album"].lower() == delete["album"].lower(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user