Refactor track search logic to improve matching criteria and remove ISRC dependency

This commit is contained in:
jijirae 2026-01-11 13:31:56 +09:00
parent 0c1fd126d1
commit fb34c47e16

130
js/app.js
View file

@ -1039,7 +1039,6 @@ async function parseCSV(csvText, api, onProgress) {
let trackTitle = ''; let trackTitle = '';
let artistNames = ''; let artistNames = '';
let albumName = ''; let albumName = '';
let isrc = '';
headers.forEach((header, index) => { headers.forEach((header, index) => {
const value = values[index]; const value = values[index];
@ -1061,9 +1060,6 @@ async function parseCSV(csvText, api, onProgress) {
case 'album name': case 'album name':
albumName = value; albumName = value;
break; break;
case 'isrc':
isrc = value;
break;
} }
}); });
@ -1077,81 +1073,151 @@ async function parseCSV(csvText, api, onProgress) {
} }
// Search for the track in hifi tidal api's catalog // Search for the track in hifi tidal api's catalog
if (trackTitle && (artistNames || isrc)) { if (trackTitle && artistNames) {
// Add a small delay to prevent rate limiting // Add a small delay to prevent rate limiting
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
try { try {
let foundTrack = null; let foundTrack = null;
// 0. If ISRC provided, try ISRC first (Apple CSVs include ISRC) // Helper: Normalize strings for fuzzy matching
if (isrc) { const normalize = (str) => str.toLowerCase().replace(/[^\w\s]/g, '').trim();
try {
const searchResults = await api.searchTracks(isrc); // Helper: Check if result matches our criteria
if (searchResults.items && searchResults.items.length > 0) { const isValidMatch = (track, title, artists, album) => {
foundTrack = searchResults.items[0]; if (!track) return false;
console.log(`Found by ISRC: "${trackTitle}" -> ${isrc}`);
} const trackTitle = normalize(track.title || '');
} catch (e) { const trackArtists = (track.artists || []).map(a => normalize(a.name || '')).join(' ');
console.warn(`ISRC search failed for ${isrc}:`, e); const trackAlbum = normalize(track.album?.name || '');
const queryTitle = normalize(title);
const queryArtists = normalize(artists);
const queryAlbum = normalize(album || '');
// Must match title (exact or substring match)
const titleMatch = trackTitle === queryTitle || trackTitle.includes(queryTitle) || queryTitle.includes(trackTitle);
if (!titleMatch) return false;
// Must match at least one artist
const artistMatch = trackArtists.includes(queryArtists.split(' ')[0]) || queryArtists.includes(trackArtists.split(' ')[0]);
if (!artistMatch) return false;
// If album provided, prefer matching album but not strict
if (queryAlbum) {
const albumMatch = trackAlbum === queryAlbum || trackAlbum.includes(queryAlbum) || queryAlbum.includes(trackAlbum);
return albumMatch; // Prefer album matches
} }
}
return true;
};
// 1. Initial Search: Title + All Artists (+ Album if available) // 1. Initial Search: Title + All Artists + Album (most specific)
if (!foundTrack) { if (!foundTrack) {
let searchQuery = `${trackTitle} ${artistNames}`; let searchQuery = `${trackTitle} ${artistNames}`;
if (albumName) searchQuery += ` ${albumName}`; if (albumName) searchQuery += ` ${albumName}`;
let searchResults = await api.searchTracks(searchQuery); const searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) { if (searchResults.items && searchResults.items.length > 0) {
foundTrack = searchResults.items[0]; // Try to find best match within results
for (const result of searchResults.items) {
if (isValidMatch(result, trackTitle, artistNames, albumName)) {
foundTrack = result;
break;
}
}
// Fallback: if no valid match found, use first result only if album matches
if (!foundTrack && albumName) {
const firstResult = searchResults.items[0];
if (isValidMatch(firstResult, trackTitle, artistNames, albumName)) {
foundTrack = firstResult;
}
}
} }
} }
// 2. Retry with Main Artist only // 2. Retry: Title + Main Artist + Album
if (!foundTrack && artistNames) { if (!foundTrack && artistNames) {
const mainArtist = artistNames.split(',')[0].trim(); const mainArtist = artistNames.split(',')[0].trim();
if (mainArtist && mainArtist !== artistNames) { if (mainArtist && mainArtist !== artistNames) {
let searchQuery = `${trackTitle} ${mainArtist}`; let searchQuery = `${trackTitle} ${mainArtist}`;
if (albumName) searchQuery += ` ${albumName}`; if (albumName) searchQuery += ` ${albumName}`;
console.log(`Retry 1 (Main Artist): ${searchQuery}`);
const searchResults = await api.searchTracks(searchQuery); const searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) { if (searchResults.items && searchResults.items.length > 0) {
foundTrack = searchResults.items[0]; for (const result of searchResults.items) {
if (isValidMatch(result, trackTitle, mainArtist, albumName)) {
foundTrack = result;
console.log(`Found (Retry 1 - Main Artist): ${trackTitle}`);
break;
}
}
} }
} }
} }
// 3. Retry with Cleaned Title (if " - " exists) + Main Artist // 3. Retry: Just Title + Album (strong album context)
if (!foundTrack && albumName) {
const searchQuery = `${trackTitle} ${albumName}`;
const searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) {
for (const result of searchResults.items) {
if (isValidMatch(result, trackTitle, artistNames, albumName)) {
foundTrack = result;
console.log(`Found (Retry 2 - Album): ${trackTitle}`);
break;
}
}
}
}
// 4. Retry: Cleaned Title + Main Artist (if " - " exists)
if (!foundTrack && trackTitle.includes(' - ')) { if (!foundTrack && trackTitle.includes(' - ')) {
const mainArtist = (artistNames || '').split(',')[0].trim(); const mainArtist = (artistNames || '').split(',')[0].trim();
const cleanedTitle = trackTitle.split(' - ')[0].trim(); const cleanedTitle = trackTitle.split(' - ')[0].trim();
if (cleanedTitle) { if (cleanedTitle) {
let searchQuery = `${cleanedTitle} ${mainArtist}`; let searchQuery = `${cleanedTitle} ${mainArtist}`;
if (albumName) searchQuery += ` ${albumName}`; if (albumName) searchQuery += ` ${albumName}`;
console.log(`Retry 2 (Cleaned Title): ${searchQuery}`);
const searchResults = await api.searchTracks(searchQuery); const searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) { if (searchResults.items && searchResults.items.length > 0) {
foundTrack = searchResults.items[0]; for (const result of searchResults.items) {
if (isValidMatch(result, cleanedTitle, mainArtist, albumName)) {
foundTrack = result;
console.log(`Found (Retry 3 - Cleaned Title): ${trackTitle}`);
break;
}
}
} }
} }
} }
// 4. Retry with Title + Album only (useful when artist formatting is weird) // 5. Retry: Title only with first artist
if (!foundTrack && albumName) { if (!foundTrack) {
const searchQuery = `${trackTitle} ${albumName}`; const mainArtist = (artistNames || '').split(',')[0].trim();
console.log(`Retry 3 (Album): ${searchQuery}`); const searchQuery = `${trackTitle} ${mainArtist}`;
const searchResults = await api.searchTracks(searchQuery); const searchResults = await api.searchTracks(searchQuery);
if (searchResults.items && searchResults.items.length > 0) { if (searchResults.items && searchResults.items.length > 0) {
foundTrack = searchResults.items[0]; // For title-only search, be more lenient
for (const result of searchResults.items) {
const trackTitle_ = normalize(result.title || '');
const queryTitle = normalize(trackTitle);
if (trackTitle_ === queryTitle) {
foundTrack = result;
console.log(`Found (Retry 4 - Title Match): ${trackTitle}`);
break;
}
}
} }
} }
if (foundTrack) { if (foundTrack) {
tracks.push(foundTrack); tracks.push(foundTrack);
console.log(`Found track: "${trackTitle}" by ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`); console.log(` "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`);
} else { } else {
console.warn(`Track not found: "${trackTitle}" by ${artistNames} ${albumName ? '(album: ' + albumName + ')' : ''}`); console.warn(`Track not found: "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`);
missingTracks.push(`${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`); missingTracks.push(`${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`);
} }
} catch (error) { } catch (error) {