September 2003 archive

Scripting AppleScriptable Applications with Python

Python 2.3 comes with a number of utilities which make it possible to use the same interface for accessing software as AppleScript uses. It isn’t much more difficult than AppleScript, but using it effectively is very hard due to the lack of documentation. I present to you a mini-tutorial on how to script iTunes with Python through the Open Scripting Architecture (OSA), using gensuitemodule.py.

First of all, you must run the program gensuitemodule.py on your target application (in this case, iTunes) to generate a set of class wrappers for that program. gensuitemodule.py is installed in your the plat-mac library directory of your Python installation, and can be used as a module or as a stand-alone application. In order to generate an iTunes package containing the wrapper classes, we run the program like this:

pythonw {...}/lib/python2.3/plat-mac/gensuitemodule.py \
    --output iTunes --resource --creator hook \
    /Applications/iTunes.app/Contents/Resources/iTunes.rsrc

That should automatically create an iTunes directory wherever you run the command which contains the files Internet_suite.py, Standard_Suite.py, __init__.py, and iTunes_Suite.py. If you’re familiar with AppleScript, the three suite files should sound familiar to how classes and functions are seperated in an application’s script dictionary.

From then on, scripting iTunes is fairly easy. However, accessing properties and iterating through containers takes a bit of extra work. I’ll demonstrate a few different functions. Please note that these scripts must be run with the pythonw executable because they need access to the window manager. Running them with python will cause an error.

In order to do most anything, you should create an instance of the iTunes class inside the iTunes module. This instance is responsible for sending messages to iTunes, and receiving the responses. Essentially, all communication with the application will go through this instance. Here are a few simple operations:


import iTunes
app = iTunes.iTunes()
app.start()          # fires up iTunes
app.play()           # whack the play button
app.stop()           # stop playing

# Retrieve the current track property.
trk = app.get(app.current_track)
# repr(trk) == "file_track(5721,...)"

# Retrieve the track's 'artist' property.
print app.get(trk.artist)

Iterating over a collection requires a bit more work. Here’s an example of iterating through every song in iTunes’ library:


import iTunes
app = iTunes.iTunes()
library = iTunes.library_playlist(1)
# (Note, this iteration sucks.  See 'fixed_indexing' later.)
for i in range(1, app.count(library, each = iTunes.track) + 1):
    trk = playlist.track(i)
    print app.get(trk.artist)

Notice that indicies start at 1, not 0. When we create a track object to represent the track from the playlist, we could create an iTunes.track, iTunes.file_track, iTunes.url_track, or iTunes.shared_track. Each of the more specialized track objects has the same properties as iTunes.track, but also has more specialized properties. They’re inherited classes. The easiest way to see the relationship between different classes, as well as the properties and methods they have, is to open up the Script Editor application (/Applications/AppleScript/Script Editor.app), choose the ‘Open Dictionary…’ menu option, and select the iTunes application.

I noticed that the iteration shown above would sometimes return the same tracks for different indicies. A bit of research showed that iTunes had a ‘fixed indexing’ property which is usually set to false. I suggest that before iterating through a playlist, this property be set to true:


old_fixed_indexing = app.get(app.fixed_indexing)
app.set(app.fixed_indexing, to = 1)
try:
    code()
finally:
    app.set(app.fixed_indexing, to = old_fixed_indexing)

I did all this research while trying to write an application to set the track numbers of every ogg file in my iTunes library properly. The application is short, but contains everything I know about scripting iTunes with Python. It requires that pyogg and pyvorbis be installed. One of the more difficult parts was taking the iTunes.file_track‘s location property, which is an instance of Carbon.File.Alias, and getting the path that it points to:


#!/usr/bin/env pythonw2.3

import iTunes
import ogg.vorbis

app = iTunes.iTunes()
playlist = iTunes.library_playlist(1)

# Ensure that the track order will not reset during this script.
old_fixed_indexing = app.get(app.fixed_indexing)
app.set(app.fixed_indexing, to = 1)

try:
    for i in range(1, app.count(playlist, each=iTunes.track) + 1):
        trk = playlist.file_track(i)
        location = app.get(trk.location)
        fsref, wasChanged = location.FSResolveAlias(None)
        path = fsref.FSRefMakePath()
        if path.endswith(".ogg"):
            vc = ogg.vorbis.VorbisFile(path).comment()
            for key, value in vc.items():
                if key.upper() == 'TRACKNUMBER':
                    app.set(trk.track_number, to=int(value))
                    break
finally:
    app.set(app.fixed_indexing, to=old_fixed_indexing)

And so ends my first experience with using Python and OSA. It was not as easy as it should have been, but hey… it was easier than writing an Ogg Vorbis comment decoder in AppleScript.