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
+63 -15
View File
@@ -716,27 +716,79 @@ func rowKey(value string) string {
return b.String()
}
// ImportIfDue runs an import only when the persisted state says one is owed. Due-ness is
// measured from the last recorded import rather than from process uptime, so a restart —
// a redeploy, a crash loop, a container the healthcheck bounced — cannot buy an extra
// pass, and a gateway that is restarted more often than fullEvery still reconciles.
func (s *Service) ImportIfDue(
ctx context.Context,
importEvery, fullEvery time.Duration,
) (ImportResult, bool, error) {
state, err := s.store.TracearrImportState(ctx)
if err != nil {
return ImportResult{}, false, err
}
full, due := importDue(state, time.Now().UTC(), importEvery, fullEvery)
if !due {
s.log.Debug("Tracearr import not due",
"lastIncremental", state.LastIncrementalAt, "lastFull", state.LastFullAt)
return ImportResult{}, false, nil
}
result, err := s.Import(ctx, full)
return result, err == nil, err
}
// importDue is the whole scheduling rule, kept pure so it can be tested without a
// database. A stamp in the future is treated as due: a clock correction must not be able
// to strand the importer for an arbitrary length of time.
func importDue(
state store.TracearrImportState,
now time.Time,
importEvery, fullEvery time.Duration,
) (full bool, due bool) {
elapsed := func(at *time.Time) (time.Duration, bool) {
if at == nil {
return 0, false
}
return now.Sub(*at), true
}
if fullEvery > 0 {
since, recorded := elapsed(state.LastFullAt)
if !recorded || since >= fullEvery || since < 0 {
return true, true
}
}
if importEvery > 0 {
since, recorded := elapsed(state.LastIncrementalAt)
if !recorded || since >= importEvery || since < 0 {
return false, true
}
}
return false, false
}
func (s *Service) Schedule(
ctx context.Context,
importEvery, fullEvery time.Duration,
rebuildHour int,
) {
// One ticker asks "is anything owed?"; the persisted stamps decide what and whether.
// Two independent tickers measured process uptime, which is what let a restart reset
// the cadence and a bounced container import far more often than configured.
checkEvery := importEvery
if checkEvery <= 0 || (fullEvery > 0 && fullEvery < checkEvery) {
checkEvery = fullEvery
}
var importC <-chan time.Time
var importTicker *time.Ticker
if importEvery > 0 {
importTicker = time.NewTicker(importEvery)
if checkEvery > 0 {
importTicker := time.NewTicker(checkEvery)
importC = importTicker.C
defer importTicker.Stop()
s.log.Info("Tracearr auto-import scheduled",
"incremental", importEvery.String(), "full", fullEvery.String())
} else {
s.log.Info("Tracearr auto-import disabled")
}
var fullC <-chan time.Time
var fullTicker *time.Ticker
if fullEvery > 0 {
fullTicker = time.NewTicker(fullEvery)
fullC = fullTicker.C
defer fullTicker.Stop()
}
nextRebuild := nextDailyRebuild(time.Now(), s.location, rebuildHour)
rebuildTimer := time.NewTimer(time.Until(nextRebuild))
defer rebuildTimer.Stop()
@@ -746,13 +798,9 @@ func (s *Service) Schedule(
case <-ctx.Done():
return
case <-importC:
if _, err := s.Import(ctx, false); err != nil {
if _, _, err := s.ImportIfDue(ctx, importEvery, fullEvery); err != nil {
s.log.Warn("scheduled Tracearr import failed", "error", err)
}
case <-fullC:
if _, err := s.Import(ctx, true); err != nil {
s.log.Warn("scheduled full Tracearr import failed", "error", err)
}
case <-rebuildTimer.C:
if err := s.RebuildAll(ctx, true); err != nil {
s.log.Warn("scheduled daily For You rebuild failed", "error", err)