Your music taste as a solar system

Give it a Last.fm username and your listening history turns into a solar system. You’re the sun. Each genre is a planet, and every artist you listen to is a star orbiting it, bigger the more you’ve played them. Similar artists pull toward each other, so your taste sorts itself out. Then a fleet of ships leaves your top artist and hops from one similar artist to the next until they’ve reached everyone they can.

Launch solar.fm →source ↗runs on any public Last.fm username, or tick mock data
Solar.fm: genre planets orbiting the user's sun, with King Krule's artist card open
A real run. Genres orbit the sun as planets; the card shows an artist’s plays, system, closest matches and top tracks.
Three.jsReact Three Fiberd3-force-3dSSELast.fm APISpotify APINext.jsTypeScript
How it fits together

Almost all the work happens on the server. One API route fetches your artists, looks up each one’s genre and similar artists, builds a similarity graph, runs a physics simulation on it, and streams the finished universe to the browser. The browser only draws what it’s sent.

The hard part is the data, not the graphics. A full library means over a thousand API calls to two services with rate limits, so most of the design is about queues and caching.

Fetching the data
Top artists
One user.gettopartists call pulls up to 500 artists for the time period you pick. This is the only call about you, so it is never cached.
Per-artist lookups
Every artist then needs its genre tags and its similar artists. That is about 1,000 Last.fm calls for a full library. Each call has a 15 second timeout and retries twice with exponential backoff (500 ms, then 1 s).
Spotify
Spotify search fills in each artist's photo, link and popularity. Several Spotify artists can share a name, so an exact name match wins over Spotify's top hit, and among exact matches the most popular one wins.
Streaming
The API route is a Server-Sent Events stream. Progress events drive the loading bar while lookups run. Then the finished universe arrives in one burst, and the Spotify cards trickle in after it. If you close the tab, the route sees the aborted request and stops writing.
Queues
Worker pool
All bulk fetching goes through one small helper: N workers pull the next index off a shared counter until the list is empty, and results keep their original order. Last.fm runs 5 workers, about its rate limit. Spotify runs 3, because its development-mode limit is tighter.
Spotify queue
Once the universe is drawn, artists are sent to Spotify in rank order. Each answer is pushed to the browser as soon as it lands, so your top artists' cards fill in first.
Shared backoff
A 429 from Spotify means everyone should slow down, not just the worker that got it. One shared timestamp pauses every worker until the Retry-After passes. If Spotify asks for more than 30 seconds, the queue gives up on Spotify instead of stalling the page.
Ships wait on the queue
A ship can't leave an artist until that artist's Spotify data has arrived, so the fleet moves at the speed of the queue. When the queue drains, or the connection drops, every ship is released.
Caching
Data cache, 30 days
Genre tags, similar artists and Spotify results are stored in Next's data cache for 30 days. That cache is shared across server instances and deploys, so a second listener with overlapping taste skips most of the calls. "Spotify has no such artist" is cached too, so it isn't asked again.
Failures aren't cached
A lookup that throws (a timeout, a 429) is never stored. A rate-limited artist gets retried on the next search instead of being stuck without a genre for a month.
Finished universes, 5 minutes
Each finished universe, Spotify cards included, is kept in memory for 5 minutes per username and time period, up to 100 entries with the oldest evicted first. A reload replays it as the same SSE events, so the client code doesn't know the difference. A run that was abandoned halfway is never cached, so nobody gets a half-empty replay.
Top tracks, 10 minutes
The top tracks on an artist card come from one user.gettoptracks call (your top 1,000 tracks for the period), cached for 10 minutes and filtered per artist. Clicking around the universe costs no extra Last.fm calls.
Request sharing
If two parts of the pipeline ask Spotify about the same artist at once (the genre fill-in and the card queue), they share one request. The Spotify access token is shared the same way and reused until a minute before it expires.
Graph algorithms
Similarity graph
Last.fm returns up to 100 similar artists per artist. Only the ones already in your library are kept, the strongest 8 per artist. Edges are undirected, and when both artists list each other the stronger match wins.
Filling in genres
Some artists have no useful tags. Those take a vote from their neighbours in the graph, weighted by how similar they are, and the vote repeats until nothing changes, so a genre can spread down a chain of untagged artists. Next come Spotify's genres, then another round of voting. Anything still unknown gets your most-played genre.
Physics layout
Positions come from a d3-force-3d simulation run once on the server, 300 ticks. Similarity edges act as gravity: the stronger the match, the harder the pull, and the lighter artist moves more, so big artists end up in the middle of their system. Edges across genres pull at 15% strength so genres stay apart. Each artist is also pulled toward its genre's planet, pushed off its neighbours, and pulled weakly toward the sun. The starting positions come from a fixed hash, so the same library always settles into the same universe.
Star systems
After the layout settles, a k-means pass over the final positions (weighted by plays) groups artists into systems. It starts from the heaviest artist and then the farthest points, so the result is the same every time. Each system is named after its most-played artist.
Ship routes
Each ship starts at an artist and keeps flying to the most similar artist nobody has visited yet. When there's nowhere new to go, it lands. The next ship launches from the earliest-visited artist that still has unexplored neighbours. No artist is visited twice, so the routes form a spanning tree of the graph. Groups of artists that aren't connected to the rest each get their own launch from their top-ranked artist.
Rendering
Scene
React Three Fiber and Three.js with bloom. You're the sun at the centre, each genre is a planet with an orbit ring, and artists are stars sized by plays. Similarity inside a genre is drawn as straight lines. Links between genres are curved arcs that bow outward, capped at the strongest 80.
Ships
Ships are one instanced mesh, and the path behind each ship stays painted as a trail. Spectator mode puts the camera behind one ship from launch to landing, then picks another.
Cards
Click an artist for its card: rank, plays, genre, system, Spotify data, its closest matches in your library and your top tracks by them. Click a planet to see every ship that landed there, or step through the planets one by one with the tour controls.
What I’d do next

Real limits in the current build.

Slow first run
A big library needs about 1,000 Last.fm calls at 5 at a time, and nothing is drawn until all of them finish. Drawing the universe in stages as lookups land would make the first visit feel much faster.
Per-instance cache
The 5 minute universe cache lives in one server's memory. On serverless it's lost on a cold start and not shared between instances. Moving it to something like Redis would fix both.
Route planning cost
Finding the next launch point rescans from the start each time, which is O(n²) in the worst case. Fine at 500 artists, not at 5,000.
Deep-cut tracks
Top tracks only searches your top 1,000 tracks for the period, so an artist you rarely play can show no tracks at all.
Genre guessing
Artists with no tags, no tagged neighbours and no Spotify genres just get your top genre. That's a guess, not data.
← back to all projects