Publish current app and server
This commit is contained in:
+142
-27
@@ -75,10 +75,13 @@ headers attached.
|
||||
| Method | Path | Purpose |
|
||||
| --- | --- | --- |
|
||||
| POST | `/v1/auth/login` | Emby credentials + device identity in, gateway token out |
|
||||
| GET | `/v1/auth/policy` | Public client allowance used by the sign-in screen |
|
||||
| POST | `/v1/auth/logout` | Retire this device's token |
|
||||
| GET | `/v1/auth/session` | Confirm a stored token is still valid |
|
||||
| GET | `/v1/auth/devices` | List this user's signed-in TVs |
|
||||
| PUT | `/v1/auth/devices/{deviceId}` | Rename a signed-in TV |
|
||||
| DELETE | `/v1/auth/devices/{deviceId}` | Revoke another signed-in TV |
|
||||
| GET | `/v1/status` | Lightweight live maintenance state plus informational alerts; remains available during maintenance |
|
||||
| GET | `/v1/features` | Versioned, capability-gated feature document evaluated for this TV |
|
||||
| GET | `/v1/home?limit=` | **Every launcher row in one response** |
|
||||
| GET | `/v1/recommendations?refresh=1` | Recommendation rows alone; `refresh` forces a rebuild |
|
||||
| GET | `/v1/for-you?minutes=` | Tracearr-powered, explainable picks for a 0/30/60/120-minute viewing window |
|
||||
@@ -109,7 +112,8 @@ 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": "sonarr-airing-today", "title": "Shows airing today", "kind": "schedule", "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": [...]},
|
||||
{"id": "latest-movies", "title": "Recent New Releases", "kind": "latest", "items": [...]},
|
||||
{"id": "similar:sev", "title": "Because you watched Severance", "kind": "similar", "items": [...]},
|
||||
@@ -149,12 +153,10 @@ Rows shorter than four items are dropped, and a user with no history gets no row
|
||||
rather than a strip of noise, except curated shelves: these remain useful with their
|
||||
quality-ranked fallback.
|
||||
|
||||
**The home screen never waits on the engine.** Rows live in their own `r:<userId>:rows:v2`
|
||||
cache key with a long TTL (2h). A cache miss serves home immediately without them and
|
||||
**The home screen never waits on the engine.** Rows live in their own `r:<userId>:rows:v3`
|
||||
cache key with a long TTL (24h). A cache miss serves home immediately without them and
|
||||
triggers a background rebuild — deduplicated per user, so four TVs waking together do the
|
||||
work once. Because the key sits outside the `u:` namespace, a favourite toggle does not
|
||||
throw the recommendations away; only a finished playback does, since that is the one event
|
||||
that genuinely changes viewing history.
|
||||
work once. Playback and favourite mutations do not discard this slow-moving taste cache.
|
||||
|
||||
The scoring is pure and unit-tested (`profile_test.go`), and the row assembly runs against
|
||||
a fake Emby (`engine_test.go`), so neither needs a server to verify.
|
||||
@@ -184,7 +186,7 @@ after two unchanged pages (at most ten); a daily full pass catches late or out-o
|
||||
updates and reconciles deletions. Imports are idempotent.
|
||||
|
||||
Postgres retains the compact source sessions, one derived profile per enabled Emby user,
|
||||
and every eligible ranked Movie/Series candidate. The background builder reads Tracearr's
|
||||
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
|
||||
Emby users still receive Emby-history recommendations; similar-looking names are never
|
||||
fuzzily joined without an explicit operator decision.
|
||||
@@ -197,15 +199,24 @@ there is enough playback evidence. Completed-title evidence is distributed
|
||||
deterministically across relevant candidates rather than allowing the newest Drama
|
||||
title to explain every Drama recommendation.
|
||||
|
||||
The endpoint remains one indexed PostgreSQL read plus JSON enrichment. Pools may be
|
||||
30 minutes stale; rebuilds inside a ten-minute window are coalesced. A cold or failed
|
||||
rebuild uses the original live Tracearr/Emby path, so persistence cannot blank the area.
|
||||
The endpoint remains one indexed PostgreSQL read plus JSON enrichment. Tracearr imports
|
||||
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
|
||||
path, so persistence cannot blank the area.
|
||||
|
||||
Every returned item is enriched with `MembyRecommendationReason` and
|
||||
`MembyCompatibility`. The TV shows the reason on the card and in the focused metadata
|
||||
panel. Missing Tracearr or sparse codec evidence degrades to Emby/Memby signals rather
|
||||
than blanking the area; Tracearr errors never affect the essential home rows.
|
||||
|
||||
Tracearr episode history also identifies shows abandoned in season one. A show qualifies
|
||||
after 21 days without activity only when Emby still returns a season-one `Next Up`
|
||||
episode, which prevents completed shows and later-season pauses from leaking into the
|
||||
row. Genuine matches appear on Home as **Pick this show up again**, even when only one
|
||||
show qualifies; generic recommendations are never used as padding.
|
||||
|
||||
Item payloads are Emby's own JSON, forwarded verbatim. That is deliberate: the Android
|
||||
client already models this shape, so there is no second schema to keep in sync.
|
||||
`app/src/test/.../GatewayPayloadTest.kt` and `internal/api/api_test.go` pin the envelope
|
||||
@@ -214,9 +225,12 @@ around it from both sides.
|
||||
### Sonarr schedule
|
||||
|
||||
When `MEMBY_SONARR_URL` and `MEMBY_SONARR_API_KEY` are set, the gateway reads Sonarr's
|
||||
v3 calendar and inserts **Shows airing today** directly after Next Up. Cards show the
|
||||
local air time, season/episode number, episode title and one of: upcoming, downloading,
|
||||
awaiting download, unmonitored, or the exact time Sonarr added the episode file.
|
||||
v3 calendar and inserts **Shows airing in the next 5 days** near Continue Watching.
|
||||
The window is today plus the following four local calendar days. Card timing reads
|
||||
`In 8 hours (4:00 PM)` for later today, `Tomorrow: 4:00 PM` for tomorrow, and then the
|
||||
weekday and local time. Cards also show season/episode number, episode title and one of:
|
||||
upcoming, downloading, awaiting download, unmonitored, or the exact time Sonarr added
|
||||
the episode file.
|
||||
|
||||
This row is informational. An episode listed before it is downloaded is not an Emby
|
||||
item, so the TV deliberately does not offer Play, Favourite or Watched actions on it.
|
||||
@@ -252,28 +266,56 @@ rather than once: a TV that was showing its screensaver when the episode aired w
|
||||
the alert up when someone comes back to it, and one that never does simply lets the alert
|
||||
expire with the window.
|
||||
`MEMBY_SONARR_ALERT_WINDOW` bounds how long after air time an alert stays current;
|
||||
`0` disables banners while leaving the airing-today row alone.
|
||||
`0` disables banners while leaving the five-day schedule row alone.
|
||||
|
||||
### Radarr digital-release schedule
|
||||
|
||||
When `MEMBY_RADARR_URL` and `MEMBY_RADARR_API_KEY` are set, the gateway reads Radarr's
|
||||
v3 calendar and inserts **Upcoming Movie releases** beside the Sonarr schedule near
|
||||
Continue Watching. The row covers today plus the following four local calendar days.
|
||||
Radarr's `digitalRelease` date is preferred. When it is missing, the row uses an
|
||||
estimated digital availability date 30 days after `inCinemas`; physical-disc dates are
|
||||
ignored. A known digital date always wins, including for older films with later cinema
|
||||
re-releases.
|
||||
|
||||
Movie posters and fanart are proxied through the gateway so the Radarr API key never
|
||||
leaves the server. `MEMBY_RADARR_TTL` controls the shared calendar cache lifetime.
|
||||
|
||||
## Admin interface
|
||||
|
||||
`https://mserver.sublogue.com/admin/` — a single self-contained page for library imports, the
|
||||
maintenance switch and row engagement. Set `MEMBY_ADMIN_TOKEN` to enable it; unset, every
|
||||
`/admin` route 404s so it cannot be left exposed by accident. Loading the page establishes
|
||||
a persistent HttpOnly admin cookie automatically, so the configured token survives server
|
||||
and browser reboots without being pasted into the page. Anyone who can load this local-only
|
||||
page is therefore an admin: put the whole path behind your reverse proxy's own auth before
|
||||
making the gateway reachable outside the LAN.
|
||||
`/admin` route 404s so it cannot be left exposed by accident. The page first uses the same
|
||||
discreet Emby login gate as the private installer. After successful verification it
|
||||
establishes the HttpOnly admin cookie, but browser API requests require both that cookie
|
||||
and the current 30-minute Emby-verified session. The old admin cookie therefore cannot
|
||||
bypass the gate after the browser session expires. Scripts may continue to use
|
||||
`Authorization: Bearer <MEMBY_ADMIN_TOKEN>` without a browser session.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| --- | --- | --- |
|
||||
| GET | `/admin/` | The page |
|
||||
| GET | `/admin/api/status` | Library counts, sync history, maintenance state |
|
||||
| GET | `/admin/api/runtime` | Protected Go heap, memory-limit and goroutine metrics |
|
||||
| POST | `/admin/api/sync` | `{"kind":"full"}` or `{"kind":"incremental"}` |
|
||||
| POST | `/admin/api/for-you` | `{"action":"incremental-import"}`, `{"action":"full-import"}`, or `{"action":"rebuild-all"}` |
|
||||
| POST | `/admin/api/maintenance` | `{"enabled":true,"message":"…"}` |
|
||||
| POST | `/admin/api/update-policy` | `{"enabled":true,"latestVersion":"0.1.54","downloadUrl":"…","required":false}` |
|
||||
| POST | `/admin/api/features` | Publish feature overrides, enter safe mode, reset defaults, or roll back one revision |
|
||||
| GET | `/admin/api/analytics?days=7` | Row engagement |
|
||||
|
||||
The **Features** admin page is the recovery surface for optional TV behaviour. Flags are
|
||||
registered in the server catalogue and persist only explicit overrides; clearing an
|
||||
override returns it to its code-owned safe default. Every write is revision-checked and
|
||||
retains the prior revision. Safe mode disables all optional flags while leaving sign-in,
|
||||
browsing, playback, maintenance and the admin console operational.
|
||||
|
||||
TVs report `X-Memby-Version`, `X-Memby-Protocol`, and a bounded
|
||||
`X-Memby-Capabilities` list on every request. Protocol and capability checks fail closed,
|
||||
so a server never advertises an optional contract to a client that did not declare it.
|
||||
The first migrated controls are the Sonarr preroll, automatic My Shows, and its automatic
|
||||
follow notification; all three can be stopped without publishing an APK.
|
||||
|
||||
## Library import
|
||||
|
||||
`internal/library` copies Emby's catalogue into Postgres so the gateway answers from its
|
||||
@@ -319,7 +361,41 @@ directly in the database works too.
|
||||
|
||||
The gateway decides whether a TV may keep running its current build. Clients send
|
||||
`X-Memby-Version` on every request and ask `GET /v1/update` on each launch and hourly
|
||||
while left open; the verdict is `none`, `optional` or `mandatory`.
|
||||
while left open; the verdict is `none`, `optional` or `mandatory`. This endpoint is
|
||||
deliberately public and remains available during maintenance: update policy is checked
|
||||
before login and never reads, validates, or mutates a viewer session.
|
||||
|
||||
### First-time TV installation
|
||||
|
||||
The gateway hosts a public bootstrap page at:
|
||||
|
||||
```text
|
||||
https://mserver.sublogue.com/install
|
||||
```
|
||||
|
||||
The site root displays the same page. Before any release details or download link are
|
||||
shown, it asks for an Emby household username and password and verifies them directly
|
||||
against Emby. Credentials are never stored. The temporary Emby access token is logged
|
||||
out immediately, and the browser receives a secure, HTTP-only, same-site installer cookie
|
||||
valid for 30 minutes. This browser login does not create a Memby TV session and therefore
|
||||
does not appear in the user's signed-in device list.
|
||||
|
||||
After authentication, `/updates/latest.apk` redirects to the current immutable, versioned
|
||||
APK in the persistent `memby-releases` volume. Direct APK requests require either that
|
||||
short-lived browser session or the per-release signed URL returned to an authenticated
|
||||
Memby app during its update check; guessed filenames return 404. This is the exact signed
|
||||
artefact produced and published by `deploy-server.ps1`, not a separate bootstrap build.
|
||||
The APK is compiled with `memby.gatewayUrl=https://mserver.sublogue.com`, so a fresh
|
||||
installation opens directly at Memby's login screen.
|
||||
|
||||
On a new TV, enter the short `/install` address in a browser or the Downloader app,
|
||||
download the APK, allow that browser/downloader to install unknown apps if Android asks,
|
||||
then install and launch Memby. No Files/Storage permission is needed by Memby itself.
|
||||
|
||||
The installer is also intentionally absent from search: `/robots.txt` disallows the complete
|
||||
host, the page and APK responses send `X-Robots-Tag` no-index/no-follow/no-archive
|
||||
directives, and the HTML repeats those controls for major crawlers. A scanner can discover
|
||||
that the host exists, but it cannot see a release or download an APK without valid access.
|
||||
|
||||
### Automated tagged releases
|
||||
|
||||
@@ -353,6 +429,39 @@ git push origin main v0.1.54
|
||||
Tags are the release boundary; an ordinary branch push never publishes an APK. Android
|
||||
will only accept updates signed by the same keystore as the installed app.
|
||||
|
||||
### Publishing an update with `deploy-server.ps1`
|
||||
|
||||
The normal NAS deployment can build and publish the TV app in the same operation:
|
||||
|
||||
```powershell
|
||||
.\deploy-server.ps1 -AppVersion 0.1.70 -ReleaseNotes 'Reliable in-app updates'
|
||||
```
|
||||
|
||||
Append `--m` to make the published update mandatory. Older clients receive a blocking
|
||||
update screen with no skip or dismiss action:
|
||||
|
||||
```powershell
|
||||
.\deploy-server.ps1 -ReleaseNotes 'Required security update' --m
|
||||
```
|
||||
|
||||
It loads the same user-level `MEMBY_KEYSTORE*` variables as `deploy-tv.ps1`, runs the
|
||||
Android tests, builds the requested release without editing `build.gradle.kts`, verifies
|
||||
the APK signature, and sends the signed artefact in the SSH deployment archive. After
|
||||
PostgreSQL, Redis and the replacement gateway are healthy, the remote script publishes
|
||||
the APK through `POST /admin/api/release`. The gateway records its SHA-256 digest and
|
||||
byte size in PostgreSQL and serves it from the persistent `memby-releases` volume.
|
||||
|
||||
`MEMBY_RELEASE_PUBLISH_TOKEN` must contain a stable, private value in `.env.example`.
|
||||
Every update intended for installed TVs needs a version higher than the one they already
|
||||
run; Android rejects equal or lower `versionCode` values. Reusing a version for different
|
||||
APK bytes is also rejected because release URLs are immutable.
|
||||
|
||||
For a server-only emergency deployment, explicitly opt out:
|
||||
|
||||
```powershell
|
||||
.\deploy-server.ps1 -SkipAppRelease
|
||||
```
|
||||
|
||||
Set it on the admin page: **latest version**, **APK URL** (normally the same file the
|
||||
landing page serves), release notes, and a **Require this update** toggle.
|
||||
|
||||
@@ -405,8 +514,8 @@ Postgres is the durable half: it holds gateway sessions, the imported Emby catal
|
||||
Tracearr's compact recommendation history, derived profiles and prepared candidate pools.
|
||||
Losing Redis costs a cold cache; losing Postgres signs everyone out and requires catalogue
|
||||
and recommendation backfills. A gateway session is unique per Emby user and stable device
|
||||
ID; signing the same TV in again rotates its token, while a new TV is rejected once
|
||||
`MEMBY_MAX_CLIENTS_PER_USER` is reached.
|
||||
ID; signing the same TV in again rotates its token. Accounts have no device cap; viewers
|
||||
can review and revoke signed-in TVs from the app's Settings screen.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -418,17 +527,21 @@ ID; signing the same TV in again rotates its token, while a new TV is rejected o
|
||||
| `MEMBY_REDIS_URL` | `redis://localhost:6379/0` | |
|
||||
| `MEMBY_LISTEN_ADDR` | `:8080` outside Compose; `:32768` in the NAS stack | |
|
||||
| `MEMBY_LOG_LEVEL` | `INFO` | Use `DEBUG` for successful probe, status-poll and artwork requests |
|
||||
| `MEMBY_LOG_BUFFER_CAPACITY` | `5000` | Bounded in-memory admin event ring; `0` disables capture |
|
||||
| `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_HOME_TTL` | `60s` | Also `MEMBY_ITEM_TTL`, `MEMBY_SEARCH_TTL`, `MEMBY_SCREENSAVER_TTL` |
|
||||
| `MEMBY_RECOMMEND_TTL` | `2h` | How long computed recommendation rows stay warm |
|
||||
| `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_FOR_YOU_MIN_REBUILD_AGE` | `10m` | Coalesces bursts of pool invalidations |
|
||||
| `MEMBY_FOR_YOU_REFRESH_INTERVAL` | `30m` | Acceptable prepared-pool staleness |
|
||||
| `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 (0–23) for the daily prepared rebuild |
|
||||
| `MEMBY_ADMIN_TOKEN` | *empty* | Enables `/admin`. Empty = admin disabled |
|
||||
| `MEMBY_PUBLIC_URL` | *empty* | Public gateway origin used for APK download links |
|
||||
| `MEMBY_RELEASE_DIR` | `/data/releases` | Persistent signed APK directory |
|
||||
@@ -440,11 +553,13 @@ ID; signing the same TV in again rotates its token, while a new TV is rejected o
|
||||
| `MEMBY_SONARR_API_KEY` | *empty* | Sonarr Settings → General → Security API key |
|
||||
| `MEMBY_SONARR_TTL` | `5m` | Shared Redis lifetime for today's calendar |
|
||||
| `MEMBY_SONARR_ALERT_WINDOW` | `3h` | How long after air time an "aired" banner stays current; `0` disables banners |
|
||||
| `MEMBY_RADARR_URL` | *empty* | Radarr address reachable by the gateway; empty disables integration |
|
||||
| `MEMBY_RADARR_API_KEY` | *empty* | Radarr Settings → General → Security API key |
|
||||
| `MEMBY_RADARR_TTL` | `5m` | Shared Redis lifetime for the five-day digital-release calendar |
|
||||
| `MEMBY_SYNC_USER_ID` / `MEMBY_SYNC_API_KEY` | *empty* | Emby service account for imports |
|
||||
| `MEMBY_ANALYTICS_RETENTION` | `2160h` (90d) | Raw row events are pruned past this |
|
||||
| `MEMBY_SESSION_CACHE_TTL` | `5m` | How long a token lookup stays in Redis |
|
||||
| `MEMBY_SESSION_IDLE_EXPIRY` | `2160h` (90d) | Unused tokens are swept every 6h |
|
||||
| `MEMBY_MAX_CLIENTS_PER_USER` | `1` | Strict maximum number of distinct signed-in TVs per Emby user |
|
||||
| `MEMBY_UPSTREAM_TIMEOUT` | `20s` | |
|
||||
|
||||
## Security notes
|
||||
|
||||
Reference in New Issue
Block a user