Your personal Spotify stats, all year round. Like Wrapped plus stats.fm, running on your own computer.
Stack: MongoDB, Express, React (Vite), Node. Research notes are in RESEARCH.md.
- Node 20.6 or newer
- MongoDB running locally (or a MongoDB Atlas connection string)
- A Spotify Premium account. Spotify requires this for Development Mode apps.
- Go to https://developer.spotify.com/dashboard and log in.
- Click Create app.
- Name:
Echoes(anything works). - Redirect URI:
http://127.0.0.1:5173/api/auth/callback, then click Add. Spotify banslocalhost, so it must be127.0.0.1. - APIs used: tick Web API.
- Name:
- Save, then open Settings. Copy the Client ID. Click View client secret and copy that too.
- Open User Management. Add your own name and the email of your Spotify account. Up to 5 people can be added. Anyone not on this list gets an error when logging in.
- Copy
.env.exampleto.env. - Paste the Client ID and Client Secret into
.env. - Set
SESSION_SECRETto a long random string. One way to make one:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" - Install everything:
npm run setup
If you installed MongoDB as a Windows service, start it from an admin PowerShell:
Start-Service MongoDBnpm run devOpen http://127.0.0.1:5173. Use 127.0.0.1, not localhost, or the login cookie won't stick.
- Go to https://www.spotify.com/account/privacy/.
- Tick Extended streaming history. You don't need the other boxes.
- Click Request data and confirm in the email Spotify sends you.
- It can take up to 30 days. You'll get a zip with
Streaming_History_Audio_*.jsonfiles.
Meanwhile, Echoes saves your plays by itself every 30 minutes, starting the moment you log in.
- Unzip it.
- Either:
- In the app: click Import Streaming_History_Audio files and pick all the
Streaming_History_Audio_*.jsonfiles. (Skip theVideoones.) - Or from a terminal:
npm run import -- "C:\path\to\the\unzipped\folder"
- In the app: click Import Streaming_History_Audio files and pick all the
- Re-importing the same files is safe. Repeats are skipped.
- Covers and artist photos fill in slowly in the background (about 1 lookup per second) while the server runs. Your most-played songs go first. The app shows the progress.
On your laptop the server runs nonstop. On Vercel nothing runs between visits, so an outside timer calls /api/cron every 30 minutes to save new plays, sync Liked Songs and fill in covers.
- Database: make a free M0 cluster at mongodb.com/atlas. Add a database user, allow access from anywhere (
0.0.0.0/0, Vercel has no fixed IP), and copy themongodb+srv://...connection string. Add/echoesbefore the?so the data lands in a database called echoes. - Copy your data (optional):
npm run copy-db -- "mongodb+srv://...". Safe to run twice. - Vercel: import the GitHub repo at vercel.com/new. Leave the build settings alone (
vercel.jsonsets them). Under Environment Variables add:SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRETSPOTIFY_REDIRECT_URI=https://<your-name>.vercel.app/api/auth/callbackSESSION_SECRET(same as your.envif you copied data; otherwise just log in again)MONGODB_URI= the Atlas stringCRON_SECRET= a new long random stringTIMEZONE,STREAM_MIN_MS(optional)
- Spotify dashboard: add
https://<your-name>.vercel.app/api/auth/callbackas a Redirect URI (keep the 127.0.0.1 one for local use). - Timer: at cron-job.org make a job for
https://<your-name>.vercel.app/api/cron, every 30 minutes, with the headerAuthorization: Bearer <CRON_SECRET>. Its response shows new plays and covers filled.
Spotify Development Mode allows 5 users, each added by email under User Management.
All under /api/stats, and all need you to be logged in. range is today, 7d, 4w, 6m, 1y, life or custom (with from=YYYY-MM-DD&to=YYYY-MM-DD). Add private=hide to leave out private sessions.
| Endpoint | What it gives |
|---|---|
/top/tracks?range=4w (also artists, albums, genres) |
{ source, items }. Top 100 with streams, minutes, cover, Spotify link and prevRank (rank in the previous equal-length period; null = new). If your history doesn't cover 4w/6m/1y yet, tracks/artists come from Spotify's own ranking (source: "spotify", no counts) |
/summary?range=7d |
Streams, minutes, hours, unique tracks/artists/albums, plus previous for ↑/↓ |
/clock?range=1y |
Streams and minutes per hour × weekday |
/timeline?range=6m&unit=week |
Minutes per day, week or month |
/streak |
Longest and current listening streak |
/skips?range=life |
Skip rate and most-skipped songs (imported history only) |
/first?q=LE SSERAFIM |
First time you played an artist or song (exact name, any case) |
/recent?limit=20 |
Recently played, with covers |
/wrapped?range=1y |
Everything for a Wrapped story: totals, top 5 artists/tracks, top album, top genres, obsession (most-played song in one week), monthly eras, listening personality |
- ✅ Spotify login + token refresh + MongoDB connection
- ✅ Plays collection + the every-30-minutes recently-played check + history import + background cover/genre filler
- ✅ Counting endpoints (top lists for any range, stats)
- ✅ Mockups → Home + Top pages. Until enough history exists, the 4W/6M/1Y tabs fall back to Spotify's own top lists (rank only, no counts).
- ✅ Stats page + charts (plain SVG, no chart library)
- ✅ Wrapped data (obsession, eras, personality)
- ✅ Mockups → Wrapped story cards + image download (tap/swipe/arrow keys; each card saves as a 1080×1920 PNG)