From ab2912b4fa4a82bc8ebbe51f5bc65431712a48aa Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 12 Sep 2021 21:06:03 -0400 Subject: [PATCH] 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` --- conf/configuration.go | 2 + consts/consts.go | 3 + go.mod | 1 + go.sum | 2 + scanner/playlist_sync.go | 22 ++++++- scanner/playlist_sync_test.go | 68 ++++++++++++++++++-- scanner/tag_scanner.go | 2 +- tests/fixtures/playlists/subfolder1/pls1.m3u | 2 + tests/fixtures/playlists/subfolder2/pls2.m3u | 2 + tests/mock_persistence.go | 6 +- 10 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/playlists/subfolder1/pls1.m3u create mode 100644 tests/fixtures/playlists/subfolder2/pls2.m3u diff --git a/conf/configuration.go b/conf/configuration.go index 79ce7da55..185c1e3e5 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -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 diff --git a/consts/consts.go b/consts/consts.go index 4a64715bc..57cd36b03 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -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 ( diff --git a/go.mod b/go.mod index 5b0f7ee84..8dd310b14 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ca0091636..1d6e148e8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/scanner/playlist_sync.go b/scanner/playlist_sync.go index 69f54c6e0..79c996fcd 100644 --- a/scanner/playlist_sync.go +++ b/scanner/playlist_sync.go @@ -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 { diff --git a/scanner/playlist_sync_test.go b/scanner/playlist_sync_test.go index 445fb079d..d3f95889e 100644 --- a/scanner/playlist_sync_test.go +++ b/scanner/playlist_sync_test.go @@ -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 +} diff --git a/scanner/tag_scanner.go b/scanner/tag_scanner.go index 598a132b4..c841b9f93 100644 --- a/scanner/tag_scanner.go +++ b/scanner/tag_scanner.go @@ -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, } diff --git a/tests/fixtures/playlists/subfolder1/pls1.m3u b/tests/fixtures/playlists/subfolder1/pls1.m3u new file mode 100644 index 000000000..af745ba59 --- /dev/null +++ b/tests/fixtures/playlists/subfolder1/pls1.m3u @@ -0,0 +1,2 @@ +test.mp3 +test.ogg diff --git a/tests/fixtures/playlists/subfolder2/pls2.m3u b/tests/fixtures/playlists/subfolder2/pls2.m3u new file mode 100644 index 000000000..af745ba59 --- /dev/null +++ b/tests/fixtures/playlists/subfolder2/pls2.m3u @@ -0,0 +1,2 @@ +test.mp3 +test.ogg diff --git a/tests/mock_persistence.go b/tests/mock_persistence.go index 42c3077bc..b68f559ad 100644 --- a/tests/mock_persistence.go +++ b/tests/mock_persistence.go @@ -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 {