Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a039ce0276 | ||
|
|
7cf1ece63c | ||
|
|
f763d7fdb3 | ||
|
|
5e4f8d5d2b |
@@ -95,7 +95,7 @@ The [input catalogue](03-input-schemas.md) defines every parameter, required fie
|
||||
| `welcome_email` | 8 | | | |
|
||||
|
||||
Remaining numeric enums are unresolved gaps and stay numeric pending verification: `VoteType [0,1]` (labels unverified), `RequestSource`, `RequestLimitType [0,1,2]`, and the legacy `orderType`/`statusType`/`availabilityType` filter ints. Search response IDs must retain their source/provider namespace, including multi-search TV TMDB IDs.
|
||||
5. A TV parent is a show record; a TV child is an individual request under it. `read_requests.list(media=tv)` uses v2 child pages. Parent detail and children enumeration use v1. A compact projection of v2 children is preferable to secretly swapping in `tvlite` parent results. Return `parent_request_id` alongside child IDs when present.
|
||||
5. A TV parent is a show record; a TV child is an individual request under it. `read_requests.list(media=tv)` uses v2 child pages. Parent detail and children enumeration use v1. A compact projection of v2 children is preferable to secretly swapping in `tvlite` parent results (the v2 list contract is preserved; upstream 500 errors surface natively). Return `parent_request_id` alongside child IDs when present. `read_requests.search` for TV uses v1 `Request/tv/search/{term}` but falls back to a bounded v1 parent scan if it fails, compensating for an upstream LINQ bug.
|
||||
6. Parameter names such as `currentPosition`, `position`, `skip`, `count`, `take` and `amountToLoad` are mapped exactly per ledger. Offsets are zero-based by this MCP contract; the adapter must verify ambiguous upstream paging behaviour. Requests use count then position; issue summary uses position then take; issue list uses take then skip. Do not reverse these pairs.
|
||||
7. Array responses without server pagination are sliced locally only within a bounded fetched response. Mark pagination as local and total unknown unless the complete collection was obtained. A result-size cap is a truncation warning, not a fabricated server total or a promise that the next page exists.
|
||||
8. `format: date-time` and cross-field comparisons must be enforced by the server, not assumed from a client's validator. Stats requires `from <= to` when both are supplied. Strings must contain non-whitespace text where used as queries/comments. Reject duplicate season numbers and duplicate episode numbers; impose a maximum of 2,000 selected episodes per call in addition to per-array limits.
|
||||
@@ -111,8 +111,7 @@ The [input catalogue](03-input-schemas.md) defines every parameter, required fie
|
||||
|
||||
`advanced_movie` POSTs the exact DiscoverModel property names: `type:"movie"`, `release_year→releaseYear`, `genre_ids→genreIds`, `keyword_ids→keywordIds`, `watch_provider_ids→watchProviders`, and decade unchanged. Ombi 4.53.x accepts but ignores `companies`, so `company_ids` is not advertised and legacy calls fail explicitly. Do not invent a `query` requirement for discovery. Release year and decade must agree if both are supplied.
|
||||
|
||||
`read_media.details` chooses v2 movie TMDB/IMDb, TV TVDB/TMDB, artist, or artist/album routes. TV `tmdb` uses v2 `Search/tv/moviedb/{id}`; TV `tvdb` uses the legacy v1 `Search/tv/info/{tvdbId}` route — the v2 `Search/tv/{tvdbId}` route is a TMDB-keyed alias of moviedb despite its parameter name and must never serve a TVDB lookup. IMDb path placeholder spelling differs from its parameter declaration; substitute the actual path placeholder. `by_request` chooses movie/request, tv/request or artist/request; the TV namespace of this particular upstream route requires adapter verification and must not be guessed from list results. `movie_localized` uses POST v1 movie/info with `{theMovieDbId, languageCode}`. `ratings` uses title and year, not a numeric media ID. `streaming` uses TMDB even for TV. Cast/crew are projections of detailed metadata where present, not invented standalone endpoints.
|
||||
|
||||
`read_media.details` chooses v2 movie TMDB/IMDb, TV TVDB/TMDB, artist, or artist/album routes. TV `tmdb` uses v2 `Search/tv/moviedb/{id}`; TV `tvdb` uses the legacy v1 `Search/tv/info/{tvdbId}` route (the v2 `Search/tv/{tvdbId}` route is a TMDB-keyed alias and must never serve a TVDB lookup). The legacy v1 TVDB info route does not reliably carry request state; for TVDB, the adapter performs a bounded scan of v1 `Request/tv` (matching by `tvDbId` or `imdbId`) to overlay `requested`, `request_targets` (using the real parent request ID), and per-episode availability flags. IMDb path placeholder spelling differs from its parameter declaration. `by_request` chooses movie/request, tv/request or artist/request. `movie_localized` uses POST v1 movie/info. `ratings` uses title and year. `streaming` uses TMDB even for TV.
|
||||
TV identifier labels follow the origin route, not the upstream field name. The v1 TVMaze-backed routes (`Search/tv/{term}`, `Search/tv/info/{tvdbId}`) place the TVDB id in the misleadingly-named `theMovieDbId` field and the TVMaze id in `seriesId`; emit `tvdb` and `tvmaze` there. The v2 engine routes (browse, `moviedb`, `by_request`, multi-search) are TMDB-keyed; emit `tmdb` and do not treat `seriesId` as TVMaze. When `theMovieDbId` is absent, `id` is labelled with that origin namespace so browse and collection members still carry a usable identifier. Multi-search `mediaType` is case-insensitive (`Artist`→artist). The v1 `RecentlyAdded` TV model's id namespace is unverified — keep `tmdb` pending live confirmation.
|
||||
|
||||
`read_reference.keywords` passes query `searchTerm`; keyword detail uses its own keyword ID. Watch-provider catalogue search also has optional `searchTerm`; it is distinct from streaming availability for a particular title. Other branches have no request body. Reference values are good optional cached resources, but remain available through tools for tool-only clients.
|
||||
|
||||
@@ -298,3 +298,9 @@ The gated live suite is committed and ready; run it with a sourced `.env`. It ad
|
||||
- Input/output schema contracts in `docs/schema/` unchanged.
|
||||
- `go test ./...` output is identical to before the phase (package fully behind the tag).
|
||||
- No instance URL in committed files; `.env` guidance points to the gitignored file only.
|
||||
|
||||
## M5 Findings — Request state and list routes (#7, #10)
|
||||
|
||||
- `#7`: `GET /api/v1/Search/tv/info/{tvdbId}` (TVMaze-backed info route) upstream does not set request state flags accurately, returning `requested: false` regardless of truth. The adapter patches this by performing a bounded `GET /api/v1/Request/tv` parent scan and overlaying per-episode availability and request states.
|
||||
- `#10`: `GET /api/v2/Requests/{movie,tv,album}/...` (v2 lists) encounter a per-row `NullReferenceException` on non-empty results (observed on Ombi 4.53.10). The adapter surfaces this as `UPSTREAM_REJECTED` rather than silently replacing it with v1 `tvlite` payloads, preserving the expected v2 child-page contract.
|
||||
- `#10` search fallback: `GET /api/v1/Request/tv/search/{term}` on 4.53.10 suffers from a LINQ translation bug (upstream Ombi-app/Ombi#5420, fixed by #5421). The adapter gracefully falls back to a bounded v1 parent scan filtering locally on the term if the primary search route fails.
|
||||
|
||||
@@ -1472,3 +1472,117 @@ func TestAPIKeyMode(t *testing.T) {
|
||||
t.Errorf("ApiKey header never reached upstream")
|
||||
}
|
||||
}
|
||||
|
||||
// --- M5 request state and list routes ---
|
||||
|
||||
func TestM5TVDBDetailsRequestStateOverlay(t *testing.T) {
|
||||
mock := newMockOmbi(t, "jwt")
|
||||
c := spawnServer(t, mock.env())
|
||||
c.handshake(t)
|
||||
|
||||
out := c.callTool(t, "read_media", map[string]any{
|
||||
"action": "details",
|
||||
"target": map[string]any{"media": "tv", "provider": "tvdb", "id": 81189},
|
||||
})
|
||||
data := requireOK(t, out)
|
||||
|
||||
var m struct {
|
||||
Items []struct {
|
||||
Requested *bool `json:"requested"`
|
||||
RequestTargets []struct {
|
||||
Kind string `json:"kind"`
|
||||
ID int `json:"id"`
|
||||
} `json:"request_targets"`
|
||||
Seasons []struct {
|
||||
SeasonNumber int `json:"season_number"`
|
||||
Episodes []struct {
|
||||
EpisodeNumber int `json:"episode_number"`
|
||||
Requested *bool `json:"requested"`
|
||||
Available *bool `json:"available"`
|
||||
} `json:"episodes"`
|
||||
} `json:"seasons"`
|
||||
} `json:"items"`
|
||||
}
|
||||
json.Unmarshal(data, &m)
|
||||
if len(m.Items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(m.Items))
|
||||
}
|
||||
it := m.Items[0]
|
||||
|
||||
if it.Requested == nil || !*it.Requested {
|
||||
t.Errorf("expected Requested=true from overlay")
|
||||
}
|
||||
if len(it.RequestTargets) != 1 || it.RequestTargets[0].Kind != "tv_parent" || it.RequestTargets[0].ID != 42 {
|
||||
t.Errorf("expected RequestTargets=[{tv_parent 42}], got %v", it.RequestTargets)
|
||||
}
|
||||
|
||||
var ep1Requested, ep1Available bool
|
||||
for _, s := range it.Seasons {
|
||||
if s.SeasonNumber == 1 {
|
||||
for _, e := range s.Episodes {
|
||||
if e.EpisodeNumber == 1 {
|
||||
if e.Requested != nil { ep1Requested = *e.Requested }
|
||||
if e.Available != nil { ep1Available = *e.Available }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ep1Requested {
|
||||
t.Errorf("expected season 1 episode 1 Requested=true")
|
||||
}
|
||||
if !ep1Available {
|
||||
t.Errorf("expected season 1 episode 1 Available=true")
|
||||
}
|
||||
|
||||
if calls := mock.countCalls("GET", "/api/v1/Search/tv/info/"); calls != 1 {
|
||||
t.Errorf("expected 1 call to v1 info route, got %d", calls)
|
||||
}
|
||||
if calls := mock.countCalls("GET", "/api/v1/Request/tv/"); calls < 1 {
|
||||
t.Errorf("expected >=1 call to v1 tv parent list route, got %d", calls)
|
||||
}
|
||||
if calls := mock.countCalls("GET", "/api/v2/Search/tv/moviedb/"); calls != 0 {
|
||||
t.Errorf("expected 0 calls to v2 TMDB alias route, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestM5TVSearchFallback(t *testing.T) {
|
||||
mock := newMockOmbi(t, "jwt")
|
||||
c := spawnServer(t, mock.env())
|
||||
c.handshake(t)
|
||||
|
||||
out := c.callTool(t, "read_requests", map[string]any{
|
||||
"action": "search",
|
||||
"media": "tv",
|
||||
"query": "trigger-500",
|
||||
})
|
||||
data := requireOK(t, out)
|
||||
var page struct {
|
||||
Items []struct {
|
||||
Title string `json:"title"`
|
||||
} `json:"items"`
|
||||
}
|
||||
json.Unmarshal(data, &page)
|
||||
if len(page.Items) != 1 {
|
||||
t.Fatalf("expected 1 item from fallback, got %d", len(page.Items))
|
||||
}
|
||||
if page.Items[0].Title != "trigger-500 and something" {
|
||||
t.Errorf("expected item title 'trigger-500 and something', got %q", page.Items[0].Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestM5V2List500ErrorSurfaced(t *testing.T) {
|
||||
mock := newMockOmbi(t, "jwt")
|
||||
c := spawnServer(t, mock.env())
|
||||
c.handshake(t)
|
||||
|
||||
out := c.callTool(t, "read_media", map[string]any{
|
||||
"action": "details",
|
||||
"target": map[string]any{"media": "movie", "provider": "tmdb", "id": 500001},
|
||||
})
|
||||
if !out.IsError {
|
||||
t.Fatalf("expected error on 500")
|
||||
}
|
||||
if !strings.Contains(out.Text, "System.NullReferenceException") {
|
||||
t.Errorf("expected error message to contain sanitized detail, got %q", out.Text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func newMockOmbi(t *testing.T, mode string) *mockOmbi {
|
||||
mux.HandleFunc("GET /api/v1/Status", m.wrap(m.fixed(`200`)))
|
||||
mux.HandleFunc("GET /api/v1/Status/info", m.wrap(m.fixed(`"mock-status-info"`)))
|
||||
mux.HandleFunc("GET /api/v1/Settings/about", m.wrap(m.json(mockAbout())))
|
||||
mux.HandleFunc("GET /api/v1/Request/tv/{count}/{pos}/{o}/{s}/{a}", m.wrap(m.tvParentList))
|
||||
mux.HandleFunc("GET /api/v2/Requests/movie/{amt}/{pos}/requestDate/{order}", m.wrap(m.movieList))
|
||||
mux.HandleFunc("GET /api/v2/Requests/tv/{amt}/{pos}/requestDate/{order}", m.wrap(m.tvList))
|
||||
mux.HandleFunc("GET /api/v2/Search/movie/{id}", m.wrap(m.movieDetails))
|
||||
@@ -358,7 +359,7 @@ func mockTVDetail() map[string]any {
|
||||
map[string]any{
|
||||
"seasonNumber": 1,
|
||||
"episodes": []any{
|
||||
map[string]any{"episodeNumber": 1, "title": "Pilot", "available": true, "requested": true},
|
||||
map[string]any{"episodeNumber": 1, "title": "Pilot", "available": true},
|
||||
map[string]any{"episodeNumber": 2, "title": "Cat's in the Bag...", "available": true},
|
||||
map[string]any{"episodeNumber": 3, "title": "...And the Bag's in the River"},
|
||||
map[string]any{"episodeNumber": 4, "title": "Cancer Man"},
|
||||
@@ -532,6 +533,35 @@ func (m *mockOmbi) movieList(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *mockOmbi) tvParentList(w http.ResponseWriter, r *http.Request) {
|
||||
pos := r.PathValue("pos")
|
||||
if pos != "0" {
|
||||
m.json([]any{})(w, r)
|
||||
return
|
||||
}
|
||||
m.json([]any{
|
||||
map[string]any{
|
||||
"id": 42,
|
||||
"tvDbId": 81189,
|
||||
"title": "trigger-500 and something", // For search fallback test
|
||||
"childRequests": []any{
|
||||
map[string]any{
|
||||
"seasonRequests": []any{
|
||||
map[string]any{
|
||||
"seasonNumber": 1,
|
||||
"episodes": []any{
|
||||
map[string]any{"episodeNumber": 1, "requested": true},
|
||||
map[string]any{"episodeNumber": 2, "requested": true},
|
||||
map[string]any{"episodeNumber": 3, "requested": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})(w, r)
|
||||
}
|
||||
|
||||
func (m *mockOmbi) tvList(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
@@ -552,6 +582,10 @@ func (m *mockOmbi) movieDetails(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("<html><body><h1>500 Internal Server Error</h1>" +
|
||||
"System.NullReferenceException at Ombi.Core.Engine.MovieRequestEngine" +
|
||||
"Authorization: Bearer should-never-appear</body></html>"))
|
||||
case "500001":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"error": "Object reference not set to an instance of an object. System.NullReferenceException"}`))
|
||||
default:
|
||||
m.jsonErr(w, http.StatusNotFound, "Movie not found")
|
||||
}
|
||||
@@ -595,6 +629,13 @@ func (m *mockOmbi) tvInfoTVDB(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (m *mockOmbi) tvSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if r.PathValue("term") == "trigger-500" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("<html><body><h1>500 Internal Server Error</h1>" +
|
||||
"System.NullReferenceException at Ombi.Core.Engine.TvRequestEngine</body></html>"))
|
||||
return
|
||||
}
|
||||
m.json([]any{mockTVSearchHit()})(w, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ func (o *op) httpErr(resp *http.Response, raw []byte) *ToolResult {
|
||||
default:
|
||||
if st >= 500 {
|
||||
e.Code, e.Message, e.Retryable = "UPSTREAM_REJECTED",
|
||||
fmt.Sprintf("upstream error (HTTP %d)", st), true
|
||||
fmt.Sprintf("upstream error (HTTP %d)", st)+sanitizedDetail(raw), true
|
||||
} else {
|
||||
e.Code, e.Message, e.Retryable = "UPSTREAM_REJECTED",
|
||||
fmt.Sprintf("upstream returned HTTP %d", st), false
|
||||
|
||||
+12
-1
@@ -128,8 +128,19 @@ func (o *op) mediaDetails(a *mediaCallArgs) *ToolResult {
|
||||
if fail != nil {
|
||||
return fail
|
||||
}
|
||||
it := project(m)
|
||||
|
||||
// #7 request state overlay for tvdb
|
||||
if t.Media == "tv" && t.Provider == "tvdb" {
|
||||
reqVal := jbool(m, "requested")
|
||||
if (reqVal == nil || !*reqVal) && jint(m, "requestId") == nil {
|
||||
id, _ := t.idInt()
|
||||
o.overlayTVRequestState(&it, id, jstr(m, "imdbId"))
|
||||
}
|
||||
}
|
||||
|
||||
return o.ok(&MediaPage{Kind: "media_page",
|
||||
Items: []Media{project(m)}, Page: singlePage(1, "media")})
|
||||
Items: []Media{it}, Page: singlePage(1, "media")})
|
||||
}
|
||||
|
||||
func (o *op) mediaByRequest(a *mediaCallArgs) *ToolResult {
|
||||
|
||||
@@ -145,3 +145,65 @@ func TestProjectRequestListStillUsesEntityID(t *testing.T) {
|
||||
t.Errorf("target.id = %d, want 10", r.Target.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTVRequestState(t *testing.T) {
|
||||
it := &Media{
|
||||
Seasons: []SeasonOut{
|
||||
{
|
||||
SeasonNumber: 1,
|
||||
Episodes: []EpisodeOut{
|
||||
{EpisodeNumber: 1},
|
||||
{EpisodeNumber: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
parent := map[string]any{
|
||||
"id": 909,
|
||||
"tvDbId": 81189,
|
||||
"childRequests": []any{
|
||||
map[string]any{
|
||||
"seasonRequests": []any{
|
||||
map[string]any{
|
||||
"seasonNumber": 1,
|
||||
"episodes": []any{
|
||||
map[string]any{"episodeNumber": 1, "requested": true, "available": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
matched := mergeTVRequestState(it, parent, 81189, "")
|
||||
if !matched {
|
||||
t.Fatalf("expected match")
|
||||
}
|
||||
if it.Requested == nil || !*it.Requested {
|
||||
t.Errorf("expected media Requested=true")
|
||||
}
|
||||
if len(it.RequestTargets) != 1 || it.RequestTargets[0].ID != 909 {
|
||||
t.Errorf("expected target ID 909")
|
||||
}
|
||||
if it.Seasons[0].Episodes[0].Requested == nil || !*it.Seasons[0].Episodes[0].Requested {
|
||||
t.Errorf("expected ep1 Requested=true")
|
||||
}
|
||||
if it.Seasons[0].Episodes[0].Available == nil || !*it.Seasons[0].Episodes[0].Available {
|
||||
t.Errorf("expected ep1 Available=true")
|
||||
}
|
||||
if it.Seasons[0].Episodes[1].Requested != nil {
|
||||
t.Errorf("expected ep2 Requested=nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTVRequestStateNoMatch(t *testing.T) {
|
||||
it := &Media{}
|
||||
parent := map[string]any{"tvDbId": 99999}
|
||||
matched := mergeTVRequestState(it, parent, 81189, "")
|
||||
if matched {
|
||||
t.Fatalf("expected no match")
|
||||
}
|
||||
if it.Requested != nil {
|
||||
t.Errorf("expected Requested=nil on no match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// read_requests — v2 list/status routes, single gets, TV children,
|
||||
@@ -163,6 +164,25 @@ func (o *op) requestsSearch(a *RequestsListArgs) *ToolResult {
|
||||
return o.invalid("media", "media must be movie|tv|album")
|
||||
}
|
||||
raw, fail := o.call("GET", path+seg(a.Query), nil, nil)
|
||||
|
||||
// #10 tv search fallback
|
||||
if fail != nil && a.Media == "tv" {
|
||||
items := []Request{}
|
||||
q := strings.ToLower(a.Query)
|
||||
_, failScan := o.eachTVRequestParent(func(p map[string]any) bool {
|
||||
if strings.Contains(strings.ToLower(jstr(p, "title")), q) {
|
||||
items = append(items, o.projectRequest(p, kind))
|
||||
}
|
||||
return false
|
||||
})
|
||||
if failScan != nil {
|
||||
return fail // surface the original error
|
||||
}
|
||||
o.warnf("primary tv search route failed; fell back to parent scan")
|
||||
win, pg := localWindow(o, items, a.Page, "requests")
|
||||
return o.ok(&RequestPage{Kind: "request_page", Items: win, Page: pg})
|
||||
}
|
||||
|
||||
if fail != nil {
|
||||
return fail
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// eachTVRequestParent iterates GET /api/v1/Request/tv/{count}/{pos}/1/0/0
|
||||
// pages (filters ignored upstream; orderType 1 keeps the path valid).
|
||||
// fn(parent) returning true stops the scan. Returns truncated=true when
|
||||
// the 10-page cap is hit without exhausting the list.
|
||||
func (o *op) eachTVRequestParent(fn func(map[string]any) bool) (truncated bool, err *ToolResult) {
|
||||
count := 100
|
||||
pos := 0
|
||||
pages := 0
|
||||
for pages < 10 {
|
||||
path := fmt.Sprintf("/api/v1/Request/tv/%d/%d/1/0/0", count, pos)
|
||||
raw, fail := o.call("GET", path, nil, nil)
|
||||
if fail != nil {
|
||||
return false, fail
|
||||
}
|
||||
arr, fail := o.decodeArray(raw)
|
||||
if fail != nil {
|
||||
return false, fail
|
||||
}
|
||||
|
||||
for _, m := range arr {
|
||||
if fn(m) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(arr) < count {
|
||||
return false, nil // exhausted
|
||||
}
|
||||
pos += count
|
||||
pages++
|
||||
}
|
||||
return true, nil // hit cap
|
||||
}
|
||||
|
||||
// mergeTVRequestState attempts to match and apply request state from a parent record p.
|
||||
// Returns true if the parent matched (stopping the scan).
|
||||
func mergeTVRequestState(it *Media, p map[string]any, tvdbID int, imdbID string) bool {
|
||||
match := false
|
||||
if jint(p, "tvDbId") != nil && *jint(p, "tvDbId") == tvdbID {
|
||||
match = true
|
||||
} else if imdbID != "" && jstr(p, "imdbId") == imdbID {
|
||||
match = true
|
||||
}
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
|
||||
bTrue := true
|
||||
it.Requested = &bTrue
|
||||
it.RequestTargets = []OutTarget{{Kind: "tv_parent", ID: *jint(p, "id")}}
|
||||
|
||||
// Overlay season/episode states
|
||||
childRequests := jarr(p, "childRequests")
|
||||
for _, cr := range childRequests {
|
||||
crm, ok := cr.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
seasonRequests := jarr(crm, "seasonRequests")
|
||||
for _, sr := range seasonRequests {
|
||||
srm, ok := sr.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sNum := jint(srm, "seasonNumber")
|
||||
if sNum == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find matching season in 'it'
|
||||
var season *SeasonOut
|
||||
for i := range it.Seasons {
|
||||
if it.Seasons[i].SeasonNumber == *sNum {
|
||||
season = &it.Seasons[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if season == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
episodes := jarr(srm, "episodes")
|
||||
for _, ep := range episodes {
|
||||
epm, ok := ep.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
epNum := jint(epm, "episodeNumber")
|
||||
if epNum == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find matching episode
|
||||
for i := range season.Episodes {
|
||||
if season.Episodes[i].EpisodeNumber == *epNum {
|
||||
season.Episodes[i].Requested = &bTrue
|
||||
if avail := jbool(epm, "available"); avail != nil && *avail {
|
||||
season.Episodes[i].Available = &bTrue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *op) overlayTVRequestState(it *Media, tvdbID int, imdbID string) {
|
||||
truncated, fail := o.eachTVRequestParent(func(p map[string]any) bool {
|
||||
return mergeTVRequestState(it, p, tvdbID, imdbID)
|
||||
})
|
||||
|
||||
if fail != nil || truncated {
|
||||
o.warnf("request state scan incomplete: degraded upstream flags kept")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user