Self-hosted DJ ingestion service for your homelab. Paste a URL (single track or playlist), review and edit the metadata, batch-download to MP3 320 kbps or FLAC, auto-tag with cover art, and ingest straight into your Rekordbox library via XML — all from a fast, MediaHuman-inspired web UI.
Use GetSetMix only for content you have the rights to download.
- Paste & go — single videos or whole playlists/sets fan out into individual editable rows
- Metadata review before download — title + artist required, genre highlighted with a DJ-genre picker, optional album; cover override via cover search (iTunes) or manual upload
- Batch downloads — global parallelism default of 2 with per-batch override, per-track status (queued → downloading → tagging → ingested / error), overall batch progress bar, cancel button
- Failures don't halt the batch — failed tracks are flagged with per-track retry
- Tagging — ID3v2.4 / FLAC Vorbis comments: title, artist, album, genre, embedded cover, source URL in the comment field
- Filename templates —
{title} {artist} {album} {source} {id} {genre}, sanitized, missing tokens omitted, collision-safe suffixes - Rekordbox XML — tracks appended to a configurable XML and an "Inbox" playlist (name configurable); reload the XML source in Rekordbox to see new tracks
- Collection-aware inbox — optionally point GetSetMix at your full Rekordbox collection XML: before each batch, tracks you've already imported are purged from the inbox XML, so it only ever lists new songs
- Duplicate detection — right after metadata is fetched, each track is checked against the inbox XML and your collection XML by source URL or by normalized/fuzzy title+artist (filename-independent, so changing the naming template still counts). Duplicates get an amber badge, and starting a batch with duplicates prompts you to download anyway or skip them
- Share to GetSetMix (Android) — install the web UI as a PWA and it shows up in the Android share sheet, so you can share a link straight from the YouTube or SoundCloud app; the track lands staged and ready to review
- Persistence — SQLite for tracks, URL history, and counters; manual purge from the UI
- Observability — Prometheus
/metrics: job counts by status, active downloads, download durations, errors by source, songs in last 30d / 365d / all time, health - Private by default — bind to localhost or your LAN; optional static-token or Basic Auth for public exposure
- i18n — English (default) and Italian UI
- Runs comfortably under 1 GB RAM
# deploy/docker-compose.yml (edit the music volume path)
services:
getsetmix:
build: ..
ports: ["8765:8765"]
volumes:
- getsetmix-data:/data
- /path/to/your/music/library:/music
environment:
GSM_LANGUAGE: en # or it
# GSM_AUTH_TOKEN: change-me # enable token auth if exposed
volumes:
getsetmix-data:docker compose -f deploy/docker-compose.yml up -d --build
# open http://localhost:8765Plain manifests live in deploy/k8s/getsetmix.yaml (Namespace, ConfigMap, PVCs for data + music, Deployment with health probes and Prometheus scrape annotations, Service, optional Ingress commented out).
# 1. build & push the image, then point the Deployment at it
docker build -t ghcr.io/Fre0Grella/getsetmix:latest .
docker push ghcr.io/Fre0Grella/getsetmix:latest
# 2. apply
kubectl apply -f deploy/k8s/getsetmix.yaml
kubectl -n getsetmix get podsNotes:
replicas: 1withstrategy: Recreateis intentional — state is SQLite on a PVC.- Mount your real music library into the
musicPVC (or replace it withhostPath/NFS as your cluster dictates). - A Helm chart is not included; the manifests are small enough to kustomize directly.
Same UI, not always-on — auto-opens your browser. Grab a prebuilt executable from the Releases page (no Python needed, data lives in ~/.getsetmix), or run from source:
pip install -r requirements.txt
# ffmpeg must be on PATH (apt install ffmpeg / brew install ffmpeg / winget install ffmpeg)
python run_local.py # opens http://127.0.0.1:8765
python run_local.py --port 9000 --no-browser- Click Paste link — the URL on your clipboard is added (playlists fan out into one row per entry).
- Rows resolve their metadata; fix title/artist, pick a genre, optionally set album and cover (camera button on the thumbnail → search or upload).
- Press Download to start the batch (the ×N selector overrides parallelism for this batch only). Edits lock once a track is queued.
- Watch per-track progress and the batch bar; cancel anytime (in-flight tracks are stopped and re-staged).
- When tracks show Done, open Rekordbox → File ▸ Import collection / reload the rekordbox.xml source and find them in the Inbox playlist.
In Rekordbox, point Preferences ▸ Advanced ▸ Database ▸ rekordbox xml at the XML path shown in Settings (default <data>/rekordbox/getsetmix.xml), then refresh the rekordbox xml tree in the sidebar after each batch.
To keep the inbox tidy, export your full collection (File ▸ Export Collection in xml format) and set its path as Rekordbox collection XML in Settings (or GSM_COLLECTION_XML_PATH). Before each batch GetSetMix compares the inbox XML against the collection (by file location, falling back to title + artist) and removes the tracks you've already imported — the Inbox playlist only ever shows what's still missing from your collection.
GetSetMix ships as an installable PWA with a Web Share Target, so you can push a link into it straight from the YouTube or SoundCloud share sheet instead of copy-pasting.
- Expose the instance over HTTPS. Browsers only let a site install (and register a share target) from a secure origin. The simplest route is the Kubernetes deployment behind a TLS Ingress — uncomment and adapt the Ingress block in
deploy/k8s/getsetmix.yaml(it includes a cert-managertls:example). Any HTTPS reverse proxy works too. - Install it. Open the HTTPS URL in Chrome/Edge on your Android phone → menu → Install app / Add to Home screen.
- Share. In the YouTube or SoundCloud app, tap Share → GetSetMix. The app opens, the link is fetched, and the track appears staged in the list — edit metadata and hit Download as usual.
Notes:
- Android only. iOS Safari doesn't implement the Web Share Target API, so GetSetMix can't appear in the iPhone share sheet (paste-link still works).
- If you've enabled
GSM_AUTH_TOKEN/ Basic Auth, open the installed app and authenticate once first — the token is stored in the browser and reused when you share.
Everything is editable in the UI (gear icon) and persisted to <data>/config.json. Environment variables override on boot:
| Variable | Default | Purpose |
|---|---|---|
GSM_DATA_DIR |
./data (/data in Docker) |
SQLite DB, config, covers, default XML location |
GSM_LIBRARY_ROOT |
./music (/music in Docker) |
Downloads land directly here (no inbox folder) |
GSM_XML_PATH |
<data>/rekordbox/getsetmix.xml |
Inbox XML path (new downloads) |
GSM_COLLECTION_XML_PATH |
(unset) | Full Rekordbox collection XML; when set, already-imported tracks are purged from the inbox XML before each batch |
GSM_PLAYLIST_NAME |
Inbox |
Target playlist inside the XML |
GSM_OUTPUT_FORMAT |
mp3 |
mp3 (320 kbps) or flac — global only |
GSM_CONCURRENCY |
2 |
Global parallel-download default |
GSM_FILENAME_TEMPLATE |
{artist} - {title} |
Tokens: {title} {artist} {album} {source} {id} {genre} |
GSM_LANGUAGE |
en |
en or it |
GSM_AUTH_TOKEN |
(unset) | Static token auth (X-Auth-Token, Authorization: Bearer, or ?token=) |
GSM_BASIC_USER / GSM_BASIC_PASS |
(unset) | HTTP Basic Auth alternative |
With auth enabled the API and /metrics return 401 without credentials; the UI prompts for the token once and stores it in the browser.
GET /metrics (Prometheus text format):
getsetmix_jobs{status="..."} # job counts by status
getsetmix_active_downloads
getsetmix_download_duration_seconds_sum / _count
getsetmix_errors_total{source="..."}
getsetmix_songs_downloaded{window="30d"|"365d"|"all"}
getsetmix_healthy
GET /healthz returns ok for probes.
pip install -r requirements.txt pytest httpx ruff pre-commit
python -m pytest tests/ -v # unit + API tests (no network needed)
ruff check app tests run_local.py
pre-commit install --hook-type commit-msg --hook-type pre-commit # local guard railsCommit messages must follow Conventional Commits — versioning is fully automated from them, so a non-conforming message fails CI:
| Commit | Example | Version bump |
|---|---|---|
fix: |
fix: stop crash on empty playlist |
patch — 1.0.0 → 1.0.1 |
feat: |
feat: share target for YouTube |
minor — 1.0.0 → 1.1.0 |
feat!: / BREAKING CHANGE: footer |
feat!: drop Python 3.10 |
major — 1.0.0 → 2.0.0 |
Other types (chore, docs, ci, refactor, test, …) are valid and release nothing on their own. The local commit-msg hook (from pre-commit install above) and the commitlint.yml workflow both enforce the format; to hard-block bad commits, enable branch protection on main and require the Commit lint check.
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
push to main, every PR |
ruff lint, pytest suite (with ffmpeg), and a no-push Docker build as a PR safety net |
commitlint.yml |
push to main, every PR |
validates commit messages against Conventional Commits |
docker-publish.yml |
push to main |
builds the image for linux/amd64 + linux/arm64 and pushes to GHCR as latest, main, and the commit SHA |
release.yml |
push to main |
automated semantic versioning: bumps the version from the commits, tags vX.Y.Z, creates the GitHub Release, builds the standalone executables (Windows / macOS / Linux, PyInstaller) and the immutable versioned images (:1.2.3, :1.2, :1) |
pages.yml |
push to main touching site/** |
deploys the landing + docs site in site/ to GitHub Pages |
One-time setup after pushing to GitHub:
-
Replace
Fre0Grellawith your GitHub username indeploy/k8s/getsetmix.yaml,site/*.html, and this README (grep -rl Fre0Grella .). -
Settings ▸ Pages ▸ Source: GitHub Actions to enable the site.
-
After the first publish, Packages ▸ getsetmix ▸ Package settings ▸ Change visibility if you want the image public (no
imagePullSecretsneeded in K8s). -
(optional) Settings ▸ Branches → protect
mainand require the Commit lint check so only Conventional Commits can land. -
If
mainrequires signed commits, give the release bot a signing key so its version-bump commit is accepted (run once, locally, withghauthenticated):ssh-keygen -t ed25519 -C "getsetmix-release-bot" -f gsm_release_key -N "" gh ssh-key add gsm_release_key.pub --type signing --title "getsetmix release bot" gh secret set SSH_SIGNING_PRIVATE_KEY < gsm_release_key gh secret set SSH_SIGNING_PUBLIC_KEY < gsm_release_key.pub rm gsm_release_key gsm_release_key.pub # the keys now live in GitHub
Set
git_committer_emailinrelease.ymlto a verified email on the account that owns the signing key so the bot's commits show as Verified.
Releasing is automatic — just merge Conventional Commits to main:
feat: … → semantic-release tags v1.1.0, publishes the GitHub Release with
getsetmix-windows-x64.exe / -macos-arm64 / -linux-x64, and pushes
ghcr.io/Fre0Grella/getsetmix:1.1.0 (+ :1.1, :1)
The website lives in site/ — a static landing page (index.html) and documentation (docs.html), no build step. Edit and push; the workflow handles the rest.
app/
main.py # FastAPI app, API routes, auth middleware
worker.py # asyncio download queue (yt-dlp + ffmpeg)
metadata.py # server-side metadata fetch, playlist fan-out
tagger.py # ID3v2.4 / FLAC tagging + cover embedding
rekordbox.py # DJ_PLAYLISTS XML writer (atomic, corruption-safe)
naming.py # filename templates + sanitization
db.py # SQLite persistence (tracks, history, counters)
metrics.py # Prometheus exposition
config.py # settings + env overrides
static/ # the UI (vanilla JS, no build step)
deploy/
docker-compose.yml
k8s/getsetmix.yaml
site/ # landing + docs, deployed to GitHub Pages
tests/ # pytest suite run by CI
.github/workflows/ # ci.yml · docker-publish.yml · pages.yml
run_local.py # local app mode
| Method & path | Purpose |
|---|---|
POST /api/tracks {url} |
Add URL (playlist fans out); returns ids + duplicate flag |
GET /api/tracks |
All rows + active download count |
PATCH /api/tracks/{id} |
Edit metadata (only before download / after error) |
DELETE /api/tracks/{id} |
Remove row |
POST /api/tracks/{id}/cover / GET …/cover |
Upload / fetch cover |
GET /api/cover-search?q= |
Cover candidates (iTunes) |
POST /api/tracks/{id}/retry |
Retry a failed track |
POST /api/batch/start {ids?, concurrency?} |
Start batch |
POST /api/batch/cancel |
Cancel batch, re-stage in-flight |
GET/PUT /api/settings |
Read / update settings |
GET /api/stats · GET /api/history · POST /api/purge |
Stats, URL history, manual purge |
