github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/metadata/command_test.go (about) 1 // Copyright 2023 Daniel Erat. 2 // All rights reserved. 3 4 package metadata 5 6 import ( 7 "context" 8 "encoding/json" 9 "net/http" 10 "net/http/httptest" 11 "path/filepath" 12 "testing" 13 "time" 14 15 "github.com/derat/nup/cmd/nup/client" 16 "github.com/derat/nup/cmd/nup/client/files" 17 "github.com/derat/nup/server/db" 18 "github.com/derat/nup/test" 19 "github.com/google/go-cmp/cmp" 20 "golang.org/x/time/rate" 21 ) 22 23 type testEnv struct { 24 t *testing.T 25 mux *http.ServeMux 26 srv *httptest.Server 27 28 cfg *client.Config 29 api *api 30 31 recordings map[string]recording 32 releases map[string]release 33 } 34 35 func newTestEnv(t *testing.T) *testEnv { 36 dir := t.TempDir() 37 env := testEnv{ 38 t: t, 39 mux: http.NewServeMux(), 40 cfg: &client.Config{ 41 MusicDir: filepath.Join(dir, "music"), 42 MetadataDir: filepath.Join(dir, "metadata"), 43 }, 44 recordings: make(map[string]recording), 45 releases: make(map[string]release), 46 } 47 env.mux.HandleFunc(recPathPrefix, env.handleRecording) 48 env.mux.HandleFunc(relPathPrefix, env.handleRelease) 49 env.srv = httptest.NewServer(env.mux) 50 env.api = newAPI(env.srv.URL) 51 env.api.limiter.SetLimit(rate.Inf) 52 return &env 53 } 54 55 func (env *testEnv) close() { 56 env.srv.Close() 57 } 58 59 func (env *testEnv) handleRecording(w http.ResponseWriter, req *http.Request) { 60 if rec, ok := env.recordings[req.URL.Path[len(recPathPrefix):]]; !ok { 61 http.NotFound(w, req) 62 } else { 63 env.writeJSON(w, rec) 64 } 65 } 66 67 func (env *testEnv) handleRelease(w http.ResponseWriter, req *http.Request) { 68 if rel, ok := env.releases[req.URL.Path[len(relPathPrefix):]]; !ok { 69 http.NotFound(w, req) 70 } else { 71 env.writeJSON(w, rel) 72 } 73 } 74 75 func (env *testEnv) writeJSON(w http.ResponseWriter, obj interface{}) { 76 w.Header().Set("Content-Type", "application/json") 77 if err := json.NewEncoder(w).Encode(obj); err != nil { 78 env.t.Error("Failed writing response:", err) 79 } 80 } 81 82 const ( 83 recPathPrefix = "/ws/2/recording/" 84 relPathPrefix = "/ws/2/release/" 85 ) 86 87 func TestScanSong_Release(t *testing.T) { 88 env := newTestEnv(t) 89 defer env.close() 90 91 song := test.Song0s 92 test.Must(t, test.CopySongs(env.cfg.MusicDir, song.Filename)) 93 94 want := song 95 want.Title = "New Title" 96 want.Artist = "Artist A feat. B & C" 97 want.Album = "New Album" 98 want.AlbumArtist = "Artist A" 99 want.DiscSubtitle = "The Third Disc" 100 want.AlbumID = "5109de80-7946-41ea-9060-05d10da87219" 101 want.RecordingID = "4b834e6e-a694-4cbe-a715-f5b0e36c57e2" 102 want.OrigAlbumID = song.AlbumID 103 want.OrigRecordingID = song.RecordingID 104 want.Track = 4 105 want.Disc = 3 106 want.Date = time.Date(2020, 6, 7, 0, 0, 0, 0, time.UTC) 107 108 want.SHA1 = "" 109 want.Length = 0 110 want.TrackGain = 0 111 want.AlbumGain = 0 112 want.PeakAmp = 0 113 114 env.releases[song.AlbumID] = release{ 115 Title: want.Album, 116 Artists: []artistCredit{{Name: "Artist A"}}, 117 ID: want.AlbumID, 118 Media: []medium{ 119 {Position: 1}, 120 {Position: 2}, 121 { 122 Title: want.DiscSubtitle, 123 Position: 3, 124 Tracks: []track{ 125 {Position: 1}, 126 {Position: 2}, 127 {Position: 3}, 128 { 129 Title: want.Title, 130 Artists: []artistCredit{ 131 {Name: "Artist A", JoinPhrase: " feat. "}, 132 {Name: "B", JoinPhrase: " & "}, 133 {Name: "C"}, 134 }, 135 Recording: recording{ID: want.RecordingID}, 136 Position: 4, 137 }, 138 }, 139 }, 140 }, 141 ReleaseGroup: releaseGroup{FirstReleaseDate: date(want.Date)}, 142 } 143 env.recordings[song.RecordingID] = recording{ID: want.RecordingID} 144 145 ctx := context.Background() 146 p := filepath.Join(env.cfg.MusicDir, song.Filename) 147 test.Must(t, scanSong(ctx, env.cfg, env.api, p, nil, nil)) 148 if got, err := files.ReadSong(env.cfg, p, nil /* fi */, files.SkipAudioData, nil /* gc */); err != nil { 149 t.Error("ReadSong failed:", err) 150 } else if diff := cmp.Diff(want, *got); diff != "" { 151 t.Error("ReadSong returned unexpected results:\n" + diff) 152 } 153 } 154 155 // TODO: Add a test for a standalone recording (non-album track). 156 // I think that this will be hard to do unless I add a song file 157 // with a recording ID but no album ID. 158 159 func TestSetAlbum(t *testing.T) { 160 // Helper functions to make it easier to create objects. 161 ms := func(title, rec string, sec int) *db.Song { 162 return &db.Song{ 163 Title: title, 164 RecordingID: rec, 165 Length: float64(sec), 166 } 167 } 168 mt := func(title, rec string, sec int) track { 169 msec := int64(sec) * 1000 170 return track{ 171 Title: title, 172 Length: msec, 173 Recording: recording{Title: title, ID: rec, Length: msec}, 174 } 175 } 176 mr := func(id string, tls ...[]track) *release { 177 rel := release{ID: id} 178 for _, tl := range tls { 179 rel.Media = append(rel.Media, medium{Tracks: tl}) 180 } 181 return &rel 182 } 183 184 for _, tc := range []struct { 185 desc string 186 songs []*db.Song 187 rel *release 188 recIDs []string 189 want []*db.Song // nil for error 190 }{ 191 { 192 "direct mapping", 193 []*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120)}, 194 mr("album", []track{mt("a0", "1", 60), mt("b0", "2", 40), mt("c0", "3", 120)}), 195 nil, 196 []*db.Song{ms("a0", "1", 60), ms("b0", "2", 40), ms("c0", "3", 120)}, 197 }, 198 { 199 "reordered", 200 []*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120)}, 201 mr("album", []track{mt("c0", "3", 120), mt("a0", "1", 60), mt("b0", "2", 40)}), 202 nil, 203 []*db.Song{ms("a0", "1", 60), ms("b0", "2", 40), ms("c0", "3", 120)}, 204 }, 205 { 206 "new recordings", 207 []*db.Song{ms("a", "1", 60), ms("b", "2", 40), ms("c", "3", 120), ms("d", "4", 50)}, 208 mr("album", []track{mt("a0", "5", 61), mt("b0", "6", 43)}, []track{mt("c0", "7", 122), mt("d0", "8", 50)}), 209 nil, 210 []*db.Song{ms("a0", "5", 60), ms("b0", "6", 40), ms("c0", "7", 120), ms("d0", "8", 50)}, 211 }, 212 { 213 "override recording IDs", 214 []*db.Song{ms("a", "1", 60), ms("b", "2", 40)}, 215 mr("album", []track{mt("a0", "3", 60), mt("c0", "4", 30), mt("b0", "5", 40)}), 216 []string{"3", "5"}, 217 []*db.Song{ms("a0", "3", 60), ms("b0", "5", 40)}, 218 }, 219 { 220 "different lengths", 221 []*db.Song{ms("a", "1", 60), ms("b", "2", 40)}, 222 mr("album", []track{mt("a0", "3", 40), mt("b0", "2", 40)}), 223 nil, 224 nil, // should fail 225 }, 226 { 227 "different track counts", 228 []*db.Song{ms("a", "1", 60), ms("b", "2", 40)}, 229 mr("album", []track{mt("a0", "1", 60)}), 230 nil, 231 nil, // should fail 232 }, 233 { 234 "duplicate recordings", 235 []*db.Song{ms("a", "1", 60), ms("b", "1", 60)}, 236 mr("album", []track{mt("a0", "1", 60), mt("b0", "2", 40)}), 237 nil, 238 nil, // should fail 239 }, 240 } { 241 t.Run(tc.desc, func(t *testing.T) { 242 got, err := setAlbum(tc.songs, tc.rel, tc.recIDs) 243 if err != nil { 244 if tc.want != nil { 245 t.Error("setAlbum failed:", err) 246 } 247 return 248 } else if tc.want == nil { 249 t.Errorf("setAlbum unexpectedly succeeded with %v", got) 250 return 251 } 252 for _, s := range tc.want { 253 s.AlbumID = tc.rel.ID // set for diff 254 } 255 if diff := cmp.Diff(got, tc.want); diff != "" { 256 t.Error("setAlbum returned wrong results:\n" + diff) 257 } 258 }) 259 260 } 261 }