package engine import ( "encoding/json" "os" "path/filepath" "testing" "github.com/cdnutter/playlink/internal/store" ) func song(kv ...string) map[string]string { m := map[string]string{} for i := 0; i < len(kv); i += 2 { m[kv[i]] = kv[i+1] } return m } func TestRenderPath(t *testing.T) { d := DefaultNaming() with := func(mod func(*NamingConfig)) NamingConfig { c := DefaultNaming(); mod(&c); return c } nightmare := song("artist", "Avenged Sevenfold", "album", "Nightmare", "title", "Nightmare", "track", "1", "date", "2010-07-27") cases := []struct { name string c NamingConfig f map[string]string ext string want string }{ {"default", d, nightmare, ".FLAC", "Avenged Sevenfold/Nightmare/01 Nightmare.flac"}, {"multi-disc disc 1 also gets its folder", d, song("artist", "The Beatles", "album", "White Album", "title", "Back in the USSR", "track", "1", "disc", "1", "disc_total", "2"), ".flac", "The Beatles/White Album/Disc 01/01 Back in the USSR.flac"}, {"multi-disc disc 2", d, song("artist", "The Beatles", "album", "White Album", "title", "Birthday", "track", "1", "disc", "2/2"), ".flac", "The Beatles/White Album/Disc 02/01 Birthday.flac"}, {"album artist wins", d, song("artist", "Queen", "album_artist", "Various Artists", "album", "Hits", "title", "Bohemian Rhapsody", "track", "12"), ".mp3", "Various Artists/Hits/12 Bohemian Rhapsody.mp3"}, {"year with empty brackets removed", with(func(c *NamingConfig) { c.AlbumFolder = "{Album Title} ({Release Year})" }), song("artist", "A", "album", "B", "title", "S", "track", "3"), ".mp3", "A/B/03 S.mp3"}, {"year present", with(func(c *NamingConfig) { c.AlbumFolder = "{Album Title} ({Release Year})" }), nightmare, ".flac", "Avenged Sevenfold/Nightmare (2010)/01 Nightmare.flac"}, {"no track number", d, song("artist", "A", "album", "B", "title", "Song"), ".mp3", "A/B/Song.mp3"}, {"separator left by missing number", with(func(c *NamingConfig) { c.TrackFormat = "{track:00} - {Track Title}" }), song("artist", "A", "album", "B", "title", "Song"), ".mp3", "A/B/Song.mp3"}, {"three digit tracks", d, song("artist", "A", "album", "B", "title", "S", "track", "105"), ".mp3", "A/B/105 S.mp3"}, {"NameThe and first character", with(func(c *NamingConfig) { c.ArtistFolder = "{Artist NameFirstCharacter}" c.AlbumFolder = "{Artist NameThe}" }), song("artist", "The Beatles", "album", "X", "title", "S", "track", "1"), ".mp3", "B/Beatles, The/01 S.mp3"}, {"dotted and uppercase tokens", with(func(c *NamingConfig) { c.ArtistFolder = "{Artist.Name}"; c.TrackFormat = "{track:0} {TRACK TITLE}" }), nightmare, ".flac", "Avenged.Sevenfold/Nightmare/1 NIGHTMARE.flac"}, {"clean name", with(func(c *NamingConfig) { c.ArtistFolder = "{Artist CleanName}" }), song("artist", "AC/DC", "album", "B", "title", "S", "track", "1"), ".mp3", "ACDC/B/01 S.mp3"}, {"illegal characters replaced, smart colon", d, song("artist", "AC/DC", "album", "Who Made Who?", "title", "Hells Bells: Live \"1986\"", "track", "7"), ".mp3", "AC-DC/Who Made Who/07 Hells Bells - Live '1986'.mp3"}, {"illegal characters removed, colon deleted", with(func(c *NamingConfig) { c.Illegal = "remove"; c.Colon = "delete" }), song("artist", "AC/DC", "album", "B", "title", "Hells Bells: Live", "track", "7"), ".mp3", "ACDC/B/07 Hells Bells Live.mp3"}, {"colon space-dash-space", with(func(c *NamingConfig) { c.Colon = "space_dash_space" }), song("artist", "A", "album", "B", "title", "Part:Two", "track", "1"), ".mp3", "A/B/01 Part - Two.mp3"}, {"no hidden names", d, song("artist", "...And Justice", "album", "B", "title", "S", "track", "1"), ".mp3", "And Justice/B/01 S.mp3"}, } for _, c := range cases { got, missing := renderPath(c.c, c.ext, c.f) if got != c.want || missing != "" { t.Errorf("%s: got %q (missing %q), want %q", c.name, got, missing, c.want) } } for field, f := range map[string]map[string]string{ "artist or album artist": song("album", "B", "title", "S"), "album": song("artist", "A", "title", "S"), "title": song("artist", "A", "album", "B"), } { if _, missing := renderPath(d, ".mp3", f); missing != field { t.Errorf("want missing %q, got %q", field, missing) } } } func TestValidateNaming(t *testing.T) { bad := []func(*NamingConfig){ func(c *NamingConfig) { c.ArtistFolder = "{Artist Name}/{Album Title}" }, func(c *NamingConfig) { c.TrackFormat = "{track:00}" }, func(c *NamingConfig) { c.TrackFormat = "{Bogus} {Track Title}" }, func(c *NamingConfig) { c.MultiDiscFormat = "../{Track Title}" }, func(c *NamingConfig) { c.AlbumFolder = "" }, func(c *NamingConfig) { c.Colon = "weird" }, func(c *NamingConfig) { c.AlbumFolder = "{Album Title" }, } for i, mod := range bad { c := DefaultNaming() mod(&c) if ValidateNaming(c) == nil { t.Errorf("case %d should be rejected: %+v", i, c) } } for _, p := range NamingPresets { if err := ValidateNaming(p.Config); err != nil { t.Errorf("preset %q invalid: %v", p.Name, err) } } } func TestNamingOfConvertsOldSettings(t *testing.T) { c := NamingOf(store.Pair{OrganizeTemplate: "{artist}/{album} ({year})/{track} - {title}", OrganizeMultiDisc: "prefix", OrganizeDiscDigits: 1}) if c.ArtistFolder != "{Artist Name}" || c.AlbumFolder != "{Album Title} ({Release Year})" || c.TrackFormat != "{track:00} - {Track Title}" || c.MultiDiscFormat != "{medium:0}-{track:00} - {Track Title}" { t.Fatalf("old settings not converted: %+v", c) } if got := NamingOf(store.Pair{}); got != DefaultNaming() { t.Fatalf("pairs without settings should use the defaults, got %+v", got) } raw, _ := json.Marshal(NamingConfig{TrackFormat: "{track:0}. {Track Title}"}) got := NamingOf(store.Pair{OrganizeConfig: raw}) if got.TrackFormat != "{track:0}. {Track Title}" || got.ArtistFolder != "{Artist Name}" { t.Fatalf("stored settings should win and gaps take defaults, got %+v", got) } ex := NamingExamples(DefaultNaming()) if len(ex) != 4 || ex[1].Path != "The Beatles/The Beatles (White Album)/Disc 01/01 Back in the U.S.S.R.flac" { t.Fatalf("examples: %+v", ex) } } func TestRenameNowPreviewAndApply(t *testing.T) { ev := tagEnv(t, false) mkTagged(t, filepath.Join(ev.slave, "in/a.mp3"), "title", "One", "artist", "Band", "album", "LP", "track", "1") mkTagged(t, filepath.Join(ev.slave, "in/b.mp3"), "title", "Two", "artist", "Band", "album", "LP", "track", "2") if _, err := ev.e.StartOrganizePreview(ev.pair.ID, "slave"); err != nil { t.Fatal(err) } ev.wait() plan, items := ev.e.OrganizePreview(ev.pair.ID) if plan == nil || plan.Moves != 2 || len(items) != 2 { t.Fatalf("preview: %+v %+v", plan, items) } var first int for _, it := range items { if it.From == "in/a.mp3" { first = it.ID } } if _, err := ev.e.StartOrganizeApply(ev.pair.ID, []int{first}); err != nil { t.Fatal(err) } if j := ev.wait(); j.Results["renamed"] != 1 { t.Fatalf("only the selected file should be renamed: %+v", j.Results) } if _, err := os.Stat(filepath.Join(ev.slave, "Band/LP/01 One.mp3")); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(ev.slave, "in/b.mp3")); err != nil { t.Fatal("unselected files stay") } if p, _ := ev.e.OrganizePreview(ev.pair.ID); p != nil { t.Fatal("the preview is stale after renaming and must be cleared") } if _, err := ev.e.StartOrganizeApply(ev.pair.ID, nil); err == nil { t.Fatal("applying without a fresh preview must be refused") } }