Publish current app and server

This commit is contained in:
ponzischeme89
2026-08-02 22:10:19 +12:00
parent a265636139
commit 1ed180c739
203 changed files with 23933 additions and 2788 deletions
+48 -5
View File
@@ -16,6 +16,7 @@ import (
"errors"
"log/slog"
"net/http"
"slices"
"strconv"
"strings"
"sync"
@@ -26,6 +27,7 @@ import (
"github.com/ponzischeme89/memby/server/internal/emby"
"github.com/ponzischeme89/memby/server/internal/foryou"
serverlogging "github.com/ponzischeme89/memby/server/internal/logging"
"github.com/ponzischeme89/memby/server/internal/radarr"
"github.com/ponzischeme89/memby/server/internal/recommend"
"github.com/ponzischeme89/memby/server/internal/sonarr"
"github.com/ponzischeme89/memby/server/internal/store"
@@ -39,10 +41,16 @@ type Server struct {
recommender *recommend.Engine
forYou *foryou.Service
sonarr *sonarr.Client
radarr *radarr.Client
syncer syncerHandle
log *slog.Logger
events *serverlogging.Buffer
sonarrMu sync.Mutex
radarrMu sync.Mutex
// alertMu serialises the read-modify-write of the shared alert list. Its producers
// are events — a webhook, a finished sync, a health probe — none of them paced by
// this server, so two can land at once.
alertMu sync.Mutex
recommendationBuilds recommendationBuilds
maintenance maintenanceState
@@ -58,15 +66,13 @@ type Deps struct {
Recommender *recommend.Engine
ForYou *foryou.Service
Sonarr *sonarr.Client
Radarr *radarr.Client
Syncer syncerHandle
Log *slog.Logger
Events *serverlogging.Buffer
}
func New(cfg config.Config, deps Deps) *Server {
if cfg.MaxClientsPerUser < 1 {
cfg.MaxClientsPerUser = 1
}
return &Server{
cfg: cfg,
emby: deps.Emby,
@@ -75,6 +81,7 @@ func New(cfg config.Config, deps Deps) *Server {
recommender: deps.Recommender,
forYou: deps.ForYou,
sonarr: deps.Sonarr,
radarr: deps.Radarr,
syncer: deps.Syncer,
log: deps.Log,
events: deps.Events,
@@ -87,22 +94,38 @@ func (s *Server) Routes() http.Handler {
v1 := http.NewServeMux()
v1.HandleFunc("POST /v1/auth/login", s.handleLogin)
v1.HandleFunc("GET /v1/auth/policy", s.handleAuthPolicy)
v1.Handle("POST /v1/auth/logout", s.authed(s.handleLogout))
v1.Handle("GET /v1/auth/session", s.authed(s.handleSession))
v1.Handle("GET /v1/auth/devices", s.authed(s.handleDevices))
v1.Handle("PUT /v1/auth/devices/{deviceID}", s.authed(s.handleRenameDevice))
v1.Handle("DELETE /v1/auth/devices/{deviceID}", s.authed(s.handleDeleteDevice))
v1.Handle("GET /v1/home", s.authed(s.handleHome))
v1.Handle("GET /v1/screensaver", s.authed(s.handleScreensaver))
v1.Handle("GET /v1/search", s.authed(s.handleSearch))
v1.Handle("GET /v1/search/history", s.authed(s.handleRecentSearches))
v1.Handle("POST /v1/search/history", s.authed(s.handleSearchHistory))
v1.Handle("GET /v1/requests/lookup", s.authed(s.handleRequestLookup))
v1.Handle("POST /v1/requests", s.authed(s.handleRequest))
v1.Handle("GET /v1/recommendations", s.authed(s.handleRecommendations))
v1.Handle("PUT /v1/recommendations/{id}/action", s.authed(s.handleRecommendationAction))
v1.Handle("DELETE /v1/recommendations/{id}/action", s.authed(s.handleRecommendationAction))
v1.Handle("PUT /v1/recommendations/preferences", s.authed(s.handleRecommendationPreferences))
v1.Handle("GET /v1/recommendations/preferences", s.authed(s.handleRecommendationPreferences))
v1.Handle("GET /v1/for-you", s.authed(s.handleForYou))
v1.Handle("GET /v1/preroll", s.authed(s.handlePreroll))
v1.Handle("GET /v1/update", s.authed(s.handleUpdate))
v1.Handle("GET /v1/my-shows", s.authed(s.handleMyShows))
v1.Handle("POST /v1/my-shows", s.authed(s.handleMyShows))
v1.Handle("DELETE /v1/my-shows/{id}", s.authed(s.handleMyShow))
v1.Handle("GET /v1/notifications", s.authed(s.handleNotifications))
v1.Handle("PUT /v1/notifications", s.authed(s.handleNotifications))
v1.Handle("POST /v1/notifications/{id}/{action}", s.authed(s.handleNotificationAction))
v1.Handle("GET /v1/features", s.authed(s.handleFeatures))
v1.Handle("GET /v1/items/{id}", s.authed(s.handleItem))
v1.Handle("GET /v1/items/{id}/season-finale", s.authed(s.handleSeasonFinale))
v1.Handle("GET /v1/items/{id}/episodes", s.authed(s.handleSeriesEpisodes))
v1.Handle("GET /v1/items/{id}/related", s.authed(s.handleRelated))
v1.Handle("POST /v1/items/{id}/favorite", s.authed(s.handleFavorite))
v1.Handle("POST /v1/items/{id}/played", s.authed(s.handlePlayed))
v1.Handle("GET /v1/items/{id}/playback", s.authed(s.handlePlayback))
@@ -117,11 +140,25 @@ func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", s.handleHealth)
mux.HandleFunc("GET /readyz", s.handleReady)
// Update policy is app-scoped, not user-scoped. Keep it outside authentication and
// maintenance so a fresh install, a signed-out TV, and a retired build can all learn
// whether the server requires an update without touching a viewer session.
mux.HandleFunc("GET /v1/update", s.handleUpdate)
// Exact route outside the maintenance gate: signed-in clients poll this lightweight
// status even while every normal /v1 operation is deliberately unavailable.
mux.Handle("GET /v1/status", s.authed(s.handleServiceStatus))
mux.Handle("/v1/", s.maintenanceGate(v1))
// Radarr pushes here when an import finishes. Outside the gate on purpose: an event
// arriving during maintenance would otherwise be lost rather than delayed.
mux.HandleFunc("POST /hooks/radarr", s.handleRadarrWebhook)
mux.Handle("/admin/", s.adminRoutes())
mux.HandleFunc("GET /{$}", s.handleInstallPage)
mux.HandleFunc("GET /install", s.handleInstallPage)
mux.HandleFunc("GET /install/{$}", s.handleInstallPage)
mux.HandleFunc("POST /install/login", s.handleInstallLogin)
mux.HandleFunc("POST /install/logout", s.handleInstallLogout)
mux.HandleFunc("GET /robots.txt", handleRobots)
mux.HandleFunc("GET /updates/latest.apk", s.handleLatestReleaseDownload)
mux.HandleFunc("GET /updates/{filename}", s.handleReleaseDownload)
return s.withLogging(mux)
@@ -165,6 +202,7 @@ func (s *Server) captureClientIdentity(r *http.Request, sess store.Session) stor
if changed {
if err := s.store.UpdateSessionClientIdentity(
r.Context(), sess.TokenHash, sess.ClientVersion, sess.ClientProtocol,
sess.ClientCapabilities,
); err != nil {
s.log.Warn("client identity update failed", "error", err)
} else {
@@ -177,6 +215,7 @@ func (s *Server) captureClientIdentity(r *http.Request, sess store.Session) stor
func mergeClientIdentity(r *http.Request, sess *store.Session) bool {
version := clientVersion(r)
protocol := clientProtocol(r)
capabilities := clientCapabilities(r)
changed := false
if version != "" && version != sess.ClientVersion {
sess.ClientVersion = version
@@ -186,6 +225,10 @@ func mergeClientIdentity(r *http.Request, sess *store.Session) bool {
sess.ClientProtocol = protocol
changed = true
}
if len(capabilities) > 0 && !slices.Equal(capabilities, sess.ClientCapabilities) {
sess.ClientCapabilities = capabilities
changed = true
}
if version == "" && sess.ClientVersion != "" {
r.Header.Set("X-Memby-Version", sess.ClientVersion)
}