0.2.58 - Requests module
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/radarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
@@ -24,6 +25,21 @@ type requestCandidate struct {
|
||||
PosterURL string `json:"posterUrl,omitempty"`
|
||||
AlreadyAdded bool `json:"alreadyAdded"`
|
||||
InLibrary bool `json:"inLibrary"`
|
||||
// What pressing this card would mean, said once by the server so the television never
|
||||
// has to work it out from three booleans and get a different answer than the page next
|
||||
// door. Mine is this viewer's own ask, which alreadyAdded cannot distinguish — the
|
||||
// household adding a film is not the same as you having asked for it.
|
||||
Status string `json:"status"`
|
||||
StatusLabel string `json:"statusLabel"`
|
||||
Mine bool `json:"mine"`
|
||||
// Released feeds the status rule and is kept on the wire for the same reason the
|
||||
// lifecycle slugs are: it is evidence, and a later build may want to word it better.
|
||||
Released bool `json:"released"`
|
||||
// hasFile is the *arr's own answer about media on disk, which is a different claim from
|
||||
// InLibrary — Radarr can hold a downloaded film Emby has not imported yet. Unexported
|
||||
// because it only feeds the status rule; the television is told the verdict, not the
|
||||
// evidence behind it.
|
||||
hasFile bool
|
||||
}
|
||||
|
||||
type requestLookupResponse struct {
|
||||
@@ -73,6 +89,10 @@ func (s *Server) handleRequestLookup(w http.ResponseWriter, r *http.Request, ses
|
||||
MediaType: "movie", ForeignID: movie.TMDBID, Title: movie.Title,
|
||||
Year: movie.Year, Overview: movie.Overview,
|
||||
PosterURL: radarrCoverURL(movie.Images, "poster"), AlreadyAdded: movie.ID > 0,
|
||||
// Radarr's lookup fills these in for a title it already tracks; for one it
|
||||
// does not, hasFile is false and the status rule never reads Released.
|
||||
hasFile: movie.HasFile,
|
||||
Released: movieReleased(movie.Status),
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -94,6 +114,7 @@ func (s *Server) handleRequestLookup(w http.ResponseWriter, r *http.Request, ses
|
||||
MediaType: "series", ForeignID: show.TVDBID, Title: show.Title,
|
||||
Year: show.Year, Overview: show.Overview,
|
||||
PosterURL: sonarrCoverURL(show.Images, "poster"), AlreadyAdded: show.ID > 0,
|
||||
Released: seriesReleased(show.Status, show.NextAiring, time.Now()),
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -114,12 +135,32 @@ func (s *Server) handleRequestLookup(w http.ResponseWriter, r *http.Request, ses
|
||||
s.loggerFor(r.Context()).Warn("request library status unavailable",
|
||||
"movie_error", movieErr, "series_error", seriesErr)
|
||||
}
|
||||
for index := range candidates {
|
||||
if candidates[index].MediaType == "movie" {
|
||||
candidates[index].InLibrary = moviesInLibrary[candidates[index].ForeignID]
|
||||
} else {
|
||||
candidates[index].InLibrary = seriesInLibrary[candidates[index].ForeignID]
|
||||
// Which of these the viewer has already asked for themselves. A failure here costs the
|
||||
// "Requested" wording and nothing else, so it is not allowed to fail the search.
|
||||
mine := map[string]bool{}
|
||||
if stored, err := s.store.MediaRequests(r.Context(), sess.EmbyUserID); err == nil {
|
||||
for _, req := range stored {
|
||||
mine[req.MediaType+":"+strconv.Itoa(req.ForeignID)] = true
|
||||
}
|
||||
} else {
|
||||
s.loggerFor(r.Context()).Warn("own requests unavailable for lookup", "error", err)
|
||||
}
|
||||
|
||||
for index := range candidates {
|
||||
candidate := &candidates[index]
|
||||
if candidate.MediaType == "movie" {
|
||||
candidate.InLibrary = moviesInLibrary[candidate.ForeignID]
|
||||
} else {
|
||||
candidate.InLibrary = seriesInLibrary[candidate.ForeignID]
|
||||
}
|
||||
candidate.Mine = mine[candidate.MediaType+":"+strconv.Itoa(candidate.ForeignID)]
|
||||
candidate.Status = lookupStatusFor(RequestSubject{
|
||||
Tracked: candidate.AlreadyAdded,
|
||||
HasFile: candidate.hasFile,
|
||||
InLibrary: candidate.InLibrary,
|
||||
Released: candidate.Released,
|
||||
}, candidate.Mine)
|
||||
candidate.StatusLabel = requestStatusLabel(candidate.Status)
|
||||
}
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
return requestMatchScore(term, candidates[i].Title) < requestMatchScore(term, candidates[j].Title)
|
||||
@@ -213,6 +254,11 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
|
||||
// a connection reset. If the first request already added it, the retry is the
|
||||
// same successful action rather than an error shown to the viewer.
|
||||
req.Title = movie.Title
|
||||
// Still recorded as this viewer's ask. The household having the film already
|
||||
// is not the same as them never having asked for it, and their page is the
|
||||
// only place that distinction is kept.
|
||||
s.recordMediaRequest(r.Context(), sess, req, movie.Year,
|
||||
radarrCoverURL(movie.Images, "poster"))
|
||||
s.logMediaRequest(r.Context(), req, "already added", nil)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "already_added", "title": movie.Title})
|
||||
return
|
||||
@@ -225,6 +271,8 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
|
||||
return
|
||||
}
|
||||
req.Title = added.Title
|
||||
s.recordMediaRequest(r.Context(), sess, req, added.Year,
|
||||
radarrCoverURL(added.Images, "poster"))
|
||||
s.logMediaRequest(r.Context(), req, "successful", nil)
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"status": "requested", "title": added.Title})
|
||||
return
|
||||
@@ -247,6 +295,8 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
|
||||
}
|
||||
if show.ID > 0 {
|
||||
req.Title = show.Title
|
||||
s.recordMediaRequest(r.Context(), sess, req, show.Year,
|
||||
sonarrCoverURL(show.Images, "poster"))
|
||||
s.logMediaRequest(r.Context(), req, "already added", nil)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "already_added", "title": show.Title})
|
||||
return
|
||||
@@ -259,6 +309,8 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
|
||||
return
|
||||
}
|
||||
req.Title = added.Title
|
||||
s.recordMediaRequest(r.Context(), sess, req, added.Year,
|
||||
sonarrCoverURL(added.Images, "poster"))
|
||||
s.logMediaRequest(r.Context(), req, "successful", nil)
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"status": "requested", "title": added.Title})
|
||||
return
|
||||
@@ -272,6 +324,30 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request, sess stor
|
||||
writeError(w, http.StatusNotFound, "title was not found")
|
||||
}
|
||||
|
||||
// recordMediaRequest writes down who asked, which is the one thing Radarr and Sonarr do not
|
||||
// keep. It is deliberately best-effort: the title has already been added by the time this
|
||||
// runs, so failing the response here would tell a viewer their request did not work when it
|
||||
// did. The cost of a lost write is that the ask is missing from their own page, which is
|
||||
// recoverable by asking again — the cost of the opposite is a viewer requesting it twice.
|
||||
func (s *Server) recordMediaRequest(
|
||||
ctx context.Context, sess store.Session, req requestPayload, year int, posterURL string,
|
||||
) {
|
||||
if s.store == nil || sess.EmbyUserID == "" {
|
||||
return
|
||||
}
|
||||
err := s.store.SaveMediaRequest(ctx, sess.EmbyUserID, store.MediaRequest{
|
||||
MediaType: req.MediaType,
|
||||
ForeignID: req.ForeignID,
|
||||
Title: req.Title,
|
||||
Year: year,
|
||||
PosterURL: posterURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("media request not recorded",
|
||||
"type", req.MediaType, "foreign_id", req.ForeignID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) logMediaRequest(
|
||||
ctx context.Context, req requestPayload, outcome string, err error,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user