diff --git a/go_backend/title_match_utils.go b/go_backend/title_match_utils.go index d7564050..8690b16b 100644 --- a/go_backend/title_match_utils.go +++ b/go_backend/title_match_utils.go @@ -369,10 +369,52 @@ type resolvedTrackInfo struct { SkipNameVerification bool } +func exactLooseIdentityMatch(expected, found string, normalize func(string) string) bool { + normExpected := normalize(expected) + normFound := normalize(found) + if normExpected != "" && normFound != "" { + return normExpected == normFound + } + return strings.EqualFold(strings.TrimSpace(expected), strings.TrimSpace(found)) +} + +func durationMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo) bool { + expectedDurationSec := req.DurationMS / 1000 + if expectedDurationSec <= 0 || resolved.Duration <= 0 { + return false + } + diff := expectedDurationSec - resolved.Duration + if diff < 0 { + diff = -diff + } + return diff <= 10 +} + +func hasStrongTrackIdentity(req DownloadRequest, resolved resolvedTrackInfo) bool { + if req.TrackName == "" || resolved.Title == "" || + req.ArtistName == "" || resolved.ArtistName == "" { + return false + } + + titleExact := exactLooseIdentityMatch(req.TrackName, resolved.Title, normalizeLooseTitle) + if !titleExact { + return false + } + + artistExact := exactLooseIdentityMatch( + req.ArtistName, + resolved.ArtistName, + normalizeLooseArtistName, + ) + return artistExact || (artistsMatch(req.ArtistName, resolved.ArtistName) && + durationMatchesRequest(req, resolved)) +} + func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPrefix string) bool { exactISRCMatch := req.ISRC != "" && resolved.ISRC != "" && strings.EqualFold(strings.TrimSpace(req.ISRC), strings.TrimSpace(resolved.ISRC)) + conflictingISRC := req.ISRC != "" && resolved.ISRC != "" && !exactISRCMatch if !exactISRCMatch && !resolved.SkipNameVerification { if req.ArtistName != "" && resolved.ArtistName != "" && @@ -391,9 +433,13 @@ func trackMatchesRequest(req DownloadRequest, resolved resolvedTrackInfo, logPre if req.AlbumName != "" && resolved.AlbumName != "" && !titlesMatch(req.AlbumName, resolved.AlbumName) { - GoLog("[%s] Verification failed: album mismatch — expected '%s', got '%s'\n", + if conflictingISRC || !hasStrongTrackIdentity(req, resolved) { + GoLog("[%s] Verification failed: album mismatch — expected '%s', got '%s'\n", + logPrefix, req.AlbumName, resolved.AlbumName) + return false + } + GoLog("[%s] Verification accepted album mismatch for matching track identity — expected '%s', got '%s'\n", logPrefix, req.AlbumName, resolved.AlbumName) - return false } } diff --git a/go_backend/title_match_utils_test.go b/go_backend/title_match_utils_test.go index c41f8286..8a8b0974 100644 --- a/go_backend/title_match_utils_test.go +++ b/go_backend/title_match_utils_test.go @@ -91,6 +91,80 @@ func TestTrackMatchesRequestAcceptsDifferentEditionWithExactISRC(t *testing.T) { } } +func TestTrackMatchesRequestAcceptsSameTrackFromDifferentRelease(t *testing.T) { + req := DownloadRequest{ + TrackName: "Crossing Field", + ArtistName: "LiSA", + AlbumName: "Crossing Field - EP", + DurationMS: 233000, + } + resolved := resolvedTrackInfo{ + Title: "Crossing Field", + ArtistName: "LiSA", + AlbumName: "LANDSPACE", + Duration: 233, + } + + if !trackMatchesRequest(req, resolved, "test") { + t.Fatal("expected the same track to be accepted across release albums") + } +} + +func TestTrackMatchesRequestRejectsAlbumMismatchForDifferentVersion(t *testing.T) { + req := DownloadRequest{ + TrackName: "Song (Live)", + ArtistName: "Artist", + AlbumName: "Live at the Theatre", + } + resolved := resolvedTrackInfo{ + Title: "Song", + ArtistName: "Artist", + AlbumName: "Studio Album", + } + + if trackMatchesRequest(req, resolved, "test") { + t.Fatal("expected an album mismatch to reject a different track version") + } +} + +func TestTrackMatchesRequestRejectsConflictingISRCDespiteStrongNames(t *testing.T) { + req := DownloadRequest{ + TrackName: "Song", + ArtistName: "Artist", + AlbumName: "Original Album", + ISRC: "USAAA2600001", + } + resolved := resolvedTrackInfo{ + Title: "Song", + ArtistName: "Artist", + AlbumName: "Other Album", + ISRC: "USAAA2600002", + } + + if trackMatchesRequest(req, resolved, "test") { + t.Fatal("expected conflicting ISRCs to keep album verification strict") + } +} + +func TestTrackMatchesRequestRejectsDurationMismatchAcrossReleases(t *testing.T) { + req := DownloadRequest{ + TrackName: "Crossing Field", + ArtistName: "LiSA", + AlbumName: "Crossing Field - EP", + DurationMS: 233000, + } + resolved := resolvedTrackInfo{ + Title: "Crossing Field", + ArtistName: "LiSA", + AlbumName: "LANDSPACE", + Duration: 280, + } + + if trackMatchesRequest(req, resolved, "test") { + t.Fatal("expected a large duration mismatch to reject another recording") + } +} + func TestTitlesMatch_SeparatorVariants(t *testing.T) { if !titlesMatch("Doctor / Cops", "Doctor _ Cops") { t.Fatal("expected tidal titlesMatch to accept / vs _ variant")