|
|
|
@@ -61,14 +61,14 @@ type ScanOptions struct {
|
|
|
|
IsFull bool
|
|
|
|
IsFull bool
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) ScanAndClean(opts ScanOptions) (*Context, error) {
|
|
|
|
func (s *Scanner) ScanAndClean(opts ScanOptions) (*State, error) {
|
|
|
|
if !s.StartScanning() {
|
|
|
|
if !s.StartScanning() {
|
|
|
|
return nil, ErrAlreadyScanning
|
|
|
|
return nil, ErrAlreadyScanning
|
|
|
|
}
|
|
|
|
}
|
|
|
|
defer s.StopScanning()
|
|
|
|
defer s.StopScanning()
|
|
|
|
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
c := &Context{
|
|
|
|
st := &State{
|
|
|
|
seenTracks: map[int]struct{}{},
|
|
|
|
seenTracks: map[int]struct{}{},
|
|
|
|
seenAlbums: map[int]struct{}{},
|
|
|
|
seenAlbums: map[int]struct{}{},
|
|
|
|
isFull: opts.IsFull,
|
|
|
|
isFull: opts.IsFull,
|
|
|
|
@@ -77,28 +77,28 @@ func (s *Scanner) ScanAndClean(opts ScanOptions) (*Context, error) {
|
|
|
|
log.Println("starting scan")
|
|
|
|
log.Println("starting scan")
|
|
|
|
defer func() {
|
|
|
|
defer func() {
|
|
|
|
log.Printf("finished scan in %s, +%d/%d tracks (%d err)\n",
|
|
|
|
log.Printf("finished scan in %s, +%d/%d tracks (%d err)\n",
|
|
|
|
durSince(start), c.SeenTracksNew(), c.SeenTracks(), len(c.errs))
|
|
|
|
durSince(start), st.SeenTracksNew(), st.SeenTracks(), len(st.errs))
|
|
|
|
}()
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
for _, dir := range s.musicDirs {
|
|
|
|
for _, dir := range s.musicDirs {
|
|
|
|
err := filepath.WalkDir(dir, func(absPath string, d fs.DirEntry, err error) error {
|
|
|
|
err := filepath.WalkDir(dir, func(absPath string, d fs.DirEntry, err error) error {
|
|
|
|
return s.scanCallback(c, absPath, d, err)
|
|
|
|
return s.scanCallback(st, absPath, d, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("walk: %w", err)
|
|
|
|
return nil, fmt.Errorf("walk: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := s.cleanTracks(c); err != nil {
|
|
|
|
if err := s.cleanTracks(st); err != nil {
|
|
|
|
return nil, fmt.Errorf("clean tracks: %w", err)
|
|
|
|
return nil, fmt.Errorf("clean tracks: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := s.cleanAlbums(c); err != nil {
|
|
|
|
if err := s.cleanAlbums(st); err != nil {
|
|
|
|
return nil, fmt.Errorf("clean albums: %w", err)
|
|
|
|
return nil, fmt.Errorf("clean albums: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := s.cleanArtists(c); err != nil {
|
|
|
|
if err := s.cleanArtists(st); err != nil {
|
|
|
|
return nil, fmt.Errorf("clean artists: %w", err)
|
|
|
|
return nil, fmt.Errorf("clean artists: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := s.cleanGenres(c); err != nil {
|
|
|
|
if err := s.cleanGenres(st); err != nil {
|
|
|
|
return nil, fmt.Errorf("clean genres: %w", err)
|
|
|
|
return nil, fmt.Errorf("clean genres: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -106,7 +106,7 @@ func (s *Scanner) ScanAndClean(opts ScanOptions) (*Context, error) {
|
|
|
|
return nil, fmt.Errorf("set scan time: %w", err)
|
|
|
|
return nil, fmt.Errorf("set scan time: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return c, errors.Join(c.errs...)
|
|
|
|
return st, errors.Join(st.errs...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) ExecuteWatch(done <-chan struct{}) error {
|
|
|
|
func (s *Scanner) ExecuteWatch(done <-chan struct{}) error {
|
|
|
|
@@ -138,7 +138,7 @@ func (s *Scanner) ExecuteWatch(done <-chan struct{}) error {
|
|
|
|
break
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for absPath := range batchSeen {
|
|
|
|
for absPath := range batchSeen {
|
|
|
|
c := &Context{
|
|
|
|
st := &State{
|
|
|
|
seenTracks: map[int]struct{}{},
|
|
|
|
seenTracks: map[int]struct{}{},
|
|
|
|
seenAlbums: map[int]struct{}{},
|
|
|
|
seenAlbums: map[int]struct{}{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -150,7 +150,7 @@ func (s *Scanner) ExecuteWatch(done <-chan struct{}) error {
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = filepath.WalkDir(absPath, func(absPath string, d fs.DirEntry, err error) error {
|
|
|
|
err = filepath.WalkDir(absPath, func(absPath string, d fs.DirEntry, err error) error {
|
|
|
|
return s.scanCallback(c, absPath, d, err)
|
|
|
|
return s.scanCallback(st, absPath, d, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("error walking: %v", err)
|
|
|
|
log.Printf("error walking: %v", err)
|
|
|
|
@@ -207,9 +207,9 @@ func watchCallback(watcher *fsnotify.Watcher, absPath string, d fs.DirEntry, err
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) scanCallback(c *Context, absPath string, d fs.DirEntry, err error) error {
|
|
|
|
func (s *Scanner) scanCallback(st *State, absPath string, d fs.DirEntry, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
c.errs = append(c.errs, err)
|
|
|
|
st.errs = append(st.errs, err)
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -219,7 +219,7 @@ func (s *Scanner) scanCallback(c *Context, absPath string, d fs.DirEntry, err er
|
|
|
|
eval, _ := filepath.EvalSymlinks(absPath)
|
|
|
|
eval, _ := filepath.EvalSymlinks(absPath)
|
|
|
|
return filepath.WalkDir(eval, func(subAbs string, d fs.DirEntry, err error) error {
|
|
|
|
return filepath.WalkDir(eval, func(subAbs string, d fs.DirEntry, err error) error {
|
|
|
|
subAbs = strings.Replace(subAbs, eval, absPath, 1)
|
|
|
|
subAbs = strings.Replace(subAbs, eval, absPath, 1)
|
|
|
|
return s.scanCallback(c, subAbs, d, err)
|
|
|
|
return s.scanCallback(st, subAbs, d, err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
@@ -233,15 +233,15 @@ func (s *Scanner) scanCallback(c *Context, absPath string, d fs.DirEntry, err er
|
|
|
|
log.Printf("processing folder %q", absPath)
|
|
|
|
log.Printf("processing folder %q", absPath)
|
|
|
|
|
|
|
|
|
|
|
|
return s.db.Transaction(func(tx *db.DB) error {
|
|
|
|
return s.db.Transaction(func(tx *db.DB) error {
|
|
|
|
if err := s.scanDir(tx, c, absPath); err != nil {
|
|
|
|
if err := s.scanDir(tx, st, absPath); err != nil {
|
|
|
|
c.errs = append(c.errs, fmt.Errorf("%q: %w", absPath, err))
|
|
|
|
st.errs = append(st.errs, fmt.Errorf("%q: %w", absPath, err))
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
|
|
|
func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
|
|
|
|
musicDir, relPath := musicDirRelative(s.musicDirs, absPath)
|
|
|
|
musicDir, relPath := musicDirRelative(s.musicDirs, absPath)
|
|
|
|
if musicDir == absPath {
|
|
|
|
if musicDir == absPath {
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
@@ -280,7 +280,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
|
|
|
return fmt.Errorf("first or create parent: %w", err)
|
|
|
|
return fmt.Errorf("first or create parent: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.seenAlbums[parent.ID] = struct{}{}
|
|
|
|
st.seenAlbums[parent.ID] = struct{}{}
|
|
|
|
|
|
|
|
|
|
|
|
dir, basename := filepath.Split(relPath)
|
|
|
|
dir, basename := filepath.Split(relPath)
|
|
|
|
var album db.Album
|
|
|
|
var album db.Album
|
|
|
|
@@ -288,12 +288,12 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
|
|
|
return fmt.Errorf("populate album basics: %w", err)
|
|
|
|
return fmt.Errorf("populate album basics: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.seenAlbums[album.ID] = struct{}{}
|
|
|
|
st.seenAlbums[album.ID] = struct{}{}
|
|
|
|
|
|
|
|
|
|
|
|
sort.Strings(tracks)
|
|
|
|
sort.Strings(tracks)
|
|
|
|
for i, basename := range tracks {
|
|
|
|
for i, basename := range tracks {
|
|
|
|
absPath := filepath.Join(musicDir, relPath, basename)
|
|
|
|
absPath := filepath.Join(musicDir, relPath, basename)
|
|
|
|
if err := s.populateTrackAndArtists(tx, c, i, &album, basename, absPath); err != nil {
|
|
|
|
if err := s.populateTrackAndArtists(tx, st, i, &album, basename, absPath); err != nil {
|
|
|
|
return fmt.Errorf("populate track %q: %w", basename, err)
|
|
|
|
return fmt.Errorf("populate track %q: %w", basename, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -301,7 +301,7 @@ func (s *Scanner) scanDir(tx *db.DB, c *Context, absPath string) error {
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *db.Album, basename string, absPath string) error {
|
|
|
|
func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db.Album, basename string, absPath string) error {
|
|
|
|
stat, err := os.Stat(absPath)
|
|
|
|
stat, err := os.Stat(absPath)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("stating %q: %w", basename, err)
|
|
|
|
return fmt.Errorf("stating %q: %w", basename, err)
|
|
|
|
@@ -312,8 +312,8 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *d
|
|
|
|
return fmt.Errorf("query track: %w", err)
|
|
|
|
return fmt.Errorf("query track: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !c.isFull && track.ID != 0 && stat.ModTime().Before(track.UpdatedAt) {
|
|
|
|
if !st.isFull && track.ID != 0 && stat.ModTime().Before(track.UpdatedAt) {
|
|
|
|
c.seenTracks[track.ID] = struct{}{}
|
|
|
|
st.seenTracks[track.ID] = struct{}{}
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -384,8 +384,8 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, c *Context, i int, album *d
|
|
|
|
return fmt.Errorf("populate track artists: %w", err)
|
|
|
|
return fmt.Errorf("populate track artists: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.seenTracks[track.ID] = struct{}{}
|
|
|
|
st.seenTracks[track.ID] = struct{}{}
|
|
|
|
c.seenTracksNew++
|
|
|
|
st.seenTracksNew++
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -540,9 +540,9 @@ func populateArtistAppearances(tx *db.DB, album *db.Album, artistIDs []int) erro
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) cleanTracks(c *Context) error {
|
|
|
|
func (s *Scanner) cleanTracks(st *State) error {
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
defer func() { log.Printf("finished clean tracks in %s, %d removed", durSince(start), c.TracksMissing()) }()
|
|
|
|
defer func() { log.Printf("finished clean tracks in %s, %d removed", durSince(start), st.TracksMissing()) }()
|
|
|
|
|
|
|
|
|
|
|
|
var all []int
|
|
|
|
var all []int
|
|
|
|
err := s.db.
|
|
|
|
err := s.db.
|
|
|
|
@@ -553,18 +553,18 @@ func (s *Scanner) cleanTracks(c *Context) error {
|
|
|
|
return fmt.Errorf("plucking ids: %w", err)
|
|
|
|
return fmt.Errorf("plucking ids: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, a := range all {
|
|
|
|
for _, a := range all {
|
|
|
|
if _, ok := c.seenTracks[a]; !ok {
|
|
|
|
if _, ok := st.seenTracks[a]; !ok {
|
|
|
|
c.tracksMissing = append(c.tracksMissing, int64(a))
|
|
|
|
st.tracksMissing = append(st.tracksMissing, int64(a))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s.db.TransactionChunked(c.tracksMissing, func(tx *db.DB, chunk []int64) error {
|
|
|
|
return s.db.TransactionChunked(st.tracksMissing, func(tx *db.DB, chunk []int64) error {
|
|
|
|
return tx.Where(chunk).Delete(&db.Track{}).Error
|
|
|
|
return tx.Where(chunk).Delete(&db.Track{}).Error
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) cleanAlbums(c *Context) error {
|
|
|
|
func (s *Scanner) cleanAlbums(st *State) error {
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
defer func() { log.Printf("finished clean albums in %s, %d removed", durSince(start), c.AlbumsMissing()) }()
|
|
|
|
defer func() { log.Printf("finished clean albums in %s, %d removed", durSince(start), st.AlbumsMissing()) }()
|
|
|
|
|
|
|
|
|
|
|
|
var all []int
|
|
|
|
var all []int
|
|
|
|
err := s.db.
|
|
|
|
err := s.db.
|
|
|
|
@@ -575,18 +575,18 @@ func (s *Scanner) cleanAlbums(c *Context) error {
|
|
|
|
return fmt.Errorf("plucking ids: %w", err)
|
|
|
|
return fmt.Errorf("plucking ids: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, a := range all {
|
|
|
|
for _, a := range all {
|
|
|
|
if _, ok := c.seenAlbums[a]; !ok {
|
|
|
|
if _, ok := st.seenAlbums[a]; !ok {
|
|
|
|
c.albumsMissing = append(c.albumsMissing, int64(a))
|
|
|
|
st.albumsMissing = append(st.albumsMissing, int64(a))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s.db.TransactionChunked(c.albumsMissing, func(tx *db.DB, chunk []int64) error {
|
|
|
|
return s.db.TransactionChunked(st.albumsMissing, func(tx *db.DB, chunk []int64) error {
|
|
|
|
return tx.Where(chunk).Delete(&db.Album{}).Error
|
|
|
|
return tx.Where(chunk).Delete(&db.Album{}).Error
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) cleanArtists(c *Context) error {
|
|
|
|
func (s *Scanner) cleanArtists(st *State) error {
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
defer func() { log.Printf("finished clean artists in %s, %d removed", durSince(start), c.ArtistsMissing()) }()
|
|
|
|
defer func() { log.Printf("finished clean artists in %s, %d removed", durSince(start), st.ArtistsMissing()) }()
|
|
|
|
|
|
|
|
|
|
|
|
// gorm doesn't seem to support subqueries without parens for UNION
|
|
|
|
// gorm doesn't seem to support subqueries without parens for UNION
|
|
|
|
q := s.db.Exec(`
|
|
|
|
q := s.db.Exec(`
|
|
|
|
@@ -602,13 +602,13 @@ func (s *Scanner) cleanArtists(c *Context) error {
|
|
|
|
if err := q.Error; err != nil {
|
|
|
|
if err := q.Error; err != nil {
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c.artistsMissing = int(q.RowsAffected)
|
|
|
|
st.artistsMissing = int(q.RowsAffected)
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Scanner) cleanGenres(c *Context) error { //nolint:unparam
|
|
|
|
func (s *Scanner) cleanGenres(st *State) error { //nolint:unparam
|
|
|
|
start := time.Now()
|
|
|
|
start := time.Now()
|
|
|
|
defer func() { log.Printf("finished clean genres in %s, %d removed", durSince(start), c.GenresMissing()) }()
|
|
|
|
defer func() { log.Printf("finished clean genres in %s, %d removed", durSince(start), st.GenresMissing()) }()
|
|
|
|
|
|
|
|
|
|
|
|
subTrack := s.db.
|
|
|
|
subTrack := s.db.
|
|
|
|
Select("genres.id").
|
|
|
|
Select("genres.id").
|
|
|
|
@@ -625,7 +625,7 @@ func (s *Scanner) cleanGenres(c *Context) error { //nolint:unparam
|
|
|
|
q := s.db.
|
|
|
|
q := s.db.
|
|
|
|
Where("genres.id IN ? AND genres.id IN ?", subTrack, subAlbum).
|
|
|
|
Where("genres.id IN ? AND genres.id IN ?", subTrack, subAlbum).
|
|
|
|
Delete(&db.Genre{})
|
|
|
|
Delete(&db.Genre{})
|
|
|
|
c.genresMissing = int(q.RowsAffected)
|
|
|
|
st.genresMissing = int(q.RowsAffected)
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -663,7 +663,7 @@ func durSince(t time.Time) time.Duration {
|
|
|
|
return time.Since(t).Truncate(10 * time.Microsecond)
|
|
|
|
return time.Since(t).Truncate(10 * time.Microsecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Context struct {
|
|
|
|
type State struct {
|
|
|
|
errs []error
|
|
|
|
errs []error
|
|
|
|
isFull bool
|
|
|
|
isFull bool
|
|
|
|
|
|
|
|
|
|
|
|
@@ -677,14 +677,14 @@ type Context struct {
|
|
|
|
genresMissing int
|
|
|
|
genresMissing int
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Context) SeenTracks() int { return len(c.seenTracks) }
|
|
|
|
func (s *State) SeenTracks() int { return len(s.seenTracks) }
|
|
|
|
func (c *Context) SeenAlbums() int { return len(c.seenAlbums) }
|
|
|
|
func (s *State) SeenAlbums() int { return len(s.seenAlbums) }
|
|
|
|
func (c *Context) SeenTracksNew() int { return c.seenTracksNew }
|
|
|
|
func (s *State) SeenTracksNew() int { return s.seenTracksNew }
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Context) TracksMissing() int { return len(c.tracksMissing) }
|
|
|
|
func (s *State) TracksMissing() int { return len(s.tracksMissing) }
|
|
|
|
func (c *Context) AlbumsMissing() int { return len(c.albumsMissing) }
|
|
|
|
func (s *State) AlbumsMissing() int { return len(s.albumsMissing) }
|
|
|
|
func (c *Context) ArtistsMissing() int { return c.artistsMissing }
|
|
|
|
func (s *State) ArtistsMissing() int { return s.artistsMissing }
|
|
|
|
func (c *Context) GenresMissing() int { return c.genresMissing }
|
|
|
|
func (s *State) GenresMissing() int { return s.genresMissing }
|
|
|
|
|
|
|
|
|
|
|
|
type MultiValueMode uint8
|
|
|
|
type MultiValueMode uint8
|
|
|
|
|
|
|
|
|
|
|
|
|