Publish current app and server
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/cache"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -17,6 +21,22 @@ type flagRequest struct {
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
type seasonFinaleResponse struct {
|
||||
SeasonFinale bool `json:"seasonFinale"`
|
||||
SeriesName string `json:"seriesName,omitempty"`
|
||||
SeasonNumber int `json:"seasonNumber,omitempty"`
|
||||
EpisodeNumber int `json:"episodeNumber,omitempty"`
|
||||
}
|
||||
|
||||
type finaleEmbyItem struct {
|
||||
Type string `json:"Type"`
|
||||
SeriesID string `json:"SeriesId"`
|
||||
SeriesName string `json:"SeriesName"`
|
||||
ParentIndexNumber int `json:"ParentIndexNumber"`
|
||||
IndexNumber int `json:"IndexNumber"`
|
||||
ProviderIDs map[string]string `json:"ProviderIds"`
|
||||
}
|
||||
|
||||
// handleItem serves full metadata for one item. The TV asks for this only after D-pad
|
||||
// focus settles, so it is worth caching for longer than a home row.
|
||||
func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
@@ -28,7 +48,7 @@ func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
}
|
||||
// Version the entry when the detail contract grows so older cached payloads cannot
|
||||
// hide newly requested fields such as People.
|
||||
key := cache.UserKey(sess.EmbyUserID, "item:v2:"+itemID)
|
||||
key := cache.UserKey(sess.EmbyUserID, "item:v3:"+itemID)
|
||||
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
@@ -48,6 +68,131 @@ func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.S
|
||||
writeRaw(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
// handleSeasonFinale verifies an episode against Sonarr's complete season, including
|
||||
// future episodes. Looking only at Emby's downloaded files would call every current
|
||||
// weekly episode a finale until the following episode arrived.
|
||||
func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
ctx := r.Context()
|
||||
itemID := r.PathValue("id")
|
||||
if itemID == "" {
|
||||
writeError(w, http.StatusBadRequest, "item id is required")
|
||||
return
|
||||
}
|
||||
empty := seasonFinaleResponse{}
|
||||
if s.sonarr == nil {
|
||||
writeJSON(w, http.StatusOK, empty)
|
||||
return
|
||||
}
|
||||
key := cache.UserKey(sess.EmbyUserID, "season-finale:v1:"+itemID)
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
|
||||
currentRaw, err := s.emby.Item(
|
||||
ctx, credentials(sess), itemID,
|
||||
"ProviderIds,SeriesId,SeriesName,ParentIndexNumber,IndexNumber",
|
||||
)
|
||||
if err != nil {
|
||||
s.writeUpstreamError(w, err, "could not inspect the playing episode")
|
||||
return
|
||||
}
|
||||
var current finaleEmbyItem
|
||||
if json.Unmarshal(currentRaw, ¤t) != nil ||
|
||||
!strings.EqualFold(current.Type, "Episode") || current.SeriesID == "" ||
|
||||
current.ParentIndexNumber <= 0 || current.IndexNumber <= 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
|
||||
seriesRaw, err := s.emby.Item(ctx, credentials(sess), current.SeriesID, "ProviderIds")
|
||||
if err != nil {
|
||||
s.log.Warn("season finale series metadata unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
var seriesItem finaleEmbyItem
|
||||
if json.Unmarshal(seriesRaw, &seriesItem) != nil {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
tvdbID, err := strconv.Atoi(providerID(seriesItem.ProviderIDs, "tvdb"))
|
||||
if err != nil || tvdbID <= 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
|
||||
series, err := s.sonarr.Series(ctx)
|
||||
if err != nil {
|
||||
s.log.Warn("season finale Sonarr series unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
sonarrSeriesID := 0
|
||||
for _, candidate := range series {
|
||||
if candidate.TVDBID == tvdbID {
|
||||
sonarrSeriesID = candidate.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if sonarrSeriesID == 0 {
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
episodes, err := s.sonarr.Episodes(ctx, sonarrSeriesID)
|
||||
if err != nil {
|
||||
s.log.Warn("season finale Sonarr episodes unavailable", "item", itemID, "error", err)
|
||||
s.writeSeasonFinaleResponse(ctx, key, empty, w)
|
||||
return
|
||||
}
|
||||
result := seasonFinaleResponse{
|
||||
SeasonFinale: isSeasonFinale(current.ParentIndexNumber, current.IndexNumber, episodes),
|
||||
SeriesName: current.SeriesName,
|
||||
SeasonNumber: current.ParentIndexNumber,
|
||||
EpisodeNumber: current.IndexNumber,
|
||||
}
|
||||
if !result.SeasonFinale {
|
||||
result = empty
|
||||
}
|
||||
s.writeSeasonFinaleResponse(ctx, key, result, w)
|
||||
}
|
||||
|
||||
func (s *Server) writeSeasonFinaleResponse(
|
||||
ctx context.Context, key string, result seasonFinaleResponse, w http.ResponseWriter,
|
||||
) {
|
||||
body, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not encode finale status")
|
||||
return
|
||||
}
|
||||
if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
|
||||
s.log.Warn("season finale cache write failed", "error", err)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
func providerID(ids map[string]string, wanted string) string {
|
||||
for key, value := range ids {
|
||||
if strings.EqualFold(key, wanted) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isSeasonFinale(seasonNumber, episodeNumber int, episodes []sonarr.Episode) bool {
|
||||
if seasonNumber <= 0 || episodeNumber <= 0 {
|
||||
return false
|
||||
}
|
||||
lastEpisode := 0
|
||||
for _, episode := range episodes {
|
||||
if episode.SeasonNumber == seasonNumber && episode.EpisodeNumber > lastEpisode {
|
||||
lastEpisode = episode.EpisodeNumber
|
||||
}
|
||||
}
|
||||
return lastEpisode > 0 && episodeNumber == lastEpisode
|
||||
}
|
||||
|
||||
// handleSeriesEpisodes supplies the complete episode browser in one cached response.
|
||||
// The TV groups by ParentIndexNumber locally, so changing seasons never reaches Emby.
|
||||
func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
@@ -154,7 +299,6 @@ func (s *Server) setFlag(
|
||||
}
|
||||
if s.forYou != nil {
|
||||
s.forYou.MarkDirty(r.Context(), sess)
|
||||
s.forYou.RefreshAsync(sess, true)
|
||||
}
|
||||
writeRaw(w, http.StatusOK, userData)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user