136 lines
4.8 KiB
Go
136 lines
4.8 KiB
Go
package api
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/ponzischeme89/memby/server/internal/library"
|
||
|
|
)
|
||
|
|
|
||
|
|
// News about the service itself, as opposed to news about what is in it.
|
||
|
|
//
|
||
|
|
// Both producers here answer the same question a viewer would otherwise have to guess
|
||
|
|
// at: why the library changed under them, and why playback stopped working. They are
|
||
|
|
// the alerts most worth showing *during* a film, which is why they are worded to be read
|
||
|
|
// in one glance and never ask for an action.
|
||
|
|
const (
|
||
|
|
// A refresh is only interesting while it is recent — long enough that a set switched
|
||
|
|
// on shortly afterwards still learns why there is something new on the home screen,
|
||
|
|
// short enough that it is never yesterday's news.
|
||
|
|
librarySyncAlertWindow = 30 * time.Minute
|
||
|
|
|
||
|
|
// Reachability is a live fact, so its window is short: a TV waking up half an hour
|
||
|
|
// after Emby went down should find out by asking, not by being told about the past.
|
||
|
|
reachabilityAlertWindow = 10 * time.Minute
|
||
|
|
|
||
|
|
// embyFailureThreshold is how many probes in a row must fail before it is announced.
|
||
|
|
// One timeout is a hiccup — a restart, a slow scan — and telling a room full of
|
||
|
|
// people about it is worse than saying nothing.
|
||
|
|
embyFailureThreshold = 3
|
||
|
|
)
|
||
|
|
|
||
|
|
// AnnounceLibrarySync tells every TV that the catalogue moved.
|
||
|
|
//
|
||
|
|
// Only a run that actually changed something is announced: the import is scheduled, so
|
||
|
|
// most passes find nothing, and an hourly "nothing happened" banner would train viewers
|
||
|
|
// to ignore the one that matters. Removals alone are deliberately silent — a title
|
||
|
|
// disappearing is not something to celebrate mid-film.
|
||
|
|
func (s *Server) AnnounceLibrarySync(ctx context.Context, result library.Result) {
|
||
|
|
if result.Changed <= 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
now := time.Now().UTC()
|
||
|
|
s.publishAlert(ctx, clientAlert{
|
||
|
|
// Keyed on the minute the sync finished: two runs are two pieces of news, but a
|
||
|
|
// retried publish of the same run is not.
|
||
|
|
ID: fmt.Sprintf("library:%d", now.Truncate(time.Minute).Unix()),
|
||
|
|
Kind: alertKindLibrarySync,
|
||
|
|
Label: "LIBRARY UPDATED",
|
||
|
|
Title: librarySyncTitle(result.Changed),
|
||
|
|
Message: "Memby has finished refreshing — it is on the home screen now.",
|
||
|
|
AiredAt: now.Format(time.RFC3339),
|
||
|
|
}, librarySyncAlertWindow)
|
||
|
|
}
|
||
|
|
|
||
|
|
func librarySyncTitle(changed int) string {
|
||
|
|
if changed == 1 {
|
||
|
|
return "1 title added or updated"
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%d titles added or updated", changed)
|
||
|
|
}
|
||
|
|
|
||
|
|
// WatchEmbyReachability announces Emby going away and coming back.
|
||
|
|
//
|
||
|
|
// This is the one alert that matters more during playback than on the home screen: video
|
||
|
|
// direct-plays from Emby, so when Emby stops answering a film stops with no explanation
|
||
|
|
// the viewer can act on. The gateway keeps serving /v1/status either way, which is what
|
||
|
|
// makes it able to say so.
|
||
|
|
//
|
||
|
|
// Only *transitions* are announced. A server that is down stays down, and repeating it
|
||
|
|
// every minute would bury everything else.
|
||
|
|
func (s *Server) WatchEmbyReachability(ctx context.Context, interval time.Duration) {
|
||
|
|
if interval <= 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ticker := time.NewTicker(interval)
|
||
|
|
defer ticker.Stop()
|
||
|
|
|
||
|
|
// Start out assuming reachable: a gateway booting while Emby is down should not
|
||
|
|
// open with a banner about a state nobody has seen change.
|
||
|
|
reachable := true
|
||
|
|
failures := 0
|
||
|
|
for {
|
||
|
|
select {
|
||
|
|
case <-ctx.Done():
|
||
|
|
return
|
||
|
|
case <-ticker.C:
|
||
|
|
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
|
|
err := s.emby.Ping(probeCtx)
|
||
|
|
cancel()
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
failures++
|
||
|
|
if reachable && failures >= embyFailureThreshold {
|
||
|
|
reachable = false
|
||
|
|
s.log.Warn("emby unreachable, announcing", "error", err)
|
||
|
|
s.publishAlert(ctx, s.reachabilityAlert(false), reachabilityAlertWindow)
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !reachable {
|
||
|
|
s.log.Info("emby reachable again, announcing")
|
||
|
|
s.publishAlert(ctx, s.reachabilityAlert(true), reachabilityAlertWindow)
|
||
|
|
}
|
||
|
|
reachable = true
|
||
|
|
failures = 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// reachabilityAlert is timestamped per transition, so the "back online" banner never
|
||
|
|
// collides with the "not responding" one it replaces.
|
||
|
|
func (s *Server) reachabilityAlert(up bool) clientAlert {
|
||
|
|
now := time.Now().UTC()
|
||
|
|
if up {
|
||
|
|
return clientAlert{
|
||
|
|
ID: fmt.Sprintf("emby:up:%d", now.Unix()),
|
||
|
|
Kind: alertKindServerUp,
|
||
|
|
Label: "SERVER BACK ONLINE",
|
||
|
|
Title: "Emby is responding again",
|
||
|
|
Message: "Playback and browsing are working normally.",
|
||
|
|
AiredAt: now.Format(time.RFC3339),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return clientAlert{
|
||
|
|
ID: fmt.Sprintf("emby:down:%d", now.Unix()),
|
||
|
|
Kind: alertKindServerDown,
|
||
|
|
Label: "SERVER NOT RESPONDING",
|
||
|
|
Title: "Emby has stopped communicating",
|
||
|
|
// Says what the viewer will see rather than what failed: a diagnosis they cannot
|
||
|
|
// act on from the sofa is worse than none.
|
||
|
|
Message: "Playback may stop until it is back. Memby will say when it returns.",
|
||
|
|
AiredAt: now.Format(time.RFC3339),
|
||
|
|
}
|
||
|
|
}
|