Add interactive release search (M6)

Release listing and grab in the shared arr client, a full-screen release
list (sort, hide rejected, rejection reasons, confirmation before grabbing
rejected releases; Usenet only), and a season/episode picker for Sonarr.
Overlays are now a stack so dialogs can open above the release list.
Add a fake Newznab indexer to the dev setup.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 18:09:19 +02:00
co-authored by Claude Sonnet 5.5
parent 446cbdd6c5
commit f928427993
18 changed files with 1002 additions and 23 deletions
+33
View File
@@ -115,3 +115,36 @@ func TestSearchSeries(t *testing.T) {
t.Errorf("body = %v", got)
}
}
func TestEpisodesAndReleaseQueries(t *testing.T) {
var queries []string
c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
queries = append(queries, r.URL.Path+"?"+r.URL.RawQuery)
if r.URL.Path == "/api/v3/episode" {
w.Write([]byte(`[{"id":10,"seriesId":3,"seasonNumber":1,"episodeNumber":2,"title":"Two","hasFile":true}]`))
return
}
w.Write([]byte(`[]`))
})
ctx := context.Background()
eps, err := c.Episodes(ctx, 3)
if err != nil || len(eps) != 1 || eps[0].ID != 10 || !eps[0].HasFile || eps[0].EpisodeNumber != 2 {
t.Fatalf("episodes = %+v, %v", eps, err)
}
if _, err := c.EpisodeReleases(ctx, 10); err != nil {
t.Fatal(err)
}
if _, err := c.SeasonReleases(ctx, 3, 1); err != nil {
t.Fatal(err)
}
want := []string{
"/api/v3/episode?seriesId=3",
"/api/v3/release?episodeId=10",
"/api/v3/release?seasonNumber=1&seriesId=3",
}
for i := range want {
if queries[i] != want[i] {
t.Errorf("query %d = %q, want %q", i, queries[i], want[i])
}
}
}