Add source provenance and AcoustID result to download history
Track original source filename, track ID, and AcoustID verification result for every download. Helps debug wrong-file downloads from streaming sources like Tidal. Each column migrated independently for crash safety. Frontend shows source detail line and color-coded AcoustID badge per entry. Button renamed to "Download History".
This commit is contained in:
parent
afd5125262
commit
f8fbcb507c
5 changed files with 77 additions and 7 deletions
|
|
@ -509,6 +509,10 @@ class MusicDatabase:
|
|||
if 'download_source' not in lh_cols:
|
||||
cursor.execute("ALTER TABLE library_history ADD COLUMN download_source TEXT")
|
||||
logger.info("Added download_source column to library_history")
|
||||
for _col in ['source_track_id', 'source_track_title', 'source_filename', 'acoustid_result']:
|
||||
if _col not in lh_cols:
|
||||
cursor.execute(f"ALTER TABLE library_history ADD COLUMN {_col} TEXT")
|
||||
logger.info(f"Added {_col} column to library_history")
|
||||
|
||||
# Sync history table — tracks the last 100 sync operations with cached context for re-trigger
|
||||
cursor.execute("""
|
||||
|
|
@ -9617,16 +9621,19 @@ class MusicDatabase:
|
|||
|
||||
def add_library_history_entry(self, event_type, title, artist_name=None, album_name=None,
|
||||
quality=None, server_source=None, file_path=None, thumb_url=None,
|
||||
download_source=None):
|
||||
download_source=None, source_track_id=None, source_track_title=None,
|
||||
source_filename=None, acoustid_result=None):
|
||||
"""Record a download or import event to the library history table."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO library_history (event_type, title, artist_name, album_name,
|
||||
quality, server_source, file_path, thumb_url, download_source)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (event_type, title, artist_name, album_name, quality, server_source, file_path, thumb_url, download_source))
|
||||
quality, server_source, file_path, thumb_url, download_source,
|
||||
source_track_id, source_track_title, source_filename, acoustid_result)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (event_type, title, artist_name, album_name, quality, server_source, file_path, thumb_url,
|
||||
download_source, source_track_id, source_track_title, source_filename, acoustid_result))
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1910,6 +1910,19 @@ def _record_library_history_download(context):
|
|||
if isinstance(album_info, dict):
|
||||
thumb_url = album_info.get('album_image_url', '')
|
||||
|
||||
# Source provenance — what file/track was actually downloaded
|
||||
source_filename = search_result.get('filename', '')
|
||||
# Track ID: try search result first, then track_info (Spotify ID used for streaming lookups)
|
||||
source_track_id = (search_result.get('track_id', '')
|
||||
or search_result.get('id', '')
|
||||
or ti.get('id', ''))
|
||||
# Source track title: only save if it differs from expected title (otherwise it's just noise)
|
||||
_src_title = search_result.get('title', '') or search_result.get('name', '')
|
||||
source_track_title = _src_title if _src_title and _src_title != title else ''
|
||||
|
||||
# AcoustID verification result
|
||||
acoustid_result = context.get('_acoustid_result', '')
|
||||
|
||||
db = get_database()
|
||||
db.add_library_history_entry(
|
||||
event_type='download',
|
||||
|
|
@ -1919,7 +1932,11 @@ def _record_library_history_download(context):
|
|||
quality=quality,
|
||||
file_path=file_path,
|
||||
thumb_url=thumb_url,
|
||||
download_source=download_source
|
||||
download_source=download_source,
|
||||
source_track_id=source_track_id,
|
||||
source_track_title=source_track_title,
|
||||
source_filename=source_filename,
|
||||
acoustid_result=acoustid_result
|
||||
)
|
||||
except Exception:
|
||||
pass # Non-critical, never block download flow
|
||||
|
|
@ -19588,6 +19605,7 @@ def _post_process_matched_download(context_key, context, file_path):
|
|||
context
|
||||
)
|
||||
print(f"🔍 AcoustID verification result: {verification_result.value} - {verification_msg}")
|
||||
context['_acoustid_result'] = verification_result.value
|
||||
|
||||
if verification_result == VerificationResult.FAIL:
|
||||
# Move to quarantine instead of Transfer
|
||||
|
|
@ -19629,11 +19647,14 @@ def _post_process_matched_download(context_key, context, file_path):
|
|||
return # NEVER continue processing a known-wrong file
|
||||
else:
|
||||
print(f"⚠️ AcoustID verification skipped: missing track/artist info")
|
||||
context['_acoustid_result'] = 'skip'
|
||||
else:
|
||||
print(f"ℹ️ AcoustID verification not available: {available_reason}")
|
||||
context['_acoustid_result'] = 'disabled'
|
||||
except Exception as verify_error:
|
||||
# Any verification error should NOT block the download - fail open
|
||||
print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}")
|
||||
context['_acoustid_result'] = 'error'
|
||||
# --- END ACOUSTID VERIFICATION ---
|
||||
|
||||
# --- SIMPLE DOWNLOAD HANDLING ---
|
||||
|
|
@ -20887,6 +20908,17 @@ def get_version_info():
|
|||
"title": "What's New in SoulSync",
|
||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||
"sections": [
|
||||
{
|
||||
"title": "📋 Download History — Source Provenance",
|
||||
"description": "Download history now tracks the original source file info for every download",
|
||||
"features": [
|
||||
"• Source filename, track ID, and original track title saved with each download",
|
||||
"• AcoustID verification result (Verified/Failed/Skipped/Off) shown as a badge per entry",
|
||||
"• Source details displayed in monospace under each history entry for easy debugging",
|
||||
"• Settings Connections tab redesigned with collapsible accordion services and brand-colored dots"
|
||||
],
|
||||
"usage_note": "Click 'Download History' on the Dashboard to see source provenance for new downloads."
|
||||
},
|
||||
{
|
||||
"title": "🗺️ Artist Map — Visualize Your Music Universe",
|
||||
"description": "Three interactive canvas-based visualization modes on the Discover page",
|
||||
|
|
|
|||
|
|
@ -1094,7 +1094,7 @@
|
|||
<div class="dashboard-section">
|
||||
<div class="section-title-row">
|
||||
<h3 class="section-title">Recent Activity</h3>
|
||||
<button class="library-history-btn" onclick="openLibraryHistoryModal()" title="View full library history">History</button>
|
||||
<button class="library-history-btn" onclick="openLibraryHistoryModal()" title="View full library history">Download History</button>
|
||||
</div>
|
||||
<div class="activity-feed-container" id="dashboard-activity-feed">
|
||||
<div class="activity-item">
|
||||
|
|
|
|||
|
|
@ -21081,15 +21081,36 @@ function renderHistoryEntry(entry) {
|
|||
badge = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;
|
||||
}
|
||||
|
||||
// AcoustID badge
|
||||
let acoustidBadge = '';
|
||||
if (entry.acoustid_result) {
|
||||
const _aidColors = { pass: '#4caf50', fail: '#ef5350', skip: '#ff9800', disabled: '#666', error: '#ef5350' };
|
||||
const _aidLabels = { pass: 'Verified', fail: 'Failed', skip: 'Skipped', disabled: 'Off', error: 'Error' };
|
||||
const color = _aidColors[entry.acoustid_result] || '#666';
|
||||
const label = _aidLabels[entry.acoustid_result] || entry.acoustid_result;
|
||||
acoustidBadge = `<span class="library-history-badge" style="border-color:${color};color:${color}">AcoustID: ${label}</span>`;
|
||||
}
|
||||
|
||||
const meta = [entry.artist_name, entry.album_name].filter(Boolean).join(' — ');
|
||||
|
||||
// Source provenance detail line
|
||||
let sourceDetail = '';
|
||||
if (entry.event_type === 'download' && (entry.source_filename || entry.source_track_title)) {
|
||||
const parts = [];
|
||||
if (entry.source_filename) parts.push(`File: ${escapeHtml(entry.source_filename)}`);
|
||||
else if (entry.source_track_title) parts.push(`Source track: ${escapeHtml(entry.source_track_title)}`);
|
||||
if (entry.source_track_id) parts.push(`ID: ${escapeHtml(entry.source_track_id)}`);
|
||||
sourceDetail = `<div class="library-history-entry-source">${parts.join(' · ')}</div>`;
|
||||
}
|
||||
|
||||
return `<div class="library-history-entry">
|
||||
${thumb}
|
||||
<div class="library-history-entry-text">
|
||||
<div class="library-history-entry-title">${escapeHtml(entry.title || 'Unknown')}</div>
|
||||
<div class="library-history-entry-meta">${escapeHtml(meta)}</div>
|
||||
${sourceDetail}
|
||||
</div>
|
||||
${badge}
|
||||
${badge}${acoustidBadge}
|
||||
<div class="library-history-entry-time">${formatHistoryTime(entry.created_at)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9423,6 +9423,16 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.library-history-entry-source {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.library-history-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
|
|
|
|||
Loading…
Reference in a new issue