package library import ( "testing" "time" ) func TestProposeSurvivor(t *testing.T) { older := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) newer := older.Add(24 * time.Hour) cases := []struct { name string cands []SurvivorCandidate wantID string wantReason string }{ { // Lossless wins even against a much larger lossy file, and even // when the lossy copy has been in the library longer. name: "lossless beats larger and older", cands: []SurvivorCandidate{ {TrackID: "mp3", FileFormat: "mp3", FileSize: 90_000_000, AddedAt: older}, {TrackID: "flac", FileFormat: "FLAC", FileSize: 30_000_000, AddedAt: newer}, }, wantID: "flac", wantReason: "lossless (flac)", }, { // m4a may be AAC; it must not outrank an mp3 just for being m4a. name: "m4a is not treated as lossless", cands: []SurvivorCandidate{ {TrackID: "m4a", FileFormat: "m4a", FileSize: 5_000_000, AddedAt: older}, {TrackID: "mp3", FileFormat: "mp3", FileSize: 9_000_000, AddedAt: newer}, }, wantID: "mp3", wantReason: "largest file", }, { name: "larger file wins among lossy copies", cands: []SurvivorCandidate{ {TrackID: "128k", FileFormat: "mp3", FileSize: 3_400_000, AddedAt: older}, {TrackID: "320k", FileFormat: "mp3", FileSize: 8_600_000, AddedAt: newer}, }, wantID: "320k", wantReason: "largest file", }, { // The #3885 pair: identical audio, sizes equal but for the tags. name: "the longest-standing copy wins when size ties", cands: []SurvivorCandidate{ {TrackID: "www-02", FileFormat: "mp3", FileSize: 6_900_000, AddedAt: newer}, {TrackID: "www-01", FileFormat: "mp3", FileSize: 6_900_000, AddedAt: older}, }, wantID: "www-01", wantReason: "in the library longest", }, { name: "a full tie falls back to the lowest id, stably", cands: []SurvivorCandidate{ {TrackID: "b", FileFormat: "mp3", FileSize: 1, AddedAt: older}, {TrackID: "a", FileFormat: "mp3", FileSize: 1, AddedAt: older}, }, wantID: "a", wantReason: "copies are otherwise identical", }, { name: "one copy", cands: []SurvivorCandidate{{TrackID: "only", FileFormat: "mp3", FileSize: 1, AddedAt: older}}, wantID: "only", wantReason: "the only copy", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { id, reason := ProposeSurvivor(tc.cands) if id != tc.wantID || reason != tc.wantReason { t.Fatalf("ProposeSurvivor = (%q, %q), want (%q, %q)", id, reason, tc.wantID, tc.wantReason) } }) } } // The reason must name the rule that decided. Across three copies that is the // comparison between first and second place, not the first rule any pair // differs on: here the lossy copy differs from the others by format, but the // two FLACs are separated by size. func TestProposeSurvivor_ReasonIsTheDecidingRule(t *testing.T) { at := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) id, reason := ProposeSurvivor([]SurvivorCandidate{ {TrackID: "mp3", FileFormat: "mp3", FileSize: 99_000_000, AddedAt: at}, {TrackID: "flac-small", FileFormat: "flac", FileSize: 20_000_000, AddedAt: at}, {TrackID: "flac-big", FileFormat: "flac", FileSize: 40_000_000, AddedAt: at}, }) if id != "flac-big" || reason != "largest file" { t.Fatalf("got (%q, %q), want (flac-big, largest file)", id, reason) } } // M498 #5436: the copy Lidarr maps outranks everything, then the copy whose // tags fit the album; quality decides only between copies equal on both. Each // case is built so the older rules would choose the other copy, so a reorder // that drops the new rules fails. func TestProposeSurvivor_LidarrThenTagFit(t *testing.T) { at := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) cases := []struct { name string cands []SurvivorCandidate wantID string wantReason string }{ { name: "tracked beats lossless and better tags", cands: []SurvivorCandidate{ {TrackID: "flac", FileFormat: "flac", FileSize: 40_000_000, AddedAt: at, TagFit: 3}, {TrackID: "tracked", FileFormat: "mp3", FileSize: 5_000_000, AddedAt: at, TagFit: 1, LidarrTracked: true}, }, wantID: "tracked", wantReason: "the copy Lidarr tracks", }, { // The Humanz shape: two rips a few bytes apart, one carrying a // video marker and clashing with another track's position. name: "tag fit beats a few bytes", cands: []SurvivorCandidate{ {TrackID: "rip", FileFormat: "mp3", FileSize: 5_104_498, AddedAt: at, TagFit: 1}, {TrackID: "clean", FileFormat: "mp3", FileSize: 5_104_492, AddedAt: at, TagFit: 3}, }, wantID: "clean", wantReason: "tags fit the album", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { id, reason := ProposeSurvivor(tc.cands) if id != tc.wantID || reason != tc.wantReason { t.Fatalf("ProposeSurvivor = (%q, %q), want (%q, %q)", id, reason, tc.wantID, tc.wantReason) } }) } } func TestTagFitScore(t *testing.T) { cases := []struct { name string hasTrackNumber bool clash bool path string hasMBID bool want int }{ {"clean, numbered, identified", true, false, "/music/A/B/A - B - 01 - Song.flac", true, 3}, {"position taken by another track", true, true, "/music/A/B/A - B - 01 - Song.flac", true, 2}, {"no track number", false, false, "/music/A/B/Song.flac", true, 2}, {"video rip", true, false, "/music/A/B/A - B - 01 - Song (Official Video).mp3", true, 2}, {"rip with nothing else", false, false, "/music/A/B/Song (Official Video).mp3", false, 0}, } for _, tc := range cases { if got := TagFitScore(tc.hasTrackNumber, tc.clash, tc.path, tc.hasMBID); got != tc.want { t.Errorf("%s: TagFitScore = %d, want %d", tc.name, got, tc.want) } } }