App v0.2.26 and gateway 0.1.20

Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-06 22:33:56 +12:00
co-authored by Claude Opus 5
parent 2675e6d82b
commit 4a4df7a73c
257 changed files with 24868 additions and 3108 deletions
+51 -2
View File
@@ -28,8 +28,51 @@ const (
// 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
// A deployment is announced before it happens, and the window is the whole delivery
// mechanism: it must stay open across the image build, which is the several minutes
// during which the gateway is still answering and every open TV will poll at least
// once. It cannot be made to cover the outage itself — the alert list lives in a
// Redis with no persistence and no volume, so the swap takes it with everything else.
// That is why this is published early rather than at the swap.
deploymentAlertWindow = 20 * time.Minute
)
// Wording is fixed here rather than taken from the caller: the deployment script is not a
// place for on-screen copy, and a notice that reads differently each release is one
// viewers have to read twice.
const (
deploymentAlertTitle = "Memby server in deployment mode"
deploymentAlertMessage = "New update coming will be available soon."
)
// AnnounceDeployment tells every open TV that the gateway is about to be replaced.
//
// It is published by the deploying operator *before* the old container stops — in fact
// before the image is even built — because that is the only time there is anything left
// to say it with. Once the stack is down the status poll fails, and a television shows a
// connection error with no idea that somebody meant it to happen; publishing at the swap
// would be too late for the same reason it is needed.
//
// Nothing here waits for the deployment to finish. The gateway that publishes this is the
// one being retired; the one that comes back has no memory of having said it.
func (s *Server) AnnounceDeployment(ctx context.Context) {
s.publishAlert(ctx, deploymentAlert(time.Now().UTC()), deploymentAlertWindow)
}
func deploymentAlert(now time.Time) clientAlert {
return clientAlert{
// Keyed on the second, so a redeployment ten minutes later is a second notice
// rather than one the fleet has already dismissed as seen.
ID: fmt.Sprintf("deploy:%d", now.Unix()),
Kind: alertKindDeploying,
Label: "SERVER UPDATING",
Title: deploymentAlertTitle,
Message: deploymentAlertMessage,
AiredAt: now.Format(time.RFC3339),
}
}
// AnnounceLibrarySync tells every TV that the catalogue moved.
//
// Only a run that actually changed something is announced: the import is scheduled, so
@@ -80,6 +123,10 @@ func (s *Server) WatchEmbyReachability(ctx context.Context, interval time.Durati
// open with a banner about a state nobody has seen change.
reachable := true
failures := 0
// The same probe feeds the live state on /v1/status, so one request to Emby answers
// both "did this just change" and "is it working right now". Declared before the
// first tick so a client asking during the opening minute learns the retry interval.
s.embyHealth.begin(interval, time.Now().UTC())
for {
select {
case <-ctx.Done():
@@ -88,18 +135,20 @@ func (s *Server) WatchEmbyReachability(ctx context.Context, interval time.Durati
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
err := s.emby.Ping(probeCtx)
cancel()
s.embyHealth.record(err == nil, time.Now().UTC())
if err != nil {
failures++
if reachable && failures >= embyFailureThreshold {
reachable = false
s.log.Warn("emby unreachable, announcing", "error", err)
s.log.Warn("emby unreachable, announcing",
"component", "emby-health", "failures", failures, "error", err)
s.publishAlert(ctx, s.reachabilityAlert(false), reachabilityAlertWindow)
}
continue
}
if !reachable {
s.log.Info("emby reachable again, announcing")
s.log.Info("emby reachable again, announcing", "component", "emby-health")
s.publishAlert(ctx, s.reachabilityAlert(true), reachabilityAlertWindow)
}
reachable = true