Empfehlungssystem für lokale Musik (1/2): Datenquellen
Lokale Musik ergänzen durch last.fm
Lokale Musik ist zunächst nur das: Ordner mit Dateien, evt. noch ein paar manuell gepflegte Playlists. Es fehlt die darüber liegende Intelligenz: Was höre ich gerne? Was lässt sich daraus ableiten? Spotify besteht neben dem Zugang zu quasi jeder Musik auch aus einem Zoo von unterschiedlichen Werkzeugen, immer wieder neue Künstler zu entdecken, auf Neuerscheinungen und Konzerte hingewiesen zu werden, alte Vorlieben wieder aufleben zu lassen, den Wrapped-Report etc. Das alles gilt es, für lokale Musik nachzubauen.
Die Eingabekanäle: Was mein Musik-System füttert#
Jede Empfehlungs-Funktion ist nur so gut wie die Daten dahinter. Diese fließen aus acht Kanälen: Der eigenen FLAC-Bibliothek (beets), der Konzert-Historie (concerts.json), handkuratierten Lieblings-Playlists (playlist_data/), vier automatisch erzeugten JSON-Snapshots (loved_tracks.json, track_plays.json, Einkaufsliste.json, interesting_artists.json) und dem API-Cache. Hier geht es ausschließlich um diese Sammel-Kanäle — nicht um die Funktionen, die sie auswerten.
Die eigene Bibliothek: beets#
beets ist das Verwaltungs-Tool meiner FLAC-Sammlung. Daraus exportiere ich einen flachen Tabellen-Dump nach beets/beets_all.tsv — eine Zeile pro Track, pipe-getrennt mit sieben Feldern: Track-Künstler, Album-Künstler, Titel, Album, Jahr, Erstveröffentlichungsjahr und absoluter Dateipfad.
Diese Datei enthält somit, was ich tatsächlich besitze. Jede Auswertungs-Funktion entscheidet anhand von beets, ob ein gewünschter Song als lokale FLAC-Datei vorliegt oder über YouTube nachgeladen werden muss. Ohne diesen Index gäbe es keine Unterscheidung zwischen „habe ich” und „gibt es irgendwo”.
Aktualisiert wird der Dump manuell, nach Änderungen an der Bibliothek (neue Alben, korrigierte Tags) — nicht per Zeitplan, weil sich die Sammlung nicht von selbst ändert:
beet ls -f '$artist|$albumartist|$title|$album|$year|$original_year|$path' > beets/beets_all.tsvbashMachine Head|Machine Head|Davidian|Burn My Eyes|1994|1994|/Users/truhe/Music/FLAC_music/Machine Head/Burn My Eyes/01 Davidian.flacplaintextDie Konzert-Historie: concerts.json#
Eine manuell gepflegte Liste aller besuchten und per Ticket geplanten Konzerte — eine Zeile pro gesehenem Künstler, mit Datum, Ort und gegebenenfalls Festival. Sie ist das stärkste persönliche Interesse-Signal: Wer für eine Band Geld, Zeit und Anreise investiert, meint es ernster als bei einem nebenbei gestreamten Song.
Die Daten stammen aus alten Bestell-E-Mails und eingescannten Papiertickets; die Entstehung habe ich in einem eigenen Beitrag beschrieben. Gepflegt wird die Datei von Hand — jedes neue Konzert kommt als weitere Zeile dazu.
{"artist": "Bolt Thrower", "date": "2012-04-07", "city": "London",
"country": "GB", "venue": "HMV Forum", "festival": "Boltfest 2012"}jsonKuratierte Playlists: playlist_data/#
Ein Ordner mit handgebauten .m3u-Playlists, je eine pro Künstler oder Album, die nur die von mir bevorzugten Songs enthalten. Sie sind ein doppeltes Signal: ein „Best of”-Filter (für ein Künstler-Radio etwa, das sich auf die kuratierte Auswahl beschränkt) und ein positiver Gewichtungs-Hinweis darauf, welche Stücke aus einem Katalog ich wirklich mag.
Erstellt werden sie von Hand im Musik-Player und liegen auf der FLAC-Festplatte. Aktualisiert ebenfalls manuell — von Skripten oder Claude generierte Playlists landen bewusst nie in diesem Ordner, damit meine Handarbeit nicht versehentlich überschrieben wird.
#EXTM3U
#EXTINF:-1,Outsiders
../Against the Current/Outsiders/01 Outsiders.flac
#EXTINF:-1,Running With the Wild Things
../Against the Current/In Our Bones/01 Running With the Wild Things.flacplaintextLast.fm-Favoriten: loved_tracks.json#
Ein Abzug aller bei Last.fm mit Herz markierten Tracks, jeweils mit dem Zeitpunkt des Markierens (loved_uts). „Loved” ist eine bewusste Geste — deutlich stärker als bloße Spielzahlen — und dient an mehreren Stellen als Quelle bevorzugter Songs.
Die Daten kommen direkt aus der Last.fm-API. Erzeugt wird die Datei wöchentlich (montags), als erster Schritt der Snapshot-Kette.
{
"generated": "2026-05-27T17:47:58",
"user": "XXX",
"count": 903,
"tracks": [
{"artist": "Draconian", "track": "The Sethian", "loved_uts": 1779889935}
]
}jsonSpielzahlen je kuratiertem Track: track_plays.json#
Eine Momentaufnahme der persönlichen Last.fm-Spielzahl (userplaycount) für eine kuratierte Track-Menge — gespeist aus loved_tracks.json plus mehreren statischen Top-Songs-CSVs (historische Spotify-Wrapped-Exporte und Jahres-Top-100). Für jeden Track wird per Last.fm track.getInfo die aktuelle Eigen-Spielzahl und der kanonische Albumname geholt; das Feld sources hält fest, aus welcher Quelle der Track stammt.
Sinn der Datei: die Spielzahlen einmal sammeln, statt sie bei jedem Bedarf neu abzufragen. Erzeugt wöchentlich (montags), mit einer Frische von 25 Tagen pro Track — der Wochenlauf frischt nur auf, was älter ist.
{
"artist": "Taylor Swift",
"track": "All Too Well (10 minute version) (Taylor’s version) (from The Vault)",
"plays": 2,
"album": "Taylor Swift | The Eras Tour Film",
"snapshot_ts": "2026-05-27T14:18:05",
"sources": ["loved"]
}jsonFehlende Musik: Einkaufsliste.json#
Eine nach Alben gruppierte Liste von Musik, die ich oft höre oder geliked habe, aber noch nicht lokal besitze — die Grundlage für Käufe. Pro Album werden die persönliche Gesamt-Spielzahl, die Zahl distinkter Tracks und die Herkunfts-Quellen geführt, plus die einzelnen Tracks. Schwellwerte filtern Ausreißer heraus (ein einzelner Track mit wenigen Plays reicht nicht).
Die Daten verrechnen loved_tracks.json, track_plays.json, die Top-Songs-CSVs und den beets-Bestand (um schon Vorhandenes auszuschließen). Erzeugt wöchentlich (montags), als letzter Schritt der Snapshot-Kette.
{
"artist": "Daft Punk",
"album": "TRON: Legacy",
"distinct_tracks": 22,
"plays_total": 790,
"tracks_ge4": 22,
"sources": ["deine_top-songs_2010", "deine_top-songs_2011", "loved"]
}jsonDie zentrale Künstler-Rangliste: interesting_artists.json#
Der Knotenpunkt unter den Eingabekanälen: eine nach einem Gesamt-Score gerankte Liste aller relevanten Künstler. Der Score bündelt vier Signale — gestaffelte Last.fm-Spielzahlen (1 Monat / 3 Monate / 12 Monate / gesamt), die Zahl lokal vorhandener Alben aus beets, offene Einkaufslisten-Einträge und die decay-gewichteten Konzerte aus concerts.json. sources zeigt, welche dieser Kanäle einen Künstler überhaupt erfasst haben.
Diese Rangliste beantwortet die Frage „welche Künstler sind mir wichtig” einmal zentral, damit die nachgelagerten Funktionen sich darauf stützen können. Erzeugt täglich um 09:00.
{"name": "Nessa Barrett", "mbid": null, "score": 53.5, "local_albums": 4,
"plays_overall": 381, "plays_12month": 191, "plays_3month": 45,
"plays_1month": 35, "acquire_pending": 0, "concerts": 0.6,
"sources": ["lastfm", "beets", "concerts"]}jsonDer API-Cache: ~/.cache/lastfm-mb/#
Hier liegen die zwischengespeicherten Antworten der externen Dienste — Last.fm, MusicBrainz und YouTube Music — als je eine JSON-Datei pro Abfrage, gegliedert nach Abfragetyp. Aktuell rund 150 MB.
Der Cache ist aus zwei Gründen nötig: Tempo und Höflichkeit. MusicBrainz erlaubt nur eine Anfrage pro Sekunde; ohne Cache würde jede Funktion lange dauern und die Dienste unnötig belasten. Die zweite Abfrage derselben Sache ist so sofort beantwortet.
Gefüllt wird der Cache bei Bedarf (beim ersten Zugriff, der einen Treffer verfehlt). Damit Bewegliches aktuell bleibt, werden manche Bereiche — ähnliche Künstler, Top-Tracks, Tags — donnerstags gelöscht und beim nächsten Zugriff neu geladen. Die yt-dlp-URLs sind dabei nicht gecacht; sie wandern direkt in die erzeugten Playlists.
~/.cache/lastfm-mb/
├── recent_tracks/ (Last.fm: Scrobble-Historie)
├── similar_artists/ (Last.fm: ähnliche Künstler)
├── lastfm_tags/ (Last.fm: Genre-Tags)
├── artist_top_tracks/ (Last.fm: Top-Tracks je Künstler)
├── mb_artist_lookup/ (MusicBrainz: Künstler → MBID)
├── mb_release_groups*/ (MusicBrainz: Releases je Künstler)
├── mb_members/ (MusicBrainz: Band-Mitglieder)
├── yt_related/ (YouTube Music: ähnliche Künstler)
└── ytm_artist/, ytm_album/ (YouTube Music: Diskografie)plaintextZusammen ergeben diese acht Kanäle das Fundament: drei manuell gepflegte (Bibliothek, Konzerte, Lieblings-Playlists), vier automatisch erzeugte Snapshots und ein Cache als schneller Puffer vor den externen Diensten.
Abhängigkeiten#
Dependency-Graph der lokalen Musik-Automatisierung. Pfeile = „liefert Daten an”. Erzeugt aus dem tatsächlichen Code (Inputs/Outputs je Skript verifiziert).
flowchart TD
%% ===================== Externe Dienste & Cache =====================
subgraph SVC["Externe Dienste"]
LFM["Last.fm API"]
MB["MusicBrainz API"]
YTM["YouTube Music<br/>(ytmusicapi)"]
YTDLP["yt-dlp<br/>(URL-Auflösung, live)"]
end
CACHE[("~/.cache/lastfm-mb<br/>API-Cache · ~150 MB<br/>(Do 16:00 TTL-Cleanup)")]
LFM --> CACHE
MB --> CACHE
YTM --> CACHE
%% ===================== Manuelle / Bibliotheks-Eingaben =====================
subgraph IN["Eingaben (manuell / Bibliothek)"]
BEETS[("beets_all.tsv<br/>FLAC-Bibliothek")]
CONC["concerts.json<br/>(manuell)"]
PLDATA["playlist_data/*.m3u<br/>(kuratiert, manuell)"]
SPOTCSV["src/*.csv<br/>(Spotify-Export)"]
end
MATCH["match.py<br/>(beets-Matching)"]
BEETS --> MATCH
%% ===================== Profil-/Snapshot-Pipeline =====================
LOVEDSNAP["loved_snapshot.py"]
TRACKSNAP["track_plays_snapshot.py"]
EINKAUF["einkaufsliste.py"]
IA["interesting_artists.py"]
LOVED["loved_tracks.json"]
PLAYS["track_plays.json"]
EINKJSON["Einkaufsliste.json"]
IAJSON(["interesting_artists.json<br/>★ zentrale Künstler-Rangliste"])
LFM --> LOVEDSNAP --> LOVED
LOVED --> TRACKSNAP
SPOTCSV --> TRACKSNAP
LFM --> TRACKSNAP --> PLAYS
LOVED --> EINKAUF
PLAYS --> EINKAUF
SPOTCSV --> EINKAUF
BEETS --> EINKAUF --> EINKJSON
LFM --> IA
BEETS --> IA
CONC --> IA
EINKJSON --> IA --> IAJSON
%% ===================== Playlist-Generatoren =====================
DAYLIST["daylist.py"]
RADIO["radio.py"]
DISC["discovery_yt_playlist.py"]
ONREP["on_repeat.py"]
REDISC["rediscovery.py"]
LFM --> DAYLIST
MATCH --> DAYLIST
YTDLP --> DAYLIST
DAYLIST --> O1["m3u_playback/daylist.m3u"]
IAJSON --> RADIO
LOVED --> RADIO
PLDATA --> RADIO
MATCH --> RADIO
MB --> RADIO
YTM --> RADIO
YTDLP --> RADIO
RADIO -.->|"importiert MB-Release-Lookup"| RR
RADIO --> O2["m3u_playback/Radio_*.m3u"]
IAJSON --> DISC
LFM --> DISC
YTDLP --> DISC
DISC --> O3["m3u_playback/discovery-yt.m3u"]
LFM --> ONREP
MATCH --> ONREP
ONREP --> O4["m3u_playback/On Repeat.m3u<br/>Repeat Rewind.m3u"]
LFM --> REDISC
MATCH --> REDISC
REDISC --> O5["m3u (Re-Discovery)"]
%% ===================== Release Radar / Rückblicke / Migration =====================
RR["release_radar.py"]
RENDER["render_release_radar_html.py"]
SEND["send_release_radar.py"]
WRAP["wrapped_month.py"]
TOPSONGS["generate_topsongs_csv.py"]
IAJSON --> RR
LFM --> RR
MB --> RR
YTM --> RR
EINKJSON --> RR
BEETS --> RR
RR --> RADARMD["release-radar-*.md"] --> RENDER --> RADARHTML["HTML"] --> SEND --> MAIL["E-Mail"]
LFM --> WRAP
MB --> WRAP
BEETS --> WRAP
WRAP --> O6["dist/wrapped/*.png"]
LFM --> TOPSONGS
BEETS --> TOPSONGS
TOPSONGS --> SPOTCSV
SPOTCSV --> MATCH --> O7["m3u/*.m3u (Migration)"]
%% ===================== Stile =====================
classDef svc fill:#fde2e2,stroke:#c0392b,color:#000;
classDef cache fill:#fff3cd,stroke:#b8860b,color:#000;
classDef data fill:#e8f0fe,stroke:#3367d6,color:#000;
classDef script fill:#e6f4ea,stroke:#1e8e3e,color:#000;
classDef hub fill:#d7f5dd,stroke:#0b8043,color:#000,stroke-width:3px;
classDef out fill:#f3e8fd,stroke:#7b2ff7,color:#000;
class LFM,MB,YTM,YTDLP svc;
class CACHE cache;
class BEETS,CONC,PLDATA,SPOTCSV,LOVED,PLAYS,EINKJSON data;
class IAJSON hub;
class LOVEDSNAP,TRACKSNAP,EINKAUF,IA,DAYLIST,RADIO,DISC,ONREP,REDISC,RR,RENDER,SEND,WRAP,TOPSONGS,MATCH script;
class O1,O2,O3,O4,O5,O6,O7,RADARMD,RADARHTML,MAIL out;
Legende#
- 🟥 Externe Dienste — Last.fm, MusicBrainz, YouTube Music (
ytmusicapi),yt-dlp. Ihre API-Antworten landen im Cache (außer den live aufgelösten yt-dlp-URLs). - 🟨 Cache
~/.cache/lastfm-mb/— pro Dienst/Abfrage eine JSON-Datei; donnerstags werden bewegliche Bereiche (Similar, Top-Tracks, Tags) gelöscht. - 🟦 Daten — Eingaben (beets-Bibliothek,
concerts.json,playlist_data/, Spotify-CSVs) und abgeleitete Artefakte (loved_tracks.json,track_plays.json,Einkaufsliste.json). - 🟩 Skripte (Python).
- 🟪 Outputs —
.m3u-Playlists, Release-Radar-Mail, Wrapped-PNGs.
Die vier Hauptketten#
- Snapshot-Kette (montags):
loved_snapshot.py→loved_tracks.json→track_plays_snapshot.py→track_plays.json→einkaufsliste.py→Einkaufsliste.json. - Zentraler Hub (täglich):
interesting_artists.pybündelt Last.fm-Plays + beets +concerts.json+Einkaufsliste.jsonzuinteresting_artists.json(★) — der gerankten Künstlerliste. - Hub speist die Empfehlungen:
interesting_artists.json→release_radar.py,discovery_yt_playlist.py,radio.py. - Generatoren teilen Bausteine: alle Playlist-Skripte nutzen
match.py(beets-Matching) undyt-dlp;radio.pyimportiert zusätzlich ausdaylist.py(Resolver) undrelease_radar.py(MB-Releases für den Recency-Opener).