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.
NERD.
What gave it away? The point where I was all tingly over a dict full of lambda functions?
Yes.
I keep expecting that the next time I enter a comment on Granades.com, WordPress will respond, “DO YOU WANT TO PLAY A GAME?”
Have you tried using Firefly Media Server to serve up your iTunes library to XBMC? For serving up playlists it works great. It will parse the iTunes Library XML file and make the music library playable on any device that supports Apples DAAP protocol (i.e. XBMC). It will even parse smart playlists or playlists generated via the Genius feature.
My heart got all a twittery when I saw the lambda expressions too!
Josh:
Oh, huh. I’d played with SlimServer years ago, but this is the first I’ve heard of Firefly. What’s the benefit of using Firefly over just mounting the SMB share? Given that I’m having to use iTunes to put music on my iPhone, won’t I still need the SMB-shared music in my local iTunes library to make that happen?
abovenyquist:
I don’t use lambda functions often, but when I do I feel all studly.
The biggest benefit would be that you wouldn’t need to export all of your playlists from iTunes to get them to play on XBMC. It’s not a prefect solution, but it might be something worth playing around with. It’s currently the solution I’ve settled on.
I’m curious what line 34: outf.write(m3uHeader % pname) is attempting to do? This doesn’t appear to work. If i just write(m3uHeader) it works fine. I don’t think % is a valid operator on strings (unless part of a formatting code), and pname isn’t defined anywhere.
Thanks for the help. Useful program–glad I found it.
Zach: Looking at it, I think it’s an atavistic holdover from when I was including the optional title attributes of the M3U format. I was using % to have the string include the playlist name. As you discovered, that part should be thrown away.
@stephen: Thanks for the clarification.
+3 points for excellent use of the word “atavistic.” I had to look it up!
I’ve just created a repository for my XBMC addon based on your work.
It’s a real addon who make things easier, parse the library to export many playlist in the right directory in XBMC
(gui, compatible with dharma / current svn)
Thank you for your work, it made my work easier.
http://code.google.com/p/itunestom3u
Wonderful! I’m glad it was helpful.
nice code, i’ve done a lot of work in vb.net and c++ using the iTunes COM interface. one thing i think would make it faster would be is if you just get a handle to the playlist you want to export and for each item echo it(the fullpathname) to a text file. that way you dont have to first export to xml plus it cuts out the loading/parsing of the xml file