* docs: design spec for multi-instance Sonarr/Radarr request routing Seerr-style multi-instance arr management inside Silo's request system: many instances per kind, HD/4K default routing, entitlement-driven dual-quality fan-out, per-instance anime overrides (keyword 210024), and a one-to-many media_request_targets model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for multi-instance arr request routing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): migration for multi-instance arr routing Adds migration 169 to convert request_integrations from a one-row-per-kind table keyed on `kind` to a multi-instance table keyed on `id`, with HD/4K defaults, anime overrides, and a new one-to-many media_request_targets table for per-quality fulfillment tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(requests): instance, target, and dual-quality types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(requests): id-based integration CRUD Replace upsert-by-kind (UpsertIntegration/UpsertIntegrations) with GetIntegration, CreateIntegration, UpdateIntegration, DeleteIntegration, and ClearDefault. Rewrites scanIntegration and integrationColumns to cover all new multi-instance columns (id, name, is_4k, is_default, is_default_4k, anime_* fields). Updates the Store interface accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(requests): target persistence and aggregate status * feat(tmdb): expose keyword ids and original language on detail * feat(requests): Seerr-exact anime detection (keyword 210024) * feat(requests): quality/anime routing engine Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): force_dual_quality setting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(requests): multi-target fulfillment, reconcile, retry, and instance CRUD Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): request integration CRUD endpoints, targets in responses, entitlement wiring Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): multi-instance request integration types and CRUD hooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): multi-instance arr manager, dual-quality toggle, per-target queue Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): UX review fixes for arr manager (delete confirm, switch hints, test feedback, dirty + target status) * fix(requests): address code-review findings (test-connection by id, HD-only default ceiling, retryable partial failure, idempotent submit, transactional defaults, presence/target reconcile, auto-approve gate) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review (anime override fallback, non-null slices, save gate, a11y, DeleteTarget not-found) - routing: anime fields only override standard root/profile/tags when set, so enabling anime with blank fields reuses standard values instead of clearing them into an invalid submission - api: normalize nil Tags/AnimeTags to [] so they serialize as arrays not null - web: require an API key before saving a NEW instance; add aria-expanded/ aria-controls to the anime-overrides disclosure toggle - repo: DeleteTarget returns ErrNotFound when no row was deleted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): address PR review findings --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package requests
|
|
|
|
import "github.com/Silo-Server/silo-server/internal/access"
|
|
|
|
// plannedTarget is a routing decision: which instance, at which quality, with
|
|
// which profile/folder/tags resolved (standard vs anime).
|
|
type plannedTarget struct {
|
|
Instance Integration
|
|
Quality Quality
|
|
IsAnime bool
|
|
}
|
|
|
|
func integrationKindForMediaType(mediaType MediaType) string {
|
|
if mediaType == MediaTypeSeries {
|
|
return "sonarr"
|
|
}
|
|
return "radarr"
|
|
}
|
|
|
|
// routeTargets decides the fulfillment targets for an approved request.
|
|
// 1080p is always desired; 2160p is added when the requester's ceiling allows
|
|
// 4K OR force-dual is on. A quality is emitted only if its default instance
|
|
// exists for the kind.
|
|
func routeTargets(req Request, ceiling string, settings Settings, instances []Integration) []plannedTarget {
|
|
kind := integrationKindForMediaType(req.MediaType)
|
|
|
|
wants4K := settings.ForceDualQuality || access.CompareQuality(ceiling, access.PlaybackQuality4K) >= 0
|
|
|
|
var hd, uhd *Integration
|
|
for i := range instances {
|
|
in := instances[i]
|
|
if in.Kind != kind || !in.Enabled {
|
|
continue
|
|
}
|
|
if in.IsDefault && hd == nil {
|
|
hd = &instances[i]
|
|
}
|
|
if in.IsDefault4K && uhd == nil {
|
|
uhd = &instances[i]
|
|
}
|
|
}
|
|
|
|
var out []plannedTarget
|
|
if hd != nil {
|
|
out = append(out, plannedTarget{Instance: *hd, Quality: Quality1080p, IsAnime: req.IsAnime && hd.AnimeEnabled})
|
|
}
|
|
if wants4K && uhd != nil {
|
|
out = append(out, plannedTarget{Instance: *uhd, Quality: Quality2160p, IsAnime: req.IsAnime && uhd.AnimeEnabled})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// resolveInstance returns a copy of the instance with root folder / quality
|
|
// profile / tags (and Sonarr series_type) set for standard vs anime fulfillment.
|
|
func resolveInstance(pt plannedTarget) Integration {
|
|
in := pt.Instance
|
|
if in.Options == nil {
|
|
in.Options = map[string]any{}
|
|
} else {
|
|
clone := make(map[string]any, len(in.Options))
|
|
for k, v := range in.Options {
|
|
clone[k] = v
|
|
}
|
|
in.Options = clone
|
|
}
|
|
if pt.IsAnime {
|
|
// Anime fields are overrides: only replace the standard value when the
|
|
// anime counterpart is set, so an admin can enable anime detection while
|
|
// reusing the standard root folder / quality profile / tags for any field
|
|
// they leave blank (rather than clearing them into an invalid submission).
|
|
if in.AnimeRootFolder != "" {
|
|
in.RootFolder = in.AnimeRootFolder
|
|
}
|
|
if in.AnimeQualityProfileID != nil {
|
|
in.QualityProfileID = in.AnimeQualityProfileID
|
|
}
|
|
if len(in.AnimeTags) > 0 {
|
|
in.Tags = in.AnimeTags
|
|
}
|
|
if in.Kind == "sonarr" {
|
|
in.Options["series_type"] = "anime"
|
|
}
|
|
}
|
|
return in
|
|
}
|