mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-09 19:07:12 +02:00
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling vacuum v0.30.6's `bundle --composed` mangles component names for this spec's multi-file layout (duplicates Problem as Problem__schemas etc.), so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead. * fix(api): pin the Redocly CLI version Tried moving components out of the root document (per libopenapi's nested_files example) so vacuum's own bundler could produce clean names, but any component declared via $ref inside components.* still gets a __<parent>-suffixed twin regardless of collisions elsewhere, so vacuum's --composed bundler can't cleanly bundle this spec. Pin the already-working Redocly fallback to an exact version instead of @latest. * fix(api): bundle the OpenAPI spec with vacuum vacuum's --composed bundler suffixes any component reached via a $ref written directly inside the root document's own components.* block, regardless of collisions elsewhere. Dropping the root-level schemas/ parameters/responses declarations (keeping only securitySchemes, and leaving every component file under api/openapi/components/ untouched) lets vacuum bundle cleanly with no __ suffixes, going back to Go-only tooling. Components nothing references yet (ListMeta, offset, limit, BadRequest, Unauthorized, Forbidden, NotFound) are absent from the bundle until a later task's operation references them. * fix(api): make spec lint rules cover all schemas and error codes nd-schema-property-descriptions targeted $.components.schemas, but our schemas live in path/response files, not the root document, so it was dead code; switched to $..properties[*] to walk every resolved schema wherever it ends up. nd-error-responses-are-problems only checked a hardcoded status-code list; switched to a patternProperties schema matching the full 4xx/5xx range. Also: api-diff now diffs against the merge-base with API_DIFF_BASE (falling back to its tip with a notice if no merge-base exists), gen no longer depends on api-gen until Task 3 wires up oapi-codegen, and api-lint suppresses vacuum's banner. * feat(api): embed the bundled OpenAPI spec and expose its version * feat(api): generate the v1 server interface with oapi-codegen * feat(api): add RFC 9457 problem responses for API v1 * feat(api): add API v1 router with /server discovery and spec routes * fix(api): serve the OpenAPI document without range support * feat(api): mount API v1 behind the DevAPIv1 flag * chore(ci): lint, regenerate and diff the OpenAPI v1 spec * refactor(api): tighten spec version access, lint rules and test naming * refactor(api): simplify spec routes, tests and OpenAPI tooling Share one If-None-Match parser (utils/req) between the image and spec routes, declare the YAML spec response as an object so tests need no decoder override, and reuse ETag/304 spec components. Install the OpenAPI tools only when missing or at a different version, fail api-diff when its base ref does not exist, and in CI cache the tools, fold regeneration into the go generate check, and fetch only the PR base commit for the breaking-change gate. * refactor(api): raise the list limit maximum to 2000 and drop the flag test * feat(api): treat added enum values as non-breaking Enums in API v1 are open: clients must accept unknown values. api-diff now downgrades response-property-enum-value-added to INFO, while removing a value from a request enum stays breaking. * feat(api): gate breaking changes on x-stability-level Every operation declares x-stability-level (alpha, beta, stable). oasdiff ignores breaking changes to alpha operations and rejects lowering a level, so unreleased endpoints can evolve while beta and stable ones stay additive. All current operations start as alpha. * feat(api): declare loginMethods as an enum Prefix generated enum constants with their type name so enums sharing a value (for example password) cannot collide in package apiv1. * feat(api): send Allow on 405 and answer HEAD wherever GET is routed chi only sets Allow in its default 405 handler, so the problem-format handler now builds it by matching each method against the v1 router. HEAD requests fall back to the GET route, as RFC 9110 expects. * refactor(api): hash the spec ETag with xxh3 The bytes are compiled in, and the digest was truncated to 64 bits anyway, so this matches the artwork ETags instead of paying for cryptographic strength we discard. * docs(api): explain the about:blank problem type * feat(api): make code the problem identifier and omit a blank type RFC 9457 says clients switch on the type URI, but no adopter surveyed ships both a populated type and a separate code. Declare code as an enum, and send type only once a problem has semantics of its own. * fix(api): advertise the configured base path in the served OpenAPI spec With BaseURL=/music the API is mounted at /music/api/v1, but the spec told clients to call /api/v1 at the host root. The server now rewrites servers[0].url to BasePath + /api/v1 when it serves the document. Relative server URLs were tested first: "." and "../v1" work in openapi-generator, Swagger UI and Redoc, but Scalar resolves them against the page origin, so it breaks even without a base path. The committed bundle keeps /api/v1, and a test pins that it appears exactly once, which the rewrite relies on.
501 lines
18 KiB
Go
501 lines
18 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) {
|
|
runNavidrome(cmd.Context())
|
|
},
|
|
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 will log it and exit. If the process receives a signal to exit,
|
|
// it will cancel the context and exit gracefully.
|
|
func runNavidrome(ctx context.Context) {
|
|
defer db.Init(ctx)()
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
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")
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
log.Error("Fatal error in Navidrome. Aborting", err)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
func pidHashChanged(ds model.DataStore) (bool, error) {
|
|
pidAlbum, err := ds.Property().DefaultGet(context.Background(), consts.PIDAlbumKey, "")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
pidTrack, err := ds.Property().DefaultGet(context.Background(), consts.PIDTrackKey, "")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !strings.EqualFold(pidAlbum, conf.Server.PID.Album) || !strings.EqualFold(pidTrack, conf.Server.PID.Track), 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
|
|
}
|
|
pidHasChanged, err := pidHashChanged(ds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scanOnStartup := conf.Server.Scanner.Enabled && conf.Server.Scanner.ScanOnStartup
|
|
scanNeeded := scanOnStartup || inProgress || fullScanRequired == "1" || pidHasChanged
|
|
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 pidHasChanged:
|
|
log.Warn(ctx, "PID config changed, performing full scan")
|
|
fullScanRequired = "1"
|
|
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"))
|
|
}
|