Show track ownership indicators and file metadata in wishlist modal for all releases

This commit is contained in:
Broque Thomas 2026-02-19 09:39:56 -08:00
parent 2ca410fadd
commit 3663a75769
3 changed files with 67 additions and 18 deletions

View file

@ -6097,7 +6097,7 @@ def library_check_tracks():
if not db_tracks:
# No tracks by this artist in DB — none owned
owned_map = {t.get('name', ''): False for t in tracks if t.get('name')}
owned_map = {t.get('name', ''): {"owned": False} for t in tracks if t.get('name')}
return jsonify({"success": True, "owned_tracks": owned_map})
# Pre-normalize all DB track titles for fast in-memory comparison
@ -6126,8 +6126,8 @@ def library_check_tracks():
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
return cleaned
# Pre-compute normalized DB titles once
db_title_pairs = [(_normalize(t.title), _clean_title(t.title)) for t in db_tracks]
# Pre-compute normalized DB titles once (keep reference to db_track for metadata)
db_title_entries = [(_normalize(t.title), _clean_title(t.title), t) for t in db_tracks]
owned_map = {}
for track in tracks:
@ -6137,12 +6137,12 @@ def library_check_tracks():
search_norm = _normalize(track_name)
search_clean = _clean_title(track_name)
is_owned = False
matched_db_track = None
for db_norm, db_clean in db_title_pairs:
for db_norm, db_clean, db_track in db_title_entries:
# Check normalized match first (fast path for exact/near-exact)
if search_norm == db_norm or search_clean == db_clean:
is_owned = True
matched_db_track = db_track
break
# Fuzzy match: try both normalized and cleaned
sim = max(
@ -6150,10 +6150,20 @@ def library_check_tracks():
SequenceMatcher(None, search_clean, db_clean).ratio()
)
if sim >= 0.7:
is_owned = True
matched_db_track = db_track
break
owned_map[track_name] = is_owned
if matched_db_track:
import os
file_ext = os.path.splitext(matched_db_track.file_path or '')[1].lstrip('.').upper() or None
owned_map[track_name] = {
"owned": True,
"format": file_ext,
"bitrate": matched_db_track.bitrate,
"album": getattr(matched_db_track, 'album_title', None)
}
else:
owned_map[track_name] = {"owned": False}
return jsonify({"success": True, "owned_tracks": owned_map})

View file

@ -10907,9 +10907,11 @@ function generateWishlistTrackList(tracks, trackOwnership) {
const artistsString = formatArtists(track.artists) || 'Unknown Artist';
const duration = formatDuration(track.duration_ms);
const isOwned = trackOwnership ? trackOwnership[track.name] === true : null;
const ownershipClass = isOwned === true ? 'owned' : (isOwned === false ? 'missing' : '');
const badge = isOwned === true
const trackData = trackOwnership ? trackOwnership[track.name] : null;
const isOwned = trackData && (trackData.owned === true || trackData === true);
const isKnown = trackData !== null && trackData !== undefined;
const ownershipClass = isOwned ? 'owned' : (isKnown && !isOwned ? 'missing' : '');
const badge = isOwned
? '<div class="wishlist-track-badge owned"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></div>'
: '';
@ -11095,10 +11097,26 @@ async function lazyLoadTrackOwnership(artistName, tracks, sourceCard) {
trackItems.forEach((item, index) => {
const track = tracks[index];
if (!track) return;
const isOwned = ownership[track.name] === true;
const trackData = ownership[track.name];
const isOwned = trackData && trackData.owned === true;
if (isOwned) {
ownedCount++;
item.classList.add('owned');
// Add metadata line below track name
const trackInfo = item.querySelector('.wishlist-track-info');
if (trackInfo && (trackData.format || trackData.bitrate)) {
const metaDiv = document.createElement('div');
metaDiv.className = 'wishlist-track-meta';
let metaHtml = '';
if (trackData.format) {
metaHtml += `<span class="wishlist-track-format">${trackData.format}</span>`;
}
if (trackData.bitrate) {
metaHtml += `<span class="wishlist-track-bitrate">${trackData.bitrate} kbps</span>`;
}
metaDiv.innerHTML = metaHtml;
trackInfo.appendChild(metaDiv);
}
const badge = document.createElement('div');
badge.className = 'wishlist-track-badge owned';
badge.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
@ -26488,10 +26506,8 @@ function createReleaseCard(release) {
hideLoadingOverlay();
await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType);
// Lazy-load per-track ownership for partial albums (non-blocking)
if (rel.track_completion && typeof rel.track_completion === 'object' && rel.track_completion.missing_tracks > 0) {
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card);
}
// Always lazy-load track ownership + metadata (non-blocking)
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card);
} catch (error) {
hideLoadingOverlay();

View file

@ -14176,12 +14176,12 @@ body {
/* Track ownership indicators */
.wishlist-track-item.owned {
opacity: 0.5;
opacity: 0.65;
border-left: 3px solid rgba(29, 185, 84, 0.6);
}
.wishlist-track-item.owned:hover {
opacity: 0.7;
opacity: 0.85;
}
.wishlist-track-item.missing {
@ -14203,6 +14203,29 @@ body {
color: rgba(29, 185, 84, 0.9);
}
/* Track metadata (format + bitrate) */
.wishlist-track-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
}
.wishlist-track-format {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
padding: 1px 5px;
border-radius: 4px;
background: rgba(29, 185, 84, 0.15);
color: rgba(29, 185, 84, 0.9);
}
.wishlist-track-bitrate {
font-size: 11px;
color: rgba(255, 255, 255, 0.45);
}
/* Modal Footer */
.add-to-wishlist-modal-footer {
background: linear-gradient(135deg,