Self-hosted music app that owns its data: a Beatbump fork with a local library, full offline PWA and automatic acquisition
  • Go 30.2%
  • TypeScript 28.6%
  • Svelte 19.9%
  • JavaScript 13.7%
  • Shell 3.5%
  • Other 4%
Find a file
beatbump-agents 96b55e047d
All checks were successful
CI / Web (vitest + svelte-check ceiling) (push) Successful in 1m3s
CI / Shell (shellcheck) (push) Successful in 21s
CI / Python (py_compile + ruff) (push) Successful in 14s
CI / Harness headers (X-Ytm-Harness on every Playwright context) (push) Successful in 10s
CI / Compose (deploy/compose.yml config) (push) Successful in 14s
CI / Go (vet + test) (push) Successful in 2m11s
Release / Build and push images (push) Successful in 2m3s
Release / Forgejo release (push) Successful in 11s
Remove host-specific defaults from the audit scripts and comments
Audit scripts default to http://127.0.0.1:8080, take the resolver from
HARNESS_RESOLVER and the analytics host to block from YTM_ANALYTICS_HOST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:06:49 +00:00
.forgejo Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
.github Add the 1.0.0 changelog, repository hygiene files and the images override 2026-10-05 11:50:25 +00:00
.idea intellij/visual studio 2025-05-23 11:43:15 -07:00
.vscode fix import 2025-11-28 20:30:41 -08:00
app Use neutral names in app placeholders, test fixtures and comments 2026-10-06 12:06:49 +00:00
backend Use neutral names in app placeholders, test fixtures and comments 2026-10-06 12:06:49 +00:00
bridge Remove host-specific defaults from the audit scripts and comments 2026-10-06 12:06:49 +00:00
deploy Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
docs Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
e2e Remove host-specific defaults from the audit scripts and comments 2026-10-06 12:06:49 +00:00
fixtures Sample library: a rare genre; e2e: rare-genre precondition and an album row fallback 2026-10-05 16:32:15 +00:00
indexer Re-index everything when the Meilisearch index is empty but the state is not 2026-10-05 11:41:52 +00:00
ops Make ops/env.example a generic layout and document ops/env.local 2026-10-06 12:06:49 +00:00
scripts/release Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
yubal Remove host-specific defaults from the audit scripts and comments 2026-10-06 12:06:49 +00:00
.dockerignore Add the 1.0.0 changelog, repository hygiene files and the images override 2026-10-05 11:50:25 +00:00
.editorconfig Add the 1.0.0 changelog, repository hygiene files and the images override 2026-10-05 11:50:25 +00:00
.gitattributes Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
.gitignore Delete stray Vite config bundles and ignore them 2026-10-06 12:06:49 +00:00
AGENTS.md Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
Beatbump.code-workspace Fixed error 400 when using Oauth 2025-09-11 21:57:15 -07:00
CHALLENGES.md Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
CHANGELOG.md Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
CONTRIBUTING.md Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
docker-compose.yaml Update readme 2026-02-22 23:36:09 -08:00
Dockerfile Ship the app without a hard-coded analytics script; make it an optional build value 2026-10-05 15:57:15 +00:00
frontend.iml initial commit 2024-08-11 12:29:32 -07:00
go.mod Refactoring and refining 2025-11-24 00:28:36 -08:00
go.sum Refactoring and refining 2025-11-24 00:28:36 -08:00
healthcheck.go ops(healthcheck): beat-server -healthcheck probe for the scratch image (B7-14) 2026-10-01 23:46:27 +00:00
healthcheck_test.go ops(healthcheck): beat-server -healthcheck probe for the scratch image (B7-14) 2026-10-01 23:46:27 +00:00
LICENSE Document the fork: README, AGENTS, CHALLENGES, CONTRIBUTING, docs, LICENSE 2026-10-05 11:39:15 +00:00
logger.go perf(obs): cache verdicts in the access log, log-latency.py excludes monitors and harness (PF5-8) 2026-10-02 07:34:02 +00:00
logger_test.go perf(obs): cache verdicts in the access log, log-latency.py excludes monitors and harness (PF5-8) 2026-10-02 07:34:02 +00:00
main.go Use neutral names in app placeholders, test fixtures and comments 2026-10-06 12:06:49 +00:00
main_test.go fix(server): trust X-Forwarded-For only from private proxies (L9-4) 2026-10-01 16:51:07 +00:00
Makefile Add Forgejo CI and release workflows with the scripts they call 2026-10-05 11:50:25 +00:00
NOTICE Document the fork: README, AGENTS, CHALLENGES, CONTRIBUTING, docs, LICENSE 2026-10-05 11:39:15 +00:00
og_preview_main_test.go feat(share): Open Graph cards for link-preview robots (AP3) 2026-10-01 15:25:18 +00:00
README.md Drop the private program archive and sanitise the documentation 2026-10-06 12:06:49 +00:00
SECURITY.md Add the 1.0.0 changelog, repository hygiene files and the images override 2026-10-05 11:50:25 +00:00
spa_notfound.go fix(spa): bienvenue is a SvelteKit route root (direct load answered 404, TestSpaRootsMatchSvelteKitRoutes) 2026-10-02 01:24:38 +00:00
spa_notfound_test.go fix(shell): missing /_app/ files answer 404 no-store, SW never stores HTML as an asset 2026-10-01 19:26:41 +00:00
static_etag.go c59g: SPA routes carry the shell ETag so a rollback never freezes the new shell 2026-10-05 01:34:05 +00:00
static_etag_test.go c59g: SPA routes carry the shell ETag so a rollback never freezes the new shell 2026-10-05 01:34:05 +00:00
up.sh Fresh install: pack step checks a pack finished before the Espace card mounted; up.sh reports derived counts 2026-10-06 08:17:39 +00:00
VERSION Add Forgejo CI and release workflows with the scripts they call 2026-10-05 11:50:25 +00:00

beatbump (self-hosted, own-your-data music app)

A fork of Beatbump (SvelteKit + Go) turned into a complete, self-hosted music app. It plays your local library and the YouTube Music catalogue through one interface, builds the local library from what you listen to, and works fully offline as an installed PWA. The goal is feature parity with YouTube Music or Spotify on infrastructure you own. The repository keeps the name beatbump to honour the upstream authors.

What makes it different:

  • Local and online, transparently. One search returns your library and YouTube. When a track exists in both, the player serves the best copy without asking.
  • Best quality wins. Every local track has a quality score (codec, bitrate, sample rate). If the copy being served is online or lower quality, a better one is fetched in the background and replaces it in place.
  • The library builds itself from listening. Following an artist or liking a track queues a slow, quota-respecting download of the matching content. The more you listen, the more you own.
  • Full offline PWA. The app shell, the pages you visited and every track you explicitly kept (or listened to for long enough) play with no network.
  • Own your data. Profiles, favourites, follows, playlists, history and statistics live in a SQLite file on your server. Audio files live in a folder you control. Nothing leaves the box.

The user interface is in French, by decision of the owner (program decisions 1 and 5): the app was built for a French-speaking circle, and a dictionary with browser detection was judged more costly than useful. All UI strings live in the SvelteKit sources under app/src (Svelte markup, the toast and label modules, and the DROPDOWN_LABELS_FR / SHELF_TITLES_FR tables in app/src/lib/configs/dropdowns.config.ts and the search route). A structural test, app/src/lib/utils/frenchScreens.test.ts, scans those files for English UI words. An i18n contribution would replace those literals with a lookup and extend or retire that test; see CONTRIBUTING.md.

Screenshots

No screenshots are committed yet. Run the app and take your own, or see the UX audit captures produced by e2e/ux-audit/shots.cjs (not in the repository).

Quick start

Requirements: Docker Engine with the compose plugin (docker compose), a Linux host, a folder of music files (optional: the sample library is enough to try the app).

git clone <this repository> beatbump
cd beatbump
./up.sh --sample-library

Then open http://localhost:8080.

up.sh writes deploy/.env from deploy/.env.example on the first run, generates the three secrets, creates deploy/data/, builds the images, starts the stack and waits for every container to be healthy (HEALTH_TIMEOUT, 600 s by default). --sample-library fills an empty MUSIC_DIR with 18 short CC0 synthetic tracks (fixtures/make-sample-library.sh) so the app has content.

To index your own music, set MUSIC_DIR in deploy/.env to your music folder (relative paths resolve from deploy/; MUSIC_DIR is mounted read-only in the indexer and the bridge, and yubal writes its downloads to MUSIC_DIR/ytm) and run ./up.sh again. The indexer walks the folder, reads the tags and fills Meilisearch every INDEXER_SCAN_INTERVAL seconds; the library pages fill up as it goes. See deploy/VERIFICATION.md for what to check after the first start.

Other up.sh commands:

Command Effect
./up.sh first run: create deploy/.env and deploy/data, build, start, wait for health; later runs: rebuild and restart
./up.sh --sample-library same, and generate the sample library if MUSIC_DIR has no audio file
./up.sh down [-v] stop the stack (-v also deletes the Meilisearch and companion volumes)
./up.sh logs [service] follow logs
./up.sh ps container status and health
./up.sh update git pull --ff-only, rebuild with --pull, restart, wait for health
./up.sh build rebuild the images only

Environment overrides read by up.sh: COMPOSE_PROJECT_NAME (default beatbump; several stacks can coexist), BEATBUMP_PORT, MUSIC_DIR and BEATBUMP_BIND (written into deploy/.env on the first run), HEALTH_TIMEOUT (600 s), BUILD_PARALLEL=1 to build the four images in parallel (default: one at a time, kinder to a loaded host; ./up.sh build <service> rebuilds one image). The raw equivalent once deploy/.env exists: docker compose -p beatbump --project-directory deploy -f deploy/compose.yml up -d.

Requirements

Item Why
Docker Engine 24+ with docker compose v2 every service runs as a container
Linux host (x86_64) the app image is FROM scratch with a static Go binary and ffmpeg from Alpine
A music folder (read-only is fine) indexed by indexer/indexer.py, served by bridge/bridge.py
Disk for Meilisearch and the SQLite database the index of a 56 000-track library is a few hundred MB
Outbound HTTPS YouTube playback through invidious-companion, lyrics through lrclib.net
Optional: a residential egress proxy and a po-token provider YouTube blocks many datacenter IPs; see "Status and limits"

Node 22 and Go 1.24 are only needed to build outside Docker (see "Testing").

Configuration

All configuration is environment variables. deploy/.env.example is the reference file; ./up.sh copies it to deploy/.env (mode 600) on the first run and fills the secrets. Edit deploy/.env afterwards, never the example. Relative paths resolve from deploy/.

deploy/.env (what you edit)

Variable Default Role
BEATBUMP_PORT 8080 host port published for the app (the only published port)
BEATBUMP_BIND 0.0.0.0 host address to bind (127.0.0.1 = this machine only)
MEILI_MASTER_KEY generated (64 hex) Meilisearch master key (16 characters minimum); also used by beatbump, bridge and indexer
COMPANION_SECRET_KEY generated (16 hex) shared secret between beatbump and invidious-companion; exactly 16 characters (companion constraint, up.sh refuses otherwise)
YTM_ADMIN_TOKEN generated (64 hex) Bearer token for the operator-only endpoints (GET /api/v1/client-log)
MUSIC_DIR ./data/music your music folder, mounted read-only; downloads go to MUSIC_DIR/ytm, the sample library to MUSIC_DIR/sample
INDEXER_SOURCES * top-level folders of MUSIC_DIR to index: * = all of them plus loose files at the root, or a list such as ytm,lidarr,soulseek
INDEXER_SCAN_INTERVAL 300 seconds between two library scans (new files appear after a scan)
PUID, PGID the user running up.sh (1000:1000 when root) uid:gid the containers run as; deploy/data is owned by them
BEATBUMP_AUTOCACHE true queue a yubal download of what you play when it is not in the library yet
YTM_ACQUIRE_DAILY_CAP 20 albums or playlists auto-acquired per profile per UTC day (0 = unlimited)
YTM_REPORT_EMAIL empty contact shown on /about for reports (optional; without it the page offers "copy the diagnostic")
BEATBUMP_VERSION dev, replaced by the git short sha when available version reported by /api/v1/stats/library and baked into the image
TZ UTC timezone of yubal's scheduler
YUBAL_AUDIO_FORMAT opus download codec (m4a, mp3, flac also accepted by yubal)
YUBAL_LOG_LEVEL INFO yubal log level
EGRESS_PROXY empty (direct) HTTP proxy for every YouTube-facing call: companion, bgutil, yubal, the bridge's /vp, the app's background lookups (http://user:pass@proxy:8080)
IVVP_UPSTREAM empty (disabled) logged-in player sidecar (iv-vp protocol) used when the anonymous companion is walled; empty keeps audio on the app's /vp proxy
INVIDIOUS_UPSTREAM empty (/iv/* answers 502) Invidious instance behind the bridge's /iv/* routes (only an external ytify client uses them)
COMPANION_IMAGE quay.io/invidious/invidious-companion:latest pin a tag here if latest misbehaves

deploy/compose.yml maps these onto each service and sets the wiring that never changes (COMPANION_URL=http://bridge:8789: the bridge fronts the companion and adds the local-library resolution; same-origin LOCALF_BASE=/localf, COVER_BASE=/cover; YTM_PREFER_IVVP_AUDIO=0 since the stack has no iv-vp; the companion under base path /companion with deploy/companion/config.toml). The services expose more knobs than the .env file; the tables below list every variable each one reads, as the code defines it, for an operator who writes their own compose file.

beatbump (Go server, main.go and backend/)

Variable Default Role
COMPANION_URL required base URL of invidious-companion, or of the bridge that fronts it (backend/_youtube/api/api.go)
COMPANION_SECRET_KEY required API key shared with invidious-companion (SERVER_SECRET_KEY on the companion side)
BEATBUMP_DB_PATH /db directory of the SQLite database inside the container
MEILI_URL required Meilisearch base URL
MEILI_KEY required Meilisearch API key
LOCALF_BASE base path of local audio URLs; /localf keeps them same-origin
COVER_BASE base path of cover URLs; /cover keeps them same-origin
IVVP_URL sidecar that serves /aud/<videoId> (progressive audio); optional
AUDIO_PUBLIC_BASES built-in table mapping of public audio bases to proxy paths (backend/api/audioproxy.go)
RESIDENTIAL_PROXY empty (direct) outbound HTTP proxy for the background InnerTube next lookups (e.g. a residential egress proxy)
PLAYER_TIMEOUT_SECONDS 20 budget of one player.json call to YouTube
BEATBUMP_AUTOCACHE true a play of a track you do not own queues its acquisition; false, 0, no or off disables
YTM_ACQUIRE_DAILY_CAP 20 acquisitions (albums) per profile per UTC day; 0 = no cap
YTM_PREFER_IVVP_AUDIO 1 0 disables the switch of YouTube audio to /aud
YTM_API_CACHE 1 0 disables the server response cache (backend/api/rescache.go)
YTM_STATS_INCLUDE_HARNESS unset 1 records plays made by the test harness (staging only)
YTM_ADMIN_TOKEN unset Bearer token for GET /api/v1/client-log; the route answers 404 while unset
YTM_REPORT_EMAIL unset "Report a problem" mailto link on /about; without it the page offers "Copy the diagnostic"
YTM_VERSION dev version reported by /api/v1/stats/library when the image was not built with --build-arg VERSION=

bridge (bridge/bridge.py, local-first audio and cover server)

Variable Default Role
PORT 8789 listen port
LIBRARY_DIR /library root of the music files served by /localf
MEILI_URL, MEILI_KEY http://meili:7700, empty the index used for title + artist matching
COMPANION_UPSTREAM http://companion:8282 invidious-companion
INVIDIOUS_UPSTREAM empty (/iv/* answers 502) Invidious API behind /iv/* (optional)
IVVP_UPSTREAM empty (disabled) logged-in player sidecar (optional)
YUBAL_URL http://yubal:8000 acquisition service
GOST_PROXY empty (direct) egress proxy for /vp streams
VP_PUBLIC_BASE, LOCALF_PUBLIC_BASE, AUD_PUBLIC_BASE /vp, /localf, /aud public bases rewritten into player answers (same-origin relative paths)
STATE_DIR /app/state last-access log and cover cache
COVER_CACHE_DIR $STATE_DIR/covers on-disk cover cache
COVER_SEM_N 4 concurrent cover extractions
COVER_MISS_TTL 3600 seconds a missing cover is remembered
COVER_MAX_PX 600 target size (resizing needs Pillow, not in the image: covers are stored as is)
DEBOUNCE_TTL 21600 seconds before the same track is enqueued again
LOCAL_CACHE_TTL 600 seconds a local-match answer is cached
LA_FLUSH_SECS 60 last-access flush interval

indexer (indexer/indexer.py)

Variable Default Role
MUSIC_ROOT /music mounted music folder
SOURCES * top-level folders of MUSIC_ROOT to index (* = discover them all)
MEILI_URL, MEILI_KEY http://meili:7700, empty target index (the indexer waits for Meilisearch instead of crash-looping)
INDEX tracks index name (albums and artists are derived)
SCAN_INTERVAL 1800 seconds between incremental scans (the compose file passes INDEXER_SCAN_INTERVAL)
BATCH 1000 documents per upsert
STATE_DIR /state index_state.json (mtimes)

yubal (yubal/, acquisition)

Variable Default Role
YTM_YTDLP_PROXY empty (direct) proxy forced into every yt-dlp download (EGRESS_PROXY in the stack)
YTM_POT_BASE_URL http://bgutil:4416 bgutil po-token provider (optional)
YUBAL_* see deploy/compose.yml yubal's own settings (YUBAL_AUDIO_FORMAT, YUBAL_AUDIO_QUALITY, YUBAL_FETCH_LYRICS, YUBAL_REPLAYGAIN, YUBAL_TZ, YUBAL_LOG_LEVEL)

The yubal image is rebuilt from ghcr.io/guillevc/yubal (--build-arg YUBAL_BASE= to pin) with deno, the bgutil plugin and the latest yt-dlp; rebuild it (./up.sh build yubal) when downloads start failing in bulk.

Architecture

            browser / installed PWA (service worker: shell, API, audio caches)
                                  |
                                  v  :8080
   +------------------------------------------------------------------+
   |  beatbump  (Go, Echo)  static SPA build + /api/v1/*               |
   |   - local pages, profiles, stats, mixes      -> Meilisearch       |
   |   - YouTube pages (search, home, next...)    -> bridge/companion  |
   |   - /localf /cover /vp (reverse proxy)       -> bridge            |
   |   - /aud/<id> (reverse proxy)                -> iv-vp (optional)  |
   |   - SQLite (profiles, favourites, history, acquire jobs)          |
   +------------------------------------------------------------------+
         |                 |
         v                 v
   +-----------+   +-----------------+   +---------------------------+
   | meili     |   | bridge (python) |   | invidious-companion       |
   | tracks,   |<--| match title +   |-->| (/companion base path)    |
   | albums,   |   | artist, serve   |   | YouTube player / streams  |
   | artists   |   | /localf from    |   +---------------------------+
   +-----------+   | disk, covers,   |          |  EGRESS_PROXY, bgutil po-tokens
         ^         | enqueue yubal   |          v
         |         +-----------------+       YouTube
         |                 |
   +-----------+           v
   | indexer   |   +-----------------+
   | tags ->   |   | yubal (yt-dlp)  |  slow acquisition of what you listen to
   | meili     |   | writes files    |  (deno, bgutil plugin, EGRESS_PROXY)
   +-----------+   +-----------------+
         ^                 |
         |                 v
       MUSIC_DIR  <--  MUSIC_DIR/ytm   (files appear, the indexer picks them up)

Request flows in detail, including offline playback, the service worker update and the rollback path: docs/ARCHITECTURE.md.

Features

Search and playback

  • Unified search over the local library and YouTube, with suggestions and typo-tolerant local suggestions.
  • Immediate playback; the best copy is served when a track exists both locally and online.
  • Persistent bottom player with queue, play/pause, next/previous, shuffle, repeat, sleep timer, keyboard shortcuts and MediaSession (lock screen) handlers.
  • Clickable artist link from the player and every track row.

Library

  • Artists, Albums, Tracks, Genres pages as grids with sorting, filters, infinite scroll, an A-Z index on mobile, "no year" and "never played" filters.
  • Full artist (top tracks and albums), album (tracklist) and playlist pages for local and YouTube content.
  • Library hub with quick access to For you, Artists, Albums, Tracks, Genres, Favourites, Recent, Offline and Account.
  • Mixes by decade, genre, year and crossovers; album and artist of the day; duplicate-album report.

Radio and queue

  • Starting a track starts an infinite radio of similar tracks (real YouTube radio, also for a local track whose online twin is found); local continuation inside the same decade, genre or mix.
  • Prefetch of the next tracks for seamless transitions (measured gap: 29 ms median on cached local tracks).
  • Queue actions: play next, add to queue, drag to reorder, clear, save the queue as a playlist.
  • Tracks skipped twice are excluded from recommendations.

Account and personal data

  • Sign in by first name (multi-user profiles stored on the server, no password by owner's decision).
  • Favourites, followed artists, server playlists, listening history: everything persists across sessions and devices. An anonymous device profile keeps the same features without signing in, and is merged into the named profile at sign-in.
  • Resume across devices: "Resume from " card, "Continue here" takes over the queue.

Recommendations and statistics

  • For you / Made for you: a mix built from your plays and favourites.
  • Statistics: recent and most played, top by artist, streaks, listening clock (day x hour), decades, "Your year", CSV export, shareable week.

Automatic acquisition

  • Following an artist queues the progressive download of its discography into the local library (yt-dlp through yubal, po-tokens, rate limited, through a residential proxy).
  • Playing a track you do not own queues it too (BEATBUMP_AUTOCACHE), capped per profile per day (YTM_ACQUIRE_DAILY_CAP).
  • Download queue with pause / resume / retry per task.

Quality

  • Quality score per track (codec, bitrate, sample rate).
  • Upgrade in place: a better copy replaces a lower-quality one in the background.

Lyrics

  • Synced lyrics from lrclib.net, from the player and the fullscreen view.

Downloads, three ways

  1. Offline (in the app): the service worker caches the track for playback without network.
  2. To the device: saves the real audio file (Downloads / Files) for tracks in the local library.
  3. Server side: adds the track or discography to the local library (see Acquisition).

Offline (PWA)

  • App shell and already-visited data available offline; explicit "keep offline" per track, album, playlist or mix; pins never evicted; storage quota and "free up space"; sized packs ("1 hour", by duration) and a Friday weekend pack; data-saver mode.
  • A track is auto-cached only after a counted play (half the track or two minutes), so a single skipped try never downloads 300 MB silently.
  • After an update the app reloads once, at the end of the current track or on pause, never mid-song.

Mobile

  • Touch fullscreen player: Back, View artist, Download, Lyrics, close by swipe down; share target (Android) accepts YouTube, Spotify, Deezer and Apple Music links.

Sharing

  • Open Graph cards for link-preview robots (WhatsApp, Telegram, Signal, Discord...) on /listen, /release and /playlist; humans get the normal app.

Updating and rollback

The app image is promoted, never rebuilt in production: the exact image that passed the staging harness is tagged and started. ops/promote.sh does that and, before recreating the container, saves the image to a verified tarball (gzip -t, size check), keeps the previous image under a prod-backup-<timestamp> tag and the previous compose file under docker-compose.yml.pre-*. It prints the two exact rollback commands (by tag and by tarball) at the end.

Why tarballs: a scheduled Docker cleanup once deleted the production image and its rollback tags under the running container (containerd image store). Tarballs outside the Docker store are the real backup; ops/smoke.sh restores the tag from the tarball every morning if it is missing.

Why a "deploy survives" probe: e2e/probe-deploy-survives.cjs keeps one persistent browser profile across two builds (seed on the old build, verify after the upgrade, verify again after a rollback). It is mandatory whenever app.html, manifest.json, the service worker, the root layout or anything the service worker imports changed. It is what found the rollback bug fixed in cycle 59 (SPA routes now carry the shell's content ETag, static_etag.go).

Full procedures: docs/OPERATIONS.md.

Testing

Unit and static checks (run in each tree before merging):

# Go (the Dockerfile uses golang:1.24.5)
go vet ./... && go test ./...

# Front end
cd app && npm ci && npx vitest run
npm run check          # svelte-check; the error count must not exceed ops/svelte-check.baseline (411)

The svelte-check baseline is a ceiling inherited from upstream, never a target: a change may lower it (commit the new number in the same change) and must never raise it.

End-to-end, in a real browser (Google Chrome in a Playwright container with --network host, run from the host that serves the app; Docker is the only requirement):

cd e2e
./run.sh                                                  # http://127.0.0.1:8080, harness-core.cjs, fixtures query
./run.sh http://127.0.0.1:8080 "" harness-core.cjs        # ~100 steps: browse, play, queue, lyrics, stats...
./run.sh http://127.0.0.1:8080 "" harness-offline.cjs     # 20 steps: caching, pins, packs, offline playback
./run.sh http://127.0.0.1:8080 "" harness-smoke.cjs       # 5 steps in under 90 s

run.sh [url] [query] [harness] [extra harness args...]: the URL defaults to YTM_URL or http://127.0.0.1:8080; an empty query uses the query key of the fixtures file. Options (environment variables, forwarded to the container):

Variable Values Effect
HARNESS_TIER chain (default), full chain skips four slow, never-regressing steps; full plays everything (used on production after a promotion)
HARNESS_ONLY a,b play only those steps (a new step is always played alone first)
HARNESS_FIXTURES <file> fixtures file instead of e2e/fixtures.json (the steps' tracks, album, artist, genre, robot UAs)
HARNESS_RESOLVER MAP *.example.org 127.0.0.1 Chrome --host-resolver-rules, used only when the URL host is a name (a host whose hairpin NAT is broken)
HARNESS_RESOLVE_IP 127.0.0.1 where the harness's Node-side requests connect for that name (SNI and Host kept); defaults to the single IP of HARNESS_RESOLVER
HARNESS_STATS_INCLUDE_HARNESS 0, 1 override the runtime probe that decides whether the target records harness plays (YTM_STATS_INCLUDE_HARNESS=1 on the server); the steps that need counted plays are skipped otherwise and listed in report.envSkipped
YTM_SMOKE_EXPECT <sha> the version the smoke harness must see
HARNESS_GOTO_QUIET_MS, HARNESS_GOTO_CAP_MS ms the post-navigation quiet window (1500) and its cap (4000)
YTM_RUN_SKIP_BUILD_WAIT, YTM_BUILD_LOCK, HARNESS_LOAD_MAX, HARNESS_WAIT_MAX the guards: one browser at a time (exit 2), wait for the build lock, wait for a 1-minute load under 60 (15 min max)
HARNESS_IMAGE, HARNESS_CHROME_IMAGE image overrides (mcr.microsoft.com/playwright:v1.47.0-jammy, ytm-harness-chrome:1.47.0 built from e2e/chrome-image)

Each run writes e2e/out/<timestamp>/report.json (passed, failed, upstream, skipped, skippedTier, envSkipped, gated, per-step duration, firstSoundMs) and one screenshot per step; the last console line is Report: N passed / M failed / K upstream / S skipped. A step whose precondition is not met (library too small, no owned YouTube track in the fixtures) prints SKIP <name> - <reason> and never counts as a failure. e2e/fixtures.json pins the tracks, album, artist and genre the harness uses so that no run triggers a new acquisition; on a fresh install write your own fixtures file from your library (/api/v1/local/songs, /api/v1/local/albums) and pass it with HARNESS_FIXTURES. Every browser context sends X-Ytm-Harness: 1; node e2e/check-harness-headers.cjs verifies that statically. e2e/harness-lib.cjs holds the shared helpers (rawRequest, skip, requireLibrary, loadFixtures).

The autonomous loop

The repository ships the loop that produced it: a staging chain (ops/stage-cycle.sh: build, restart staging, header checks, both harnesses), guards (svelte-check ceiling, harness header check, gated-step check, deploy-survives probe), promotion with verified backups and a browser smoke (ops/promote.sh, ops/smoke.sh), a journal (ops/journal.py), weekly measurements with alert thresholds (ops/weekly.sh, ops/monday.sh) and a recipe for audits and brainstorms. In a 48-hour program (30 September to 2 October 2026) it ran 58 promoted cycles over 74 staging chains, then a cycle 59 on 4 and 5 October that applied 21 pending product decisions by their recommended defaults. AGENTS.md is the hand-over for an AI agent taking the loop over; ops/README.md documents each script; the lessons are in CHALLENGES.md and AGENTS.md, and the raw (French) journal of the program is kept privately by the maintainers.

Status and limits

  • YouTube playback depends on invidious-companion and on your IP reputation. Datacenter IPs are often blocked or throttled; the production setup uses a residential egress proxy and a bgutil po-token provider. yt-dlp must be kept current (YouTube changes its SABR and n-sig schemes; see CHALLENGES.md). Local playback never depends on YouTube.
  • French UI (see the top of this file).
  • Single-box design. One Go server, one SQLite file, one Meilisearch, one bridge. No horizontal scaling, no multi-tenant isolation; profiles are first names without passwords and the app is meant to sit behind your own reverse proxy and bot wall.
  • The bridge, the indexer and yubal have no staging in the production layout; changing them is a maintenance window.
  • The ops/ scripts were written for one production host (its paths, host names and container names); ops/README.md and ops/env.example say how to adapt them. up.sh and deploy/ are the portable part.
  • Upstream merges need a re-patch: this fork diverged widely from Beatbump.

Credits and license

This repository is derived from Beatbump by snuffyDev, published under the GNU Affero General Public License v3.0 (AGPL-3.0), through the giwty/Beatbump fork (which carries no license file of its own). This repository keeps AGPL-3.0: the full text is in LICENSE, the attribution is in NOTICE. If you run a modified version for other people over a network, the AGPL requires you to offer them the corresponding source.

Repository, mirror and releases

  • Canonical repository: https://forgejo.ekaii.fr/Ekaii/beatbump (issues, pull requests, CI).
  • GitHub mirror (read-only, updated by push mirror; a fork of giwty/Beatbump): https://github.com/uncaney/beatbump
  • Releases are git tags vX.Y.Z. Release notes live in the root CHANGELOG.md (Keep a Changelog format, starting at 1.0.0). The release workflow publishes the images to the Forgejo container registry; deploy/compose.images.yml is the compose override that pulls those images instead of building locally.