Hack 28 Controlling iTunes with Perl


The Mac::iTunes module means that controlling iTunes from across the room or across the world is only a Perl script away.

I created the Mac::iTunes Perl module to control iTunes from my scripts and from other machines. Everything that I present in this hack comes with either the Mac::iTunes or Apache::iTunes distribution (http://search.cpan.org/author/BDFOY/), available on the Comprehensive Perl Archive Network (CPAN).

Once I have a back end, I can create almost any interface to iTunes that I like ? and I do.

28.1 iTunes Is AppleScriptable

Apple's MP3 player, iTunes (http://www.apple.com/itunes/), has been AppleScript-aware since Version 2 (the latest version is 3.0.1). This gives me a lot of freedom to control how I use iTunes.

I can use Script Editor to create a script, but I can also use the osascript command-line tool from a Terminal window. I can use the -e switch to run a short script on the command line:

% osascript -e 'tell application "iTunes" activate'

Or, I can store the script in a file and pass it to the osascript on the command line:

-- iTunes script "quit_itunes"
-- run as "osascript quit_itunes"
tell application "iTunes"
end tell
% osascript quit_itunes

Once I liberate myself from Script Editor, I have more flexibility.

Scripts for iTunes can automate a lot of my common tasks. Apple has a collection of scripts (http://www.apple.com/AppleScript/itunes/), and Doug's AppleScripts for iTunes & SoundJam (http://www.malcolmadams.com/itunes/scrxcont.shtml) has several more good ones.

28.2 iTunes and Perl

Although I like AppleScript for very simple things, I think it gets tedious for complicated scripts. The language is verbose and does not have a good extension mechanism. Perl, on the other hand, does, but at the moment it does not have good access to Aqua applications, even though it can control the usual Unix applications in Mac OS X, just as it can on other Unix platforms.

When I started to work with iTunes AppleScripts, I wanted it to be as easy to do as writing Perl scripts, even though it was not. After a while, I decided to fix that by writing a Perl module to handle the AppleScript portions of iTunes. I already had a MacOSX::iTunes Perl module that I used to parse the binary format of the iTunes Music Library file. I needed to add AppleScript support to it.

On the suggestion of Chris Nandor, the caretaker of MacPerl and author of Mac::Carbon, I changed the name of my distribution to Mac::iTunes and added the Mac::iTunes::AppleScript module, which wrapped common AppleScripts in Perl functions. The meat of the module was the _osascript routine, which creates an AppleScript string and calls osascript just as I did earlier:

sub _osascript
  my $script = shift;

  require IPC::Open2;

  my( $read, $write );
  my $pid = IPC::Open2::open2( $read, $write, 'osascript' );

  print $write qq(tell application "iTunes"\n), $script, qq(\nend tell\n);
  close $write;

  my $data = do { local $/; <$read> };

  return $data;

The Mac::iTunes::AppleScript works much like the osascript command-line tool. Indeed, the first version simply created a script string (called osascript), and captured the output, if any, for parsing. About the same time I finished the first version, Nathan Torkington needed Perl access to AppleScript and convinced Dan Sugalski to write Mac::AppleScript. With that module, Perl could work with AppleScript without calling an external program. I replaced the _osascript routine with tell( ), which uses the RunAppleScript function from Mac::AppleScript:

sub tell
  my $self = shift;
  my $command = shift;

  my $script = qq(tell application "iTunes"\n$command\nend tell);

  my $result = RunAppleScript( $script );

  if( $@ )
    carp $@;

  return 1 if( defined $result and $result eq '' );

  $result =~ s/^"|"$//g;

  return $result;

Once I have tell( ), I simply feed it an AppleScript string, which it runs and then returns the result. For example, iTunes can play Internet streams. The AppleScript way to say this uses open location:

tell application "iTunes"
  open location "http://www.example.com/streaming.mp3"
end tell

In Mac::iTunes::AppleScript, I wrapped this little script in a method, named open_url( ), which takes a URL as an argument and uses tell( ) to run it:

sub open_url
  my $self = shift;
  my $url = shift;

  $self->tell( qq|open location "$url"| );

Most of the AppleScript commands for iTunes have a corresponding method in Mac::iTunes::AppleScript. Now I can use the full power of Perl, even though I am really using AppleScript behind the scenes.

28.3 iTunes, Perl, and Terminal

Just as I ran AppleScripts from the Terminal window with osascript, I can now run Perl programs that interact with iTunes. I want to play streaming media with very few keystrokes and without going to the iTunes Open Streaming . . . menu item; it's just too much work when I do not want to switch applications. I created a simple program, named stream, using Mac::iTunes. I create an iTunes controller object, then call the open_url( ) method with the first command-line argument. Perl tells iTunes to play the MP3 stream, and even though iTunes starts to do something, it stays in the background while I continue whatever I am doing. I can even use this program from shell scripts.

use Mac::iTunes;
my $controller = Mac::iTunes->controller;
$controller->open_url( $ARGV[0] );
% stream http://www.example.com/streaming.mp3

Small scripts do not have much of an advantage over the equivalent AppleScripts, but as things get more complex, Perl starts to shine.

28.4 iTunes, Perl, and Apache

I have been using Apple's AirPort for a while. We swear by it in my household, and my guests like to bring their laptops and wireless cards when they visit. The AirPort has raised our computer expectations; we want to be able to do any task from anywhere in the house. However, when it comes to playing music, we have a problem. Which computer is hooked up to the stereo? I do not like listening to music on the built-in speakers of my laptop, so I have another Mac hooked up to my stereo and a very large external hard drive filled with MP3s.

With all of that, I cannot carry that computer around my apartment. Even if I could, I want it to just play music and perhaps perform other silent tasks. I should not have to interrupt my music because I decide to change something on the Mac I am working on. I want the music to keep playing even if I restart the iTunes on my laptop, which I do frequently while developing Mac::iTunes.

I need to control this central MP3 player remotely. I could create a command-line tool to control iTunes and then log in the machine with ssh, but not everyone who wants to control iTunes likes using the Terminal. I need a more pleasing interface. Since Mac OS X comes with the Apache web server (which runs by default), I can write a CGI script to control iTunes:

use strict;

use CGI qw(:standard);
use Mac::iTunes;
use Text::Template;

my $Template = '/Users/brian/Dev/MacOSX/iTunes/html/iTunes.html';

=head1 NAME

iTunes.cgi - control iTunes from the web



This is only a proof-of-concept script.

=head1 AUTHOR

brian d foy, E<bdfoy@cpan.org>


Copyright 2002 brian d foy, All rights reserved


my $controller = Mac::iTunes->new(  )->controller;

my $command = param('command');
my $playlist = param('playlist') || 'Library';
my $set_playlist = param('set_playlist');

if( $command )
  my %Commands = map { $_, 1 } qw( play stop pause back_track);
  $controller->$command if exists $Commands{$command};
elsif( $set_playlist )
  $controller->_set_playlist( $set_playlist );
  $playlist = $set_playlist;

my %var;

$var{base} = '';
$var{state} = $controller->player_state;
$var{current} = $controller->current_track_name;
$var{playlist} = $playlist;
$var{playlists} = $controller->get_playlists;
$var{tracks} = $controller->get_track_names_in_playlist( $playlist );

my $html = Text::Template::fill_in_file( $Template, HASH => \%var );

print header(  ), $html, "\n";

On the first run without input, the script creates an iTunes controller object, sets the starting playlist to Library (the iTunes virtual playlist that has everything iTunes knows about), then asks iTunes for a lot of state information, including the names of tracks in the playlists, the names of the playlists, and what iTunes is currently doing (e.g., playing or stopped). The script uses Text::Template to turn all of this into HTML, which it sends back to a web browser. The template file I use is in the html directory of the Mac::iTunes distribution, and those with any sort of design skills will surely want to change it to something more pleasing. The code is separated from the presentation. Figure 3-49 shows the iTunes Web Interface.

Figure 3-49. iTunes Web Interface

I have a small problem with this approach. To tell an application to do something through AppleScript, the telling program has to be running as a logged-in user. The web server is set up to run as the unprivileged pseudouser nobody, so this CGI script will not work from the stock Apache configuration. This is not much of a problem, since I can make Apache run under my user. On my machine, I run a second Apache server with the same configuration file, except for a couple of changes.

First, I have to make the web server run as my user, so I change the User directive. Along with that, I have to choose another port, since only the root user can use port numbers below 1024, and Apache expects to use port 80. I choose port 8080 instead. I will have to pass this nonstandard port along in any URLs, but my CGI script already does that. As long as I use the web interface without typing into the web browser's location box, I will not have to worry about that.

User brian
Port 8080

I also have to change any file paths that Apache expects to write to. Since Apache runs as my user, it can create files only where I can create files.

PidFile "/Users/brian/httpd-brian.pid"

Once everything is set up, I access the CGI script from any computer in my home network, Mac or not, and I can control my central iTunes.

28.5 iTunes, Perl, Apache, and mod_perl

CGI scripts are slow. Every time I run a CGI script, the web server has to launch the script and the script has to load all of the modules that it needs to do its work. I have another problem with Mac::iTunes, though. The first call to Mac::AppleScript's RunAppleScript( ) seems to be slower than subsequent calls. I pay a first-use penalty for that. To get around that, I want to keep my iTunes controller running so I do not have to pay this overhead over and over again.

I created Apache::iTunes to do just that. I could run my CGI script under Apache::Registry, but I like the native Apache interface better. I configured my web server to hand off any requests of a URL starting with /iTunes to my module. I use PerlSetEnv directives to configure the literal data I had in the CGI version.

<Location /iTunes>
SetHandler perl-script
PerlHandler Apache::iTunes
PerlModule Mac::iTunes
PerlInitHandler Apache::StatINC
PerlSetEnv APACHE_ITUNES_HTML /web/templates/iTunes.html
PerlSetEnv APACHE_ITUNES_URL http://www.example.com:8080/iTunes

The output, shown in Figure 3-50, looks a little different from the CGI version because I used a different template that included more features. I can change the look-and-feel without touching the code.

Figure 3-50. Apache::iTunes interface

I tend to like the mod_perl interface more. Instead of passing variables around in the query string, the URL itself is the command and is simple, short, and without funny-looking characters:


28.6 iTunes, Perl, and Tk

As I was working on Apache::iTunes, I was also working on a different project that needed the Tk (http://www.lns.cornell.edu/~pvhp/ptk/ptkFAQ.html) widget toolkit. I was programming things on FreeBSD, but I like to work on my Mac. That's easy enough, since I have XonX (that's the combination of XDarwin (http://www.xdarwin.org) and OrobosX (http://oroborosx.sourceforge.net/ )) installed. Under Mac OS X 10.2 these work without a problem, although if you use 10.1 you have to perform a little bit of surgery on your system, following Steve Lidie's instructions (http://www.lehigh.edu/~sol0/Macintosh/X/ptk/). Since I had been away from the Tk world for awhile, I was referring to O'Reilly's Mastering Perl/Tk quite a bit. As I was flipping through the pages on my way to the next thing I needed to read, I noticed a screenshot of iTunes. It was not really iTunes though ? Steve Lidie had taken the iTunes look-and-feel as a front end for his MP3 player example.

I already had all of the back-end stuff to control iTunes and none of it was tied to a particular interface. Even my CGI script could output something other than HTML, like plain text or even a huge image. I could easily add a Tk interface to the same thing ? or so I thought.

Controlling iTunes is easy. Controlling it from a web page is easy. Controlling it from Tk, which has a persistent connection to whatever it hooks up to, was harder. Since I had the persistent connection, I could reflect changes in iTunes instantaneously. In the web versions, if somebody else changed the state, like changing the song or muting the volume, the web page would not show that until I reloaded. The Tk interface (shown in Figure 3-51) could show it almost instantaneously. In reality, I could get the Tk interface to poll iTunes for its state only every three and a half seconds or so before it took a big drop in performance, but that is good enough for me.

Figure 3-51. Tk iTunes interface

The tk-itunes.pl script comes with Mac::iTunes. Someday I might develop a skins mechanism for it; all I, or somebody else, need to do is make the colors configurable. The script already uses a configuration file, although I can configure only a few things at the moment.

28.7 Final Thoughts

Perl can interact with Aqua applications through AppleScript. With Mac::iTunes as a back end, I can create multiple interfaces to iTunes that I can use on the same computer or on other computers on the same network. Everyone in my house, or within range of my AirPort, can control my iTunes.

?brian d foy