package arr import ( "context" "fmt" "net/http" "net/url" "strconv" "strings" ) // StatusMessage is a warning or error attached to a queue item, e.g. why an import is blocked. type StatusMessage struct { Title string `json:"title"` Messages []string `json:"messages"` } // QueueItem is an entry of the Radarr/Sonarr download queue. type QueueItem struct { ID int `json:"id"` Title string `json:"title"` // release title Status string `json:"status"` TrackedDownloadStatus string `json:"trackedDownloadStatus"` // ok, warning, error TrackedDownloadState string `json:"trackedDownloadState"` // downloading, importPending, importBlocked, ... StatusMessages []StatusMessage `json:"statusMessages"` ErrorMessage string `json:"errorMessage"` Size float64 `json:"size"` SizeLeft float64 `json:"sizeleft"` TimeLeft string `json:"timeleft"` DownloadClient string `json:"downloadClient"` DownloadID string `json:"downloadId"` // the download client's id, e.g. SABnzbd's nzo_id Indexer string `json:"indexer"` Protocol string `json:"protocol"` // Present depending on the include* parameters of the request. Movie *QueueMovie `json:"movie,omitempty"` Series *QueueSeries `json:"series,omitempty"` Episode *QueueEpisode `json:"episode,omitempty"` } // QueueMovie is the movie a queue item belongs to. type QueueMovie struct { Title string `json:"title"` Year int `json:"year"` } // QueueSeries is the series a queue item belongs to. type QueueSeries struct { Title string `json:"title"` } // QueueEpisode is the episode a queue item belongs to. type QueueEpisode struct { SeasonNumber int `json:"seasonNumber"` EpisodeNumber int `json:"episodeNumber"` Title string `json:"title"` } // Name returns a readable name: "Movie (2020)", "Show S01E02", or the release title. func (q QueueItem) Name() string { switch { case q.Movie != nil && q.Movie.Title != "": if q.Movie.Year > 0 { return fmt.Sprintf("%s (%d)", q.Movie.Title, q.Movie.Year) } return q.Movie.Title case q.Series != nil && q.Series.Title != "": if q.Episode != nil { return fmt.Sprintf("%s S%02dE%02d", q.Series.Title, q.Episode.SeasonNumber, q.Episode.EpisodeNumber) } return q.Series.Title } return q.Title } // Progress returns the download progress in percent (0-100). func (q QueueItem) Progress() float64 { if q.Size <= 0 { return 0 } return min(max((q.Size-q.SizeLeft)/q.Size*100, 0), 100) } // NeedsAttention reports whether the item has a warning or error. func (q QueueItem) NeedsAttention() bool { return q.TrackedDownloadStatus == "warning" || q.TrackedDownloadStatus == "error" || q.TrackedDownloadState == "importBlocked" || q.TrackedDownloadState == "failed" || q.TrackedDownloadState == "failedPending" } // Problem returns the item's warnings and errors as one line ("" if none). func (q QueueItem) Problem() string { var parts []string if q.ErrorMessage != "" { parts = append(parts, q.ErrorMessage) } for _, m := range q.StatusMessages { parts = append(parts, m.Messages...) if len(m.Messages) == 0 && m.Title != "" { parts = append(parts, m.Title) } } return strings.Join(parts, "; ") } // QueueWith lists the queue with the given extra query parameters, e.g. // includeMovie=true. It fetches a single page of up to 200 items. func (c *Client) QueueWith(ctx context.Context, include url.Values) ([]QueueItem, error) { q := url.Values{"page": {"1"}, "pageSize": {"200"}} for k, v := range include { q[k] = v } var page struct { Records []QueueItem `json:"records"` } if err := c.Do(ctx, http.MethodGet, "queue", q, nil, &page); err != nil { return nil, err } return page.Records, nil } // RemoveQueueItem removes an item from the queue and the download client. // With blocklist set, the release is blocklisted and a replacement is searched. func (c *Client) RemoveQueueItem(ctx context.Context, id int, blocklist bool) error { q := url.Values{ "removeFromClient": {"true"}, "blocklist": {strconv.FormatBool(blocklist)}, } return c.Do(ctx, http.MethodDelete, "queue/"+strconv.Itoa(id), q, nil, nil) }