Projects · Music · Full Stack · May 2026
Concert Radar
Concerts near you, ranked by how much you actually listen to each artist.
Source: Concert Radar README. Score = sum of (50 minus list position) x weight, divided by 300.
A FastAPI and D3 app turns Spotify listening history into a ranked list of shows within 50 miles. A fuzzy matcher removes most tribute bands. Its accuracy is unmeasured, and the README says so.
The Problem
Ticketing sites list shows by date or distance. I wanted a list sorted by the artists I actually play, so the shows I would care about most would be at the top.
What I Built
A full-stack web app. You log in with Spotify, set your city, and get a feed of upcoming concerts within 50 miles, ranked by how much you listen to each artist. A D3 chart shows the affinity scores behind the ranking.
The full flow runs locally: Spotify login, sync, scoring, Ticketmaster search and the ranked feed. The public link is a demo that runs in the browser on made-up artists and shows, scored by the same formula the backend uses. No account needed.
How It Works
- Spotify syncThe backend fetches your top 50 artists for three time ranges: about the last 4 weeks, the last 6 months, and all time.
- Affinity score, 0 to 100Spotify returns ranked lists, not play counts, so the score is rank-based: 50 points for #1 down to 1 for #50, weighted 3, 2 and 1 by recency, divided by the 300-point maximum.
- Ticketmaster searchFor your top 20 artists, search music events within 50 miles. Your city becomes coordinates through OpenStreetMap Nominatim.
- Fuzzy matchingA keyword search also returns tribute acts and festivals. Comparing the artist against each event’s listed performers with difflib throws most of them out, with a stricter threshold when only the title is available.
- Ranked feedSorted by affinity, then by date. Cached per user for 6 hours.
Three engineering choices worth explaining:
- Bearer tokens instead of cookies. The frontend is on GitHub Pages and the API on Render. Browsers increasingly block cross-site cookies, so the session travels as a JWT in the Authorization header.
- One fetch function made the demo cheap. Every request already went through
apiFetch(). Demo mode swaps what that one function calls and leaves the rendering code alone, so the demo runs the same UI as the real app. - All SQL in one module. Since every connection comes from
get_db(), the tests swap in a fake that records queries. That is why 39 tests finish in about a second with no network.
Results
The full app works end to end locally. GitHub Actions runs the test suite and ruff on every push, and publishes the demo to GitHub Pages. The demo is about 40 KB of HTML, CSS and JavaScript and works on phone-sized screens.
Limitations
The first version cached shows by artist, for everyone. Two users in different cities who liked the same artist saw each other’s local shows, and changing your city deleted cached shows for other users. The cache now lives at the user level.
A second bug hid behind the first: a city with no matching shows left no cached rows, so every page load searched Ticketmaster again. The app now stores when each user’s concerts were last fetched instead of inferring it from the rows.
Spotify access tokens last an hour, and nothing called the refresh endpoint, so sync failed an hour after login. Sync now refreshes the token when it is within a minute of expiring.
- Matcher accuracy is unmeasured. Its thresholds (0.70 for attraction names, 0.82 for event titles) were tuned by hand on real searches. There is no labelled dataset.
- Spotify Development Mode only lets users added in the developer dashboard log in, which is why the public link is a demo.
- Ticketmaster only. AXS, Dice, Eventbrite and many small venues are missing. The 20 artist searches run one after another to stay under the free-tier rate limit, so the first uncached load takes a few seconds.
- Not production security. Spotify tokens are stored unencrypted, the session JWT lives in
localStorage, and OAuth state is kept in process memory, so the backend must run as a single worker. - The tests never run real SQL. The fake database means a typo in a query would only show up against a real Postgres.
- Free-tier hosting. Render sleeps when idle, so the first request takes about a minute, and Supabase pauses free projects after a week without activity.