Big changes
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Kinds are part of the wire contract: an unknown kind is rendered with the generic
|
||||
// banner rather than dropped, so adding one never needs an app release.
|
||||
const (
|
||||
alertKindSonarrAired = "sonarr-aired"
|
||||
|
||||
// A TV shows one banner at a time; more than a few queued up is noise, not news.
|
||||
maxAlerts = 3
|
||||
)
|
||||
|
||||
// clientAlert is a short-lived, informational nudge delivered on the /v1/status poll —
|
||||
// the only channel the app already listens to while it is open. It carries no action:
|
||||
// the client slides it in, shows it for a few seconds and forgets it.
|
||||
type clientAlert struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
ItemID string `json:"itemId,omitempty"`
|
||||
ImageTag string `json:"imageTag,omitempty"`
|
||||
AiredAt string `json:"airedAt,omitempty"`
|
||||
}
|
||||
|
||||
// sonarrAiredAlerts reads the day's calendar through the same cache the airing-today row
|
||||
// uses, so polling clients never cost a Sonarr request of their own.
|
||||
func (s *Server) sonarrAiredAlerts(ctx context.Context) []clientAlert {
|
||||
if s.sonarr == nil || s.cfg.SonarrAlertWindow <= 0 {
|
||||
return nil
|
||||
}
|
||||
row, err := s.sonarrAiringTodayRow(ctx)
|
||||
if err != nil {
|
||||
s.log.Warn("sonarr alerts unavailable", "error", err)
|
||||
return nil
|
||||
}
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
items := make([]sonarrScheduleItem, 0, len(row.Items))
|
||||
for _, raw := range row.Items {
|
||||
var item sonarrScheduleItem
|
||||
if json.Unmarshal(raw, &item) != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
location := s.cfg.SonarrLocation
|
||||
if location == nil {
|
||||
location = time.Local
|
||||
}
|
||||
return buildSonarrAlerts(items, time.Now().In(location), s.cfg.SonarrAlertWindow)
|
||||
}
|
||||
|
||||
// buildSonarrAlerts announces episodes that have aired but are not in Emby yet — the
|
||||
// gap the viewer would otherwise experience as "it's Tuesday, so why isn't it there".
|
||||
// Anything already downloaded is deliberately silent: it is on the home screen, which
|
||||
// says it better than a banner would.
|
||||
func buildSonarrAlerts(items []sonarrScheduleItem, now time.Time, window time.Duration) []clientAlert {
|
||||
if window <= 0 {
|
||||
return nil
|
||||
}
|
||||
type dated struct {
|
||||
alert clientAlert
|
||||
airs time.Time
|
||||
}
|
||||
found := make([]dated, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.MembyAirsAt == "" {
|
||||
continue
|
||||
}
|
||||
airsAt, err := time.Parse(time.RFC3339, item.MembyAirsAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Not yet aired, or aired so long ago that saying so is no longer news.
|
||||
if airsAt.After(now) || now.Sub(airsAt) > window {
|
||||
continue
|
||||
}
|
||||
message, ok := airedMessage(item, airsAt)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
found = append(found, dated{
|
||||
alert: clientAlert{
|
||||
ID: item.ID + ":aired",
|
||||
Kind: alertKindSonarrAired,
|
||||
Title: item.Name,
|
||||
Message: message,
|
||||
ItemID: item.ID,
|
||||
ImageTag: item.ImageTags["Primary"],
|
||||
AiredAt: item.MembyAirsAt,
|
||||
},
|
||||
airs: airsAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Newest first: if several aired inside the window, the most recent is the one the
|
||||
// viewer is most likely to be waiting on.
|
||||
sort.SliceStable(found, func(i, j int) bool { return found[i].airs.After(found[j].airs) })
|
||||
if len(found) > maxAlerts {
|
||||
found = found[:maxAlerts]
|
||||
}
|
||||
alerts := make([]clientAlert, 0, len(found))
|
||||
for _, entry := range found {
|
||||
alerts = append(alerts, entry.alert)
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
func airedMessage(item sonarrScheduleItem, airsAt time.Time) (string, bool) {
|
||||
episode := item.MembyEpisodeCode
|
||||
if item.MembyEpisodeTitle != "" {
|
||||
episode = fmt.Sprintf("%s — %s", episode, item.MembyEpisodeTitle)
|
||||
}
|
||||
switch item.MembyAvailability {
|
||||
case "downloading":
|
||||
return fmt.Sprintf("%s aired at %s and is downloading now.", episode, airsAt.Format("3:04 PM")), true
|
||||
case "awaiting":
|
||||
return fmt.Sprintf("%s aired at %s and will be in Emby soon.", episode, airsAt.Format("3:04 PM")), true
|
||||
default:
|
||||
// available (already watchable) and unmonitored (never coming) both have
|
||||
// nothing useful to announce.
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user