fix(import): prevent incorrect artist matches and missing tracks

This commit is contained in:
Samidy 2026-03-02 06:47:50 +03:00
parent 0fd5ad9c58
commit 23fdf10f86
2 changed files with 60 additions and 6 deletions

View file

@ -1134,6 +1134,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const xspfFileInput = document.getElementById('xspf-file-input'); const xspfFileInput = document.getElementById('xspf-file-input');
const xmlFileInput = document.getElementById('xml-file-input'); const xmlFileInput = document.getElementById('xml-file-input');
const m3uFileInput = document.getElementById('m3u-file-input'); const m3uFileInput = document.getElementById('m3u-file-input');
const importOptions = { strictArtistMatch: true, albumMatch: true };
let tracks = []; let tracks = [];
let importSource = 'manual'; let importSource = 'manual';
let cover = document.getElementById('playlist-cover-input').value.trim(); let cover = document.getElementById('playlist-cover-input').value.trim();
@ -1214,7 +1217,7 @@ document.addEventListener('DOMContentLoaded', async () => {
currentTrackElement.textContent = progress.currentTrack; currentTrackElement.textContent = progress.currentTrack;
if (currentArtistElement) if (currentArtistElement)
currentArtistElement.textContent = progress.currentArtist || ''; currentArtistElement.textContent = progress.currentArtist || '';
}); }, importOptions);
tracks = result.tracks; tracks = result.tracks;
const missingTracks = result.missingTracks; const missingTracks = result.missingTracks;
@ -1361,7 +1364,7 @@ document.addEventListener('DOMContentLoaded', async () => {
? `Importing ${progress.type}...` ? `Importing ${progress.type}...`
: ''; : '';
} }
}); }, importOptions);
const hasMultipleTypes = const hasMultipleTypes =
result.tracks.length > 0 && (result.albums.length > 0 || result.artists.length > 0); result.tracks.length > 0 && (result.albums.length > 0 || result.artists.length > 0);
@ -2559,6 +2562,7 @@ function escapeHtml(text) {
function showMissingTracksNotification(missingTracks) { function showMissingTracksNotification(missingTracks) {
const modal = document.getElementById('missing-tracks-modal'); const modal = document.getElementById('missing-tracks-modal');
const listUl = document.getElementById('missing-tracks-list-ul'); const listUl = document.getElementById('missing-tracks-list-ul');
const copyBtn = document.getElementById('copy-missing-tracks-btn');
listUl.innerHTML = missingTracks listUl.innerHTML = missingTracks
.map((track) => { .map((track) => {
@ -2568,6 +2572,25 @@ function showMissingTracksNotification(missingTracks) {
}) })
.join(''); .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'); const closeModal = () => modal.classList.remove('active');
// Remove old listeners if any (though usually these functions are called once per instance, // Remove old listeners if any (though usually these functions are called once per instance,

View file

@ -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 * 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'); const lines = csvText.trim().split('\n');
if (lines.length < 2) { if (lines.length < 2) {
return { return {
@ -277,7 +306,7 @@ export async function parseDynamicCSV(csvText, api, onProgress) {
const searchQuery = `"${trackName}" ${artistName}`.trim(); const searchQuery = `"${trackName}" ${artistName}`.trim();
const searchResult = await api.searchTracks(searchQuery); const searchResult = await api.searchTracks(searchQuery);
if (searchResult.items && searchResult.items.length > 0) { 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; return results;
} }
export async function parseCSV(csvText, api, onProgress) { export async function parseCSV(csvText, api, onProgress, options = {}) {
const lines = csvText.trim().split('\n'); const lines = csvText.trim().split('\n');
if (lines.length < 2) return { tracks: [], missingTracks: [] }; if (lines.length < 2) return { tracks: [], missingTracks: [] };
@ -517,7 +546,9 @@ export async function parseCSV(csvText, api, onProgress) {
const searchResult = await api.searchTracks(searchQuery); const searchResult = await api.searchTracks(searchQuery);
if (searchResult.items && searchResult.items.length > 0) { 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 { } else {
missingTracks.push({ title: trackTitle, artist: artistNames, album: albumName }); missingTracks.push({ title: trackTitle, artist: artistNames, album: albumName });
} }