Monday, July 18, 2011

Scripting Madness

I've recently taken the plunge and decided to dive into the geeky world of AppleScript. I had a couple of specific ideas for useful programs to automate some tedious tasks that I regularly perform in iTunes, so it seemed like as good a time as ever to have a go. The following is a brief summary of the two scripts I have so far produced and some of the things I learned while writing them.
Generalities
I won't purport to turn this into a general purpose AppleScript lesson (which I'm far from qualified to provide at this point), but in case any other noobs out there want to follow in my footsteps, here is a basic orientation.
AppleScript is a scripting language that comes bundled as standard with the Mac OSX operating system. It allows the creation of scripts that can tell OSX applications to do various things. Although not a very powerful language, it has a fairly intuitive object-oriented model and natural language syntax which makes it quite easy to use. Only some applications are "scriptable"; the current list includes the Finder, iTunes, iCal, Mail and some others.
The Script Editor application (in the Applications > AppleScript folder) provides a fairly decent IDE that more than serves the needs of the basic scripter. The syntax highlighting and automatic formatting makes editing and debugging quite pleasant.
One of the fundamental constructs is the "tell" block, which is used to direct commands at particular applications. So in my programs, all the action occurs with an overarching tell block of the form:
tell application "iTunes"
-- code here
end tell
(by the way, "--" is used to denote comments.)
Note that "tell" blocks are not scope-limiting, so variables defined inside them will be available after exiting. They can also be nested (as in the nested "iTunes" and "Finder" blocks in my second script below).
Deploying scripts
There are several ways to save and use scripts. The simplest is to save it as a "script" (extension .scpt). Note that this is not a plain text file so, for example, to use the scripts I list below, you'd need to paste them into a new script in Script Editor and save. Alternatively, you can save it as an "application" (extension .app).
To make scripts available within iTunes, create (if it there already) the folder "~/Library/iTunes/Scripts/", and copy your .scpt or .app file there. You will notice a new menu appears in iTunes, containing the scripts you have put in that folder.
Note also that, within iTunes at least, .scpt and .app versions are processed in a slightly different way. There are some cases where it seems to be necessary to use the .app version to get the expected behaviour. So when in doubt, try saving as an .app.
iTunes gotchas
As intuitive and easy as coding in AppleScript is, it can also be the source of some infuriating "gotchas" that can only really be debugged in a haphazard, trial-and-error fashion.
In working with iTunes, one particular issue I've encountered repeatedly is that when you perform an operation which involves copying a file (e.g. importing into iTunes, copying to an iPod) the script doesn't wait for the process to finish; instead, it blithely skips on to the next command. This can cause problems if your next line happens to rely on that file being there (e.g. you want to modify the track info of the newly-copied file). The most effective way I could see to get around this was to use a "try" block inside a "repeat" loop. In other words, keep trying to perform the operation until it works. The basic construct is:
repeat until <desired operation has been performed>
try
<attempt to perform desired operation>
end try
end repeat
Script #1: create album playlists on an iPod Shuffle
I recently purchased a new iPod Shuffle, which has two nice features: the ability to organize music into playlists, and a "VoiceOver" function that can read out the current track/artist or playlist. There is, however, no way to change between individual albums, so I have found it useful to create playlists for each album.
Unfortunately, manually creating playlists for each album becomes quite tedious, particularly when you're changing the contents of the iPod at least once a week. So this script was written primarily to automate that process. It works by assuming that you've loaded all the desired albums onto the iPod; it then deletes any existing playlists before stepping through each track, creating a playlist for each new album it encounters and copying the track into it.
Added niceties that I added later were the ability to recognize multi-disc albums and create separate playlists for each disc, and the option to insert a prefix into the track name giving the track number in the form "n of N". This last feature is very handy when you want to know where you are in an album while listening to it.
So here, without any further garnish, is the script:
set iPodName to "Gekko MMXI"
set addTrackNumberPrefix to true
tell application "iTunes"
-- get index of ipod source
set myIPod to some source whose kind is iPod and name is iPodName
-- STEP 1: delete existing playlists
delete (every user playlist of myIPod whose smart is false and special kind is none)
--- STEP 2: loop thru tracks on ipod, assigning to new playlist according to artist and album
repeat with trk in tracks of myIPod
-- only consider tracks with a non-empty artist and album
if artist of trk is not "" and album of trk is not "" then
-- get album artist, using the album artist if it's defined
if album artist of trk is not equal to "" then
set albumArtist to (album artist of trk as string)
else
set albumArtist to (artist of trk as string)
end if
-- get album name, appending disc number if defined
if disc number of trk is not equal to 0 then
set albumName to (album of trk as string) & " (disc " & (disc number of trk as string) & ")"
else
set albumName to (album of trk as string)
end if
set listName to albumArtist & " - " & albumName
-- get playlist, creating it if it doesn't exist
if exists (some user playlist of myIPod whose name is listName) then
set plist to (some user playlist of myIPod whose name is listName)
else
set plist to (make user playlist of myIPod with properties {name:listName})
end if
-- copy track to playlist
set ipod_trk to (duplicate trk to plist)
-- add track number to track name
if addTrackNumberPrefix then
set trackNumber to track number of trk
set trackCount to track count of trk
if (trackNumber as string) is not "" and (trackCount as string) is not "" then
set numPrefix to ((trackNumber as string) & " of " & (trackCount as string) & ". ")
if (name of trk as string) does not start with numPrefix then
set name of trk to (numPrefix & (name of trk as string))
end if
end if
end if
end if
end repeat
end tell
Script #2: add ear-training exercises to iTunes and copy to iPod Shuffle
This one is slightly more esoteric but demonstrates some useful actions involving importing files into iTunes. The basic purpose of this script is to import audio files (in this case, ear training mp3s whose creation I described in a previous post), add some track info, and copy into playlists on the iPod Shuffle. You'll notice copious use of the repeat/try trick that I mentioned above. Note also the multiple "tell" blocks: in addition to the main iTunes block, there are several calls to the Finder to get directory/file lists.
set iPodName to "Gekko MMXI"
set fileFormat to "MPEG-4 audio"
set myLabel to "My Ear Training"
-- delete existing ear training tracks from ipod
tell application "iTunes"
-- get index of ipod source
set myIPod to (some source whose kind is iPod and name is iPodName)
-- delete existing ear training tracks
delete (tracks of myIPod whose artist is myLabel and genre is myLabel)
delete (tracks of library playlist 1 whose artist is myLabel and genre is myLabel)
end tell
-- import new files into itunes and copy to ipod
tell application "Finder"
-- get folders of each group of tests
set sourceDirs to folders of folder "EarTraining" of home
end tell
tell application "iTunes"
-- loop over test folders
repeat with sourceDir in sourceDirs
set sourceName to name of sourceDir
set listName to (myLabel & " - " & sourceName)
-- ensure that ipod has a playlist with this name
if not (exists (some user playlist of myIPod whose name is listName)) then
set plist to (make user playlist of myIPod with properties {name:listName})
else
set plist to (some user playlist of myIPod whose name is listName)
end if
-- get source files in this directory
tell application "Finder"
set sourceFiles to (files of sourceDir whose kind is fileFormat)
end tell
-- get number of tracks in this directory
set trackCount to count of sourceFiles
-- import each file into iTunes and set info
set trackNumber to 0
repeat with sourceFile in sourceFiles
set trackNumber to trackNumber + 1
set trackName to (text 1 thru -5 of (name of sourceFile as string))
-- import track into itunes
with timeout of 30 seconds
set trk to (add alias (sourceFile as string) to library playlist 1)
end timeout
-- add track info: use "try" inside a "repeat" loop to overcome the flaky behaviour which occasionally denies write permission to file
repeat while album of trk is not equal to (name of sourceDir as string)
try
set album of trk to (name of sourceDir)
end try
end repeat
repeat while artist of trk is not equal to myLabel
try
set artist of trk to myLabel
end try
end repeat
repeat while genre of trk is not equal to myLabel
try
set genre of trk to myLabel
end try
end repeat
repeat while track count of trk is not equal to trackCount
try
set track count of trk to trackCount
end try
end repeat
repeat while track number of trk is not equal to trackNumber
try
set track number of trk to trackNumber
end try
end repeat
-- finally, explicitly set the track name to the file name, since iTunes will append a counter to it if the same file has been imported multiple times (do this after the previous steps because the change only occurs when the track finished loading)
repeat while name of trk is not equal to trackName
try
set name of trk to trackName
end try
end repeat
set ipod_trk to (duplicate trk to library playlist 1 of myIPod)
-- need a repeated try loop here as the copy takes some time to complete
repeat until exists (some track of plist whose name is trackName)
try
duplicate ipod_trk to plist
end try
end repeat
end repeat
end repeat
end tell

No comments:

Post a Comment