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
+59 -15
View File
@@ -57,14 +57,37 @@ It deploys the **local working tree**, including uncommitted server changes.
## Logs
The server writes one compact, structured text line per event, designed for
`docker compose logs -f server`. At the default `MEMBY_LOG_LEVEL=INFO`, successful
health checks, live maintenance polls and artwork requests are hidden; warnings and
failures from those routes are still shown.
The server writes one aligned line per event, designed for
`docker compose logs -f server` and for the admin page's live log:
Set `MEMBY_LOG_LEVEL=DEBUG` temporarily and recreate the server container when those
high-frequency requests are useful during diagnosis. Library imports log their start,
progress after each 500-item page, and final counts and duration.
```
2026-08-03 11:33:40 INFO playback requested component=playback user=matt device="Living room" client=0.1.60 title="Severance Good News About Hell" item=184223 type=Episode play_method=DirectStream resume=12m0s runtime=57m0s version=0.1.0
2026-08-03 11:33:40 INFO request component=home user=matt device="Living room" client=0.1.60 protocol=1 method=GET path=/v1/home status=200 duration=412ms cache=miss version=0.1.0
```
The timestamp is a column rather than a `time=` field, and the fields are always in the
same order: **who and where** first (`component`, `user`, `device`, `client`), then what
the event is about, with `version` (the gateway build) and `error` last. `MEMBY_LOG_FORMAT`
switches the whole stream to `logfmt` (slog's own text) or `json` for a collector.
Every line from a request carries the viewer, the television, the app build and the
`component` — the part of the app the call came from, derived from the route, so it is
right even for an APK too old to report anything about itself.
Beyond the per-request line, these are logged as events in their own right: sign-in,
sign-out and rejected sign-ins; a device removed or renamed from another TV; what
somebody asked to play and what happened to it (`playback requested`, `playback
started`, `playback stopped` with how much was watched, `next episode resolved`); an
update offered or forced; a media request; maintenance and feature-policy changes; and
library imports (start, progress per 500-item page, final counts and duration).
At the default `MEMBY_LOG_LEVEL=INFO`, successful health checks, live maintenance polls,
artwork requests, search terms and ten-second playback progress reports are hidden;
warnings and failures from those routes are still shown. Set `MEMBY_LOG_LEVEL=DEBUG`
temporarily and recreate the server container when they are useful during diagnosis.
The build that wrote a line is `internal/buildinfo/VERSION`, embedded at compile time —
bump it with a meaningful server change. It is also on `/healthz` and in the admin rail.
## API
@@ -112,7 +135,6 @@ three fixed rows repeated flat for the client's offline cache:
{
"rows": [
{"id": "continue", "title": "Continue Watching", "kind": "continue", "items": [...]},
{"id": "next-up", "title": "Next Up", "kind": "nextup", "items": [...]},
{"id": "for-you:pick-up", "title": "Pick this show up again", "kind": "for-you", "items": [...]},
{"id": "sonarr-airing-today", "title": "Shows airing in the next 5 days", "kind": "schedule", "items": [...]},
{"id": "favorites", "title": "Favourites", "kind": "favorites", "items": [...]},
@@ -130,6 +152,11 @@ three fixed rows repeated flat for the client's offline cache:
The TV renders whatever arrives, so a new row ships without an app release. `kind` picks
the card shape; an unrecognised kind falls back to poster cards rather than being dropped.
There is no Next Up row: those episodes are interleaved into Continue Watching, most
recently watched first, so the episode after one that just finished is the first card
rather than a card in another row. The response still carries a flat `nextUp` array for
televisions running a build that predates the merge.
The user's own section toggles still hide the three fixed rows, but never rows the server
invented — nobody opted out of a row that did not exist when they last opened Settings.
@@ -186,6 +213,14 @@ stable `(serverId, sessionId)` key. Incremental passes run every five minutes an
after two unchanged pages (at most ten); a daily full pass catches late or out-of-order
updates and reconciles deletions. Imports are idempotent.
Whether a pass is owed is decided by the timestamps recorded in `app_settings`, not by
how long this process has been up: the scheduler ticks, asks, and usually does nothing.
That is what makes the cadence survive a restart. Driving it from tickers alone meant a
redeploy or a bounced container started the interval again and imported immediately, so a
gateway that restarted often imported far more often than configured — and, in the other
direction, one restarted daily never reached its 24-hour full reconciliation at all. The
startup pass follows the same rule; `/admin` still forces either kind on demand.
Postgres retains the compact source sessions, one derived profile per enabled Emby user,
and at most 750 ranked Movie/Series candidates per user. The background builder reads Tracearr's
public users endpoint and exact-matches usernames against Emby's user list. Unmatched
@@ -204,7 +239,7 @@ The endpoint remains one indexed PostgreSQL read plus JSON enrichment. Tracearr
stay frequent but do not rebuild pools. A session's first transition to stopped/completed
dirties only its matched user, and all pools rebuild once daily at the configured local
off-peak hour. A stored algorithm version triggers a one-time startup migration only when
ranking behavior changes. A cold or failed rebuild uses the original live Tracearr/Emby
ranking behaviour changes. A cold or failed rebuild uses the original live Tracearr/Emby
path, so persistence cannot blank the area.
Every returned item is enriched with `MembyRecommendationReason` and
@@ -293,6 +328,14 @@ and the current 30-minute Emby-verified session. The old admin cookie therefore
bypass the gate after the browser session expires. Scripts may continue to use
`Authorization: Bearer <MEMBY_ADMIN_TOKEN>` without a browser session.
That 30 minutes is idle time, not a hard limit: opening an admin page, making any change,
or reading one while interacting with it slides the expiry forward once it is inside the
last fifteen minutes. What deliberately does **not** extend it is the page's own status
poll — a console left open on a second monitor still times out, which is the whole point
of the TTL. The page marks its own requests with `X-Memby-Admin-Active` when there has
been interaction in the last five minutes, and on a 401 it reloads, so an expiry lands as
the sign-in form with `next` pointing back at the page rather than as an error banner.
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/admin/` | The page |
@@ -325,9 +368,10 @@ The **Movie ratings** admin page enables MDBList, stores its API key only in the
by default. Movie detail pages request `/v1/items/{id}/ratings` independently of essential
Emby metadata; missing provider IDs, MDBList errors, invalid or unavailable values all
produce an empty successful response. Successful MDBList responses are cached in Redis for
24 hours before the configured source filter is applied, so changing visible sources does
not consume more MDBList quota. The API key is never returned by either the TV or admin
status APIs.
24 hours and persisted in Postgres for seven days before the configured source filter is
applied, so restarts, multiple TVs, and changing visible sources do not consume more
MDBList quota. If MDBList is temporarily unavailable, the most recently persisted response
is served stale. The API key is never returned by either the TV or admin status APIs.
## Library import
@@ -544,14 +588,14 @@ can review and revoke signed-in TVs from the app's Settings screen.
| `MEMBY_GOMEMLIMIT` | `384MiB` | Compose value passed to Go as `GOMEMLIMIT` |
| `MEMBY_SERVER_MEMORY_LIMIT` | `512m` | Compose hard memory ceiling for the server container |
| `MEMBY_TIMEZONE` | `Pacific/Auckland` | Local day and time labels for schedule rows |
| `MEMBY_CLIENT_NAME` | `Memby` | Shown in Emby's device list |
| `MEMBY_CLIENT_NAME` | `MbyATV` | Client name sent to Emby; must match the app's direct path |
| `MEMBY_HOME_TTL` | `60s` | Also `MEMBY_ITEM_TTL`, `MEMBY_SEARCH_TTL`, `MEMBY_SCREENSAVER_TTL` |
| `MEMBY_RECOMMEND_TTL` | `24h` | How long computed recommendation rows stay warm |
| `MEMBY_RECOMMEND_TIMEOUT` | `60s` | Bounds a background rebuild |
| `MEMBY_TRACEARR_URL` / `MEMBY_TRACEARR_API_KEY` | *empty* | Read-only Tracearr public API; both values are required |
| `MEMBY_TRACEARR_SERVER_ID` | *empty* | Optional server scope when Tracearr monitors several servers |
| `MEMBY_TRACEARR_SYNC_INTERVAL` | `5m` | Incremental session import cadence; `0` disables scheduling |
| `MEMBY_TRACEARR_FULL_INTERVAL` | `24h` | Full reconciliation cadence |
| `MEMBY_TRACEARR_SYNC_INTERVAL` | `5m` | Minimum age of the last incremental import before another is owed; `0` disables incremental passes |
| `MEMBY_TRACEARR_FULL_INTERVAL` | `24h` | Minimum age of the last full reconciliation before another is owed; `0` disables full passes |
| `MEMBY_FOR_YOU_MIN_REBUILD_AGE` | `24h` | Safety bound for non-forced pool rebuilds |
| `MEMBY_FOR_YOU_REFRESH_INTERVAL` | `24h` | Acceptable prepared-pool staleness |
| `MEMBY_FOR_YOU_REBUILD_HOUR` | `4` | Local hour (023) for the daily prepared rebuild |