mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
package scanner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
ppl "github.com/google/go-pipeline/pkg/pipeline"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
)
|
|
|
|
type phasePlaylists struct {
|
|
ctx context.Context
|
|
scanState *scanState
|
|
ds model.DataStore
|
|
pls core.Playlists
|
|
cw artwork.CacheWarmer
|
|
refreshed atomic.Uint32
|
|
}
|
|
|
|
func createPhasePlaylists(ctx context.Context, scanState *scanState, ds model.DataStore, pls core.Playlists, cw artwork.CacheWarmer) *phasePlaylists {
|
|
return &phasePlaylists{
|
|
ctx: ctx,
|
|
scanState: scanState,
|
|
ds: ds,
|
|
pls: pls,
|
|
cw: cw,
|
|
}
|
|
}
|
|
|
|
func (p *phasePlaylists) description() string {
|
|
return "Import/update playlists"
|
|
}
|
|
|
|
func (p *phasePlaylists) producer() ppl.Producer[*model.Folder] {
|
|
return ppl.NewProducer(p.produce, ppl.Name("load folders with playlists from db"))
|
|
}
|
|
|
|
func (p *phasePlaylists) produce(put func(entry *model.Folder)) error {
|
|
if !conf.Server.AutoImportPlaylists {
|
|
log.Info(p.ctx, "Playlists will not be imported, AutoImportPlaylists is set to false")
|
|
return nil
|
|
}
|
|
u, _ := request.UserFrom(p.ctx)
|
|
if !u.IsAdmin {
|
|
log.Warn(p.ctx, "Playlists will not be imported, as there are no admin users yet, "+
|
|
"Please create an admin user first, and then update the playlists for them to be imported")
|
|
return nil
|
|
}
|
|
|
|
count := 0
|
|
cursor, err := p.ds.Folder(p.ctx).GetTouchedWithPlaylists()
|
|
if err != nil {
|
|
return fmt.Errorf("loading touched folders: %w", err)
|
|
}
|
|
log.Debug(p.ctx, "Scanner: Checking playlists that may need refresh")
|
|
for folder, err := range cursor {
|
|
if err != nil {
|
|
return fmt.Errorf("loading touched folder: %w", err)
|
|
}
|
|
count++
|
|
put(&folder)
|
|
}
|
|
if count == 0 {
|
|
log.Debug(p.ctx, "Scanner: No playlists need refreshing")
|
|
} else {
|
|
log.Debug(p.ctx, "Scanner: Found folders with playlists that may need refreshing", "count", count)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *phasePlaylists) stages() []ppl.Stage[*model.Folder] {
|
|
return []ppl.Stage[*model.Folder]{
|
|
ppl.NewStage(p.processPlaylistsInFolder, ppl.Name("process playlists in folder"), ppl.Concurrency(3)),
|
|
}
|
|
}
|
|
|
|
func (p *phasePlaylists) processPlaylistsInFolder(folder *model.Folder) (*model.Folder, error) {
|
|
files, err := os.ReadDir(folder.AbsolutePath())
|
|
if err != nil {
|
|
log.Error(p.ctx, "Scanner: Error reading files", "folder", folder, err)
|
|
p.scanState.sendWarning(err.Error())
|
|
return folder, nil
|
|
}
|
|
for _, f := range files {
|
|
started := time.Now()
|
|
if strings.HasPrefix(f.Name(), ".") {
|
|
continue
|
|
}
|
|
if !model.IsValidPlaylist(f.Name()) {
|
|
continue
|
|
}
|
|
// BFR: Check if playlist needs to be refreshed (timestamp, sync flag, etc)
|
|
pls, err := p.pls.ImportFile(p.ctx, folder, f.Name())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if pls.IsSmartPlaylist() {
|
|
log.Debug("Scanner: Imported smart playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "elapsed", time.Since(started))
|
|
} else {
|
|
log.Debug("Scanner: Imported playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks), "elapsed", time.Since(started))
|
|
}
|
|
p.cw.PreCache(pls.CoverArtID())
|
|
p.refreshed.Add(1)
|
|
}
|
|
return folder, nil
|
|
}
|
|
|
|
func (p *phasePlaylists) finalize(err error) error {
|
|
refreshed := p.refreshed.Load()
|
|
logF := log.Info
|
|
if refreshed == 0 {
|
|
logF = log.Debug
|
|
} else {
|
|
p.scanState.changesDetected.Store(true)
|
|
}
|
|
logF(p.ctx, "Scanner: Finished refreshing playlists", "refreshed", refreshed, err)
|
|
return err
|
|
}
|
|
|
|
var _ phase[*model.Folder] = (*phasePlaylists)(nil)
|