Refactored playlist auto-import support
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
+5
-114
@@ -1,11 +1,7 @@
|
|||||||
package scanner
|
package scanner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,7 +9,6 @@ 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/utils"
|
"github.com/deluan/navidrome/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +16,7 @@ type TagScanner2 struct {
|
|||||||
rootFolder string
|
rootFolder string
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
mapper *mediaFileMapper
|
mapper *mediaFileMapper
|
||||||
|
plsSync *playlistSync
|
||||||
albumMap *flushableMap
|
albumMap *flushableMap
|
||||||
artistMap *flushableMap
|
artistMap *flushableMap
|
||||||
cnt *counters
|
cnt *counters
|
||||||
@@ -30,6 +26,7 @@ func NewTagScanner2(rootFolder string, ds model.DataStore) *TagScanner2 {
|
|||||||
return &TagScanner2{
|
return &TagScanner2{
|
||||||
rootFolder: rootFolder,
|
rootFolder: rootFolder,
|
||||||
mapper: newMediaFileMapper(rootFolder),
|
mapper: newMediaFileMapper(rootFolder),
|
||||||
|
plsSync: newPlaylistSync(ds),
|
||||||
ds: ds,
|
ds: ds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,10 +58,10 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err
|
|||||||
deletedDirs, _ := s.getDeletedDirs(ctx, allDirs, changedDirs)
|
deletedDirs, _ := s.getDeletedDirs(ctx, allDirs, changedDirs)
|
||||||
|
|
||||||
if log.CurrentLevel() >= log.LevelTrace {
|
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, ";"))
|
"changed", strings.Join(changedDirs, ";"), "deleted", strings.Join(deletedDirs, ";"))
|
||||||
} else {
|
} 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)
|
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 {
|
if err != nil {
|
||||||
log.Error("Error removing deleted folder from DB", "path", dir, err)
|
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 {
|
for _, dir := range changedDirs {
|
||||||
err := s.processChangedDir(ctx, dir)
|
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
|
// Now that all mediafiles are imported/updated, search for and import playlists
|
||||||
for _, dir := range changedDirs {
|
for _, dir := range changedDirs {
|
||||||
_ = s.processPlaylists(ctx, dir)
|
_ = s.plsSync.processPlaylists(ctx, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.ds.GC(log.NewContext(ctx))
|
err = s.ds.GC(log.NewContext(ctx))
|
||||||
@@ -304,108 +300,3 @@ func (s *TagScanner2) loadTracks(filePaths []string) (model.MediaFiles, error) {
|
|||||||
}
|
}
|
||||||
return mfs, nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user