# mediamark Keyboard-centric web UI to mark media as cheeztv (kids) content via hardlinks; oauth2-proxy fronted, talks to sonarr/radarr APIs. A single Go binary carrying its own UI. It lists the titles in the `fafflix` library and lets an authorized Authentik group promote any of them into the `cheeztv` kids tree. Promotion is a recursive **hardlink** sync, so a marked title consumes no extra space and unmarking can never destroy media. ## How marking works The media root holds two parallel trees: ``` / fafflix/{movies,tvseries}//... source library (radarr / sonarr) cheeztv/{movies,tvseries}/<title>/... kids tree (hardlinks into fafflix) ``` * **Mark** walks the source title and hardlinks every regular file into the same-named directory under `cheeztv/`. It is idempotent: files already pointing at the same inode are left alone, a kids-side file pointing at a *different* inode is replaced, and symlinks/devices are skipped. Re-running it after new episodes land syncs only the new files. * **Unmark** removes the `cheeztv/` directory only. The source inode keeps its link, so nothing is lost. * A marked title whose source has files that are not linked across is reported as `needsSync`, and the detail view offers a **Sync new files** button. Title names come from directory entries and are validated as a single clean path element (no separators, no `..`, no absolute paths) before touching the disk. ## Configuration All configuration is environment-only. Startup fails closed on an empty group allow-list, a relative media root, or a non-HTTP *arr URL. | Variable | Default | Purpose | | --- | --- | --- | | `MEDIAMARK_LISTEN` | `:8080` | HTTP listen address | | `MEDIAMARK_MEDIA_ROOT` | `/media` | Root holding `fafflix/` and `cheeztv/` | | `MEDIAMARK_KEYS_DIR` | `/etc/mediamark/keys` | Directory of `sonarr`/`radarr` API key files, re-read per use so VSO rotations land without a restart | | `MEDIAMARK_SONARR_URL` | `http://sonarr.arrstack.svc.cluster.local:8989/3aa168/sonarr` | Sonarr base URL, including its `UrlBase` | | `MEDIAMARK_RADARR_URL` | `http://radarr.arrstack.svc.cluster.local:7878/3aa168/radarr` | Radarr base URL, including its `UrlBase` | | `MEDIAMARK_GROUPS_HEADER` | `X-Forwarded-Groups` | oauth2-proxy header carrying Authentik groups | | `MEDIAMARK_ALLOWED_GROUPS` | `akP-mediamark-user` | Comma/space separated group allow-list; must be non-empty | | `ARR_SONARR_APIKEY` / `ARR_RADARR_APIKEY` | _(unset)_ | Development escape hatch, overrides the key files | oauth2-proxy fronts the app, but group membership is re-checked server-side on every API call *and* on the page load itself, so an unauthorized user gets a 403 rather than an empty shell. `/livez` and `/readyz` are ungated for the kubelet; `/readyz` fails while the media root is not visible. ## HTTP API | Route | Purpose | | --- | --- | | `GET /api/library/{section}` | Titles in `movies` or `tvseries` with marked/drift state, size, and *arr metadata | | `POST /api/library/{section}/{name}/mark` | Hardlink-sync the title into `cheeztv` (idempotent) | | `POST /api/library/{section}/{name}/unmark` | Remove the title's `cheeztv` directory (idempotent) | | `GET /api/art/{section}/{id}` | Poster proxy; the *arr API key is injected server-side and never exposed | | `GET /livez`, `GET /readyz` | Health probes | Sonarr/radarr list responses are cached in memory for ~60s so list pages stay fast; a failed *arr lookup degrades to an art-less listing rather than an error. ## Keyboard map | Where | Key | Action | | --- | --- | --- | | Landing | `m` / `t` | Open Movies / TV Series | | Landing | `←` `→` `↑` `↓` | Move between tiles | | Landing | `Enter` | Open the selected tile | | Section | _any character_ | Filter (fuzzy, fzf-style subsequence scoring) | | Section | `↑` `↓` | Move the selection (scrolls into view) | | Section | `Enter` | Open the title detail | | Section | `m` | Toggle cheeztv on the selection (after arrowing off the search box) | | Section | `Esc` | Clear the search, then return to the landing page | | Detail | `m` | Toggle cheeztv | | Detail | `Esc` | Back to the section list | The search box is autofocused, so typing always filters. Arrowing moves focus to the list, which is what frees `m` to act as a shortcut; typing any other character hands focus straight back to the search box. ## Development ```sh make build # dist/mediamark make test # go test -race -count=1 ./... make pre-commit # gofmt + go vet, as CI runs them make lint # golangci-lint uvx pre-commit run --all-files ``` To run against a scratch library with no *arr backends: ```sh mkdir -p /tmp/mm/{fafflix,cheeztv}/{movies,tvseries} mkdir -p "/tmp/mm/fafflix/movies/Some Film (2019)" MEDIAMARK_MEDIA_ROOT=/tmp/mm ./dist/mediamark ``` Then send the group header yourself, since there is no oauth2-proxy in front: ```sh curl -H 'X-Forwarded-Groups: akP-mediamark-user' localhost:8080/api/library/movies ``` ## Release Tagging `v*` (via `make patch|minor|major`) builds and pushes `artifactapi.k8s.syd1.au.unkin.net/docker-internal/mediamark:<tag>`.