@ -10579,6 +10579,7 @@ function getSuccessfulDownloadCount(process) {
// ===============================
let currentWishlistModalData = null ;
let wishlistModalVersion = 0 ;
/ * *
* Open the Add to Wishlist modal for an album / EP / single
@ -10587,7 +10588,8 @@ let currentWishlistModalData = null;
* @ param { Array } tracks - Array of track objects
* @ param { string } albumType - Type of release ( album , EP , single )
* /
async function openAddToWishlistModal ( album , artist , tracks , albumType ) {
async function openAddToWishlistModal ( album , artist , tracks , albumType , trackOwnership ) {
wishlistModalVersion ++ ;
showLoadingOverlay ( 'Preparing wishlist...' ) ;
console . log ( ` 🎵 Opening Add to Wishlist modal for: ${ artist . name } - ${ album . name } ` ) ;
@ -10609,14 +10611,14 @@ async function openAddToWishlistModal(album, artist, tracks, albumType) {
}
// Generate and populate hero section
const heroContent = generateWishlistModalHeroSection ( album , artist , tracks , albumType );
const heroContent = generateWishlistModalHeroSection ( album , artist , tracks , albumType , trackOwnership );
const heroContainer = document . getElementById ( 'add-to-wishlist-modal-hero' ) ;
if ( heroContainer ) {
heroContainer . innerHTML = heroContent ;
}
// Generate and populate track list
const trackListHTML = generateWishlistTrackList ( tracks );
const trackListHTML = generateWishlistTrackList ( tracks , trackOwnership );
const trackListContainer = document . getElementById ( 'wishlist-track-list' ) ;
if ( trackListContainer ) {
trackListContainer . innerHTML = trackListHTML ;
@ -10644,11 +10646,21 @@ async function openAddToWishlistModal(album, artist, tracks, albumType) {
/ * *
* Generate the hero section HTML for the wishlist modal
* /
function generateWishlistModalHeroSection ( album , artist , tracks , albumType ) {
function generateWishlistModalHeroSection ( album , artist , tracks , albumType , trackOwnership ) {
const artistImage = artist . image _url || '' ;
const albumImage = album . image _url || '' ;
const trackCount = tracks . length ;
// Calculate missing tracks if ownership info is available
let trackDetailText = ` ${ trackCount } track ${ trackCount !== 1 ? 's' : '' } ` ;
if ( trackOwnership ) {
const ownedCount = Object . values ( trackOwnership ) . filter ( v => v === true ) . length ;
const missingCount = trackCount - ownedCount ;
if ( missingCount > 0 && ownedCount > 0 ) {
trackDetailText = ` ${ missingCount } of ${ trackCount } tracks missing ` ;
}
}
let heroBackgroundImage = '' ;
if ( albumImage ) {
heroBackgroundImage = ` <div class="add-to-wishlist-modal-hero-bg" style="background-image: url(' ${ albumImage } ');"></div> ` ;
@ -10665,7 +10677,7 @@ function generateWishlistModalHeroSection(album, artist, tracks, albumType) {
< div class = "add-to-wishlist-modal-hero-subtitle" > by $ { escapeHtml ( artist . name || 'Unknown Artist' ) } < / d i v >
< div class = "add-to-wishlist-modal-hero-details" >
< span class = "add-to-wishlist-modal-hero-detail" > $ { albumType || 'Album' } < / s p a n >
< span class = "add-to-wishlist-modal-hero-detail" > $ { track Count} track$ { trackCount !== 1 ? 's' : '' } < / s p a n >
< span class = "add-to-wishlist-modal-hero-detail" > $ { track DetailText } < / s p a n >
< / d i v >
< / d i v >
< / d i v >
@ -10680,7 +10692,7 @@ function generateWishlistModalHeroSection(album, artist, tracks, albumType) {
/ * *
* Generate the track list HTML for the wishlist modal
* /
function generateWishlistTrackList ( tracks ) {
function generateWishlistTrackList ( tracks , trackOwnership ) {
if ( ! tracks || tracks . length === 0 ) {
return '<div style="text-align: center; padding: 40px; color: rgba(255, 255, 255, 0.6);">No tracks found</div>' ;
}
@ -10691,14 +10703,21 @@ function generateWishlistTrackList(tracks) {
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
? '<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>'
: '' ;
return `
< div class = "wishlist-track-item" >
< div class = "wishlist-track-item ${ownershipClass} ">
< div class = "wishlist-track-number" > $ { trackNumber } < / d i v >
< div class = "wishlist-track-info" >
< div class = "wishlist-track-name" > $ { trackName } < / d i v >
< div class = "wishlist-track-artists" > $ { artistsString } < / d i v >
< / d i v >
< div class = "wishlist-track-duration" > $ { duration } < / d i v >
$ { badge }
< / d i v >
` ;
} ) . join ( '' ) ;
@ -10843,6 +10862,84 @@ async function handleAddToWishlist() {
}
}
/ * *
* Lazy - load per - track ownership indicators into an already - open wishlist modal .
* Fetches ownership from the backend , then updates the modal DOM in - place .
* If all tracks are owned ( Spotify metadata discrepancy ) , also fixes the source card .
* /
async function lazyLoadTrackOwnership ( artistName , tracks , sourceCard ) {
const myVersion = wishlistModalVersion ;
try {
const resp = await fetch ( '/api/library/check-tracks' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( {
artist _name : artistName ,
tracks : tracks . map ( t => ( { name : t . name , track _number : t . track _number } ) )
} )
} ) ;
const data = await resp . json ( ) ;
if ( ! data . success ) return ;
// Guard against stale updates if user reopened modal for a different album
if ( myVersion !== wishlistModalVersion ) return ;
const ownership = data . owned _tracks ;
const trackItems = document . querySelectorAll ( '#wishlist-track-list .wishlist-track-item' ) ;
let ownedCount = 0 ;
trackItems . forEach ( ( item , index ) => {
const track = tracks [ index ] ;
if ( ! track ) return ;
const isOwned = ownership [ track . name ] === true ;
if ( isOwned ) {
ownedCount ++ ;
item . classList . add ( 'owned' ) ;
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>' ;
item . appendChild ( badge ) ;
} else {
item . classList . add ( 'missing' ) ;
}
} ) ;
// Update hero subtitle with missing count
const missingCount = tracks . length - ownedCount ;
const heroDetails = document . querySelectorAll ( '.add-to-wishlist-modal-hero-detail' ) ;
const trackDetailEl = heroDetails . length > 1 ? heroDetails [ heroDetails . length - 1 ] : null ;
if ( trackDetailEl && missingCount > 0 && ownedCount > 0 ) {
trackDetailEl . textContent = ` ${ missingCount } of ${ tracks . length } tracks missing ` ;
}
// If ALL returned tracks are owned, this is a Spotify metadata discrepancy
// (e.g. total_tracks says 15 but API only returns 14, and all 14 are owned)
// Fix the source card to show complete
if ( missingCount === 0 && sourceCard && sourceCard . _releaseData ) {
sourceCard . _releaseData . track _completion = {
owned _tracks : ownedCount ,
total _tracks : tracks . length ,
percentage : 100 ,
missing _tracks : 0
} ;
const completionText = sourceCard . querySelector ( '.completion-text' ) ;
if ( completionText ) {
completionText . textContent = ` Complete ( ${ ownedCount } ) ` ;
completionText . className = 'completion-text complete' ;
completionText . title = '' ;
}
const completionFill = sourceCard . querySelector ( '.completion-fill' ) ;
if ( completionFill ) {
completionFill . style . width = '100%' ;
completionFill . classList . remove ( 'partial' ) ;
completionFill . classList . add ( 'complete' ) ;
}
}
} catch ( e ) {
console . warn ( 'Could not load track ownership:' , e ) ;
}
}
/ * *
* Close the Add to Wishlist modal
* /
@ -26012,11 +26109,15 @@ function createReleaseCard(release) {
// Use the actual album type from release data
const albumType = rel . album _type || rel . type || 'album' ;
// Open the Add to Wishlist modal
// Note: openAddToWishlistModal has its own loading overlay
// Open the Add to Wishlist modal immediately (no waiting for ownership check)
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 ) ;
}
} catch ( error ) {
hideLoadingOverlay ( ) ;
console . error ( '❌ Error handling release click:' , error ) ;
@ -26150,15 +26251,20 @@ function updateLibraryReleaseCard(data) {
card . classList . add ( 'missing' ) ;
}
// If backend says "completed" (>=90%), trust it — Spotify metadata track counts
// can be wrong (e.g. total_tracks=15 but API only returns 14 actual tracks)
const isComplete = data . status === 'completed' ;
const effectiveMissing = isComplete ? 0 : ( data . expected _tracks - data . owned _tracks ) ;
// Update the mutable release data on the card
if ( card . _releaseData ) {
card . _releaseData . owned = isOwned ;
if ( isOwned && data . expected _tracks > 0 ) {
card . _releaseData . track _completion = {
owned _tracks : data . owned _tracks ,
total _tracks : data. expected _tracks ,
percentage : data. completion _percentage ,
missing _tracks : data. expected _tracks - data . owned _tracks
total _tracks : isComplete ? data . owned _tracks : data. expected _tracks ,
percentage : isComplete ? 100 : data. completion _percentage ,
missing _tracks : effectiveMissing
} ;
} else if ( isOwned ) {
card . _releaseData . track _completion = {
@ -26177,14 +26283,13 @@ function updateLibraryReleaseCard(data) {
if ( completionText ) {
completionText . classList . remove ( 'checking' , 'complete' , 'partial' , 'missing' ) ;
if ( isOwned ) {
const missing = data . expected _tracks - data . owned _tracks ;
if ( missing <= 0 ) {
if ( effectiveMissing <= 0 ) {
completionText . textContent = ` Complete ( ${ data . owned _tracks } ) ` ;
completionText . className = 'completion-text complete' ;
} else {
completionText . textContent = ` ${ data . owned _tracks } / ${ data . expected _tracks } tracks ` ;
completionText . className = 'completion-text partial' ;
completionText . title = ` Missing ${ missing} track ${ m issing !== 1 ? 's' : '' } ` ;
completionText . title = ` Missing ${ effectiveMissing} track ${ effectiveM issing !== 1 ? 's' : '' } ` ;
}
} else {
completionText . textContent = 'Missing' ;
@ -26197,10 +26302,9 @@ function updateLibraryReleaseCard(data) {
if ( completionFill ) {
completionFill . classList . remove ( 'checking' , 'complete' , 'partial' , 'missing' ) ;
if ( isOwned ) {
const pct = data. completion _percentage || 100 ;
const pct = isComplete ? 100 : ( data. completion _percentage || 100 ) ;
completionFill . style . width = ` ${ pct } % ` ;
const missing = data . expected _tracks - data . owned _tracks ;
completionFill . classList . add ( missing <= 0 ? 'complete' : 'partial' ) ;
completionFill . classList . add ( effectiveMissing <= 0 ? 'complete' : 'partial' ) ;
} else {
completionFill . style . width = '0%' ;
completionFill . classList . add ( 'missing' ) ;