diff --git a/database/music_library.db-shm b/database/music_library.db-shm new file mode 100644 index 00000000..2dca81bb Binary files /dev/null and b/database/music_library.db-shm differ diff --git a/database/music_library.db-wal b/database/music_library.db-wal new file mode 100644 index 00000000..4dba5675 Binary files /dev/null and b/database/music_library.db-wal differ diff --git a/web_server.py b/web_server.py index 003d8e23..b0b00321 100644 --- a/web_server.py +++ b/web_server.py @@ -13724,6 +13724,9 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): print(f"🔄 [Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing") for i, track_data in enumerate(tracks_json): + # Use original table index if provided (for partial track selection), + # otherwise fall back to enumeration index + track_index = track_data.get('_original_index', i) track_name = track_data.get('name', '') artists = track_data.get('artists', []) found, confidence = False, 0.0 @@ -13749,7 +13752,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): break analysis_results.append({ - 'track_index': i, 'track': track_data, 'found': found, 'confidence': confidence + 'track_index': track_index, 'track': track_data, 'found': found, 'confidence': confidence }) # WISHLIST REMOVAL: If track is found in database, check if it should be removed from wishlist diff --git a/webui/static/mobile.css b/webui/static/mobile.css index b58362ac..6b409853 100644 --- a/webui/static/mobile.css +++ b/webui/static/mobile.css @@ -1141,6 +1141,10 @@ font-size: 13px; } + .track-selection-count { + font-size: 10px; + } + .download-tracks-table { table-layout: auto; } @@ -1154,17 +1158,38 @@ text-overflow: ellipsis; } + /* Checkbox column sizing on mobile */ + .track-select-header, + .track-select-cell { + width: auto; + padding: 6px 4px !important; + } + + .track-select-header input[type="checkbox"], + .track-select-cell input[type="checkbox"] { + width: 14px; + height: 14px; + } + /* Hide # and Duration columns on mobile */ - .download-tracks-table th:nth-child(1), .download-tracks-table td.track-number { display: none; } - .download-tracks-table th:nth-child(4), .download-tracks-table td.track-duration { display: none; } + /* Hide matching th headers — different positions depending on checkbox column */ + .download-tracks-table:has(.track-select-header) th:nth-child(2), + .download-tracks-table:not(:has(.track-select-header)) th:nth-child(1) { + display: none; + } + + .download-tracks-table:has(.track-select-header) th:nth-child(5) { + display: none; + } + .track-name { width: auto; max-width: 120px; @@ -1810,11 +1835,15 @@ } /* Hide Library Match column too at 480px */ - .download-tracks-table th:nth-child(5), .download-tracks-table td.track-match-status { display: none; } + .download-tracks-table:has(.track-select-header) th:nth-child(6), + .download-tracks-table:not(:has(.track-select-header)) th:nth-child(4) { + display: none; + } + .track-name { max-width: 100px; } diff --git a/webui/static/script.js b/webui/static/script.js index c7f17de5..21fa2ea2 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -6007,11 +6007,17 @@ async function openDownloadMissingModal(playlistId) {

📋 Track Analysis & Download Status

+ ${tracks.length} / ${tracks.length} tracks selected
+ @@ -6024,6 +6030,11 @@ async function openDownloadMissingModal(playlistId) { ${tracks.map((track, index) => ` + @@ -6394,11 +6405,17 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -6411,6 +6428,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam ${spotifyTracks.map((track, index) => ` + @@ -7886,9 +7908,34 @@ async function startMissingTracksProcess(playlistId) { forceToggleContainer.style.display = 'none'; } + // Filter tracks based on checkbox selection (if checkboxes exist in this modal) + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + let selectedTracks = process.tracks; + if (tbody) { + const allCbs = tbody.querySelectorAll('.track-select-cb'); + if (allCbs.length > 0) { + // Checkboxes exist — filter to only checked tracks + const checkedCbs = tbody.querySelectorAll('.track-select-cb:checked'); + const selectedIndices = new Set([...checkedCbs].map(cb => parseInt(cb.dataset.trackIndex))); + console.log(`🔲 [Track Selection] Total checkboxes: ${allCbs.length}, Checked: ${checkedCbs.length}`); + console.log(`🔲 [Track Selection] Checked indices:`, [...selectedIndices]); + console.log(`🔲 [Track Selection] process.tracks has ${process.tracks.length} items, first: "${process.tracks[0]?.name}", last: "${process.tracks[process.tracks.length-1]?.name}"`); + // Stamp each selected track with its original table index so the backend + // maps status updates back to the correct modal row + selectedTracks = process.tracks + .map((track, i) => ({ ...track, _original_index: i })) + .filter(track => selectedIndices.has(track._original_index)); + console.log(`🔲 [Track Selection] Filtered to ${selectedTracks.length} tracks:`, selectedTracks.map(t => `[${t._original_index}] ${t.name}`)); + // Disable checkboxes once analysis starts + allCbs.forEach(cb => { cb.disabled = true; }); + } + } + const selectAllCb = document.getElementById(`select-all-${playlistId}`); + if (selectAllCb) selectAllCb.disabled = true; + // Prepare request body - add album/artist context for artist album downloads const requestBody = { - tracks: process.tracks, + tracks: selectedTracks, force_download_all: forceDownloadAll }; @@ -8793,6 +8840,52 @@ async function updateModalWithLiveDownloadProgress() { } } +function toggleAllTrackSelections(playlistId, checked) { + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + if (!tbody) return; + const checkboxes = tbody.querySelectorAll('.track-select-cb'); + checkboxes.forEach(cb => { cb.checked = checked; }); + updateTrackSelectionCount(playlistId); +} + +function updateTrackSelectionCount(playlistId) { + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + if (!tbody) return; + const allCbs = tbody.querySelectorAll('.track-select-cb'); + const checkedCbs = tbody.querySelectorAll('.track-select-cb:checked'); + const total = allCbs.length; + const selected = checkedCbs.length; + + // Update selection count label + const countLabel = document.getElementById(`track-selection-count-${playlistId}`); + if (countLabel) { + countLabel.textContent = `${selected} / ${total} tracks selected`; + } + + // Update select-all checkbox state + const selectAll = document.getElementById(`select-all-${playlistId}`); + if (selectAll) { + selectAll.checked = selected === total; + selectAll.indeterminate = selected > 0 && selected < total; + } + + // Update row dimming + allCbs.forEach(cb => { + const row = cb.closest('tr'); + if (row) row.classList.toggle('track-deselected', !cb.checked); + }); + + // Disable Begin Analysis and Add to Wishlist buttons when 0 selected + const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`); + if (beginBtn) { + beginBtn.disabled = selected === 0; + } + const wishlistBtn = document.getElementById(`add-to-wishlist-btn-${playlistId}`); + if (wishlistBtn) { + wishlistBtn.disabled = selected === 0; + } +} + async function cancelAllOperations(playlistId) { const process = activeDownloadProcesses[playlistId]; if (!process) return; @@ -11573,7 +11666,18 @@ async function addModalTracksToWishlist(playlistId) { return; } - const tracks = process.tracks; + // Filter tracks based on checkbox selection (if checkboxes exist in this modal) + const wishlistTbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + let tracks = process.tracks; + if (wishlistTbody) { + const allCbs = wishlistTbody.querySelectorAll('.track-select-cb'); + if (allCbs.length > 0) { + const checkedCbs = wishlistTbody.querySelectorAll('.track-select-cb:checked'); + const selectedIndices = new Set([...checkedCbs].map(cb => parseInt(cb.dataset.trackIndex))); + tracks = process.tracks.filter((_, i) => selectedIndices.has(i)); + } + } + // Get artist/album context if available (for artist album downloads) const artist = process.artist || { name: 'Unknown Artist', id: null }; const album = process.album || process.playlist || { name: 'Playlist', id: playlistId }; @@ -15406,11 +15510,17 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -15423,6 +15533,11 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, ${spotifyTracks.map((track, index) => ` + @@ -22498,11 +22613,17 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -22515,6 +22636,11 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis ${spotifyTracks.map((track, index) => ` + diff --git a/webui/static/style.css b/webui/static/style.css index 88815ec3..a2682109 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -10518,6 +10518,9 @@ body { background: linear-gradient(135deg, rgba(40, 40, 40, 0.8) 0%, rgba(30, 30, 30, 0.9) 100%); + display: flex; + align-items: center; + justify-content: space-between; } .download-tracks-title { @@ -10578,16 +10581,16 @@ body { .track-number { color: #888888; font-weight: 500; - width: 5%; - /* 5% for track numbers */ + width: 4%; + /* 4% for track numbers */ text-align: center; } .track-name { font-weight: 600; color: #ffffff; - width: 25%; - /* 25% for track names */ + width: 24%; + /* 24% for track names */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -10633,8 +10636,8 @@ body { .track-download-status { text-align: center; - width: 20%; - /* 20% for download status with progress */ + width: 19%; + /* 19% for download status with progress */ font-weight: 500; overflow: hidden; text-overflow: ellipsis; @@ -10667,6 +10670,43 @@ body { width: auto; } +.track-select-header, +.track-select-cell { + width: 3%; + text-align: center; + padding: 12px 6px !important; +} + +.track-select-header input[type="checkbox"], +.track-select-cell input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #1db954; +} + +.track-select-cell input[type="checkbox"]:disabled { + cursor: default; + opacity: 0.5; +} + +.track-deselected td:not(.track-select-cell) { + opacity: 0.4; +} + +.track-deselected .track-name, +.track-deselected .track-artist { + text-decoration: line-through; + text-decoration-color: rgba(255, 255, 255, 0.3); +} + +.track-selection-count { + font-size: 12px; + color: #999; + font-weight: 500; + white-space: nowrap; +} + .cancel-track-btn { background: #f44336; color: #ffffff;
+ + # Track Name Artist(s)
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}