Tuesday, July 31, 2007

Export iTunes Playlists as .m3u Files



I got really pissed at iTunes today: it crashed and then wouldn't start back up, throwing the infamous 'locked file' error. In exasperation, I tried Songbird. It looks promising, but still needs lots of work and didn't play my music-over-Samba very well. I then got the latest Cog release.

In my experience, Cog is simple, straight-forward, performs no voodoo and just works (Troll: "Stay away from the voodoo!"). I really like the fact that I can browse the file system in Cog, and drag the albums into the list pane, but I didn't want to start years worth of playlists all over again...

At which point I remembered that a couple of years ago I blogged about parsing iTunes playlists with ElemetTree. It looks like the effbot archives no longer point to the link I updated that post with, but google cache served me well and I found Fredrik Lundh's code, repasted here:

try:
from cElementTree import iterparse
except ImportError:
from elementtree.ElementTree import iterparse
import base64, datetime, re

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 data

Which was a great start, but I needed .m3u output. After reading how simple the format was, I was off and running. The end result was all of my iTunes playlists playable by Cog, which I am using now -- as I type -- to enjoy my music, free of pain. One thing worth exploring would be how to preserve the ordering of iTunes' playlist items.

Here's the code I used to "export" the iTunes playlists as .m3u:

import re

m3uList = "#EXTM3U\n%s\n"
m3uEntry = "#EXTINF:%(length)s,"
m3uEntry += "%(artist)s - %(album)s - %(song)s\n%(filename)s\n"

def phraseUnicode2ASCII(message):
"""
Works around the built-in function str(message) which aborts when non-ASCII
unicode characters are given to it.

Modified from http://mail.python.org/pipermail/python-list/2002-June/150077.html
"""
try:
newMsg = message.encode('ascii')
except (UnicodeDecodeError, UnicodeEncodeError):
chars=[]
for uc in message:
try:
char = uc.encode('ascii')
chars.append(char)
except (UnicodeDecodeError, UnicodeEncodeError):
pass
newMsg = ''.join(chars)
return newMsg.strip()

class Playlists(object):

def __init__(self, filename=None, destDir=None):
self.lib = None
if filename:
self.lib = load(filename)
if not destDir:
destDir = './'
self.destDir = destDir

def processTrack(self, trackData):
length = trackData.get('Total Time') or 300000
song = trackData.get('Name') or 'Unknown'
artist = trackData.get('Artist') or 'Unknown'
album = trackData.get('Album') or 'Unknown'
data = {
'filename': trackData['Location'],
'length': int(length) / 1000 + 1,
'song': phraseUnicode2ASCII(song),
'artist': phraseUnicode2ASCII(artist),
'album': phraseUnicode2ASCII(album),
}
return m3uEntry % data

def processTrackIDs(self, ids):
output = ''
for id in ids:
try:
trackData = self.lib['Tracks'][str(id)]
output += self.processTrack(trackData)
except KeyError:
print "Could not find track %i; skipping ..." % id
return output

def cleanName(self, unclean):
clean = re.sub('[^\w]', '_', unclean)
clean = re.sub('_{1,}', '_', clean)
return clean

def exportPlaylists(self):
for playlist in self.lib['Playlists']:
playlistName = self.cleanName(playlist['Name'])
try:
items = playlist['Playlist Items']
except KeyError:
print "Playlist seems to be empty; skipping ..."
continue
trackIDs = [x['Track ID'] for x in items]
data = m3uList % self.processTrackIDs(trackIDs)
fh = open("%s/%s.m3u" % (self.destDir, playlistName), 'w+')
fh.write(data)
fh.close()

def exportPlaylists(filename, dest=None):
pls = Playlists(filename, dest)
pls.exportPlaylists()

With usage like the following:

>>> from iTunesExport import exportPlaylists
>>> BASE = "/Volumes/itunes/__Playlists__"
>>> exportPlaylists('%s/Library.xml' % BASE, BASE)


Update: I've tweaked the code in this post a little bit, due to a reader's questions. To run this, copy both code blocks into a single file you should be good to go.


5 comments:

GregP said...

That's real nice and all, but I think it might be worth noting that it's completely illogical to "get pissed at iTunes". Software has no malicious intent; it simply does what it's told. We programmers have a little motto we've kicked around for years that describes one of the simplest yet most powerful principles of good software design: garbage in = garbage out. Clearly, the user must have done something to screw it up.

In all likelihood, maybe it's *you* that owes *iTunes* an apology, mmm?

Duncan McGreggor said...

Oh, in such uffish though you sit, GregP. Thou non-programming hunter of the Jabberwock. Though I dare say the Bandersnatch got you long before your manxome foe could.

In this instance, the software known as "iTunes" is a proxy for Bullshit, Own-You-in-the-Ass Corporate Policy. Apple, Fuck you Very Much.

One of these days I'm going to bail on my PowerBooks and MacBook and go for the Gold: Ubuntu running on one of these puppies:

Voodoo 19" Laptop.

Lanouvelle Josephine said...

I've just switched to mac (NOT SWITCHED, but ADDED an imac to my fold) and the comment from Gregp is the exact thing that turns me off about macs. There is a really snide, stepford wife, company line, lack of empathy about apple's software lacks, that could be overcome if users rebelled. But so many people are so self-centered and don't care about any problem they don't PERSONALLY have, and so many people are so mind-washed that it's looked down on to point out OBVIOUS problems and lacks in the software that's created with a fairly malicious policy of making sure that end users can't do anything on their own, unless you approve of it, whether or not it loses Apple money or not. The fact that itunes gives you no real way to keep your itunes library metadata on changes between platforms or new computers isn't an oversight, it's an intentional gap left that will eventually bite apple in the ass as people attempt to just go around apple. I've already started running Songbird and plan to download COG. I refuse to support such a nazi-like closed software. m4p is bullshit - long live amazon.com and walmart for mp3 downloads without DRM.

lee said...

So how exactly does one run this code on a mac? Do you save this as a shell sript to run at the CLI?

Duncan McGreggor said...

Hey Lee,

I've updated the post with a note at the bottom. As is stands, if you follow those instructions, you'll have a python module ready to use. These instructions imply that you'll save the python code in a file called iTunesExport.py. If you're in that directory where you saved the python, all you need to do is follow the usage example at the end of the post (in an interactive python prompt).

If you want to make this a script that would run like a shell script, then you could do the usual thing at the end of the python file:


if __name__ = "__main__":
  import sys
  libraryFile, dest = sys.argv[1:]
  exportPlaylists(libraryFile, dest)