diff --git a/README.md b/README.md index c8382f6..7b8cde2 100644 --- a/README.md +++ b/README.md @@ -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 bad key — is *not* recorded, so that one is retried. -The built-in moods are chosen for this library rather than as a general -taxonomy: +The built-in moods are built from the **tag distribution of this library**, +measured, rather than from a general taxonomy: | Mood | Selected on | -| ------------------ | ------------------------------------------------- | -| `80s-synths` | synthpop, new wave, synthwave — released 1975-1992 | -| `high-energy-rock` | hard rock, punk, pop punk, alternative | -| `screamo` | screamo, post-hardcore, metalcore, emo | -| `drum-and-bass` | drum and bass, liquid funk, neurofunk, jungle | -| `dance` | house, big room, hardstyle, trance, dubstep | -| `classic-rock` | classic rock, prog, psychedelic, blues rock | +| --------------- | -------------------------------------------------------- | +| `drum-and-bass` | drum and bass and its six spellings, liquid funk, neurofunk, jungle, techstep, hospital records | +| `bass` | dubstep, brostep, grime, trip-hop, big beat | +| `dance` | house and its variants, trance, techno, electro, rave | +| `pop-punk` | pop punk, punk, emo, emocore, easycore, power pop | +| `screamo` | screamo, post-hardcore, metalcore, melodic hardcore, trancecore | +| `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 of Last.fm's 0-100 scale. One low-weight tag is not a genre, it is somebody's diff --git a/music_curator.py b/music_curator.py index 03687f0..7c15463 100644 --- a/music_curator.py +++ b/music_curator.py @@ -1242,49 +1242,97 @@ PLAYLISTS = ( # `years` filters on the album's release date, which is what separates eighties # synth records from everything a synthpop tag would otherwise drag in. 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": [ - "synthpop", "synth pop", "synth-pop", "new wave", "synthwave", - "new romantic", "electropop", "80s", "1980s", + "drum and bass", "dnb", "drum n bass", "drum'n'bass", "drum & bass", + "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": [ - "hard rock", "punk rock", "pop punk", "punk", "alternative rock", - "rock", "garage rock", "skate punk", + "house", "electro house", "progressive house", "tech house", "trance", + "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", "tags": [ - "screamo", "post-hardcore", "metalcore", "emo", "hardcore", - "melodic hardcore", "emocore", + "screamo", "post-hardcore", "metalcore", "melodic metalcore", + "melodic hardcore", "hardcore", "trancecore", "deathcore", ], }, { - "name": "drum-and-bass", + "name": "heavy-metal", "tags": [ - "drum and bass", "drum n bass", "dnb", "liquid funk", "neurofunk", - "jungle", "breakbeat", + "heavy metal", "metal", "thrash metal", "thrash", "speed metal", + "power metal", "death metal", "progressive metal", "nwobhm", + "classic metal", ], }, { - "name": "dance", + "name": "hair-metal", "tags": [ - "electro house", "house", "big room", "electronic dance music", - "edm", "hardstyle", "trance", "dubstep", "electro", + "hair metal", "glam metal", "glam rock", "arena rock", "aor", + "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", "tags": [ - "classic rock", "progressive rock", "psychedelic rock", - "blues rock", "70s", "60s", + "classic rock", "progressive rock", "psychedelic rock", "psychedelic", + "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"], + }, ) diff --git a/tests/test_music_curator.py b/tests/test_music_curator.py index d47d353..738e914 100644 --- a/tests/test_music_curator.py +++ b/tests/test_music_curator.py @@ -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. 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]