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;