Converting iTunes Playlists to M3U Playlists

Because I have a crazy byzantine digital music setup, there are times when I want to take an iTunes playlist and move it to our Xbox running XBMC, because the Xbox has nice speakers whereas none of our iTunes-running laptops do. XBMC, like all sane media players, accepts M3U playlists. iTunes, however, will only export playlists as plain text files or in their plist XML format. Why? Because Apple. So I’m left needing to convert the XML files to M3U format.

Luckily, I know Python. Even more luckily, so do a lot of better programmers than I.

Digging through some mailing lists turned up a plist XML format parser that Frederik Lundh wrote. Sadly, that link I just gave no longer works. Thank goodness for the Wayback Machine! To keep you from having to re-create his code, I stuck it into a Python module called “plistloader”.

try:
    from xml.etree.cElementTree import iterparse
except ImportError:
    from xml.etree import iterparse
import base64, datetime, re, os

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 load(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

Do you see what he did there? Frederik used a dictionary filled with anonymous functions that convert each kind of XML data the plist might contain. Sexy!

My code that uses plistloader is far more prosaic.

import sys, plistloader, os

# Search-and-replace strings to adjust the mp3's locations if necessary.         
sandrStrs = { "file://localhost/Volumes": "smb://sargent" }
m3uHeader = "#EXTM3U\n"

try:
       xmlFile = sys.argv[1]
except IndexError:
       print "No xml file passed on the command line."
       sys.exit()

if not os.path.isfile(xmlFile):
       print "File %s doesn't exist."
       sys.exit()

# Load the playlist using the plistloader module                                 
playlist = plistloader.load(xmlFile)

# Base the output filename on the input one, stripping off any '.xml'            
# or similar from the right and adding in .m3u                                   
outfn = xmlFile.rsplit('.',1)[0]+'.m3u'

outf = open(outfn, 'w')

# Write the M3U header                                                           
outf.write(m3uHeader)

# Iterate through the tracks to get each one's location and name                 
for k, v in playlist['Tracks'].iteritems():
       # The key in this case is the track ID number. The value is               
       # a dict of all information associated with the track                     
       fileloc = v['Location']
       for old, new in sandrStrs.iteritems():
              fileloc = fileloc.replace(old, new)
       outf.write(fileloc+"\n")

outf.close()

To use the script, export your iTunes playlist as an XML file. Pass the XML filename to the script. If you’re on a Mac, take a look at py2app. It’ll let you turn the script into an application, which means you can then drop the XML file on the script and it’ll auto-process it.

Note that the script includes a dict of search-and-replace strings in case you need to fiddle with the mp3 files’ locations as reported by iTunes. As you can see in the example above, I needed to change iTunes’s “localhost” reference to match my SMB share.

13 Comments