github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/update/scan_test.go (about) 1 // Copyright 2020 Daniel Erat. 2 // All rights reserved. 3 4 package update 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "reflect" 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 ) 20 21 // getSongsFromChannel reads and returns num songs from ch. 22 // If an error was sent to the channel, it is returned. 23 func getSongsFromChannel(ch chan songOrErr, num int) ([]db.Song, error) { 24 var songs []db.Song 25 for i := 0; i < num; i++ { 26 s := <-ch 27 if s.err != nil { 28 return nil, fmt.Errorf("got error at position %v instead of song: %v", i, s.err) 29 } 30 songs = append(songs, *s.song) 31 } 32 return songs, nil 33 } 34 35 type scanTestOptions struct { 36 metadataDir string // client.Config.MetadataDir 37 artistRewrites map[string]string // client.Config.ArtistRewrites 38 albumIDRewrites map[string]string // client.Config.AlbumIDRewrites 39 lastUpdateDirs []string // scanForUpdatedSongs lastUpdateDirs param 40 forceGlob string // scanOptions.forceGlob 41 } 42 43 func scanAndCompareSongs(t *testing.T, desc, dir string, lastUpdateTime time.Time, 44 testOpts *scanTestOptions, expected []db.Song) (dirs []string) { 45 cfg := &client.Config{MusicDir: dir} 46 opts := &scanOptions{} 47 var lastUpdateDirs []string 48 if testOpts != nil { 49 cfg.MetadataDir = testOpts.metadataDir 50 cfg.ArtistRewrites = testOpts.artistRewrites 51 cfg.AlbumIDRewrites = testOpts.albumIDRewrites 52 opts.forceGlob = testOpts.forceGlob 53 lastUpdateDirs = testOpts.lastUpdateDirs 54 } 55 ch := make(chan songOrErr) 56 num, dirs, err := scanForUpdatedSongs(cfg, lastUpdateTime, lastUpdateDirs, ch, opts) 57 if err != nil { 58 t.Errorf("%v: %v", desc, err) 59 return dirs 60 } 61 actual, err := getSongsFromChannel(ch, num) 62 if err != nil { 63 t.Errorf("%v: %v", desc, err) 64 return dirs 65 } 66 for i := range expected { 67 found := false 68 for j := range actual { 69 if expected[i].Filename == actual[j].Filename { 70 found = true 71 if expected[i].RecordingID != actual[j].RecordingID { 72 t.Errorf("%v: song %v didn't have expected recording id: expected %q, actual %q", 73 desc, i, expected[i].RecordingID, actual[j].RecordingID) 74 return dirs 75 } 76 expected[i].Rating = 0 77 expected[i].TrackGain = 0 78 expected[i].AlbumGain = 0 79 expected[i].PeakAmp = 0 80 break 81 } 82 } 83 if !found { 84 t.Errorf("%v: didn't get song %v", desc, i) 85 } 86 } 87 if err := test.CompareSongs(expected, actual, test.IgnoreOrder); err != nil { 88 t.Errorf("%v: %v", desc, err) 89 } 90 return dirs 91 } 92 93 func TestScanAndCompareSongs(t *testing.T) { 94 dir := t.TempDir() 95 test.Must(t, test.CopySongs(dir, test.Song0s.Filename, test.Song1s.Filename)) 96 startTime := time.Now() 97 scanAndCompareSongs(t, "initial", dir, time.Time{}, nil, []db.Song{test.Song0s, test.Song1s}) 98 scanAndCompareSongs(t, "unchanged", dir, startTime, nil, []db.Song{}) 99 100 test.Must(t, test.CopySongs(dir, test.Song5s.Filename)) 101 addTime := time.Now() 102 scanAndCompareSongs(t, "add", dir, startTime, nil, []db.Song{test.Song5s}) 103 104 if err := os.Remove(filepath.Join(dir, test.Song0s.Filename)); err != nil { 105 t.Fatal("Failed removing song: ", err) 106 } 107 test.Must(t, test.CopySongs(dir, test.Song0sUpdated.Filename)) 108 updateTime := time.Now() 109 scanAndCompareSongs(t, "update", dir, addTime, nil, []db.Song{test.Song0sUpdated}) 110 111 subdir := filepath.Join(dir, "foo") 112 if err := os.Mkdir(subdir, 0700); err != nil { 113 t.Fatal("Failed making subdir: ", err) 114 } 115 renamedPath := filepath.Join(subdir, test.Song1s.Filename) 116 if err := os.Rename(filepath.Join(dir, test.Song1s.Filename), renamedPath); err != nil { 117 t.Fatal("Failed renaming song: ", err) 118 } 119 now := time.Now() 120 if err := os.Chtimes(renamedPath, now, now); err != nil { 121 t.Fatal("Failed setting times: ", err) 122 } 123 renamedSong1s := test.Song1s 124 renamedSong1s.Filename = filepath.Join(filepath.Base(subdir), test.Song1s.Filename) 125 scanAndCompareSongs(t, "rename", dir, updateTime, nil, []db.Song{renamedSong1s}) 126 127 scanAndCompareSongs(t, "force exact", dir, updateTime, 128 &scanTestOptions{forceGlob: test.Song0sUpdated.Filename}, 129 []db.Song{test.Song0sUpdated}) 130 scanAndCompareSongs(t, "force wildcard", dir, updateTime, 131 &scanTestOptions{forceGlob: "foo/*"}, 132 []db.Song{renamedSong1s}) 133 134 updateTime = time.Now() 135 test.Must(t, test.CopySongs(dir, test.ID3V1Song.Filename)) 136 scanAndCompareSongs(t, "id3v1", dir, updateTime, nil, []db.Song{test.ID3V1Song}) 137 } 138 139 func TestScanAndCompareSongs_Rewrite(t *testing.T) { 140 dir := t.TempDir() 141 142 newSong1s := test.Song1s 143 newSong1s.Artist = "Rewritten Artist" 144 145 // The album name, disc number, and disc subtitle should all be derived from 146 // the original "Another Album (disc 3: The Third Disc)" album name. 147 newSong5s := test.Song5s 148 newSong5s.Album = "Another Album" 149 newSong5s.AlbumID = "bf7ff94c-2a6a-4357-a30e-71da8c117ebc" 150 newSong5s.CoverID = test.Song5s.AlbumID 151 newSong5s.Disc = 3 152 newSong5s.DiscSubtitle = "The Third Disc" 153 154 // This also verifies that Song10s's TSST frame is used to fill DiscSubtitle. 155 test.Must(t, test.CopySongs(dir, test.Song1s.Filename, test.Song5s.Filename, test.Song10s.Filename)) 156 opts := &scanTestOptions{ 157 artistRewrites: map[string]string{test.Song1s.Artist: newSong1s.Artist}, 158 albumIDRewrites: map[string]string{test.Song5s.AlbumID: newSong5s.AlbumID}, 159 } 160 scanAndCompareSongs(t, "initial", dir, time.Time{}, opts, []db.Song{newSong1s, newSong5s, test.Song10s}) 161 } 162 163 func TestScanAndCompareSongs_NewFiles(t *testing.T) { 164 dir := t.TempDir() 165 const ( 166 oldArtist = "old_artist" 167 oldAlbum = "old_album" 168 newAlbum = "new_album" 169 newArtist = "new_artist" 170 ) 171 172 // Start out with an artist/album directory containing a single song. 173 musicDir := filepath.Join(dir, "music") 174 test.Must(t, test.CopySongs(filepath.Join(musicDir, oldArtist, oldAlbum), test.Song0s.Filename)) 175 176 // Copy some more songs into the temp dir to give them old timestamps, 177 // but don't move them under the music dir yet. 178 test.Must(t, test.CopySongs(dir, test.Song1s.Filename)) 179 test.Must(t, test.CopySongs(filepath.Join(dir, newAlbum), test.Song5s.Filename)) 180 test.Must(t, test.CopySongs(filepath.Join(dir, newArtist, newAlbum), test.ID3V1Song.Filename)) 181 182 // Updates the supplied song's filename to be under dir. 183 gs := func(s db.Song, dir string) db.Song { 184 s.Filename = filepath.Join(dir, s.Filename) 185 return s 186 } 187 188 startTime := time.Now() 189 origDirs := scanAndCompareSongs(t, "initial", musicDir, time.Time{}, nil, 190 []db.Song{gs(test.Song0s, filepath.Join(oldArtist, oldAlbum))}) 191 if want := []string{filepath.Join(oldArtist, oldAlbum)}; !reflect.DeepEqual(origDirs, want) { 192 t.Errorf("scanAndCompareSongs(...) = %v; want %v", origDirs, want) 193 } 194 195 // Move the new files into various locations under the music dir. 196 mv := func(src, dst string) { 197 if err := os.Rename(src, dst); err != nil { 198 t.Fatal("Failed renaming file: ", err) 199 } 200 } 201 waitCtime(t, dir, startTime) 202 mv(filepath.Join(dir, test.Song1s.Filename), 203 filepath.Join(musicDir, oldArtist, oldAlbum, test.Song1s.Filename)) 204 mv(filepath.Join(dir, newAlbum), 205 filepath.Join(musicDir, oldArtist, newAlbum)) 206 mv(filepath.Join(dir, newArtist), filepath.Join(musicDir, newArtist)) 207 208 // All three of the new songs should be seen. 209 updateTime := time.Now() 210 newDirs := scanAndCompareSongs(t, "updated", musicDir, startTime, 211 &scanTestOptions{lastUpdateDirs: origDirs}, 212 []db.Song{ 213 gs(test.Song1s, filepath.Join(oldArtist, oldAlbum)), 214 gs(test.Song5s, filepath.Join(oldArtist, newAlbum)), 215 gs(test.ID3V1Song, filepath.Join(newArtist, newAlbum)), 216 }) 217 allDirs := []string{ 218 filepath.Join(newArtist, newAlbum), 219 filepath.Join(oldArtist, newAlbum), 220 filepath.Join(oldArtist, oldAlbum), 221 } 222 if !reflect.DeepEqual(newDirs, allDirs) { 223 t.Errorf("scanAndCompareSongs(...) = %v; want %v", newDirs, allDirs) 224 } 225 226 // Do one more scan and check that no songs are returned. 227 newDirs = scanAndCompareSongs(t, "rescan", musicDir, updateTime, 228 &scanTestOptions{lastUpdateDirs: newDirs}, []db.Song{}) 229 if !reflect.DeepEqual(newDirs, allDirs) { 230 t.Errorf("scanAndCompareSongs(...) = %v; want %v", newDirs, allDirs) 231 } 232 } 233 234 func TestScanAndCompareSongs_OverrideMetadata(t *testing.T) { 235 td := t.TempDir() 236 musicDir := filepath.Join(td, "music") 237 test.Must(t, test.CopySongs(musicDir, test.Song1s.Filename)) 238 239 metadataDir := filepath.Join(td, "metadata") 240 cfg := &client.Config{MusicDir: musicDir, MetadataDir: metadataDir} 241 opts := &scanTestOptions{metadataDir: metadataDir} 242 243 // Perform an initial scan to pick up the song. 244 startTime := time.Now() 245 scanAndCompareSongs(t, "initial", musicDir, time.Time{}, opts, []db.Song{test.Song1s}) 246 247 // Write a file to override the song's metadata. 248 updated := test.Song1s 249 updated.Artist = "New Artist" 250 updated.Title = "New Title" 251 waitCtime(t, metadataDir, startTime) 252 test.Must(t, files.UpdateMetadataOverride(cfg, &updated)) 253 254 // The next scan should pick up the new metadata even though the song file wasn't updated. 255 overrideTime := time.Now() 256 scanAndCompareSongs(t, "wrote override", musicDir, startTime, opts, []db.Song{updated}) 257 258 // Clear the override file and scan again to go back to the original metadata. 259 waitCtime(t, metadataDir, overrideTime) 260 test.Must(t, files.UpdateMetadataOverride(cfg, &test.Song1s)) 261 clearTime := time.Now() 262 scanAndCompareSongs(t, "cleared override", musicDir, overrideTime, opts, []db.Song{test.Song1s}) 263 264 // Nothing should happen after doing a scan without any changes. 265 scanAndCompareSongs(t, "no change", musicDir, clearTime, opts, []db.Song{}) 266 } 267 268 // TODO: Test errors, skipping bogus files, etc. 269 270 // waitCtime waits until the kernel is assigning ctimes after ref to new files in dir. 271 // This is super-cheesy, but ctimes (and mtimes?) appear to get rounded, so it's sometimes 272 // (often) the case that a newly-created file's ctime/mtime will precede the time returned 273 // by an earlier time.Now() call. 274 func waitCtime(t *testing.T, dir string, ref time.Time) { 275 if err := os.MkdirAll(dir, 0755); err != nil { 276 t.Fatal(err) 277 } 278 for { 279 if func() time.Time { 280 f, err := ioutil.TempFile(dir, "temp.") 281 if err != nil { 282 t.Fatal(err) 283 } 284 defer os.Remove(f.Name()) 285 defer f.Close() 286 fi, err := f.Stat() 287 if err != nil { 288 t.Fatal(err) 289 } 290 return getCtime(fi) 291 }().After(ref) { 292 break 293 } 294 time.Sleep(time.Millisecond) 295 } 296 }