diff --git a/scanner/playlist_sync.go b/scanner/playlist_sync.go new file mode 100644 index 00000000000..ae9de2bf147 --- /dev/null +++ b/scanner/playlist_sync.go @@ -0,0 +1,128 @@ +package scanner + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/deluan/navidrome/log" + "github.com/deluan/navidrome/model" + "github.com/deluan/navidrome/model/request" +) + +type playlistSync struct { + ds model.DataStore +} + +func newPlaylistSync(ds model.DataStore) *playlistSync { + return &playlistSync{ds: ds} +} + +func (s *playlistSync) processPlaylists(ctx context.Context, dir string) error { + files, err := ioutil.ReadDir(dir) + if err != nil { + log.Error(ctx, "Error reading files", "dir", dir, err) + return err + } + for _, f := range files { + match, _ := filepath.Match("*.m3u", strings.ToLower(f.Name())) + if !match { + continue + } + pls, err := s.parsePlaylist(ctx, f.Name(), dir) + if err != nil { + log.Error(ctx, "Error parsing playlist", "playlist", f.Name(), err) + continue + } + log.Debug("Found playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks)) + err = s.updatePlaylistIfNewer(ctx, pls) + if err != nil { + log.Error(ctx, "Error updating playlist", "playlist", f.Name(), err) + } + } + return nil +} + +func (s *playlistSync) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) { + playlistPath := filepath.Join(baseDir, playlistFile) + file, err := os.Open(playlistPath) + if err != nil { + return nil, err + } + defer file.Close() + info, err := os.Stat(playlistPath) + if err != nil { + return nil, err + } + + var extension = filepath.Ext(playlistFile) + var name = playlistFile[0 : len(playlistFile)-len(extension)] + + pls := &model.Playlist{ + Name: name, + Comment: fmt.Sprintf("Auto-imported from '%s'", playlistFile), + Public: false, + Path: playlistPath, + Sync: true, + UpdatedAt: info.ModTime(), + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + path := scanner.Text() + // Skip extended info + if strings.HasPrefix(path, "#") { + continue + } + if !filepath.IsAbs(path) { + path = filepath.Join(baseDir, path) + } + mf, err := s.ds.MediaFile(ctx).FindByPath(path) + if err != nil { + log.Warn(ctx, "Path in playlist not found", "playlist", playlistFile, "path", path, err) + continue + } + pls.Tracks = append(pls.Tracks, *mf) + } + + return pls, scanner.Err() +} + +func (s *playlistSync) updatePlaylistIfNewer(ctx context.Context, newPls *model.Playlist) error { + owner := s.getPlaylistsOwner(ctx) + ctx = request.WithUsername(ctx, owner.UserName) + ctx = request.WithUser(ctx, *owner) + + pls, err := s.ds.Playlist(ctx).FindByPath(newPls.Path) + if err != nil && err != model.ErrNotFound { + return err + } + if err == nil && !pls.Sync { + log.Debug(ctx, "Playlist already imported and not synced", "playlist", pls.Name, "path", pls.Path) + return nil + } + + if err == nil { + log.Info(ctx, "Updating synced playlist", "playlist", pls.Name, "path", newPls.Path) + newPls.ID = pls.ID + newPls.Name = pls.Name + newPls.Comment = pls.Comment + newPls.Owner = pls.Owner + } else { + log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName) + newPls.Owner = owner.UserName + } + return s.ds.Playlist(ctx).Put(newPls) +} + +func (s *playlistSync) getPlaylistsOwner(ctx context.Context) *model.User { + u, err := s.ds.User(ctx).FindFirstAdmin() + if err != nil { + log.Error(ctx, "Error retrieving playlist owner", err) + } + return u +} diff --git a/scanner/tag_scanner_2.go b/scanner/tag_scanner_2.go index 2f7a78b25bd..d00f6b7aff9 100644 --- a/scanner/tag_scanner_2.go +++ b/scanner/tag_scanner_2.go @@ -1,11 +1,7 @@ package scanner import ( - "bufio" "context" - "fmt" - "io/ioutil" - "os" "path/filepath" "sort" "strings" @@ -13,7 +9,6 @@ import ( "github.com/deluan/navidrome/log" "github.com/deluan/navidrome/model" - "github.com/deluan/navidrome/model/request" "github.com/deluan/navidrome/utils" ) @@ -21,6 +16,7 @@ type TagScanner2 struct { rootFolder string ds model.DataStore mapper *mediaFileMapper + plsSync *playlistSync albumMap *flushableMap artistMap *flushableMap cnt *counters @@ -30,6 +26,7 @@ func NewTagScanner2(rootFolder string, ds model.DataStore) *TagScanner2 { return &TagScanner2{ rootFolder: rootFolder, mapper: newMediaFileMapper(rootFolder), + plsSync: newPlaylistSync(ds), ds: ds, } } @@ -61,10 +58,10 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err deletedDirs, _ := s.getDeletedDirs(ctx, allDirs, changedDirs) if log.CurrentLevel() >= log.LevelTrace { - log.Info(ctx, "Folder changes found", "changedFolders", len(changedDirs), "deletedFolders", len(deletedDirs), + log.Info(ctx, "Folder changes detected", "changedFolders", len(changedDirs), "deletedFolders", len(deletedDirs), "changed", strings.Join(changedDirs, ";"), "deleted", strings.Join(deletedDirs, ";")) } else { - log.Info(ctx, "Folder changes found", "changedFolders", len(changedDirs), "deletedFolders", len(deletedDirs)) + log.Info(ctx, "Folder changes detected", "changedFolders", len(changedDirs), "deletedFolders", len(deletedDirs)) } s.albumMap = newFlushableMap(ctx, "album", s.ds.Album(ctx).Refresh) @@ -76,7 +73,6 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err if err != nil { log.Error("Error removing deleted folder from DB", "path", dir, err) } - // TODO "Un-sync" all playlists synced from a deleted folder } for _, dir := range changedDirs { err := s.processChangedDir(ctx, dir) @@ -90,7 +86,7 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err // Now that all mediafiles are imported/updated, search for and import playlists for _, dir := range changedDirs { - _ = s.processPlaylists(ctx, dir) + _ = s.plsSync.processPlaylists(ctx, dir) } err = s.ds.GC(log.NewContext(ctx)) @@ -304,108 +300,3 @@ func (s *TagScanner2) loadTracks(filePaths []string) (model.MediaFiles, error) { } return mfs, nil } - -func (s *TagScanner2) processPlaylists(ctx context.Context, dir string) error { - files, err := ioutil.ReadDir(dir) - if err != nil { - log.Error(ctx, "Error reading files", "dir", dir, err) - return err - } - for _, f := range files { - match, _ := filepath.Match("*.m3u", strings.ToLower(f.Name())) - if !match { - continue - } - pls, err := s.parsePlaylist(ctx, f.Name(), dir) - if err != nil { - log.Error(ctx, "Error parsing playlist", "playlist", f.Name(), err) - continue - } - log.Debug("Found playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks)) - err = s.updatePlaylistIfNewer(ctx, pls) - if err != nil { - log.Error(ctx, "Error updating playlist", "playlist", f.Name(), err) - } - } - return nil -} - -func (s *TagScanner2) parsePlaylist(ctx context.Context, playlistFile string, baseDir string) (*model.Playlist, error) { - playlistPath := filepath.Join(baseDir, playlistFile) - file, err := os.Open(playlistPath) - if err != nil { - return nil, err - } - defer file.Close() - info, err := os.Stat(playlistPath) - if err != nil { - return nil, err - } - - var extension = filepath.Ext(playlistFile) - var name = playlistFile[0 : len(playlistFile)-len(extension)] - - pls := &model.Playlist{ - Name: name, - Comment: fmt.Sprintf("Auto-imported from '%s'", playlistFile), - Public: false, - Path: playlistPath, - Sync: true, - UpdatedAt: info.ModTime(), - } - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - path := scanner.Text() - // Skip extended info - if strings.HasPrefix(path, "#") { - continue - } - if !filepath.IsAbs(path) { - path = filepath.Join(baseDir, path) - } - mf, err := s.ds.MediaFile(ctx).FindByPath(path) - if err != nil { - log.Warn(ctx, "Path in playlist not found", "playlist", playlistFile, "path", path, err) - continue - } - pls.Tracks = append(pls.Tracks, *mf) - } - - return pls, scanner.Err() -} - -func (s *TagScanner2) updatePlaylistIfNewer(ctx context.Context, newPls *model.Playlist) error { - owner := s.getPlaylistsOwner(ctx) - ctx = request.WithUsername(ctx, owner.UserName) - ctx = request.WithUser(ctx, *owner) - - pls, err := s.ds.Playlist(ctx).FindByPath(newPls.Path) - if err != nil && err != model.ErrNotFound { - return err - } - if err == nil && !pls.Sync { - log.Debug(ctx, "Playlist already imported and not synced", "playlist", pls.Name, "path", pls.Path) - return nil - } - - if err == nil { - log.Info(ctx, "Updating synced playlist", "playlist", pls.Name, "path", newPls.Path) - newPls.ID = pls.ID - newPls.Name = pls.Name - newPls.Comment = pls.Comment - newPls.Owner = pls.Owner - } else { - log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName) - newPls.Owner = owner.UserName - } - return s.ds.Playlist(ctx).Put(newPls) -} - -func (s *TagScanner2) getPlaylistsOwner(ctx context.Context) *model.User { - u, err := s.ds.User(ctx).FindFirstAdmin() - if err != nil { - log.Error(ctx, "Error retrieving playlist owner", err) - } - return u -}