diff --git a/web_server.py b/web_server.py index 0a6fb48c..d5387be9 100644 --- a/web_server.py +++ b/web_server.py @@ -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}) diff --git a/webui/static/script.js b/webui/static/script.js index 3440ceff..6555b0be 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -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 ? '
' : ''; @@ -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 += `${trackData.format}`; + } + if (trackData.bitrate) { + metaHtml += `${trackData.bitrate} kbps`; + } + metaDiv.innerHTML = metaHtml; + trackInfo.appendChild(metaDiv); + } const badge = document.createElement('div'); badge.className = 'wishlist-track-badge owned'; badge.innerHTML = ''; @@ -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(); diff --git a/webui/static/style.css b/webui/static/style.css index 2055813c..d16daafe 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -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,