README: how this works, assumptions and decisions
A private record of how Tour Watch works and why. Built . Counts marked like this are filled in from this page's data. Current limits and usage appear only in the two counters at the top, and live numbers on coverage are in the "Coverage, gaps and skipped artists" panel.
Planned features
Not built. Refreshes run from GitHub (section 8); what follows is still to do.
- JamBase top-up, every two weeks. Re-query, in batches of twelve, the artists JamBase has found shows for, plus a rotating slice of the rest (each artist about every eight weeks). This covers a delta's blind spot: shows JamBase already had but dated beyond its six-month window, which only enter the window later. Ticketmaster has no date limit, so far-off tours are covered there. Not yet a workflow action.
- Other pending: SeatGeek (adapter built, needs a client ID, never run live); Spotify streaming history for a better ranking; pinning ambiguous Ticketmaster matches (H.E.R., Noname, Brasstracks) by hand; automatic deployment of the page to the website (today the new page is downloaded and uploaded by hand).
Pulling the Spotify library directly
Today the library arrives as Exportify CSVs that are exported and uploaded by hand. Two planned replacements, in the order they would be built. Neither is started.
- Direct from Spotify's API (planned). A one-time login, then a sync command that reads liked songs and saved albums (50 per request) and feeds the same tallies the CSV import does, so ranking, classification and everything downstream are unchanged.
- Login: PKCE authorisation with the single scope
user-library-read. It needs a Client ID from a Spotify developer app, which is not a secret under PKCE. The browser redirect lands on an address that cannot be reached from the cloud container, so the full redirect address is pasted back once to complete the login. - Token storage: the refresh token goes in the private
.envfile only, never in this page or the code. Spotify can issue a new refresh token on every refresh, so each refresh writes the newest one back. A failed refresh stops with a message and is never retried in a loop. - Syncing: artist IDs are stored alongside names, with the existing normalised-name key kept so overrides and skip lists still apply. Liked songs arrive newest first, so routine syncs read only pages newer than the last one. Songs removed from the library are missed that way, so a full re-read runs about monthly or on request.
- Switching over: the API sync first runs next to the CSV import and the two top-1000 lists are compared. Every difference should be explainable (for example a song liked since the export) before the API becomes the default. The CSV path stays as a fallback.
- Conditions to confirm before building: Spotify's 2026 rules for developer apps require the app owner to have active Premium (the app stops working if it lapses) and cap new apps at 5 users. The library read endpoints must still work as documented after the 2026 changes, so the first step is one test request for a single item. Since refreshes now run in GitHub, the refresh token would live in a repository secret and each refresh would need to write the newest one back (a workflow cannot update its own secret without a token with that permission, so this needs a design decision first). A manual export cannot be automated, so only this option allows an unattended library sync.
- Login: PKCE authorisation with the single scope
- Easier Exportify import (planned, small). Keeps the manual export but removes the friction.
- The import accepts Exportify's "export all" zip directly and unpacks it, instead of needing the individual CSV files.
- This page shows how many days ago the library was last imported, so a stale ranking is obvious.
- Pasting the CSV text into the chat also works as an alternative to attaching files. It stays manual and cannot be scheduled.
1. What this is
A personal tour-date finder. It takes artists from your saved Spotify library, looks up their upcoming shows through official ticketing APIs (nothing is scraped, only your own keys are used) and shows those near New York, Long Island and, optionally, Philadelphia. Spotify's "Live events" tab missed shows for artists you like, so every artist on a ranked list is looked up explicitly. The data lives in a local SQLite database; this page is a snapshot of it, with all filtering done in your browser.
2. The artist list and ranking
- Inputs: Exportify CSVs of Liked Songs and Saved Albums, about 2,100 distinct artists. Playlists, followed artists and streaming history are not included; play counts need Spotify's "Extended streaming history" download, which can take days or weeks.
- Score: 3 × liked songs + 4 × saved albums (once per album per artist). The code also supports playlist-only songs, followed artists and plays, but those inputs are empty. Every artist credited on a track gets full credit, so frequently featured artists rank higher than their own catalogue warrants.
- Position: artists are ranked, excluded artists (section 3) are removed, and the first 1,000 are searched. The "Artists included" slider cuts at that position (not the raw Spotify rank). Hover an artist's name in any view for their position and liked-song and album counts.
3. Who is left out, and why
The aim is to save requests and keep to artists who can play a show. Classification comes from MusicBrainz, which is separate from any ticketing quota, matched by exact normalised name (accents, "The" and "&" folded).
| Category | Rule | Skipped now |
|---|---|---|
| deceased | A person whose MusicBrainz life-span has ended (Duke Ellington, Beethoven, Coltrane) | |
| disbanded | A group whose life-span has ended (Pink Floyd; Panic! At The Disco, 2023) | |
| classical | An active orchestra or choir, or an artist whose top three tags are classical. With no MusicBrainz match, an orchestral-sounding name counts (philharmonic, symphony, orchestra, sinfonie, concerto, chamber choir). Quartets, quintets, trios and big bands are not caught by name, since many are living jazz acts. | |
| unknown | No confident match. Never skipped. | — |
Safeguards against skipping a living artist
- Same-name matches: when several exact matches exist, the winner needs at least 3× the runner-up's tag weight, otherwise the artist is left ambiguous and not skipped.
- Touring evidence wins: a deceased or disbanded artist with upcoming shows on file is never skipped (this kept Bill Evans and D'Angelo).
- Release-year check, for end dates in 1990 or later only: if every saved track came out more than two years after the supposed end, the match is wrong and the artist is kept (this kept Venna). Older artists are exempt because their saved recordings are reissues with modern dates.
- Overrides:
overrides.csv(artist,action,includeorexclude) beats everything. Some living classical soloists are in the classical group on purpose. The full skipped list is in the coverage panel.
4. How shows are found
Ticketmaster (primary)
- One lookup per artist finds their "attraction": an exact normalised-name match among music attractions, so tribute acts and same-name artists are rejected. Exact matches tied on upcoming shows are ambiguous and unused; no exact match is none.
- One events request per matched artist (more only above 200 shows), skipped when Ticketmaster reported zero upcoming shows at lookup time, unless
--ignore-zero-flagis given. That count goes stale either way (Red Hot Chili Peppers reported 1 but had none; an artist reporting 0 may announce a tour later). Past shows are dropped (section 6). Cancelled and vanished shows are kept and flagged, as described next. - Cancelled and vanished shows stay visible, struck out. On every refresh, a show its source marks cancelled is kept with status cancelled, and a show a source stopped listing (a Ticketmaster event gone from the artist's list, or a JamBase deleted-event notice) is kept as no longer listed. Either way the listing stays on this page, crossed out and labelled, so you are not left searching for it; it disappears on its own at 3 AM Eastern after its date, like any past show. If a vanished show comes back it is live again. Rules: any listing saying cancelled marks the show cancelled, even if another site still lists it; a vanished listing hides behind a live duplicate (a re-issued event is not shown twice). A multi-night run with only some nights cancelled stays normal with those nights struck out. A filter, Hide cancelled / no longer listed, removes them, the count line says how many are shown, and the Markdown report and CSV mark them (
statuscolumn). They are left out of the coverage tallies. - Never re-ask: lookups are cached for good, event fetches are skipped within
--fresh-hours, and every run has a hard cap (--max-requests). - It also covers TicketWeb venues (Blue Note, Birdland), so many club shows arrive through it.
JamBase (gap-filler)
- Every searched artist was queried once, in batches: one request carries twelve names (
artistName=A|B|C) with a 110-mile circle around New York and a six-month window (the free plan's limit), 100 events per page. Matching is by keyword, so a show counts only when a listed performer's name matches exactly. A batch needing more than three pages is mostly noise and is abandoned. Results are cached 45 days. Refreshes use a delta: only events changed since the last clean run, matched locally. - Plan: Developer (free). Past its quota JamBase keeps serving and bills $0.05 per call, so nothing server-side stops an overage; your own limit is lower and enforced in code. A "Concert data by JamBase" credit is shown in the footer, as its terms require.
- Links: for a show on both sources the page keeps one link per site (Ticketmaster's direct link, not JamBase's affiliate redirect to it); JamBase-only shows use JamBase's link as returned.
JamBase budget guards
- Hard stop of 500 is a constant in the code and applies to each billing period; settings can only lower it.
- The count is JamBase's billing period, exactly: calls from the period's start to its end, back to zero at the turnover, with no rolling window and nothing carried over. Period dates come from JamBase's quota endpoint; later periods are derived as whole months until the next reading.
- Two layers. (1) A local SQLite ledger plus a separate append-only log; a call is recorded before it is sent, usage is the larger of the two, and concurrent runs cannot both pass. If one file is deleted the other keeps the count; if both go, calls refuse until re-initialised. (2) JamBase's free
/v3/quotaendpoint is read before a run, every 10 calls and afterwards; any failure stops the run. - Per run:
--confirm Nis a hard cap; no automatic retries; two failures in a row stop the run; the generic HTTP helper refuses JamBase URLs, so nothing bypasses the guard. Known HTTP errors are not counted (JamBase does not bill them); timeouts and crashes are. Every response is logged injambase_logfor diagnosis without spending calls.
Considered and not used
- Bandsintown (API for artists and their reps), Songkick (closed to new users), AXS, DICE, Resident Advisor (no public API; scrapers exist but you chose not to scrape). DICE shows still arrive through JamBase. Bandsintown and Songkick accounts linked to Spotify give a no-code alert service alongside this tool.
5. From raw results to listings
- One show, several listings: same artist and date, at the same place (within 1.5 miles or the same venue name), and start times within 30 minutes (or one missing). An 8:00 PM and a 10:30 PM show stay separate, each with its own ticket links.
- Cards: one card is one artist in one city, with nights no more than 7 days apart; Brooklyn in October and again in March is two cards. Two shows on a night are labelled Early and Late, three or more are numbered. Venue spellings that differ between sites are merged within a card (within 0.1 mile, or under a mile with one name containing the other) and the shorter is shown.
- Guest / bill appears when the artist's name is not in the event title (a support slot, or a festival such as Jingle Ball); the title is then shown. New means first seen after the previous fetch run began, so after a big first run most shows are new.
6. Filters, views and counters
- Areas each have a centre, a radius slider (2–100 miles) and an on/off box; a show matches inside any active area, by straight-line distance. NYC + NJ (20 mi) is centred in Manhattan: it takes in Brooklyn, Queens, the Bronx and nearby NJ, but not Asbury Park. Long Island is 40 mi and Philadelphia 25 mi (off by default). Across Long Island Sound, New Haven falls inside the Long Island circle though the drive is longer. Shows over 100 miles from every centre are not stored in the page.
- States can be switched off (Connecticut is off by default). Dates start today, and past shows are never listed: a show stays visible until 3:00 AM Eastern the morning after its date (a 6 PM Tuesday show until 3 AM Wednesday), whatever its start time, using New York time wherever you are viewing from. The same rule applies when the page is built, in the Markdown report and CSV, and when events are fetched, and past events are deleted from the database at each fetch and page build. Artists included hides artists below a position, with a line saying what it hides. Choices are remembered in this browser only.
- Views: Timeline, By artist and a Calendar, all using the same filters, plus the Saved list (below); on a phone, calendar days show a count, and tapping one opens its details.
- Saved list: the ☆ on any listing (Timeline, By artist or the Calendar's day detail) saves it, and the Saved view shows what you saved with a small notes box on each (600 characters, saved as you type). Saved shows ignore the filters, upcoming ones use the latest data (new nights and links appear), and shows that have passed or disappeared from the data stay, with their notes, from the copy kept when you saved them. Removing a show that has a note asks for a second click. A show is matched across refreshes by artist, city and overlapping dates. Where it is kept: on the copy hosted on your own site, in this browser only, so it is per device and is lost if you clear the site's data. Export list downloads it as a small file and Import list loads one (it merges, the newer note wins, and a file is checked and only known fields are kept), which moves a list between devices and backs it up. Inside Claude's page viewer the list is stored privately for you instead.
- Counters are snapshots from the last build, because a published page cannot call either service (and no key is ever in the page). Ticketmaster: requests left today, from its response headers, with the reset time. JamBase: calls left under your limit in the current billing period, with calls used, plan quota, period dates and exact reset time. After a period turns over the page says so until it is rebuilt.
7. Findings and limitations
- What JamBase adds: small-venue shows (DICE, Etix and See Tickets sales), late second shows, festival bills and support slots that Ticketmaster does not list under the artist. The yield falls with rank, and artists Ticketmaster could not find almost never have JamBase shows either. Current numbers are in the coverage panel. A single-artist test and a three-name batch matched Ticketmaster's shows exactly, which verified the query, parsing and batching.
- Not found on Ticketmaster ( of ): mostly small electronic, jazz and indie acts, session players, film composers and YouTubers who rarely tour. A few ambiguous names are real touring acts.
- Saves-only ranking is a weak stand-in for listening: classical and soundtrack albums and featured artists rank too high. Streaming history would fix it.
- No sold-out marker (a decision). Neither source exposes availability: Ticketmaster only reports on sale or off sale, and "off sale" often just means the sale has not opened yet; JamBase has no such field. A marker would be guesswork, so it is left out.
- Cancellations are caught at refresh time only. A show cancelled after the last fetch looks normal until the next refresh, as with any snapshot; the refresh then shows it struck out (section 4). A show can also vanish without a cancellation (a re-issued event, a venue change), and then reads "no longer listed" rather than "cancelled".
- Ticketmaster data is untidy: one show under several IDs, some wrong city fields, times as listed locally. Venue coordinates differ between sources by up to about half a mile.
8. Running it
| Command (project folder) | What it does |
|---|---|
python tourwatch.py ingest data | Reads the Spotify exports and ranks artists. Free. |
python tourwatch.py classify --top 1100 | MusicBrainz classification for the exclusions. |
python tourwatch.py resolve --top 1000 --max-requests N | Ticketmaster lookups for artists not yet resolved. |
python tourwatch.py fetch --top 1000 --fresh-hours 24 --max-requests N | Ticketmaster events for artists not fetched recently. Add --ignore-zero-flag to include artists that had no shows when looked up. |
python tourwatch.py quota | Reads the Ticketmaster allowance (1 request). |
python tourwatch.py jambase plan --include-matched --top 1000 | Offline preview of a JamBase run's cost. |
python tourwatch.py jambase fetch --confirm N --include-matched --top 1000 | Guarded JamBase batches; N is a hard cap on calls. |
python tourwatch.py jambase delta --confirm N | Refresh from changed events only (--max-pages limits its size). |
python tourwatch.py jambase probe --artist "A|B" --confirm 1 | One call for named artists, scored against Ticketmaster. |
python tourwatch.py jambase status / quota | Local usage and period (offline) / JamBase's own count (free call). |
python tourwatch.py report --regions nyc,long-island --condense | A Markdown version of the shows. |
python tourwatch.py page --out tourwatch.html | Rebuilds this page. |
Files: tourwatch.py (commands, report, page), sources.py (Ticketmaster, SeatGeek, Bandsintown adapters), jambase.py (guarded client), classify.py (MusicBrainz), spotify_ingest.py, page_template.html, readme_fragment.html (this text), tests/ (offline), tourwatch.db (everything fetched), jambase_ledger.db and jambase_requests.log (JamBase usage, never delete), .env (keys, never published or committed).
Refreshing from GitHub (no Claude needed)
The private repository tour-watch has four workflows, all started by hand (nothing is scheduled). Open Actions, pick one and press Run workflow (the "Run a refresh" link at the top of the page goes to the Actions tab). Each rebuilds tourwatch.html; download it from the run (Artifacts, kept 30 days) or from the state branch, and upload it to the website yourself. Estimates below are from real runs where available.
| Workflow | What it does | Ticketmaster calls | JamBase calls |
|---|---|---|---|
| 1 Ticketmaster refresh | Re-fetches events for every matched artist, including those flagged "0 upcoming" at lookup. Safe to run daily. | about 650 (cap 1,200; 630 on Nov 2), about 13% of the day's 5,000 | 0 (cannot reach JamBase) |
| 2 Ticketmaster + light JamBase | Workflow 1, then asks JamBase again only about the artists it already has shows for (66 artists, 6 requests of 12 names). Catches changes and cancellations for them; does not find new announcements for other artists. | about 650 | about 6 (cap 15) |
| 3 Full refresh | Ticketmaster refresh plus a look-up of unmatched artists, then JamBase: one measuring call, the delta (every event changed since the last run, which finds new announcements), the same top-up as workflow 2, and a rotating slice of the 240 artists asked about longest ago (20 requests), so everyone is re-asked about every few weeks. | about 1,050 (cap 1,600) | about 45 to 55: 1 + the delta (17 pages when last measured, 25 at most) + about 6 + about 20. Worst case 81. |
| Tools | A dropdown of single actions: status (free credit report), rebuild (rebuild the page, no calls), and each step of the others alone (tm-refresh, tm-monthly, jb-measure, jb-delta, jb-topup, jb-rotate). | status: 1; others as above | as above; status and rebuild: 0 |
- Limits live in the code, not in the form. An action is a name, never a number. The caps, the rules below and JamBase's hard stop of 500 cannot be changed from the workflow page.
- Combined runs check the worst case first. Workflows 2 and 3 refuse before doing anything (not even Ticketmaster) if their worst-case JamBase total (15 and 81) plus what is already used would pass the 300 target. A routine rule that leaves one step out (a 24-hour cooldown, a delta over 25 pages, no fresh measurement) is reported and the other steps carry on; a hard stop ends the JamBase steps, still builds the page, and is flagged as a partial run.
- JamBase rules:
jb-deltaruns only if ajb-measureunder 24 hours old exists, the watermark has not moved since, and it needs at most 25 pages (otherwise it refuses and says so, for you to decide); each JamBase step can run once per 24 hours; the top-up is capped at 15 calls and the rotating slice at 40; and a run is refused if it would take the billing period past 300 calls. Your 500 hard stop, the two-layer checks and the server count check stay underneath. A missing ledger refuses everything. Nothing retries. - Typed confirmation for JamBase. Workflows 2 and 3, and every JamBase action in Tools, refuse unless the word
JAMBASEis typed into the workflow's confirm box, so a stray tap cannot spend credits. GitHub's own approval step (a required reviewer on an environment) is not used: its availability for private repositories on the free plan was not confirmed. - Credit usage is reported in GitHub, not only here. Every run writes a "Credit usage" section to its summary: JamBase calls this run, used this billing period against the 300 target, 500 hard stop and 1,000 plan, what JamBase last reported, today's Ticketmaster allowance, and the last ten runs. Warnings appear at the top of the run when JamBase passes 80% of the target, reaches it, disagrees with the ledger by more than three calls, or Ticketmaster has under 1,000 calls left today. Every run is also appended to
usage.csvon thestatebranch (time, action, result, calls, JamBase total, note), and the page's Credit usage and run history panel shows the same figures as of its last build. Runstatusany time for the live count. - One run at a time. The workflow queues runs, so two cannot spend the same budget. The database, ledger and request log live on the
statebranch and are saved after every run, even a failed one, because they record JamBase spending. Keys are repository secrets and never appear in the page or the code. - Publish check: a rebuilt page is dropped, not offered for download, if it has under 60% of the previous page's shows or uses JamBase data without the attribution.
Housekeeping: the keys were rotated after being pasted into a chat during setup. Do not enable JamBase's Claude or ChatGPT connector: its calls use the same monthly counter and bypass the ledger.
9. Build log
- Oct 22, 2026: Ticketmaster searched for the top 100, then 400, then (after MusicBrainz exclusions) the top 1,000 artists, never repeating a request already made.
- Oct 22, 2026: JamBase added with its budget guards, verified with one-artist and three-name test calls against Ticketmaster, then seeded for all 1,000 artists in batches (the unmatched group first, then matched artists by rank), each step capped and reviewed.
- Oct 22, 2026: the JamBase counter and ledger were changed from a conservative rolling window to the exact billing period.
- Oct 22, 2026: README and coverage panel rewritten for the current state.
- Nov 4, 2026: refreshes moved to a private GitHub repository (manual runs only, fixed actions, spending rules in code, state on a branch); saved-list export/import and a staleness note added.
- Nov 2, 2026: cancelled and vanished shows are now kept and shown struck out instead of being deleted at refresh;
fetch --ignore-zero-flagadded; a saved-shows list with notes added to the page.