Monthly Archives: January 2009

Converting iTunes Playlists to M3U Playlists on a Mac

So I wasn’t content with just one way of creating M3U playlists out of iTunes. I went and wrote another. There’s more than one way to do it, as I learned writing Perl programs.

Pity I’m writing in Python.

Anyway, this method only works on a Mac, because it uses Applescript. Actually, it uses appscript, an event bridge that lets you do what Applescript does, only in a real language like Python. It’s called, and requires that you install py-appscript first. To use it, have iTunes open. Select a playlist. Then run the script (or double-click the script if you turn it into an app with py2app) and it’ll create an .m3u file in the same directory as the script.

#!/usr/bin/env python

# Using the appscript Python-to-applescript bridge, convert an iTunes
# playlist into m3u format.
# Author: Stephen Granade
# Date: 2 January 2009

import math, re, sys, os
from appscript import *

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

# For more information about what classes etc. are available from iTunes
# via appscript, see

# Get ahold of iTunes
iTunes = app('iTunes')

# The browser window's view is the currently-selected playlist
except CommandError, detail:
    if detail.errornumber == -1731:
        print "There is no current playlist."
        raise CommandError, detail

# Use the playlist's name as that of the m3u file
outfn = os.path.abspath(__file__)
outfn = os.path.join(os.path.dirname(outfn),".m3u")

outf = open(outfn, "w")


for track in playlist.tracks():
    # Skip any files whose locations aren't currently available
    # If the file doesn't exist on disk, then trying to access the
    # location will throw an AttributeError exception.
        fileloc = track.location().path
        if not os.path.isfile(fileloc):
        # Perform search-and-replace on the file location
        for old, new in sandrStrs.iteritems():
            fileloc = fileloc.replace(old, new)
        durstr = "%d" % math.floor(track.duration())
    except AttributeError:
        print "\"""\" isn't available on disk. Skipping."

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”.

    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.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"

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

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

# 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                                                           

# 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)


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.