0.2.78
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/radarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
)
|
||||
|
||||
// Reading the download queues, which is the one part of the request page that cannot be
|
||||
// cached for long and is read by every television in the house at once.
|
||||
//
|
||||
// The two *arr catalogues beside this are cached for the day: what Radarr holds and whether
|
||||
// it has a file changes a handful of times a day, and a viewer finding out at midnight costs
|
||||
// nothing. A queue is the opposite — it is the number moving on the card — so it carries its
|
||||
// own short cache instead. requestQueueTTL is what stops four televisions on the launcher
|
||||
// turning a page refresh into four requests apiece: within the window they share one answer,
|
||||
// and past it the figure is stale by at most that window, which at this cadence is invisible
|
||||
// against a download measured in minutes.
|
||||
const (
|
||||
radarrQueueCacheKey = "radarr:queue:v1"
|
||||
sonarrQueueCacheKey = "sonarr:queue:v1"
|
||||
requestQueueTTL = 15 * time.Second
|
||||
)
|
||||
|
||||
// radarrQueue is what Radarr's download client is working on.
|
||||
//
|
||||
// Single-flighted behind the same mutex the catalogue uses, so a burst of polls costs one
|
||||
// upstream read. A miss is not an error the page can be failed over: see decorateRequests,
|
||||
// where a queue that will not answer costs the percentage and never the card.
|
||||
func (s *Server) radarrQueue(ctx context.Context) ([]radarr.QueueItem, error) {
|
||||
if !s.radarrEnabled(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
if queue, ok := cachedQueue[radarr.QueueItem](ctx, s, radarrQueueCacheKey); ok {
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
s.radarrMu.Lock()
|
||||
defer s.radarrMu.Unlock()
|
||||
if queue, ok := cachedQueue[radarr.QueueItem](ctx, s, radarrQueueCacheKey); ok {
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
queue, err := s.radarr.Queue(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.cacheQueue(ctx, radarrQueueCacheKey, queue)
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
// sonarrQueue is what Sonarr's download client is working on.
|
||||
func (s *Server) sonarrQueue(ctx context.Context) ([]sonarr.QueueItem, error) {
|
||||
if !s.sonarrEnabled(ctx) {
|
||||
return nil, nil
|
||||
}
|
||||
if queue, ok := cachedQueue[sonarr.QueueItem](ctx, s, sonarrQueueCacheKey); ok {
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
s.sonarrSeriesMu.Lock()
|
||||
defer s.sonarrSeriesMu.Unlock()
|
||||
if queue, ok := cachedQueue[sonarr.QueueItem](ctx, s, sonarrQueueCacheKey); ok {
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
queue, err := s.sonarr.Queue(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.cacheQueue(ctx, sonarrQueueCacheKey, queue)
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
// cachedQueue reads a stored queue. An empty queue is a real and common answer — most of the
|
||||
// time the household is downloading nothing — so the second return distinguishes "nothing is
|
||||
// stored" from "nothing is downloading", which a nil slice could not.
|
||||
func cachedQueue[T any](ctx context.Context, s *Server, key string) ([]T, bool) {
|
||||
if s.cache == nil {
|
||||
return nil, false
|
||||
}
|
||||
raw, err := s.cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var queue []T
|
||||
if err := json.Unmarshal(raw, &queue); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if queue == nil {
|
||||
queue = []T{}
|
||||
}
|
||||
return queue, true
|
||||
}
|
||||
|
||||
func (s *Server) cacheQueue(ctx context.Context, key string, queue any) {
|
||||
if s.cache == nil {
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(queue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, requestQueueTTL); err != nil {
|
||||
s.loggerFor(ctx).Warn("download queue cache write failed", "key", key, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// groupMovieWork turns Radarr's queue into work per *TMDb* id.
|
||||
//
|
||||
// The translation is the point. A queue row names Radarr's own movie id, which is an
|
||||
// internal number nothing outside Radarr has ever seen; a request is recorded against the
|
||||
// TMDb id, which is what the catalogue, the library and Emby all agree on. The map comes
|
||||
// from the catalogue the caller already has, so this costs no extra request — and a row
|
||||
// whose film is not in that map is dropped rather than guessed at, which is what a download
|
||||
// for something added since the catalogue was cached looks like.
|
||||
func groupMovieWork(queue []radarr.QueueItem, tmdbByMovieID map[int]int) map[int][]requestWork {
|
||||
work := map[int][]requestWork{}
|
||||
for _, row := range queue {
|
||||
tmdbID, ok := tmdbByMovieID[row.MovieID]
|
||||
if !ok || tmdbID == 0 {
|
||||
continue
|
||||
}
|
||||
work[tmdbID] = append(work[tmdbID], requestWork{
|
||||
Size: row.Size,
|
||||
SizeLeft: row.Sizeleft,
|
||||
TimeLeft: row.Timeleft,
|
||||
Status: row.Status,
|
||||
TrackedState: row.TrackedDownloadState,
|
||||
TrackedStatus: row.TrackedDownloadStatus,
|
||||
})
|
||||
}
|
||||
return work
|
||||
}
|
||||
|
||||
// groupSeriesWork turns Sonarr's queue into work per TVDb id.
|
||||
//
|
||||
// A show legitimately has many rows — a season pack is one row per episode — and they are
|
||||
// kept as a list rather than reduced here, because downloadProgress is what decides how a
|
||||
// dozen episodes at a dozen stages become one sentence.
|
||||
func groupSeriesWork(queue []sonarr.QueueItem, tvdbBySeriesID map[int]int) map[int][]requestWork {
|
||||
work := map[int][]requestWork{}
|
||||
for _, row := range queue {
|
||||
tvdbID, ok := tvdbBySeriesID[row.SeriesID]
|
||||
if !ok || tvdbID == 0 {
|
||||
continue
|
||||
}
|
||||
work[tvdbID] = append(work[tvdbID], requestWork{
|
||||
Size: row.Size,
|
||||
SizeLeft: row.Sizeleft,
|
||||
TimeLeft: row.Timeleft,
|
||||
Status: row.Status,
|
||||
TrackedState: row.TrackedDownloadState,
|
||||
TrackedStatus: row.TrackedDownloadStatus,
|
||||
})
|
||||
}
|
||||
return work
|
||||
}
|
||||
Reference in New Issue
Block a user