-
-
Notifications
You must be signed in to change notification settings - Fork 799
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
558 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package cmd | ||
|
||
import ( | ||
_ "github.com/navidrome/navidrome/adapters/taglib" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package storage | ||
|
||
import ( | ||
"io/fs" | ||
|
||
"github.com/navidrome/navidrome/model/tag" | ||
) | ||
|
||
type Storage interface { | ||
FS() (MusicFS, error) | ||
} | ||
|
||
// MusicFS is an interface that extends the fs.FS interface with the ability to read tags from files | ||
type MusicFS interface { | ||
fs.FS | ||
ReadTags(path ...string) (map[string]tag.Properties, error) | ||
} | ||
|
||
// WatcherFS is an interface that extends the fs.FS interface with the ability to start and stop a fs watcher. | ||
type WatcherFS interface { | ||
fs.FS | ||
|
||
// StartWatcher starts a watcher on the whole FS and returns a channel to send detected changes | ||
StartWatcher() (chan<- string, error) | ||
|
||
// StopWatcher stops the watcher | ||
StopWatcher() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package local | ||
|
||
import ( | ||
"io/fs" | ||
"sync" | ||
|
||
"github.com/navidrome/navidrome/model/tag" | ||
) | ||
|
||
// Extractor is an interface that defines the methods that an tag/metadata extractor must implement | ||
type Extractor interface { | ||
Parse(files ...string) (map[string]tag.Properties, error) | ||
Version() string | ||
} | ||
|
||
type extractorConstructor func(fs.FS, string) Extractor | ||
|
||
var ( | ||
extractors = map[string]extractorConstructor{} | ||
lock sync.RWMutex | ||
) | ||
|
||
// RegisterExtractor registers a new extractor, so it can be used by the local storage. The one to be used is | ||
// defined with the configuration option Scanner.Extractor. | ||
func RegisterExtractor(id string, f extractorConstructor) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
extractors[id] = f | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package local | ||
|
||
import ( | ||
"fmt" | ||
"io/fs" | ||
"net/url" | ||
"os" | ||
"time" | ||
|
||
"github.com/djherbis/times" | ||
"github.com/navidrome/navidrome/conf" | ||
"github.com/navidrome/navidrome/core/storage" | ||
"github.com/navidrome/navidrome/log" | ||
"github.com/navidrome/navidrome/model/tag" | ||
) | ||
|
||
// localStorage implements a Storage that reads the files from the local filesystem and uses registered extractors | ||
// to extract the metadata and tags from the files. | ||
type localStorage struct { | ||
u url.URL | ||
extractor Extractor | ||
} | ||
|
||
func newLocalStorage(u url.URL) storage.Storage { | ||
newExtractor, ok := extractors[conf.Server.Scanner.Extractor] | ||
if !ok || newExtractor == nil { | ||
log.Fatal("Extractor not found: %s", conf.Server.Scanner.Extractor) | ||
} | ||
return localStorage{u: u, extractor: newExtractor(os.DirFS(u.Path), u.Path)} | ||
} | ||
|
||
func (s localStorage) FS() (storage.MusicFS, error) { | ||
path := s.u.Path | ||
if _, err := os.Stat(path); err != nil { | ||
return nil, fmt.Errorf("%w: %s", err, path) | ||
} | ||
return localFS{FS: os.DirFS(path), extractor: s.extractor}, nil | ||
} | ||
|
||
type localFS struct { | ||
fs.FS | ||
extractor Extractor | ||
} | ||
|
||
func (lfs localFS) ReadTags(path ...string) (map[string]tag.Properties, error) { | ||
res, err := lfs.extractor.Parse(path...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for path, v := range res { | ||
if v.FileInfo == nil { | ||
info, err := fs.Stat(lfs, path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
v.FileInfo = localFileInfo{info} | ||
res[path] = v | ||
} | ||
} | ||
return res, nil | ||
} | ||
|
||
// localFileInfo is a wrapper around fs.FileInfo that adds a BirthTime method, to make it compatible | ||
// with tag.FileInfo | ||
type localFileInfo struct { | ||
fs.FileInfo | ||
} | ||
|
||
func (lfi localFileInfo) BirthTime() time.Time { | ||
if ts := times.Get(lfi.FileInfo); ts.HasBirthTime() { | ||
return ts.BirthTime() | ||
} | ||
|
||
return time.Time{} | ||
} | ||
|
||
func init() { | ||
storage.Register(storage.LocalSchemaID, newLocalStorage) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package storage | ||
|
||
import ( | ||
"errors" | ||
"net/url" | ||
"path/filepath" | ||
"strings" | ||
"sync" | ||
) | ||
|
||
const LocalSchemaID = "file" | ||
|
||
type constructor func(url.URL) Storage | ||
|
||
var ( | ||
registry = map[string]constructor{} | ||
lock sync.RWMutex | ||
) | ||
|
||
func Register(schema string, c constructor) { | ||
lock.Lock() | ||
defer lock.Unlock() | ||
registry[schema] = c | ||
} | ||
|
||
func For(uri string) (Storage, error) { | ||
lock.RLock() | ||
defer lock.RUnlock() | ||
parts := strings.Split(uri, "://") | ||
|
||
// Paths without schema are treated as file:// and use the default LocalStorage implementation | ||
if len(parts) < 2 { | ||
uri, _ = filepath.Abs(uri) | ||
uri = LocalSchemaID + "://" + uri | ||
} | ||
|
||
u, err := url.Parse(uri) | ||
if err != nil { | ||
return nil, err | ||
} | ||
c, ok := registry[u.Scheme] | ||
if !ok { | ||
return nil, errors.New("schema '" + u.Scheme + "' not registered") | ||
} | ||
return c(*u), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package storage | ||
|
||
import ( | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestApp(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Storage Test Suite") | ||
} | ||
|
||
var _ = Describe("Storage", func() { | ||
When("schema is not registered", func() { | ||
BeforeEach(func() { | ||
registry = map[string]constructor{} | ||
}) | ||
|
||
It("should return error", func() { | ||
_, err := For("file:///tmp") | ||
Expect(err).To(HaveOccurred()) | ||
}) | ||
}) | ||
When("schema is registered", func() { | ||
BeforeEach(func() { | ||
registry = map[string]constructor{} | ||
Register("file", func(url url.URL) Storage { return &fakeLocalStorage{u: url} }) | ||
Register("s3", func(url url.URL) Storage { return &fakeS3Storage{u: url} }) | ||
}) | ||
|
||
It("should return correct implementation", func() { | ||
s, err := For("file:///tmp") | ||
Expect(err).ToNot(HaveOccurred()) | ||
Expect(s).To(BeAssignableToTypeOf(&fakeLocalStorage{})) | ||
Expect(s.(*fakeLocalStorage).u.Scheme).To(Equal("file")) | ||
Expect(s.(*fakeLocalStorage).u.Path).To(Equal("/tmp")) | ||
|
||
s, err = For("s3:///bucket") | ||
Expect(err).ToNot(HaveOccurred()) | ||
Expect(s).To(BeAssignableToTypeOf(&fakeS3Storage{})) | ||
Expect(s.(*fakeS3Storage).u.Scheme).To(Equal("s3")) | ||
Expect(s.(*fakeS3Storage).u.Path).To(Equal("/bucket")) | ||
}) | ||
It("should return a file implementation when schema is not specified", func() { | ||
s, err := For("/tmp") | ||
Expect(err).ToNot(HaveOccurred()) | ||
Expect(s).To(BeAssignableToTypeOf(&fakeLocalStorage{})) | ||
Expect(s.(*fakeLocalStorage).u.Scheme).To(Equal("file")) | ||
Expect(s.(*fakeLocalStorage).u.Path).To(Equal("/tmp")) | ||
}) | ||
It("should return a file implementation for a relative folder", func() { | ||
s, err := For("tmp") | ||
Expect(err).ToNot(HaveOccurred()) | ||
cwd, _ := os.Getwd() | ||
Expect(s).To(BeAssignableToTypeOf(&fakeLocalStorage{})) | ||
Expect(s.(*fakeLocalStorage).u.Scheme).To(Equal("file")) | ||
Expect(s.(*fakeLocalStorage).u.Path).To(Equal(filepath.Join(cwd, "tmp"))) | ||
}) | ||
It("should return error if schema is unregistered", func() { | ||
_, err := For("webdav:///tmp") | ||
Expect(err).To(HaveOccurred()) | ||
}) | ||
}) | ||
}) | ||
|
||
type fakeLocalStorage struct { | ||
Storage | ||
u url.URL | ||
} | ||
type fakeS3Storage struct { | ||
Storage | ||
u url.URL | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.