Gigs
web app · open to view · updated every 3 hours
Gigs tells me when artists I actually listen to are playing in SF, the East Bay, or the South Bay. It reads my Spotify listening, pulls every music event from Ticketmaster, SeatGeek, Resident Advisor, and DICE, and keeps the shows that match.
Anyone can open it. You see my artists first, and you can flip to everything or search another city.
Why I built this
Music is really important to me, and I listen to a lot of it. A lot of the artists I listen to aren’t very big, so it’s hard to keep track of when they’re in town.
Gigs keeps track for me. It checks every show around the Bay against what I actually play, and lets me know when someone I listen to is coming through.
What it does
- My artists Top artists, recent plays, my Spotify library, and my full listening history. Stream counts sit next to every show.
- Four sources Ticketmaster, SeatGeek, Resident Advisor, and DICE. The same show on two sites becomes one card, cheapest seller first.
- Filters This weekend, under $50, 21+, all ages, and small rooms, about 700 people or fewer.
- Alerts New shows from my artists arrive as iPhone notifications, and a push every Monday at 9 AM lists the week.
- Any city Search somewhere else and it fetches that city live, then keeps it for six hours.
- Similar artists From Last.fm, for when I want something new.
How it’s built
One Worker, one database
A single Cloudflare Worker in TypeScript serves the page, the API, and the scheduled jobs. D1, Cloudflare’s SQLite, holds two tables: a small key-value store and one row per source, area, and month of events. No framework and no runtime dependencies, about 4,900 lines and 142 tests.
Artists from this site
Every run reads the same Spotify endpoints behind /listening: top artists for three time ranges twice a day, and the last 50 plays, each counted once. A one-time import of my extended streaming history fills in the past, and my Spotify library connects with PKCE, so no client secret is stored.
Matching on Spotify IDs
Ticketmaster and SeatGeek often link a performer to a Spotify artist. The same ID is a sure match even when the names are spelled differently, and a different ID rules out a namesake, so a local band called Sage never passes for the Sage I listen to. Name-only matches get marked unsure.
A sync that resumes
A cron runs every three hours and pulls every music event in each home area, a month at a time. Each source, area, and month is its own row, so a run that hits its budget of 300 requests saves its place and the next run picks up there. This month refreshes every six hours, later months less often.
DICE, from my Mac mini
DICE has no public API and checks for bots, so I don’t try to get around it. A script drives a real Chrome window on my Mac mini three times a day, loads DICE’s San Francisco pages, reads the events the page itself loads, and posts them to the Worker.
Push without a library
Web Push needs the payload encrypted (RFC 8291) and every request signed (VAPID, RFC 8292). Both are written against WebCrypto in the Worker. The first device to subscribe marks every listed show as seen, so turning alerts on doesn’t set off a flood. The Monday push is scheduled at two UTC hours so it lands at 9 AM Pacific all year.
Private where it counts
Ticket sites only see a city, a radius, and a date range, never my artists. The history import drops IPs, devices, and track names before anything leaves my machine. Anyone can look, but changes need a password, and the app lives on its own subdomain so nothing on this site can reach its login.
Views
Links
Questions? [email protected]