blog.bartlweb - a technologist's external brain

iTunes Musikbibliothek per Skript in M3U-Playlists konvertieren

Ich verwalte meine Musikbibliothek seit Jahren mit iTunes und pflege dort auch unzählige in diversen Ordnern sortierte Playlisten. Mit Kodi (kodi.tv) habe ich eine komfortable Mediacenter-Lösung für meinen Smart-TV gefunden, auf dem ich jetzt auch auf meine Playlists zugreifen möchte. Leider versteht sich Kodi mit iTunes nicht, kann jedoch klassische M3U-Playlists lesen.

Daher habe ich ein Python-Skript erstellt, dass die iTunes Library in M3U-Playlists konvertiert und dabei sogar die in iTunes erstellte Ordnerstruktur nachbildet.

Das Skript ist sehr einfach gehalten und schreibt die generierten Dateien ohne Rücksicht auf Fehlerfälle in den angegebenen Zielpfad. Es löscht weder bestehende Dateien, noch kann es Dateien überschreiben. Wer seine Playlists also regelmäßig automatisiert aktualisieren möchte, muss dazu noch ein kleines Hilfsskript rundherum bauen. Bei mir hat das Skript sowohl unter Windows als auch Mac OS funktioniert, wobei die unten angegebene Konfiguration im Abschnitt config an Mac OS angepasst ist.

Die Konfiguration des Skripts erfolgt über den Abschnitt config direkt im Skript:

#
# config
#
 
  # Pfad in dem die Playlists abgespeichert werden
outputPath = '/Mounts/storage-itunes-kodi'
  # Pfad in dem die XML-Variante der iTunes Library zu finden ist
iTunesLibraryPath = os.path.join('/Mounts/storage-itunes-macserver', 'iTunes Music Library.xml')
  # in der Library verwendeter Basispfad zu den Musikdateien, der gegen einen neuen für den Player erreichbaren Pfad ausgetauscht wird
iTunesBasePathOld = 'file:///Mounts/iTunes-Musik/'
  # neuer Pfad in dem die MP3-Dateien für den Player zugänglich sind
iTunesBasePathNew = 'smb://SERVER/iTunes-Musik/'

Das Skript

# -*- coding: utf-8 -*-

# ##########################
# convert itunes xml library to m3u playlists (incl. itunes folder structure)
# based on Kodi Addon "iTunes Playlist Converter" 1.0.3 by kodiful (https://www.tvaddons.ag/kodi-addons/show/plugin.audio.itunesconverter/)
#
 
 
#
# import modules
#
 
from __future__ import unicode_literals
 
import sys, urllib
import os, re
import exceptions
import base64, datetime
import codecs
import unicodedata
 
from xml.etree.ElementTree import *
 
reload(sys)
sys.setdefaultencoding('utf-8')
 
 
#
# config
#
 
outputPath = '/Mounts/storage-itunes-kodi'
iTunesLibraryPath = os.path.join('/Mounts/storage-itunes-macserver', 'iTunes Music Library.xml')
iTunesBasePathOld = 'file:///Mounts/iTunes-Musik/'
iTunesBasePathNew = 'smb://SERVER/iTunes-Musik/'
 
 
#
# functions
#
 
filePathTree = []
 
 
unmarshallers = {
    # collections
    "array": lambda x: [v.text for v in x],
    "dict": lambda x: dict((x[i].text, x[i+1].text) for i in range(0, len(x), 2)),
    "key": lambda x: x.text or "",
    # simple types
    "string": lambda x: x.text or "",
    "data": lambda x: base64.decodestring(x.text or ""),
    "date": lambda x: datetime.datetime(*map(int, re.findall("\d+", x.text))),
    "true": lambda x: True,
    "false": lambda x: False,
    "real": lambda x: float(x.text),
    "integer": lambda x: int(x.text),
}
 
 
def loadLibrary(file):
    parser = iterparse(file)
    for action, elem in parser:
        unmarshal = unmarshallers.get(elem.tag)
        if unmarshal:
            data = unmarshal(elem)
            elem.clear()
            elem.text = data
        elif elem.tag != "plist":
           raise IOError("unknown plist type: %r" % elem.tag)
    return parser.root[0].text
 
 
def createFilename(plist, path, isFolder=False):
    sid = plist['Playlist Persistent ID'];
    try:
        pid = plist['Parent Persistent ID'];
    except:
        pid = None
    
    name = plist['Name']
    name = name.replace('/', '-')
    name = name.replace('\\', '-')
    name = name.replace(':', '')
    name = name.replace(' .', '')
    name = name.replace('.', '')
    name = name.replace('*', '')
    name = name.replace('?', '')
    name = name.replace('"', '')
    name = name.replace('\'', '-')
    name = name.replace('<', '')
    name = name.replace('>', '')
    name = name.replace('|', '-')
    #name = name.replace('Ö', 'Oe')
    #name = name.replace('Ü', 'Ue')
    #name = name.replace('Ä', 'Ae')
    #name = name.replace('ö', 'oe')
    #name = name.replace('ü', 'ue')
    #name = name.replace('ä', 'ae')
    #name = name.replace('ß', 'sz')
    
    name = unicodedata.normalize('NFC', name)
 
    # skip some top level playlists
    try:
        master = plist['Master'];
        return None
    except:
        pass
    try:
        special = plist['Distinguished Kind'];
        return None
    except:
        pass
 
    item = {"sid":sid, "pid":pid, "name":name}
    if isFolder:
        filePathTree.append(item)
 
    dirs = [name]
    f1 = item
    while not f1['pid'] is None:
        pid = f1['pid']
        for i in range(len(filePathTree)):
            f2 = filePathTree[i]
            if f2['sid'] == pid:
                f1 = f2
                dirs.insert(0, f1['name'])
                break
 
    if not isFolder:
        dirs[-1] += ".m3u"
 
    result = path
    for d in dirs:
        result = os.path.join(result, d)
 
    print result
    return result
 
 
def convertPlaylist(p, playlist, oldmusicpath, musicpath, m3upath):
    # convert filename
    filename = createFilename(p, m3upath, isFolder=False)
    if filename is None: return
    # open the future playlist file
    outf = codecs.open(filename,'w','utf-8')
    # write the m3u header
    outf.write("#EXTM3U\n")
    # dictionnary with all tracks {'Track ID : 4042},{'Track ID : 4046}, etc
    tracks = p['Playlist Items']
    # Iterate through all tracks in the current playlist
    for t in tracks:
        try:
            track_id = t['Track ID']
            music = playlist['Tracks'][str(track_id)]
            # title
            title = unicodedata.normalize('NFC', music['Name'].encode('utf-8','ignore').decode('utf-8'))
            title += " [" + unicodedata.normalize('NFC', music['Artist'].encode('utf-8','ignore').decode('utf-8')) + "]"
            # .encode().decode() makes this line work
            
            
            # total time
            totalTime = music['Total Time']
            # location
            location = music['Location']
            # write file locations except m4p
            if re.search('\.m4p$',location) is None:
                # title & duration
                outf.write("#EXTINF:%d,%s\n" % (int(totalTime/1000),title))
                # iTunes put quote to transform space to %20 and so, we have to convert them
                location = urllib.unquote(location).decode('utf-8')
                location = unicodedata.normalize('NFC', location)
                # Replace old location to the new location
                if oldmusicpath!="": location = location.replace(oldmusicpath, musicpath)
                # write the file location in the playlist file
                outf.write("%s\n" % (location))
        except:
            print 'parse failed in Track ID %s' % t['Track ID']
            pass
    outf.close()
 
def main():
    # load itunes library
    print 'parse itunes library ...'
    playlist = loadLibrary(iTunesLibraryPath)
 
    print 'create playlists ...'
    # iterate through all playlists
    for p in playlist['Playlists']:
        # folder
        if 'Folder' in p:
            # create directories for folders in library
            dirname = createFilename(p, outputPath, isFolder=True)
            os.makedirs(dirname)
        # playlist
        elif 'Playlist Items' in p:
            # create m3u playlists for playlists in library
            convertPlaylist(p, playlist, iTunesBasePathOld, iTunesBasePathNew, outputPath)
 
 
    # notify & exit
    print 'Done.'
 
if __name__  == '__main__': main()

Die Basis des Skripts basiert auf dem Kodi-Addon "iTunes Playlist Converter" 1.0.3 by kodiful (www.tvaddons.ag/kodi-addons/show/plugin.audio.itunesconverter).

Beispiel für ein Hilfsskript zur Automatisierung unter Mac OS

#!/bin/bash
 
# remove all existing playlists
rm -R /Mounts/smb-share-for-kodi/*
 
# convert playlist
python /Users/bartlweb/Applications/itunes-playlist-converter.py
 
# exit script
exit 0;

Dieser Artikel hat Dir deinen Tag gerettet?

... und mühevolles Probieren, Recherchieren und damit Stunden an Zeit gespart? Oder einfach nur Dein Problem gelöst?

Dann würde ich mich freuen, wenn Du meine Zeit für die Erstellung dieses Blogartikels mit einer kleinen Anerkennung honorierst:

Zahlung mit PayPal oder Kreditkarte.

Hinweis zur Verwendung

Die Übermittlung einer Zahlung ist eine persönliche Anerkennung Ihrerseits an den Entwickler (Christian Bartl, Privatperson). Eine Zahlung ist nicht zweckgebunden und es ist keine Gegenleistung zu erwarten. Bitte beachten Sie, dass für eine übermittelte Zahlung keine Quittung ausgestellt werden kann.

Über den Autor

Christian Bartl

Christian Bartl Requirements Engineer
& Solution Architect für Online und Mobile

Als Technologie-Enthusiast und begeisterter Programmierer entwickle ich in meiner Freizeit Websites, Software und IT-Lösungen, die mir selbst und anderen den Alltag vereinfachen.

mehr auf christian.bartl.me

Kommentare

Noch kein Kommentar vorhanden.
Sei der Erste! - Ich freue mich über deine Anmerkung, Kritik oder Frage.

Kommentar schreiben

Der hier angegebene Name wird gemeinsam mit deinem Kommentar auf der Website veröffentlicht.

Deine E-Mail-Adresse wird zur einmaligen Benachrichtigung bei Veröffentlichung des Kommentars genutzt.

Benachrichtigung per E-Mail über Antworten auf meinen Kommentar erhalten.

Bitte tippe die Zahlenkombination "4577" ein, nur dann kann ich deinen Kommentar entgegennehmen.

Bitte fülle dieses Feld nicht aus, nur dann kann ich deinen Kommentar entgegennehmen.