Friday, July 31, 2009

Scripting OS X

One of the nice things about Mac OS X is that you can interact with many of the applications using external scripts. The main issue is what scripting language you use to write your scripts in. The obvious choice is, of course, AppleScript, but while it makes it easy to interact with the applications it isn't as functional at text or date manipulation as a traditional scripting language, such as Perl.

A few years ago I was pleased to find there was a Perl module available called Mac::Glue that would let you talk to applications in OS X without having to use AppleScript. I had downloaded the AppleScript guide from Apple and had tried to use it, honest. But having used Perl nearly every waking hour over the previous 7 years I had a fair amount of expertise locked up in Perl and in a short time, with the help of Mac::Glue, I was able to knock up a quick script to help me organise my iPhoto library, and later I added scripts that I used with other applications.

Perl, however, is not an officially supported scripting language for OS X, and in OS X 10.5 both Ruby and Python were supported by Apple for scripting OS X. I decided that Ruby was the cooler of the two languages to use as it's a bit like Perl rewritten by a Smalltalk geek (that and I have always had a reservation about Python's use of syntactic whitespace), so in 2008 I dabbled a bit by rewriting my iPhoto script in Ruby using RubyOSA and was pleasantly suprised to find that it ran quite a lot faster.

That was all fine and dandy, and I transitioned from OS X 10.4 on my PowerBook G4 to OS X 10.5 on my MacBook without a glitch. Until Apple released OS X 10.5.7, at which point my RubyOSA scripts stopped working, so I went back to using Perl and Mac::Glue.

Recently I have wanted to export playlists from iTunes to the memory stick from my phone (or sometimes to a USB stick or just to a directory), something that iTunes doesn't seem that keen on (unless optical media is involved). And I have also been looking for a suitable programming language for my 10 year old nephew (who has just got a netbook for his birthday - Hi, Matthew!) to learn. So, bolstered by a comment on Slashdot that had mentioned it only takes a couple of hours to learn, I put away my irrational dislike of Python and decided to give it a go. And sure enough I was able to knock up a script, very straightforwardly, that did exactly what I wanted.

If you're a UNIX command line geek and you'd like to give it a go you can download from the link below. Obviously you'll need the Python appscript library for it to work.
The script currently supports the following actions:
  • list-playlists [<pattern>]
    This lists the playlists in iTunes (that optionally match the specified pattern). The playlists listed include the names of the enclosing folders, and are prefixed by an integer index for easy reference (especially useful if you have multiple playlists with the same name). Also displayed is a track count, the duration of the playlist and the cumulative size of the files in the playlist.
  • export-playlist <playlist> [<dir>]
    This exports media from the specified playlist to specified directory (or the current directory if none is specified). You can specify the playlist either as the playlist name (along with enclosing folders) as, or the playlist index, both of which are printed out by list-playlists. The directory will be created, if it doesn't exist. The filenames for the exported media are generated from the track numbers in the playlist, along with the track name, and have punctuation and spaces removed or translated to make them more friendly.
  • export-current-playlist [<dir>]
    If you are currently listening to something in iTunes this will export the current playlist to the specified directory (or the current directory if none is specified).
  • help
    List the available actions, along with brief descriptions.
So you can use it like this:
% itunes list-playlists never
[282] CDs > Nirvana > Nevermind (12trk 42m31s 61.4MB)
[547] Compilations > 16. Never Give In (2008) (28trk 1h33m15s 127.4MB)
% itunes export-playlist 547 /Volumes/JIM\'S\ W380I/music/compilations/never_give_in
Exporting: "Compilations > 16. Never Give In (2008)" -> /Volumes/JIM'S W380I/music/compilations/never_give_in
Creating directory: /Volumes/JIM'S W380I/music/compilations/never_give_in
[ 1] "Changed Daily" -> 01-changed_daily.mp3
[ 2] "It's A Fine Day" -> 02-its_a_fine_day.mp3
[ 3] "Road To Nowhere" -> 03-road_to_nowhere.mp3
[ 4] "To Get Down" -> 04-to_get_down.mp3
[ 5] "Teenage Dirtbag" -> 05-teenage_dirtbag.mp3
[ 6] "Gay Bar" -> 06-gay_bar.mp3
[ 7] "Voodoo Child" -> 07-voodoo_child.mp3
[ 8] "Games Without Frontiers" -> 08-games_without_frontiers.mp3
[ 9] "Overload (Original Edit)" -> 09-overload.mp3
[10] "Best Of You" -> 10-best_of_you.mp3
[11] "19-2000 (Soulchild Remix)" -> 11-19-2000.mp3
[12] "Come On Home" -> 12-come_on_home.mp3
[13] "National Express" -> 13-national_express.mp3
[14] "Some Girls" -> 14-some_girls.mp3
[15] "Turning Japanese" -> 15-turning_japanese.mp3
[16] "All The Small Things" -> 16-all_the_small_things.mp3
[17] "Flagpole Sitta" -> 17-flagpole_sitta.mp3
[18] "Sk8er Boi" -> 18-sk8er_boi.mp3
[19] "Get Over It" -> 19-get_over_it.mp3
[20] "Thrillseeker" -> 20-thrillseeker.mp3
[21] "Single Girl" -> 21-single_girl.mp3
[22] "Sale Of The Century" -> 22-sale_of_the_century.mp3
[23] "P.V.C." -> 23-pvc.mp3
[24] "Take Me Out" -> 24-take_me_out.mp3
[25] "Scream" -> 25-scream.mp3
[26] "Underwater Love" -> 26-underwater_love.mp3
[27] "Green Bird" -> 27-green_bird.mp3
[28] "Changed Pandimensionally" -> 28-changed_pandimensionally.mp3

Note that I have a symlink to itunes from itunes.py, so I can point the itunes command to whichever implementation of the script is currently in favour.

If you don't like the " > " separator used to indicate playlist folders you can change the SEP variable in the script to whatever you prefer.

One day I may learn to do GUI scripting in OS X and put an interface on to it.

The only problem I did have was with Unicode characters. Although my terminal is set to use UTF-8 and the $LANG variable is set accordingly, Python kept blowing up when it encountered playlists or folders with non-ASCII characters in. So I did a bit of jiggery-pokery in the script that seemed to sort it out for me. When I am more familiar with Python I may come up with a better solution.

Feel free to use this script, and also to pass any comments on to me, although it works for me as I intended so I'm unlikely to put a great deal of effort into maintenance. Bear in mind it is my first Python script (and I happen to like 2 space indents). I'm hoping that it will continue to work when OS X 10.6 (Snow Leopard) is released in September (and even OS X 10.5.8 which is likely to come even sooner).