Automating iTunes on Windows with IronRuby

I recently bought a new Windows computer and, since there is no AppleScript on Windows, I have to do without the Doug’s scripts for ITunes that I used on Mac OS X to automate the management of my music library. But Apple has made available a COM API, that can be used by any language to script iTunes, as long as it supports COM. This includes Ruby MRI, with the Win32OLE package in the standard library. But I was curious about IronRuby, the .NET implementation of Ruby, which recently reached version 1.0 and therefore, I decided to reimplement with it some of the scripts I used the most.

The scripts shown in this post can be downloaded here.

Installing IronRuby

The PATH environment variable is updated and now contains the “ir.exe” executable.

Connecting to the iTunes COM object

Since this functionality is going to be used by all the scripts, I extracted it in a separate file called “iTunesLib.rb” :

module ITunes
      itunes_type = System::Type.get_type_from_prog_i_d("iTunes.Application")
      itunes = System::Activator.create_instance(itunes_type)
      yield itunes
    rescue Exception => e
      puts e

This modules hides all the COM plumbing from the scripts. It uses the standard .NET libraries to interface with COM: For example, by using the System.Activator class. One nice thing IronRuby does is to allow the use of rubyfied names for methods, using undescores. It is still possible to use the original .NET names though.

Prefix to track number

The first script is very simple: All it does is check if the “Song” fields of the tracks currently selected in iTunes begin with a number, and if they do, copy this number to the “Track Number” field: “07 – Hypersurface” will have track number 7.

require "iTunesLib" do |itunes|
  sels = itunes.selected_tracks

  if sels.nil?
    puts "No selection"

  1.upto(sels.count) do |i|
    track = sels.item(i)
    if =~ /^\s*(\d+)/
      track.track_number = $1.to_i

After selecting some tracks with the iTunes interface, the script can be run on the command line like this (no arguments are necessary):

ir.exe prefix_to_track_number.rb

(Note: This really modifies the tracks. No undo is possible…)

Remove n characters from front or back

This script removes an arbitrary number of characters, from the front and/or back of either the “Artist”, “Album” or “Song” field. At the beginning, there are various checks of the arguments, followed by the main code, which loops through the tracks currently selected in iTunes. Update: Added checks for the field inside the loop

require "iTunesLib"

if ARGV.length <= 1
  puts "Usage: #{__FILE__} [a|l|n] [<f or b> <num chars to remove>]+"
  puts "a => artist, l => album, n => song name"

scope = ARGV.shift
if scope !~ /a|l|n/
  puts "Bad scope"

rff = nil
rfb = nil

ARGV.each_with_index do |a,i|
  if a == "f"
    rff = ARGV[i + 1].to_i if i < ARGV.length - 1
  elsif a == "b"
    rfb = ARGV[i + 1].to_i if i < ARGV.length - 1

if rff.nil? and rfb.nil?
  puts "Nothing to do"
end do |itunes|
  sels = itunes.selected_tracks

  if sels.nil?
    puts "No selection"

  1.upto(sels.count) do |i|
    track = sels.item(i)

    if scope == "n"
      name =
    elsif scope == "l"
      name = track.album
      name = track.artist

    if !rff.nil?
      #name is an immutable .NET string
      name = name.slice(rff..-1)

    if !rfb.nil?
      name = name.slice(0..-(rfb+1))

    if scope == "n" = name
    elsif scope == "l"
      track.album = name
      track.artist = name

One of the only gotchas I encountered with IronRuby had to do with the fact that .NET strings are immutable. But in Ruby, strings are mutable and there are methods ending with “!” (such as “slice!”) that can be used to modify the current String instance instead of returning a new one. Those methods cannot be used on .NET/CLR strings (that are returned by the methods of .NET objects). This is why I used “name = name.slice” instead of something like “name.slice!” in the code above. I later learned that the “to_s” method could have been used on a .NET string to obtain a Ruby string.

After selecting some tracks with the iTunes interface, the script can be run on the command line like this:

ir.exe remove_n_characters_from_front_or_back.rb n f 5

(“n” means the “Song” field; “f” means the front of the field; “5” the number of characters to remove)

Using the previous song name example: “07 – Hypersurface” will become “Hypersurface”.


You have read the complete article! Here is a great track as a reward:

Dopplereffekt – Z-Boson (from “Linear Accelerator“)


Ruby Tutorial: Scraping Muxtape for mp3 + iTunes playlist creation

Muxtape is a website that lets users share mixtapes online, which is pretty cool, as is their minimalistic design. This Ruby tutorial will show how to use Mechanize (together with Hpricot) and rb-appscript to scrape the site, download the mp3’s and add them automatically to an iTunes playlist. The last part will only work on Mac OS X (because of rb-appscript).

First download the gems :

gem install mechanize rb-appscript

For clarity, I will do it in 4 steps : crawling (ie download the HTML pages), analysis (ie get the URL’s of the mp3’s), download (ie download the mp3’s) and iTunes integration (ie playlist creation and file addition).


def crawl(n = 10)
    agent =
    page = agent.get("")
    page.save_as "crawl/main.html"
    new_mixtapes = {}
    (page/"ul.featured li a").each_with_index do |mixtape,i|
      link = mixtape.attributes['href']
      name = mixtape.inner_text
      file_name = "crawl/" + name + "_mixtape.html"
      break if  i + 1 > n
      next if File.exist?(file_name) #don't crawl/analyze... if already crawled
      new_mixtapes[name] = file_name

What I did here was instantiate a Mechanize object to download the first page. When I have that page, I use the Mechanize integration with Hpricot to search for all links that match the CSS expression : “ul.featured li a”, which I got by looking at the HTML content of the Muxtape page. These links point to individual mixtape pages (which in turn will contain the links to the mp3’s). Then I download the pages pointed to by the links and return a mapping between the mixtape name and the location of the downloaded HTML file.


Once I have the individual mixtape pages, I can try to find where the mp3’s are. One caveat here is that the HTML pages do not have it in plain. The “embed” elements which do are created in JavaScript, after the page has loaded, which is no good for Mechanize. To overcome this, it could be possible to use something like Watir, which uses an actual browser instead of only emulating one, but I won’t, since the JavaScript to parse is not all that hard. To do the parsing, I briefly considered RKelly, which looks very interesting, but there is no gem yet, so I passed. I just use Hpricot to parse the page and give me the list of script elements, the content of which I will parse by myself.

def analyze(mixtapes)
    mixtape_songs = {}
    mixtapes.each do |name,file_name|
      page = Hpricot(open(file_name))
      songs = []
      (page/"script").each do |script|
        src = script.inner_text
        if src =~ /new\s+Kettle\(\[([^\]]+)\],\[([^\]]+)\]/
          ids, codes = [$1, $2].map {|a| a.gsub("'",'').split(",") }
 do |ic|
            songs << =>"#{ic[0]}.mp3",
                :url => "{ic[0]}?#{ic[1]}")
      mixtape_songs[name] = songs

Here again, I looked at a mixtape page to learn how the URL for the mp3’s are formed. It turns out they are stored on Amazon S3. The JavaScript that creates the “embed” element for the Flash mp3 player is a one-liner that looks like this :

var x480bc8459f377 = new Kettle(['62f2c1f39e0'.......],[...])

After using a regexp to parse this, I now have all the info needed to determine the URL’s of the mp3’s for the mixtape. The list is returned for each mixtape considered at the crawling step.


def download(mixtapes)
    mixtape_songs = {}
    mixtapes.each do |mixtape, songs|
      dl_songs = []
      songs.each do |song|
          song_file =  "dl/#{song.sid}"
          open(song.url) do |f|
              open(song_file,"wb") {|mp3| mp3.write }
          dl_songs << song_file
     mixtape_songs[mixtape] = dl_songs

This step is pretty straightforward: Just go through all the URL’s determined at the previous step and download them. This uses the “openuri” standard library to open the remote URL. A mapping between the mixtape and the locations of the downloaded mp3’s is returned.

iTunes integration

While the previous code could be used on other platforms, this step is restricted to Mac OS X. It uses the rb-appscript library, which is a bridge between AppleScript and Ruby. There are other libraries that do this (Apple’s own Scripting Bridge, or Ruby OSA), but this one is well documented so I use it. This step will create an iTunes playlist for each considered mixtape, add the mp3’s to the iTunes library and to the corresponding playlist.

def itunes(mixtapes)
    i_tunes = app('iTunes')
    mixtapes.each do |mixtape,song_files|
      next if i_tunes.playlists[].exists #skip if exists
      pl = i_tunes.make(:new => :user_playlist, :with_properties => {:name => mixtape})
      song_files.each do |sf|
          i_tunes.add(MacTypes::FileURL.path(File.expand_path(File.dirname(__FILE__) + "/#{sf}")),  :to => pl)

The first line gets a reference to the iTunes application. If it is not launched, it will be as soon as it is asked for info or an action is called on it. Then for each mixtape/playlist, I first test if one of the same name already exists, using a filter expression (the “its” referring to each considered playlist in turn). If there isn’t, I create one, with the “make” method. I then add all the mixtape songs to the newly created playlist. For this, I use “add” on the iTunes reference, to which I pass a URL created with the “MacTypes::FileURL.path” method, which takes as argument an absolute path to the mp3 I want to add (a relative one does not seem to work). And that’s it! New playlists have been created in iTunes, with an iPod synchronization for offline listening just a step away.


Here is the file with the complete code. It will only download the first muxtape. Run as :

ruby crawler.rb