0.2.78
This commit is contained in:
@@ -3,11 +3,14 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/radarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -25,6 +28,15 @@ type myRequest struct {
|
||||
Status string `json:"status"`
|
||||
StatusLabel string `json:"statusLabel"`
|
||||
StatusDetail string `json:"statusDetail"`
|
||||
// Progress is whole percent and rides only a downloading card. It is omitted rather
|
||||
// than sent as zero everywhere else, so the television draws a bar when it is given a
|
||||
// figure and nothing at all when it is not — there is no "0%" state to confuse with a
|
||||
// download that has not started.
|
||||
Progress int `json:"progress,omitempty"`
|
||||
// EstimatedReadySeconds is how long the download client says the bytes will take, and is
|
||||
// omitted whenever nothing could say. That omission is the whole of "never fabricate an
|
||||
// ETA": a card with no number here prints the plain wording in StatusDetail instead.
|
||||
EstimatedReadySeconds int `json:"estimatedReadySeconds,omitempty"`
|
||||
// EmbyItemID is set once Emby has imported the title, so the card can open the ordinary
|
||||
// detail page instead of being a dead end at the moment it finally becomes watchable.
|
||||
EmbyItemID string `json:"embyItemId,omitempty"`
|
||||
@@ -49,21 +61,36 @@ func (s *Server) handleMyRequests(w http.ResponseWriter, r *http.Request, sess s
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, myRequestsResponse{
|
||||
Requests: s.decorateRequests(r.Context(), stored),
|
||||
Requests: s.decorateRequests(r.Context(), stored, requestProgressSupported(r)),
|
||||
Allowed: true,
|
||||
})
|
||||
}
|
||||
|
||||
// requestProgressSupported asks whether this television can draw the download states.
|
||||
//
|
||||
// A capability rather than a version floor, because that is what the client already declares
|
||||
// and what the console reports against — and because the answer is about what the app can
|
||||
// *draw*, which is exactly what a capability token says. An older build is sent the collapsed
|
||||
// vocabulary it understands (collapseRequestStatus) rather than four words it would file
|
||||
// under "Nothing happening" and a percentage it has nowhere to put.
|
||||
func requestProgressSupported(r *http.Request) bool {
|
||||
return slices.Contains(clientCapabilities(r), "request_progress_v1")
|
||||
}
|
||||
|
||||
// decorateRequests turns stored asks into cards by asking the two catalogues and the
|
||||
// library what has become of each.
|
||||
//
|
||||
// The three lookups run concurrently and every one of them is allowed to fail: a request
|
||||
// whose state cannot be established falls back to "requested", which is the honest answer —
|
||||
// The lookups run concurrently and every one of them is allowed to fail: a request whose
|
||||
// state cannot be established falls back to "requested", which is the honest answer —
|
||||
// somebody asked and we cannot currently say more. A page that errored because Radarr was
|
||||
// restarting would be a page that is broken exactly when a viewer wants to know why their
|
||||
// title has not arrived.
|
||||
// title has not arrived. The download queues degrade one step further and more gently
|
||||
// still: losing them costs the percentage and the estimate, and the card falls back to
|
||||
// "Searching", which is what the page said before there was a queue reader at all.
|
||||
//
|
||||
// progressAware is what this television can draw; see requestProgressSupported.
|
||||
func (s *Server) decorateRequests(
|
||||
ctx context.Context, stored []store.MediaRequest,
|
||||
ctx context.Context, stored []store.MediaRequest, progressAware bool,
|
||||
) []myRequest {
|
||||
if len(stored) == 0 {
|
||||
return []myRequest{}
|
||||
@@ -85,6 +112,14 @@ func (s *Server) decorateRequests(
|
||||
seriesInLibrary = map[int]bool{}
|
||||
movieItemIDs = map[int]string{}
|
||||
seriesItemIDs = map[int]string{}
|
||||
// The two halves of the queue join, gathered separately and put together after the
|
||||
// wait: a queue row names the *arr's own internal id, and only the catalogue can say
|
||||
// which TMDb or TVDb id that is. Fetching them concurrently and joining afterwards is
|
||||
// what keeps the queue read off the catalogue's critical path.
|
||||
tmdbByMovieID = map[int]int{}
|
||||
tvdbBySeriesID = map[int]int{}
|
||||
movieQueue []radarr.QueueItem
|
||||
seriesQueue []sonarr.QueueItem
|
||||
)
|
||||
now := time.Now()
|
||||
|
||||
@@ -101,6 +136,7 @@ func (s *Server) decorateRequests(
|
||||
if movie.TMDBID == 0 {
|
||||
continue
|
||||
}
|
||||
tmdbByMovieID[movie.ID] = movie.TMDBID
|
||||
movies[movie.TMDBID] = requestCatalogueEntry{
|
||||
tracked: true,
|
||||
hasFile: movie.HasFile,
|
||||
@@ -110,6 +146,16 @@ func (s *Server) decorateRequests(
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
queue, err := s.radarrQueue(ctx)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("radarr queue unavailable for requests", "error", err)
|
||||
return
|
||||
}
|
||||
movieQueue = queue
|
||||
}()
|
||||
}
|
||||
if len(seriesIDs) > 0 && s.sonarrEnabled(ctx) {
|
||||
wg.Add(1)
|
||||
@@ -124,6 +170,7 @@ func (s *Server) decorateRequests(
|
||||
if show.TVDBID == 0 {
|
||||
continue
|
||||
}
|
||||
tvdbBySeriesID[show.ID] = show.TVDBID
|
||||
series[show.TVDBID] = requestCatalogueEntry{
|
||||
tracked: true,
|
||||
released: seriesReleased(show.Status, show.NextAiring, now),
|
||||
@@ -132,6 +179,16 @@ func (s *Server) decorateRequests(
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
queue, err := s.sonarrQueue(ctx)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("sonarr queue unavailable for requests", "error", err)
|
||||
return
|
||||
}
|
||||
seriesQueue = queue
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -148,12 +205,18 @@ func (s *Server) decorateRequests(
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
movieWork := groupMovieWork(movieQueue, tmdbByMovieID)
|
||||
seriesWork := groupSeriesWork(seriesQueue, tvdbBySeriesID)
|
||||
|
||||
cards := make([]myRequest, 0, len(stored))
|
||||
for _, req := range stored {
|
||||
entry, inLibrary, itemID := movies[req.ForeignID], moviesInLibrary[req.ForeignID], movieItemIDs[req.ForeignID]
|
||||
work := movieWork[req.ForeignID]
|
||||
if req.MediaType == "series" {
|
||||
entry, inLibrary, itemID = series[req.ForeignID], seriesInLibrary[req.ForeignID], seriesItemIDs[req.ForeignID]
|
||||
work = seriesWork[req.ForeignID]
|
||||
}
|
||||
progress := downloadProgress(work)
|
||||
status := RequestStatusRequested
|
||||
// Only claim a state when something actually answered. With every catalogue down,
|
||||
// "requested" is all that is known and is what the card must say.
|
||||
@@ -163,13 +226,14 @@ func (s *Server) decorateRequests(
|
||||
HasFile: entry.hasFile,
|
||||
InLibrary: inLibrary,
|
||||
Released: entry.released,
|
||||
Progress: progress,
|
||||
})
|
||||
}
|
||||
poster := req.PosterURL
|
||||
if poster == "" {
|
||||
poster = entry.poster
|
||||
}
|
||||
cards = append(cards, myRequest{
|
||||
card := myRequest{
|
||||
MediaType: req.MediaType,
|
||||
ForeignID: req.ForeignID,
|
||||
Title: req.Title,
|
||||
@@ -179,9 +243,27 @@ func (s *Server) decorateRequests(
|
||||
RequestedAt: req.RequestedAt.UTC().Format(time.RFC3339),
|
||||
Status: status,
|
||||
StatusLabel: requestStatusLabel(status),
|
||||
StatusDetail: requestStatusDetail(status, req.MediaType),
|
||||
StatusDetail: requestStatusDetail(status, req.MediaType, progress.EstimatedReadySeconds),
|
||||
EmbyItemID: itemID,
|
||||
})
|
||||
}
|
||||
// The figures belong to a download and to nothing else. A title that is available,
|
||||
// pending or unavailable may still have a row in the queue — an upgrade, a stale
|
||||
// entry — and pinning a percentage to it would put a progress bar under a card
|
||||
// somebody can already press Play on.
|
||||
if status == RequestStatusDownloading {
|
||||
card.Progress = progress.Progress
|
||||
card.EstimatedReadySeconds = progress.EstimatedReadySeconds
|
||||
}
|
||||
if !progressAware {
|
||||
// Narrowed on the way out rather than on the way in, so the label and the
|
||||
// detail are still computed from the truth and only the *slug* is generalised.
|
||||
// That is the better half of the trade: an older television files the card under
|
||||
// "On the way" as it always did, and still reads "Downloading" on the chip and
|
||||
// the honest sentence underneath it. Only the bar and the number go.
|
||||
card.Status = collapseRequestStatus(card.Status)
|
||||
card.Progress, card.EstimatedReadySeconds = 0, 0
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user