Sunday, March 18, 2012

iTunes backup playlist

Since they removed the DRM from music downloads at the iTunes store, I've been buying songs from Apple using that approach. It's great for impulse buying. Also, it's nice in combination with an app for the iPhone called Shazam, which can identify a track that is playing wherever you are at the moment. I thought I was fairly modest in my purchases, but now find that I have over 500 songs after 18 months. So the question then is how to archive these purchases so they don't go **kabluie** in the night.

One strategy is to just let Apple do it, but I don't trust them.

The next idea is to manually copy all 500 songs to a backup disk. This would be easy, except that (i) I don't want the whole library, just a playlist, and (ii) iTunes uses nested folders to preserve the artist:album:songtitle information. I need to merge u/v/w with u/v/x to create a directory u/v containing both w and x. You can't do that just copying x.

The playlist info is contained in xml format, file: 'iTunes Music Library.xml' but I decided to export the playlist to disk from within iTunes ('File > Library > Export Playlist...'). Each entry in the exported text file ends with something like this as the last field (tab-separated, one entry per line):

HD:Users:Shared:iTunes Music:Bob Marley:Natty Dread:01 Lively Up Yourself.m4a

The following script finds each song on the playlist and copies it to a directory temp in the directory where the script is run. Simple. The result is 3.77 GB of .m4a files with the desired directory structure.

A detail that is (or should be) embarassing: iTunes (on OS X) uses '\r' (CR) as newline. Talk about the constraints of backward compatibility.

One last thing: please, please, please back-up and test before using this. YMMV. Caveat lector. No warranty express or implied. Don't blame me if your library vanishes.

import os, subprocess

name = 'playlist'
FH = open(name + '.txt', 'r')
data = FH.read().strip()
FH.close()

# iTunes uses '\r' (CR) as newline!
data = data.split('\r')

# data[0] is metadata (column names)
data.pop(0)

# file path is the last value
L = [item.split('\t')[-1] for item in data]

# ':' is path separator
L = [item.replace(':','/') for item in L]

for item in L:
    # remove HD name from file path
    item = item.split('/', 1)[1]
    
    # file path has spaces
    # must be quoted for shell command below
    src = '"/' + item + '"'

    artist, album, songfile = item.split('/')[-3:]
    # construct directory tree if it doesn't exist
    path = '/'.join(('temp', artist, album))
    try:
        os.stat(path)
    except OSError:
        os.makedirs(path)
    
    dst = '"' + '/'.join((path, songfile)) + '"'    
    cmd = ' '.join(('cp', src, dst))
    obj = subprocess.call(cmd,shell=True)
    if obj != 0:
        print 'e',
    else:
        print '*',
    print dst