068e7e9534
scrub-watermark-text.py crashed on every run (missing import os). strip-watermark-art.py was starved every Sunday by lock contention from strip-mb-tags' growing runtime -- moved it to 01:00 so the rest of the weekly chain has room. enrich-buy-url.py's summary line counted matches found, not successful writes, so a run where every metaflac write failed (root-owned files) still reported "N tagged". pipeline-status.sh now tells a 403 from Qobuz (Akamai/CDN block) apart from a 401 (actually expired token) -- re-exporting the token does nothing for the former. Also fixes the dedup review table's "Caught by" badge overflowing into the Keep column's format pill.
130 lines
5.5 KiB
HTML
130 lines
5.5 KiB
HTML
{% 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-caughtby {{ '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>
|
|
<h1>Dedup review</h1>
|
|
<p class="subtitle muted">Scheduled runs are dry-run only, every day. Nothing is deleted without a confirm below.</p>
|
|
</div>
|
|
<div class="actions">
|
|
<form method="post" action="/dedup/scan" class="inline">
|
|
<button type="submit" class="btn btn-primary" title="Checks both filenames/tags and acoustic fingerprints">Scan now</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% if request.query_params.get('started') %}
|
|
<div class="notice">Scan started. It runs in the background; reload this page in a bit to see new candidates, or watch its progress under <a href="/settings/jobs">Jobs</a>.</div>
|
|
{% endif %}
|
|
|
|
<h2>Pending candidates</h2>
|
|
<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:10.75rem"><col style="width:37.5%">
|
|
<col style="width:37.5%"><col style="width:10rem">
|
|
</colgroup>
|
|
<thead>
|
|
<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>
|
|
<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>
|
|
</tbody>
|
|
{% endfor %}
|
|
</table>
|
|
</div>
|
|
{% if candidates %}
|
|
<button type="submit" form="bulk-delete-form" class="btn btn-danger">Delete selected</button>
|
|
{% endif %}
|
|
|
|
{% if ignored %}
|
|
<h2>Kept both</h2>
|
|
<p class="muted" style="margin-top:-0.5rem;">Marked as legitimate alt copies -- future scans skip these pairs.</p>
|
|
<div class="table-wrap scroll">
|
|
<table class="dedup-table">
|
|
<colgroup>
|
|
<col style="width:10.75rem"><col style="width:39.25%">
|
|
<col style="width:39.25%"><col style="width:7rem">
|
|
</colgroup>
|
|
<thead>
|
|
<tr><th>Caught by</th><th>Keep</th><th>Delete</th><th></th></tr>
|
|
</thead>
|
|
{% for c in ignored %}
|
|
{{ candidate_rows(c, 'ignored') }}
|
|
{% endfor %}
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|