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:
@@ -1,5 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dedup — alembic{% endblock %}
|
||||
|
||||
{% macro candidate_rows(c, mode) %}
|
||||
<tbody class="dedup-row-group">
|
||||
<tr class="dedup-title-row">
|
||||
<td colspan="{{ 5 if mode == 'pending' else 4 }}">
|
||||
{% if c.same_title %}
|
||||
<div class="dedup-title">{{ c.keep.title }}</div>
|
||||
{% else %}
|
||||
<div class="dedup-title">{{ c.keep.title }} <span class="dedup-title-alt">/ {{ c.delete.title }}</span></div>
|
||||
{% endif %}
|
||||
{% if c.same_artist and c.same_album and c.keep.album %}
|
||||
<div class="dedup-context muted">{{ c.keep.artist }} · {{ c.keep.album }}</div>
|
||||
{% elif c.same_artist %}
|
||||
<div class="dedup-context muted">{{ c.keep.artist }}</div>
|
||||
{% else %}
|
||||
<div class="dedup-context muted">{{ c.keep.artist }} / {{ c.delete.artist }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="dedup-detail-row">
|
||||
{% if mode == 'pending' %}
|
||||
<td><input type="checkbox" name="candidate_id" value="{{ c.id }}" form="bulk-delete-form"></td>
|
||||
{% endif %}
|
||||
<td><span class="badge {{ 'badge-warning' if c.pass_label == 'Acoustically Similar' else 'badge-info' }}" title="{{ c.pass_name }}">{{ c.pass_label }}</span></td>
|
||||
<td>
|
||||
<div class="dedup-side dedup-side-keep">
|
||||
<span class="badge {{ c.keep.badge }}">{{ c.keep.ext or '?' }}</span>
|
||||
{% if c.keep.size_bytes %}<span class="muted dedup-side-size">{{ (c.keep.size_bytes / 1024 / 1024) | round(1) }} MB</span>{% endif %}
|
||||
</div>
|
||||
<div class="dedup-side-path mono muted">{{ c.keep.display }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dedup-side dedup-side-delete">
|
||||
<span class="badge {{ c.delete.badge }}">{{ c.delete.ext or '?' }}</span>
|
||||
{% if c.delete.size_bytes %}<span class="muted dedup-side-size">{{ (c.delete.size_bytes / 1024 / 1024) | round(1) }} MB</span>{% endif %}
|
||||
</div>
|
||||
<div class="dedup-side-path mono muted">{{ c.delete.display }}</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
{% if mode == 'pending' %}
|
||||
<form method="post" action="/dedup/confirm" class="inline"
|
||||
onsubmit="return confirm('Permanently delete this file from disk?\n{{ c.delete.display }}')">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
<form method="post" action="/dedup/ignore" class="inline">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-ghost" title="Keep both copies and never flag this pair again">Keep both</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="/dedup/unignore" class="inline">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-ghost">Un-ignore</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
@@ -18,48 +78,30 @@
|
||||
{% endif %}
|
||||
|
||||
<h2>Pending candidates</h2>
|
||||
<p class="muted" style="margin-top:-0.5rem;">Paths are relative to the library root. Hover a name for the full path.</p>
|
||||
<p class="muted" style="margin-top:-0.5rem;">Song title up top so you can scan straight down the list; the actual file path is always shown underneath each side so you can check they're really the same file before confirming.</p>
|
||||
<form id="bulk-delete-form" method="post" action="/dedup/confirm"
|
||||
onsubmit="return confirm('Permanently delete every selected file from disk? This cannot be undone.')"></form>
|
||||
<div class="table-wrap scroll">
|
||||
<table class="dedup-table">
|
||||
<colgroup>
|
||||
<col style="width:2.2rem"><col style="width:9.5rem"><col style="width:28%">
|
||||
<col style="width:28%"><col style="width:5rem"><col style="width:9rem">
|
||||
<col style="width:2.2rem"><col style="width:9.5rem"><col style="width:38%">
|
||||
<col style="width:38%"><col style="width:10rem">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th></th><th>Caught by</th><th>Keep</th><th>Delete</th><th>Size</th><th>Action</th></tr>
|
||||
<tr><th></th><th>Caught by</th><th>Keep</th><th>Delete</th><th>Action</th></tr>
|
||||
</thead>
|
||||
{% for c in candidates %}
|
||||
{{ candidate_rows(c, 'pending') }}
|
||||
{% else %}
|
||||
<tbody>
|
||||
{% set pass_badge = {"File Naming": "badge-info", "Acoustically Similar": "badge-warning"} %}
|
||||
{% for c in candidates %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="candidate_id" value="{{ c.id }}" form="bulk-delete-form"></td>
|
||||
<td><span class="badge {{ pass_badge.get(c.pass_label, 'badge-info') }}" title="{{ c.pass_name }}">{{ c.pass_label }}</span></td>
|
||||
<td class="muted mono path-cell" title="{{ c.keep_path }}">{{ c.keep_display }}</td>
|
||||
<td class="mono path-cell" title="{{ c.delete_path }}">{{ c.delete_display }}</td>
|
||||
<td class="muted">{{ (c.delete_size_bytes / 1024 / 1024) | round(1) if c.delete_size_bytes else '?' }} MB</td>
|
||||
<td class="actions-cell">
|
||||
<form method="post" action="/dedup/confirm" class="inline"
|
||||
onsubmit="return confirm('Permanently delete this file from disk?\n{{ c.delete_display }}')">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
<form method="post" action="/dedup/ignore" class="inline">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-ghost" title="Keep both copies and never flag this pair again">Keep both</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="empty-row"><td colspan="6">
|
||||
<tr class="empty-row"><td colspan="5">
|
||||
<div class="empty-state">
|
||||
<span class="sparkle" style="position:relative; display:inline-block; margin-bottom:0.5rem;"></span>
|
||||
<p class="muted" style="margin:0;">No pending candidates. Run a scan.</p>
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% if candidates %}
|
||||
@@ -72,28 +114,15 @@
|
||||
<div class="table-wrap scroll">
|
||||
<table class="dedup-table">
|
||||
<colgroup>
|
||||
<col style="width:9.5rem"><col style="width:33%">
|
||||
<col style="width:33%"><col style="width:5rem"><col style="width:7rem">
|
||||
<col style="width:9.5rem"><col style="width:40%">
|
||||
<col style="width:40%"><col style="width:7rem">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th>Caught by</th><th>Keep</th><th>Delete</th><th>Size</th><th></th></tr>
|
||||
<tr><th>Caught by</th><th>Keep</th><th>Delete</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in ignored %}
|
||||
<tr>
|
||||
<td><span class="badge badge-muted" title="{{ c.pass_name }}">{{ c.pass_label }}</span></td>
|
||||
<td class="muted mono path-cell" title="{{ c.keep_path }}">{{ c.keep_display }}</td>
|
||||
<td class="muted mono path-cell" title="{{ c.delete_path }}">{{ c.delete_display }}</td>
|
||||
<td class="muted">{{ (c.delete_size_bytes / 1024 / 1024) | round(1) if c.delete_size_bytes else '?' }} MB</td>
|
||||
<td>
|
||||
<form method="post" action="/dedup/unignore" class="inline">
|
||||
<input type="hidden" name="candidate_id" value="{{ c.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-ghost">Un-ignore</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% for c in ignored %}
|
||||
{{ candidate_rows(c, 'ignored') }}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user