Show track ownership indicators and file metadata in wishlist modal for all releases
This commit is contained in:
parent
2ca410fadd
commit
3663a75769
3 changed files with 67 additions and 18 deletions
|
|
@ -6097,7 +6097,7 @@ def library_check_tracks():
|
||||||
|
|
||||||
if not db_tracks:
|
if not db_tracks:
|
||||||
# No tracks by this artist in DB — none owned
|
# 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})
|
return jsonify({"success": True, "owned_tracks": owned_map})
|
||||||
|
|
||||||
# Pre-normalize all DB track titles for fast in-memory comparison
|
# 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()
|
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
# Pre-compute normalized DB titles once
|
# Pre-compute normalized DB titles once (keep reference to db_track for metadata)
|
||||||
db_title_pairs = [(_normalize(t.title), _clean_title(t.title)) for t in db_tracks]
|
db_title_entries = [(_normalize(t.title), _clean_title(t.title), t) for t in db_tracks]
|
||||||
|
|
||||||
owned_map = {}
|
owned_map = {}
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
|
|
@ -6137,12 +6137,12 @@ def library_check_tracks():
|
||||||
|
|
||||||
search_norm = _normalize(track_name)
|
search_norm = _normalize(track_name)
|
||||||
search_clean = _clean_title(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)
|
# Check normalized match first (fast path for exact/near-exact)
|
||||||
if search_norm == db_norm or search_clean == db_clean:
|
if search_norm == db_norm or search_clean == db_clean:
|
||||||
is_owned = True
|
matched_db_track = db_track
|
||||||
break
|
break
|
||||||
# Fuzzy match: try both normalized and cleaned
|
# Fuzzy match: try both normalized and cleaned
|
||||||
sim = max(
|
sim = max(
|
||||||
|
|
@ -6150,10 +6150,20 @@ def library_check_tracks():
|
||||||
SequenceMatcher(None, search_clean, db_clean).ratio()
|
SequenceMatcher(None, search_clean, db_clean).ratio()
|
||||||
)
|
)
|
||||||
if sim >= 0.7:
|
if sim >= 0.7:
|
||||||
is_owned = True
|
matched_db_track = db_track
|
||||||
break
|
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})
|
return jsonify({"success": True, "owned_tracks": owned_map})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10907,9 +10907,11 @@ function generateWishlistTrackList(tracks, trackOwnership) {
|
||||||
const artistsString = formatArtists(track.artists) || 'Unknown Artist';
|
const artistsString = formatArtists(track.artists) || 'Unknown Artist';
|
||||||
const duration = formatDuration(track.duration_ms);
|
const duration = formatDuration(track.duration_ms);
|
||||||
|
|
||||||
const isOwned = trackOwnership ? trackOwnership[track.name] === true : null;
|
const trackData = trackOwnership ? trackOwnership[track.name] : null;
|
||||||
const ownershipClass = isOwned === true ? 'owned' : (isOwned === false ? 'missing' : '');
|
const isOwned = trackData && (trackData.owned === true || trackData === true);
|
||||||
const badge = isOwned === 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>'
|
? '<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) => {
|
trackItems.forEach((item, index) => {
|
||||||
const track = tracks[index];
|
const track = tracks[index];
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
const isOwned = ownership[track.name] === true;
|
const trackData = ownership[track.name];
|
||||||
|
const isOwned = trackData && trackData.owned === true;
|
||||||
if (isOwned) {
|
if (isOwned) {
|
||||||
ownedCount++;
|
ownedCount++;
|
||||||
item.classList.add('owned');
|
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');
|
const badge = document.createElement('div');
|
||||||
badge.className = 'wishlist-track-badge owned';
|
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>';
|
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();
|
hideLoadingOverlay();
|
||||||
await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType);
|
await openAddToWishlistModal(albumData, currentArtist, data.tracks, albumType);
|
||||||
|
|
||||||
// Lazy-load per-track ownership for partial albums (non-blocking)
|
// Always lazy-load track ownership + metadata (non-blocking)
|
||||||
if (rel.track_completion && typeof rel.track_completion === 'object' && rel.track_completion.missing_tracks > 0) {
|
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card);
|
||||||
lazyLoadTrackOwnership(currentArtist.name, data.tracks, card);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hideLoadingOverlay();
|
hideLoadingOverlay();
|
||||||
|
|
|
||||||
|
|
@ -14176,12 +14176,12 @@ body {
|
||||||
|
|
||||||
/* Track ownership indicators */
|
/* Track ownership indicators */
|
||||||
.wishlist-track-item.owned {
|
.wishlist-track-item.owned {
|
||||||
opacity: 0.5;
|
opacity: 0.65;
|
||||||
border-left: 3px solid rgba(29, 185, 84, 0.6);
|
border-left: 3px solid rgba(29, 185, 84, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlist-track-item.owned:hover {
|
.wishlist-track-item.owned:hover {
|
||||||
opacity: 0.7;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlist-track-item.missing {
|
.wishlist-track-item.missing {
|
||||||
|
|
@ -14203,6 +14203,29 @@ body {
|
||||||
color: rgba(29, 185, 84, 0.9);
|
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 */
|
/* Modal Footer */
|
||||||
.add-to-wishlist-modal-footer {
|
.add-to-wishlist-modal-footer {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue