Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

A present for Palm

Jerry Stratton, January 20, 2010

I was browsing the web, minding my own business, when a twelve-point Macalope strode past. Being from flyover country, my first thought was “dinner on the table”. But since this is the holiday season and it’s possible he’s a distant relation to Rudolph, I offered him a beer and we started talking.

Well, not so much talking. When a creature has a computer screen for a head, their side of the conversation is all blog. It’s not like the Macalope’s got high-res graphics in that MacPlus.

Apparently Computerworld is taking Apple to task because Palm’s software doesn’t work. Some idiocy is so bad it requires fictional creatures to respond; the Macalope, in a springtime editorial, does the job.

  1. Palm is hardly a “little guy”.
  2. It’s hardly Apple’s job to do Palm’s work for them.

But the mythical man/Mac/beast forgot point 3: Apple has already done Palm’s work for them. The Macalope writes:

Just move the files. All you’re really complaining about is losing the playlists. Even in the realm of first-world problems, that rates pretty low.

In fact, even that isn’t a problem no matter what world you’re in (or on). Apple has made all of this data easily available in a standard, easy-to-read (for computers) format. Every iTunes user’s Music folder contains an “iTunes Music Library.xml” file. As you might guess from the extension, this file is simple XML. It continuously updates with not just your music, but also all of your playlists: both the static ones and the dynamic ones.

Why do they do this? So that ungrateful third-parties like Palm can use it to make life easier for iTunes-using music lovers.

So if you want to blame someone for Palm not syncing your iTunes music, blame Palm; Apple’s already done the work, clocked out, and gone home to the little tablet and six tiny iPods.

But why talk about blame? It’s a new year and a new era of bipartisan peace, love, and understanding. In the spirit of the post-holiday season, here’s a present for Palm.

[toggle code]

  • #!/usr/bin/python
  • import os, sys, optparse
  • import lxml.etree
  • home = os.getenv('HOME')
  • iTunesFile = os.path.join(home, 'Music/iTunes/iTunes Music Library.xml')
  • def parsePlaylist(playlist):
    • name = None
    • identifier = None
    • itemList = None
    • for element in playlist.iterchildren('key'):
      • if element.text == 'Name':
        • name = element.getnext().text
      • elif element.text == 'Playlist Persistent ID':
        • identifier = element.getnext().text
      • elif element.text == 'Playlist Items':
        • itemList = element.getnext()
    • return name, identifier, itemList
  • def parseTrack(trackInfo, tracks):
    • track = None
    • trackId = None
    • path = None
    • for element in trackInfo.iterchildren('key'):
      • if element.text == 'Track ID':
        • trackId = element.getnext().text
    • for possibleTrack in tracks.iterchildren('key'):
      • if possibleTrack.text == trackId:
        • trackElement = possibleTrack.getnext()
        • track = {}
        • for trackdata in trackElement.iterchildren('key'):
          • track[trackdata.text] = trackdata.getnext().text
    • return track, trackId
  • def humanReadableFilesize(filesize):
    • for bytesLevel in ['B', 'KB', 'MB', 'GB', 'TB']:
      • if filesize < 1024.0:
        • return "%3.1f%s" % (filesize, bytesLevel)
      • filesize /= 1024.0
    • return "%3.1f%s" % (filesize, 'PB')
  • def quit(message):
    • print message
    • sys.exit()
  • parser = optparse.OptionParser("itunes [-options] <playlist>")
  • parser.add_option('-s', '--space', help='Tally up disk usage for tracks', default=False, action='store_true', dest='tallySpace')
  • parser.add_option('-t', '--tracks', help='Show all tracks', default=False, action='store_true', dest='showTracks')
  • parser.add_option('-m', '--main', help='Include main library', default=False, action='store_true', dest='showMain')
  • (options, listsToShow) = parser.parse_args()
  • if not os.path.exists(iTunesFile):
    • quit("You do not have an iTunes XML file at " + iTunesFile)
  • root = lxml.etree.parse(iTunesFile)
  • iTunes = root.find('dict')
  • for element in iTunes.iterchildren('key'):
    • if element.text == 'Music Folder':
      • iTunesFolder = element.getnext().text
    • if element.text == 'Playlists':
      • playlists = element.getnext()
    • if element.text == 'Tracks':
      • tracks = element.getnext()
  • for playlist in playlists:
    • playlistName, playlistId, playlistItems = parsePlaylist(playlist)
    • if playlistName == 'Library':
      • continue
    • if playlistName == 'Music' and not options.showMain and playlistName not in listsToShow:
      • continue
    • if playlistItems is None:
      • continue
    • if not listsToShow or playlistName in listsToShow:
      • itemCount = len(playlistItems)
      • spaceUsed = 0
      • print playlistName, "("+str(itemCount), 'item'+('s' if itemCount!=1 else '') +')'
      • if options.showTracks or options.tallySpace:
        • for track in playlistItems:
          • track, trackId = parseTrack(track, tracks)
          • if options.showTracks:
            • info = [track['Name']]
            • if 'Album' in track:
              • info.append(track['Album'])
            • if 'Artist' in track:
              • info.append(track['Artist'])
            • print "\t", "\t".join(info)
          • if options.tallySpace and 'Size' in track:
            • spaceUsed = spaceUsed + int(track['Size'])
        • if options.tallySpace:
          • print "\t", 'Space used:', humanReadableFilesize(spaceUsed)

It requires lxml, because lxml is a lot faster than Python’s built-in XML module, and music lovers have big XML files, IYKWIMAITYD.

It’s very simple; run the script and it will display a list of all playlists; type some playlist names as arguments, and it will only display those playlists. Add the option --space to tally up how much space the playlist will use, and --tracks to list all of the tracks in the playlist.

You’re welcome! Happy New Year to you, too.

May 15, 2011: Copying an iTunes playlist

When you give, you receive. For my Pioneer 3200BT review I needed to copy 32 gigabytes of music to an SD card, to see how fast the unit would load that many tracks. Making a giant playlist in iTunes was easy. But iTuneMyWalkman, an otherwise very useful application, was taking forever just to collect the list of songs from iTunes—it had taken over two hours and hadn’t even started a copy yet. And iTunes itself doesn’t let you drag that many items to the Finder.

But the iTunes script I wrote as a joke for Palm does almost everything I needed: it loops through all tracks in a named playlist. All I had to do was add a function to copy each track to a specified folder.

At the top, along with the other imports, add:

  • import urllib, shutil

The urllib library is necessary because iTunes stores track locations in its XML file as file:// encoded URLs. The shutil library will copy a file.

In the OptionParser section, add a new option:

  • parser.add_option('-c', '--copy', help='Copy tracks')

Because --copy means it has to loop through all tracks, just like --space and --tracks, change the “if options.showTracks or options.tallySpace:” line to:

  • if options.showTracks or options.tallySpace or options.copy:

Underneath the other two track-based options, add an “if” for copying:

[toggle code]

  • if options.tallySpace and 'Size' in track:
    • spaceUsed = spaceUsed + int(track['Size'])
  • if options.copy:
    • copyTrack(track)

And, finally, among the other functions, add a copyTrack function:

  1. <- Inkscape extension scripts
  2. All roads lead up ->