Give warning when playlists are not imported due to not having an admin user

This commit is contained in:
Deluan 2020-07-19 13:58:46 -04:00
parent 41138bd665
commit feca030c6d
5 changed files with 71 additions and 30 deletions

View File

@ -12,10 +12,16 @@ import (
"github.com/deluan/navidrome/utils" "github.com/deluan/navidrome/utils"
) )
type dirMap = map[string]time.Time type (
dirMapValue struct {
modTime time.Time
hasPlaylist bool
}
dirMap = map[string]dirMapValue
)
func loadDirTree(ctx context.Context, rootFolder string) (dirMap, error) { func loadDirTree(ctx context.Context, rootFolder string) (dirMap, error) {
newMap := make(map[string]time.Time) newMap := make(dirMap)
err := loadMap(ctx, rootFolder, rootFolder, newMap) err := loadMap(ctx, rootFolder, rootFolder, newMap)
if err != nil { if err != nil {
log.Error(ctx, "Error loading directory tree", err) log.Error(ctx, "Error loading directory tree", err)
@ -24,7 +30,7 @@ func loadDirTree(ctx context.Context, rootFolder string) (dirMap, error) {
} }
func loadMap(ctx context.Context, rootPath string, currentFolder string, dirMap dirMap) error { func loadMap(ctx context.Context, rootPath string, currentFolder string, dirMap dirMap) error {
children, lastUpdated, err := loadDir(ctx, currentFolder) children, dirMapValue, err := loadDir(ctx, currentFolder)
if err != nil { if err != nil {
return err return err
} }
@ -36,18 +42,18 @@ func loadMap(ctx context.Context, rootPath string, currentFolder string, dirMap
} }
dir := filepath.Clean(currentFolder) dir := filepath.Clean(currentFolder)
dirMap[dir] = lastUpdated dirMap[dir] = dirMapValue
return nil return nil
} }
func loadDir(ctx context.Context, dirPath string) (children []string, lastUpdated time.Time, err error) { func loadDir(ctx context.Context, dirPath string) (children []string, info dirMapValue, err error) {
dirInfo, err := os.Stat(dirPath) dirInfo, err := os.Stat(dirPath)
if err != nil { if err != nil {
log.Error(ctx, "Error stating dir", "path", dirPath, err) log.Error(ctx, "Error stating dir", "path", dirPath, err)
return return
} }
lastUpdated = dirInfo.ModTime() info.modTime = dirInfo.ModTime()
files, err := ioutil.ReadDir(dirPath) files, err := ioutil.ReadDir(dirPath)
if err != nil { if err != nil {
@ -63,9 +69,10 @@ func loadDir(ctx context.Context, dirPath string) (children []string, lastUpdate
if isDir && !isDirIgnored(dirPath, f) && isDirReadable(dirPath, f) { if isDir && !isDirIgnored(dirPath, f) && isDirReadable(dirPath, f) {
children = append(children, filepath.Join(dirPath, f.Name())) children = append(children, filepath.Join(dirPath, f.Name()))
} else { } else {
if f.ModTime().After(lastUpdated) { if f.ModTime().After(info.modTime) {
lastUpdated = f.ModTime() info.modTime = f.ModTime()
} }
info.hasPlaylist = info.hasPlaylist || utils.IsPlaylist(f.Name())
} }
} }
return return

View File

@ -12,6 +12,7 @@ import (
"github.com/deluan/navidrome/log" "github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model" "github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request" "github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/utils"
) )
type playlistSync struct { type playlistSync struct {
@ -22,15 +23,15 @@ func newPlaylistSync(ds model.DataStore) *playlistSync {
return &playlistSync{ds: ds} return &playlistSync{ds: ds}
} }
func (s *playlistSync) processPlaylists(ctx context.Context, dir string) error { func (s *playlistSync) processPlaylists(ctx context.Context, dir string) int {
count := 0
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
log.Error(ctx, "Error reading files", "dir", dir, err) log.Error(ctx, "Error reading files", "dir", dir, err)
return err return count
} }
for _, f := range files { for _, f := range files {
match, _ := filepath.Match("*.m3u", strings.ToLower(f.Name())) if !utils.IsPlaylist(f.Name()) {
if !match {
continue continue
} }
pls, err := s.parsePlaylist(ctx, f.Name(), dir) pls, err := s.parsePlaylist(ctx, f.Name(), dir)
@ -43,8 +44,9 @@ func (s *playlistSync) processPlaylists(ctx context.Context, dir string) error {
if err != nil { if err != nil {
log.Error(ctx, "Error updating playlist", "playlist", f.Name(), err) log.Error(ctx, "Error updating playlist", "playlist", f.Name(), err)
} }
count++
} }
return nil return count
} }
func (s *playlistSync) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) { func (s *playlistSync) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) {

View File

@ -33,19 +33,22 @@ func NewTagScanner2(rootFolder string, ds model.DataStore) *TagScanner2 {
} }
// Scan algorithm overview: // Scan algorithm overview:
// Load all directories under the music folder, with their ModTime (self or any non-dir children) // Load all directories under the music folder, with their ModTime (self or any non-dir children, whichever is newer)
// Find changed folders (based on lastModifiedSince) and deletes folders (comparing to the DB) // Find changed folders (based on lastModifiedSince) and deleted folders (comparing to the DB)
// For each deleted folder: delete all files from DB whose path starts with the delete folder path // For each deleted folder: delete all files from DB whose path starts with the delete folder path (non-recursively)
// For each changed folder: Get all files from DB whose path starts with the changed folder, scan each file: // For each changed folder: get all files from DB whose path starts with the changed folder (non-recursively), check each file:
// if file in folder is newer, update the one in DB // if file in folder is newer, update the one in DB
// if file in folder does not exists in DB, add // if file in folder does not exists in DB, add it
// for each file in the DB that is not found in the folder, delete from DB // for each file in the DB that is not found in the folder, delete it from DB
// Create new albums/artists, update counters: // Create new albums/artists, update counters:
// collect all albumIDs and artistIDs from previous steps // collect all albumIDs and artistIDs from previous steps
// refresh the collected albums and artists with the metadata from the mediafiles // refresh the collected albums and artists with the metadata from the mediafiles
// Delete all empty albums, delete all empty Artists // For each changed folder, process playlists:
// If the playlist is not in the DB, import it, setting sync = true
// If the playlist is in the DB and sync == true, import it, or else skip it
// Delete all empty albums, delete all empty artists, clean-up playlists
func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) error { func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) error {
ctx = s.setAdminUser(ctx) ctx = s.withAdminUser(ctx)
start := time.Now() start := time.Now()
allDirs, err := s.getDirTree(ctx) allDirs, err := s.getDirTree(ctx)
@ -88,13 +91,23 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err
_ = s.artistMap.flush() _ = s.artistMap.flush()
// Now that all mediafiles are imported/updated, search for and import playlists // Now that all mediafiles are imported/updated, search for and import playlists
u, _ := request.UserFrom(ctx)
plsCount := 0
for _, dir := range changedDirs { for _, dir := range changedDirs {
_ = s.plsSync.processPlaylists(ctx, dir) info := allDirs[dir]
if info.hasPlaylist {
if !u.IsAdmin {
log.Warn("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", "dir", dir)
} else {
plsCount = s.plsSync.processPlaylists(ctx, dir)
}
}
} }
err = s.ds.GC(log.NewContext(ctx)) err = s.ds.GC(log.NewContext(ctx))
log.Info("Finished processing Music Folder", "folder", s.rootFolder, "elapsed", time.Since(start), log.Info("Finished processing Music Folder", "folder", s.rootFolder, "elapsed", time.Since(start),
"added", s.cnt.added, "updated", s.cnt.updated, "deleted", s.cnt.deleted) "added", s.cnt.added, "updated", s.cnt.updated, "deleted", s.cnt.deleted, "playlistsImported", plsCount)
return err return err
} }
@ -114,8 +127,8 @@ func (s *TagScanner2) getChangedDirs(ctx context.Context, dirs dirMap, lastModif
start := time.Now() start := time.Now()
log.Trace(ctx, "Checking for changed folders") log.Trace(ctx, "Checking for changed folders")
var changed []string var changed []string
for d, t := range dirs { for d, info := range dirs {
if t.After(lastModified) { if info.modTime.After(lastModified) {
changed = append(changed, d) changed = append(changed, d)
} }
} }
@ -206,7 +219,7 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
return nil return nil
} }
// If track from folder is newer than the one in DB, select for update/insert in DB and delete from the current tracks // If track from folder is newer than the one in DB, select for update/insert in DB
log.Trace(ctx, "Processing changed folder", "dir", dir, "tracksInDB", len(currentTracks), "tracksInFolder", len(files)) log.Trace(ctx, "Processing changed folder", "dir", dir, "tracksInDB", len(currentTracks), "tracksInFolder", len(files))
var filesToUpdate []string var filesToUpdate []string
for filePath, info := range files { for filePath, info := range files {
@ -219,7 +232,6 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
filesToUpdate = append(filesToUpdate, filePath) filesToUpdate = append(filesToUpdate, filePath)
s.cnt.updated++ s.cnt.updated++
} }
delete(currentTracks, filePath)
// Force a refresh of the album and artist, to cater for cover art files // Force a refresh of the album and artist, to cater for cover art files
err = s.albumMap.update(c.AlbumID) err = s.albumMap.update(c.AlbumID)
@ -230,6 +242,10 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
if err != nil { if err != nil {
return err return err
} }
// Remove it from currentTracks (the ones found in DB). After this loop any currentTracks remaining
// are considered gone from the music folder and will be deleted from DB
delete(currentTracks, filePath)
} }
numUpdatedTracks := 0 numUpdatedTracks := 0
@ -249,7 +265,8 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
} }
} }
log.Info(ctx, "Finished processing changed folder", "dir", dir, "updated", numUpdatedTracks, "purged", numPurgedTracks, "elapsed", time.Since(start)) log.Info(ctx, "Finished processing changed folder", "dir", dir, "updated", numUpdatedTracks,
"purged", numPurgedTracks, "elapsed", time.Since(start))
return nil return nil
} }
@ -325,10 +342,10 @@ func (s *TagScanner2) loadTracks(filePaths []string) (model.MediaFiles, error) {
return mfs, nil return mfs, nil
} }
func (s *TagScanner2) setAdminUser(ctx context.Context) context.Context { func (s *TagScanner2) withAdminUser(ctx context.Context) context.Context {
u, err := s.ds.User(ctx).FindFirstAdmin() u, err := s.ds.User(ctx).FindFirstAdmin()
if err != nil { if err != nil {
log.Error(ctx, "Error retrieving playlist owner", err) log.Warn(ctx, "No admin user found!", err)
u = &model.User{} u = &model.User{}
} }

View File

@ -21,3 +21,8 @@ func IsImageFile(filePath string) bool {
extension := filepath.Ext(filePath) extension := filepath.Ext(filePath)
return strings.HasPrefix(mime.TypeByExtension(extension), "image/") return strings.HasPrefix(mime.TypeByExtension(extension), "image/")
} }
func IsPlaylist(filePath string) bool {
extension := filepath.Ext(filePath)
return strings.ToLower(extension) == ".m3u"
}

View File

@ -43,4 +43,14 @@ var _ = Describe("Files", func() {
Expect(IsImageFile("test.mp3")).To(BeFalse()) Expect(IsImageFile("test.mp3")).To(BeFalse())
}) })
}) })
Describe("IsPlaylist", func() {
It("returns true for a M3U file", func() {
Expect(IsPlaylist(filepath.Join("path", "to", "test.m3u"))).To(BeTrue())
})
It("returns false for a non-playlist file", func() {
Expect(IsPlaylist("testm3u")).To(BeFalse())
})
})
}) })