Note: This project was 99% written by Claude (Anthropic's AI model) using Claude Code. From architecture decisions to implementation details, the vast majority of the codebase — including the YouTube engine, download pipeline, TUI, web dashboard, BotGuard solver, and this README — was generated through AI-assisted development.
YouTube and Twitch live stream archiver with a terminal UI and web dashboard. Monitors channels, detects live streams, and downloads video + live chat automatically.
Written in Go. Single binary, no runtime dependencies beyond FFmpeg.
I kept the Moom because of Nanashi Mumei being my oshi. I might change it to a different name related to a certain orca in time, but for now, it's just Moombox.
- YouTube + Twitch — Monitors and downloads live streams from both platforms
- Channel monitoring — Independent RSS feed polling (YouTube), DECAPI polling (YouTube), and GQL polling (Twitch) with regex filtering on titles and descriptions
- Live stream archiving — Downloads DASH/HLS video segments in real-time with automatic parallel catch-up when falling behind
- Live chat capture — Archives chat alongside video (YouTube live chat + Twitch IRC), including pre-stream messages from the waiting room
- VOD downloads — Download regular videos and post-live DVR recordings with parallel segment fetching
- Resume on crash — Periodic state saves allow resuming interrupted downloads without data loss
- Parallel downloads — Process multiple streams simultaneously with configurable concurrency
- Quality monitoring — Probes stream quality every 30 seconds during live downloads. If the resolution or framerate changes mid-stream, automatically muxes the current segment and starts a new one — no data lost, no mismatched frames
- Manual format selection — Choose specific video and audio formats per download, or select "None" for video-only/audio-only
- Timestamp selection — Download a specific time range of a stream (start/end time), with frame-accurate trimming via FFmpeg re-encode
- Post-download trimming — Create trimmed clips from finished downloads with CRF-based encoding for optimal quality/size
- 60fps support — Prefers 60fps streams when available at the same resolution
- Auto-updater — Checks GitHub for new releases and downloads updates in-place. Apply updates from the web dashboard or TUI — the app restarts automatically with the new version
- Launcher/supervisor — A lightweight launcher process manages the app lifecycle. Config changes, updates, and setup wizard restarts are seamless — no terminal flicker, no process chain buildup
- HTTPS support — Auto-generated self-signed certificates with dual-protocol (TLS + plain HTTP) on a single port
- Password authentication — Optional password protection for external access (scrypt-hashed, session-based)
- Reverse-proxy aware —
trusted_proxiesmakes IP-based access control judge the real client behind nginx/Caddy/Traefik instead of the proxy (see Remote Access)
- Member-only support — Cookie-based authentication for members-only streams (YouTube + Twitch)
- Automatic cookie refresh — Keeps sessions alive with periodic background refresh with real API-based validation
- Auto cookie setup — Launch a browser from the dashboard or TUI, log in, and cookies are extracted automatically (Firefox recommended, Chromium supported)
- Auto-reacquisition — When cookies expire or are deleted, Moombox automatically re-launches the browser to reacquire them
- Terminal UI — Full-screen TUI built with Charmbracelet's Bubble Tea suite (Bubble Tea + Bubbles + Huh + Lip Gloss) with mouse support, keyboard navigation, job management, settings editor, and live logs
- Web dashboard — Real-time job monitoring at
localhost:774(HTTPS when external) with video player, synchronized chat replay (Niconico-style flying overlay + sidebar), settings management, and zip import - Mobile responsive — Web dashboard adapts to tablets and phones with reorganized layouts and touch-friendly controls
- Statistics dashboard — At-a-glance disk usage, total archive size, platform breakdown (YouTube vs Twitch), job counts, and activity metrics
- First-run wizard — Built-in setup wizard in both TUI and web dashboard for initial configuration (quick mode or 8-step advanced walkthrough)
- Process restart — Restart Moombox from the TUI or web dashboard when settings require it
- Native PO Token generation — Built-in BotGuard solver using Goja (pure-Go JavaScript engine, no CGo or V8)
- yt-dlp compatibility — Built-in PO Token HTTP endpoint and bundled yt-dlp plugin
- YouTube cipher decryption — Native implementation of signature and n-parameter decryption via Goja
- Webhook notifications — Notifications for stream events via any webhook-compatible service (Discord, Slack, ntfy, etc.)
- Single binary — Compiles to a single executable with embedded web assets, no external runtime dependencies
- Built-in FFmpeg installer — Install FFmpeg via Chocolatey or Winget directly from the setup flow, with UAC elevation support and script review for non-admin users
Running the pre-built executable:
- Windows x64, or Linux x64, or Linux arm64
- FFmpeg in your PATH (for muxing video + audio)
Running with Docker:
- Docker (x64 or arm64 host) — FFmpeg is included in the image
Building from source:
- Download
Moombox.exefrom the latest release - Place it in a directory of your choice
- Run
Moombox.exe
wget https://github.com/vampiricwulf/Moombox/releases/latest/download/moombox-linux-amd64
chmod +x moombox-linux-amd64
./moombox-linux-amd64wget https://github.com/vampiricwulf/Moombox/releases/latest/download/moombox-linux-arm64
chmod +x moombox-linux-arm64
./moombox-linux-arm64A built-in setup wizard walks you through first-time configuration on launch. The TUI opens by default — press W to open the web dashboard in your browser.
mkdir moombox && cd moombox
wget https://raw.githubusercontent.com/vampiricwulf/Moombox/main/docker-compose.yml
docker compose up -dThe dashboard is available at http://<host>:774. Everything (config,
database, logs, staging, finished downloads) lives under ./data; edit
./data/config.toml or use the dashboard's Settings page to configure.
Docker-specific behavior:
- Network access defaults to
"lan"(instead of"localhost") — the container is only reachable through the published port, and requests arriving over Docker's bridge network are never loopback, so a"localhost"default would make the dashboard unreachable. Use"127.0.0.1:774:774"as the port mapping to restrict access to the Docker host only. The compose file also declares an IPv6-enabled network — reach the dashboard over the host's IPv4 address (see Remote Access and the comments indocker-compose.yml). - The first-run setup wizard is skipped (the entrypoint seeds a config on first start); all of its settings are available in Settings.
- For members-only content, put a Netscape cookie file at
./data/cookies.txton the host — the./datavolume already exposes it and the seeded config points at/data/cookies.txt, so there is no extra volume line to add. Do not bind-mount the file individually (- ./cookies.txt:/data/cookies.txt): Moombox keeps the YouTube session alive by rewritingcookies.txtabout every 30 minutes with the values YouTube rotates back, and it does so with a temp file plus a rename. A rename cannot replace a single-file bind mount, so the write-back fails with only a warning in the log and the session quietly ages out. When the session does die, open Settings → Cookies in the dashboard and paste (or upload) a fresh Netscape export: Moombox merges it intocookies.txt, reloads it immediately and tells you whether it authenticates. Overwriting./data/cookies.txton the host works too and takes effect within 30 minutes, or right away if you press "Refresh cookies". The interactive browser login in Settings needs a headed browser and a person at it, so it is not an option here. Export from a private window and close that window afterwards: continuing to browse in the source profile rotates the session and invalidates the export. - Alternatively, mount a Firefox profile directory into the
./datavolume as./data/browser-profile. Moombox readscookies.sqliteout of it directly — no browser process involved, and nothing is written back into the profile — and writes what it finds intocookies.txt. Close Firefox before copying the profile, and copycookies.sqlitetogether with anycookies.sqlite-walbeside it; the main file alone can read as empty. Leaveauto_enabledoff: it only starts a headless-browser refresh timer, and the image has no browser. A first start with nocookies.txtimports the profile on its own; after that the profile is read when you ask for it — the Settings page's "Refresh cookies from browser profile" button, shift+click on the header's "Refresh cookies", orR Fin the TUI — because nothing inside the container changes the profile, so nothing else can know when there is something new to read. See Cookie Setup. - Update by pulling a new image (
docker compose pull && docker compose up -d) — an in-app update would be lost when the container is recreated, so the seeded config disables automatic update checks (auto_check_updates = false; the manual "Check for updates" button still works).
To build the image from source instead of pulling:
docker build -t moombox . from a checkout of this repository.
To add a video manually:
Moombox.exe add <video_url_or_id>To run without the TUI (web dashboard only):
Moombox.exe --no-tuiOther flags:
Moombox.exe --version # Show version and exit
Moombox.exe --config path.toml # Use a specific config file
Moombox.exe --log-level DEBUG # Override log levelOut of the box Moombox is reachable only from the machine it runs on
(network_access = "localhost"). The Docker image seeds "lan" instead,
because requests arriving through a published port come over Docker's
bridge network and never look like loopback. In both cases the dashboard
has no password and needs none — the IP filter is the boundary, and
loopback/private clients always skip authentication.
To reach the dashboard from outside that boundary, pick one of these — strongest first.
Put the host on a tailnet or WireGuard network and change nothing in
Moombox. VPN clients arrive with private addresses, so they pass the
lan filter as if they were on the LAN. No open ports, no password to
manage, and network membership is the authentication.
Terminate TLS at nginx/Caddy/Traefik and forward to Moombox. In
[network]:
network_access = "external"
trusted_proxies = ["172.18.0.2"] # the proxy's address — as narrow as possible
trust_forwarded_proto = true # proxy terminates TLS; Moombox sees plain HTTPtrusted_proxies is what makes this safe. Without it every forwarded
request is judged by the proxy's address — private or loopback, and
trusted either way — so internet traffic passes the lan filter and
skips authentication. With it, Moombox reads X-Forwarded-For (and only
from that peer) and applies the IP gate, the auth skip, and rate limiting
to the real client. Changes take effect immediately; no restart.
Declare the narrowest thing that works — the single proxy address, not a
/16. Anything inside a declared range is trusted to state who the
client is, including claiming a loopback address. That is inherent to
trusting a range, so keep the range small.
Configure the proxy to append to (or replace) X-Forwarded-For —
never to forward the client's header unchanged. Moombox trusts the
rightmost entry the proxy did not vouch for, which is safe only because
the proxy writes the address it actually saw to the right of whatever the
client sent. A bare pass-through makes the client's own value the
rightmost entry and reopens exactly the bypass this setting closes. In
nginx use proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
(or $remote_addr to replace); Caddy and Traefik append by default.
Either append style works — extending the existing header line or adding
a second X-Forwarded-For line (HAProxy's option forwardfor) — since
Moombox reads every line of the header, not just the first.
The address to declare is the one Moombox actually sees, which is not always the proxy's own IP:
- Proxy on the Docker host, connecting to
127.0.0.1:774— the connection is relayed bydocker-proxy, so the peer address is the bridge gateway (docker network inspect <network>reports it, commonly172.17.0.1). - Proxy as a container on a shared Docker network — the peer address is that container's address on the network.
- Moombox running directly on the host — the peer address is the proxy's
own IP, or
127.0.0.1if both are on the same machine.
When unsure, configure trusted_proxies last: while it is empty, any
Moombox log line that records a client address (the [Auth] login lines,
for example) shows the direct peer — exactly the value to declare.
Note that network_access must be external/public here, not lan.
Once trusted_proxies resolves the real client, that client is an
internet address — the lan filter would 403 it, which is the whole
point of resolving it.
Then choose where authentication happens:
- Moombox authenticates — set a dashboard password and keep
network_access = "external". Remote clients get the login page; loopback and private clients still skip it. Set the password before switching Network Access — see "Direct exposure" below for why, and for how to set the first password inside a container. - The proxy authenticates — set
network_access = "public"inconfig.toml. It behaves identically to"external"and exists to label this deployment; it is deliberately absent from the Settings dropdowns and rejected by the config API, because it is only meaningful behind an authenticating proxy. With no dashboard password, Moombox logs a startup warning and shows a red banner in the dashboard and the TUI — on purpose, because it cannot verify that your proxy really does authenticate. Setting a Moombox password as well clears the warning and gives you a second lock.
Either way, make the proxy the only route to Moombox's port: publish
it as 127.0.0.1:774:774, or put the proxy and Moombox on a shared
Docker network and don't publish the port at all. A directly reachable
port defeats both the proxy's authentication and trusted_proxies.
Set a dashboard password first, then network_access = "external" and
https_enabled = true.
That order is enforced: the web dashboard, the TUI, the config API, and
the setup wizard all refuse to enable external access while no password
is set, and removing the password drops network_access back to
"localhost" in the same write.
Setting the first password inside a container needs a workaround —
first-time password setup requires a loopback connection, and requests
through Docker's bridge are never loopback. Put the plaintext password in
/data/config.toml and restart the container:
[network]
password_hash = "your-password-here"Moombox detects that this is not a scrypt hash, converts it, and writes the hash back on the next start — the plaintext only sits in the file until then.
If a hand-edited config ends up on external/public with no password,
Moombox still boots — it logs a warning, reports passwordlessExternal
on /api/auth/status, and shows a persistent red banner in both UIs. It
never refuses to start, so an existing deployment fronted by an
authenticating proxy keeps working.
- Docker Desktop (Windows/macOS) proxies every inbound connection
through its VM, so Moombox sees all clients as the private gateway
address and the
lanfilter cannot tell them apart. There, the port publish is the only exposure control — keep it host-only or LAN-firewalled. - Published ports bypass
ufw/firewalldon Linux: Docker inserts its own DNAT rules, so a host firewall rule does not cover a published port. Restrict the publish itself (127.0.0.1:774:774) rather than relying on the firewall. - IPv6. Moombox binds IPv4 only.
docker-compose.ymldeclares an IPv6-enabled network so that inbound IPv6 to the published port is handled by ip6tables instead of Docker's userland proxy — which would otherwise re-originate those connections from the bridge gateway's private IPv4 address, making an internet IPv6 client look like a LAN client to thelanfilter. The practical effect is that IPv6 connections are refused at the container rather than misclassified: reach the dashboard over the host's IPv4 address. A hostname with an AAAA record generally still works, since browsers fall back to IPv4 after the refusal. This relies on Docker Engine 27+, where ip6tables is enabled by default; on older engines the misclassification remains and nothing in Moombox can detect it. Publishing as0.0.0.0:774:774stops the port accepting IPv6 in the first place. See the comments indocker-compose.yml, including what to do if your host cannot create an IPv6-enabled network at all.
Moombox includes a built-in first-time setup wizard — no manual configuration is necessary. All settings can be changed at any time from the Settings page in the web dashboard or TUI.
For advanced users, a config.example.toml reference is included. Moombox looks for config.toml in the current directory, ./config/, or ~/.config/moombox/.
| Setting | Default | Description |
|---|---|---|
port |
774 |
Web dashboard port |
network_access |
"localhost" |
"localhost", "lan", "external", or "public" — "public" behaves like "external" and is only settable in config.toml (Remote Access) |
trusted_proxies |
[] |
Reverse-proxy IPs/CIDRs whose X-Forwarded-For is honored (Remote Access) |
log_level |
"INFO" |
"DEBUG", "INFO", "WARN", "ERROR" |
downloader.max_video_resolution |
1080 |
Max resolution (based on max of width/height, handles portrait) |
downloader.cookie_file |
"./cookies.txt" |
Netscape-format cookie file |
downloader.download_chat |
true |
Download live chat alongside streams |
downloader.prefer_60fps |
true |
Prefer 60fps when same resolution available |
downloader.num_parallel_downloads |
2 |
Simultaneous download jobs |
downloader.output_template |
"${channel}/${start_date} ${title} [${id}]" |
Output path template |
feed_check_interval |
10 |
Minutes between RSS feed checks (also accepts "10m") |
twitch_check_interval |
15 |
Seconds between Twitch GQL live-status checks (with jitter) |
tasklist.hide_finished_age_days |
30 |
Days before finished jobs move to Archived (also accepts "30d") |
memory.go_soft_limit_mb |
256 |
Soft memory cap for the Go process (no OOM; just GC pressure as memory approaches it) |
memory.sidecar_soft_limit_mb |
200 |
RSS threshold at which Moombox tells the sidecar to run a full V8 GC |
memory.sidecar_hard_limit_mb |
512 |
Sidecar V8 --max-old-space-size ceiling (does OOM if hit; set well above the soft cap) |
Scan report · 2026-09-17
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
From the balcony · 0 of 1 clapped
Cap'm Slop read it and passed. Their reasons are on the balcony, with every other verdict.
Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.



0 comments
log in to comment.