Auto-Import playlists found in the Music Folder
This commit is contained in:
@@ -93,12 +93,14 @@ func (s *TagScanner) Scan(ctx context.Context, lastModifiedSince time.Time) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO Search for playlists and import (with `sync` on)
|
||||
}
|
||||
for _, c := range deleted {
|
||||
err := s.processDeletedDir(ctx, c, updatedArtists, updatedAlbums, cnt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO "Un-sync" all playlists synched from a deleted folder
|
||||
}
|
||||
|
||||
err = s.flushAlbums(ctx, updatedAlbums)
|
||||
@@ -152,7 +154,7 @@ func (s *TagScanner) processChangedDir(ctx context.Context, dir string, updatedA
|
||||
|
||||
// Load folder's current tracks from DB into a map
|
||||
currentTracks := map[string]model.MediaFile{}
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,7 +284,7 @@ func (s *TagScanner) processDeletedDir(ctx context.Context, dir string, updatedA
|
||||
dir = filepath.Join(s.rootFolder, dir)
|
||||
start := time.Now()
|
||||
|
||||
mfs, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
mfs, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+118
-4
@@ -1,7 +1,11 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -9,6 +13,7 @@ import (
|
||||
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/model/request"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
)
|
||||
|
||||
@@ -70,20 +75,24 @@ func (s *TagScanner2) Scan(ctx context.Context, lastModifiedSince time.Time) err
|
||||
err := s.processDeletedDir(ctx, dir)
|
||||
if err != nil {
|
||||
log.Error("Error removing deleted folder from DB", "path", dir, err)
|
||||
continue
|
||||
}
|
||||
// TODO "Un-sync" all playlists synced from a deleted folder
|
||||
}
|
||||
for _, dir := range changedDirs {
|
||||
err := s.processChangedDir(ctx, dir)
|
||||
if err != nil {
|
||||
log.Error("Error updating folder in the DB", "path", dir, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_ = s.albumMap.flush()
|
||||
_ = s.artistMap.flush()
|
||||
|
||||
// Now that all mediafiles are imported/updated, search for and import playlists
|
||||
for _, dir := range changedDirs {
|
||||
_ = s.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)
|
||||
@@ -153,7 +162,7 @@ func (s *TagScanner2) getDeletedDirs(ctx context.Context, allDirs dirMap, change
|
||||
func (s *TagScanner2) processDeletedDir(ctx context.Context, dir string) error {
|
||||
start := time.Now()
|
||||
|
||||
mfs, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
mfs, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,7 +188,7 @@ func (s *TagScanner2) processChangedDir(ctx context.Context, dir string) error {
|
||||
|
||||
// Load folder's current tracks from DB into a map
|
||||
currentTracks := map[string]model.MediaFile{}
|
||||
ct, err := s.ds.MediaFile(ctx).FindByPath(dir)
|
||||
ct, err := s.ds.MediaFile(ctx).FindAllByPath(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -295,3 +304,108 @@ 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user