package engine import ( "path/filepath" "strings" "testing" "github.com/cdnutter/playlink/internal/store" ) func enableApprox(t *testing.T, ev *env) { t.Helper() ev.pair.ApproxMatching = true ev.pair.ApproxThreshold = 85 if err := ev.st.SavePair(ctx, &ev.pair); err != nil { t.Fatal(err) } } // The user's example: the master album has disc folders, the slave does not. func TestApproxDiscFolderUpgrade(t *testing.T) { ev := setup(t, false) master := ev.write(ev.master, "Andrew Lloyd Webber/Cats/Disc 02/07 Memory.flac", "lossless") ev.write(ev.master, "Andrew Lloyd Webber/Cats/Disc 01/07 Macavity.flac", "other") ev.write(ev.master, "Andrew Lloyd Webber/Evita/03 Memory.flac", "same title, other album") ev.write(ev.slave, "Andrew Lloyd Webber/Cats/07 Memory.mp3", "lossy") // Off by default: the title exists on two albums, so nothing matches. ev.scan() if m := ev.match("Andrew Lloyd Webber/Cats/07 Memory.mp3"); m.Status != store.StatusUnmatched { t.Fatalf("approximate matching must be opt-in, got %s/%s", m.Status, m.Kind) } enableApprox(t, ev) ev.scan() m := ev.match("Andrew Lloyd Webber/Cats/07 Memory.mp3") if m.Kind != store.KindApprox || m.Status != store.StatusSimilar || m.RelMaster != "Andrew Lloyd Webber/Cats/Disc 02/07 Memory.flac" { t.Fatalf("unexpected match %s %s -> %s", m.Status, m.Kind, m.RelMaster) } if !strings.Contains(m.Reason, "disc folder ignored") || !strings.Contains(m.Reason, "track 07 matches") { t.Fatalf("reason should explain the match, got %q", m.Reason) } // Linking upgrades the slave: it gets the master's FLAC in its own folder. if _, err := ev.e.Link(ctx, m.ID, LinkOptions{}); err != nil { t.Fatal(err) } upgraded := filepath.Join(ev.slave, "Andrew Lloyd Webber/Cats/07 Memory.flac") if !sameInode(t, master, upgraded) { t.Fatal("slave should now hold a hardlink of the master FLAC") } } func TestApproxTitleVariations(t *testing.T) { ev := setup(t, false) enableApprox(t, ev) ev.write(ev.master, "Band/Album/CD1/03 - Dont Stop Believin.flac", "a") ev.write(ev.slave, "Band/Album/3 Don't Stop Believing.mp3", "b") // typo-level difference ev.write(ev.master, "Band/Album/CD1/04 - Song.flac", "c") ev.write(ev.slave, "Band/Album/04 Song (Live).mp3", "d") // different version: must not match ev.write(ev.master, "Band/Album/CD2/05 Totally Different.flac", "e") ev.write(ev.slave, "Band/Album/05 Something Else.mp3", "f") // too dissimilar ev.scan() if m := ev.match("Band/Album/3 Don't Stop Believing.mp3"); m.Kind != store.KindApprox { t.Fatalf("near-identical title should match approximately, got %s", m.Kind) } if m := ev.match("Band/Album/04 Song (Live).mp3"); m.Status != store.StatusUnmatched { t.Fatalf("live version must not match the studio track, got %s -> %s", m.Kind, m.RelMaster) } if m := ev.match("Band/Album/05 Something Else.mp3"); m.Status != store.StatusUnmatched { t.Fatalf("dissimilar titles must not match, got %s -> %s", m.Kind, m.RelMaster) } } func TestApproxAmbiguousIsLeftAlone(t *testing.T) { ev := setup(t, false) enableApprox(t, ev) // Two discs each carry an equally good candidate: refuse to guess. ev.write(ev.master, "Band/Album/Disc 1/Anthem.flac", "a") ev.write(ev.master, "Band/Album/Disc 2/Anthem.flac", "b") ev.write(ev.slave, "Band/Album/Anthem.mp3", "c") ev.scan() if m := ev.match("Band/Album/Anthem.mp3"); m.Status != store.StatusUnmatched { t.Fatalf("ambiguous candidates must not be matched, got %s -> %s", m.Kind, m.RelMaster) } } func TestApproxAlbumFolderRename(t *testing.T) { ev := setup(t, false) enableApprox(t, ev) ev.write(ev.master, "Cast/Cats (Original London Cast)/Disc 2/07 Memory.flac", "a") ev.write(ev.slave, "Cast/Cats/07. Memory.mp3", "b") ev.scan() m := ev.match("Cast/Cats/07. Memory.mp3") if m.Kind != store.KindApprox || !strings.Contains(m.Reason, "different album folder") { t.Fatalf("should match across album folder names, got %s %q", m.Kind, m.Reason) } } func TestTitleScore(t *testing.T) { cases := []struct { a, b string min float64 max float64 }{ {"07 Memory.flac", "07 Memory.mp3", 1, 1}, {"07 Memory.flac", "08 Memory.mp3", 0.9, 0.96}, {"Memory.flac", "Memory (Live).flac", -1, -1}, {"Song (Remix).flac", "Song (Remix).mp3", 1, 1}, {"Love.flac", "Love Story.flac", 0, 0.6}, {"Memory from Cats.flac", "Memory from Cats Remastered.flac", 0.85, 1}, } for _, c := range cases { got := titleScore(parseTitle(c.a), parseTitle(c.b)) if got < c.min-1e-9 || got > c.max+1e-9 { t.Errorf("titleScore(%q, %q) = %.3f, want [%.2f, %.2f]", c.a, c.b, got, c.min, c.max) } } if k := albumKey("A/Cats/Disc 02/07 Memory.flac"); k != "a/cats" { t.Errorf("albumKey = %q", k) } if k := albumKey("A/Cats/CD2 - Bonus/x.flac"); k != "a/cats" { t.Errorf("albumKey = %q", k) } }