Restoring itunes ratings from an itunes library .xml file

Tags: python, apple

Rating songs

During the years, I went through my entire music collection in iTunes and rated everything. 1-5 stars.

  • 1: to be deleted.
  • 2: not something I want to listen to normally, but I do want to keep it. Bad sound quality stuff, for instance, but I want to listen to it once per year.
  • 3: normal.
  • 4: quite good. Normally I listen to a 4+5 star random playlist.
  • 5: Bloody good. These songs are epic. Or they contain parts that make me close my eyes and enjoy what’s coming. Or they bring back memories. Some perfect piece of Bach. Or the opening tune of Battlestar Galactica (old series). Or that 20+ minute Neal Morse progressive rock epic. Or the heart-rending laat dit het laatste zijn from the Dutch musical oorlogswinter.

Problem

My problem? I had a non-apple laptop for a year. Ubuntu. So no iTunes. I kept the iTunes Music Library.xml file with all the file and rating information and retained the existing folder structure on ubuntu with ubuntu’s player.

I retained the .xml file as I guessed I could use it to get my ratings back.

Problem: nope, whatever I tried, I couldn’t get my ratings back. I wrote a small script to read my old .xml file and my current one. I mapped tracks in both of them based on filenames and set ratings based on the old ones. I copied the resulting .xml file over the real iTunes Music Library.xml and restarted iTunes. No effect.

The reason: iTunes doesn’t use that file itself, it is only there to provide information to other programs that might need to grab the music library info! It uses some unreadable .itl database.

Solution: playlists

I got it working in the end by using a hint I found. The trick is to make playlists per rating. One for 1 star, one for 2 star items, etc.

So I modified my script to not change the ratings, but to add 5 playlists, one for each rating. And to assign the tracks for which I wanted to set the rating to the correct playlists.

In itunes, there’s a “File - Library - Import library...” menu option. I gave it my newly generated .xml file and got my 5 playlists!

I then went to each of the playlists, selected everything, right clicked and gave them all the correct rating. Success!

Implementation: Python

Turns out that Python can read those apple .xml “plist” files out of the box with plistlib. Real handy. A plist file is more or less an XML version of a JSON file.

I had my original .xml file and a copy of iTunes current one in the same directory as the script. The output, updated.xml was what I imported into iTunes.

Anyway, here is the script. I also added it as a github gist in case that’s handier. There’s a lot to refactor, but obviously I didn’t bother once I got it working: it is a one-time-only script. I added a couple of comments to make the usage clearer:

from collections import defaultdict
import plistlib

# I copied files locally.
# NEW is what gets written.
# CURRENT is the current "iTunes Library.xml" file (or rather, my copy).
# OLD is an old backup with lots of good ratings.
OLD = 'itunes.xml'
CURRENT = 'itunes_2012.xml'
NEW = 'updated.xml'

# I match based on filenames. IDs are different, sigh.
# But I needed to compensate for a new iTunes folder structure.
OLD_PREFIX = 'file://localhost/Users/reinout/Music/iTunes/iTunes%20Music/'
CURRENT_PREFIX = 'file://localhost/Users/reinout/Music/iTunes/iTunes%20Media/Music/'


def main():
    # Read in the old tracks and grab the ratings.
    old = plistlib.readPlist(OLD)
    old_tracks = old['Tracks']
    print "Found {} old tracks".format(len(old_tracks))
    old_ratings = {}
    for track in old_tracks.values():
        filename = track['Location'].replace(OLD_PREFIX, '')
        rating = track.get('Rating')
        if rating is None:
            continue
        old_ratings[filename] = rating
    print "Found {} old ratings".format(len(old_ratings))

    # Same with the current ratings. If we've rated something, we want to
    # keep that rating.
    current = plistlib.readPlist(CURRENT)
    current_tracks = current['Tracks']
    print "Found {} current tracks".format(len(current_tracks))
    current_ratings = {}
    for track in current_tracks.values():
        filename = track['Location'].replace(CURRENT_PREFIX, '')
        rating = track.get('Rating')
        if rating is None:
            continue
        current_ratings[filename] = rating
    print "Found {} current ratings".format(len(current_ratings))

    # Figure out which old ratings we want to move over.
    new_ratings = {}
    for filename in old_ratings:
        if filename not in current_ratings:
            new_ratings[filename]= old_ratings[filename]
    print "This means {} new ratings".format(len(new_ratings))

    # Create a dict {rating: tracks} for the new ratings I want to set.
    new_playlists = defaultdict(list)
    for track in current_tracks.values():
        filename = track['Location'].replace(CURRENT_PREFIX, '')
        new_rating = new_ratings.get(filename)
        if new_rating is None:
            continue
        track_id = track['Track ID']
        new_playlists[new_rating].append(track_id)

    # Create a playlist per rating. You can set the playlist's items to
    # the correct rating in iTunes just fine.
    playlists = []
    for rating, track_ids in new_playlists.items():
        print rating, len(track_ids)
        new_playlist = {}
        new_playlist['Name'] = str(rating) + ' points'
        new_playlist['Visible'] = True
        new_playlist['Playlist ID'] = 8000 + rating
        new_playlist['All Items'] = True
        new_playlist['Playlist Items'] = [{'Track ID': track_id}
                                          for track_id in track_ids]
        playlists.append(new_playlist)
    # Zap the existing ones, otherwise you end up with double items.
    current['Playlists'] = playlists
    plistlib.writePlist(current, NEW)


if __name__ == '__main__':
    main()
blog comments powered by Disqus
 
vanrees.org logo

About me

My name is Reinout van Rees and I work a lot with Python (programming language) and Django (website framework). I live in The Netherlands and I'm happily married to Annie van Rees-Kooiman.

Weblog feeds

Most of my website content is in my weblog. You can keep up to date by subscribing to the automatic feeds (for instance with Google reader):