Python appscript for Audio Hijack

After messing around with my Audio Hijack Pro setup last week, I thought it would be nice to have a command that prints out all my scheduled timers. The usual main page of Audio Hijack Pro shows the timers in the scrolling list on the left side, but doesn’t show the whole schedule at once.

I started writing an AppleScript that I could call from the command line but soon got frustrated with AppleScript’s limited text formatting functions. I decided to try Python with appscript. From the appscript home page:

Appscript is a high-level, user-friendly Apple event bridge that allows you to control scriptable Mac OS X applications using ordinary Python scripts. Appscript makes Python a serious alternative to Apple’s own AppleScript language for automating your Mac.

I’d installed appscript a while ago, and played around with a few examples, but never tried scripts of my own until this one, which I call ahtimers. It goes through the Audio Hijack Pro timers, picks out the scheduled ones (which have a check in the “On” box—see the screen shot above), and prints out a nicely formatted list of the shows, days, and times. For my current setup, running ahtimers from the command line produces this:

Alternative Sixties: ----T-- from  9:00 PM to 10:05 PM  (65 min)
 Mark Steel Lecture: -----F- from  6:00 AM to  6:40 AM  (40 min)
      Saturday Show: S------ from 12:30 AM to  3:35 AM (185 min)
  Sounds of the 60s: S------ from 10:00 AM to 12:05 PM (125 min)
  Sounds of the 70s: -----F- from  7:30 PM to  8:05 PM  (35 min)

I think the output explains itself. What I like about it—and what I was having trouble doing in AppleScript—is how the fields line up for easy reading. The timers are alphabetized by name because that’s how Audio Hijack Pro keeps them ordered. I thought about ordering them according to time, but didn’t see any advantage to it.

Here’s the source code for ahtimers:

 1:  #!/usr/bin/env python
 2:  
 3:  from appscript import *
 4:  import datetime
 5:  
 6:  # Get a list of all the sessions.
 7:  allsessions = app('Audio Hijack Pro').sessions.get()
 8:  
 9:  # Make a list with the sessions that have scheduled timers.
10:  timersessions = []
11:  for s in allsessions:
12:      for t in s.timers.get():
13:          if t.scheduled():
14:            timersessions.append(s)
15:            break # go to next session after finding a scheduled timer
16:  
17:  # Get the length of the longest name of a timersession.
18:  longest = max(len(s.name()) for s in timersessions)
19:  
20:  # Make a 7-character string with the days that the timer runs.
21:  def timerdays(t):
22:    daylist = ['-'] * 7
23:    if t.runs_Sunday():
24:      daylist[0] = 'S'
25:    if t.runs_Monday():
26:      daylist[1] = 'M'
27:    if t.runs_Tuesday():
28:      daylist[2] = 'T'
29:    if t.runs_Wednesday():
30:      daylist[3] = 'W'
31:    if t.runs_Thursday():
32:      daylist[4] = 'T'
33:    if t.runs_Friday():
34:      daylist[5] = 'F'
35:    if t.runs_Saturday():
36:      daylist[6] = 'S'
37:    return ''.join(daylist)
38:  
39:  # Print the info for all the sessions with enabled timers.
40:  for s in timersessions:
41:    for t in s.timers.get():
42:      if t.scheduled():
43:        dur = t.duration()
44:        durstr = '(%d min)' % (dur/60)
45:        st = t.start_time()
46:        et = st + datetime.timedelta(seconds = dur)
47:        dow = timerdays(t)
48:        ststr = st.strftime("%l:%M %p")
49:        etstr = et.strftime("%l:%M %p")
50:        fmtstr = "%" + str(longest) + "s: %s from %s to %s %s"
51:        print fmtstr % (s.name(), dow, ststr, etstr, durstr.rjust(9))

The longest show name, determined from the generator on line 18, is used to create the format string in line 50 which gets all the names lined up and right justified. The days of the week are formatted through the timerdays function in lines 21-37. The start and end times line up because of the %l format string in lines 48 and 49; it puts a space before single-digit hours. The durations at the end of each line are aligned by the rjust(9) method in line 51; I used a specific number for the field width because I feel pretty confident that I’ll never have a timer that records for more than 999 minutes (over 16 hours). And if I’m wrong about that, it will only screw up the alignment at the end of the line, which isn’t a big deal.

Overall, I found appscript pretty easy to work with. The get() method is a bit of a mystery to me—it was needed to get the sessions and timers lists, for example, but not needed to get a timer’s duration or start time—but I suspect there’s a good reason for it that I’ll learn as I keep using it.

The biggest problem with using appscript (or rb-appscript for Ruby) rather than AppleScript is that it isn’t included with the system. If you’re developing scripts to be used by others—especially nonprogrammers—you can’t expect them to have or install appscript. I’ve read that PyObjC is expected to be included with Leopard but haven’t heard the same about appscript. I have my fingers crossed.

Tags:


3 Responses to “Python appscript for Audio Hijack”

  1. Nick says:

    The deal with the Get() method is that that is the method which actually sends the Apple Event—-most everything else you do in appscript is simply creating names for things.

    This is important for performance reasons, the fewer Apple Events you send the better and so it is better to get() a collection of items than iterate through the collection of names and get() each one.

    This is one of the many things about Applescript I never understood until I started programming with appscript.

  2. has says:

    “The get() method is a bit of a mystery to me—it was needed to get the sessions and timers lists, for example, but not needed to get a timer’s duration or start time”

    Note that ‘t.duration()’ and ‘t.start_time()’ are merely shorthand for ‘t.duration.get()’ and ‘t.start_time.get()’. You’re still sending ‘get’ events; it’s just that py-appscript lets you skip typing the ‘.get’ part if you want to.

    See chapter 11 of the appscript manual for more information on the various convenience shortcuts that appscript supports.

    “If you’re developing scripts to be used by others—especially nonprogrammers—you can’t expect them to have or install appscript.”

    One option is to convert your Python and Ruby scripts into standalone or semi-standalone executables using py2app/RubyScript2Exe. This will automatically include any third-party dependencies in the application bundle.

    Another option might be to distribute your scripts as .mpkgs with the appscript installers included in the metapackage and automatically run as part of your script’s installation procedure.

    Also, if Leopard includes Easy Install and RubyGems as part of its stock Python and Ruby installations, it may be possible to install 3rd-party dependencies (semi-?)automatically via those. I’m not too familiar with these technologies myself, but it’s something I’ll look into after Leopard ships in October.

    “I’ve read that PyObjC is expected to be included with Leopard but haven’t heard the same about appscript. I have my fingers crossed.”

    Apple did consider including appscript, but in the end decided to peddle Leopard’s existing ObjC/Cocoa-based Scripting Bridge to Python and Ruby users as well. As to how using Scripting Bridge via PyObjC and RubyCocoa will compare to appscript, that’s just something we’ll have to wait till October to find out.

    HTH

    has

    http://appscript.sourceforge.net http://rb-appscript.rubyforge.org

  3. Dr. Drang says:

    Thanks to Nick and has for the detailed comments. Maybe, as with Nick, understanding get() will come to me with more use of appscript.