Gamdldocker
Self-hosted Apple Music playlist downloader with WebUI

Gamdl Docker + WebUI
Polished Apple Music playlist downloader stack with:
gamdl-downloader(scheduled sync loop)gamdl-webui(playlist + settings + cookie upload UI)- default downloader mode set to N_m3u8DL-RE
- automatic in-container tool updates (gamdl + N_m3u8DL-RE)
Highlights
-
N_m3u8DL-RE default mode
DOWNLOAD_MODE=nm3u8dlreis the default in settings and runtime env.- Entry point passes
--download-mode nm3u8dlrewhen supported by installedgamdl.
-
Automatic updates
- Downloader container auto-updates tools on interval:
AUTO_UPDATE=trueAUTO_UPDATE_INTERVAL=86400(24h)
- Optional Watchtower service is included behind profile
updates.
- Downloader container auto-updates tools on interval:
-
Persistent cookie upload
- WebUI accepts Netscape cookie uploads.
- Uploaded cookie file is persisted to both:
config/music.apple.com_cookies.txtconfig/cookies.txt
- Downloader reads
config/cookies.txt, with automatic mirror fallback frommusic.apple.com_cookies.txt.
-
Playlist m3u export location
- Playlist files are moved into
/playlistsunder the music output folder. - Default path:
PLAYLIST_M3U_DIR=/data/music/playlists.
- Playlist files are moved into
-
Theme mode toggle
- Web UI includes Light, Dark, and System appearance modes.
Quick Start (Local Laptop)
From this folder (Github/):
cp .env.example .env
docker compose up -d --build
Open:
- Web UI:
http://localhost:3000
Stop:
docker compose down
Optional: Enable Watchtower Auto-Image Updates
docker compose --profile updates up -d
This only updates pullable tagged images (gamdl-downloader:latest, gamdl-webui:latest) when they are available.
Cookie Setup
Preferred (Web UI)
- Open Settings tab.
- Upload your Netscape
cookies.txtexport. - Verify status badge shows cookie file loaded.
Manual (filesystem)
Place your cookie file at either path:
config/music.apple.com_cookies.txt(preferred)config/cookies.txt
Playlist Setup
Add playlist URLs from the UI, or edit:
config/playlists.txt
One URL per line. # comments are allowed.
If no playlists are configured, downloader stays alive and retries every 60s.
Playlist naming
Playlist .m3u8 files are written to PLAYLIST_M3U_DIR (default /data/music/playlists). Naming rules:
-
Source of truth. The downloader uses Apple Music's stored playlist title, cached in
config/playlist-name-cache.json. The web UI keeps this cache in sync as it resolves URLs against the Apple API. -
Sanitization. Filesystem-unsafe chars (
\ / : * ? " < > |) are replaced with Unicode lookalikes (\ / : * ? " < > |) so the rendered name matches Apple's stored title. ASCII control chars are still stripped. Trailing dots/spaces are trimmed (Windows refuses them). Emoji and Unicode letters are preserved —🪨+roll.m3u8,💍.m3u8, and¯\_(ツ)_/¯.m3u8round-trip cleanly on ext4 / APFS / NTFS / Tailscale-served Syncthing peers. -
Case-insensitive collision suffix. If two playlists in the same cycle resolve to names that differ only in case (e.g.
Jamsandjams), the second one gets a(<short-id>)suffix (last 6 chars of thepl.u-...ID). This keeps both files visible on macOS HFS+/APFS, Windows NTFS, and exFAT. -
Manual overrides. When Apple's stored title is wrong (or you want a different display name), copy
config/playlist-overrides.json.exampletoconfig/playlist-overrides.jsonand add entries:{ "https://music.apple.com/us/playlist/jams/pl.u-EXAMPLE111111": "Bops" }Overrides take precedence over the cached API name. Sanitization and collision suffixing still apply. The override file is gitignored; the example is tracked.
-
Precedence after a fresh download. Overrides and the API-title cache take precedence over gamdl's filesystem-mangled m3u filename, so the title round-trips even when gamdl's internal sanitizer would have mangled
+,\,/, etc. Edge case: if a freshly-added URL has neither an override nor a cache hit yet, the URL-slug last-resort defers to gamdl's source basename for that cycle (it usually resolves on the next pass once the WebUI populates the cache). -
Cross-platform sync mode. Set
SAFE_FILENAMES=truein.envto additionally strip non-ASCII characters. Use this only when targeting legacy SMB or exFAT shares that mishandle UTF-8.
Run the naming-helper tests locally:
bats tests/test_sanitize.bats
Key Environment Variables
BIND_HOST=0.0.0.0(host interface for WebUI; set to a Tailscale IP /127.0.0.1to restrict)WEBUI_PORT=3000(host port mapped to container's 3000)FREQUENCY=3600OUTPUT_LOCATION=/data/musicPLAYLIST_M3U_DIR=/data/music/playlistsDOWNLOAD_MODE=nm3u8dlreAUTO_UPDATE=trueAUTO_UPDATE_INTERVAL=86400PLAYLIST_OVERRIDES_FILE=/config/playlist-overrides.json(optional; absent = no overrides)SAFE_FILENAMES=false(settrueto strip non-ASCII for legacy SMB / exFAT)TZ=America/New_York
Restricting WebUI to a private interface
By default the UI binds to 0.0.0.0:3000 (every interface). To restrict it to a private interface (e.g. Tailscale, loopback), set BIND_HOST in .env:
# Tailscale-only access
BIND_HOST=100.x.y.z
# Loopback only
BIND_HOST=127.0.0.1
API Endpoints (WebUI)
GET/POST /api/settingsGET/POST/DELETE /api/playlistsPOST /api/download(restarts downloader container to trigger immediate cycle)GET/POST /api/cookies
Deploy to Server Later
This same folder is server-ready. After syncing to server:
docker compose up -d --build
For remote host permission issues, run docker commands with sudo.