package engine import ( "bytes" "fmt" "image" "image/color" "image/png" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/cdnutter/playlink/internal/art" "github.com/cdnutter/playlink/internal/store" "github.com/cdnutter/playlink/internal/tags" ) func squarePNG(n int) []byte { img := image.NewRGBA(image.Rect(0, 0, n, n)) for i := 0; i < n; i++ { img.Set(i, i, color.RGBA{0, 0, 200, 255}) } var b bytes.Buffer png.Encode(&b, img) return b.Bytes() } // artEnv is a tag-editing env with an art store and a fake iTunes that knows // one album: Avenged Sevenfold - Nightmare. func artEnv(t *testing.T) *env { t.Helper() ev := tagEnv(t, false) var base string mux := http.NewServeMux() mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"results":[{"artistName":"Avenged Sevenfold","collectionName":"Nightmare (Deluxe Edition)","artworkUrl100":"%s/img/100x100bb.jpg"}]}`, base) }) mux.HandleFunc("/img/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.Write(squarePNG(1200)) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) base = srv.URL c := art.New("test") c.HTTP = srv.Client() c.ITunesURL = base ff, _ := exec.LookPath("ffmpeg") ev.e.SetArt(c, &art.Store{Dir: filepath.Join(ev.root, "art-cache"), FFmpeg: ff}) ev.st.SetSetting(ctx, "art_sources", "itunes") return ev } func prepared(t *testing.T, ev *env) string { t.Helper() p, err := ev.e.artStore.Prepare(ctx, squarePNG(800), 1000) if err != nil { t.Fatal(err) } return p.Token } func TestAlbumEditWithArt(t *testing.T) { ev := artEnv(t) dir := "Avenged Sevenfold/Nightmare" mkTagged(t, filepath.Join(ev.slave, dir, "a.mp3"), "title", "x", "artist", "Avenged Sevenfold", "album", "Nightmar") mkTagged(t, filepath.Join(ev.slave, dir, "Disc 2/b.flac"), "title", "y", "artist", "Avenged Sevenfold", "album", "Nightmar") mkTagged(t, filepath.Join(ev.slave, dir, "c.ogg"), "title", "z", "artist", "Avenged Sevenfold", "album", "Nightmar") a, err := ev.e.AlbumSongs(ctx, ev.pair.ID, SideSlave, dir) if err != nil || len(a.Songs) != 3 { t.Fatalf("album should include the disc folder: %+v %v", a, err) } req := TagEdit{Side: SideSlave, Set: map[string]string{tags.Album: "Nightmare", tags.Genre: "Metal", tags.TrackTotal: "3"}, Files: []FileEdit{ {Path: dir + "/a.mp3", Set: map[string]string{tags.Title: "Nightmare", tags.Track: "1"}}, {Path: dir + "/Disc 2/b.flac", Set: map[string]string{tags.Title: "Welcome to the Family", tags.Track: "2"}}, {Path: dir + "/c.ogg", Set: map[string]string{tags.Title: "Danger Line", tags.Track: "3"}}, }, Art: &ArtApply{Token: prepared(t, ev), Embed: true, FolderFile: true}} if _, err := ev.e.StartTagEdit(ev.pair.ID, req); err != nil { t.Fatal(err) } j := ev.wait() if j.Results["edited"] != 3 || j.Results["folder art"] != 1 { t.Fatalf("results %+v %v", j.Results, j.Skipped) } for rel, title := range map[string]string{"a.mp3": "Nightmare", "Disc 2/b.flac": "Welcome to the Family", "c.ogg": "Danger Line"} { p := filepath.Join(ev.slave, dir, rel) f, _ := ev.e.tagTools.Read(ctx, p) if f.Fields[tags.Title] != title || f.Fields[tags.Album] != "Nightmare" || f.Fields[tags.Genre] != "Metal" { t.Errorf("%s: fields %+v", rel, f.Fields) } wantCover := !strings.HasSuffix(rel, ".ogg") if ev.e.tagTools.HasCover(ctx, p) != wantCover { t.Errorf("%s: embedded cover should be %v", rel, wantCover) } } if _, err := os.Stat(filepath.Join(ev.slave, dir, "cover.jpg")); err != nil { t.Fatal("the album folder should get cover.jpg") } // Undo the folder image: it is removed again. ops, _, _ := ev.st.Ops(ctx, store.OpFilter{PairID: ev.pair.ID}) for _, o := range ops { if o.Action == store.ActCover { if err := ev.e.Undo(ctx, o.ID); err != nil { t.Fatal(err) } } } if _, err := os.Stat(filepath.Join(ev.slave, dir, "cover.jpg")); !os.IsNotExist(err) { t.Fatal("undo should remove the new cover.jpg") } } func TestFolderCoverReplaceKeepsOld(t *testing.T) { ev := artEnv(t) dir := "A/B" mkTagged(t, filepath.Join(ev.slave, dir, "s.flac"), "title", "S", "artist", "A", "album", "B") old := filepath.Join(ev.slave, dir, "cover.jpg") os.WriteFile(old, []byte("old cover"), 0o644) ev.e.StartTagEdit(ev.pair.ID, TagEdit{Side: SideSlave, Paths: []string{dir + "/s.flac"}, Art: &ArtApply{Token: prepared(t, ev), FolderFile: true}}) ev.wait() if b, _ := os.ReadFile(old); string(b) == "old cover" { t.Fatal("cover.jpg should be replaced") } ops, _, _ := ev.st.Ops(ctx, store.OpFilter{PairID: ev.pair.ID, Limit: 1}) if ops[0].Action != store.ActCover || ops[0].QuarantinePath == "" { t.Fatalf("old cover should be quarantined: %+v", ops[0]) } if err := ev.e.Undo(ctx, ops[0].ID); err != nil { t.Fatal(err) } if b, _ := os.ReadFile(old); string(b) != "old cover" { t.Fatal("undo should restore the old cover.jpg") } } func TestAutoMatchArt(t *testing.T) { ev := artEnv(t) mkTagged(t, filepath.Join(ev.slave, "Avenged Sevenfold/Nightmare/01.mp3"), "title", "Nightmare", "artist", "Avenged Sevenfold", "album", "Nightmare") mkTagged(t, filepath.Join(ev.slave, "Other/Record/01.mp3"), "title", "Song", "artist", "Other Band", "album", "Record") m := ArtMatch{Side: SideSlave, OnlyMissing: true, Embed: true, FolderFile: true} if _, err := ev.e.StartArtAutoMatch(ev.pair.ID, m); err != nil { t.Fatal(err) } j := ev.wait() if j.Results["albums matched"] != 1 || len(j.Notes) != 1 || !strings.Contains(strings.Join(j.Skipped, "\n"), "Other Band – Record: no confident match") { t.Fatalf("results %+v notes %v skipped %v", j.Results, j.Notes, j.Skipped) } if !ev.e.tagTools.HasCover(ctx, filepath.Join(ev.slave, "Avenged Sevenfold/Nightmare/01.mp3")) { t.Fatal("the matched album should get embedded art") } if ev.e.tagTools.HasCover(ctx, filepath.Join(ev.slave, "Other/Record/01.mp3")) { t.Fatal("an unmatched album must not get art") } // Running again: the matched album already has art. ev.e.StartArtAutoMatch(ev.pair.ID, m) if j := ev.wait(); j.Results["already have art"] != 1 { t.Fatalf("second run: %+v", j.Results) } } func TestBestArt(t *testing.T) { res := []art.Result{ {Source: "itunes", Artist: "Avenged Sevenfold", Title: "Nightmare (Deluxe Edition)", Width: 1000}, {Source: "deezer", Artist: "Avenged Sevenfold", Title: "Hail to the King", Width: 1000}, {Source: "deezer", Artist: "Someone Else", Title: "Nightmare", Width: 1000}, } best, _, ok := bestArt(res, "Avenged Sevenfold", "Nightmare") if !ok || best.Title != "Nightmare (Deluxe Edition)" { t.Fatalf("got %+v", best) } if _, _, ok := bestArt(res, "The Beatles", "Abbey Road"); ok { t.Fatal("no confident match expected") } }