Fix Sonarr TV request policy and bump gateway to 0.1.40

This commit is contained in:
ponzischeme89
2026-08-14 10:30:25 +12:00
parent 5e2ed3d12e
commit 19f08e6293
11 changed files with 366 additions and 29 deletions
+88 -1
View File
@@ -11,6 +11,7 @@ import (
"sync"
"time"
"github.com/ponzischeme89/memby/server/internal/adminevents"
"github.com/ponzischeme89/memby/server/internal/radarr"
"github.com/ponzischeme89/memby/server/internal/sonarr"
"github.com/ponzischeme89/memby/server/internal/store"
@@ -302,13 +303,22 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
return
}
req.Title = show.Title
added, err := s.sonarr.AddRequested(r.Context(), show)
requestOptions, rootFolder, profileName, err := s.sonarrRequestOptions(r.Context())
if err != nil {
s.logSonarrRequest(r.Context(), sess, req, show, 0, "failed", "", 0, "", false, err)
s.publishSonarrRequestConfigurationProblem(r.Context(), err)
writeError(w, http.StatusServiceUnavailable, "TV requests are unavailable: "+err.Error())
return
}
added, err := s.sonarr.AddRequested(r.Context(), show, rootFolder, requestOptions)
if err != nil {
s.logSonarrRequest(r.Context(), sess, req, show, 0, "failed", profileName, requestOptions.QualityProfileID, rootFolder, requestOptions.SearchImmediately, err)
s.logMediaRequest(r.Context(), req, "failed", err)
s.writeRequestUpstreamError(r.Context(), w, err, "could not request that series")
return
}
req.Title = added.Title
s.logSonarrRequest(r.Context(), sess, req, added, added.ID, "successful", profileName, requestOptions.QualityProfileID, rootFolder, requestOptions.SearchImmediately, nil)
s.recordMediaRequest(r.Context(), sess, req, added.Year,
sonarrCoverURL(added.Images, "poster"))
s.logMediaRequest(r.Context(), req, "successful", nil)
@@ -378,3 +388,80 @@ func (s *Server) writeRequestUpstreamError(
s.loggerFor(ctx).Error(message, "error", err)
writeError(w, http.StatusBadGateway, message)
}
// sonarrRequestOptions validates every component before a POST can reach Sonarr. A missing
// configured profile is an error, not permission to fall back to Sonarr's "Any" profile.
func (s *Server) sonarrRequestOptions(ctx context.Context) (sonarr.RequestOptions, string, string, error) {
if s.sonarr == nil {
return sonarr.RequestOptions{}, "", "", errors.New("Sonarr integration is unavailable")
}
policy, err := s.store.SonarrRequestPolicy(ctx)
if err != nil {
return sonarr.RequestOptions{}, "", "", errors.New("could not read the Sonarr request policy")
}
roots, err := s.sonarr.RootFolders(ctx)
if err != nil {
return sonarr.RequestOptions{}, "", "", errors.New("could not validate Sonarr root folders")
}
if len(roots) == 0 || strings.TrimSpace(roots[0].Path) == "" {
return sonarr.RequestOptions{}, "", "", errors.New("Sonarr has no valid root folder")
}
profiles, err := s.sonarr.QualityProfiles(ctx)
if err != nil {
return sonarr.RequestOptions{}, "", "", errors.New("could not validate Sonarr quality profiles")
}
profileID := policy.QualityProfileID
if profileID == 0 {
for _, profile := range profiles {
if is720pProfile(profile.Name) {
profileID = profile.ID
break
}
}
if profileID == 0 {
return sonarr.RequestOptions{}, "", "", errors.New("no request quality profile is configured and Sonarr has no 720p profile")
}
// First-run policy is the safe 720p recommendation. Persist its id immediately so
// later profile renames or deletion are caught as configuration errors instead of
// becoming a fresh name-based selection.
policy.QualityProfileID = profileID
if err := s.store.SetSonarrRequestPolicy(ctx, policy); err != nil {
return sonarr.RequestOptions{}, "", "", errors.New("could not save the default Sonarr request quality profile")
}
}
for _, profile := range profiles {
if profile.ID == profileID {
return sonarr.RequestOptions{QualityProfileID: profile.ID, SearchImmediately: policy.SearchImmediately}, roots[0].Path, profile.Name, nil
}
}
return sonarr.RequestOptions{}, "", "", errors.New("the configured Sonarr request quality profile no longer exists")
}
func (s *Server) logSonarrRequest(
ctx context.Context, sess store.Session, req requestPayload, series sonarr.Series, seriesID int,
outcome, profileName string, profileID int, rootFolder string, searchImmediately bool, err error,
) {
fields := []any{
"user", clientLogValue(sess.Username), "user_id", sess.EmbyUserID,
"title", clientLogValue(series.Title), "tvdb_id", series.TVDBID,
"sonarr_series_id", seriesID, "quality_profile", profileName,
"quality_profile_id", profileID, "monitoring_strategy", "all",
"root_folder", rootFolder, "search_immediately", searchImmediately,
"outcome", outcome,
}
if err != nil {
fields = append(fields, "sonarr_result", err.Error())
s.loggerFor(ctx).Warn("Sonarr TV request", fields...)
return
}
fields = append(fields, "sonarr_result", "created")
s.loggerFor(ctx).Info("Sonarr TV request", fields...)
}
func (s *Server) publishSonarrRequestConfigurationProblem(ctx context.Context, err error) {
s.publishAdmin(ctx, adminevents.Event{
Type: "sonarr.request_configuration", Severity: adminevents.SeverityError,
Title: "Sonarr TV requests need attention", Summary: err.Error(), Actor: "memby-server",
Link: "/admin/integrations", Metadata: adminevents.Meta(map[string]any{"error": err.Error()}),
})
}