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 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,
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue