Publish current app and server
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/radarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/recommend"
|
||||
)
|
||||
|
||||
const radarrCalendarCachePrefix = "radarr:calendar:v2:"
|
||||
const radarrScheduleDays = 5
|
||||
const radarrTheatricalDelayDays = 30
|
||||
|
||||
type radarrRelease struct {
|
||||
at time.Time
|
||||
estimated bool
|
||||
}
|
||||
|
||||
type radarrScheduleItem struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
Overview string `json:"Overview,omitempty"`
|
||||
ProductionYear int `json:"ProductionYear,omitempty"`
|
||||
RunTimeTicks int64 `json:"RunTimeTicks,omitempty"`
|
||||
Genres []string `json:"Genres"`
|
||||
ImageTags map[string]string `json:"ImageTags"`
|
||||
BackdropImageTags []string `json:"BackdropImageTags"`
|
||||
MembySource string `json:"MembySource"`
|
||||
MembyAirsAt string `json:"MembyAirsAt"`
|
||||
MembyAirDayLabel string `json:"MembyAirDayLabel"`
|
||||
MembyAirLabel string `json:"MembyAirLabel"`
|
||||
MembyAvailability string `json:"MembyAvailability"`
|
||||
MembyAvailabilityText string `json:"MembyAvailabilityText"`
|
||||
MembyPlayable bool `json:"MembyPlayable"`
|
||||
}
|
||||
|
||||
func (s *Server) radarrUpcomingMoviesRow(ctx context.Context) (*recommend.Row, error) {
|
||||
if s.radarr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
location := s.cfg.RadarrLocation
|
||||
if location == nil {
|
||||
location = time.Local
|
||||
}
|
||||
now := time.Now().In(location)
|
||||
dayStart := localDayStart(now, location)
|
||||
cacheKey := radarrCalendarCachePrefix + dayStart.Format("2006-01-02")
|
||||
if row := s.cachedRadarrRow(ctx, cacheKey); row != nil {
|
||||
return row, nil
|
||||
}
|
||||
|
||||
s.radarrMu.Lock()
|
||||
defer s.radarrMu.Unlock()
|
||||
if row := s.cachedRadarrRow(ctx, cacheKey); row != nil {
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// A cinema release can stand in for an unknown digital date at cinema + 30 days,
|
||||
// so include the preceding 30 days in the Radarr query. buildRadarrRow applies the
|
||||
// actual five-day effective-release window after the response arrives.
|
||||
movies, err := s.radarr.Calendar(
|
||||
ctx,
|
||||
dayStart.AddDate(0, 0, -radarrTheatricalDelayDays),
|
||||
dayStart.AddDate(0, 0, radarrScheduleDays),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, err := buildRadarrRow(movies, now, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body, marshalErr := json.Marshal(row); marshalErr == nil {
|
||||
if cacheErr := s.cache.Set(ctx, cacheKey, body, s.cfg.RadarrTTL); cacheErr != nil {
|
||||
s.log.Warn("radarr calendar cache write failed", "error", cacheErr)
|
||||
}
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *Server) cachedRadarrRow(ctx context.Context, key string) *recommend.Row {
|
||||
raw, err := s.cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var row recommend.Row
|
||||
if json.Unmarshal(raw, &row) != nil {
|
||||
return nil
|
||||
}
|
||||
return &row
|
||||
}
|
||||
|
||||
func buildRadarrRow(movies []radarr.Movie, now time.Time, location *time.Location) (*recommend.Row, error) {
|
||||
sort.SliceStable(movies, func(i, j int) bool {
|
||||
left, leftOK := effectiveRadarrRelease(movies[i])
|
||||
right, rightOK := effectiveRadarrRelease(movies[j])
|
||||
if !leftOK {
|
||||
return false
|
||||
}
|
||||
if !rightOK {
|
||||
return true
|
||||
}
|
||||
return left.at.Before(right.at)
|
||||
})
|
||||
|
||||
items := make([]json.RawMessage, 0, len(movies))
|
||||
dayStart := localDayStart(now, location)
|
||||
windowEnd := dayStart.AddDate(0, 0, radarrScheduleDays)
|
||||
for _, movie := range movies {
|
||||
release, ok := effectiveRadarrRelease(movie)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
localRelease := release.at.In(location)
|
||||
if localRelease.Before(dayStart) || !localRelease.Before(windowEnd) {
|
||||
continue
|
||||
}
|
||||
raw, err := json.Marshal(toRadarrScheduleItem(movie, release, now, location))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, raw)
|
||||
}
|
||||
return &recommend.Row{
|
||||
ID: "radarr-upcoming-movies",
|
||||
Title: "Upcoming Movie releases",
|
||||
Kind: "movie-schedule",
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// effectiveRadarrRelease prefers Radarr's actual digital date. Cinema + 30 days is
|
||||
// only a fallback when Radarr has no digital date at all. In particular, an old movie
|
||||
// with an old digital date cannot appear because of a newer cinema/re-release date.
|
||||
func effectiveRadarrRelease(movie radarr.Movie) (radarrRelease, bool) {
|
||||
if movie.DigitalRelease != nil {
|
||||
return radarrRelease{at: *movie.DigitalRelease}, true
|
||||
}
|
||||
if movie.InCinemas != nil {
|
||||
return radarrRelease{
|
||||
at: movie.InCinemas.AddDate(0, 0, radarrTheatricalDelayDays),
|
||||
estimated: true,
|
||||
}, true
|
||||
}
|
||||
return radarrRelease{}, false
|
||||
}
|
||||
|
||||
func toRadarrScheduleItem(movie radarr.Movie, release radarrRelease, now time.Time, location *time.Location) radarrScheduleItem {
|
||||
availabilityText := "Upcoming digital release"
|
||||
if release.estimated {
|
||||
availabilityText = "Estimated digital release"
|
||||
}
|
||||
item := radarrScheduleItem{
|
||||
ID: fmt.Sprintf("radarr:%d", movie.ID),
|
||||
Name: movie.Title,
|
||||
Type: "MembyRadarrMovie",
|
||||
Overview: movie.Overview,
|
||||
ProductionYear: movie.Year,
|
||||
RunTimeTicks: int64(movie.Runtime) * 600_000_000,
|
||||
Genres: nonNilStrings(movie.Genres),
|
||||
ImageTags: map[string]string{},
|
||||
BackdropImageTags: []string{},
|
||||
MembySource: "radarr",
|
||||
MembyAvailability: "upcoming",
|
||||
MembyAvailabilityText: availabilityText,
|
||||
MembyPlayable: false,
|
||||
}
|
||||
if hasRadarrCover(movie.Images, "poster") {
|
||||
item.ImageTags["Primary"] = "radarr"
|
||||
}
|
||||
if hasRadarrCover(movie.Images, "fanart") {
|
||||
item.BackdropImageTags = []string{"radarr"}
|
||||
}
|
||||
localRelease := release.at.In(location)
|
||||
item.MembyAirsAt = localRelease.Format(time.RFC3339)
|
||||
item.MembyAirDayLabel = scheduleAirDayLabel(localRelease, now, location)
|
||||
item.MembyAirLabel = digitalReleaseLabel(localRelease, now, location, release.estimated)
|
||||
switch {
|
||||
case movie.HasFile:
|
||||
item.MembyAvailability = "available"
|
||||
item.MembyAvailabilityText = "Downloaded"
|
||||
if movie.MovieFile != nil && movie.MovieFile.DateAdded != nil {
|
||||
item.MembyAvailabilityText = "Added at " +
|
||||
movie.MovieFile.DateAdded.In(location).Format("3:04 PM")
|
||||
}
|
||||
case !movie.Monitored:
|
||||
item.MembyAvailability = "unmonitored"
|
||||
item.MembyAvailabilityText = "Not monitored"
|
||||
case release.at.Before(now):
|
||||
item.MembyAvailability = "awaiting"
|
||||
item.MembyAvailabilityText = "Awaiting download"
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func digitalReleaseLabel(release, now time.Time, location *time.Location, estimated bool) string {
|
||||
release = release.In(location)
|
||||
now = now.In(location)
|
||||
today := localDayStart(now, location)
|
||||
releaseDay := localDayStart(release, location)
|
||||
prefix := "Digital release "
|
||||
if estimated {
|
||||
prefix = "Estimated digital release "
|
||||
}
|
||||
switch {
|
||||
case releaseDay.Equal(today):
|
||||
return prefix + "today"
|
||||
case releaseDay.Equal(today.AddDate(0, 0, 1)):
|
||||
return prefix + "tomorrow"
|
||||
default:
|
||||
return prefix + release.Format("Monday")
|
||||
}
|
||||
}
|
||||
|
||||
func hasRadarrCover(images []radarr.Image, coverType string) bool {
|
||||
for _, image := range images {
|
||||
if strings.EqualFold(image.CoverType, coverType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user