- Go 68.4%
- HTML 20.1%
- CSS 9%
- Nix 1.1%
- Shell 0.9%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .direnv | ||
| data.fresh-1782635978/catalog | ||
| deploy | ||
| design | ||
| docs | ||
| internal | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| kokosbananas.db.fresh-1782635978 | ||
| main.go | ||
| module.nix | ||
| README.md | ||
| shell.nix | ||
Golten Stories
Self-hosted platform for audio programming for kids (Norwegian). Holds lossless masters as the source of truth, transcodes them into platform-tailored renditions, and distributes them as podcasts via RSS to Spotify and Apple Podcasts. A PWA for direct listening and stats tracking is planned but not yet built.
The domain model and decisions: docs/CONTEXT.md and
docs/adr/. Entity reference: docs/CATALOG.md.
Why these platforms and settings: docs/FINDINGS.md.
What it does
Lydlab (/tools/audio-preview-lab)
- Upload a WAV/FLAC master (streamed straight to disk).
- Measures it (EBU R128: integrated LUFS, true peak, LRA) via ffmpeg.
- Self-hosted rendition — your tunable "high-res" master (this is literally what Apple, Overcast, and Amazon/Audible listeners hear, since they don't re-encode).
- Spotify emulation — re-encodes the self-hosted file to Ogg Vorbis ~96 kbps, gained to −14 LUFS (how Spotify normalizes at playback).
- YouTube emulation — re-encodes to Opus 128 kbps, gained to −14 LUFS.
- Each rendition gets a post-encode measurement (LUFS + true peak — peaks above −1 dBTP are flagged) and an A/B player (playing one pauses the others).
- Tune & regenerate: change codec / bitrate / target loudness / true-peak ceiling; the emulations re-derive from your new self-hosted file (htmx swap).
Katalog (/katalog)
- Manage shows, episodes, persons, and credits — the publication admin surface. Seasons and Works are dormant in v1 (one per show/episode); the creator sees shows, episodes, persons, and credits.
- Upload lossless masters (WAV/FLAC) — content-hashed and deduped; immutable.
- Transcode per encoding profile — two versioned profiles (
main-v1,spotify-v2) seeded at startup. Synchronous stopgap; a job queue + worker are deferred (ADR-0005, ADR-0018). - Download statistics (
/katalog/statistikk, authed) — renders the precomputed kloggos report (per-episode, per-week, per-platform origin fetches) fetched from the kloggos JSON API over the closed Incus bridge. kloggos owns data production; this app owns presentation. Degrades to a friendly page when kloggos is unreachable (ADR-0007).
Feeder (/feed/…)
- Generate PSP-1 RSS feeds per
(Show × Edition × Platform)— a pure projection of canonical metadata, rendered to disk and served as a static file (ADR-0015). - Per-platform enclosures — each feed encloses the rendition from its platform profile, served from our own origin; downloads are measured at the origin, not via a third-party prefix (ADR-0017, supersedes ADR-0008).
- Manual refresh — the operator clicks "Generer/Oppdater" on the show page. Automatic regeneration on catalog change is deferred (ADR-0004 phasing).
Stack
Go (stdlib only) · ffmpeg via subprocess · htmx + tiny vanilla-JS helpers · SQLite · local-disk storage. No npm, no TypeScript, no build step. Single binary; templates/assets embedded.
internal/audio ffmpeg/ffprobe adapter (probe, measure, encode, decode) ← ADR-0005
internal/catalog domain model & store interface (Show/…/Feed) ← ADR-0003
internal/catalog/sqlite SQLite implementation of the catalog store ← ADR-0009
internal/feed PSP-1 RSS renderer (pure function, golden-file tested) ← ADR-0004/0015
internal/store one dir per upload + meta.json on disk (preview lab) ← ADR-0009
internal/pipeline master → self-hosted → emulations; profile-driven Transcode
internal/stats kloggos JSON API client (download statistics) ← ADR-0007
internal/web HTTP handlers + embedded templates/static
main.go wiring + startup ffmpeg check + catalog DB + profile seed
Run
ffmpeg/ffprobe must be on PATH (or set KOKOS_FFMPEG / KOKOS_FFPROBE).
# NixOS: brings go + ffmpeg into scope
nix-shell
# build is pure-Go (no cgo / no gcc needed)
CGO_ENABLED=0 go run . # serves http://127.0.0.1:8080
# or:
CGO_ENABLED=0 go run . -addr 127.0.0.1:9000 -data ./data
# with feed generation (base URL required for absolute enclosure/feed URLs):
CGO_ENABLED=0 go run . -base-url http://localhost:8080
# live-reload (watchexec from shell.nix): rebuilds + restarts on .go/.html/.css edits.
# templates and static assets are embedded, so a restart is required to see changes.
watchexec -r -e go,html,css -- CGO_ENABLED=0 go run . -addr 127.0.0.1:9000 -base-url http://localhost:8080
| Flag | Env | Default | Purpose |
|---|---|---|---|
-addr |
— | 127.0.0.1:8080 |
listen address |
-data |
— | data |
lab upload dir |
-db |
— | kokosbananas.db |
catalog SQLite path |
-catalog-data |
— | data/catalog |
catalog media dir (masters, renditions, feeds) |
-base-url |
KOKOS_BASE_URL |
(none) | public origin for RSS enclosure/feed URLs (required to generate feeds) |
-kloggos-url |
KLOGOS_API_URL |
http://kloggos.incus:8080 |
kloggos JSON API base URL for /katalog/statistikk |
| — | KLOGOS_API_TOKEN |
(none) | optional Bearer token sent to the kloggos API (unused by kloggos today) |
| — | KOKOS_OIDC_ISSUER |
(none) | OIDC provider (e.g. https://tilgang.golten.as); when set, the whole site except /feed, /audio, /images requires login. Unset = no login (local dev) |
| — | KOKOS_OIDC_CLIENT_ID / KOKOS_OIDC_CLIENT_SECRET |
(none) | OIDC client credentials (secret optional for a public PKCE client) |
| — | KOKOS_OIDC_REDIRECT_URL |
<base-url>/auth/callback |
callback URL registered at the provider |
| — | KOKOS_SESSION_SECRET |
random per start | HMAC key for session cookies; set it or everyone is logged out on restart |
| — | KOKOS_FFMPEG / KOKOS_FFPROBE |
PATH | ffmpeg/ffprobe binary paths |
Then open the address: / is the landing page, /katalog is the admin,
/katalog/statistikk is the download report (authed), /tools/audio-preview-lab is the lab.
Tests / checks
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go vet ./...
CGO_ENABLED=0 go test ./...
Known limitations
- Loudness is matched with a single-pass static gain, so output can land ~1 dB off target.
Two-pass
loudnormwould tighten it — deferred. - Emulations are approximations, not bit-exact (we don't have the platforms' exact encoder builds). They reliably surface artifacts and post-encode true-peak clipping.
- Transcoding is synchronous on request; fine for short masters. A job queue comes later (ADR-0005 phasing).
- Feeds are regenerated manually — after a metadata or rendition change, click "Oppdater" on the show page. Automatic regeneration is deferred (ADR-0004 phasing).
- nb-NO only — the path structure is language-keyed, but only Norwegian editions are wired (ADR-0012).
- Stats are read-only and sourced from kloggos (
/katalog/statistikk): as fresh as kloggos' last report run, and the page degrades to a friendly note when kloggos is unreachable. The in-app analytics store (ADR-0007) and PWA (ADR-0010) are still deferred.