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"
)
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) {
newMap := make(map[string]time.Time)
newMap := make(dirMap)
err := loadMap(ctx, rootFolder, rootFolder, newMap)
if err != nil {
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 {
children, lastUpdated, err := loadDir(ctx, currentFolder)
children, dirMapValue, err := loadDir(ctx, currentFolder)
if err != nil {
return err
}
@ -36,18 +42,18 @@ func loadMap(ctx context.Context, rootPath string, currentFolder string, dirMap
}
dir := filepath.Clean(currentFolder)
dirMap[dir] = lastUpdated
dirMap[dir] = dirMapValue
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)
if err != nil {
log.Error(ctx, "Error stating dir", "path", dirPath, err)
return
}
lastUpdated = dirInfo.ModTime()
info.modTime = dirInfo.ModTime()
files, err := ioutil.ReadDir(dirPath)
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) {
children = append(children, filepath.Join(dirPath, f.Name()))
} else {
if f.ModTime().After(lastUpdated) {
lastUpdated = f.ModTime()
if f.ModTime().After(info.modTime) {
info.modTime = f.ModTime()
}
info.hasPlaylist = info.hasPlaylist || utils.IsPlaylist(f.Name())
}
}
return

View File

@ -12,6 +12,7 @@ import (
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/utils"
)
type playlistSync struct {
@ -22,15 +23,15 @@ func newPlaylistSync(ds model.DataStore) *playlistSync {
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)
if err != nil {
log.Error(ctx, "Error reading files", "dir", dir, err)
return err
return count
}
for _, f := range files {
match, _ := filepath.Match("*.m3u", strings.ToLower(f.Name()))
if !match {
if !utils.IsPlaylist(f.Name()) {
continue
}
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 {
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) {

View File

@ -33,19 +33,22 @@ func NewTagScanner2(rootFolder string, ds model.DataStore) *TagScanner2 {
}
// Scan algorithm overview:
// Load all directories under the music folder, with their ModTime (self or any non-dir children)
// Find changed folders (based on lastModifiedSince) and deletes folders (comparing to the DB)
// For each deleted folder: delete all files from DB whose path starts with the delete folder path
// For each changed folder: Get all files from DB whose path starts with the changed folder, scan each file:
// 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 deleted folders (comparing to the DB)
// 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 (non-recursively), check each file:
// if file in folder is newer, update the one in DB
// if file in folder does not exists in DB, add
// for each file in the DB that is not found in the folder, delete from DB
// 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 it from DB
// Create new albums/artists, update counters:
// collect all albumIDs and artistIDs from previous steps
// 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 {
ctx = s.setAdminUser(ctx)
ctx = s.withAdminUser(ctx)
start := time.Now()
allDirs, err := s.getDirTree(ctx)
@ -88,13 +91,23 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err
_ = s.artistMap.flush()
// Now that all mediafiles are imported/updated, search for and import playlists
u, _ := request.UserFrom(ctx)
plsCount := 0
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))
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
}
@ -114,8 +127,8 @@ func (s *TagScanner2) getChangedDirs(ctx context.Context, dirs dirMap, lastModif
start := time.Now()
log.Trace(ctx, "Checking for changed folders")
var changed []string
for d, t := range dirs {
if t.After(lastModified) {
for d, info := range dirs {
if info.modTime.After(lastModified) {
changed = append(changed, d)
}
}
@ -206,7 +219,7 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
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))
var filesToUpdate []string
for filePath, info := range files {
@ -219,7 +232,6 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
filesToUpdate = append(filesToUpdate, filePath)
s.cnt.updated++
}
delete(currentTracks, filePath)
// Force a refresh of the album and artist, to cater for cover art files
err = s.albumMap.update(c.AlbumID)
@ -230,6 +242,10 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
if err != nil {
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
@ -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
}
@ -325,10 +342,10 @@ func (s *TagScanner2) loadTracks(filePaths []string) (model.MediaFiles, error) {
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()
if err != nil {
log.Error(ctx, "Error retrieving playlist owner", err)
log.Warn(ctx, "No admin user found!", err)
u = &model.User{}
}

View File

@ -21,3 +21,8 @@ func IsImageFile(filePath string) bool {
extension := filepath.Ext(filePath)
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())
})
})
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())
})
})
})