Skip to content

Commit

Permalink
Refactored playlist auto-import support
Browse files Browse the repository at this point in the history
  • Loading branch information
deluan committed Jul 18, 2020
1 parent b9b6ce0 commit 8f512a4
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 114 deletions.
128 changes: 128 additions & 0 deletions 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
}
119 changes: 5 additions & 114 deletions scanner/tag_scanner_2.go
@@ -1,26 +1,22 @@
package scanner

import (
"bufio"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"

"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/model/request"
"github.com/deluan/navidrome/utils"
)

type TagScanner2 struct {
rootFolder string
ds model.DataStore
mapper *mediaFileMapper
plsSync *playlistSync
albumMap *flushableMap
artistMap *flushableMap
cnt *counters
Expand All @@ -30,6 +26,7 @@ func NewTagScanner2(rootFolder string, ds model.DataStore) *TagScanner2 {
return &TagScanner2{
rootFolder: rootFolder,
mapper: newMediaFileMapper(rootFolder),
plsSync: newPlaylistSync(ds),
ds: ds,
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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
}

0 comments on commit 8f512a4

Please sign in to comment.