Tap to Play!

Back

Wer seine Musik lokal pflegt — FLAC, sauber getaggt, ohne Streaming-Lock-in — kennt das Problem: Über die Jahre sammeln sich auf Spotify Playlisten an, die man irgendwann lokal abspielen möchte. Idealerweise in der gewohnten Track-Reihenfolge, ohne jeden Song manuell zu suchen.

Der folgende Beitrag beschreibt einen Migrationsprozess, der genau das automatisiert. Eingabe: ein Spotify-Playlist-Export als CSV. Ausgabe: eine .m3u-Playlist mit Pfaden in die lokale Bibliothek plus ein Bericht über alle nicht gefundenen Tracks.

Der gesamte Prozess wurde von Claude Code entworfen. Es sollte möglich sein, diesen Blog-Post der KI des geringsten Misstrauens zu geben, sodass der Prozess verstanden und reproduziert wird.

Die Werkzeuge#

  • Spotify-Export. Spotify selbst bietet keine offizielle CSV-Schnittstelle. Browser-Tools wie Exportify liefern pro Playlist eine CSV mit Track-Name, Künstler, Album, Veröffentlichungsdatum und Hinzufüge-Zeitstempel.

Screenshot einer als CSV exportierten Playlist als Tabelle mit Spalten für Interpret, Album, Titel und Spotify-internen Referenzen

  • Lokale Bibliothek mit beets. beets verwaltet die FLAC-Sammlung mit konsistenten Tags und Pfaden. Für die Migration wird die Bibliothek einmal als Tabelle exportiert (TSV-Datei) (Artist, Albumartist, Titel, Album, Jahr, Pfad) und in den nachfolgenden Vergleichen wiederverwendet. Wird kein beets verwendet, so kann diese TSV-Datei auch einmalig erstellt werden, indem das Dateisystem analysiert und die Metadaten ausgewertet werden.

Screenshot einer Tabellenansicht der TSV-Datei mit Interpret, Album-Name, Veröffentlichungs-Jahr, absolutem Pfad zur Datei

  • Ein Python-Skript liest die CSV, schickt jeden Track durch eine Match-Pipeline gegen den beets-Export und produziert am Ende eine .m3u-Playlist plus einen Markdown-Bericht.

Die Aufgabe: zwei Listen vergleichen#

Im Kern müssen zwei Listen abgeglichen werden — Spotify-Metadaten und lokale Tags. Theoretisch ein einfacher String-Vergleich. In der Praxis weicht beides in so vielen kleinen Details voneinander ab, dass naives Vergleichen fast nichts findet.

Normalisierung#

Bevor zwei Tracks verglichen werden, durchlaufen beide Strings dieselbe Normalisierung:

  • Mojibake-Reparatur. Falsch dekodierte UTF-8-Strings (Künstler statt Künstler) kommen gelegentlich aus Spotify-Exports und werden zurückübersetzt.
  • Sonderzeichen-Transliteration. Zeichen wie Ø, Æ, Œ, Þ, ß werden auf ASCII-Äquivalente abgebildet (O, AE, OE, TH, ss). Spotify schreibt LØLØ oder ØF KINGDØM AND CRØWN, beets transliteriert das beim Import üblicherweise zu LOLO und Of Kingdom and Crown.
  • Diakritika strippen. Umlaute und Akzente werden auf Basisbuchstaben reduziert — Müller zu muller, é zu e. Das deckt Namen aus mehreren europäischen Sprachen ab.
  • Apostroph-Varianten vereinheitlichen. ', , ʼ, `, ´ — Spotify und Tagging-Software verwenden hier inkonsistent unterschiedliche Glyphen. Alle werden ersatzlos entfernt.
  • & durch and ersetzen. Beide Schreibweisen kommen in echten Daten vor.
  • Andere Trennzeichen normalisieren. Punktuation und Whitespace werden vereinheitlicht.

Wichtig: Nicht-lateinische Schriften (Kanji, Hiragana, Katakana, Kyrillisch) bleiben erhalten, statt verworfen zu werden. Andernfalls würden alle japanischen Titel auf einen leeren String kollabieren und gegenseitig matchen — was zu Crash-Treffern führt, in denen ein 美波-Track plötzlich als BABYMETAL-あわだまフィーバー auftaucht.

Annotationen abstreifen#

Spotify und Tagging-Software hängen oft Klammerzusätze an Titel und Albumnamen: (Taylor's Version), (From The Vault), (Deluxe Edition), [Deluxe], (Live), (Acoustic), (Piano Version), (Remix), (feat. Jack Antonoff). Ähnliches gilt für Bindestrich-Suffixe: - Live/2011, - Live From Paris, - the long pond studio sessions, - Excision Remix.

Eine zweite Funktion entfernt diese Annotationen — aber nur, wenn sie eines aus einer Whitelist von Schlüsselwörtern enthalten. Ein generisches Entfernen aller Klammern würde auch sinnvolle Inhalte fressen, etwa bei einem Track namens Three Days (After You). So bleibt für jeden Track sowohl der Original-Titel als auch eine bereinigte Variante verfügbar; im Vergleich wird dann von strikt nach lose probiert.

Vier Match-Stufen#

Pro Track wird in dieser Reihenfolge versucht:

  1. Exakt — voller Titel + Künstler + Album passen normalisiert. Der Idealfall.
  2. Titel-exakt — voller Titel + Künstler passen, Album ist egal. Greift, wenn das Album lokal anders heißt als bei Spotify (etwa weil die Deluxe-Edition gespeichert ist statt der Standard).
  3. Bereinigt + Album-Score — Annotations-bereinigter Titel + Künstler, und das Album wird gegen den Cache gescort: Exakt > Substring > Token-Überlappung > kein Overlap. Der höchstgescorete Kandidat gewinnt. Das ist die wichtigste Stufe für reale Daten: Spotify gibt das Album z. B. als Death by a Thousand Cuts - Live From Paris aus, lokal heißt das Sammelalbum Lover (Live from Paris) — diese Stufe findet die Verbindung.
  4. Locker — reine Substring-Suche auf dem bereinigten Titel. Notfall-Fallback für die Fälle, in denen Album-Informationen zwischen Spotify und lokalen Tags zu stark divergieren.

In der Markdown-Ausgabe steht hinter jedem Match die genutzte Stufe. So sieht man auf einen Blick, welche Treffer eine genauere Prüfung verdienen — ein loose-Treffer ist immer mit Vorsicht zu genießen.

Live und Studio getrennt halten#

Der Hörer will entweder das eine oder das andere, nicht beides als Synonym. Ohne explizite Behandlung würde das Skript Spotifys Spit Out The Bone vom Studioalbum Hardwired problemlos durch denselben Track aus einem Live-Mitschnitt ersetzen — falls das Studioalbum lokal fehlt, der Live-Mitschnitt aber vorhanden ist. Aus Match-Sicht ein Treffer, aus Hörsicht ein Fehler.

Die Lösung ist eine Live-Detektion auf beiden Seiten:

  • Wort-Marker in Titel oder Album: Live, Tour, Concert, Webster Hall, Stripped, Acoustic, Hansa Session, Long Pond, Wembley, Wacken, Unplugged, Olympiastadion, Stadion, Arena, Halle, Theater, In Japan, World Tour, Recorded at.
  • ISO-Datumsmuster wie 2019-07-06. Viele Bands benennen Bootleg- und Live-Mitschnitte mit dem Konzertdatum im Albumnamen.
  • Eine kleine Whitelist für Live-Alben ohne erkennbaren Marker: Paramores The Final RIOT!, Caspers Der Druck steigt, Apple Music Live: Gracie Abrams, Manic World Tour, Showtime, Storytime, End of an Era. Diese Liste umfasst typischerweise unter 20 Einträge selbst bei einer großen Bibliothek, weil die meisten Live-Alben im Titel schon “Live” oder einen Venue-Namen tragen.

Daraus entsteht für jeden Track — Spotify-Query wie lokaler Eintrag — ein einfaches Live/Studio-Flag. Im Match werden nur Treffer akzeptiert, deren Flag übereinstimmt. Eine Studio-Query findet kein Live-Match und umgekehrt; fehlende spezifische Aufnahmen werden ehrlich als “nicht vorhanden” gemeldet, statt durch eine andere Version ersetzt zu werden.

Schutz vor Cross-Artist-Fehlmatches#

Die lockeren Match-Stufen bringen ein Risiko: Sehr kurze oder generische Titel können auf wildfremde Tracks treffen. Spotify-Query Hot Girls in Hell von einer Künstlerin namens LØLØ landet — wenn der Algorithmus nicht aufpasst — bei Hot aus einer Avril-Lavigne-Live-EP. Aus der Datenperspektive ein langer Substring-Match, aus der Hörerperspektive Unsinn.

Die letzte Match-Stufe (lockere Substring-Suche) erfordert deshalb, dass der bereinigte Titel mindestens 4 Zeichen lang ist und das Original mindestens 2 Wörter hat. Einsilbige Tracks wie Hot oder Run werden nie über lockere Suche aufgelöst — entweder es gibt einen exakten oder Album-bestätigten Match, oder der Track gilt als nicht vorhanden. Das verhindert kaskadierende Fehlmatches, ohne legitime Treffer zu verlieren.

Spotify-Einträge haben oft mehrere Künstler im Komma-getrennten Feld (Halsey, BTS, Suga). Lokal ist der Track meistens nur dem Hauptkünstler getaggt. Das Skript nimmt deshalb stets nur den ersten Künstler aus der Spotify-Liste für den Match — eine pragmatische Vereinfachung mit der bekannten Limitierung, dass Tracks unter einem Feature-Artist getaggt fehlschlagen können. In der Praxis selten; die Fälle landen sauber in der “fehlt”-Liste.

Pfade: relativ statt absolut#

Die generierte .m3u enthält relative Pfade:

#EXTM3U
#EXTINF:-1,Let's Eat Grandma - Snakes & Ladders
../Let's Eat Grandma/I'm All Ears/05 Snakes & Ladders.flac
plaintext

Das .. ist relativ zu einem Playlist-Verzeichnis innerhalb des Musik-Roots. So ist die Datei portabel: dieselbe .m3u funktioniert auf dem Mac mit IINA und auf der SD-Karte im Android-Smartphone mit Poweramp — überall, wo der Musik-Ordner identisch heißt. Absolute Pfade wären brüchig, weil jedes Gerät einen anderen Mount-Punkt hat.

Das Ergebnis#

Der Markdown-Bericht enthält drei Dinge:

  1. Eine Tabelle aller Playlist-Einträge mit gefundener lokaler Datei und Match-Stufe. Treffer in der lockersten Stufe verdienen eine kurze Sichtprüfung.
  2. Eine Statistikzeile wie Playlist tracks: 100 | found: 84 | missing: 16.
  3. Eine Liste der fehlenden Tracks, sortiert nach Playlist-Position. Diese Liste ist die Basis für Acquisitions-Entscheidungen: Welche Alben sollten beschafft werden, welche Tracks sind so selten, dass sich ein Einzelkauf lohnt?

Bei realen Daten — über mehrere Jahre gewachsene Top-Songs-Playlisten, Stimmungs-Mixes, Live-Lieblingsalben — erreicht der Prozess Trefferquoten zwischen 60% und 90%, je nachdem wie eng die lokale Bibliothek den Spotify-Geschmack abdeckt. Wichtiger als die absolute Quote ist das, was nicht passiert: Fehl-Treffer sind selten. Was nicht eindeutig gematcht werden kann, landet auf der Wunschliste.

Was bewusst nicht automatisiert ist#

Drei Dinge bleiben Hand-Arbeit:

  • Die Wunschliste pflegen. Wenn neue Alben in die Bibliothek aufgenommen werden, meldet das Skript zwar “Eintrag X ist jetzt erworben” — aber gelöscht wird die Wunschliste nie automatisch. Album-Namen sind in fuzzy Matching zu unsicher, als dass eine destruktive Auto-Operation auf einer kuratierten Liste vertretbar wäre.
  • Die Live-Album-Whitelist erweitern. Vorschläge werden nach jedem Cache-Refresh automatisch generiert, der Eintrag selbst passiert nur nach manueller Bestätigung.
  • Re-Matching aller Playlists. Wenn der Cache aktualisiert wird, kommt eine explizite Rückfrage, bevor existierende .m3u-Dateien überschrieben werden. Auch hier gilt: Auto-Überschreiben ist destruktiv, eine Bestätigungs-Geste kostet eine Sekunde und verhindert Frust.

Beispiel-Ausführung#

Beispielhafter Import einer Playlist mit zwei Dateien, die lokal nicht oder nur in anderen Versionen vorhanden sind. Potentiell falsche Matches dieser Art werden abgefangen und Lösungen interaktiv erfragt.

Screenshot von Claude mit den Auführungsergebnissen

Screenshot von Claude mit den Auführungsergebnissen

Screenshot von Claude mit den Auführungsergebnissen

Ausschnitt aus der Markdown-Berichtsdatei:

Screenshot der Ergebnistabelle mit Playlist-Titel-Informationen, gematchter lokaler Datei und Typ des Matches

Aktualisierung der Bibliothek#

Kommen neue Tracks zur Bibliothek hinzu, so muss der beets-Cache aktualisiert werden, damit die neuen Tracks zukünftig berücksichtigt werden: Die TSV-Datei wird neu geschrieben. Dabei können optional auch alle bereits migrierten Playlisten erneut migriert werden und somit frühere Lücken in den Playlisten geschlossen werden.

Screenshot vom Cache-Refresh mit Report über neue Tracks

Screenshot der Aktualisierung der früher migrierten Playlists inkl. Report, welche Playlists jetzt mehr Treffer enthalten

Fazit#

Spotify-Playlisten in eine lokale Bibliothek zu migrieren ist kein Ein-Zeiler. Die Quelldaten sind unsauber, die Zielbibliothek hat eigene Konventionen, und in der Mitte gibt es semantische Unterschiede, die ein einfacher String-Vergleich nicht auflöst. Mit einer Normalisierungs-Pipeline, einer vierstufigen Match-Strategie, einer expliziten Live-Detection und ein paar pragmatischen Schutz-Heuristiken erreicht man bei realen Daten gute Trefferquoten — mit fast keinen Fehl-Treffern. Der Rest wird ehrlich als “fehlt” gemeldet und landet auf der Wunschliste.