Files
arr-tui/internal/arr/queue.go
T
nilsandClaude Opus 5.5 2b6cf71967 Library: mark titles that are downloading
While the Library is visible it reads the Radarr and Sonarr queues (on
opening, every 10 s, and shortly after a grab) and matches them to the
titles by movieId / seriesId. Those titles get a blue dot instead of the
green or grey one, and their details say how far the download is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:04:53 +02:00

133 lines
4.4 KiB
Go

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"`
MovieID int `json:"movieId,omitempty"` // Radarr
SeriesID int `json:"seriesId,omitempty"` // Sonarr
// 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)
}