mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 18:37:09 +02:00
* feat(model): add per-library PID config columns * refactor(metadata): pass PID config to ToMediaFile and add spec validation * feat(scanner): rescan only libraries whose PID config changed * feat(server): validate library PID config and rescan on change * feat(ui): edit per-library PID config * fix(ui): label the PID mode selects * fix: tighten per-library PID rescan edge cases An interrupted PID rescan no longer upgrades every library to a full scan, a save that loses the race for the scanner logs at debug, the confirm dialog only shows when the effective PID spec changes, and it now gets translation keys. * refactor(metadata): pass the library to ToMediaFile ToMediaFile and core.Inspect took the library ID and its PID config as separate arguments, so a caller could mix values from two libraries. They now take the model.Library and resolve the effective PID config from it. * chore: tidy per-library PID comments, PropTypes and migration Trim comments that restated the code, add PropTypes to the new UI components, and recreate the migration with make migration-sql. * fix(ui): show the PID spec help under its input * feat(cmd): make inspect use the file's library PID config inspect always used the global PID config, so it showed different IDs than the scanner for files in a library with an override. It now finds the file's library in the DB and uses its effective config, falling back to the global config when there is no DB or the file is outside every library. It never creates a DB. The library path matcher moves from core/playlists to model so both can use it. * refactor: simplify per-library PID code Share the DB-file check between CLI commands, move ErrAlreadyScanning to model so core no longer imports scanner, read the libraries once for insights, and let ValidatePIDSpec accept an empty spec and look tags up directly. In the scanner, use FullScanInProgress instead of a second flag, and skip recomputing album IDs when the album spec did not change. In the UI, share the PID inputs between Create and Edit, and use docsUrl. * feat(ui): add section titles to Library Create and pre-fill Custom PID specs Custom now starts from the global spec, so admins edit a working spec instead of typing one from scratch. * fix(inspect): map files with the library-relative path the scanner uses Inspect gave metadata the file's directory as typed, so folder-based PIDs never matched the DB. It now uses the path relative to the library root, through the scanner's helper, which moves to model. * fix(scanner): say when a PID rescan only covers target folders * fix: reject tag aliases in album PID specs and match root libraries Tags are stored under canonical names, so an alias in a spec always reads as empty. In an album spec that gives every album the same ID, so album specs now require the tag name. Track specs keep accepting aliases, since the default one uses them. LibraryMatcher now matches paths under a library at the filesystem root. * refactor(model): move the tag alias lookup to tag_mappings.go * test: run the library matcher and inspect tests on Windows Build test paths with filepath instead of Unix literals, so they use the OS separator like filepath.Abs output, and drop the Windows skips. * feat(ui): add pt-BR translations for per-library PID settings
509 lines
19 KiB
Go
509 lines
19 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/db"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/resources"
|
|
"github.com/navidrome/navidrome/scanner"
|
|
"github.com/navidrome/navidrome/scheduler"
|
|
"github.com/navidrome/navidrome/server/backgrounds"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
// Import adapters to register them
|
|
_ "github.com/navidrome/navidrome/adapters/deezer"
|
|
_ "github.com/navidrome/navidrome/adapters/gotaglib"
|
|
_ "github.com/navidrome/navidrome/adapters/lastfm"
|
|
_ "github.com/navidrome/navidrome/adapters/listenbrainz"
|
|
)
|
|
|
|
var (
|
|
cfgFile string
|
|
noBanner bool
|
|
|
|
rootCmd = &cobra.Command{
|
|
Use: "navidrome",
|
|
Short: "Navidrome is a self-hosted music server and streamer",
|
|
Long: `Navidrome is a self-hosted music server and streamer.
|
|
Complete documentation is available at https://www.navidrome.org/docs`,
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
preRun()
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runNavidrome(cmd.Context()); err != nil {
|
|
log.Fatal("Fatal error in Navidrome. Aborting", err)
|
|
}
|
|
},
|
|
PostRun: func(cmd *cobra.Command, args []string) {
|
|
postRun()
|
|
},
|
|
Version: consts.Version,
|
|
}
|
|
)
|
|
|
|
// Execute runs the root cobra command, which will start the Navidrome server by calling the runNavidrome function.
|
|
func Execute() {
|
|
ctx, cancel := mainContext(context.Background())
|
|
defer cancel()
|
|
|
|
rootCmd.SetVersionTemplate(`{{println .Version}}`)
|
|
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func preRun() {
|
|
if !noBanner {
|
|
println(resources.Banner())
|
|
}
|
|
conf.Load(noBanner)
|
|
}
|
|
|
|
func postRun() {
|
|
log.Info("Navidrome stopped, bye.")
|
|
}
|
|
|
|
// runNavidrome is the main entry point for the Navidrome server. It starts all the services and blocks.
|
|
// If any of the services returns an error, it stops the others and returns that error, so the caller can
|
|
// exit with a non-zero code. If the context is cancelled (a signal or a service stop), it returns nil.
|
|
func runNavidrome(parentCtx context.Context) error {
|
|
defer db.Init(parentCtx)()
|
|
|
|
g, ctx := errgroup.WithContext(parentCtx)
|
|
g.Go(startServer(ctx))
|
|
g.Go(startSignaller(ctx))
|
|
g.Go(startScheduler(ctx))
|
|
g.Go(startPlaybackServer(ctx))
|
|
g.Go(startJellyfinDiscovery(ctx))
|
|
g.Go(schedulePeriodicBackup(ctx))
|
|
g.Go(startInsightsCollector(ctx))
|
|
g.Go(scheduleDBAnalyzer(ctx))
|
|
g.Go(startPluginManager(ctx))
|
|
artworkWorker := CreateArtworkWorker()
|
|
g.Go(startArtworkWorker(ctx, artworkWorker))
|
|
g.Go(scheduleArtworkHousekeeping(ctx, artworkWorker))
|
|
g.Go(runInitialScan(ctx))
|
|
if conf.Server.Scanner.Enabled {
|
|
g.Go(startScanWatcher(ctx))
|
|
g.Go(schedulePeriodicScan(ctx))
|
|
} else {
|
|
log.Warn(ctx, "Automatic Scanning is DISABLED")
|
|
}
|
|
|
|
// Errors caused by a normal shutdown are not failures
|
|
if err := g.Wait(); err != nil && parentCtx.Err() == nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mainContext returns a context that is cancelled when the process receives a signal to exit.
|
|
func mainContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return signal.NotifyContext(ctx,
|
|
os.Interrupt,
|
|
syscall.SIGHUP,
|
|
syscall.SIGTERM,
|
|
syscall.SIGABRT,
|
|
)
|
|
}
|
|
|
|
// startServer starts the Navidrome web server, adding all the necessary routers.
|
|
func startServer(ctx context.Context) func() error {
|
|
return func() error {
|
|
a := CreateServer()
|
|
a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter(ctx))
|
|
a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter(ctx))
|
|
a.MountRouter("Public Endpoints", consts.URLPathPublic, CreatePublicRouter())
|
|
if conf.Server.LastFM.Enabled {
|
|
a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
|
|
}
|
|
if conf.Server.ListenBrainz.Enabled {
|
|
a.MountRouter("ListenBrainz Auth", consts.URLPathNativeAPI+"/listenbrainz", CreateListenBrainzRouter())
|
|
}
|
|
if conf.Server.Jellyfin.Enabled {
|
|
a.MountRouter("Jellyfin API", consts.URLPathJellyfinAPI, CreateJellyfinAPIRouter(ctx))
|
|
}
|
|
if conf.Server.DevAPIv1 {
|
|
a.MountRouter("API v1", consts.URLPathAPIv1, CreateAPIv1Router(ctx))
|
|
}
|
|
if conf.Server.Prometheus.Enabled {
|
|
p := CreatePrometheus()
|
|
// blocking call because takes <100ms but useful if fails
|
|
p.WriteInitialMetrics(ctx)
|
|
a.MountRouter("Prometheus metrics", conf.Server.Prometheus.MetricsPath, p.GetHandler())
|
|
}
|
|
if conf.Server.DevEnableProfiler {
|
|
a.MountRouter("Profiling", "/debug", profilerHandler())
|
|
}
|
|
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
|
|
a.MountRouter("Background images", conf.Server.UILoginBackgroundURL, backgrounds.NewHandler())
|
|
}
|
|
return a.Run(ctx, conf.Server.Address, conf.Server.Port, conf.Server.TLSCert, conf.Server.TLSKey)
|
|
}
|
|
}
|
|
|
|
// profilerHandler returns the pprof handler. net/http/pprof resolves the profile
|
|
// name from the raw request path, so the BasePath has to come off first.
|
|
func profilerHandler() http.Handler {
|
|
// A trailing or root slash would make StripPrefix drop the leading slash chi needs.
|
|
basePath := strings.TrimRight(conf.Server.BasePath, "/")
|
|
return http.StripPrefix(basePath, middleware.Profiler())
|
|
}
|
|
|
|
// schedulePeriodicScan schedules a periodic scan of the music library, if configured.
|
|
func schedulePeriodicScan(ctx context.Context) func() error {
|
|
return func() error {
|
|
schedule := conf.Server.Scanner.Schedule
|
|
if schedule == "" {
|
|
log.Info(ctx, "Periodic scan is DISABLED")
|
|
return nil
|
|
}
|
|
|
|
s := CreateScanner(ctx)
|
|
schedulerInstance := scheduler.GetInstance()
|
|
|
|
log.Info("Scheduling periodic scan", "schedule", schedule)
|
|
_, err := schedulerInstance.Add(schedule, func() {
|
|
_, err := s.ScanAll(ctx, false)
|
|
if err != nil {
|
|
log.Error(ctx, "Error executing periodic scan", err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
log.Error(ctx, "Error scheduling periodic scan", err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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 nil, err
|
|
}
|
|
var names []string
|
|
for _, lib := range libs {
|
|
if lib.PIDChanged() {
|
|
names = append(names, lib.Name)
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// runInitialScan runs an initial scan of the music library if needed.
|
|
func runInitialScan(ctx context.Context) func() error {
|
|
return func() error {
|
|
ds := CreateDataStore()
|
|
fullScanRequired, err := ds.Property().DefaultGet(ctx, consts.FullScanAfterMigrationFlagKey, "0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inProgress, err := ds.Library().ScanInProgress(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pidChangedLibs, err := librariesWithChangedPID(ctx, ds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scanOnStartup := conf.Server.Scanner.Enabled && conf.Server.Scanner.ScanOnStartup
|
|
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)
|
|
switch {
|
|
case fullScanRequired == "1":
|
|
log.Warn(ctx, "Full scan required after migration")
|
|
_ = ds.Property().Delete(ctx, consts.FullScanAfterMigrationFlagKey)
|
|
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:
|
|
log.Info("Executing initial scan")
|
|
}
|
|
|
|
_, err = s.ScanAll(ctx, fullScanRequired == "1")
|
|
if err != nil {
|
|
log.Error(ctx, "Scan failed", err)
|
|
} else {
|
|
log.Info(ctx, "Scan completed")
|
|
}
|
|
} else {
|
|
log.Debug(ctx, "Initial scan not needed")
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func startScanWatcher(ctx context.Context) func() error {
|
|
return func() error {
|
|
if conf.Server.Scanner.WatcherWait == 0 {
|
|
log.Debug("Folder watcher is DISABLED")
|
|
return nil
|
|
}
|
|
w := CreateScanWatcher(ctx)
|
|
err := w.Run(ctx)
|
|
if err != nil {
|
|
log.Error("Error starting watcher", err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func schedulePeriodicBackup(ctx context.Context) func() error {
|
|
return func() error {
|
|
schedule := conf.Server.Backup.Schedule
|
|
if schedule == "" {
|
|
log.Info(ctx, "Periodic backup is DISABLED")
|
|
return nil
|
|
}
|
|
|
|
schedulerInstance := scheduler.GetInstance()
|
|
|
|
log.Info("Scheduling periodic backup", "schedule", schedule)
|
|
_, err := schedulerInstance.Add(schedule, func() {
|
|
start := time.Now()
|
|
path, err := db.Backup(ctx)
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
log.Error(ctx, "Error backing up database", "elapsed", elapsed, err)
|
|
return
|
|
}
|
|
log.Info(ctx, "Backup complete", "elapsed", elapsed, "path", path)
|
|
|
|
count, err := db.Prune(ctx)
|
|
if err != nil {
|
|
log.Error(ctx, "Error pruning database", "error", err)
|
|
} else if count > 0 {
|
|
log.Info(ctx, "Successfully pruned old files", "count", count)
|
|
} else {
|
|
log.Info(ctx, "No backups pruned")
|
|
}
|
|
})
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
func scheduleDBAnalyzer(ctx context.Context) func() error {
|
|
return func() error {
|
|
if !conf.Server.EnableScheduledDBAnalyze {
|
|
log.Info(ctx, "Scheduled DB analysis is DISABLED")
|
|
return nil
|
|
}
|
|
log.Info(ctx, "Scheduling DB analysis check", "schedule", consts.DBAnalyzeCheckSchedule)
|
|
schedulerInstance := scheduler.GetInstance()
|
|
_, err := schedulerInstance.Add(consts.DBAnalyzeCheckSchedule, func() {
|
|
release, ok := scanner.LockForMaintenance()
|
|
if !ok {
|
|
log.Debug(ctx, "Skipping DB analysis check because a scan is in progress")
|
|
return
|
|
}
|
|
defer release()
|
|
if _, err := db.OptimizeIfNeeded(ctx); err != nil {
|
|
log.Error(ctx, "Error analyzing DB", err)
|
|
}
|
|
})
|
|
return err
|
|
}
|
|
}
|
|
|
|
// startScheduler starts the Navidrome scheduler, which is used to run periodic tasks.
|
|
func startScheduler(ctx context.Context) func() error {
|
|
return func() error {
|
|
log.Info(ctx, "Starting scheduler")
|
|
schedulerInstance := scheduler.GetInstance()
|
|
schedulerInstance.Run(ctx)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// startInsightsCollector starts the Navidrome Insight Collector, if configured.
|
|
func startInsightsCollector(ctx context.Context) func() error {
|
|
return func() error {
|
|
if !conf.Server.EnableInsightsCollector {
|
|
log.Info(ctx, "Insight Collector is DISABLED")
|
|
return nil
|
|
}
|
|
log.Info(ctx, "Starting Insight Collector")
|
|
select {
|
|
case <-time.After(conf.Server.DevInsightsInitialDelay):
|
|
case <-ctx.Done():
|
|
return nil
|
|
}
|
|
ic := CreateInsights()
|
|
ic.Run(ctx)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// startJellyfinDiscovery never returns an error: a discovery failure must not stop the server.
|
|
func startJellyfinDiscovery(ctx context.Context) func() error {
|
|
return func() error {
|
|
if !conf.Server.Jellyfin.Enabled || !conf.Server.Jellyfin.AutoDiscovery {
|
|
log.Debug("Jellyfin auto-discovery is DISABLED")
|
|
return nil
|
|
}
|
|
CreateJellyfinDiscovery().Serve(ctx)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// startPlaybackServer starts the Navidrome playback server, if configured.
|
|
// It is responsible for the Jukebox functionality
|
|
func startPlaybackServer(ctx context.Context) func() error {
|
|
return func() error {
|
|
if !conf.Server.Jukebox.Enabled {
|
|
log.Debug("Jukebox is DISABLED")
|
|
return nil
|
|
}
|
|
log.Info(ctx, "Starting Jukebox service")
|
|
playbackInstance := GetPlaybackServer()
|
|
return playbackInstance.Run(ctx)
|
|
}
|
|
}
|
|
|
|
// startArtworkWorker starts the background artwork acquisition worker. It always
|
|
// runs; the queue is simply empty until something enqueues work into it.
|
|
func startArtworkWorker(ctx context.Context, worker *artwork.Worker) func() error {
|
|
return func() error {
|
|
log.Info(ctx, "Starting artwork worker")
|
|
// The scanner writes to the DB for its whole run; competing for the write lock makes both fail.
|
|
worker.PauseWhile(scanner.IsScanning)
|
|
return worker.Run(ctx)
|
|
}
|
|
}
|
|
|
|
// outsideScan runs a DB maintenance job unless a scan is running, and keeps a scan from starting
|
|
// until it ends; both write to the DB, and competing for the lock can make either fail.
|
|
func outsideScan(ctx context.Context, job string, run func(context.Context) error) {
|
|
release, ok := scanner.LockForMaintenance()
|
|
if !ok {
|
|
log.Debug(ctx, "Skipping "+job+" because a scan is in progress")
|
|
return
|
|
}
|
|
defer release()
|
|
if err := run(ctx); err != nil {
|
|
log.Error(ctx, "Error running "+job, err)
|
|
}
|
|
}
|
|
|
|
// scheduleArtworkHousekeeping registers the recurring missing-state and prune jobs, and
|
|
// reports an artwork config change without acting on it.
|
|
func scheduleArtworkHousekeeping(ctx context.Context, worker *artwork.Worker) func() error {
|
|
return func() error {
|
|
schedulerInstance := scheduler.GetInstance()
|
|
|
|
if _, err := schedulerInstance.Add(consts.ArtworkEnqueueMissingSchedule, func() {
|
|
outsideScan(ctx, "artwork missing-state recheck", worker.EnqueueMissingAll)
|
|
}); err != nil {
|
|
log.Error(ctx, "Error scheduling artwork missing-state recheck", err)
|
|
}
|
|
|
|
if _, err := schedulerInstance.Add(consts.ArtworkPruneSchedule, func() {
|
|
outsideScan(ctx, "artwork prune", worker.RunPrune)
|
|
}); err != nil {
|
|
log.Error(ctx, "Error scheduling artwork prune", err)
|
|
}
|
|
|
|
// Also run the missing-row recheck once at startup so a never-scanned entity is picked up
|
|
// immediately, not only on the next hourly tick (e.g. after enabling the feature).
|
|
outsideScan(ctx, "artwork missing-state recheck", worker.EnqueueMissingAll)
|
|
|
|
if err := worker.ReconcileConfig(ctx); err != nil {
|
|
log.Error(ctx, "Error checking the artwork config fingerprint", err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// startPluginManager starts the plugin manager, if configured.
|
|
func startPluginManager(ctx context.Context) func() error {
|
|
return func() error {
|
|
manager := GetPluginManager(ctx)
|
|
if !conf.Server.Plugins.Enabled {
|
|
log.Debug("Plugin system is DISABLED")
|
|
return nil
|
|
}
|
|
log.Info(ctx, "Starting plugin manager")
|
|
return manager.Start(ctx)
|
|
}
|
|
}
|
|
|
|
// TODO: Implement some struct tags to map flags to viper
|
|
func init() {
|
|
cobra.OnInitialize(func() {
|
|
conf.InitConfig(cfgFile, true)
|
|
})
|
|
|
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
|
|
rootCmd.PersistentFlags().BoolVarP(&noBanner, "nobanner", "n", false, `don't show banner`)
|
|
rootCmd.PersistentFlags().String("musicfolder", viper.GetString("musicfolder"), "folder where your music is stored")
|
|
rootCmd.PersistentFlags().String("datafolder", viper.GetString("datafolder"), "folder to store application data (DB), needs write access")
|
|
rootCmd.PersistentFlags().String("cachefolder", viper.GetString("cachefolder"), "folder to store cache data (transcoding, images...), needs write access")
|
|
rootCmd.PersistentFlags().StringP("loglevel", "l", viper.GetString("loglevel"), "log level, possible values: error, info, debug, trace")
|
|
rootCmd.PersistentFlags().String("logfile", viper.GetString("logfile"), "log file path, if not set logs will be printed to stderr")
|
|
|
|
_ = viper.BindPFlag("musicfolder", rootCmd.PersistentFlags().Lookup("musicfolder"))
|
|
_ = viper.BindPFlag("datafolder", rootCmd.PersistentFlags().Lookup("datafolder"))
|
|
_ = viper.BindPFlag("cachefolder", rootCmd.PersistentFlags().Lookup("cachefolder"))
|
|
_ = viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
|
|
_ = viper.BindPFlag("logfile", rootCmd.PersistentFlags().Lookup("logfile"))
|
|
|
|
rootCmd.Flags().StringP("address", "a", viper.GetString("address"), "IP address to bind to")
|
|
rootCmd.Flags().IntP("port", "p", viper.GetInt("port"), "HTTP port Navidrome will listen to")
|
|
rootCmd.Flags().String("baseurl", viper.GetString("baseurl"), "base URL to configure Navidrome behind a proxy (ex: /music or http://my.server.com)")
|
|
rootCmd.Flags().String("tlscert", viper.GetString("tlscert"), "optional path to a TLS cert file (enables HTTPS listening)")
|
|
rootCmd.Flags().String("unixsocketperm", viper.GetString("unixsocketperm"), "optional file permission for the unix socket")
|
|
rootCmd.Flags().String("tlskey", viper.GetString("tlskey"), "optional path to a TLS key file (enables HTTPS listening)")
|
|
|
|
rootCmd.Flags().Duration("sessiontimeout", viper.GetDuration("sessiontimeout"), "how long Navidrome will wait before closing web ui idle sessions")
|
|
rootCmd.Flags().Duration("scaninterval", viper.GetDuration("scaninterval"), "how frequently to scan for changes in your music library")
|
|
rootCmd.Flags().String("uiloginbackgroundurl", viper.GetString("uiloginbackgroundurl"), "URL to a backaground image used in the Login page")
|
|
rootCmd.Flags().Bool("enabletranscodingconfig", viper.GetBool("enabletranscodingconfig"), "enables transcoding configuration in the UI")
|
|
rootCmd.Flags().Bool("enabletranscodingcancellation", viper.GetBool("enabletranscodingcancellation"), "enables transcoding context cancellation")
|
|
rootCmd.Flags().String("transcodingcachesize", viper.GetString("transcodingcachesize"), "size of transcoding cache")
|
|
rootCmd.Flags().String("imagecachesize", viper.GetString("imagecachesize"), "size of image (art work) cache. set to 0 to disable cache")
|
|
rootCmd.Flags().String("albumplaycountmode", viper.GetString("albumplaycountmode"), "how to compute playcount for albums. absolute (default) or normalized")
|
|
rootCmd.Flags().Bool("autoimportplaylists", viper.GetBool("autoimportplaylists"), "enable/disable .m3u playlist auto-import`")
|
|
|
|
rootCmd.Flags().Bool("prometheus.enabled", viper.GetBool("prometheus.enabled"), "enable/disable prometheus metrics endpoint")
|
|
rootCmd.Flags().String("prometheus.metricspath", viper.GetString("prometheus.metricspath"), "http endpoint for prometheus metrics")
|
|
|
|
_ = viper.BindPFlag("address", rootCmd.Flags().Lookup("address"))
|
|
_ = viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
|
|
_ = viper.BindPFlag("tlscert", rootCmd.Flags().Lookup("tlscert"))
|
|
_ = viper.BindPFlag("unixsocketperm", rootCmd.Flags().Lookup("unixsocketperm"))
|
|
_ = viper.BindPFlag("tlskey", rootCmd.Flags().Lookup("tlskey"))
|
|
_ = viper.BindPFlag("baseurl", rootCmd.Flags().Lookup("baseurl"))
|
|
|
|
_ = viper.BindPFlag("sessiontimeout", rootCmd.Flags().Lookup("sessiontimeout"))
|
|
_ = viper.BindPFlag("scaninterval", rootCmd.Flags().Lookup("scaninterval"))
|
|
_ = viper.BindPFlag("uiloginbackgroundurl", rootCmd.Flags().Lookup("uiloginbackgroundurl"))
|
|
|
|
_ = viper.BindPFlag("prometheus.enabled", rootCmd.Flags().Lookup("prometheus.enabled"))
|
|
_ = viper.BindPFlag("prometheus.metricspath", rootCmd.Flags().Lookup("prometheus.metricspath"))
|
|
|
|
_ = viper.BindPFlag("enabletranscodingconfig", rootCmd.Flags().Lookup("enabletranscodingconfig"))
|
|
_ = viper.BindPFlag("enabletranscodingcancellation", rootCmd.Flags().Lookup("enabletranscodingcancellation"))
|
|
_ = viper.BindPFlag("transcodingcachesize", rootCmd.Flags().Lookup("transcodingcachesize"))
|
|
_ = viper.BindPFlag("imagecachesize", rootCmd.Flags().Lookup("imagecachesize"))
|
|
}
|