From feca030c6dfe9217e5f2822b95276c24134f7a6d Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 19 Jul 2020 13:58:46 -0400 Subject: [PATCH] Give warning when playlists are not imported due to not having an admin user --- scanner/load_tree.go | 23 +++++++++++------- scanner/playlist_sync.go | 12 ++++++---- scanner/tag_scanner_2.go | 51 ++++++++++++++++++++++++++-------------- utils/files.go | 5 ++++ utils/files_test.go | 10 ++++++++ 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/scanner/load_tree.go b/scanner/load_tree.go index 668acfa8f76..4fbac44a224 100644 --- a/scanner/load_tree.go +++ b/scanner/load_tree.go @@ -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 diff --git a/scanner/playlist_sync.go b/scanner/playlist_sync.go index c30c6226737..ab689ac2e72 100644 --- a/scanner/playlist_sync.go +++ b/scanner/playlist_sync.go @@ -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) { diff --git a/scanner/tag_scanner_2.go b/scanner/tag_scanner_2.go index d72d88a6fc0..aa25574e13a 100644 --- a/scanner/tag_scanner_2.go +++ b/scanner/tag_scanner_2.go @@ -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{} } diff --git a/utils/files.go b/utils/files.go index 5e9150c941a..d3fa9f7f5a2 100644 --- a/utils/files.go +++ b/utils/files.go @@ -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" +} diff --git a/utils/files_test.go b/utils/files_test.go index cc237ce5b32..8f5cf5bbfbd 100644 --- a/utils/files_test.go +++ b/utils/files_test.go @@ -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()) + }) + }) })