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:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
|
||||
)
|
||||
@@ -166,3 +167,35 @@ func (c *Client) SearchSeries(ctx context.Context, seriesID int) error {
|
||||
func (c *Client) Queue(ctx context.Context) ([]arr.QueueItem, error) {
|
||||
return c.QueueWith(ctx, url.Values{"includeSeries": {"true"}, "includeEpisode": {"true"}})
|
||||
}
|
||||
|
||||
// Episode is an episode of a library series.
|
||||
type Episode struct {
|
||||
ID int `json:"id"`
|
||||
SeriesID int `json:"seriesId"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Title string `json:"title"`
|
||||
AirDate string `json:"airDate"`
|
||||
HasFile bool `json:"hasFile"`
|
||||
Monitored bool `json:"monitored"`
|
||||
}
|
||||
|
||||
// Episodes lists all episodes of a library series.
|
||||
func (c *Client) Episodes(ctx context.Context, seriesID int) ([]Episode, error) {
|
||||
var episodes []Episode
|
||||
err := c.Do(ctx, http.MethodGet, "episode", url.Values{"seriesId": {strconv.Itoa(seriesID)}}, nil, &episodes)
|
||||
return episodes, err
|
||||
}
|
||||
|
||||
// EpisodeReleases runs an interactive search for one episode.
|
||||
func (c *Client) EpisodeReleases(ctx context.Context, episodeID int) ([]arr.Release, error) {
|
||||
return c.Client.Releases(ctx, url.Values{"episodeId": {strconv.Itoa(episodeID)}})
|
||||
}
|
||||
|
||||
// SeasonReleases runs an interactive search for a whole season (season packs and episodes).
|
||||
func (c *Client) SeasonReleases(ctx context.Context, seriesID, season int) ([]arr.Release, error) {
|
||||
return c.Client.Releases(ctx, url.Values{
|
||||
"seriesId": {strconv.Itoa(seriesID)},
|
||||
"seasonNumber": {strconv.Itoa(season)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user