Refactoring my IPod

OK I'm getting waaaayyy off topic now:

I just bought an IPod and plan on burning my classical music collection
to it. I've heard there are some issues categorizing and labeling
classic music on ITunes. Are there any good Ruby libraries for dealing
with Itunes catalogs? I'm using a Mac so Ruby AppleScript wrappers
would work too.

Steve

···

-----Original Message-----
From: Ryan Leavengood [mailto:leavengood@gmail.com]
Sent: Thursday, April 20, 2006 11:13 AM
To: ruby-talk ML
Subject: Re: [SUMMARY] Refactoring (#75)

On 4/20/06, Molitor, Stephen L <Stephen.L.Molitor@erac.com> wrote:

Would you (or
someone else) care to give examples of 'Replace Similar Methods with
Metaprogrammed Definitions' and a 'Convert a Family of Methods to a
Single method_missing()'?

I'll provide an example:

Let's say you have a class for managing a list of music. Each item in
the list is a class which contains a lot of meta-data about the music
files (artist, title, album, track number, duration, genre, release
date, etc.) Part of managing this list of music is sorting it by various
criteria, for display in a media application for example.

There are a lot of different ways to sort based on the various kinds of
meta-data, and it would be a pain to have to code a special method to
sort on each type of meta-data:

list.sort_by_artist, list.sort_by_title, etc.

Of course in this case it might not be too difficult to just do a custom
sort when needed:

list.sort_by {|song| song.artist}

But why not make things really handy by adding a method missing?

# Full test code
class Song < Struct.new('Song', :title, :artist, :album)
  def to_s
    "#{title} by #{artist} (#{album})"
  end
end

class MusicList
  attr_accessor :list

  def initialize
    @list =
  end

  def method_missing(name, *args)
    if name.to_s =~ /sort_by_(.*)/
      puts "Sorting by #$1:"
      @list.sort_by {|song| song.send($1.to_sym)}
    end
  end
end

if $0 == __FILE__
  music = MusicList.new
  music.list << Song.new('Here I Am', 'Al Green', 'Greatest Hits')
  music.list << Song.new('Ain\'t No Sunsine', 'Al Green', 'Greatest
Hits')
  music.list << Song.new('Until It Sleeps', 'Metallica', 'Load')
  music.list << Song.new('Bleeding Me', 'Metallica', 'Load')
  music.list << Song.new('King Nothing', 'Metallica', 'Load')
  music.list << Song.new('Shiver', 'Maroon 5', 'Songs About Jane')
  music.list << Song.new('This Love', 'Maroon 5', 'Songs About Jane')
  puts music.sort_by_title, '----'
  puts music.sort_by_artist, '----'
  puts music.sort_by_album, '----'
end
# End test code

Now things like list.sort_by_artist and list.sort_by_title will call
method missing, and any new meta-data can be sorted by without changing
the list code.

Ryan

If one were satisfied with a one-key sort, this would work. I, alas, wasn't willing to have all the tracks randomly listed within each artist or whatever.

I created a ruby program that would allow me to do quite a bit of processing on my iTunes library. First, I had to (re)create the information from iTunes about/for each Track.

class Track
  attr_reader :dbid
  attr_accessor :name, :album, :artist, :tracknumber, :trackcount
  attr_accessor :location, :genre, :year, :rating, :enabled, :playeddate
  attr_accessor :playedcount, :start, :finish, :duration, :EQ, :composer, :dateadded

  def initialize(id)
    @dbid = id
    self
  end
end

iTunes has a unique id number for a track which is reuses as it reappears in various playlists and whatnot.

The rest of the information was added to each track with the method 'bulkload', usually, although it could be done one attribute at a time....

  def bulkLoad(params)
    @location = params[1]
    @location.downcase! if @location
    @name = params[2]
    @album = params[3]
    @artist = params[4]
    @tracknumber = params[5]
    @trackcount = params[6]
    @genre = params[7]
    @year = params[8]
    @rating = params[9]
    @enabled = params[10]
    @playeddate = params[11]
    @playedcount = params[12]
    @start = params[13]
    @finish = params[14]
    @duration = params[15]
    @EQ = params[16]
    @composer = params[17]
    @dateadded = params[18]
    self
  end

That, naturally, depends on feeding in the parameters in the correct order after getting them from iTunes. I used AppleScript to get the data from iTunes.

    applescript = %{tell application "iTunes"
    with timeout of 1200 seconds
    set trackData to {database id, location, name, album, artist, track number, track count, genre, year, rating, enabled, played date, played count, start, finish, duration, EQ, composer, date added, disc number} of } + @trackSource + '
    end
    repeat with I from 1 to count of item 2 of trackData
      try
        set item I of item 2 of trackData to (POSIX path of item I of item 2 of trackData)
      end
    end repeat
    get trackData'
    itunesdata = OSX.do_osascript applescript

What's returned isn't a Ruby Object at first, so AppleScript lists must be turned into Ruby Arrays, as well as their contents...

    tracklist = itunesdata.to_rbobj.map!{|list| list.to_rbobj.map{|i| i.to_rbobj}}

Then the array of Tracks is built...

    @tracks = Array.new
    tracklist.transpose.each do |track|
      newTrack = Track.new(track[0]) # first item is the database id
      newTrack.bulkLoad(track) # dump all the rest of the stuff into the new track
      @tracks.push(newTrack)
    end

All of the above is to illustrate the methodology I used for sorting iTunes tracks.

class Track
  def <=>(other)
    answer = self.artist <=> other.artist
    answer = self.album <=> other.album if answer == 0
    answer = self.name <=> other.name if answer == 0
    answer = self.dbid <=> other.dbid if answer == 0
    return answer
  end
end

(Yes, it could be made shorter. It's plenty fast as it is, and nice and clear.)

Now, all I have to do is something like @tracks.sort! and they'll be sorted by artist, then by album, then by name, and if there are multiple tracks on an album with the same name, they're arbitrarily sorted by their iTunes ID. I'm not personally all that interested in the order they appeared on my CD, so I didn't use that as a sort key.

*IF* I have the track's metadata correct then I find iTunes itself is perfectly capable of handling classical music. Unfortunately, 95% of what CDDB has is garbage, alas, with the composer listed as the artist, or the movement in the title and the title as the performer, or all sorts of corrupted or incomplete information. So my iTunes ruby scripts (and applescripts) were created mostly for mass library maintenance, to get the metadata in better shape.

If your metadata includes proper use of "Composer" and "Grouping" (which I see my current script doesn't, oops), then I don't know what "issues" there might be remaining with how iTunes handles classical music.

···

On Apr 20, 2006, at 14:45, Molitor, Stephen L wrote:

OK I'm getting waaaayyy off topic now:

I just bought an IPod and plan on burning my classical music collection
to it. I've heard there are some issues categorizing and labeling
classic music on ITunes. Are there any good Ruby libraries for dealing
with Itunes catalogs? I'm using a Mac so Ruby AppleScript wrappers
would work too.

There are a lot of different ways to sort based on the various kinds of
meta-data, and it would be a pain to have to code a special method to
sort on each type of meta-data:

list.sort_by_artist, list.sort_by_title, etc.