arrapi

package module
v1.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: GPL-2.0, GPL-3.0 Imports: 12 Imported by: 0

README

arrapi

Go Reference Go version Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard

Typed, resilient Go clients for the Sonarr and Radarr v3 APIs

A standalone Go library that wraps the Sonarr and Radarr v3 HTTP APIs behind two small, type-safe clients. Requests are authenticated, size-bounded, and retried on transient failures with jittered exponential backoff (via cplieger/httpx). The only runtime dependency is httpx.

Design

Two constructors return two concrete types, so an operation can only be called against the service that supports it:

  • NewSonarr(...) returns a *Sonarr with GetSeries and GetEpisodes.
  • NewRadarr(...) returns a *Radarr with GetMovies.

Both embed a shared core exposing the endpoints common to either service (GetTags, GetSystemStatus, Ping, Close). This replaces the single-client-does-both shape where a wrong call (GetMovies on a Sonarr instance) is only caught at runtime.

Install

go get github.com/cplieger/arrapi@latest

Usage

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cplieger/arrapi"
)

func main() {
	ctx := context.Background()

	sonarr, err := arrapi.NewSonarr("http://sonarr:8989", "your-api-key")
	if err != nil {
		log.Fatal(err)
	}
	defer sonarr.Close()

	// Verify connectivity + credentials up front (fails fast on a bad key).
	if err := sonarr.Ping(ctx); err != nil {
		log.Fatalf("sonarr unreachable: %v", err)
	}

	// Fetch the whole series library in one batched, retried request.
	series, err := sonarr.GetSeries(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Tag filtering: keep series tagged "anime", drop those tagged "skip".
	tags, err := sonarr.GetTags(ctx)
	if err != nil {
		log.Fatal(err)
	}
	anime := arrapi.TagIDs(tags, "anime")
	skip := arrapi.TagIDs(tags, "skip")
	for _, s := range series {
		if arrapi.HasAnyTag(s.Tags, anime) && !arrapi.HasAnyTag(s.Tags, skip) {
			fmt.Printf("%s (tvdb %d, %d)\n", s.Title, s.TvdbID, s.Year)
		}
	}

	// Radarr is a separate typed client; options tune retry/timeout.
	radarr, err := arrapi.NewRadarr("http://radarr:7878", "your-api-key", arrapi.WithMaxAttempts(5))
	if err != nil {
		log.Fatal(err)
	}
	defer radarr.Close()

	movies, err := radarr.GetMovies(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d movies\n", len(movies))
}

API

Constructors
  • NewSonarr(baseURL, apiKey string, opts ...Option) (*Sonarr, error)
  • NewRadarr(baseURL, apiKey string, opts ...Option) (*Radarr, error)

baseURL must be an absolute http(s) URL with a host and no query or fragment (a path is allowed, for reverse-proxy sub-paths); apiKey must be non-empty. Both are validated at construction.

Sonarr
  • GetSeries(ctx) ([]Series, error) — every series in the library
  • GetSeriesByID(ctx, seriesID int) (Series, error) — a single series by ID (IsNotFound reports a missing ID)
  • GetEpisodes(ctx, seriesID int) ([]Episode, error) — episodes for a series, including episode-file details
  • GetEpisodeByID(ctx, episodeID int) (Episode, error) — a single episode by ID (IsNotFound reports a missing ID)
  • RescanSeries(ctx, seriesID int) (Command, error) — rescan the series' folder for new or changed files; returns the queued command
  • RefreshSeries(ctx, seriesID int) (Command, error) — refresh series metadata and rescan; returns the queued command
Radarr
  • GetMovies(ctx) ([]Movie, error) — every movie in the library
  • GetMovieByID(ctx, movieID int) (Movie, error) — a single movie by ID (IsNotFound reports a missing ID)
  • RescanMovie(ctx, movieID int) (Command, error) — rescan the movie's folder for new or changed files; returns the queued command
  • RefreshMovie(ctx, movieID int) (Command, error) — refresh movie metadata and rescan; returns the queued command
Shared (both clients)
  • GetTags(ctx) ([]Tag, error) — all tags defined on the instance
  • ResolveTagIDs(ctx, labels ...string) (ids map[int]struct{}, unmatched []string, err error) — fetch tags and resolve labels to IDs in one call; returns the matched IDs and the labels that matched no tag (no labels = no request)
  • GetQualityProfiles(ctx) ([]QualityProfile, error) — configured quality profiles
  • GetRootFolders(ctx) ([]RootFolder, error) — configured root folders
  • GetSystemStatus(ctx) (SystemStatus, error) — version and app name
  • GetHistorySince(ctx, since time.Time, eventTypes ...EventType) ([]HistoryRecord, error) — history events on or after since, newest first; pass one or more EventTypes to filter (client-side), or none for all
  • GetHistory(ctx, opts HistoryOptions) (HistoryPage, error) — one page of history (newest first), bounded by page size for backfills and large scans
  • GetCommandByID(ctx, id int) (Command, error) — the state of a queued command, to poll a rescan or refresh to completion
  • Ping(ctx) error — connectivity + credential check with a short timeout (no retry)
  • Close() — release idle connections; safe to call more than once
History types

GetHistorySince returns []HistoryRecord (Date, EventType, SourceTitle, SeriesID/EpisodeID for Sonarr or MovieID for Radarr, plus a Data map). HistoryRecord.ImportedPath() pulls the imported file path from a download-import event. EventType decodes both Sonarr's integer and Radarr's string encodings; the exported constants are EventGrabbed, EventFolderImported, EventDownloadImported, EventDownloadFailed, EventFileDeleted, EventFileRenamed, and EventDownloadIgnored. It implements fmt.Stringer for logs, and an unrecognized upstream event decodes to 0 with its raw name preserved in HistoryRecord.RawEventType. Event filtering is client-side: the arr eventType query parameter is numbered per service (Sonarr and Radarr disagree on the integers), so a server-side filter is not portable.

GetHistory returns a HistoryPage (Records, Page, PageSize, TotalRecords) for bounded paging; HistoryOptions sets Page and PageSize.

Tag helpers (pure)
  • TagIDs(tags []Tag, labels ...string) map[int]struct{} — resolve label names to their IDs (case-insensitive, whitespace-trimmed)
  • UnmatchedLabels(tags []Tag, labels ...string) []string — the labels (verbatim) that match no tag, for flagging a misconfigured name
  • HasAnyTag(itemTags []int, ids map[int]struct{}) bool — does an item carry any of those tag IDs

DTO methods that build a link to the item's page in the arr web UI:

  • (*Series).WebURL(baseURL string) string{baseURL}/series/{titleSlug} (Sonarr)
  • (*Movie).WebURL(baseURL string) string{baseURL}/movie/{tmdbID} (Radarr keys its web UI by the TMDB id)

Each returns "" when baseURL or the required field (the Sonarr title slug / Radarr TMDB id) is empty, so a caller reads "" as "no link". The Sonarr title slug is percent-escaped and confined to a single path segment — a ./.. or slash-bearing slug can't break out into the path, query, or fragment — so a community-editable slug is safe to interpolate.

Options
Option Description
WithHTTPClient(c) Use a caller-owned *http.Client (share a pool, pin a CA, inject a test client)
WithMaxAttempts(n) Total attempts including the first, for a transient failure. Clamped to ≥1. Default 3
WithBaseDelay(d) Base delay for the exponential backoff between retries. Default 1s
WithTimeout(d) Per-request timeout applied when the caller's context has no deadline. Default 120s
Errors

Non-2xx responses surface as *StatusError (fields Code, Path, Body, and RetryAfter, the capped Retry-After hint on a 429). It implements httpx.Transient, so a 429 or any 5xx is classified as retryable and every 4xx as permanent, and httpx.RetryAfterHint, so httpx.RetryWithBackoff waits out that capped Retry-After before the next retry instead of its jittered backoff. IsNotFound(err) and IsRateLimited(err) report whether an error is (or wraps) a *StatusError with a 404 or 429. A response body that exceeds the size cap surfaces as *ResponseTooLargeError rather than being silently truncated.

Resilience

  • Retries 429, any 5xx, and transient transport errors (timeouts, connection resets, DNS failures) with jittered exponential backoff (via httpx.RetryWithBackoff), honoring the server's Retry-After hint (capped) on a 429. 4xx (non-429) and non-transient transport errors fail immediately.
  • Retry diagnostics are emitted through httpx's default slog logger (a Debug line per retry, a Warn when retries are exhausted, tagged arrapi); the library owns no logger of its own and still returns typed errors regardless.
  • Every request carries the X-Api-Key header and a User-Agent, and is bounded by WithTimeout (spanning the body decode) when the caller's context has none.
  • Redirects are followed only within the same host (via httpx.RedirectPolicyFunc with WithSameHost), so the X-Api-Key is never forwarded to another origin (Go strips only Authorization/Cookie headers on a cross-host redirect, not custom ones). A same-host http->https upgrade is followed; a same-host https->http downgrade is refused so the key never rides a cleartext hop, and a cross-host redirect is refused outright. The policy matches on host only, not port, so a same-host redirect to a different port is also followed. A caller-supplied client via WithHTTPClient owns its own redirect policy.
  • Response bodies are size-capped before decoding (64 MB for list endpoints, 1 MB for single objects); an over-cap body is rejected as *ResponseTooLargeError rather than truncated.
  • Clients own no long-lived goroutines and hold no locks a caller can observe; a single client is safe for concurrent use.

Timeouts and retries

arrapi bounds every request by context and retries only transient failures:

  • A caller-supplied context deadline is the authoritative total budget across all attempts and backoffs. It is honored as-is; arrapi imposes no separate client-level ceiling on top of it.
  • WithTimeout (default 120s) is a per-attempt budget, applied only when the caller's context carries no deadline of its own. The total is then bounded by the attempt count (WithMaxAttempts, default 3).
  • Retries cover transient failures: HTTP 429 and 5xx (honoring a capped Retry-After), and transient transport errors (timeouts, connection resets, DNS). A 4xx other than 429 is permanent and fails fast.
  • A timeout or deadline expiry is terminal, not a retryable condition: once the budget is exhausted the call stops rather than retrying. Mutations (rescan/refresh commands) are single-attempt and never retried.

Unsupported by Design

Deliberate non-goals, not TODOs:

Not included Rationale
Adding / editing / deleting media This is a read + connectivity + rescan client, not a full library-management client. Use golift/starr or the devopsarr *-go clients for CRUD.
Quality-profile item / cutoff detail QualityProfile models identity (name + ID); the nested quality-item and custom-format tree is out of scope.
Indexer / download-client / notification config Management-plane surface with no consumer need.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0 — see LICENSE.

Documentation

Overview

Package arrapi provides typed, resilient clients for the Sonarr and Radarr v3 HTTP APIs. It covers read access, connectivity checks, and the rescan/refresh commands.

Two constructors return two concrete client types, so an operation can only be called against the instance that supports it: NewSonarr returns a *Sonarr (GetSeries, GetEpisodes, RescanSeries, RefreshSeries) and NewRadarr returns a *Radarr (GetMovies, RescanMovie, RefreshMovie). Both embed a shared core that exposes the endpoints common to either service (GetTags, GetSystemStatus, Ping, Close).

Every request is authenticated with the instance's X-Api-Key, bounded by a per-request timeout, and retried on transient failures (HTTP 429, any 5xx, and transient transport errors) with jittered exponential backoff via github.com/cplieger/httpx/v2. Non-2xx responses surface as a *StatusError, which reports whether it was transient and lets callers detect a 404 with IsNotFound.

Response bodies are size-bounded before decoding to guard against oversized or malicious payloads. The clients own no goroutines and hold no locks; a single client is safe for concurrent use.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func HasAnyTag

func HasAnyTag(itemTags []int, ids map[int]struct{}) bool

HasAnyTag reports whether itemTags contains any of the tag IDs in ids. It is the companion to TagIDs for include/exclude filtering of series and movies.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is (or wraps) a *StatusError with a 404 status.

func IsRateLimited added in v1.1.0

func IsRateLimited(err error) bool

IsRateLimited reports whether err is (or wraps) a *StatusError with a 429 status. When true, the *StatusError's RetryAfter carries the server's hint if it sent one.

func TagIDs

func TagIDs(tags []Tag, labels ...string) map[int]struct{}

TagIDs returns the set of tag IDs whose labels match any of the given labels. Matching is case-insensitive and trims surrounding whitespace (Sonarr and Radarr store tag labels lowercased). Labels with no matching tag are simply absent from the result. It returns nil when no labels are supplied.

Example

ExampleTagIDs shows resolving tag labels to IDs and using the result to filter items by tag.

package main

import (
	"fmt"

	"github.com/cplieger/arrapi"
)

func main() {
	tags := []arrapi.Tag{
		{ID: 1, Label: "anime"},
		{ID: 2, Label: "4k"},
		{ID: 3, Label: "kids"},
	}
	want := arrapi.TagIDs(tags, "anime", "kids")

	fmt.Println(arrapi.HasAnyTag([]int{3, 9}, want)) // carries "kids"
	fmt.Println(arrapi.HasAnyTag([]int{2}, want))    // only "4k"
}
Output:
true
false

func UnmatchedLabels added in v1.4.0

func UnmatchedLabels(tags []Tag, labels ...string) []string

UnmatchedLabels returns the labels (verbatim, in input order) that match no tag in tags, following the same case-insensitive, whitespace-trimmed rule as TagIDs. Empty or whitespace-only labels are ignored. It returns nil when every non-empty label matches or none is supplied, and is the companion to TagIDs for surfacing a misconfigured tag name.

Types

type AlternateTitle

type AlternateTitle struct {
	Title string `json:"title"`
}

AlternateTitle is an alternate title for a series or movie.

type Command added in v1.1.0

type Command struct {
	Name   string `json:"name"`
	Status string `json:"status"`
	ID     int    `json:"id"`
}

Command is a Sonarr/Radarr command resource. The endpoint queues the command and returns it with an ID and a Status ("queued", "started", "completed", or "failed"); poll GetCommandByID to follow it to completion.

type Episode

type Episode struct {
	EpisodeFile           *EpisodeFile `json:"episodeFile"`
	Title                 string       `json:"title"`
	AirDate               string       `json:"airDate"`
	ID                    int          `json:"id"`
	SeriesID              int          `json:"seriesId"`
	SeasonNumber          int          `json:"seasonNumber"`
	EpisodeNumber         int          `json:"episodeNumber"`
	AbsoluteEpisodeNumber int          `json:"absoluteEpisodeNumber"`
	SceneSeasonNumber     int          `json:"sceneSeasonNumber"`
	SceneEpisodeNumber    int          `json:"sceneEpisodeNumber"`
	HasFile               bool         `json:"hasFile"`
	Monitored             bool         `json:"monitored"`
}

Episode is a Sonarr episode.

type EpisodeFile

type EpisodeFile struct {
	MediaInfo    *MediaInfo `json:"mediaInfo"`
	RelativePath string     `json:"relativePath"`
	Path         string     `json:"path"`
	SceneName    string     `json:"sceneName"`
	ReleaseGroup string     `json:"releaseGroup"`
	ID           int        `json:"id"`
	Size         int64      `json:"size"`
}

EpisodeFile holds file details for a Sonarr episode.

type EventType

type EventType int

EventType identifies a Sonarr/Radarr history event. Its integer values follow Sonarr's HistoryEventType enum; Radarr's string encoding is mapped to the same semantic values (see eventTypeByName). Decode with UnmarshalJSON, which accepts either the integer (Sonarr) or the string (Radarr) form.

const (
	// EventGrabbed is a release grabbed from an indexer.
	EventGrabbed EventType = 1
	// EventFolderImported is a download folder imported at the series or movie
	// level (Sonarr "seriesFolderImported", Radarr "movieFolderImported").
	EventFolderImported EventType = 2
	// EventDownloadImported is a downloaded file imported into the library
	// (the "downloadFolderImported" event).
	EventDownloadImported EventType = 3
	// EventDownloadFailed is a failed download.
	EventDownloadFailed EventType = 4
	// EventFileDeleted is an episode-file or movie-file deletion.
	EventFileDeleted EventType = 5
	// EventFileRenamed is an episode-file or movie-file rename.
	EventFileRenamed EventType = 6
	// EventDownloadIgnored is a grabbed download that was ignored.
	EventDownloadIgnored EventType = 7
)

func (EventType) String added in v1.1.0

func (e EventType) String() string

String returns a stable, human-readable name for the event type, suitable for structured logs. Known types return a service-agnostic label (which for single-name events matches the arr wire token); any other value (including the zero/unknown type) returns the Go-style EventType(<n>).

func (*EventType) UnmarshalJSON

func (e *EventType) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the integer form (Sonarr) or the string form (Radarr). The integer path uses Sonarr's numbering, which is safe because Radarr encodes the event as a string. An unknown string, negative integer, or unmodeled positive integer decodes to 0 (unknown); HistoryRecord preserves the raw token in RawEventType.

type HistoryOptions added in v1.1.0

type HistoryOptions struct {
	Page     int
	PageSize int
}

HistoryOptions parameterizes a paged GetHistory call. A zero Page or PageSize uses the arr default (page 1; the server's default page size).

type HistoryPage added in v1.1.0

type HistoryPage struct {
	Records      []HistoryRecord `json:"records"`
	Page         int             `json:"page"`
	PageSize     int             `json:"pageSize"`
	TotalRecords int             `json:"totalRecords"`
}

HistoryPage is one page from the paged /history endpoint (newest first).

type HistoryRecord

type HistoryRecord struct {
	Date         time.Time         `json:"date"`
	Data         map[string]string `json:"data"`
	SourceTitle  string            `json:"sourceTitle"`
	RawEventType string            `json:"-"`
	EventType    EventType         `json:"eventType"`
	ID           int               `json:"id"`
	SeriesID     int               `json:"seriesId"`
	EpisodeID    int               `json:"episodeId"`
	MovieID      int               `json:"movieId"`
}

HistoryRecord is a single Sonarr or Radarr history event. Sonarr populates SeriesID/EpisodeID; Radarr populates MovieID. Data carries event-specific key/value detail (e.g. "importedPath"). RawEventType holds the original wire token when the event is one arrapi does not model (EventType is then 0).

func (*HistoryRecord) ImportedPath

func (h *HistoryRecord) ImportedPath() string

ImportedPath returns the imported file path from a download-import event's data dictionary, or "" when absent.

func (*HistoryRecord) UnmarshalJSON added in v1.1.0

func (h *HistoryRecord) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a history record and, when the event is not one arrapi models, preserves the raw eventType token in RawEventType so a new upstream event stays identifiable in logs rather than collapsing silently to 0.

type Language added in v1.2.0

type Language struct {
	Name string `json:"name"`
	ID   int    `json:"id"`
}

Language is a Sonarr/Radarr language reference, such as a series' or movie's original language. Only the identity fields (name + id) are modeled; the arr resource carries no further language detail this client needs.

type MediaInfo

type MediaInfo struct {
	AudioLanguages string `json:"audioLanguages"`
	Subtitles      string `json:"subtitles"`
	VideoCodec     string `json:"videoCodec"`
	AudioCodec     string `json:"audioCodec"`
}

MediaInfo holds media-analysis details for an episode or movie file.

type Movie

type Movie struct {
	MovieFile        *MovieFile       `json:"movieFile"`
	OriginalLanguage *Language        `json:"originalLanguage"`
	Title            string           `json:"title"`
	SortTitle        string           `json:"sortTitle"`
	ImdbID           string           `json:"imdbId"`
	InCinemas        string           `json:"inCinemas"`
	DigitalRelease   string           `json:"digitalRelease"`
	Path             string           `json:"path"`
	RootFolderPath   string           `json:"rootFolderPath"`
	Status           string           `json:"status"`
	AlternateTitles  []AlternateTitle `json:"alternateTitles"`
	Tags             []int            `json:"tags"`
	ID               int              `json:"id"`
	TmdbID           int              `json:"tmdbId"`
	Year             int              `json:"year"`
	QualityProfileID int              `json:"qualityProfileId"`
	HasFile          bool             `json:"hasFile"`
	Monitored        bool             `json:"monitored"`
}

Movie is a Radarr movie.

func (*Movie) WebURL added in v1.5.0

func (m *Movie) WebURL(baseURL string) string

WebURL returns the Radarr web-UI deep-link to this movie for the given Radarr base URL, or "" when the base URL is empty or the movie has no TMDB id. Radarr's per-movie route is /movie/{titleSlug}, and Radarr sets that slug to the TMDB id, so the numeric TMDB id resolves the page directly.

type MovieFile

type MovieFile struct {
	MediaInfo    *MediaInfo `json:"mediaInfo"`
	RelativePath string     `json:"relativePath"`
	Path         string     `json:"path"`
	SceneName    string     `json:"sceneName"`
	ReleaseGroup string     `json:"releaseGroup"`
	ID           int        `json:"id"`
	Size         int64      `json:"size"`
}

MovieFile holds file details for a Radarr movie.

type Option

type Option func(*config)

Option configures a Sonarr or Radarr client at construction time.

func WithBaseDelay

func WithBaseDelay(d time.Duration) Option

WithBaseDelay sets the base delay for the exponential backoff between retries. Default: 1s.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient sets the underlying *http.Client. When set, the caller owns the client's transport, timeout, and redirect policy; arrapi's defaults are not applied. A nil client is ignored. Use this to share a connection pool, pin a custom CA (see github.com/cplieger/httpx/v2.CATransport), or inject a test server client.

Security: arrapi's default client restricts redirects to the same host and refuses https->http downgrades, so the X-Api-Key (sent on every request) is never forwarded to another origin or onto a cleartext hop. Go forwards custom headers across redirects, stripping only Authorization and Cookie. A client supplied here that follows cross-host redirects (for example one using net/http's default policy) leaks the X-Api-Key to the redirect target, and a same-host downgrade sends it over cleartext. Set CheckRedirect to a policy such as httpx.RedirectPolicyFunc(httpx.WithSameHost(), httpx.WithMaxHops(10)) from github.com/cplieger/httpx/v2; that policy keeps both guards unless WithAllowSchemeDowngrade is explicitly enabled.

func WithMaxAttempts

func WithMaxAttempts(n int) Option

WithMaxAttempts sets the total number of HTTP attempts (including the first) for a transient failure. Values below 1 are clamped to 1 (try exactly once). Default: 3.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-request timeout applied when the caller's context carries no deadline of its own. When the caller's context has a deadline, it is authoritative and used as-is; arrapi imposes no separate client-level ceiling on top of it. Default: 120s.

type QualityProfile

type QualityProfile struct {
	Name string `json:"name"`
	ID   int    `json:"id"`
}

QualityProfile is a Sonarr or Radarr quality profile. Only the identity fields are modeled; the profile's item/cutoff detail is out of scope.

type Radarr

type Radarr struct {
	// contains filtered or unexported fields
}

Radarr is a client for a single Radarr v3 instance. The zero value is not usable; construct one with NewRadarr. A Radarr is safe for concurrent use.

func NewRadarr

func NewRadarr(baseURL, apiKey string, opts ...Option) (*Radarr, error)

NewRadarr returns a Radarr client for the given base URL (e.g. "http://radarr:7878") and API key. It returns an error if the URL is not an absolute http(s) URL or the key is empty.

func (Radarr) Close

func (c Radarr) Close()

Close releases idle connections held by the client's HTTP transport. It is safe to call more than once.

func (Radarr) GetCommandByID added in v1.1.0

func (c Radarr) GetCommandByID(ctx context.Context, id int) (Command, error)

GetCommandByID returns the current state of a previously issued command, so a caller can poll a rescan or refresh to completion. It returns a *StatusError for which IsNotFound reports true when no command has that ID. Available on both Sonarr and Radarr.

func (Radarr) GetHistory added in v1.1.0

func (c Radarr) GetHistory(ctx context.Context, opts HistoryOptions) (HistoryPage, error)

GetHistory returns one page of history from the paged /history endpoint, newest first. Unlike GetHistorySince it is bounded by page size, so it suits backfills and large scans. Filter the returned records by EventType client-side; the arr eventType query parameter is numbered per service and is not portable. Available on both Sonarr and Radarr.

func (Radarr) GetHistorySince

func (c Radarr) GetHistorySince(ctx context.Context, since time.Time, eventTypes ...EventType) ([]HistoryRecord, error)

GetHistorySince returns history records on or after since (the arr endpoint orders newest first). Pass one or more event types to filter the result; pass none to return every type. Available on both Sonarr and Radarr.

A zero-value/unset EventType in the filter is ignored, so a filter made up only of unset values behaves the same as passing none (returns every type).

Filtering is client-side: the arr eventType query parameter is numbered per service (Sonarr and Radarr disagree on the integers), so a server-side filter is not portable. Note that /history/since is unbounded, so a wide since window can return a large payload (subject to the response size cap); use GetHistory for bounded, paged scans.

func (*Radarr) GetMovieByID added in v1.1.0

func (r *Radarr) GetMovieByID(ctx context.Context, movieID int) (Movie, error)

GetMovieByID returns the single movie with the given Radarr ID. It returns a *StatusError for which IsNotFound reports true when no movie has that ID.

func (*Radarr) GetMovies

func (r *Radarr) GetMovies(ctx context.Context) ([]Movie, error)

GetMovies returns every movie in the Radarr library.

func (Radarr) GetQualityProfiles

func (c Radarr) GetQualityProfiles(ctx context.Context) ([]QualityProfile, error)

GetQualityProfiles returns every quality profile defined on the instance.

func (Radarr) GetRootFolders

func (c Radarr) GetRootFolders(ctx context.Context) ([]RootFolder, error)

GetRootFolders returns every root folder configured on the instance.

func (Radarr) GetSystemStatus

func (c Radarr) GetSystemStatus(ctx context.Context) (SystemStatus, error)

GetSystemStatus returns the instance's system status (version, app name).

func (Radarr) GetTags

func (c Radarr) GetTags(ctx context.Context) ([]Tag, error)

GetTags returns all tags defined in the Sonarr or Radarr instance.

func (Radarr) Ping

func (c Radarr) Ping(ctx context.Context) error

Ping verifies connectivity and credentials against the instance's system-status endpoint. It returns nil on success, a *StatusError (a 401 for an invalid API key), or a transport error. A short timeout bounds it so config validation fails fast; it does not retry.

func (*Radarr) RefreshMovie

func (r *Radarr) RefreshMovie(ctx context.Context, movieID int) (Command, error)

RefreshMovie asks Radarr to refresh the movie's metadata and rescan its files. It returns the queued command.

func (*Radarr) RescanMovie

func (r *Radarr) RescanMovie(ctx context.Context, movieID int) (Command, error)

RescanMovie asks Radarr to rescan the movie's folder for new or changed files. It returns the queued command.

func (Radarr) ResolveTagIDs added in v1.4.0

func (c Radarr) ResolveTagIDs(ctx context.Context, labels ...string) (ids map[int]struct{}, unmatched []string, err error)

ResolveTagIDs fetches the instance's tags and resolves the given labels to their tag IDs. It returns the set of matched IDs and, separately, the labels that matched no tag, so a caller filtering by configured tag names can flag a misconfiguration rather than silently ignore it. Matching is case-insensitive and trims surrounding whitespace. Passing no labels returns (nil, nil, nil) without issuing a request. It is the network-backed convenience over GetTags + TagIDs + UnmatchedLabels. Available on both Sonarr and Radarr.

type ResponseTooLargeError added in v1.1.0

type ResponseTooLargeError struct {
	Path  string
	Limit int64
}

ResponseTooLargeError is returned when a response body exceeds the client's size cap for that endpoint. The body is rejected rather than truncated, so a caller never decodes a silently-cut payload.

func (*ResponseTooLargeError) Error added in v1.1.0

func (e *ResponseTooLargeError) Error() string

Error implements the error interface.

type RootFolder

type RootFolder struct {
	Path       string `json:"path"`
	ID         int    `json:"id"`
	FreeSpace  int64  `json:"freeSpace"`
	Accessible bool   `json:"accessible"`
}

RootFolder is a configured root folder on a Sonarr or Radarr instance.

type Season

type Season struct {
	Statistics   *SeasonStatistics `json:"statistics"`
	SeasonNumber int               `json:"seasonNumber"`
	Monitored    bool              `json:"monitored"`
}

Season is per-season metadata on a Sonarr series.

type SeasonStatistics

type SeasonStatistics struct {
	EpisodeFileCount  int `json:"episodeFileCount"`
	EpisodeCount      int `json:"episodeCount"`
	TotalEpisodeCount int `json:"totalEpisodeCount"`
}

SeasonStatistics holds per-season episode counts.

type Series

type Series struct {
	Statistics       *SeriesStatistics `json:"statistics"`
	OriginalLanguage *Language         `json:"originalLanguage"`
	Title            string            `json:"title"`
	SortTitle        string            `json:"sortTitle"`
	TitleSlug        string            `json:"titleSlug"`
	ImdbID           string            `json:"imdbId"`
	FirstAired       string            `json:"firstAired"`
	Path             string            `json:"path"`
	RootFolderPath   string            `json:"rootFolderPath"`
	Status           string            `json:"status"`
	Seasons          []Season          `json:"seasons"`
	AlternateTitles  []AlternateTitle  `json:"alternateTitles"`
	Tags             []int             `json:"tags"`
	ID               int               `json:"id"`
	TvdbID           int               `json:"tvdbId"`
	TmdbID           int               `json:"tmdbId"`
	Year             int               `json:"year"`
	QualityProfileID int               `json:"qualityProfileId"`
	Monitored        bool              `json:"monitored"`
}

Series is a Sonarr series.

func (*Series) WebURL added in v1.5.0

func (s *Series) WebURL(baseURL string) string

WebURL returns the Sonarr web-UI deep-link to this series for the given Sonarr base URL (e.g. "https://sonarr.example.com"), or "" when the base URL or the series title slug is empty. Sonarr's only per-series route is /series/{titleSlug}, where the slug is a TheTVDB text slug (set from the metadata provider) that is not derivable from any ID, so the slug must come from the series record.

type SeriesStatistics

type SeriesStatistics struct {
	SeasonCount       int `json:"seasonCount"`
	EpisodeFileCount  int `json:"episodeFileCount"`
	EpisodeCount      int `json:"episodeCount"`
	TotalEpisodeCount int `json:"totalEpisodeCount"`
}

SeriesStatistics holds series-level episode counts.

type Sonarr

type Sonarr struct {
	// contains filtered or unexported fields
}

Sonarr is a client for a single Sonarr v3 instance. The zero value is not usable; construct one with NewSonarr. A Sonarr is safe for concurrent use.

func NewSonarr

func NewSonarr(baseURL, apiKey string, opts ...Option) (*Sonarr, error)

NewSonarr returns a Sonarr client for the given base URL (e.g. "http://sonarr:8989") and API key. It returns an error if the URL is not an absolute http(s) URL or the key is empty.

Example

ExampleNewSonarr shows the construct → ping → fetch flow. It is compiled as documentation but not run (no Output comment), so it needs no live instance.

package main

import (
	"context"
	"fmt"

	"github.com/cplieger/arrapi"
)

func main() {
	ctx := context.Background()

	sonarr, err := arrapi.NewSonarr("http://sonarr:8989", "your-api-key")
	if err != nil {
		return
	}
	defer sonarr.Close()

	if err := sonarr.Ping(ctx); err != nil {
		return
	}
	series, err := sonarr.GetSeries(ctx)
	if err != nil {
		return
	}
	fmt.Printf("%d series\n", len(series))
}

func (Sonarr) Close

func (c Sonarr) Close()

Close releases idle connections held by the client's HTTP transport. It is safe to call more than once.

func (Sonarr) GetCommandByID added in v1.1.0

func (c Sonarr) GetCommandByID(ctx context.Context, id int) (Command, error)

GetCommandByID returns the current state of a previously issued command, so a caller can poll a rescan or refresh to completion. It returns a *StatusError for which IsNotFound reports true when no command has that ID. Available on both Sonarr and Radarr.

func (*Sonarr) GetEpisodeByID added in v1.1.0

func (s *Sonarr) GetEpisodeByID(ctx context.Context, episodeID int) (Episode, error)

GetEpisodeByID returns the single episode with the given Sonarr ID. It returns a *StatusError for which IsNotFound reports true when no episode has that ID.

func (*Sonarr) GetEpisodes

func (s *Sonarr) GetEpisodes(ctx context.Context, seriesID int) ([]Episode, error)

GetEpisodes returns all episodes for the given series, including episode-file details (release group, size, media info) where present.

func (Sonarr) GetHistory added in v1.1.0

func (c Sonarr) GetHistory(ctx context.Context, opts HistoryOptions) (HistoryPage, error)

GetHistory returns one page of history from the paged /history endpoint, newest first. Unlike GetHistorySince it is bounded by page size, so it suits backfills and large scans. Filter the returned records by EventType client-side; the arr eventType query parameter is numbered per service and is not portable. Available on both Sonarr and Radarr.

func (Sonarr) GetHistorySince

func (c Sonarr) GetHistorySince(ctx context.Context, since time.Time, eventTypes ...EventType) ([]HistoryRecord, error)

GetHistorySince returns history records on or after since (the arr endpoint orders newest first). Pass one or more event types to filter the result; pass none to return every type. Available on both Sonarr and Radarr.

A zero-value/unset EventType in the filter is ignored, so a filter made up only of unset values behaves the same as passing none (returns every type).

Filtering is client-side: the arr eventType query parameter is numbered per service (Sonarr and Radarr disagree on the integers), so a server-side filter is not portable. Note that /history/since is unbounded, so a wide since window can return a large payload (subject to the response size cap); use GetHistory for bounded, paged scans.

func (Sonarr) GetQualityProfiles

func (c Sonarr) GetQualityProfiles(ctx context.Context) ([]QualityProfile, error)

GetQualityProfiles returns every quality profile defined on the instance.

func (Sonarr) GetRootFolders

func (c Sonarr) GetRootFolders(ctx context.Context) ([]RootFolder, error)

GetRootFolders returns every root folder configured on the instance.

func (*Sonarr) GetSeries

func (s *Sonarr) GetSeries(ctx context.Context) ([]Series, error)

GetSeries returns every series in the Sonarr library.

func (*Sonarr) GetSeriesByID added in v1.1.0

func (s *Sonarr) GetSeriesByID(ctx context.Context, seriesID int) (Series, error)

GetSeriesByID returns the single series with the given Sonarr ID. It returns a *StatusError for which IsNotFound reports true when no series has that ID.

func (Sonarr) GetSystemStatus

func (c Sonarr) GetSystemStatus(ctx context.Context) (SystemStatus, error)

GetSystemStatus returns the instance's system status (version, app name).

func (Sonarr) GetTags

func (c Sonarr) GetTags(ctx context.Context) ([]Tag, error)

GetTags returns all tags defined in the Sonarr or Radarr instance.

func (Sonarr) Ping

func (c Sonarr) Ping(ctx context.Context) error

Ping verifies connectivity and credentials against the instance's system-status endpoint. It returns nil on success, a *StatusError (a 401 for an invalid API key), or a transport error. A short timeout bounds it so config validation fails fast; it does not retry.

func (*Sonarr) RefreshSeries

func (s *Sonarr) RefreshSeries(ctx context.Context, seriesID int) (Command, error)

RefreshSeries asks Sonarr to refresh the series' metadata and rescan its files. It returns the queued command.

func (*Sonarr) RescanSeries

func (s *Sonarr) RescanSeries(ctx context.Context, seriesID int) (Command, error)

RescanSeries asks Sonarr to rescan the series' folder for new or changed files (for example after an external tool wrote a subtitle). It does not re-fetch metadata; use RefreshSeries for that. It returns the queued command.

func (Sonarr) ResolveTagIDs added in v1.4.0

func (c Sonarr) ResolveTagIDs(ctx context.Context, labels ...string) (ids map[int]struct{}, unmatched []string, err error)

ResolveTagIDs fetches the instance's tags and resolves the given labels to their tag IDs. It returns the set of matched IDs and, separately, the labels that matched no tag, so a caller filtering by configured tag names can flag a misconfiguration rather than silently ignore it. Matching is case-insensitive and trims surrounding whitespace. Passing no labels returns (nil, nil, nil) without issuing a request. It is the network-backed convenience over GetTags + TagIDs + UnmatchedLabels. Available on both Sonarr and Radarr.

type StatusError

type StatusError struct {
	Body       string
	Path       string
	RetryAfter time.Duration
	Code       int
}

StatusError is a non-2xx HTTP response from a Sonarr or Radarr API. It implements the httpx.Transient interface, so the httpx retry helpers treat a 429 or any 5xx as retryable and every 4xx as permanent. RetryAfter carries the server's Retry-After hint (capped) whenever the response includes a Retry-After header (in practice a 429 or a 503); it is zero otherwise. Only a transient status actually consults it during retry.

func (*StatusError) Error

func (e *StatusError) Error() string

Error implements the error interface.

func (*StatusError) IsTransient

func (e *StatusError) IsTransient() bool

IsTransient reports whether the response is retryable: HTTP 429 (rate limited) or any 5xx (server error).

func (*StatusError) RetryAfterHint added in v1.3.0

func (e *StatusError) RetryAfterHint() time.Duration

RetryAfterHint implements httpx.RetryAfterHint: it exposes the capped Retry-After hint so httpx.RetryWithBackoff waits it out before the next retry instead of its jittered backoff. It is zero when the response carried no Retry-After (in practice, anything but a 429 or a 503), in which case RetryWithBackoff falls back to the jittered exponential backoff.

type SystemStatus

type SystemStatus struct {
	Version      string `json:"version"`
	AppName      string `json:"appName"`
	InstanceName string `json:"instanceName"`
}

SystemStatus is the subset of a Sonarr/Radarr system-status response used for connectivity checks and version reporting.

type Tag

type Tag struct {
	Label string `json:"label"`
	ID    int    `json:"id"`
}

Tag is a Sonarr or Radarr tag.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL