Mobile-first karaoke jukebox web app with Chromecast support. We used to have a karaoke machine but no matter how many songs are loaded on, it never has the songs you want. One day, I wen to a bar in Singapore that queued up karaoke songs using YouTube, ads and all! So we started using YouTube at home but people keep mashing the wrong buttons and killing queues or playing their song. So I built this.
It deliberately uses Chromecasts for playout over the local network. That's what I've got at home so that's what I use. Makes it easy to get videos to the TV.
There is now also an mpv playback backend for portable setups (think Raspberry Pi plugged into a projector in a hall) - see mpv playout below.
- Mobile first
- Super simple for users to start. Just enter your name and search/queue
- Uses the YouTube API to search for songs
- Users can only manage their own songs in the queue
- Simple admin login for managing the queue and playback
- Chromecast playout including searching for devices
- mpv playout for local HDMI output (e.g. Raspberry Pi + projector), with an idle "screensaver" video loop between songs
- Auto queue advance
# Run the setup script
./setup.shThis will:
- Create
.envfrom.env.example - Generate a secure
SECRET_KEY - Prompt you to edit
.envfor required values
Edit .env and set:
# Admin password (choose something secure)
ADMIN_PASSWORD=your_secure_password
# YouTube API Key (get from https://console.cloud.google.com/apis/credentials)
YOUTUBE_API_KEY=your_youtube_api_keyOption 1: Shell script (from the repository root)
./run.shOption 2: Make target
make runOption 3: Directly with uv (from the repository root)
uv sync
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Open your browser to: http://localhost:8000
- Enter your name on the login page
- Search for karaoke songs
- Queue songs you want to sing
- See the queue update in real-time
- Login with username:
adminand your admin password - Access admin controls to:
- Scan for Chromecast devices
- Start/stop playback
- Skip songs
- Clear the queue
- Python 3.13
- ffmpeg - Required for video downloads
- macOS:
brew install ffmpeg - Ubuntu/Debian:
sudo apt-get install ffmpeg - Windows: Download from https://ffmpeg.org/download.html
- macOS:
- deno - Required at runtime for yt-dlp's JS-challenge solver (downloads fail without it)
- uv - Python dependency management (install with
curl -LsSf https://astral.sh/uv/install.sh | shorbrew install uv) - All dependencies in
pyproject.toml(install withuv syncfrom the repository root;uv.lockpins exact versions) - YouTube Data API v3 key
- Chromecast device on the same network (default playback), or a spare HDMI
output +
libmpv2for the mpv backend (see mpv playout section)
docker build -t karaoke-jukebox .The image is multi-stage. A short export stage uses uv to generate
requirements.txt from the committed uv.lock; the runtime stage then
pip installs it. uv.lock is the single source of truth, requirements.txt
is generated at build time (not committed), and uv never ends up in the
runtime image.
docker run -d \
--name karaoke-jukebox \
-p 8000:8000 \
-e ADMIN_PASSWORD=your_secure_password \
-e YOUTUBE_API_KEY=your_youtube_api_key \
-e SECRET_KEY=your_secret_key \
-e SERVER_HOST=192.168.1.100 \
karaoke-jukeboxImportant Docker Configuration:
-
SERVER_HOST is REQUIRED when running in Docker
- Set it to your Docker host's IP address (find with
ip addrorifconfig) - DO NOT use
localhostor127.0.0.1(Chromecast cannot reach the container) - Example:
SERVER_HOST=192.168.1.100
- Set it to your Docker host's IP address (find with
-
SECRET_KEY must be at least 32 characters
- Generate with:
python -c "import secrets; print(secrets.token_hex(32))"
- Generate with:
-
Port Mapping: If you map to a different external port (e.g.,
-p 80:8000):- Set
SERVER_PORTto the EXTERNAL port that Chromecast will use - Example:
docker run -p 80:8000 ... -e SERVER_PORT=80 ...
- Set
-
Data Persistence: Database and videos are ephemeral (lost on container restart)
- To persist data, add:
-v /path/to/data:/app/data
- To persist data, add:
-
Using DOCKER_BUILDKIT for caching of apt data to speed up rebuilds. Ensure you set DOCKER_BUILDKIT=1 before building
A docker-compose.yml file is included. Before running:
-
Edit
docker-compose.ymland set the required environment variables:ADMIN_PASSWORDYOUTUBE_API_KEYSECRET_KEY(generate with:python -c "import secrets; print(secrets.token_hex(32))")SERVER_HOST(your Docker host's IP address)
-
Run with:
docker-compose up -d
Note: Database and videos are ephemeral by default. To persist data, uncomment the volumes section in docker-compose.yml
Status: merged and unit-tested; pending final acceptance on real Pi hardware. The full test suite covers the backend against a fake mpv, and the mpv options below were validated on a Pi with a standalone prototype, but the end-to-end acceptance checklist (screensaver at boot, gapless queue advance, clean display release on shutdown) has not been signed off on the target hardware yet. Treat it as beta until then. Chromecast remains the default and is unaffected.
The mpv backend plays downloaded videos straight out of the local filesystem to an HDMI output via libmpv/DRM - no desktop session, no X/Wayland. When nothing has played for 15 seconds (before playback starts, between-sets, after a stop), it loops an idle "screensaver" video so the projector never sits on a black screen.
In .env:
# Switch the playback backend (default: chromecast)
PLAYER_BACKEND=mpv
# Optional: video file looped as the idle screensaver. Unset = disabled
# (black screen when idle).
IDLE_VIDEO_PATH=./data/idle.mp4
# Single-admin-only mode (default: false). When enabled, only the admin account
# can log in, and the admin panel gains a search-and-queue card for queueing
# songs on behalf of participants.
PILOT_MODE=falseWhere to put the idle video: anywhere EXCEPT data/videos/. That directory
is managed by the cleanup job, which deletes any .mp4 not referenced by a
queue item after a few hours - your screensaver would disappear mid-party. The
recommended spot is the data/ root (./data/idle.mp4, as above); any other
path outside data/videos/ works too, since IDLE_VIDEO_PATH is just a file
path. Any format/resolution mpv can decode is fine - it plays on a loop, so a
short clip (10-30 seconds) is plenty.
"Up next" overlay font: while a song plays, mpv flashes "Up next: <title>
- for
<owner>" for the last 15 seconds, naming whoever's up next in the queue. It's rendered in Roboto, loaded fromdata/Roboto-Regular.ttf- like the idle video, this file is NOT committed to git and must be placed there manually on every fresh deployment. If it's missing, mpv falls back to its default font instead of failing (the startup log warns explicitly), so the overlay still works - just not in Roboto.
On the Pi (or any Linux box with a free DRM output):
sudo apt install libmpv2 # the libmpv system library
uv sync --extra mpv # installs python-mpv (kept out of default installs)python-mpv is an optional dependency (mpv extra) and is imported lazily - the
Chromecast/Docker paths never need libmpv installed. Do NOT use Debian's
apt-packaged python3-mpv (it is the old 0.x API); the extra pins
python-mpv>=1.0.
The video-output options are currently hardcoded constants in
app/services/players/mpv_player.py (MPV_OPTIONS): DRM output on
/dev/dri/card1, connector HDMI-A-2, 1280x720, v4l2m2m hardware decoding.
If your hardware differs, edit those constants. Making them admin-configurable
(video out, audio device, scaling) is the planned next phase.
# The mpv backend's unit tests run everywhere - no libmpv, no display needed
uv run pytest tests/test_mpv_player.py -v
# Whole suite (backend selection, playout policy, config validation)
make testManual acceptance on the Pi (the remaining gate) is the checklist in
docs/superpowers/plans/2026-07-07-mpv-player-backend.md (Task 5): screensaver
within ~15s of boot, songs replace it, no screensaver flash between queued
songs, skip/stop behave, screensaver returns ~15s after stop, clean exit
releases the display.
Notes for admins: with mpv there are no devices to scan or select - just start playback. If mpv fails to initialize (missing libmpv, DRM device busy), the app stays up and logs the reason; playback start will then report a connection failure in the logs rather than crashing the server.
yt-dlp is the most fragile part of this app. YouTube periodically changes how
it serves video, which breaks downloads until yt-dlp is updated. Downloads
also require ffmpeg and deno (yt-dlp runs a deno-based JS-challenge
solver) to be installed at runtime.
How this project stays ahead of breakage:
- Canary test: an opt-in integration test downloads a known-good clip and
validates it. It hits the live network, so it is skipped by default.
make canary # uv run pytest -m integration --run-integration - Deploy gate:
make preflightbumps yt-dlp and then runs the canary.make builddepends onpreflight, so the Docker image will only build if a freshly-updated yt-dlp can still download:make preflight # bump yt-dlp (updates uv.lock) + canary make build # preflight, then docker build
- Daily CI canary:
.github/workflows/yt-dlp-canary.ymlruns the same canary on a daily schedule. A red run means YouTube changed and yt-dlp needs a bump. The Docker image also force-upgrades yt-dlp on every rebuild, so a plain rebuild usually picks up the fix.
To recover from broken downloads: run make preflight locally, commit the
updated uv.lock, and rebuild the image (the build regenerates
requirements.txt from uv.lock automatically).
The app uses:
- FastAPI - Backend framework
- HTMX - Dynamic UI updates
- DaisyUI - UI components (Tailwind CSS)
- Server-Sent Events (SSE) - Real-time queue updates
- yt-dlp - Video downloads
- pychromecast - Chromecast control
- python-mpv (optional
mpvextra) - local HDMI playout via libmpv/DRM
make test # fast unit suite (uv run pytest); the live yt-dlp canary is skipped
make lint # uv run ruff check . + ruff format --check .See DEVELOPMENT.md for the full contributor workflow.
- Ensure your Chromecast and server are on the same network
- Check firewall settings
- Try the "Scan for Devices" button in admin controls
- Check the logs for
mpv initialization failed- the usual causes are a missinglibmpv2package, the DRM device path or HDMI connector inMPV_OPTIONSnot matching your hardware, or something else (a desktop session) holding the display - Installed python-mpv from apt? Remove it and use
uv sync --extra mpv(the apt package is the incompatible 0.x API) - Black screen when idle is expected if
IDLE_VIDEO_PATHis unset or the file is missing - the startup log says so explicitly - Overlay showing in the wrong font is expected if
data/Roboto-Regular.ttfis missing - the startup log says so explicitly ("Overlay font not found")
- Check if ffmpeg is installed: Run
ffmpeg -versionin terminal- If not installed, see Requirements section above
- yt-dlp requires a JavaScript engine (deno) for its challenge solver
- This is already taken care of in the Dockerfile
- Verify your
YOUTUBE_API_KEYis valid - Check
data/videos/directory permissions - Look at server logs for specific errors
- Common error: "ffmpeg is not installed" - Install ffmpeg to fix