diff --git a/js/app.js b/js/app.js index d256865..ffe937e 100644 --- a/js/app.js +++ b/js/app.js @@ -1134,6 +1134,9 @@ document.addEventListener('DOMContentLoaded', async () => { const xspfFileInput = document.getElementById('xspf-file-input'); const xmlFileInput = document.getElementById('xml-file-input'); const m3uFileInput = document.getElementById('m3u-file-input'); + + const importOptions = { strictArtistMatch: true, albumMatch: true }; + let tracks = []; let importSource = 'manual'; let cover = document.getElementById('playlist-cover-input').value.trim(); @@ -1214,7 +1217,7 @@ document.addEventListener('DOMContentLoaded', async () => { currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; - }); + }, importOptions); tracks = result.tracks; const missingTracks = result.missingTracks; @@ -1361,7 +1364,7 @@ document.addEventListener('DOMContentLoaded', async () => { ? `Importing ${progress.type}...` : ''; } - }); + }, importOptions); const hasMultipleTypes = result.tracks.length > 0 && (result.albums.length > 0 || result.artists.length > 0); @@ -2559,6 +2562,7 @@ function escapeHtml(text) { function showMissingTracksNotification(missingTracks) { const modal = document.getElementById('missing-tracks-modal'); const listUl = document.getElementById('missing-tracks-list-ul'); + const copyBtn = document.getElementById('copy-missing-tracks-btn'); listUl.innerHTML = missingTracks .map((track) => { @@ -2568,6 +2572,25 @@ function showMissingTracksNotification(missingTracks) { }) .join(''); + if (copyBtn) { + const newCopyBtn = copyBtn.cloneNode(true); + copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn); + + newCopyBtn.addEventListener('click', () => { + const textToCopy = missingTracks + .map((track) => { + return typeof track === 'string' ? track : `${track.artist ? track.artist + ' - ' : ''}${track.title}`; + }) + .join('\n'); + + navigator.clipboard.writeText(textToCopy).then(() => { + const originalText = newCopyBtn.textContent; + newCopyBtn.textContent = 'Copied!'; + setTimeout(() => (newCopyBtn.textContent = originalText), 2000); + }); + }); + } + const closeModal = () => modal.classList.remove('active'); // Remove old listeners if any (though usually these functions are called once per instance, diff --git a/js/playlist-importer.js b/js/playlist-importer.js index 2e76b23..a1b3f02 100644 --- a/js/playlist-importer.js +++ b/js/playlist-importer.js @@ -1,3 +1,32 @@ +function isFuzzyMatch(str1, str2) { + if (!str1 || !str2) return false; + const s1 = str1.toLowerCase().replace(/[^\p{L}\p{N}]/gu, ''); + const s2 = str2.toLowerCase().replace(/[^\p{L}\p{N}]/gu, ''); + return s1.includes(s2) || s2.includes(s1); +} + +function findBestMatch(items, targetArtist, targetAlbum, options) { + if (!items || items.length === 0) return null; + if (!options?.strictArtistMatch && !options?.albumMatch) return items[0]; + + return items.find((item) => { + let artistOk = true; + let albumOk = true; + + if (options.strictArtistMatch && targetArtist) { + const itemArtist = item.artist?.name || item.artists?.[0]?.name; + if (!isFuzzyMatch(itemArtist, targetArtist)) artistOk = false; + } + + if (options.albumMatch && targetAlbum) { + const itemAlbum = item.album?.title; + if (itemAlbum && !isFuzzyMatch(itemAlbum, targetAlbum)) albumOk = false; + } + + return artistOk && albumOk; + }) || null; +} + /** * Helper function to get track artists string */ @@ -172,7 +201,7 @@ function detectCSVFormat(mappedHeaders) { }; } -export async function parseDynamicCSV(csvText, api, onProgress) { +export async function parseDynamicCSV(csvText, api, onProgress, options = {}) { const lines = csvText.trim().split('\n'); if (lines.length < 2) { return { @@ -277,7 +306,7 @@ export async function parseDynamicCSV(csvText, api, onProgress) { const searchQuery = `"${trackName}" ${artistName}`.trim(); const searchResult = await api.searchTracks(searchQuery); if (searchResult.items && searchResult.items.length > 0) { - foundTrack = searchResult.items[0]; + foundTrack = findBestMatch(searchResult.items, artistName, albumName, options); } } @@ -431,7 +460,7 @@ export async function importToLibrary(csvResult, db, onProgress) { return results; } -export async function parseCSV(csvText, api, onProgress) { +export async function parseCSV(csvText, api, onProgress, options = {}) { const lines = csvText.trim().split('\n'); if (lines.length < 2) return { tracks: [], missingTracks: [] }; @@ -517,7 +546,9 @@ export async function parseCSV(csvText, api, onProgress) { const searchResult = await api.searchTracks(searchQuery); if (searchResult.items && searchResult.items.length > 0) { - tracks.push(searchResult.items[0]); + const match = findBestMatch(searchResult.items, artistNames, albumName, options); + if (match) tracks.push(match); + else missingTracks.push({ title: trackTitle, artist: artistNames, album: albumName }); } else { missingTracks.push({ title: trackTitle, artist: artistNames, album: albumName }); }