Server changes/Sonarr

This commit is contained in:
ponzischeme89
2026-07-27 21:06:51 +12:00
parent 62f6345a40
commit 8d6cf2f5a1
42 changed files with 2388 additions and 284 deletions
+93 -9
View File
@@ -35,6 +35,17 @@ go run ./cmd/memby-server # needs Postgres + Redis reachable
On Windows, `go` may need `-buildvcs=false` when the working tree has no usable `.git`.
## 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.
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.
## API
All `/v1` routes need `Authorization: Bearer <token>` from `/v1/auth/login`. Image URLs
@@ -43,9 +54,11 @@ headers attached.
| Method | Path | Purpose |
| --- | --- | --- |
| POST | `/v1/auth/login` | Emby credentials in, gateway token out |
| 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/status` | Lightweight live maintenance state; remains available during maintenance |
| GET | `/v1/home?limit=` | **Every launcher row in one response** |
| GET | `/v1/recommendations?refresh=1` | Recommendation rows alone; `refresh` forces a rebuild |
| GET | `/v1/update` | Whether this client (per `X-Memby-Version`) must update |
@@ -71,10 +84,14 @@ four 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": "favorites", "title": "Favourites", "kind": "favorites", "items": [...]},
{"id": "latest-movies", "title": "Recently Added Movies", "kind": "latest", "items": [...]},
{"id": "similar:sev", "title": "Because you watched Severance", "kind": "similar", "items": [...]},
{"id": "recommended", "title": "Recommended from your watching history", "kind": "recommended", "items": [...]}
{"id": "recommended", "title": "Recommended from your watching history", "kind": "recommended", "items": [...]},
{"id": "curated:apple-tv", "title": "Apple TV+ Shows", "kind": "shows", "items": [...]},
{"id": "curated:drama-shows", "title": "Drama TV Shows", "kind": "shows", "items": [...]},
{"id": "curated:comedy-shows", "title": "Comedy TV Shows", "kind": "shows", "items": [...]}
],
"continueWatching": [...], "nextUp": [...], "favorites": [...], "latestMovies": [...],
"partial": false
@@ -98,11 +115,16 @@ invented — nobody opted out of a row that did not exist when they last opened
recent), favourites add a smaller fixed weight, and candidates are unplayed titles in the
top three genres scored by affinity + a mild community-rating nudge. Titles tagged with
many genres get a `sqrt(n)` penalty so genre-stuffing cannot buy a top slot.
- **Curated TV shelves** — Apple TV+, Drama and Comedy membership is filtered from the
imported Postgres catalogue. Within each shelf, unseen shows are ranked using the same
personal genre/studio profile; the shelves themselves are ordered by affinity too.
A new profile falls back to community rating until it has viewing history.
Rows shorter than four items are dropped, and a user with no history gets no rows at all
rather than a strip of noise.
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`
**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
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
@@ -112,11 +134,28 @@ that genuinely changes viewing history.
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.
Curated shelves require at least one completed library import because their genre/studio
filter runs against Postgres. Emby's exact studio naming is preserved during import;
Apple matching accepts `Apple TV+`, `Apple TV Plus` and `Apple Studios`.
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
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.
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.
Sonarr poster and fanart requests are proxied through the gateway; its API key is never
sent to the TV. The shared Redis entry is refreshed every five minutes by default, not
once per user.
## Admin interface
`http://<host>:8080/admin/` — a single self-contained page for library imports, the
@@ -167,8 +206,10 @@ candidate pool.
Takes Memby down independently of Emby: all `/v1` routes answer `503` with
`{"maintenance": true, "message": "…"}`, and the TV shows the operator's message instead of
a network error. `/healthz`, `/readyz` and `/admin` stay up — they are what you need while
the app is deliberately off.
a network error. The exact `/v1/status` control route stays up too: open clients poll it
every ten seconds, stop active playback, and move immediately to the maintenance screen.
`/healthz`, `/readyz` and `/admin` also stay up — they are what you need while the app is
deliberately off.
The switch lives in Postgres, not memory, so a restart cannot quietly bring the app back
up mid-repair. Each instance caches it and re-reads every 30 seconds, so toggling it
@@ -177,8 +218,40 @@ directly in the database works too.
## App update policy
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; the verdict is
`none`, `optional` or `mandatory`.
`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`.
### Automated tagged releases
`.gitea/workflows/release.yml` turns a pushed semantic-version tag into an update:
1. Gitea Actions derives the Android version from a tag such as `v0.1.54`.
2. It runs the tests and builds the APK with the established release signing key.
3. It uploads the signed APK to `POST /admin/api/release`.
4. The gateway stores the APK in its `memby-releases` volume and makes it the latest
optional update. Older TVs prompt on their next check.
The repository needs an Actions runner with the `ubuntu-latest` label and these Actions
secrets:
| Secret | Purpose |
| --- | --- |
| `ANDROID_KEYSTORE_BASE64` | Existing release keystore, base64 encoded as one line |
| `ANDROID_KEYSTORE_PASSWORD` | Keystore password |
| `ANDROID_KEY_ALIAS` | Signing alias |
| `ANDROID_KEY_PASSWORD` | Key password |
| `MEMBY_RELEASE_PUBLISH_TOKEN` | Same dedicated value configured on the gateway |
Enable repository Actions and give the job permission to read contents. Then releasing
from any clone is:
```bash
git tag -a v0.1.54 -m "Personalised favourites"
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.
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.
@@ -229,7 +302,9 @@ served but never cached. The `r:` namespace is deliberately excluded from that w
Recommendations above).
Postgres holds only sessions. It is the durable half: losing Redis costs a cold cache,
losing Postgres signs everyone out.
losing Postgres signs everyone out. A 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.
## Configuration
@@ -240,18 +315,27 @@ losing Postgres signs everyone out.
| `MEMBY_DATABASE_URL` | *required* | Postgres DSN |
| `MEMBY_REDIS_URL` | `redis://localhost:6379/0` | |
| `MEMBY_LISTEN_ADDR` | `:8080` | |
| `MEMBY_LOG_LEVEL` | `INFO` | Use `DEBUG` for successful probe, status-poll and artwork requests |
| `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_TIMEOUT` | `60s` | Bounds a background 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 |
| `MEMBY_RELEASE_PUBLISH_TOKEN` | *empty* | Enables the CI-only release upload endpoint |
| `MEMBY_SYNC_INTERVAL` | `1h` | Incremental import cadence; `0` disables |
| `MEMBY_SYNC_TIMEOUT` | `30m` | Bounds one import |
| `MEMBY_SYNC_ON_START` | `false` | Import at boot |
| `MEMBY_SONARR_URL` | *empty* | Sonarr address reachable by the gateway; empty disables integration |
| `MEMBY_SONARR_API_KEY` | *empty* | Sonarr Settings → General → Security API key |
| `MEMBY_SONARR_TTL` | `5m` | Shared Redis lifetime for today's 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