Tap to Play!

Back

Welche Musik fehlt mir?#

Bei einer lokalen Musik-Bibliothek muss die Musik irgendwoher kommen. Diese Frage wurde in vergangenen Posts bereits erläutert (QOBUZ, CDs kaufen, Büchereien), Aber welche Musik sollte ich kaufen? Im letzten Jahr habe ich mir von last.fm die Top-Alben der letzten Jahre ausgeben lassen, bin diese durchgegangen und habe CDs gekauft. Mittlerweile wird diese Einkaufsliste generiert.

Was auf die Liste gehört#

Die Liste arbeitet auf Album-Ebene: Sie sammelt erst alle Songs, die ich mag und oft höre, wirft alles raus, was lokal schon vorliegt, und gruppiert den Rest zu Alben. Ein Album landet oben, wenn ich viele verschiedene Songs daraus kenne und diese auch häufig laufen, nicht wenn ein einzelner Hit darauf liegt.

Das Rohmaterial kommt aus vier Quellen, jede mit einer eigenen Schwelle, wie ernst ein Song genommen wird:

QuelleWas sie istFilter
Loved-TracksIn Last.fm von Hand mit Herz markierte Songs (loved_tracks.json)alle (von Hand kuratiert)
deine_top-songs_YYYY.csvJahres-Top-100 je Kalenderjahr, erstellt aus last.fmnur ab 5 Plays
deine_all-time-top-songs.csvmeine meistgehörten Songs überhaupt, erstellt aus last.fmalle
Spotify-Besternt.csvvon Hand in Spotify mit Stern markiert (Altbestand)alle

Die Play-Zahlen stammen nicht aus den CSV-Spalten, sondern aus einem eigenen Snapshot: track_plays_snapshot.py fragt pro Song über track.getInfo meinen aktuellen userplaycount bei Last.fm ab und legt ihn samt zugehörigem Album in track_plays.json ab. Jeder Eintrag hat eine TTL von 25 Tagen, ein Wiederholungslauf zieht also nur veraltete Songs neu und bleibt schnell. Bei der Jahres-Top-100 zählt darum der heutige Play-Stand, nicht der von damals: Ein Song, der 2015 kurz heiß war und nie wieder lief, fällt unter die Schwelle, ein damals wie heute oft gespielter bleibt.

Vom Song zum Album#

Jeder Song wird über match.py gegen die lokale beets-Bibliothek aufgelöst. Was als Datei vorliegt, fällt raus, nur die fehlenden Songs bleiben übrig und werden pro (Künstler, Album)-Paar zusammengefasst. Aus welchem Album ein Song stammt, sagt der Last.fm-Snapshot.

flowchart TD
    LOVED["loved_tracks.json"]
    CSV["Top-Songs-CSVs<br/>+ Spotify-Besternt"]
    PLAYS["track_plays.json<br/>userplaycount + Album"]
    BEETS["beets-Bibliothek<br/>lokale FLACs"]

    SRC["Quellen sammeln"]
    FILT["Quell-Filter<br/>Play-Schwelle je Quelle"]
    MATCH["Lokaler Abgleich<br/>vorhandene Songs entfernen"]
    AGG["Album-Aggregation<br/>distinct · plays · ≥4-Plays"]
    AFILT["Album-Filter"]
    OUT["Einkaufsliste.json"]

    LOVED --> SRC
    CSV --> SRC
    PLAYS --> FILT
    SRC --> FILT
    FILT --> MATCH
    BEETS --> MATCH
    MATCH --> AGG
    AGG --> AFILT
    AFILT --> OUT

    classDef src fill:#e8f0fe,stroke:#3367d6,color:#000;
    classDef phase fill:#e6f4ea,stroke:#1e8e3e,color:#000,stroke-width:2px;
    classDef io fill:#f3e8fd,stroke:#7b2ff7,color:#000;
    class LOVED,CSV,PLAYS,BEETS src;
    class SRC,FILT,MATCH,AGG,AFILT phase;
    class OUT io;

Pro Album hält die Aggregation vier Kennzahlen, aus denen sich Relevanz und Reihenfolge ergeben:

  • distinct_tracks — wie viele verschiedene Songs des Albums in meinen Quellen auftauchen.
  • plays_total — die Summe der Last.fm-Plays über diese Songs.
  • tracks_ge4 — wie viele davon auf mindestens 4 Plays kommen.
  • sources — welche Quellen das Album überhaupt nennen.

Der Album-Filter#

Ein Album mit zwölf bekannten Songs ist ein klarer Kauf, ein Album, von dem ich einen einzigen Song zweimal gehört habe, nicht. Drei Regeln sieben das Schwache aus:

1) plays_total < 8  UND distinct_tracks < 2        → raus
2) distinct_tracks == 1 UND plays_total < 20       → raus
3) distinct_tracks >= 2 UND tracks_ge4/distinct < 0.34 → raus
plaintext

Regel 1 entfernt kaum gehörte Einzeltreffer, Regel 2 verlangt für ein Album, von dem nur ein einziger Song bekannt ist, mindestens 20 Plays auf diesem Song, bevor sich das ganze Album lohnt. Regel 3 ist die wichtigste: Hat ein Album fünf bekannte Songs, aber nur einer davon kommt auf 4 oder mehr Plays, ist das Verhältnis 1/5 = 0,2 und liegt unter 0,34, das Album fliegt raus. Ein einzelner Lieblingssong rechtfertigt kein Komplett-Album, eine breite Streuung mit echten Play-Zahlen schon.

Sortiert wird danach nach distinct_tracks, dann tracks_ge4, dann plays_total, jeweils absteigend. Oben steht also, wovon ich am meisten kenne und am meisten höre. Der aktuelle Stand führt 283 Alben mit 683 Songs; die Spitze sieht so aus:

#KünstlerAlbumDistinct≥4 PlaysΣ PlaysQuellen
1NightwishImaginaerum (Deluxe Version)2113128Spotify-Besternt, deine_top-songs_2019, loved
2OK Go180/3651212524deine_top-songs_2015, loved
3Parkway DriveHorizons1111455deine_top-songs_2008, deine_top-songs_2009, deine_top-songs_2012, loved
4L’Âme ImmortelleWenn Der Letzte Schatten Fällt99120deine_top-songs_2006, deine_top-songs_2007, loved
5Oceans of SlumberOceans of Slumber99102Spotify-Besternt, deine_top-songs_2021, loved
6ParamoreRIOT!9894Spotify-Besternt, deine_top-songs_2009, loved
7All Shall PerishThe Price of Existence88130deine_top-songs_2014, loved
8Paradise LostMedusa8865Spotify-Besternt, deine_top-songs_2017, loved
9My Dying BrideSongs of Darkness, Words of Light77179deine_top-songs_2009, deine_top-songs_2010, loved
10Lene MarlinAnother Day66166deine_top-songs_2014

Pflege und Betrieb#

Drei Skripte halten die Kette aktuell: loved_snapshot.py zieht alle Loved-Tracks, track_plays_snapshot.py die aktuellen Play-Zahlen, einkaufsliste.py baut daraus die Liste neu.

python3 lastfm/loved_snapshot.py
python3 lastfm/track_plays_snapshot.py
python3 lastfm/einkaufsliste.py
bash

Per Cron läuft das wöchentlich montags. Beim Neuaufbau überleben von Hand ergänzte Einträge ("sources": ["manual"]) für brandneue Releases, die noch in keiner Quell-CSV stehen, und fallen erst weg, sobald ihr Album aus den Quellen auftaucht oder lokal vorliegt.

Zwei Sync-Modi verbinden die Liste mit dem Rest:

  • Nach der Aktualisierung der beets-Bibliothek meldet einkaufsliste_sync.py --check-refresh, welche Alben jetzt neu lokal vorhanden sind und entfernt werden können
  • Nach einer Playlist-Migration aus einer CSV-Datei trägt --check-migration neu erkannte Lücken nach.

Beide laufen standardmäßig als Dry-Run und schreiben erst mit --apply.

Das Setup hinter diesen Beiträgen#

Die Idee#

Ich baue Spotify-Funktionen wie Wrapped, Daylist, Radio, Release Radar und Discovery zu Hause nach, auf Basis meiner eigenen, in hoher Qualität gespeicherten Musiksammlung (FLAC-Dateien) und kostenloser Datenquellen. Ohne Abhängigkeit von Spotify, voll automatisiert, und Songs, die ich (noch) nicht besitze, kommen über YouTube dazu.

Jede dieser Funktionen erzeugt am Ende eine .m3u-Datei — eine simple Playlist-Textdatei, die entweder lokale Dateien oder YouTube-Links auflistet. Abgespielt wird sie im Mac-Player IINA.

KI als Kommandozeile#

Die Funktionen sind als Python-Skripte implementiert und können manuell oder automatisiert (cron) gestartet werden. bequemer ist allerdings ein KI-Tool, welches natürlichsprachige Anfragen interpretieren, ausführen und die erzeugte Playlist direkt abspielen lassen kann:

Erstelle und spiele ein Radio für “Against the Current”

Spiele Death Metal

Spiele 90er Metal/Crossover

Spiele die ersten beiden Alben von Gracie Abrams

Spiele das aktuelle Album von Taylor Swift

Die Datenquellen#

Vier Quellen liefern das Rohmaterial:

QuelleWas sie istWas ich daraus hole
Last.fmEin Dienst, der jeden abgespielten Song automatisch mitschreibt („Scrobbeln”)Komplette Hörhistorie, Lieblingssongs („Loved”), wie oft ich was höre, „ähnliche Künstler/Songs”, Genre-Schlagworte, globale Popularität
MusicBrainzEine offene Musik-Enzyklopädie (wie Wikipedia für Musik)Erscheinungsdaten neuer Releases, Band-Mitglieder, Genres
YouTube MusicDer Streaming-Dienst(1) Abspielquelle für Songs, die ich nicht lokal habe — das Werkzeug yt-dlp findet die passende YouTube-URL; (2) ein „ähnliche Künstler”-Graph über die Bibliothek ytmusicapi
Lokale FLAC-BibliothekMeine tatsächlich besessene Musik, verwaltet mit beets (einem Musik-Bibliotheks-Tool)Was „lokal verfügbar” ist, inkl. Künstler/Album/Jahr

Dazu kommt eigene Handarbeit: kuratierte Lieblings-Playlists je Künstler oder Album (playlist_data/), eine Liste besuchter und geplanter Konzerte (concerts.json) und eine Einkaufsliste fehlender Musik.

Die Bausteine (Skripte)#

Kleine Python-Programme, jeweils für eine Aufgabe. Grob nach Zweck:

  • Profile bilden — welche Künstler/Songs sind mir wichtig? interesting_artists.py (Künstler-Rangliste), track_plays_snapshot.py, loved_snapshot.py.
  • Playlists erzeugen (die Spotify-Pendants)daylist.py (Mix nach Tageszeit), radio.py (Künstler-Radio), discovery_yt_playlist.py (neue, unbekannte Künstler), on_repeat.py, rediscovery.py.
  • Entdecken & pflegenrelease_radar.py (neue Veröffentlichungen meiner Künstler, als E-Mail), einkaufsliste.py (was mir noch fehlt).
  • Rückblickewrapped_month.py (monatliche „Wrapped”-Grafiken), generate_topsongs_csv.py (Jahres-Top-100).
  • Migrationmatch.py ordnet einen Spotify-Playlist-Export den lokalen Dateien zu.

Wann was läuft (Automatik per Cron)#

Cron ist ein Zeitplaner des Betriebssystems: er startet Programme automatisch zu festen Zeiten. Mein Zeitplan:

WannWasWozu
täglich 09:00interesting_artists.pyRangliste „wichtige Künstler” aktualisieren (Basis für Radio, Discovery, Radar)
täglich 09:30discovery_yt_playlist.pyPlaylist mit neuen, noch unbekannten Künstlern
täglich 10:00on_repeat.py„On Repeat” & „Repeat Rewind”
täglich 11:00wrapped_month.pylaufende Monatsrückblick-Grafiken
6× täglich (0,5,8,12,17,21 Uhr)daylist.pyPlaylist passend zur Tageszeit
freitags 09:45release_radar.py (+ rendern + senden)E-Mail mit neuen Releases
wöchentlich (montags 09:15)Snapshots + einkaufsliste.pySpielzahlen festhalten, Einkaufsliste aktualisieren
donnerstags 16:00Cache-Aufräumenveraltete Daten löschen, damit sie frisch nachgeladen werden

Das Radio (radio.py) läuft bewusst nicht automatisch, sondern auf Zuruf, wenn ich ein Radio für einen bestimmten Künstler will.

Caches (warum es schnell und höflich bleibt)#

Abfragen an Last.fm, MusicBrainz und YouTube sind langsam und haben Limits — MusicBrainz erlaubt z.B. nur eine Anfrage pro Sekunde. Deshalb wird jede Antwort lokal zwischengespeichert (ein Cache unter ~/.cache/lastfm-mb/, aktuell ~150 MB). Die zweite Abfrage derselben Sache ist dann sofort da, und die Dienste werden nicht unnötig belastet.

Grob drei Gruppen: Last.fm (Hörhistorie, ähnliche Künstler, Tags), MusicBrainz (Künstler-Steckbriefe, Releases, Band-Mitglieder) und YouTube Music (ähnliche Künstler, Alben).

Wichtig: Manche Caches dürfen nicht ewig gelten — „ähnliche Künstler” und „neue Releases” sollen aktuell bleiben. Darum werden ausgewählte Bereiche wöchentlich gelöscht (donnerstags) bzw. beim Release Radar bewusst frisch geladen.