navidrome/scanner/scanner.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

252 lines
6.5 KiB
Go
Raw Normal View History

2020-01-16 22:53:48 +01:00
package scanner
import (
"context"
"errors"
"fmt"
"strconv"
2020-10-25 23:17:23 +01:00
"sync"
2020-01-16 22:53:48 +01:00
"time"
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/artwork"
2020-01-24 01:44:08 +01:00
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/events"
2020-01-16 22:53:48 +01:00
)
2020-10-25 23:17:23 +01:00
type Scanner interface {
RescanAll(ctx context.Context, fullRescan bool) error
2020-10-25 23:17:23 +01:00
Status(mediaFolder string) (*StatusInfo, error)
}
type StatusInfo struct {
MediaFolder string
Scanning bool
LastScan time.Time
2020-11-01 22:37:33 +01:00
Count uint32
FolderCount uint32
2020-10-25 23:17:23 +01:00
}
2020-11-02 00:37:17 +01:00
var (
ErrAlreadyScanning = errors.New("already scanning")
ErrScanError = errors.New("scan error")
)
2020-10-25 23:17:23 +01:00
type FolderScanner interface {
// Scan process finds any changes after `lastModifiedSince` and returns the number of changes found
Scan(ctx context.Context, lastModifiedSince time.Time, progress chan uint32) (int64, error)
2020-10-25 23:17:23 +01:00
}
2022-11-29 17:08:47 +01:00
var isScanning sync.Mutex
2020-11-02 00:37:17 +01:00
2020-10-25 23:17:23 +01:00
type scanner struct {
2022-12-23 18:28:22 +01:00
folders map[string]FolderScanner
status map[string]*scanStatus
lock *sync.RWMutex
ds model.DataStore
pls core.Playlists
broker events.Broker
cacheWarmer artwork.CacheWarmer
2020-10-25 23:17:23 +01:00
}
type scanStatus struct {
active bool
fileCount uint32
folderCount uint32
lastUpdate time.Time
2020-01-16 22:53:48 +01:00
}
func New(ds model.DataStore, playlists core.Playlists, cacheWarmer artwork.CacheWarmer, broker events.Broker) Scanner {
2020-10-25 23:17:23 +01:00
s := &scanner{
2022-12-23 18:28:22 +01:00
ds: ds,
pls: playlists,
broker: broker,
folders: map[string]FolderScanner{},
status: map[string]*scanStatus{},
lock: &sync.RWMutex{},
cacheWarmer: cacheWarmer,
2020-10-25 23:17:23 +01:00
}
2020-01-16 22:53:48 +01:00
s.loadFolders()
return s
}
func (s *scanner) rescan(ctx context.Context, mediaFolder string, fullRescan bool) error {
2020-01-16 22:53:48 +01:00
folderScanner := s.folders[mediaFolder]
start := time.Now()
2020-11-02 00:37:17 +01:00
s.setStatusStart(mediaFolder)
defer s.setStatusEnd(mediaFolder, start)
2020-01-16 22:53:48 +01:00
lastModifiedSince := time.Time{}
if !fullRescan {
lastModifiedSince = s.getLastModifiedSince(ctx, mediaFolder)
2020-01-16 22:53:48 +01:00
log.Debug("Scanning folder", "folder", mediaFolder, "lastModifiedSince", lastModifiedSince)
} else {
log.Debug("Scanning folder (full scan)", "folder", mediaFolder)
}
progress, cancel := s.startProgressTracker(mediaFolder)
defer cancel()
2020-11-12 22:12:31 +01:00
changeCount, err := folderScanner.Scan(ctx, lastModifiedSince, progress)
2020-11-12 22:12:31 +01:00
if err != nil {
log.Error("Error importing MediaFolder", "folder", mediaFolder, err)
}
if changeCount > 0 {
log.Debug(ctx, "Detected changes in the music folder. Sending refresh event",
"folder", mediaFolder, "changeCount", changeCount)
// Don't use real context, forcing a refresh in all open windows, including the one that triggered the scan
s.broker.SendMessage(context.Background(), &events.RefreshResource{})
}
2020-11-12 22:12:31 +01:00
s.updateLastModifiedSince(mediaFolder, start)
return err
}
func (s *scanner) startProgressTracker(mediaFolder string) (chan uint32, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
progress := make(chan uint32, 100)
2020-11-01 22:37:33 +01:00
go func() {
s.broker.SendMessage(ctx, &events.ScanStatus{Scanning: true, Count: 0, FolderCount: 0})
defer func() {
s.broker.SendMessage(ctx, &events.ScanStatus{
Scanning: false,
Count: int64(s.status[mediaFolder].fileCount),
FolderCount: int64(s.status[mediaFolder].folderCount),
})
}()
2020-11-01 22:37:33 +01:00
for {
select {
case <-ctx.Done():
return
case count := <-progress:
if count == 0 {
continue
}
totalFolders, totalFiles := s.incStatusCounter(mediaFolder, count)
s.broker.SendMessage(ctx, &events.ScanStatus{
Scanning: true,
Count: int64(totalFiles),
FolderCount: int64(totalFolders),
})
2020-11-01 22:37:33 +01:00
}
}
}()
return progress, cancel
2020-01-16 22:53:48 +01:00
}
func (s *scanner) RescanAll(ctx context.Context, fullRescan bool) error {
2022-11-29 17:08:47 +01:00
if !isScanning.TryLock() {
2020-10-25 23:17:23 +01:00
log.Debug("Scanner already running, ignoring request for rescan.")
2020-11-02 00:37:17 +01:00
return ErrAlreadyScanning
2020-10-25 23:17:23 +01:00
}
2022-11-29 17:08:47 +01:00
defer isScanning.Unlock()
2020-10-25 23:17:23 +01:00
2020-01-16 22:53:48 +01:00
var hasError bool
for folder := range s.folders {
err := s.rescan(ctx, folder, fullRescan)
2020-01-16 22:53:48 +01:00
hasError = hasError || err != nil
}
if hasError {
log.Error("Errors while scanning media. Please check the logs")
core.WriteAfterScanMetrics(ctx, s.ds, false)
2020-11-02 00:37:17 +01:00
return ErrScanError
2020-10-25 23:17:23 +01:00
}
core.WriteAfterScanMetrics(ctx, s.ds, true)
2020-11-02 00:37:17 +01:00
return nil
2020-10-25 23:17:23 +01:00
}
func (s *scanner) getStatus(folder string) *scanStatus {
s.lock.RLock()
defer s.lock.RUnlock()
if status, ok := s.status[folder]; ok {
return status
2020-01-16 22:53:48 +01:00
}
return nil
}
func (s *scanner) incStatusCounter(folder string, numFiles uint32) (totalFolders uint32, totalFiles uint32) {
s.lock.Lock()
defer s.lock.Unlock()
if status, ok := s.status[folder]; ok {
status.fileCount += numFiles
status.folderCount++
totalFolders = status.folderCount
totalFiles = status.fileCount
}
return
}
2020-11-02 00:37:17 +01:00
func (s *scanner) setStatusStart(folder string) {
2020-10-25 23:17:23 +01:00
s.lock.Lock()
defer s.lock.Unlock()
if status, ok := s.status[folder]; ok {
2020-11-02 00:37:17 +01:00
status.active = true
status.fileCount = 0
status.folderCount = 0
2020-10-25 23:17:23 +01:00
}
}
2020-11-02 00:37:17 +01:00
func (s *scanner) setStatusEnd(folder string, lastUpdate time.Time) {
2020-10-25 23:17:23 +01:00
s.lock.Lock()
defer s.lock.Unlock()
if status, ok := s.status[folder]; ok {
2020-11-02 00:37:17 +01:00
status.active = false
status.lastUpdate = lastUpdate
2020-10-25 23:17:23 +01:00
}
}
func (s *scanner) Status(mediaFolder string) (*StatusInfo, error) {
status := s.getStatus(mediaFolder)
if status == nil {
return nil, errors.New("mediaFolder not found")
}
return &StatusInfo{
MediaFolder: mediaFolder,
Scanning: status.active,
LastScan: status.lastUpdate,
Count: status.fileCount,
FolderCount: status.folderCount,
2020-10-25 23:17:23 +01:00
}, nil
}
func (s *scanner) getLastModifiedSince(ctx context.Context, folder string) time.Time {
ms, err := s.ds.Property(ctx).Get(model.PropLastScan + "-" + folder)
2020-01-16 22:53:48 +01:00
if err != nil {
return time.Time{}
}
if ms == "" {
return time.Time{}
}
i, _ := strconv.ParseInt(ms, 10, 64)
return time.Unix(0, i*int64(time.Millisecond))
2020-01-16 22:53:48 +01:00
}
2020-10-25 23:17:23 +01:00
func (s *scanner) updateLastModifiedSince(folder string, t time.Time) {
2020-01-16 22:53:48 +01:00
millis := t.UnixNano() / int64(time.Millisecond)
2020-04-26 18:35:26 +02:00
if err := s.ds.Property(context.TODO()).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis)); err != nil {
log.Error("Error updating DB after scan", err)
}
2020-01-16 22:53:48 +01:00
}
2020-10-25 23:17:23 +01:00
func (s *scanner) loadFolders() {
ctx := context.TODO()
fs, _ := s.ds.MediaFolder(ctx).GetAll()
2020-01-16 22:53:48 +01:00
for _, f := range fs {
log.Info("Configuring Media Folder", "name", f.Name, "path", f.Path)
s.folders[f.Path] = s.newScanner(f)
2020-10-25 23:17:23 +01:00
s.status[f.Path] = &scanStatus{
active: false,
fileCount: 0,
folderCount: 0,
lastUpdate: s.getLastModifiedSince(ctx, f.Path),
2020-10-25 23:17:23 +01:00
}
2020-01-16 22:53:48 +01:00
}
}
2020-10-25 23:17:23 +01:00
func (s *scanner) newScanner(f model.MediaFolder) FolderScanner {
2022-12-23 18:28:22 +01:00
return NewTagScanner(f.Path, s.ds, s.pls, s.cacheWarmer)
}