Merge pull request #65 from jijirae/main
Refactor track search logic to improve matching criteria and remove ISRC dependency
This commit is contained in:
commit
9ee9b6dc9c
1 changed files with 111 additions and 35 deletions
146
js/app.js
146
js/app.js
|
|
@ -1061,7 +1061,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];
|
||||||
|
|
@ -1083,9 +1082,6 @@ async function parseCSV(csvText, api, onProgress) {
|
||||||
case 'album name':
|
case 'album name':
|
||||||
albumName = value;
|
albumName = value;
|
||||||
break;
|
break;
|
||||||
case 'isrc':
|
|
||||||
isrc = value;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1099,84 +1095,164 @@ 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) =>
|
||||||
try {
|
str
|
||||||
const searchResults = await api.searchTracks(isrc);
|
.toLowerCase()
|
||||||
if (searchResults.items && searchResults.items.length > 0) {
|
.replace(/[^\w\s]/g, '')
|
||||||
foundTrack = searchResults.items[0];
|
.trim();
|
||||||
console.log(`Found by ISRC: "${trackTitle}" -> ${isrc}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`ISRC search failed for ${isrc}:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Initial Search: Title + All Artists (+ Album if available)
|
// Helper: Check if result matches our criteria
|
||||||
|
const isValidMatch = (track, title, artists, album) => {
|
||||||
|
if (!track) return false;
|
||||||
|
|
||||||
|
const trackTitle = normalize(track.title || '');
|
||||||
|
const trackArtists = (track.artists || []).map((a) => normalize(a.name || '')).join(' ');
|
||||||
|
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 (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(
|
console.log(`✓ "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`);
|
||||||
`Found track: "${trackTitle}" by ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Track not found: "${trackTitle}" by ${artistNames} ${albumName ? '(album: ' + albumName + ')' : ''}`
|
`✗ Track not found: "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`
|
||||||
);
|
);
|
||||||
missingTracks.push(
|
missingTracks.push(
|
||||||
`${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`
|
`${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue