Skip to content

Commit 85ea8d8

Browse files
committed
refactor(scanner): batch dir lookups / reduce tx overhead
1 parent eb0d8fd commit 85ea8d8

File tree

1 file changed

+71
-32
lines changed

1 file changed

+71
-32
lines changed

scanner/scanner.go

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,15 @@ func (s *Scanner) scanCallback(st *State, absPath string, d fs.DirEntry, err err
250250

251251
log.Printf("processing folder %q", absPath)
252252

253-
return s.db.Transaction(func(tx *db.DB) error {
254-
if err := s.scanDir(tx, st, absPath); err != nil {
255-
st.errs = append(st.errs, fmt.Errorf("%q: %w", absPath, err))
256-
return nil
257-
}
253+
if err := s.scanDir(st, absPath); err != nil {
254+
st.errs = append(st.errs, fmt.Errorf("%q: %w", absPath, err))
258255
return nil
259-
})
256+
}
257+
258+
return nil
260259
}
261260

262-
func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
261+
func (s *Scanner) scanDir(st *State, absPath string) error {
263262
musicDir, relPath := musicDirRelative(s.musicDirs, absPath)
264263
if musicDir == absPath {
265264
return nil
@@ -270,7 +269,7 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
270269
return err
271270
}
272271

273-
var tracks []string
272+
var trackPaths []string
274273
var cover string
275274
for _, item := range items {
276275
absPath := filepath.Join(absPath, item.Name())
@@ -287,55 +286,90 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
287286
continue
288287
}
289288
if s.tagReader.CanRead(absPath) {
290-
tracks = append(tracks, item.Name())
289+
trackPaths = append(trackPaths, item.Name())
291290
continue
292291
}
293292
}
294293

295294
pdir, pbasename := filepath.Split(filepath.Dir(relPath))
296295
var parent db.Album
297-
if err := tx.Where("root_dir=? AND left_path=? AND right_path=?", musicDir, pdir, pbasename).Assign(db.Album{RootDir: musicDir, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(&parent).Error; err != nil {
296+
if err := s.db.Where("root_dir=? AND left_path=? AND right_path=?", musicDir, pdir, pbasename).Assign(db.Album{RootDir: musicDir, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(&parent).Error; err != nil {
298297
return fmt.Errorf("first or create parent: %w", err)
299298
}
300299

301300
st.seenAlbums[parent.ID] = struct{}{}
302301

303302
dir, basename := filepath.Split(relPath)
304303
var album db.Album
305-
if err := populateAlbumBasics(tx, musicDir, &parent, &album, dir, basename, cover); err != nil {
304+
if err := populateAlbumBasics(s.db, musicDir, &parent, &album, dir, basename, cover); err != nil {
306305
return fmt.Errorf("populate album basics: %w", err)
307306
}
308307

309308
st.seenAlbums[album.ID] = struct{}{}
310309

311-
sort.Strings(tracks)
312-
for i, basename := range tracks {
313-
absPath := filepath.Join(musicDir, relPath, basename)
314-
if err := s.populateTrackAndArtists(tx, st, i, &album, basename, absPath); err != nil {
315-
return fmt.Errorf("populate track %q: %w", basename, err)
316-
}
310+
if len(trackPaths) == 0 {
311+
return nil
317312
}
318313

319-
return nil
320-
}
314+
var tracks []*db.Track
315+
if err := s.db.Where("album_id=? AND filename IN (?)", album.ID, trackPaths).Find(&tracks).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
316+
return fmt.Errorf("query track: %w", err)
317+
}
321318

322-
func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db.Album, basename string, absPath string) error {
323-
// useful to get the real create/birth time for filesystems and kernels which support it
324-
timeSpec, err := times.Stat(absPath)
325-
if err != nil {
326-
return fmt.Errorf("get times %q: %w", basename, err)
319+
trackMap := make(map[string]*db.Track, len(tracks))
320+
for _, t := range tracks {
321+
trackMap[t.Filename] = t
322+
st.seenTracks[t.ID] = struct{}{}
327323
}
328324

329-
var track db.Track
330-
if err := tx.Where("album_id=? AND filename=?", album.ID, filepath.Base(basename)).First(&track).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
331-
return fmt.Errorf("query track: %w", err)
325+
type trackUpdate struct {
326+
i int
327+
basename string
328+
absPath string
329+
track *db.Track
330+
timeSpec times.Timespec
332331
}
332+
trackUpdates := make([]trackUpdate, 0, len(trackPaths))
333333

334-
if !st.isFull && track.ID != 0 && timeSpec.ModTime().Before(track.UpdatedAt) {
335-
st.seenTracks[track.ID] = struct{}{}
334+
sort.Strings(trackPaths)
335+
336+
for i, basename := range trackPaths {
337+
absPath := filepath.Join(musicDir, relPath, basename)
338+
339+
timeSpec, err := times.Stat(absPath)
340+
if err != nil {
341+
return fmt.Errorf("get times %q: %w", basename, err)
342+
}
343+
344+
// might be nil if new track
345+
track := trackMap[basename]
346+
347+
if st.isFull || track == nil || timeSpec.ModTime().After(track.UpdatedAt) {
348+
trackUpdates = append(trackUpdates, trackUpdate{
349+
i: i,
350+
basename: basename,
351+
absPath: absPath,
352+
track: track,
353+
timeSpec: timeSpec,
354+
})
355+
}
356+
}
357+
358+
if len(trackUpdates) == 0 {
336359
return nil
337360
}
338361

362+
return s.db.Transaction(func(tx *db.DB) error {
363+
for _, t := range trackUpdates {
364+
if err := s.populateTrackAndArtists(tx, st, t.i, &album, t.track, t.timeSpec, t.basename, t.absPath); err != nil {
365+
return fmt.Errorf("populate track %q: %w", t.basename, err)
366+
}
367+
}
368+
return nil
369+
})
370+
}
371+
372+
func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db.Album, track *db.Track, timeSpec times.Timespec, basename, absPath string) error {
339373
trags, err := s.tagReader.Read(absPath)
340374
if err != nil {
341375
return fmt.Errorf("%w: %w", err, ErrReadingTags)
@@ -387,10 +421,15 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
387421
if err != nil {
388422
return fmt.Errorf("stating %q: %w", basename, err)
389423
}
390-
if err := populateTrack(tx, album, &track, trags, basename, int(stat.Size())); err != nil {
424+
425+
if track == nil {
426+
track = &db.Track{}
427+
}
428+
429+
if err := populateTrack(tx, album, track, trags, basename, int(stat.Size())); err != nil {
391430
return fmt.Errorf("process %q: %w", basename, err)
392431
}
393-
if err := populateTrackGenres(tx, &track, genreIDs); err != nil {
432+
if err := populateTrackGenres(tx, track, genreIDs); err != nil {
394433
return fmt.Errorf("populate track genres: %w", err)
395434
}
396435

@@ -403,7 +442,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
403442
}
404443
trackArtistIDs = append(trackArtistIDs, trackArtist.ID)
405444
}
406-
if err := populateTrackArtists(tx, &track, trackArtistIDs); err != nil {
445+
if err := populateTrackArtists(tx, track, trackArtistIDs); err != nil {
407446
return fmt.Errorf("populate track artists: %w", err)
408447
}
409448

0 commit comments

Comments
 (0)