Skip to content

Repository files navigation

Setlist-er-ator

tests

Create a Plex music playlist from a setlist.fm show, and report which songs are missing from your library. Works as a command-line tool (setlist_to_plex.py) or a small local web app (web.py).

Quickstart (macOS)

These steps assume macOS with Homebrew. (Other platforms aren't documented here.)

  1. Install Python and git (skip whatever you already have):

    brew install python git
  2. Get the code and install dependencies:

    git clone https://github.com/thejames/setlisterator.git
    cd setlisterator
    python3 -m venv .venv
    ./.venv/bin/pip install -r requirements.txt
  3. Grab your credentials:

  4. Configure — copy the example and fill in the four values:

    cp .env.example .env
    # edit .env: SETLISTFM_API_KEY, PLEX_BASEURL, PLEX_TOKEN
    # (and PLEX_MUSIC_LIBRARY if your music section isn't named "Music")
  5. Run the web app:

    ./.venv/bin/python web.py

    Open http://127.0.0.1:5001, paste a setlist.fm URL, hit Preview, pick track versions where you're offered a choice, then Create playlist. The History ▾ menu in the navbar holds Concerts (shows you've already made) and Playlists (every Plex audio playlist, filterable by whether this app built it).

    Prefer the terminal? Use the CLI instead:

    ./.venv/bin/python setlist_to_plex.py "https://www.setlist.fm/…"

Install as commands (optional)

An editable install exposes two console commands and keeps the web app's templates working in place:

./.venv/bin/pip install -e .
setlisterator "https://www.setlist.fm/…"   # the CLI
setlisterator-web                           # the web app (http://127.0.0.1:5001)

More detail on configuration, flags, and matching behavior is below.

Stack

  • Python 3 — CLI + core matching pipeline (setlist_to_plex.py)
  • plexapi — Plex library search and playlist creation
  • requests — setlist.fm REST API
  • python-dotenv.env config
  • Flask + Jinja templates — the local web UI (web.py, templates/); no JS build step
  • pytest — tests (test_setlist_to_plex.py, test_web.py), Plex/network-free

Persistence is a single JSON history file (no database); there's no hosted component — it talks to Plex on localhost.

Configure

Copy .env.example to .env and fill it in:

Variable Required Description
SETLISTFM_API_KEY yes setlist.fm API key (https://api.setlist.fm/docs/)
PLEX_BASEURL yes e.g. http://localhost:32400
PLEX_TOKEN yes your Plex X-Plex-Token
PLEX_MUSIC_LIBRARY no library section name (default Music)
SETLIST_TO_PLEX_HISTORY no history file path (default below)
PORT no web app port (default 5001; web UI only)
AUDITION_ALWAYS_TRANSCODE no audition at 256k instead of the original file (default on)

Usage

# By setlist ID
./.venv/bin/python setlist_to_plex.py 63de4613

# By full setlist.fm URL (the ID is parsed out)
./.venv/bin/python setlist_to_plex.py "https://www.setlist.fm/setlist/primus/2026/td-amp-ballantyne-charlotte-nc-6b756686.html"

# Override the auto-generated playlist name
./.venv/bin/python setlist_to_plex.py 63de4613 --name "Phish @ MSG"

The playlist name defaults to {artist} - {venue}, {city} ({YYYY-MM-DD}). If a playlist with that name already exists, a numeric suffix is appended rather than failing.

Flags

Flag Effect
--name NAME override the auto-generated playlist name
--history list previously created playlists (newest first) and exit
--backfill fill in missing-track detail for old history entries and exit
--quiet suppress per-song match logging on stderr
--force re-process a setlist even if it was processed before
--no-history don't read or write the processed-setlist history

--history reads only the local history file, so it works without any Plex or setlist.fm configuration: setlist_to_plex.py --history. --backfill re-matches older shows (those recorded before per-track missing detail existed) so they show up in the web Missing list; it needs the usual config and is idempotent.

Logging vs. report

The actionable report (playlist name, missing, fuzzy) goes to stdout. Per-song match decisions are logged to stderr (on by default; --quiet to silence), one line per song with the matched track, its album, tier, and source:

Setlist: Primus — TD Amp Ballantyne, Charlotte (2026-06-16) — 12 songs
Library: matching against 214 tracks by Primus
  ✓  1. Tommy the Cat → 'Tommy the Cat' on 'Sailing the Seas of Cheese'  [exact/scoped]
  ✓  7. Hello Skinny → 'Hello Skinny / Constantinople' on 'The Desaturating Seven'  [medley/scoped]
  ✗  5. Jilly's on Smack → no match

The web Preview shows the source album inline with each matched track (and in the per-song picker when there's more than one candidate).

Processed-setlist history

Each setlist that produces a playlist is recorded in a small JSON file (default ~/.config/setlist_to_plex/history.json, honoring XDG_CONFIG_HOME; override with SETLIST_TO_PLEX_HISTORY). Keys are setlist IDs, so a bare ID and a full URL for the same show dedupe correctly. On a repeat run the tool prompts when interactive, or skips with a hint to pass --force when run non-interactively (cron, pipes) so it never hangs. Only runs that actually create a playlist are recorded — a show that matched nothing is retried next time. The history lives outside the repo, so it won't be committed.

Web interface

A small local web UI (dark theme, server-rendered Jinja + a little vanilla JS — still no build step) wraps the same matching pipeline:

./.venv/bin/pip install -r requirements.txt   # pulls in Flask
./.venv/bin/python web.py                      # http://127.0.0.1:5001

Default port is 5001 (macOS uses 5000 for AirPlay Receiver). Override with PORT=8080 ./.venv/bin/python web.py.

Paste a setlist URL or ID and hit Preview — it matches the show against your library and shows the full setlist in order without creating anything; songs not in your library are flagged inline (and also listed separately below). When a song matches more than one library track (e.g. the same song on a studio album, a live record, and a compilation) its row shows a dropdown so you can pick the version — by album — you want; the best match is preselected. Review it (and tweak the playlist name if you like), then click Create playlist to commit. Create rebuilds the chosen tracks by their Plex rating keys, so nothing is re-matched, and the run is recorded in the same history the CLI uses.

The preview table has a few extra handles. A magnifier next to each match lets you search your library by hand and re-point the row when none of the auto-matches are right. Clicking the Exact/Fuzzy pill opens a popover explaining how the row matched — the title tier (exact, close, medley, prefix) and whether it came from the artist's own tracks or a library-wide search. The summary chips above the table double as filters: click one (exact, fuzzy, multi-match, missing) to flash and highlight those rows, then click it again — or the songs total — to clear. The source URL bar links out to the show on setlist.fm in a new tab. The Prefer album dropdown re-matches multi-album songs to a chosen album in place; any row you've already touched (a hand-pick, an accepted fuzzy, a filled-in missing track) stays put and is marked, so switching albums never discards your edits.

Audition (▶) lets you hear a track before you commit to it — the point being to check a match is the song you actually expect. It's on every place a track gets chosen: preview rows, each version inside a multi-match dropdown, fuzzy-match cards, library search results, album expansions, and the rows of a playlist you're building or editing. A small player opens beside whatever you're listening to, with play/pause, a scrubber and elapsed/total; Esc stops it. Auditioning a candidate never selects it and never marks a row as edited, so you can compare two versions of a song without disturbing anything.

Audio plays to your browser, not to a Plex client, so it isn't a Plex playback session. The app checks once per tab whether your browser can reach Plex directly; if it can't (behind a VPN, or with the app served over HTTPS), it proxies the audio through itself instead, so auditioning works wherever the app does. By default tracks are auditioned as 256k MP3 — smaller and faster over a remote link, and enough to identify a song — which you can turn off under the gear menu to stream the original file instead. Formats a browser can't decode (ALAC, WMA) are always transcoded, so they play either way.

Playlist image (optional): pick a JPEG/PNG/WebP (up to 10 MB) to set as the playlist's poster in Plex — no need to add it through the Plex UI afterward. It works the same on the Build page and when editing an existing playlist (there, leaving the field empty keeps the current image). The image is best-effort: if it can't be set, the playlist is still created/saved and you're told, so a bad file never costs you your work.

Concerts (under the navbar's History ▾ menu) lists every show you've created, newest first. Each row can Re-open the setlist in Preview, link out to its setlist.fm source, or Update the existing playlist: it re-matches the show against your current library and shows the tracks that are now available but not yet in the playlist, for you to confirm. Update is add-only — it tops up the existing playlist (no new copy, nothing removed), so when a song that was missing turns up in your library, you can fold it into the playlist you already made. The Missing link aggregates every show's missing tracks into one deduped list, grouped by artist, with the album each track is from. Albums come from setlist.fm's own "Songs on Albums" data (scraped from the setlist page when you preview a show), with MusicBrainz as a fallback for the few it doesn't map (e.g. covers) — looked up lazily on first Missing-list view and cached into history. Both are best-effort and degrade gracefully offline.

The Attended link turns your setlist.fm "I was there" history into a worklist. Enter a setlist.fm username (pre-filled from the optional SETLISTFM_USER in your .env) and it lists every show you've marked attended, newest first. Each row has a Preview button that drops into the normal match → create flow; shows you've already made are flagged created ✓ with Re-open / Update instead. It reads public attended data, so any public username works.

Local only. The app talks to your local Plex server and holds your Plex token, so it binds to 127.0.0.1 and has no authentication. Don't expose it to a network.

Deploy (self-host with Docker)

To run it as an always-on service on your network — your Mac, a NAS, a Pi — anywhere that can reach Plex directly:

cp .env.example .env       # fill in your keys/token and PLEX_BASEURL
docker compose up -d       # builds the image and starts it
# → http://<that-host>:5001

It runs under gunicorn, persists history.json (including the album cache) in a named volume, and reaches your Plex over the LAN — no tunnel needed. Change the published port by editing the ports: mapping in docker-compose.yml.

Still unauthenticated. This exposes the app (and your Plex token's reach) to anyone on the network, so keep it on a trusted LAN. For remote access, put it behind a tunnel (Tailscale, Cloudflare Tunnel) with authentication — not included here.

Install on Unraid

A public multi-arch image is published to GHCR by CI on every push to main: ghcr.io/thejames/setlisterator:latest (and :vX.Y.Z for tags). Unraid's Add Container Template field is a dropdown of templates it already knows — you can't paste a URL there — so use one of these:

Option A — use the bundled template (fields pre-filled). Copy unraid/setlisterator.xml to your Unraid flash drive at /boot/config/plugins/dockerMan/templates-user/ (easiest over the network: \\TOWER\flash\config\plugins\dockerMan\templates-user\setlisterator.xml). Then Docker → Add Container, pick setlisterator from the Template dropdown, fill in your keys, Apply.

Option B — configure manually (no file copy). Docker → Add Container, leave Template blank, and set:

  • Repository: ghcr.io/thejames/setlisterator:latest
  • Port: container 5001 → host 5001
  • Path: container /data → host /mnt/user/appdata/setlisterator
  • Variables: SETLISTFM_API_KEY, PLEX_BASEURL (e.g. http://10.0.0.10:32400), PLEX_TOKEN, PLEX_MUSIC_LIBRARY

Either way, open the WebUI on the mapped port (default 5001); history.json persists in the appdata path.

How matching works

Each setlist song is matched to a Plex track in two stages:

  1. Artist-scoped (primary). The setlist artist is resolved once in your library and all of their tracks are pulled and compared locally. This sidesteps Plex's search tokenizer, which can otherwise miss tracks over punctuation or Unicode quirks (e.g. an ASCII hyphen - in the setlist vs a typographic hyphen in your library) and over result truncation on common titles.
  2. Global search (fallback). Only if the artist isn't in your library, or a song isn't among their tracks (covers of other artists), a global title search is used.

Titles are compared with normalization (lowercased, punctuation stripped, whitespace collapsed) across four tiers:

Tier Quality Example
exact exact Run Like an Antelope!!! == run like an antelope
loose fuzzy Wilson, Pt. 2Wilson Part 2; drops (...), feat. ...
medley fuzzy Hello Skinny / Constantinople matches Hello Skinny
prefix fuzzy Tommy the Cat - Live matches Tommy the Cat (2+ word title)

Anything past the exact tier is reported under FUZZY MATCHES so you can spot-check for false positives. A medley track that matches more than one setlist song is added to the playlist only once.

Output

  • Playlist name and number of tracks added.
  • MISSING TRACKS — songs with no match, as {position}. {artist} - {title}.
  • FUZZY MATCHES — non-exact matches, setlist title vs the Plex track.

Exit codes: 0 success, 2 config error, 3 setlist error, 4 Plex error.

Tests

./.venv/bin/pip install pytest
./.venv/bin/python -m pytest

About

Create a Plex playlist from a setlist.fm show, and report what's missing — CLI + local web app

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages