How it works
The story behind this site. How it's built and what was learned.
Architecture
A crontab runs the pipeline every Sunday at 9 AM UTC.
The dbt DAG
Staging views normalize raw tables. Marts do the analytical joins. The api layer is narrow, pre-joined, and pre-indexed — the app never queries marts directly.
staging
stg_artist_snapshots— one row per artist per weekstg_weekly_charts— chart page/rankstg_genre_artists— artist ↔ genre tag
intermediate
int_artist_base— profanity filter applied once, at the source
marts
artist_growth_summary— first/last listener counts, total growthartist_chart_position— chart page/rank stats, left-joined so uncharted artists still get a rowlistener_growth— week-over-week deltasgenre_stats— per-genre aggregates
api (serving layer)
api_artist_profile— one pre-joined row per artistapi_artist_timeseries— listeners indexed to 100 at first observationapi_cohort_weekly— precomputed median/p25/p75 per cohort per week
Lessons learned
Next.js → static HTML
Originally, I had Music Growth's frontend entirely vibe-coded. Next.js was (and still is) a very popular framework at the time, so I chose that and had AI go to work. My goal was to create a fun and interactive frontend that anyone could use, not something boring like Power BI. However, issues and problems kept coming up, and the AI repeatedly had to implement more and more complex solutions that added on to one another.
Although the site eventually got up and worked just fine with Next.js, I had completely lost touch with the entire web directory of my codebase. I decided that I needed to simplify the frontend into something I could understand and personalize more for myself.
I had already used the HTML / Tailwind CSS / JS stack for my portfolio site (which fetched data from JSON files), so I figured I could do the same thing here (JSON export layer mentioned below). In the end, file count decreased 83% (53 → 9) and line count decreased 92% (9,226 → 721). The site runs smoother, looks the same, and most importantly, I understand it.
Neon vs. the JSON export layer
Since the site was originally dynamic, I was fetching my data from Neon, a cloud Postgres serverless database platform. Neon worked well, but had restricting free-tier limits. I decided that, since the size of the database was very small, I could just hold it in my Hetzner instance, creating a single box that hosts my site and holds the data. That still doesn't solve the static site problem, since I wouldn't be able to query any database, even the local Postgres one on my cloud instance.
So instead, after every weekly pipeline run, a python script generates JSON files from the local Postgres database for every request that previously required an API call. Javascript can then be used to read these JSON files from disk. This resulted in a much simpler, faster, and robust site.
What the data says
Listeners vs. scrobbles
Last.fm tracks both an artist's listeners and scrobbles. Listeners are unique users who have ever played that artist. Playcount/scrobbles are the total plays across all users for that artist. The data on this site uses listeners, not scrobbles. Last.fm's charts, however, use recent scrobble activity to rank artists.
So, when this site measures "growth," what it's really measuring is new users discovering an artist for the first time, not repeated engagements.
Listener counts are cumulative
This is the main problem that led to the creation of this site. Last.fm only displays cumulative listener stats. So the only way to monitor artist growth is to track stats over time (in this site's case, weekly).
Which artists grow faster?
Overall, the findings indicate what could be expected. As of week 19 data, the median % growth among the smallest artist quintile (1-28k artists) at 0.63% and the largest quintile (293k-9.1M) at 2.12%. Interestingly, the smallest quintile average % growth exceeds 50% while the largest quintile rests around 3%, meaning that most small artists stagnate, a few explode, and bigger artists grow more reliably.
As for genres, as of week 19 data, EDM, pop, and rap have the highest median growth %.
Chart position ≠ artist size
Last.fm has an API endpoint for their chart (chart.getTopArtists), which I originally assumed meant that it ordered by popularity. However, after reviewing data gained from chart placements, I learned that Last.fm's chart orders roughly by scrobble activity. This means that an artist with millions of listeners could rank lower, while buzzing smaller artists can rank higher.
After this discovery, I started grouping artists into size groups and quintiles before comparing statistics, and learned to never assume anything about data again.
Redacted artists
Certain artists have names that contain profanities. In the interest of keeping this site pg, I use an (admittedly very basic) REGEX to filter artist names before they're even included in the database.
Currently, almost 100 names have been redacted. If you feel you or your band has not been included in this database due to improper redaction, please let me know.
If you made it to the bottom of this page, thanks for sticking around! You may want to check out my other stuff.