diff --git a/migrate/spotify.go b/migrate/spotify.go index 5b8d755..ef323c1 100644 --- a/migrate/spotify.go +++ b/migrate/spotify.go @@ -215,14 +215,24 @@ func isDuplicateWithinWindow(track SpotifyTrack, existingTimestamps []time.Time) return false } -func ImportSpotify(tracks []SpotifyTrack, userId int) error { +func ImportSpotify(tracks []SpotifyTrack, userId int, progressChan chan ProgressUpdate) error { totalImported := 0 totalTracks := len(tracks) batchStart := 0 + totalBatches := (totalTracks + batchSize - 1) / batchSize + + // Send initial progress update + if progressChan != nil { + progressChan <- ProgressUpdate{ + TotalPages: totalBatches, + Status: "running", + } + } for batchStart < totalTracks { // cap batchEnd at total track count on final batch to prevent OOB error batchEnd := min(batchStart+batchSize, totalTracks) + currentBatch := (batchStart / batchSize) + 1 var validTracks []SpotifyTrack for i := batchStart; i < batchEnd; i++ { @@ -235,6 +245,16 @@ func ImportSpotify(tracks []SpotifyTrack, userId int) error { if len(validTracks) == 0 { batchStart += batchSize + // Send progress update even for empty batches + if progressChan != nil { + progressChan <- ProgressUpdate{ + CurrentPage: currentBatch, + CompletedPages: currentBatch, + TotalPages: totalBatches, + TracksImported: totalImported, + Status: "running", + } + } continue } @@ -273,7 +293,31 @@ func ImportSpotify(tracks []SpotifyTrack, userId int) error { } else { totalImported += int(copyCount) } + + // Send progress update + if progressChan != nil { + progressChan <- ProgressUpdate{ + CurrentPage: currentBatch, + CompletedPages: currentBatch, + TotalPages: totalBatches, + TracksImported: totalImported, + Status: "running", + } + } + batchStart += batchSize } + + // Send completion update + if progressChan != nil { + progressChan <- ProgressUpdate{ + CurrentPage: totalBatches, + CompletedPages: totalBatches, + TotalPages: totalBatches, + TracksImported: totalImported, + Status: "completed", + } + } + return nil } diff --git a/static/import.js b/static/import.js new file mode 100644 index 0000000..dd272a3 --- /dev/null +++ b/static/import.js @@ -0,0 +1,83 @@ +function handleImport(formId, progressPrefix, endpoint, progressUrl, formatLabel) { + const form = document.getElementById(formId); + const progressContainer = document.getElementById(progressPrefix + '-progress'); + const progressFill = document.getElementById(progressPrefix + '-progress-fill'); + const progressText = document.getElementById(progressPrefix + '-progress-text'); + const progressStatus = document.getElementById(progressPrefix + '-progress-status'); + const progressTracks = document.getElementById(progressPrefix + '-progress-tracks'); + const progressError = document.getElementById(progressPrefix + '-progress-error'); + const progressSuccess = document.getElementById(progressPrefix + '-progress-success'); + + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + // Reset and show progress + progressFill.style.width = '0%'; + progressFill.classList.add('animating'); + progressText.textContent = '0%'; + progressStatus.textContent = 'Starting import...'; + progressTracks.textContent = ''; + progressError.textContent = ''; + progressSuccess.textContent = ''; + progressContainer.style.display = 'block'; + + try { + const response = await fetch(endpoint, { + method: 'POST', + body: progressPrefix === 'lastfm' + ? new URLSearchParams(new FormData(form)) + : new FormData(form) + }); + + if (!response.ok) throw new Error('Failed to start import: ' + response.statusText); + + const { job_id } = await response.json(); + const eventSource = new EventSource(progressUrl + job_id); + + eventSource.onmessage = function(event) { + const update = JSON.parse(event.data); + if (update.status === 'connected') return; + + if (update.total_pages > 0) { + const completed = update.completed_pages || update.current_page || 0; + const percent = Math.round((completed / update.total_pages) * 100); + progressFill.style.width = percent + '%'; + progressText.textContent = percent + '%'; + progressStatus.textContent = 'Processing ' + formatLabel + ' ' + completed + ' of ' + update.total_pages; + } + + if (update.tracks_imported !== undefined) { + progressTracks.textContent = update.tracks_imported.toLocaleString() + ' tracks imported'; + } + + if (update.status === 'completed') { + progressFill.classList.remove('animating'); + progressStatus.textContent = 'Import completed!'; + progressSuccess.textContent = 'Successfully imported ' + update.tracks_imported.toLocaleString() + ' tracks from ' + (progressPrefix === 'spotify' ? 'Spotify' : 'Last.fm'); + eventSource.close(); + form.reset(); + } else if (update.status === 'error') { + progressFill.classList.remove('animating'); + progressStatus.textContent = 'Import failed'; + progressError.textContent = 'Error: ' + (update.error || 'Unknown error'); + eventSource.close(); + } + }; + + eventSource.onerror = function() { + progressFill.classList.remove('animating'); + progressStatus.textContent = 'Connection error'; + progressError.textContent = 'Lost connection to server. The import may still be running in the background.'; + eventSource.close(); + }; + + } catch (err) { + progressFill.classList.remove('animating'); + progressStatus.textContent = 'Import failed'; + progressError.textContent = 'Error: ' + err.message; + } + }); +} + +handleImport('spotify-form', 'spotify', '/import/spotify', '/import/spotify/progress?job=', 'batch'); +handleImport('lastfm-form', 'lastfm', '/import/lastfm', '/import/lastfm/progress?job=', 'page'); diff --git a/templates/import.gohtml b/templates/import.gohtml index 449e7d7..a34ec8b 100644 --- a/templates/import.gohtml +++ b/templates/import.gohtml @@ -15,16 +15,27 @@
Welcome, {{.Username}}!
- +Import your Spotify listening history from your data export.
- + +Import your Last.fm scrobbles.
@@ -33,116 +44,20 @@ - +