package api import ( "sync" "time" ) // The live answer to "is Emby answering right now", as opposed to the one-shot alerts in // server_alerts.go that announce the moment it changed. // // These are two different jobs and both are worth having. An alert is news: it slides in, // says Emby stopped communicating and goes away, which is right for someone already // watching. This is *state*: a television switched on twenty minutes into an outage was // never told anything, and the one thing it can usefully show is a bar saying so and how // long until the next attempt. It is the reason /v1/status carries it rather than the // alert list — an alert that has fallen out of its window is gone, and this has not. const ( // How many probes in a row must fail before the bar appears. // // Deliberately lower than embyFailureThreshold, which gates the announcement. The // announcement is a one-shot that cannot be taken back, so it waits to be sure; the // bar clears itself the moment Emby answers again, so being early costs a minute of // a red strip rather than a false claim left standing. embyOutageThreshold = 2 ) // embyHealth is the probe's finding, cached in memory for the status poll to read. Not // persisted: it describes this instant, and a value restored from a restart would be a // claim about a probe that never ran. type embyHealth struct { mu sync.RWMutex state embyHealthState } type embyHealthState struct { // monitored is false when MEMBY_EMBY_HEALTH_INTERVAL turns the probe off. The client // must then ignore reachable entirely rather than trust a value nothing updates. monitored bool reachable bool since time.Time checkedAt time.Time retryEvery time.Duration consecutive int } func (h *embyHealth) get() embyHealthState { h.mu.RLock() defer h.mu.RUnlock() return h.state } // begin records that the probe is running and how often, so the bar can say when the next // attempt is due before any probe has completed. func (h *embyHealth) begin(interval time.Duration, now time.Time) { h.mu.Lock() defer h.mu.Unlock() h.state = embyHealthState{ monitored: true, reachable: true, since: now, retryEvery: interval, } } // record folds one probe result in and reports whether the published verdict changed. func (h *embyHealth) record(ok bool, now time.Time) { h.mu.Lock() defer h.mu.Unlock() h.state.checkedAt = now if ok { h.state.consecutive = 0 if !h.state.reachable { h.state.reachable = true h.state.since = now } return } h.state.consecutive++ if h.state.reachable && h.state.consecutive >= embyOutageThreshold { h.state.reachable = false // Dated from the probe that crossed the threshold rather than the first failure: // the bar reports what the server knows, and until the threshold was crossed it // did not know anything. h.state.since = now } } // embyHealthPayload is the shape /v1/status carries. Times are RFC3339 so an old client // that cannot parse them still renders the bar from reachable and retrySeconds alone. type embyHealthPayload struct { Monitored bool `json:"monitored"` Reachable bool `json:"reachable"` Since string `json:"since,omitempty"` CheckedAt string `json:"checkedAt,omitempty"` RetrySeconds int `json:"retrySeconds"` } func embyHealthFor(state embyHealthState) embyHealthPayload { payload := embyHealthPayload{ Monitored: state.monitored, Reachable: state.reachable || !state.monitored, RetrySeconds: int(state.retryEvery / time.Second), } if !state.since.IsZero() { payload.Since = state.since.UTC().Format(time.RFC3339) } if !state.checkedAt.IsZero() { payload.CheckedAt = state.checkedAt.UTC().Format(time.RFC3339) } return payload }