Merge upstream/master into feat-podcast-ux-improvements/5420

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...)
This commit is contained in:
Jiho Andrew Lee 2026-09-30 15:23:00 +09:00
commit 19385dc28b
508 changed files with 13229 additions and 8764 deletions

View file

@ -2,8 +2,6 @@ package nativeapi
import (
"context"
"encoding/json"
"html"
"net/http"
"strconv"
"time"
@ -62,26 +60,26 @@ func (api *Router) routes() http.Handler {
r := chi.NewRouter()
// Public
api.RX(r, "/translation", newTranslationRepository, false)
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))
api.RX(r, "/user", api.users.NewRepository, true)
api.R(r, "/song", model.MediaFile{}, false)
api.R(r, "/album", model.Album{}, false)
rx(r, "/user", api.users.Repository(), true)
rx(r, "/song", api.ds.MediaFile(), false)
rx(r, "/album", api.ds.Album(), false)
api.addArtistRoute(r)
api.R(r, "/genre", model.Genre{}, false)
api.R(r, "/player", model.Player{}, true)
api.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
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)
api.R(r, "/tag", model.Tag{}, false)
api.R(r, "/scrobble", model.Scrobble{}, false)
rx(r, "/tag", api.ds.Tag(), false)
rx(r, "/scrobble", api.ds.Scrobble(), false)
if conf.Server.EnableSharing {
api.RX(r, "/share", api.share.NewRepository, true)
rx(r, "/share", api.share.Repository(), true)
}
api.addPlaylistRoute(r)
@ -99,47 +97,38 @@ func (api *Router) routes() http.Handler {
api.addUserLibraryRoute(r)
api.addPluginRoute(r)
api.addMetadataRoute(r)
api.RX(r, "/library", api.libs.NewRepository, true)
rx(r, "/library", api.libs.Repository(), true)
})
})
return r
}
func (api *Router) R(r chi.Router, pathPrefix string, model any, persistable bool) {
constructor := func(ctx context.Context) rest.Repository {
return api.ds.Resource(ctx, model)
}
api.RX(r, pathPrefix, constructor, persistable)
}
func (api *Router) RX(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) {
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(constructor))
r.Get("/", rest.GetAll(repo))
if persistable {
r.Post("/", rest.Post(constructor))
r.Post("/", rest.Post(repo))
}
r.Route("/{id}", func(r chi.Router) {
r.Use(server.URLParamsMiddleware)
r.Get("/", rest.Get(constructor))
r.Get("/", rest.Get(repo))
if persistable {
r.Put("/", rest.Put(constructor))
r.Delete("/", rest.Delete(constructor))
r.Put("/", rest.Put(repo))
r.Delete("/", rest.Delete(repo))
}
})
})
}
func (api *Router) addPlaylistRoute(r chi.Router) {
constructor := func(ctx context.Context) rest.Repository {
return api.playlists.NewRepository(ctx)
}
repo := api.playlists.Repository()
r.Route("/playlist", func(r chi.Router) {
r.Get("/", rest.GetAll(constructor))
r.Get("/", rest.GetAll(repo))
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") == "application/json" {
rest.Post(constructor)(w, r)
rest.Post(repo)(w, r)
return
}
createPlaylistFromM3U(api.playlists)(w, r)
@ -147,9 +136,9 @@ func (api *Router) addPlaylistRoute(r chi.Router) {
r.Route("/{id}", func(r chi.Router) {
r.Use(server.URLParamsMiddleware)
r.Get("/", rest.Get(constructor))
r.Put("/", rest.Put(constructor))
r.Delete("/", rest.Delete(constructor))
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))
})
@ -201,28 +190,24 @@ func (api *Router) addQueueRoute(r chi.Router) {
func (api *Router) addMissingFilesRoute(r chi.Router) {
r.Route("/missing", func(r chi.Router) {
api.RX(r, "/", newMissingRepository(api.ds), false)
rx(r, "/", newMissingRepository(api.ds), false)
r.Delete("/", deleteMissingFiles(api.maintenance))
})
}
func writeDeleteManyResponse(w http.ResponseWriter, r *http.Request, ids []string) {
var resp []byte
var err error
var payload any
if len(ids) == 1 {
resp = []byte(`{"id":"` + html.EscapeString(ids[0]) + `"}`)
payload = struct {
ID string `json:"id"`
}{ID: ids[0]}
} else {
resp, err = json.Marshal(&struct {
payload = struct {
Ids []string `json:"ids"`
}{Ids: ids})
if err != nil {
log.Error(r.Context(), "Error marshaling response", "ids", ids, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}{Ids: ids}
}
_, err = w.Write(resp) //nolint:gosec
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
if err := rest.RespondWithJSON(w, http.StatusOK, payload); err != nil {
log.Error(r.Context(), "Error writing response", "ids", ids, err)
}
}