Server changes/Sonarr
This commit is contained in:
+93
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user