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:
|
if 'download_source' not in lh_cols:
|
||||||
cursor.execute("ALTER TABLE library_history ADD COLUMN download_source TEXT")
|
cursor.execute("ALTER TABLE library_history ADD COLUMN download_source TEXT")
|
||||||
logger.info("Added download_source column to library_history")
|
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
|
# Sync history table — tracks the last 100 sync operations with cached context for re-trigger
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
|
|
@ -9617,16 +9621,19 @@ class MusicDatabase:
|
||||||
|
|
||||||
def add_library_history_entry(self, event_type, title, artist_name=None, album_name=None,
|
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,
|
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."""
|
"""Record a download or import event to the library history table."""
|
||||||
try:
|
try:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO library_history (event_type, title, artist_name, album_name,
|
INSERT INTO library_history (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,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
source_track_id, source_track_title, source_filename, acoustid_result)
|
||||||
""", (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, source_track_id, source_track_title, source_filename, acoustid_result))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1910,6 +1910,19 @@ def _record_library_history_download(context):
|
||||||
if isinstance(album_info, dict):
|
if isinstance(album_info, dict):
|
||||||
thumb_url = album_info.get('album_image_url', '')
|
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 = get_database()
|
||||||
db.add_library_history_entry(
|
db.add_library_history_entry(
|
||||||
event_type='download',
|
event_type='download',
|
||||||
|
|
@ -1919,7 +1932,11 @@ def _record_library_history_download(context):
|
||||||
quality=quality,
|
quality=quality,
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
thumb_url=thumb_url,
|
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:
|
except Exception:
|
||||||
pass # Non-critical, never block download flow
|
pass # Non-critical, never block download flow
|
||||||
|
|
@ -19588,6 +19605,7 @@ def _post_process_matched_download(context_key, context, file_path):
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
print(f"🔍 AcoustID verification result: {verification_result.value} - {verification_msg}")
|
print(f"🔍 AcoustID verification result: {verification_result.value} - {verification_msg}")
|
||||||
|
context['_acoustid_result'] = verification_result.value
|
||||||
|
|
||||||
if verification_result == VerificationResult.FAIL:
|
if verification_result == VerificationResult.FAIL:
|
||||||
# Move to quarantine instead of Transfer
|
# 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
|
return # NEVER continue processing a known-wrong file
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ AcoustID verification skipped: missing track/artist info")
|
print(f"⚠️ AcoustID verification skipped: missing track/artist info")
|
||||||
|
context['_acoustid_result'] = 'skip'
|
||||||
else:
|
else:
|
||||||
print(f"ℹ️ AcoustID verification not available: {available_reason}")
|
print(f"ℹ️ AcoustID verification not available: {available_reason}")
|
||||||
|
context['_acoustid_result'] = 'disabled'
|
||||||
except Exception as verify_error:
|
except Exception as verify_error:
|
||||||
# Any verification error should NOT block the download - fail open
|
# Any verification error should NOT block the download - fail open
|
||||||
print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}")
|
print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}")
|
||||||
|
context['_acoustid_result'] = 'error'
|
||||||
# --- END ACOUSTID VERIFICATION ---
|
# --- END ACOUSTID VERIFICATION ---
|
||||||
|
|
||||||
# --- SIMPLE DOWNLOAD HANDLING ---
|
# --- SIMPLE DOWNLOAD HANDLING ---
|
||||||
|
|
@ -20887,6 +20908,17 @@ def get_version_info():
|
||||||
"title": "What's New in SoulSync",
|
"title": "What's New in SoulSync",
|
||||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||||
"sections": [
|
"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",
|
"title": "🗺️ Artist Map — Visualize Your Music Universe",
|
||||||
"description": "Three interactive canvas-based visualization modes on the Discover page",
|
"description": "Three interactive canvas-based visualization modes on the Discover page",
|
||||||
|
|
|
||||||
|
|
@ -1094,7 +1094,7 @@
|
||||||
<div class="dashboard-section">
|
<div class="dashboard-section">
|
||||||
<div class="section-title-row">
|
<div class="section-title-row">
|
||||||
<h3 class="section-title">Recent Activity</h3>
|
<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>
|
||||||
<div class="activity-feed-container" id="dashboard-activity-feed">
|
<div class="activity-feed-container" id="dashboard-activity-feed">
|
||||||
<div class="activity-item">
|
<div class="activity-item">
|
||||||
|
|
|
||||||
|
|
@ -21081,15 +21081,36 @@ function renderHistoryEntry(entry) {
|
||||||
badge = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;
|
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(' — ');
|
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">
|
return `<div class="library-history-entry">
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="library-history-entry-text">
|
<div class="library-history-entry-text">
|
||||||
<div class="library-history-entry-title">${escapeHtml(entry.title || 'Unknown')}</div>
|
<div class="library-history-entry-title">${escapeHtml(entry.title || 'Unknown')}</div>
|
||||||
<div class="library-history-entry-meta">${escapeHtml(meta)}</div>
|
<div class="library-history-entry-meta">${escapeHtml(meta)}</div>
|
||||||
|
${sourceDetail}
|
||||||
</div>
|
</div>
|
||||||
${badge}
|
${badge}${acoustidBadge}
|
||||||
<div class="library-history-entry-time">${formatHistoryTime(entry.created_at)}</div>
|
<div class="library-history-entry-time">${formatHistoryTime(entry.created_at)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9423,6 +9423,16 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
||||||
margin-top: 2px;
|
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 {
|
.library-history-badge {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue