feat: rebuild the moods from the library's measured tag distribution
Build and publish container / build (pull_request) Successful in 2m40s

The first set of moods was guessed at, and a tag census of the real library
shows how badly. Four tags in the dance mood -- edm, big room, hardstyle,
electronic dance music -- carry nothing whatsoever, while techno, electro, big
beat, rave and progressive house together carry over seven thousand plays and
were all absent. The drum and bass mood missed four of the six spellings the
taggers actually use, plus techstep, darkstep and hospital records.

The eighties mood was the worst of it. It leaned on the bare `80s` tag, which is
the eleventh most-played tag here and sits on Def Leppard, Bon Jovi and AC/DC
rather than on anything with a synthesiser in it. Removing it and adding a
hair-metal mood -- glam rock, hair metal, glam metal, arena rock, AOR, windowed
to 1975-1994 -- puts roughly eight thousand plays somewhere sensible and stops
the synth playlist being a stadium rock playlist.

Whole clusters had no mood at all: metal at around twenty-five thousand plays
across heavy, thrash, speed, power, death and NWOBHM, and pop punk and emo at a
similar figure. Both now exist, along with bass, nu-metal and indie.

Three kinds of tag are excluded on principle, with a test enforcing it, because
each produces a playlist that looks reasonable and is not. Nationality describes
a passport rather than a sound, and `american` alone spans 241 artists. `rock`
and `electronic` span 340 and 275, which is most of the collection. And
Last.fm's most popular tag for an artist is frequently the artist's own name, so
selecting on `green day` yields a Green Day playlist wearing a genre's clothes.
This commit is contained in:
Emma Thorpe
2026-08-24 18:34:34 +01:00
parent fbce764dc5
commit 41f6b290d8
3 changed files with 131 additions and 26 deletions
+32 -9
View File
@@ -174,17 +174,40 @@ An artist neither key resolves is recorded as fetched with no tags, so the next
pass does not spend a request on it again. A genuine failure — a rate limit, a pass does not spend a request on it again. A genuine failure — a rate limit, a
bad key — is *not* recorded, so that one is retried. bad key — is *not* recorded, so that one is retried.
The built-in moods are chosen for this library rather than as a general The built-in moods are built from the **tag distribution of this library**,
taxonomy: measured, rather than from a general taxonomy:
| Mood | Selected on | | Mood | Selected on |
| ------------------ | ------------------------------------------------- | | --------------- | -------------------------------------------------------- |
| `80s-synths` | synthpop, new wave, synthwave — released 1975-1992 | | `drum-and-bass` | drum and bass and its six spellings, liquid funk, neurofunk, jungle, techstep, hospital records |
| `high-energy-rock` | hard rock, punk, pop punk, alternative | | `bass` | dubstep, brostep, grime, trip-hop, big beat |
| `screamo` | screamo, post-hardcore, metalcore, emo | | `dance` | house and its variants, trance, techno, electro, rave |
| `drum-and-bass` | drum and bass, liquid funk, neurofunk, jungle | | `pop-punk` | pop punk, punk, emo, emocore, easycore, power pop |
| `dance` | house, big room, hardstyle, trance, dubstep | | `screamo` | screamo, post-hardcore, metalcore, melodic hardcore, trancecore |
| `classic-rock` | classic rock, prog, psychedelic, blues rock | | `heavy-metal` | heavy metal, thrash, speed, power, death, prog, NWOBHM |
| `hair-metal` | hair metal, glam metal, glam rock, arena rock, AOR — 1975-1994 |
| `nu-metal` | nu metal, alternative metal, rapcore, industrial |
| `classic-rock` | classic rock, prog, psychedelic, blues rock, 70s, 60s |
| `80s-synths` | synthpop, electropop, new wave, post-punk — 1975-1992 |
| `indie` | indie, indie rock, indie pop, britpop, singer-songwriter |
Measuring first mattered. `synthwave`, `edm`, `big room` and `hardstyle` are
plausible tags that carry **nothing at all** here, while `techstep`, `easycore`
and `hospital records` carry real weight. Guessing produces the first list.
Three kinds of tag are never used, and there is a test enforcing it:
- **Nationality** — `american` alone spans 241 artists. A passport is not a
sound.
- **`rock` and `electronic`** — 340 and 275 artists, most of the library. A
mood that matches everything is not a mood.
- **Artist names** — Last.fm's most popular tag for an artist is frequently
their own name. `green day`, `paramore` and `queen` are single-artist
playlists waiting to happen.
`80s-synths` deliberately excludes the bare `80s` tag, which in this library
sits on Def Leppard and Bon Jovi rather than on anything with a synthesiser in
it. Those belong to `hair-metal`, which is why that mood now exists.
An artist qualifies when their tag weights inside a mood sum to at least 30 out An artist qualifies when their tag weights inside a mood sum to at least 30 out
of Last.fm's 0-100 scale. One low-weight tag is not a genre, it is somebody's of Last.fm's 0-100 scale. One low-weight tag is not a genre, it is somebody's
+65 -17
View File
@@ -1242,49 +1242,97 @@ PLAYLISTS = (
# `years` filters on the album's release date, which is what separates eighties # `years` filters on the album's release date, which is what separates eighties
# synth records from everything a synthpop tag would otherwise drag in. # synth records from everything a synthpop tag would otherwise drag in.
DEFAULT_VIBES = ( DEFAULT_VIBES = (
# Built from the tag distribution of this library rather than from a general
# taxonomy, which is why some obvious-looking tags are absent and some
# unobvious ones are here. `synthwave`, `edm`, `big room` and `hardstyle`
# carry nothing at all; `techstep`, `easycore` and `hospital records` carry
# real weight.
#
# Three kinds of tag are deliberately never used. Nationality -- american,
# british, swedish -- describes a passport, not a sound, and `american`
# alone spans 241 artists. `rock` and `electronic` span 340 and 275, which
# is most of the library and therefore no mood at all. And Last.fm's most
# popular tag for an artist is frequently their own name, so `green day`,
# `paramore` and `queen` are single-artist playlists waiting to happen.
{ {
"name": "80s-synths", "name": "drum-and-bass",
"tags": [ "tags": [
"synthpop", "synth pop", "synth-pop", "new wave", "synthwave", "drum and bass", "dnb", "drum n bass", "drum'n'bass", "drum & bass",
"new romantic", "electropop", "80s", "1980s", "drum 'n' bass", "liquid funk", "neurofunk", "jungle", "techstep",
"darkstep", "drumstep", "breakbeat", "hospital records",
], ],
"years": [1975, 1992],
}, },
{ {
"name": "high-energy-rock", "name": "bass",
"tags": ["dubstep", "brostep", "grime", "trip-hop", "big beat"],
},
{
"name": "dance",
"tags": [ "tags": [
"hard rock", "punk rock", "pop punk", "punk", "alternative rock", "house", "electro house", "progressive house", "tech house", "trance",
"rock", "garage rock", "skate punk", "techno", "electro", "rave", "dance", "minimal",
],
},
{
"name": "pop-punk",
"tags": [
"pop punk", "pop-punk", "punk rock", "punk", "skate punk", "emo",
"emocore", "easycore", "powerpop", "power pop", "post-grunge",
], ],
}, },
{ {
"name": "screamo", "name": "screamo",
"tags": [ "tags": [
"screamo", "post-hardcore", "metalcore", "emo", "hardcore", "screamo", "post-hardcore", "metalcore", "melodic metalcore",
"melodic hardcore", "emocore", "melodic hardcore", "hardcore", "trancecore", "deathcore",
], ],
}, },
{ {
"name": "drum-and-bass", "name": "heavy-metal",
"tags": [ "tags": [
"drum and bass", "drum n bass", "dnb", "liquid funk", "neurofunk", "heavy metal", "metal", "thrash metal", "thrash", "speed metal",
"jungle", "breakbeat", "power metal", "death metal", "progressive metal", "nwobhm",
"classic metal",
], ],
}, },
{ {
"name": "dance", "name": "hair-metal",
"tags": [ "tags": [
"electro house", "house", "big room", "electronic dance music", "hair metal", "glam metal", "glam rock", "arena rock", "aor",
"edm", "hardstyle", "trance", "dubstep", "electro", "rock and roll", "rock n roll",
],
"years": [1975, 1994],
},
{
"name": "nu-metal",
"tags": [
"nu metal", "nu-metal", "alternative metal", "rapcore",
"industrial metal", "industrial rock", "industrial",
], ],
}, },
{ {
"name": "classic-rock", "name": "classic-rock",
"tags": [ "tags": [
"classic rock", "progressive rock", "psychedelic rock", "classic rock", "progressive rock", "psychedelic rock", "psychedelic",
"blues rock", "70s", "60s", "blues rock", "blues", "southern rock", "art rock", "space rock",
"british invasion", "folk rock", "70s", "60s",
], ],
}, },
{
"name": "80s-synths",
# Without the bare `80s` tag, which in this library sits on Def Leppard
# and Bon Jovi rather than on anything with a synthesiser in it. The
# year window is kept as a second filter, not as the only one.
"tags": [
"synthpop", "electropop", "new wave", "post-punk",
"post-punk revival", "new romantic",
],
"years": [1975, 1992],
},
{
"name": "indie",
"tags": ["indie", "indie rock", "indie pop", "britpop", "singer-songwriter"],
},
) )
+34
View File
@@ -1198,3 +1198,37 @@ def test_a_real_failure_is_not_recorded_so_the_next_pass_retries(tmp_path):
# One artist failed hard and must still be pending. # One artist failed hard and must still be pending.
assert store.scalar("SELECT COUNT(*) FROM artist_tag_fetched") == 1 assert store.scalar("SELECT COUNT(*) FROM artist_tag_fetched") == 1
# Tags that carry real weight in a real library, against tags that describe a
# passport, span most of the collection, or are somebody's artist name.
USELESS_TAGS = {
"american", "british", "australian", "canadian", "swedish", "dutch", "german",
"scottish", "english", "uk", "usa", "canada",
"rock", "electronic", "pop", "alternative", "metal ", "all", "heavy",
"female vocalists", "male vocalists", "female vocalist",
"my top songs", "cover", "covers", "not emo",
"green day", "paramore", "queen", "bon jovi", "shinedown", "aerosmith",
"journey", "fleetwood mac",
}
def test_no_mood_selects_on_a_useless_tag():
"""Nationality is not a sound; `rock` and `electronic` span most of the
library; and Last.fm's top tag for an artist is often their own name."""
for vibe in music_curator.DEFAULT_VIBES:
overlap = {tag.casefold() for tag in vibe["tags"]} & USELESS_TAGS
assert not overlap, f"{vibe['name']} selects on {overlap}"
def test_mood_names_are_unique():
names = [vibe["name"] for vibe in music_curator.DEFAULT_VIBES]
assert len(names) == len(set(names))
def test_the_eighties_mood_does_not_lean_on_the_bare_decade_tag():
"""In this library `80s` sits on Def Leppard and Bon Jovi, not on
synthesisers. The hair-metal mood is where those belong."""
synths = next(v for v in music_curator.DEFAULT_VIBES if v["name"] == "80s-synths")
assert "80s" not in synths["tags"]
assert synths["years"] == [1975, 1992]