mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 18:37:09 +02:00
Resolve conflicts with the stateless-repositories refactor (#6149): - port podcast repositories, model interfaces, mocks, service and handlers to per-call ctx - podcast channel repo now implements generic rest.Repository/Persistable - register podcast repos in SQLStore and MockDataStore - merge OpenSubsonic extensions (apiKeyAuthentication + podcast extensions) - renumber podcast migrations after upstream's latest (20260930...)
261 lines
8.4 KiB
Go
261 lines
8.4 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/deluan/rest"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/core/external"
|
|
"github.com/navidrome/navidrome/core/metrics"
|
|
playlistsvc "github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/core/quickconnect"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/server"
|
|
)
|
|
|
|
// PluginManager defines the interface for plugin management operations.
|
|
// This interface is used by the API handlers to enable/disable plugins and update configuration.
|
|
type PluginManager interface {
|
|
EnablePlugin(ctx context.Context, id string) error
|
|
DisablePlugin(ctx context.Context, id string) error
|
|
ValidatePluginConfig(ctx context.Context, id, configJSON string) error
|
|
UpdatePluginConfig(ctx context.Context, id, configJSON string) error
|
|
UpdatePluginUsers(ctx context.Context, id, usersJSON string, allUsers bool) error
|
|
UpdatePluginLibraries(ctx context.Context, id, librariesJSON string, allLibraries, allowWriteAccess bool) error
|
|
RescanPlugins(ctx context.Context) error
|
|
UnloadDisabledPlugins(ctx context.Context)
|
|
}
|
|
|
|
type Router struct {
|
|
http.Handler
|
|
ds model.DataStore
|
|
share core.Share
|
|
playlists playlistsvc.Playlists
|
|
insights metrics.Insights
|
|
libs core.Library
|
|
users core.User
|
|
maintenance core.Maintenance
|
|
pluginManager PluginManager
|
|
imgUpload artwork.Uploader
|
|
provider external.Provider
|
|
quickConnect quickconnect.QuickConnect
|
|
}
|
|
|
|
func New(ds model.DataStore, share core.Share, playlists playlistsvc.Playlists, insights metrics.Insights, libraryService core.Library, userService core.User, maintenance core.Maintenance, pluginManager PluginManager, imgUpload artwork.Uploader, provider external.Provider, quickConnect quickconnect.QuickConnect) *Router {
|
|
r := &Router{ds: ds, share: share, playlists: playlists, insights: insights, libs: libraryService, users: userService, maintenance: maintenance, pluginManager: pluginManager, imgUpload: imgUpload, provider: provider, quickConnect: quickConnect}
|
|
r.Handler = r.routes()
|
|
return r
|
|
}
|
|
|
|
func (api *Router) routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
// Public
|
|
rx(r, "/translation", newTranslationRepository(), false)
|
|
|
|
// Protected
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(server.Authenticator(api.ds))
|
|
r.Use(server.JWTRefresher)
|
|
r.Use(server.UpdateLastAccessMiddleware(api.ds))
|
|
rx(r, "/user", api.users.Repository(), true)
|
|
rx(r, "/song", api.ds.MediaFile(), false)
|
|
rx(r, "/album", api.ds.Album(), false)
|
|
api.addArtistRoute(r)
|
|
rx(r, "/genre", api.ds.Genre(), false)
|
|
rx(r, "/player", api.ds.Player(), true)
|
|
rx(r, "/transcoding", api.ds.Transcoding(), conf.Server.EnableTranscodingConfig)
|
|
api.addRadioRoute(r)
|
|
api.addPodcastRoute(r)
|
|
rx(r, "/tag", api.ds.Tag(), false)
|
|
rx(r, "/scrobble", api.ds.Scrobble(), false)
|
|
if conf.Server.EnableSharing {
|
|
rx(r, "/share", api.share.Repository(), true)
|
|
}
|
|
|
|
api.addPlaylistRoute(r)
|
|
api.addPlaylistTrackRoute(r)
|
|
api.addSongPlaylistsRoute(r)
|
|
api.addQueueRoute(r)
|
|
api.addMissingFilesRoute(r)
|
|
api.addKeepAliveRoute(r)
|
|
api.addInsightsRoute(r)
|
|
api.addQuickConnectRoute(r)
|
|
|
|
r.With(adminOnlyMiddleware).Group(func(r chi.Router) {
|
|
api.addInspectRoute(r)
|
|
api.addConfigRoute(r)
|
|
api.addUserLibraryRoute(r)
|
|
api.addPluginRoute(r)
|
|
api.addMetadataRoute(r)
|
|
rx(r, "/library", api.libs.Repository(), true)
|
|
})
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
func rx[T any](r chi.Router, pathPrefix string, repo rest.Repository[T], persistable bool) {
|
|
r.Route(pathPrefix, func(r chi.Router) {
|
|
r.Get("/", rest.GetAll(repo))
|
|
if persistable {
|
|
r.Post("/", rest.Post(repo))
|
|
}
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.Use(server.URLParamsMiddleware)
|
|
r.Get("/", rest.Get(repo))
|
|
if persistable {
|
|
r.Put("/", rest.Put(repo))
|
|
r.Delete("/", rest.Delete(repo))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func (api *Router) addPlaylistRoute(r chi.Router) {
|
|
repo := api.playlists.Repository()
|
|
|
|
r.Route("/playlist", func(r chi.Router) {
|
|
r.Get("/", rest.GetAll(repo))
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Content-type") == "application/json" {
|
|
rest.Post(repo)(w, r)
|
|
return
|
|
}
|
|
createPlaylistFromM3U(api.playlists)(w, r)
|
|
})
|
|
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.Use(server.URLParamsMiddleware)
|
|
r.Get("/", rest.Get(repo))
|
|
r.Put("/", rest.Put(repo))
|
|
r.Delete("/", rest.Delete(repo))
|
|
r.Post("/image", uploadPlaylistImage(api.playlists))
|
|
r.Delete("/image", deletePlaylistImage(api.playlists))
|
|
})
|
|
})
|
|
}
|
|
|
|
func (api *Router) addPlaylistTrackRoute(r chi.Router) {
|
|
r.Route("/playlist/{playlistId}/tracks", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
getPlaylist(api.playlists)(w, r)
|
|
})
|
|
r.With(server.URLParamsMiddleware).Route("/", func(r chi.Router) {
|
|
r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
|
deleteFromPlaylist(api.playlists)(w, r)
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
addToPlaylist(api.playlists)(w, r)
|
|
})
|
|
})
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.Use(server.URLParamsMiddleware)
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
getPlaylistTrack(api.playlists)(w, r)
|
|
})
|
|
r.Put("/", func(w http.ResponseWriter, r *http.Request) {
|
|
reorderItem(api.playlists)(w, r)
|
|
})
|
|
r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
|
|
deleteFromPlaylist(api.playlists)(w, r)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func (api *Router) addSongPlaylistsRoute(r chi.Router) {
|
|
r.With(server.URLParamsMiddleware).Get("/song/{id}/playlists", func(w http.ResponseWriter, r *http.Request) {
|
|
getSongPlaylists(api.playlists)(w, r)
|
|
})
|
|
}
|
|
|
|
func (api *Router) addQueueRoute(r chi.Router) {
|
|
r.Route("/queue", func(r chi.Router) {
|
|
r.Get("/", getQueue(api.ds))
|
|
r.Post("/", saveQueue(api.ds))
|
|
r.Put("/", updateQueue(api.ds))
|
|
r.Delete("/", clearQueue(api.ds))
|
|
})
|
|
}
|
|
|
|
func (api *Router) addMissingFilesRoute(r chi.Router) {
|
|
r.Route("/missing", func(r chi.Router) {
|
|
rx(r, "/", newMissingRepository(api.ds), false)
|
|
r.Delete("/", deleteMissingFiles(api.maintenance))
|
|
})
|
|
}
|
|
|
|
func writeDeleteManyResponse(w http.ResponseWriter, r *http.Request, ids []string) {
|
|
var payload any
|
|
if len(ids) == 1 {
|
|
payload = struct {
|
|
ID string `json:"id"`
|
|
}{ID: ids[0]}
|
|
} else {
|
|
payload = struct {
|
|
Ids []string `json:"ids"`
|
|
}{Ids: ids}
|
|
}
|
|
if err := rest.RespondWithJSON(w, http.StatusOK, payload); err != nil {
|
|
log.Error(r.Context(), "Error writing response", "ids", ids, err)
|
|
}
|
|
}
|
|
|
|
func (api *Router) addInspectRoute(r chi.Router) {
|
|
if conf.Server.Inspect.Enabled {
|
|
r.Group(func(r chi.Router) {
|
|
if conf.Server.Inspect.MaxRequests > 0 {
|
|
log.Debug("Throttling inspect", "maxRequests", conf.Server.Inspect.MaxRequests,
|
|
"backlogLimit", conf.Server.Inspect.BacklogLimit, "backlogTimeout",
|
|
conf.Server.Inspect.BacklogTimeout)
|
|
r.Use(middleware.ThrottleBacklog(conf.Server.Inspect.MaxRequests, conf.Server.Inspect.BacklogLimit, time.Duration(conf.Server.Inspect.BacklogTimeout)))
|
|
}
|
|
r.Get("/inspect", inspect(api.ds))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (api *Router) addConfigRoute(r chi.Router) {
|
|
if conf.Server.DevUIShowConfig {
|
|
r.Get("/config/*", getConfig)
|
|
}
|
|
}
|
|
|
|
func (api *Router) addKeepAliveRoute(r chi.Router) {
|
|
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"response":"ok", "id":"keepalive"}`))
|
|
})
|
|
}
|
|
|
|
func (api *Router) addInsightsRoute(r chi.Router) {
|
|
r.Get("/insights/*", func(w http.ResponseWriter, r *http.Request) {
|
|
last, success := api.insights.LastRun(r.Context())
|
|
if conf.Server.EnableInsightsCollector {
|
|
_, _ = w.Write([]byte(`{"id":"insights_status", "lastRun":"` + last.Format("2006-01-02 15:04:05") + `", "success":` + strconv.FormatBool(success) + `}`)) //nolint:gosec
|
|
} else {
|
|
_, _ = w.Write([]byte(`{"id":"insights_status", "lastRun":"disabled", "success":false}`))
|
|
}
|
|
})
|
|
}
|
|
|
|
// Middleware to ensure only admin users can access endpoints
|
|
func adminOnlyMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := request.UserFrom(r.Context())
|
|
if !ok || !user.IsAdmin {
|
|
http.Error(w, "Access denied: admin privileges required", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|