Session 3: Radarr/Sonarr/Lidarr clients with add-on-search

Adds internal/arr with a shared HTTP layer and per-service clients that
look up candidates and POST the chosen one back with monitored=true and
search-on-add enabled. Lidarr requires metadata_profile_id, now part of
ArrConfig and validated when lidarr is set.
This commit is contained in:
prosolis
2026-05-24 20:22:50 -07:00
parent 5c7d21e574
commit 206b378d93
9 changed files with 537 additions and 1 deletions

60
internal/arr/radarr.go Normal file
View File

@@ -0,0 +1,60 @@
package arr
import (
"context"
"encoding/json"
"fmt"
"bellhop/internal/config"
)
// Radarr drives a Radarr v3 instance.
type Radarr struct {
h *httpClient
qualityProfileID int
rootFolder string
}
func NewRadarr(cfg *config.ArrConfig) *Radarr {
return &Radarr{
h: newHTTP(cfg.URL, cfg.APIKey),
qualityProfileID: cfg.QualityProfileID,
rootFolder: cfg.RootFolder,
}
}
func (r *Radarr) Search(ctx context.Context, term string) ([]Result, error) {
body, err := r.h.get(ctx, "/api/v3/movie/lookup", map[string]string{"term": term})
if err != nil {
return nil, err
}
return parseLookup(body, "title")
}
func (r *Radarr) Add(ctx context.Context, res Result) error {
if len(res.raw) == 0 {
return fmt.Errorf("radarr: empty result")
}
body, err := buildAddBody(res.raw, map[string]any{
"qualityProfileId": r.qualityProfileID,
"rootFolderPath": r.rootFolder,
"monitored": true,
"addOptions": map[string]any{
"searchForMovie": true,
},
})
if err != nil {
return err
}
_, err = r.h.post(ctx, "/api/v3/movie", body)
return err
}
// compile-time interface check
var _ Client = (*Radarr)(nil)
// makeResult is a tiny helper used by tests in this package to construct a
// Result from arbitrary JSON without going through the network.
func makeResult(title string, year int, raw string) Result {
return Result{Title: title, Year: year, raw: json.RawMessage(raw)}
}