Only import playlists from configured paths in option `PlaylistsPath`. Closes #1181

Syntax is Ant-style Globs, with support for '**' (any subfolder). Default: '.:**' (or '.;**' in Windows`, meaning all folders and subfolders under `MusicFolder`
This commit is contained in:
Deluan 2021-09-12 21:06:03 -04:00
parent 9f00aad216
commit ab2912b4fa
10 changed files with 98 additions and 12 deletions

View File

@ -32,6 +32,7 @@ type configOptions struct {
TranscodingCacheSize string
ImageCacheSize string
AutoImportPlaylists bool
PlaylistsPath string
SearchFullString bool
RecentlyAddedByModTime bool
@ -189,6 +190,7 @@ func init() {
viper.SetDefault("transcodingcachesize", "100MB")
viper.SetDefault("imagecachesize", "100MB")
viper.SetDefault("autoimportplaylists", true)
viper.SetDefault("playlistspath", consts.DefaultPlaylistsPath)
viper.SetDefault("enabledownloads", true)
// Config options only valid for file/env configuration

View File

@ -3,6 +3,7 @@ package consts
import (
"crypto/md5"
"fmt"
"path/filepath"
"strings"
"time"
)
@ -84,6 +85,8 @@ var (
"command": "ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -c:a libopus -f opus -",
},
}
DefaultPlaylistsPath = strings.Join([]string{".", "**/**"}, string(filepath.ListSeparator))
)
var (

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/lestrrat-go/jwx v1.2.6
github.com/matoous/go-nanoid v1.5.0
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mattn/go-zglob v0.0.3 // indirect
github.com/microcosm-cc/bluemonday v1.0.15
github.com/mileusna/useragent v1.0.2
github.com/oklog/run v1.1.0

2
go.sum
View File

@ -576,6 +576,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=

View File

@ -10,6 +10,8 @@ import (
"path/filepath"
"strings"
"github.com/mattn/go-zglob"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -17,14 +19,18 @@ import (
)
type playlistSync struct {
ds model.DataStore
ds model.DataStore
rootFolder string
}
func newPlaylistSync(ds model.DataStore) *playlistSync {
return &playlistSync{ds: ds}
func newPlaylistSync(ds model.DataStore, rootFolder string) *playlistSync {
return &playlistSync{ds: ds, rootFolder: rootFolder}
}
func (s *playlistSync) processPlaylists(ctx context.Context, dir string) int64 {
if !s.inPlaylistsPath(dir) {
return 0
}
var count int64
files, err := os.ReadDir(dir)
if err != nil {
@ -127,6 +133,16 @@ func (s *playlistSync) updatePlaylist(ctx context.Context, newPls *model.Playlis
return s.ds.Playlist(ctx).Put(newPls)
}
func (s *playlistSync) inPlaylistsPath(dir string) bool {
rel, _ := filepath.Rel(s.rootFolder, dir)
for _, path := range strings.Split(conf.Server.PlaylistsPath, string(filepath.ListSeparator)) {
if match, _ := zglob.Match(path, rel); match {
return true
}
}
return false
}
// From https://stackoverflow.com/a/41433698
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {

View File

@ -3,6 +3,8 @@ package scanner
import (
"context"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
@ -10,15 +12,20 @@ import (
)
var _ = Describe("playlistSync", func() {
var ds model.DataStore
var ps *playlistSync
ctx := context.Background()
BeforeEach(func() {
ds = &tests.MockDataStore{
MockedMediaFile: &mockedMediaFile{},
MockedPlaylist: &mockedPlaylist{},
}
})
Describe("parsePlaylist", func() {
var ds model.DataStore
var ps *playlistSync
ctx := context.TODO()
BeforeEach(func() {
ds = &tests.MockDataStore{
MockedMediaFile: &mockedMediaFile{},
}
ps = newPlaylistSync(ds)
ps = newPlaylistSync(ds, "tests/")
})
It("parses well-formed playlists", func() {
@ -41,6 +48,41 @@ var _ = Describe("playlistSync", func() {
Expect(err).To(BeNil())
Expect(pls.Tracks).To(HaveLen(2))
})
})
Describe("processPlaylists", func() {
Context("Default PlaylistsPath", func() {
BeforeEach(func() {
conf.Server.PlaylistsPath = consts.DefaultPlaylistsPath
})
It("finds and import playlists at the top level", func() {
ps = newPlaylistSync(ds, "tests/fixtures/playlists/subfolder1")
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(1)))
})
It("finds and import playlists at any subfolder level", func() {
ps = newPlaylistSync(ds, "tests")
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(1)))
})
})
It("ignores playlists not in the PlaylistsPath", func() {
conf.Server.PlaylistsPath = "subfolder1"
ps = newPlaylistSync(ds, "tests/fixtures/playlists")
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(1)))
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder2")).To(Equal(int64(0)))
})
It("only imports playlists from the root of MusicFolder if PlaylistsPath is '.'", func() {
conf.Server.PlaylistsPath = "."
ps = newPlaylistSync(ds, "tests/fixtures/playlists")
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists")).To(Equal(int64(3)))
Expect(ps.processPlaylists(ctx, "tests/fixtures/playlists/subfolder1")).To(Equal(int64(0)))
})
})
})
@ -54,3 +96,15 @@ func (r *mockedMediaFile) FindByPath(s string) (*model.MediaFile, error) {
Path: s,
}, nil
}
type mockedPlaylist struct {
model.PlaylistRepository
}
func (r *mockedPlaylist) FindByPath(path string) (*model.Playlist, error) {
return nil, model.ErrNotFound
}
func (r *mockedPlaylist) Put(pls *model.Playlist) error {
return nil
}

View File

@ -30,7 +30,7 @@ type TagScanner struct {
func NewTagScanner(rootFolder string, ds model.DataStore, cacheWarmer core.CacheWarmer) *TagScanner {
return &TagScanner{
rootFolder: rootFolder,
plsSync: newPlaylistSync(ds),
plsSync: newPlaylistSync(ds, rootFolder),
ds: ds,
cacheWarmer: cacheWarmer,
}

View File

@ -0,0 +1,2 @@
test.mp3
test.ogg

View File

@ -0,0 +1,2 @@
test.mp3
test.ogg

View File

@ -14,6 +14,7 @@ type MockDataStore struct {
MockedUser model.UserRepository
MockedProperty model.PropertyRepository
MockedPlayer model.PlayerRepository
MockedPlaylist model.PlaylistRepository
MockedShare model.ShareRepository
MockedTranscoding model.TranscodingRepository
MockedUserProps model.UserPropsRepository
@ -53,7 +54,10 @@ func (db *MockDataStore) Genre(context.Context) model.GenreRepository {
}
func (db *MockDataStore) Playlist(context.Context) model.PlaylistRepository {
return struct{ model.PlaylistRepository }{}
if db.MockedPlaylist == nil {
db.MockedPlaylist = struct{ model.PlaylistRepository }{}
}
return db.MockedPlaylist
}
func (db *MockDataStore) PlayQueue(context.Context) model.PlayQueueRepository {