From fb34c47e16c12ba02e39557b7fcee3374c7bf826 Mon Sep 17 00:00:00 2001 From: jijirae Date: Sun, 11 Jan 2026 13:31:56 +0900 Subject: [PATCH 1/2] Refactor track search logic to improve matching criteria and remove ISRC dependency --- js/app.js | 130 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 32 deletions(-) diff --git a/js/app.js b/js/app.js index b7e34bf..c462810 100644 --- a/js/app.js +++ b/js/app.js @@ -1039,7 +1039,6 @@ async function parseCSV(csvText, api, onProgress) { let trackTitle = ''; let artistNames = ''; let albumName = ''; - let isrc = ''; headers.forEach((header, index) => { const value = values[index]; @@ -1061,9 +1060,6 @@ async function parseCSV(csvText, api, onProgress) { case 'album name': albumName = value; 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 - if (trackTitle && (artistNames || isrc)) { + if (trackTitle && artistNames) { // Add a small delay to prevent rate limiting await new Promise(resolve => setTimeout(resolve, 300)); try { let foundTrack = null; - // 0. If ISRC provided, try ISRC first (Apple CSVs include ISRC) - if (isrc) { - try { - const searchResults = await api.searchTracks(isrc); - if (searchResults.items && searchResults.items.length > 0) { - foundTrack = searchResults.items[0]; - console.log(`Found by ISRC: "${trackTitle}" -> ${isrc}`); - } - } catch (e) { - console.warn(`ISRC search failed for ${isrc}:`, e); + // Helper: Normalize strings for fuzzy matching + const normalize = (str) => str.toLowerCase().replace(/[^\w\s]/g, '').trim(); + + // 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 if available) + // 1. Initial Search: Title + All Artists + Album (most specific) if (!foundTrack) { let searchQuery = `${trackTitle} ${artistNames}`; if (albumName) searchQuery += ` ${albumName}`; - let searchResults = await api.searchTracks(searchQuery); + const searchResults = await api.searchTracks(searchQuery); 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) { const mainArtist = artistNames.split(',')[0].trim(); if (mainArtist && mainArtist !== artistNames) { let searchQuery = `${trackTitle} ${mainArtist}`; if (albumName) searchQuery += ` ${albumName}`; - console.log(`Retry 1 (Main Artist): ${searchQuery}`); const searchResults = await api.searchTracks(searchQuery); + 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(' - ')) { const mainArtist = (artistNames || '').split(',')[0].trim(); const cleanedTitle = trackTitle.split(' - ')[0].trim(); if (cleanedTitle) { let searchQuery = `${cleanedTitle} ${mainArtist}`; if (albumName) searchQuery += ` ${albumName}`; - console.log(`Retry 2 (Cleaned Title): ${searchQuery}`); const searchResults = await api.searchTracks(searchQuery); + 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) - if (!foundTrack && albumName) { - const searchQuery = `${trackTitle} ${albumName}`; - console.log(`Retry 3 (Album): ${searchQuery}`); + // 5. Retry: Title only with first artist + if (!foundTrack) { + const mainArtist = (artistNames || '').split(',')[0].trim(); + const searchQuery = `${trackTitle} ${mainArtist}`; const searchResults = await api.searchTracks(searchQuery); + 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) { tracks.push(foundTrack); - console.log(`Found track: "${trackTitle}" by ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`); + console.log(`✓ "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`); } 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 + ')' : ''}`); } } catch (error) { From 3a63898e732c989a1ab135fd0f449384bc45799d Mon Sep 17 00:00:00 2001 From: jijirae <122718637+jijirae@users.noreply.github.com> Date: Sun, 11 Jan 2026 04:48:33 +0000 Subject: [PATCH 2/2] style: auto-fix linting issues --- js/app.js | 52 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/js/app.js b/js/app.js index 0fb5c98..7743c05 100644 --- a/js/app.js +++ b/js/app.js @@ -1103,34 +1103,46 @@ async function parseCSV(csvText, api, onProgress) { let foundTrack = null; // Helper: Normalize strings for fuzzy matching - const normalize = (str) => str.toLowerCase().replace(/[^\w\s]/g, '').trim(); - + const normalize = (str) => + str + .toLowerCase() + .replace(/[^\w\s]/g, '') + .trim(); + // 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 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); + 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]); + 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); + const albumMatch = + trackAlbum === queryAlbum || + trackAlbum.includes(queryAlbum) || + queryAlbum.includes(trackAlbum); return albumMatch; // Prefer album matches } - + return true; }; @@ -1165,7 +1177,7 @@ async function parseCSV(csvText, api, onProgress) { let searchQuery = `${trackTitle} ${mainArtist}`; if (albumName) searchQuery += ` ${albumName}`; const searchResults = await api.searchTracks(searchQuery); - + if (searchResults.items && searchResults.items.length > 0) { for (const result of searchResults.items) { if (isValidMatch(result, trackTitle, mainArtist, albumName)) { @@ -1182,7 +1194,7 @@ async function parseCSV(csvText, api, onProgress) { 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)) { @@ -1202,7 +1214,7 @@ async function parseCSV(csvText, api, onProgress) { let searchQuery = `${cleanedTitle} ${mainArtist}`; if (albumName) searchQuery += ` ${albumName}`; const searchResults = await api.searchTracks(searchQuery); - + if (searchResults.items && searchResults.items.length > 0) { for (const result of searchResults.items) { if (isValidMatch(result, cleanedTitle, mainArtist, albumName)) { @@ -1220,7 +1232,7 @@ async function parseCSV(csvText, api, onProgress) { const mainArtist = (artistNames || '').split(',')[0].trim(); const searchQuery = `${trackTitle} ${mainArtist}`; const searchResults = await api.searchTracks(searchQuery); - + if (searchResults.items && searchResults.items.length > 0) { // For title-only search, be more lenient for (const result of searchResults.items) { @@ -1239,8 +1251,12 @@ async function parseCSV(csvText, api, onProgress) { tracks.push(foundTrack); console.log(`✓ "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`); } else { - console.warn(`✗ Track not found: "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}`); - missingTracks.push(`${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}`); + console.warn( + `✗ Track not found: "${trackTitle}" by ${artistNames}${albumName ? ' [' + albumName + ']' : ''}` + ); + missingTracks.push( + `${trackTitle} - ${artistNames}${albumName ? ' (album: ' + albumName + ')' : ''}` + ); } } catch (error) { console.error(`Error searching for track "${trackTitle}":`, error);