unkin-agent df07085ecb
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Close the review gaps: CI lint, server timeouts, security headers
The initial scaffold left three holes the review caught. CI only checked
gofmt and go vet, so golangci-lint and the pre-commit hooks were advisory
rather than enforced. The HTTP server bounded only the header read, so a
slow or stalled peer could hold a connection indefinitely. And the browser
got no content-security policy at all, leaving the SPA's same-origin
assumption unenforced.

Add golangci-lint and pre-commit hook steps to the existing pre-commit
workflow, mirroring the estate's images so the required context name stays
ci/woodpecker/pr/pre-commit.
Bound the server with ReadTimeout, WriteTimeout, and IdleTimeout, keeping
the write budget generous enough for the poster proxy's streamed responses.
Stamp Content-Security-Policy, X-Content-Type-Options, and Referrer-Policy
onto every response from a single middleware wrapping the root handler.
Assert the headers across the API, UI, assets, probes, and rejections.
Guard the CSP's no-unsafe-inline assumption with a ui test that fails if a
shipped asset grows an inline script, style block, or event handler.
Extend the make pre-commit target to match the widened CI checks.
2026-08-29 21:42:39 +10:00
2026-08-29 21:27:09 +10:00
2026-08-29 21:27:09 +10:00
2026-08-29 21:27:09 +10:00
2026-08-29 21:27:09 +10:00

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:

<media root>/
  fafflix/{movies,tvseries}/<title>/...   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

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:

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:

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>.

S
Description
Keyboard-centric web UI to mark media as cheeztv (kids) content via hardlinks; oauth2-proxy fronted, talks to sonarr/radarr APIs
Readme 92 KiB
Languages
Go 66.9%
JavaScript 15.3%
CSS 11.6%
HTML 3.8%
Makefile 2%
Other 0.4%