mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-31 07:27:58 +01:00
Merge remote-tracking branch 'origin/master' into pr5949
# Conflicts: # cmd/wire_gen.go
This commit is contained in:
commit
bd6a3fb16e
58 changed files with 1732 additions and 543 deletions
|
|
@ -1,13 +1,17 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/persistence"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
|
@ -28,7 +32,7 @@ var inspectCmd = &cobra.Command{
|
|||
Long: "Show file tags as seen by Navidrome",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runInspector(args)
|
||||
runInspector(cmd.Context(), args)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -55,18 +59,24 @@ func prettyMarshal(v any) ([]byte, error) {
|
|||
return []byte(res.String()), nil
|
||||
}
|
||||
|
||||
func runInspector(args []string) {
|
||||
func runInspector(ctx context.Context, args []string) {
|
||||
marshal := marshalers[format]
|
||||
if marshal == nil {
|
||||
log.Fatal("Invalid format", "format", format)
|
||||
}
|
||||
libs := loadLibraries(ctx)
|
||||
matcher := model.NewLibraryMatcher(libs)
|
||||
var out []core.InspectOutput
|
||||
for _, filePath := range args {
|
||||
if !model.IsAudioFile(filePath) {
|
||||
log.Warn("Not an audio file", "file", filePath)
|
||||
continue
|
||||
}
|
||||
output, err := core.Inspect(filePath, 1, "")
|
||||
lib, ok := libraryForFile(matcher, filePath)
|
||||
if !ok && len(libs) > 0 {
|
||||
log.Warn("File is not in any library, using the global PID config", "file", filePath)
|
||||
}
|
||||
output, err := core.Inspect(filePath, lib, "")
|
||||
if err != nil {
|
||||
log.Warn("Unable to process file", "file", filePath, "error", err)
|
||||
continue
|
||||
|
|
@ -77,3 +87,33 @@ func runInspector(args []string) {
|
|||
data, _ := marshal(out)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
// loadLibraries reads the libraries, so each file gets its library's PID config. It never creates a DB.
|
||||
func loadLibraries(ctx context.Context) model.Libraries {
|
||||
if dbFile, ok := existingDBFile(); !ok {
|
||||
log.Warn(ctx, "No database found, using the global PID config", "path", dbFile)
|
||||
return nil
|
||||
}
|
||||
defer db.Init(ctx)()
|
||||
libs, err := persistence.New(db.Db()).Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Could not load libraries, using the global PID config", err)
|
||||
return nil
|
||||
}
|
||||
for i := range libs {
|
||||
if absPath, err := filepath.Abs(libs[i].Path); err == nil {
|
||||
libs[i].Path = absPath
|
||||
}
|
||||
}
|
||||
return libs
|
||||
}
|
||||
|
||||
// libraryForFile falls back to the default library with no overrides, which uses the global PID config.
|
||||
func libraryForFile(matcher *model.LibraryMatcher, filePath string) (model.Library, bool) {
|
||||
if absPath, err := filepath.Abs(filePath); err == nil {
|
||||
if lib, ok := matcher.FindLibrary(absPath); ok {
|
||||
return lib, true
|
||||
}
|
||||
}
|
||||
return model.Library{ID: model.DefaultLibraryID}, false
|
||||
}
|
||||
|
|
|
|||
61
cmd/inspect_test.go
Normal file
61
cmd/inspect_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("inspect", func() {
|
||||
Describe("libraryForFile", func() {
|
||||
var matcher *model.LibraryMatcher
|
||||
var root string
|
||||
|
||||
BeforeEach(func() {
|
||||
root = GinkgoT().TempDir()
|
||||
cwd, err := os.Getwd()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
matcher = model.NewLibraryMatcher(model.Libraries{
|
||||
{ID: 1, Path: filepath.Join(root, "music")},
|
||||
{ID: 2, Path: filepath.Join(cwd, "loose"), PIDAlbum: "folder"},
|
||||
})
|
||||
})
|
||||
|
||||
It("returns the library that contains an absolute path", func() {
|
||||
lib, ok := libraryForFile(matcher, filepath.Join(root, "music", "album", "track.mp3"))
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(lib.ID).To(Equal(1))
|
||||
})
|
||||
|
||||
It("resolves a relative path against the working directory", func() {
|
||||
lib, ok := libraryForFile(matcher, filepath.Join("loose", "track.mp3"))
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(lib.PIDAlbum).To(Equal("folder"))
|
||||
})
|
||||
|
||||
It("falls back to the default library without overrides", func() {
|
||||
lib, ok := libraryForFile(matcher, filepath.Join(root, "elsewhere", "track.mp3"))
|
||||
Expect(ok).To(BeFalse())
|
||||
Expect(lib).To(Equal(model.Library{ID: model.DefaultLibraryID}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("loadLibraries", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
})
|
||||
|
||||
It("does not create a database when there is none", func() {
|
||||
dbFile := filepath.Join(GinkgoT().TempDir(), "navidrome.db")
|
||||
conf.Server.DbPath = dbFile + "?_journal_mode=WAL"
|
||||
|
||||
Expect(loadLibraries(GinkgoT().Context())).To(BeNil())
|
||||
Expect(dbFile).ToNot(BeAnExistingFile())
|
||||
})
|
||||
})
|
||||
})
|
||||
28
cmd/root.go
28
cmd/root.go
|
|
@ -190,16 +190,20 @@ func schedulePeriodicScan(ctx context.Context) func() error {
|
|||
}
|
||||
}
|
||||
|
||||
func pidHashChanged(ds model.DataStore) (bool, error) {
|
||||
pidAlbum, err := ds.Property().DefaultGet(context.Background(), consts.PIDAlbumKey, "")
|
||||
// librariesWithChangedPID returns the names of the libraries whose effective PID config differs from
|
||||
// the one used by their last finished scan
|
||||
func librariesWithChangedPID(ctx context.Context, ds model.DataStore) ([]string, error) {
|
||||
libs, err := ds.Library().GetAll(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
pidTrack, err := ds.Property().DefaultGet(context.Background(), consts.PIDTrackKey, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
var names []string
|
||||
for _, lib := range libs {
|
||||
if lib.PIDChanged() {
|
||||
names = append(names, lib.Name)
|
||||
}
|
||||
}
|
||||
return !strings.EqualFold(pidAlbum, conf.Server.PID.Album) || !strings.EqualFold(pidTrack, conf.Server.PID.Track), nil
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// runInitialScan runs an initial scan of the music library if needed.
|
||||
|
|
@ -214,12 +218,12 @@ func runInitialScan(ctx context.Context) func() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pidHasChanged, err := pidHashChanged(ds)
|
||||
pidChangedLibs, err := librariesWithChangedPID(ctx, ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanOnStartup := conf.Server.Scanner.Enabled && conf.Server.Scanner.ScanOnStartup
|
||||
scanNeeded := scanOnStartup || inProgress || fullScanRequired == "1" || pidHasChanged
|
||||
scanNeeded := scanOnStartup || inProgress || fullScanRequired == "1" || len(pidChangedLibs) > 0
|
||||
time.Sleep(2 * time.Second) // Wait 2 seconds before the initial scan
|
||||
if scanNeeded {
|
||||
s := CreateScanner(ctx)
|
||||
|
|
@ -227,9 +231,9 @@ func runInitialScan(ctx context.Context) func() error {
|
|||
case fullScanRequired == "1":
|
||||
log.Warn(ctx, "Full scan required after migration")
|
||||
_ = ds.Property().Delete(ctx, consts.FullScanAfterMigrationFlagKey)
|
||||
case pidHasChanged:
|
||||
log.Warn(ctx, "PID config changed, performing full scan")
|
||||
fullScanRequired = "1"
|
||||
case len(pidChangedLibs) > 0:
|
||||
// Includes never-scanned libraries. The scanner rescans in full only the ones that need it
|
||||
log.Warn(ctx, "Libraries with a new or changed PID config, scanning", "libraries", pidChangedLibs)
|
||||
case inProgress:
|
||||
log.Warn(ctx, "Resuming interrupted scan")
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
|
|
@ -9,6 +10,8 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
|
@ -44,3 +47,30 @@ var _ = Describe("profilerHandler", func() {
|
|||
Entry("with a trailing-slash BasePath", "/music/"),
|
||||
)
|
||||
})
|
||||
|
||||
var _ = Describe("librariesWithChangedPID", func() {
|
||||
var ds *tests.MockDataStore
|
||||
var libs *tests.MockLibraryRepo
|
||||
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
libs = &tests.MockLibraryRepo{}
|
||||
ds = &tests.MockDataStore{MockedLibrary: libs}
|
||||
})
|
||||
|
||||
It("returns only the libraries whose PID config changed", func() {
|
||||
pid := model.Library{}.EffectivePID()
|
||||
libs.SetData(model.Libraries{
|
||||
{ID: 1, Name: "Same", ScannedPIDAlbum: pid.Album, ScannedPIDTrack: pid.Track},
|
||||
{ID: 2, Name: "Changed", PIDAlbum: "folder", ScannedPIDAlbum: pid.Album, ScannedPIDTrack: pid.Track},
|
||||
{ID: 3, Name: "Never scanned"},
|
||||
})
|
||||
Expect(librariesWithChangedPID(GinkgoT().Context(), ds)).To(ConsistOf("Changed", "Never scanned"))
|
||||
})
|
||||
|
||||
It("returns the error from the repository", func() {
|
||||
libs.Err = errors.New("db down")
|
||||
_, err := librariesWithChangedPID(GinkgoT().Context(), ds)
|
||||
Expect(err).To(MatchError("db down"))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
13
cmd/utils.go
13
cmd/utils.go
|
|
@ -18,11 +18,16 @@ import (
|
|||
"github.com/navidrome/navidrome/persistence"
|
||||
)
|
||||
|
||||
// requireExistingDB aborts the command when the database file (DbPath minus DSN
|
||||
// params) does not exist.
|
||||
func requireExistingDB() {
|
||||
// existingDBFile returns the database file (DbPath minus DSN params), and whether it exists.
|
||||
func existingDBFile() (string, bool) {
|
||||
path, _, _ := strings.Cut(conf.Server.DbPath, "?")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
_, err := os.Stat(path)
|
||||
return path, err == nil
|
||||
}
|
||||
|
||||
// requireExistingDB aborts the command when the database file does not exist.
|
||||
func requireExistingDB() {
|
||||
if path, ok := existingDBFile(); !ok {
|
||||
log.Fatal("No existing database", "path", path)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import (
|
|||
"github.com/navidrome/navidrome/core/matcher"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/core/playback"
|
||||
"github.com/navidrome/navidrome/core/podcasts"
|
||||
"github.com/navidrome/navidrome/core/playlists"
|
||||
"github.com/navidrome/navidrome/core/podcasts"
|
||||
"github.com/navidrome/navidrome/core/quickconnect"
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/core/sonic"
|
||||
|
|
@ -96,8 +96,9 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, imageStore, fFmpeg)
|
||||
transcodingCache := stream.GetTranscodingCache()
|
||||
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
transcodeDecider := stream.NewTranscodeDecider(dataStore, fFmpeg)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share, artworkArtwork)
|
||||
archiver := core.NewArchiver(mediaStreamer, transcodeDecider, dataStore, share, artworkArtwork)
|
||||
players := core.NewPlayers(dataStore)
|
||||
broker := events.GetBroker()
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
|
|
@ -111,10 +112,9 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
||||
playbackServer := playback.GetInstance(dataStore)
|
||||
lyricsLyrics := lyrics.NewLyrics(dataStore, manager)
|
||||
transcodeDecider := stream.NewTranscodeDecider(dataStore, fFmpeg)
|
||||
podcastService := podcasts.NewPodcastService(ctx, dataStore, fFmpeg, broker)
|
||||
podcastsPodcasts := podcasts.NewPodcastService(ctx, dataStore, fFmpeg, broker)
|
||||
sonicSonic := sonic.New(dataStore, manager, matcherMatcher)
|
||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, transcodeDecider, podcastService, sonicSonic)
|
||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, transcodeDecider, podcastsPodcasts, sonicSonic)
|
||||
return router
|
||||
}
|
||||
|
||||
|
|
@ -161,9 +161,10 @@ func CreatePublicRouter() *public.Router {
|
|||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, imageStore, fFmpeg)
|
||||
transcodingCache := stream.GetTranscodingCache()
|
||||
mediaStreamer := stream.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||
transcodeDecider := stream.NewTranscodeDecider(dataStore, fFmpeg)
|
||||
share := core.NewShare(dataStore)
|
||||
archiver := core.NewArchiver(mediaStreamer, dataStore, share, artworkArtwork)
|
||||
router := public.New(dataStore, artworkArtwork, mediaStreamer, share, archiver)
|
||||
archiver := core.NewArchiver(mediaStreamer, transcodeDecider, dataStore, share, artworkArtwork)
|
||||
router := public.New(dataStore, artworkArtwork, mediaStreamer, transcodeDecider, share, archiver)
|
||||
return router
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue