fix(downloads): unverified review actions always load + show real quality on completed

Unverified review actions (play/audit/approve/delete) only rendered for
persistent-history rows, so a freshly-completed unverified download — still a
live task without a 'history-<id>' task_id — showed no buttons until it aged
into history (Quarantine always worked because it uses the quarantine entry
id). Thread the library_history row id from import through to the live task
(add_library_history_entry now returns lastrowid -> context._history_id ->
task.history_id -> /api/downloads/all), and resolve verifHistoryId from it.

Also surface the real probed audio quality (mutagen-read from the file, e.g.
'FLAC 24bit') on completed rows as a chip, so you can see what was actually
downloaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-14 17:11:17 +02:00
parent 975cf4cf3d
commit 62d5821d26
6 changed files with 43 additions and 6 deletions

View file

@ -796,6 +796,13 @@ def build_unified_downloads_response(limit: int, deps: StatusDeps) -> dict:
'progress': progress,
'error': task.get('error_message'),
'verification_status': task.get('verification_status'),
# library_history row id (set at import) so the Unverified review
# queue can act on a still-live completed task before it becomes
# a persistent-history row.
'history_id': task.get('history_id'),
# Real probed audio quality (mutagen-read from the actual file),
# surfaced so the Downloads page can show what was downloaded.
'quality': task.get('quality') or '',
'retry_info': task.get('retry_info'),
'retry_trigger': task.get('retry_trigger'),
'batch_id': batch_id,

View file

@ -1157,6 +1157,10 @@ def post_process_matched_download_with_verification(context_key, context, file_p
_mark_task_completed(task_id, context.get('track_info'))
if context.get('_verification_status'):
download_tasks[task_id]['verification_status'] = context['_verification_status']
if context.get('_history_id'):
download_tasks[task_id]['history_id'] = context['_history_id']
if context.get('_audio_quality'):
download_tasks[task_id]['quality'] = context['_audio_quality']
with matched_context_lock:
if context_key in matched_downloads_context:
del matched_downloads_context[context_key]
@ -1171,6 +1175,10 @@ def post_process_matched_download_with_verification(context_key, context, file_p
download_tasks[task_id]['metadata_enhanced'] = True
if context.get('_verification_status'):
download_tasks[task_id]['verification_status'] = context['_verification_status']
if context.get('_history_id'):
download_tasks[task_id]['history_id'] = context['_history_id']
if context.get('_audio_quality'):
download_tasks[task_id]['quality'] = context['_audio_quality']
redownload_ctx = download_tasks[task_id].get('_redownload_context')
with matched_context_lock:

View file

@ -252,7 +252,7 @@ def record_library_history_download(context: Dict[str, Any]) -> None:
origin, origin_context = derive_download_origin(context)
db = get_database()
db.add_library_history_entry(
_history_id = db.add_library_history_entry(
event_type="download",
title=title,
artist_name=artist_name,
@ -270,6 +270,10 @@ def record_library_history_download(context: Dict[str, Any]) -> None:
origin_context=origin_context,
verification_status=context.get("_verification_status"),
)
# Stash the row id so the live download task can link to its
# library_history row (the Unverified review queue needs it).
if isinstance(_history_id, int) and _history_id > 0:
context["_history_id"] = _history_id
except Exception as e:
logger.debug("library history record failed: %s", e)

View file

@ -13062,7 +13062,10 @@ class MusicDatabase:
download_source, source_track_id, source_track_title, source_filename,
acoustid_result, source_artist, origin, origin_context, verification_status))
conn.commit()
return True
# Return the new row id (truthy on success) so callers can link the
# live download task to its library_history row — e.g. the Unverified
# review queue needs the id for its play/approve/delete actions.
return cursor.lastrowid
except Exception as e:
logger.debug(f"Error adding library history entry: {e}")
return False

View file

@ -2430,6 +2430,14 @@ function _updateDlNavBadge(count) {
}
}
function _adlQualityBadge(dl) {
// Show the real audio quality of a completed download (probed from the
// file itself — FLAC bit depth, MP3 bitrate, …), so you can see at a
// glance what was actually fetched.
if (dl.status !== 'completed' || !dl.quality) return '';
return ` <span class="adl-quality-chip" title="Audio quality of the downloaded file (read from the file itself)">${_adlEsc(dl.quality)}</span>`;
}
function _adlVerifBadge(dl) {
// Verification badge for completed downloads — how this file passed
// verification (status comes from library_history / the live task):
@ -2457,9 +2465,15 @@ function _adlVerifBadge(dl) {
function verifHistoryId(dl) {
// Persistent history rows carry task_id 'history-<dbid>'.
if (!dl.is_persistent_history || !dl.task_id) return null;
const m = String(dl.task_id).match(/^history-(\d+)$/);
return m ? m[1] : null;
if (dl.is_persistent_history && dl.task_id) {
const m = String(dl.task_id).match(/^history-(\d+)$/);
if (m) return m[1];
}
// Still-live completed tasks carry the library_history id directly, so the
// review actions (play/audit/approve/delete) work before the task becomes
// a persistent-history row — otherwise the buttons "didn't always load".
if (dl.history_id) return String(dl.history_id);
return null;
}
function _verifTimeAgo(iso) {
@ -3132,7 +3146,7 @@ function _adlRender() {
</div>
<div class="adl-row-status ${statusClass}">
<span class="adl-status-dot ${statusClass}"></span>
${statusLabel}${_adlVerifBadge(dl)}${dl.retry_info && (statusClass === 'active' || statusClass === 'queued') ? ` <span class="adl-retry-info" title="Retry engine: trying the next-best candidate (attempt ${_adlEsc(String(dl.retry_info))}${dl.retry_trigger ? ', triggered by ' + _adlEsc(dl.retry_trigger) : ''})">🔁 ${_adlEsc(String(dl.retry_info))}</span>` : ''}
${statusLabel}${_adlVerifBadge(dl)}${_adlQualityBadge(dl)}${dl.retry_info && (statusClass === 'active' || statusClass === 'queued') ? ` <span class="adl-retry-info" title="Retry engine: trying the next-best candidate (attempt ${_adlEsc(String(dl.retry_info))}${dl.retry_trigger ? ', triggered by ' + _adlEsc(dl.retry_trigger) : ''})">🔁 ${_adlEsc(String(dl.retry_info))}</span>` : ''}
</div>
${_adlReviewActions(dl)}
${cancelBtnHtml}

View file

@ -68092,6 +68092,7 @@ body.em-scroll-lock { overflow: hidden; }
.verif-badge.verif-unverified { color: #f1c40f; background: rgba(241,196,15,0.14); }
.verif-badge.verif-force { color: #e67e22; background: rgba(230,126,34,0.16); }
.adl-retry-info { margin-left: 6px; font-size: 11px; color: #e67e22; cursor: help; }
.adl-quality-chip { display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 600; letter-spacing: 0.02em; color: rgba(255,255,255,0.78); background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 0 6px; line-height: 16px; cursor: help; vertical-align: middle; }
.verif-badge.verif-human { color: #3498db; background: rgba(52,152,219,0.14); }
.verif-actions { display: inline-flex; gap: 6px; margin-left: 10px; align-items: center; }
.verif-act { border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.85); border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer; line-height: 18px; }