fix(import): prevent incorrect artist matches and missing tracks
This commit is contained in:
parent
0fd5ad9c58
commit
23fdf10f86
2 changed files with 60 additions and 6 deletions
27
js/app.js
27
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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue