mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 04:27:37 +03:00
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:
parent
9f00aad216
commit
ab2912b4fa
10 changed files with 98 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
2
tests/fixtures/playlists/subfolder1/pls1.m3u
vendored
Normal file
2
tests/fixtures/playlists/subfolder1/pls1.m3u
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
test.mp3
|
||||
test.ogg
|
2
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
Normal file
2
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
test.mp3
|
||||
test.ogg
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue