Tuesday, May 08, 2007

Remote Control Fun with iTunes and Ruby

Lately I've been using the Ruby programming language, at work and at home, and I often find myself in an interactive Ruby command line. A well-written interactive interpreter is an extremely useful thing in the Ruby language, especially when you include facilities like persistent command history and method name completion. When I found out about RubyOSA, which allows you to script the Apple user interface in Ruby, my first thought was "Oooh, I could even use that interactively!" Not that I would control iTunes on my own machine using Ruby, that would be silly. I mean, I've got Quicksilver for that. However I'm keenly interested in controlling iTunes on the iBook we use as a client to our music server.

It didn't take long to install RubyOSA on my MacBook and find out how to set up remote scripting on the iBook. I soon set to manipulating my music player remotely in irb:
$ irb -r rubygems -r rbosa
> itunes = OSA.app('iTunes', :machine=>'casper.local')
At this point I get an authentication window, and entered the name and password of my user on the iBook. This works, but it would be nice to authenticate automatically. I've tried passing :username and :password to OSA.app, but haven't had much luck with it. If you get stuck with repeated authentication errors, try opening up Keychain Access and removing the assocated entries before you try again. Also, even though I'm not the user controlling this running instance of iTunes, all of these commands work anyway. I guess it's because my user is an administrator.
> itunes.sources
=> #<OSA::ObjectSpecifierList:0x1104888 desired_class=OSA::ITunes::Source>
> itunes.sources.collect{|x|x.name}
=> ["Library", "Radio"]
Just inspecting the sources isn't going to do it, I need to collect the names to get a meaningful list. Now I can see the sources available in this remote instance of iTunes. Unfortunately, the music server isn't available until I go to the iBook and access the music server manually. Once that's done:
> itunes.sources.collect{|x|x.name}
=> ["Library", "Radio", "Music Server"]
Now the music server is listed! Let's play with it!
> music_server = itunes.sources[2].playlists.first
> music_server.search('bill cosby').collect{|x|x.name}
=> ["200 MPH Car", "Dogs", "My Brother Russell", "My Father", "Snakes and Alligators", "Kindergarten", "Personal Hygiene", "Shop", "Baby", "Driving In San Francisco", "-75 Car", "The Toothache", "Hofstra", "Tonsils", "The Playground", "Lumps", "Go Carts", "Chicken Heart", "Shop", "Special Class", "Niagara Falls"]
> music_server.search('bill cosby').first.play
Aha! We can get the playlist from the music server, and search it to find tracks. After that we can just get the first track and call the method play to switch to that track in iTunes. After that track is over, the next song in the global playlist plays, not the next one in the search results. This is a bit disappointing, I wonder if we can do better. Now is probably a good time to examine the documentation:
> quit
$ rdoc-osa --name iTunes
$ open doc/index.html
Looking through the API documentation gives me a little better idea what I can do with this facility. One approach to a temporary playlist might be:
$ irb -r rubygems -r rbosa
> t = OSA.app('iTunes', :machine=>'casper.local')
> music_server = t.sources[2].playlists.first
> music_server.search('bill cosby').sort {|a,b| a.duration<=>b.duration}.each {|x| x.play;sleep x.duration}
Technically, this will play the search results sequentially sorted by duration, although it seems pretty heavy-handed. Also, it doesn't return right away. There are doubtless better ways to do this, but I'm not much of a Ruby (or AppleScript) programmer yet.

4 comments:

lrz said...

Nice article :-)

Note that instead of writing

itunes.sources.collect{|x|x.name}

You can write

itunes.sources.every(:name)

Which is both a convenience and a faster shortcut.

Zack said...

Hey that's handy! How did you discover this? I was surprised to learn that this method isn't in the Array or Enumerable API for Ruby, but in OSA::ObjectSpecifierList.

I did discover that Rails has a different expression of the same idiom:

> array.collect &:name

Unfortunately I don't see such a shortcut anywhere in Ruby itself. Not that it would be difficult to add...

Thanks for the tip!

has said...

If you're needing a remote iTunes controller, there's several options already available (it's a not uncommon request). Just Google for "itunes remote". netTunes looks particularly nice, although I've not used it myself. Or, if you'd prefer a command line one, feel free to check out my own contribution.

HTH

David Mullet said...

Nice article, Zack!

On a related note... for those of us stuck on Windows, you can automate and manage iTunes via the COM interface, as explained here.

David

http://rubyonwindows.blogspot.com