Server changes/Sonarr

This commit is contained in:
ponzischeme89
2026-07-27 21:06:51 +12:00
parent 62f6345a40
commit 8d6cf2f5a1
42 changed files with 2388 additions and 284 deletions
+41 -3
View File
@@ -18,12 +18,14 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/ponzischeme89/memby/server/internal/cache"
"github.com/ponzischeme89/memby/server/internal/config"
"github.com/ponzischeme89/memby/server/internal/emby"
"github.com/ponzischeme89/memby/server/internal/recommend"
"github.com/ponzischeme89/memby/server/internal/sonarr"
"github.com/ponzischeme89/memby/server/internal/store"
)
@@ -33,8 +35,10 @@ type Server struct {
store *store.Store
cache *cache.Cache
recommender *recommend.Engine
sonarr *sonarr.Client
syncer syncerHandle
log *slog.Logger
sonarrMu sync.Mutex
recommendationBuilds recommendationBuilds
maintenance maintenanceState
@@ -48,17 +52,22 @@ type Deps struct {
Store *store.Store
Cache *cache.Cache
Recommender *recommend.Engine
Sonarr *sonarr.Client
Syncer syncerHandle
Log *slog.Logger
}
func New(cfg config.Config, deps Deps) *Server {
if cfg.MaxClientsPerUser < 1 {
cfg.MaxClientsPerUser = 1
}
return &Server{
cfg: cfg,
emby: deps.Emby,
store: deps.Store,
cache: deps.Cache,
recommender: deps.Recommender,
sonarr: deps.Sonarr,
syncer: deps.Syncer,
log: deps.Log,
}
@@ -70,6 +79,7 @@ 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))
@@ -93,8 +103,12 @@ func (s *Server) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", s.handleHealth)
mux.HandleFunc("GET /readyz", s.handleReady)
// 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))
mux.Handle("/admin/", s.adminRoutes())
mux.HandleFunc("GET /updates/{filename}", s.handleReleaseDownload)
return s.withLogging(mux)
}
@@ -134,15 +148,33 @@ func (s *Server) withLogging(next http.Handler) http.Handler {
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
// Path only: query strings can carry image tokens.
s.log.Info("request",
level := requestLogLevel(r.URL.Path, rec.status)
s.log.Log(r.Context(), level, "HTTP request",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"ms", time.Since(start).Milliseconds(),
"duration", time.Since(start).Round(time.Millisecond),
)
})
}
// Successful high-frequency probes and artwork fetches stay available at DEBUG without
// overwhelming the normal Docker log. Failures are always promoted so they remain
// visible regardless of path.
func requestLogLevel(path string, status int) slog.Level {
switch {
case status >= http.StatusInternalServerError:
return slog.LevelError
case status >= http.StatusBadRequest:
return slog.LevelWarn
case path == "/healthz", path == "/readyz", path == "/v1/status",
strings.HasPrefix(path, "/v1/images/"):
return slog.LevelDebug
default:
return slog.LevelInfo
}
}
type statusRecorder struct {
http.ResponseWriter
status int
@@ -184,6 +216,7 @@ type cachedSession struct {
Username string `json:"n"`
ServerID string `json:"s"`
DeviceID string `json:"d"`
DeviceName string `json:"dn,omitempty"`
}
// sessionFor resolves a token, using Redis to keep the hot path off Postgres.
@@ -201,6 +234,7 @@ func (s *Server) sessionFor(ctx context.Context, token string) (store.Session, e
Username: cs.Username,
ServerID: cs.ServerID,
DeviceID: cs.DeviceID,
DeviceName: cs.DeviceName,
}, nil
}
}
@@ -220,6 +254,7 @@ func (s *Server) sessionFor(ctx context.Context, token string) (store.Session, e
Username: sess.Username,
ServerID: sess.ServerID,
DeviceID: sess.DeviceID,
DeviceName: sess.DeviceName,
}); err == nil {
_ = s.cache.Set(ctx, key, raw, s.cfg.SessionTTL)
}
@@ -231,7 +266,10 @@ func (s *Server) sessionFor(ctx context.Context, token string) (store.Session, e
}
func credentials(sess store.Session) emby.Credentials {
return emby.Credentials{UserID: sess.EmbyUserID, Token: sess.EmbyToken, DeviceID: sess.DeviceID}
return emby.Credentials{
UserID: sess.EmbyUserID, Token: sess.EmbyToken,
DeviceID: sess.DeviceID, DeviceName: sess.DeviceName,
}
}
// --- responses --------------------------------------------------------------