A stylised depiction of Rob

Primary Unit

Mano-al-teclado

In a follow-up to his man page post, Dr Drang writes that he suspects the people who responded with keyboard-centric methods “were all slightly appalled by the notion of using the mouse or trackpad to bring up a man page.”

No such dismay here. While I generally prefer to keep my hands on the keyboard there are often cases when using the mouse is easier or faster, as explained in the Bruce Tognazzini article Dr Drang quotes.

Not all keyboard shortcuts are created equal. In my bookmarks bar post I threw out one lot for another, as it was difficult to remember the shortcut for a particular function even though the key combinations were simple (⌘-[1-9]). This led to the situation Tog describes: it took longer to remember the shortcut than it would to use the mouse.

It takes two seconds to decide upon which special-function key to press. Deciding among abstract symbols is a high-level cognitive function. Not only is this decision not boring, the user actually experiences amnesia! Real amnesia!

But I think indexes such as LaunchBar (or InDesign’s quick apply panel) sidestep the problem. Here’s what Tog says about mouse users:

They have not had to set their task aside to think about or remember abstract symbols.

That’s remarkably similar to using indexes: after typing a simple shortcut (⌘-Space, ⌘-↩) to bring up the panel, a user just has to type their intent. I suspect that cuts down on the decision time substantially.

Broken Mercurial dummy cacerts

When I tried to push my last post to Bitbucket I received this ugly error:

abort: error: _ssl.c:507: error:14090086:SSL
routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

Gross as it is, the message is straightforward: the SSL certificate failed to verify. I imagine the root cause is the whole OpenSSL mess and everyone reissuing their certificates, but it posed an immediate practical local problem: I couldn’t push to my source control.

Git(hub) seemed to be fine, but any Mercurial commands involving the network — either trying to connect to Bitbucket or Kiln — would fail.

The culprit turned out to be this line in my ~/.hgrc:

[web]
cacerts = /etc/hg-dummy-cert.pem

Which, as dumb as it looks, is the recommended way to enable certificate checking through the system keychain.

Regenerating the permissions file didn’t help. Maybe this approach will work again in the future, once the wave of reissued certificates has broken, but for now there’s a straightforward solution:

  1. Remove the cacerts line from your hgrc;
  2. Use a Mercurial command such as hg incoming that causes it to wail about a server’s certificate not being verified;
  3. Using the details from that message, add a hostfingerprints section to your hgrc;
  4. Repeat with each server you connect to.

You should end up with something like this:

[hostfingerprints]
bitbucket.org = 45:AD:AE:1A:CF:0E:73:47…
robjwells.kilnhg.com = c3:83:2c:5a:2d:0…

See Bitbucket’s post for a few more details.

Manhandled

Dr Drang and Nathan have both written recently about bringing up a man page for reference while typing a command in the terminal. I’ve two approaches to share, but unlike Nathan I can’t claim that they’re superior — just alternatives.

I prefer them over the right-click method because I don’t like to take my hands off the keyboard unless absolutely necessary, and moving to Zsh is a bit much for me right now.

First is LaunchBar’s built-in man page search (found under UTF-8 search templates in the index), which uses the x-man-page:// URL scheme to bring up a new Terminal window. Here’s what you type:

⌘-Space man Space `command` ↩

The second approach layers the wonderful Dash on top of LaunchBar, bringing up the man page in Dash’s interface instead of the Terminal.

⌘-Space dash Space `command` ↩

This gives you all the usual Dash goodies, including a section list and inline links to other man pages (if you decide, after all, to take your hands off the keyboard).

You can prepend man: to the command name to restrict the search to man pages only, or even add a search template of the form dash://man:* to LaunchBar’s index — in place of the first man page search if you like.

Misbehaving single-column NSTableView

This is mostly a note for myself, as I was ready to pull my hair out earlier.

Interface Builder can get in your way when you want to create a single-column NSTableView where the column fills the entire width available.

Setting the column count to 1 doesn’t automatically resize it, and extending the column using its resize handle or by setting its width to the width of the view can cause a horizontal scrollbar to appear.

The solution is to resize the view itself — so that you eat into the space occupied by the single column, and then expand back to your desired width. It is incredibly stupid. Here’s a video:

There’s a Stack Overflow question where the accepted answer recommends this method. Handily I misread it — only realising what it was saying after I’d stumbled across the resizing trick myself (after trying many other things).

In related news, I’m almost done with the excellent Big Nerd Ranch Cocoa book after getting sidetracked for two months.

Scraping Entourage

Early last year myself and my flatmate got into Entourage, which is incredible. We devoured all eight seasons in a few weeks. One thing that I love about it is the soundtrack. It’s a mix of older and newer stuff and, frankly, I have no idea about music so it’s nice to have a pool of well-picked tracks to dip in to.

In December, when my flatmate rewatched the series, I decided to do something with the soundtrack. Getting the music (for 96 episodes) wasn’t as easy as downloading an album — it took some programming, a few headaches, and a bit of tedium. What follows was ultimately successful, but I’d hesitate to call it a success story.

Sourcing the music

HBO’s website does list the tracks used in each episode, but you can’t get at them as it’s all Flash. However, Tunefind has good data, which I imagine some industrious person has transcribed from the official list. In some cases it’s more complete.

But the advantages of having users assemble such data are weighed against the mistakes they may make and inconsistencies that creep in. I’ll address the practical implications later. First we need to extract the track details from the website.

The programs in this post are incredibly rough. I share them in the hope they’ll help others and spark ideas, not as good examples.

All of the code is collected in a multi-file Gist.

python:
 1:  #!/usr/local/bin/python3
 2:  
 3:  import re
 4:  import json
 5:  from bs4 import BeautifulSoup
 6:  from urllib.request import urlopen
 7:  
 8:  base_url = 'http://www.tunefind.com'
 9:  seasons_index = '/show/entourage'
10:  response = urlopen(base_url + seasons_index)
11:  response_text = response.read().decode()
12:  soup = BeautifulSoup(response_text)
13:  
14:  seasons_div = soup.find('div', class_='lefttext sidebarIndent')
15:  seasons_urls = [(base_url + a_tag['href'])
16:                  for a_tag in seasons_div.find_all('a')]
17:  
18:  tracks_list = []
19:  
20:  for s in seasons_urls:
21:    season = BeautifulSoup(urlopen(s).read().decode())
22:    episode_urls = [(base_url + a_tag['href']) for a_tag
23:                    in season.find_all('a',
24:                      {'name': re.compile(r'episode\d+')})]
25:  
26:    for e in episode_urls:
27:      episode = BeautifulSoup(urlopen(e).read().decode())
28:  
29:      for raw in episode.find_all(class_='tf-songevent-text'):
30:        match = re.search(r'(.+)\n\s+by (.+)', raw.text.strip())
31:        if match:
32:          tracks_list.append(match.groups())
33:  
34:  for track in tracks_list:
35:    # filter list for duplicates
36:    if tracks_list.count(track) > 1:
37:      tracks_list.remove(track)
38:  
39:  with open('/Users/robjwells/Desktop/tracks.json', 'w') as tracks_json:
40:    json.dump(tracks_list, tracks_json)

Tunefind’s website has index pages for each series with links to each of their seasons, which link to pages for each episode that contain track details. The scraping code iterates over the seasons (lines 14–24), then the episodes (lines 26–32).

The loop starting on line 20 appends a tuple of the track title and artist name, found in line 30, to tracks_list. This is bluntly weeded for exact duplicates in lines 34–37. (Subtly different entries for the same track aren’t affected, so they have to be picked out when adding the tracks to Spotify, detailed below.)

Lastly the list is written to a file; I chose JSON because I wasn’t sure what I was going to do next. The tuples are converted to lists, which is fine for our purposes.

Doing something with it

We’ve now got a nice file of titles and artists — 789 tracks in total. The next question is how to use it. My goal was to construct a Spotify playlist, as the service has a large catalogue of tracks available for free and it would take no effort to use once set up.

Initially I considered using the Spotify API, but it appeared too daunting and the quality of the data would derail attempts to add tracks programmatically. Instead I settled on scripting the desktop client. That’s easier said than done, as its AppleScript support is poor.

When I publish I always check the post’s links. Thank god I did, because Spotify has recently overhauled its developer site. The metadata API looks far more approachable.

However, discrepancies between the Tunefind data and Spotify’s catalogue would still likely cause headaches. Also, adding tracks to a playlist is still reserved for its C API.

After mulling over how much manual work I was willing to do, I came up with the idea for an AppleScript scaffold that, since I couldn’t script Spotify directly, would script around Spotify: using its URL scheme to search for tracks and a dialog box with controls to move through the list.

Underneath that would be Python that managed the track list, assembled the URL and handled the controls:

python:
 1:  #!/usr/local/bin/python3
 2:  
 3:  import json
 4:  import subprocess
 5:  
 6:  position = open('/Users/robjwells/Desktop/position', 'r+')
 7:  reported = open('/Users/robjwells/Desktop/reported', 'a')
 8:  
 9:  
10:  def asrun(script):
11:    "Run the given AppleScript and return the standard output and error."
12:  
13:    osa = subprocess.Popen(['osascript', '-'],
14:                           stdin=subprocess.PIPE,
15:                           stdout=subprocess.PIPE)
16:    return osa.communicate(script.encode())[0]
17:  
18:  
19:  with open('/Users/robjwells/Desktop/tracks.json') as tracks_json:
20:    tracks_list = json.load(tracks_json)
21:  
22:  ascript = '''
23:  tell application "LaunchBar"
24:     perform action "Open Location" with string "spotify:search:{0}"
25:  end tell
26:  
27:  tell application "Finder"
28:     set dialog_result to display dialog "Ready for next track?" ¬
29:         buttons {{"Report", "Stop", "Next"}} default button "Next"
30:     return button returned of dialog_result
31:  end tell
32:  '''
33:  
34:  
35:  def prep_track(t):
36:    joint = ' '.join(t)
37:    return joint.replace(' ', '+')
38:  
39:  
40:  def prompt(t):
41:    script = ascript.format(prep_track(t))
42:    result = asrun(script).decode().strip()
43:    if result == 'Stop':
44:      return False
45:    elif result == 'Report':
46:      reported.write(json.dumps(t))
47:      reported.write('\n')
48:    position.seek(0)
49:    position.write(str(tracks_list.index(t) + 1))
50:    return True
51:  
52:  
53:  raw_pos = position.read()
54:  if raw_pos:
55:    pos = int(raw_pos)
56:  else:
57:    pos = 0
58:  
59:  for track in tracks_list[pos:]:
60:    if not prompt(track):
61:      break
62:  
63:  position.close()
64:  reported.close()

According to the file metadata, I created this script at 12.50am. Some of it is hilariously bad. I haven’t tidied up these scripts but I had to change a bit where I trampled all over my own global names. It’s in desperate need of re-ordering so I’m going to work through the script in the order parts are used, not in which they’re written.

At its core, it’s a little Python engine that uses Dr Drang’s asrun function to run an AppleScript via osascript.

We start by opening a position file to store our place in the track list and a reported file to store any tracks which aren’t in the Spotify catalogue (lines 6 & 7). Next we open and parse the JSON track list (lines 19 & 20).

Lines 53–57 determine from the position file where to begin in the track list, with pos used to slice it in line 59. The selected part of the list is iterated over and each track tuple fed to the prompt function (lines 40–50).

This formats the track (using prep_track to join the artist and title, and replace spaces with plus signs) and inserts it into the AppleScript (line 41), which is then run.

The AppleScript opens the Spotify search URL and displays a dialog box:

Screenshot of the dialog box on top of the Spotify search results

The chosen button is returned and assigned to result. If the searched-for track couldn’t be found, the “Report” button adds it to the reported file (lines 46 & 47) and proceeds as if “Next” had also been clicked: writing the index of the next track to the position file and returning True to cause the loop in lines 59–61 to continue. “Stop” returns false and ends the loop — and so the script.

Within the AppleScript itself (lines 22–32), LaunchBar is used to open the Spotify. This is holdover from when I was trying to use LaunchBar’s “Browse in Spotify” action (select Spotify and press space) instead of the search URL. It didn’t work. There’s no reason to use LaunchBar anymore, this would work fine:

open location "search:spotify:{0}"

Where {0} is a replacement field.

Once the sliced list has been iterated over, we clean up by closing the position and reported files (lines 63 & 64).

The script does a decent job of working around the inability to directly control Spotify. It leaves me to pick an appropriate track from the search results shown, drag it to a playlist and chose the appropriate next action from the dialog. It turned a mountain of tedious work into a reasonably-sized hill.

Being human, though, I occasionally picked the wrong button in the dialog, meaning some tracks weren’t reported or were reported in error. Ah well. I was still left with a playlist of about 570 tracks.

A musical interlude

Since creating it in mid-December, I’ve been listening to the playlist and marking my favourite tracks so I could buy them on iTunes. There’s too many to do by hand, so a bit of automation is called for.

Dragging a track from the Spotify client gives you an http://open.spotify.com/track/… URL. Opening it in a browser will likely get you the newfangled web player, which isn’t much use, but tools such as curl still return the source to the detail view you used to get. We’ll use this to turn the URLs back into track titles and artist names, and from there into iTunes links.

python:
 1:  #!/usr/local/bin/python3
 2:  
 3:  import os
 4:  import sys
 5:  import requests
 6:  from bs4 import BeautifulSoup as bs
 7:  
 8:  spot_path = os.path.join(os.getcwd(), sys.argv[1])
 9:  itunes_search = 'https://itunes.apple.com/search?term={}&country=gb'
10:  
11:  with open(spot_path) as spot_file:
12:    spot_links = spot_file.read().splitlines()
13:  
14:  
15:  def spotify_to_itunes(link):
16:    spot_soup = bs((requests.get(link)).text)
17:    title = spot_soup.h1.text
18:    artist = spot_soup.h2.a.text
19:    plussed = '+'.join([title, artist]).replace(' ', '+')
20:    itunes_response = requests.get(itunes_search.format(plussed))
21:    if itunes_response.json()['resultCount']:
22:      return itunes_response.json()['results'][0]['trackViewUrl']
23:    else:
24:      return ' '.join([artist, title])
25:  
26:  for line in spot_links:
27:    print(spotify_to_itunes(line))

Dragging several tracks from Spotify gives you URLs separated by newlines, which I saved to a file that is read and split in lines 11 & 12. We iterate over each URL at the end of the script, printing the result of passing the URL to spotify_to_itunes, which does the hard work.

We get the source for the track’s detail view in line 16 and pull out the title and artist, which are joined and the spaces replaced with plus signs in line 19 to make the string URL safe — like in the previous script.

This combined string is used to search the iTunes catalogue (line 20, with the URL in line 9). If there are any results we return the URL for the top hit, otherwise we return the artist name and track title so it can be looked up manually.

Taking the URL for the top iTunes result is potentially hazardous, but here I’m happy enough to put my faith in its search. Also the script takes two requests to process each track (lines 16 & 20) so it is slow, but as it only needs to be run once so I don’t think it really matters.

I decided not to rig up the AppleScript side here, because in my mind it doesn’t require the same kind of industrial processing. At this point it’s a matter of taking time to listen to the track previews and decide which to buy.

Also in contrast to the previous scripts, I print to stdout here instead of writing directly to a file. I don’t have reason for this but I guess partly it’s because there’s no data structure to preserve, just a set of lines to transform.

At this point I’ve run the script and redirected the output to a file. My plan is open it in BBEdit and use ⌘L to select each, send it to LaunchBar (using the Instant Send feature) and open the URLs in Safari and the leftover tracks in iTunes’s search. Thankfully the iTunes links just load a web preview and don’t activate iTunes itself.

I’ve done this already with the tracks in the reported file, run through a modified version of the above script. It works reasonably well.

Wrapping up

While writing (and re-writing) this post I began to doubt its value. These aren’t polished scripts and this shouldn’t be taken as a guide, but at the same time it is how I solved the problem. Examining the choices made — however little thought went into them — has been instructive for me and hopefully will give people facing similar problems something to think about.

And while I’d hesitate to call this a real problem, the cobbled-together solution has at least produced something I’m still listening to after two months.

My one iOS 7 problem

I don’t want to get into the business of critiquing Apple’s OS releases — there are plenty of people better placed to do that. But I have one major criticism of iOS 7. One that irritated me on the day of release and irritates me today. It’s time to share.

I’m Rob and I’m an addict (of changing the screen brightness).

I don’t always second-guess the automatic setting, but I frequently do. Before iOS 7, you had to stop what you were doing, jump to Settings.app, go to the brightness page and adjust the slider. But we’ve now got Control Centre, which you swipe up from the bottom of the screen and it has a brightness slider right there. Sorted?

Not quite. Adjusting the brightness before iOS 7 was slow and a bit haphazard since the settings screen wasn’t always representative of what you were looking at. But you got a decent idea.

Control Centre, however, flat-out lies to you about the brightness:

Side-by-side image showing the difference in brightness when the iOS7 Control Centre is shown and hidden

Now, this is by design — Control Centre’s a giant grey panel that covers the screen. But it means that the only elements at the “true” brightness are filled sections of enabled buttons and sliders.

The article in the background of the image above has a white background. If you use an eyedropper, the image on the right scores 100% brightness on a greyscale slider. On the left, with Control Centre shown, it’s just 53%.

Does this background look white to you?

Despite using iOS 7 since its release in September, I still can’t set the brightness to my desired level in a single attempt because of this.

Yes, this is trivial, but let’s try a thought experiment. Imagine the only way you can set the playback volume is through Settings.app or the Control Centre. It’d be way more convenient to use Control Centre, right? How about if it ducked the volume by half?

Next and last weekdays

I was recently reminded of David Sparks’s TextExpander date snippets, the most interesting of which use AppleScript to insert the date for the next occurrence of a certain weekday. (And were written by Ben Waldie.)

Out of curiosity, I wrote a Python command-line tool called dayshift that does something similar. Its main differences are that it doesn’t have to be set up for specific days like the AppleScript snippets, and that it can find the date for a past weekday (“last Monday”) as well as a future one (“next Monday”).

python:
 1:  #!/usr/local/bin/python3
 2:  """
 3:  Print the date of the next or last specified weekday.
 4:  
 5:  Usage:
 6:    dayshift next <weekday> [--format=<fmt> --inline]
 7:    dayshift last <weekday> [--format=<fmt> --inline]
 8:  
 9:  Options:
10:    --format=<fmt>, -f <fmt>  A strftime format string
11:                              [default: %Y-%m-%d]
12:    --inline, -i              Omit trailing newline
13:  
14:  """
15:  from docopt import docopt
16:  from datetime import date, timedelta
17:  
18:  WEEKDAYS = {'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3,
19:              'fri': 4, 'sat': 5, 'sun': 6}
20:  
21:  args = docopt(__doc__)
22:  starting_offset = 2 if args['next'] else -2
23:  start_date = date.today() + timedelta(starting_offset)
24:  start_integer = start_date.weekday()
25:  target_integer = WEEKDAYS[args['<weekday>'][:3].lower()]
26:  
27:  if args['next']:
28:    offset = target_integer - start_integer
29:  elif args['last']:
30:    offset = start_integer - target_integer
31:  
32:  if offset <= 0:
33:    offset += 7
34:  
35:  if args['last']:
36:    offset /= -1    # Make the offset negative
37:  
38:  target_date = start_date + timedelta(offset)
39:  ending = '' if args['--inline'] else '\n'
40:  print(target_date.strftime(args['--format']), end=ending)

Update

I’ve added an offset to the starting day (lines 22 & 23) to skip past the two days before and after the current date. The line numbers in the explanation below have been altered to match.

You might like to look at the corresponding Gist in case I’ve made any further changes.

The interface is created by docopt in line 21 from the script’s docstring (lines 2–14).

After parsing the arguments we reduce the given weekday to its integer representation in line 25 by getting its first three characters, converting them to lowercase and using that string to key into the WEEKDAYS dictionary (lines 18 & 19).

On line 24 we get the integer for a starting date which is offset forward or backward by two days (lines 22 & 23). You can adjust the forward and backward offsets separately, depending on where you draw the line.

Now the next and last commands come into play, deciding how we compare the two weekday integers (lines 27–30). In both instances we can end up with 0 or a negative number so we add 7 to the offset (line 33).

But surely we want a negative number if we’re after the “last” weekday? Yes, but not just yet — at this point it tells us that we’ve passed the given weekday in the current week (if next) or the given weekday is still to come (if last). We add 7 to move outside of the current week.

To ensure we go back in time we specifically invert the offset in line 36, before adding it to the starting date in line 38 to reach the target date.

Now the two options shown in the docstring come into play. Line 39 uses the --inline flag to determine whether the printed string ends in a newline or not — handy when the script is called by a snippet inserted in the middle of some text.

The other option, --format, determines how the date is printed. I use docopt’s ability to have default values (see line 11) to print an ISO 8601 date if another format isn’t given. This lets me pass the argument directly to strftime in line 40.

Using the script in TextExpander

The script works great as a command-line utility:

$ dayshift next Wednesday
2014-01-15
$ dayshift last Wednesday
2014-01-08

But, returning to the original use case, I recommend two approaches.

You can set up individual snippets to find the next or last occurrence of a certain weekday. Add a new shell script snippet, with code similar to this:

bash:
#!/bin/bash
/path/to/dayshift next Monday -i

Or you can use TextExpander’s fill-in feature to let you pick the options on the fly (line breaks inserted for readability):

bash:
#!/bin/bash
/path/to/dayshift %fillpopup:name=Next/Last:default=next:last%
%fillpopup:name=Weekday:default=Monday:Tuesday:
Wednesday:Thursday:Friday:Saturday:Sunday% -i

That looks pretty gross there, and it doesn’t look good in use either:

Screenshot of the dayshift fill-in TextExpander snippet

But it gives you a lot of control and you can tab between the fields.

Update

Shortly after posting this yesterday, Dr Drang sent me a nice email pointing out a comment by Dave Cross on how to use the date utility in Unix to similar effect, which in its simplest form is date -v+weds (to get next Wednesday’s date).

I recommend reading the entire post that comment is in reply to as it addresses a very similar situation to the one above, but with an illuminating look at working with weekdays as integers.

Hijacking the BBC

In December I started using Audio Hijack Pro to record Jazz on 3, initially from the Radio 3 live stream. I soon realised this wasn’t a great idea: live streaming, Flash and an occasionally patchy internet connection don’t make for reliable recording.

A bit of searching turned up a blog post and GitHub repository by Dr Drang — who else! His approach was to record programmes from the BBC website after they’ve been broadcast. This is more reliable, allows for repeated tries if the recording fails for any reason, and makes it easier to handle longer-than-usual episodes.

The logic is straightforward: find the unique ID for the programme’s most recent episode, use that to construct the player URL, and feed that to Audio Hijack.

Most of the work is done in Python, with Audio Hijack running bridging AppleScripts when a recording starts and finishes — the latter used by Dr Drang to add a track list to the recording’s lyrics field and add the file to iTunes.

This sounded ideal, but I couldn’t use the Doc’s work straight away as the scripts focus on Radio 2, use a player URL that doesn’t work (at least at home in Blighty) and need a few tweaks to fit my Python environment. It gave me an excuse to tinker with the wheel, if not reinvent it.

If you want to follow along at home, all the code is in a Bitbucket repository.

Before we dig in, there are two practical things to note:

  • It’s all written for Python 3, but it wouldn’t take much to work on 2.
  • You’ll need to install Beautiful Soup 4 and docopt. (Use pip.)

I initially wanted to consolidate the Python scripts into one file but that turned out to be stupid, so I (eventually) settled on having module similar to Dr Drang’s that did most of the work and another script that acted as the command-line interface.

(That partly explains why my repo has fewer files — the other being that I add the artwork in Audio Hijack’s tags tab, not a script.)

Python module: bbcradio

A look at the programme info dictionaries at the top of our modules reveals the differences in our methods:

python:
# Dr Drang’s
showinfo = {'70s': (6, re.compile(r"Sounds of the '?70s")),
            '60s': (5, re.compile(r"Sounds of the '?60s")),
            'soul':(2, re.compile(r"Trevor Nelson")),
            'at':  (3, re.compile(r"At the BBC"))}
# Mine:
PROG_DICT = {'jazz on 3': 'b006tt0y',
             'jazz line-up': 'b006tnmw'}

Each entry in the Doc’s dictionary is a tuple of the weekday as an integer — used to construct a URL for a daily schedule — and a regular expression to pick out the programme from the scraped schedule.

In mine I go after the programmes directly, as they all have a unique ID similar to the ones given to individual episodes. With this ID we can go to a list of the programme’s available episodes and pluck out the most recent one.

This has two benefits: we don’t need to care about the broadcast day and don’t have to search the schedule page. All you need is the programme ID, which you can grab from the end of a programme’s home page URL.

All the work is done in latest_episode_code in lines 17–29, which fetches the available episodes page and pulls the episode code for the first episode listed.

For weekly programmes only one episode is listed, but this technique also works for programmes which are broadcast more often — like Today, which Dr Drang has addressed specifically. In such cases just make sure to record an episode while it’s the most recent on the available episodes page.

I’ve kept Dr Drang’s episodeInfo function — used to fetch the episode title, date and track list — largely intact. I’ve added the time a track was played (if available — it’s often not) and I use a simpler method to fetch the broadcast date (pulling an RFC 3339 date from an attribute).

But there is one important difference: the use of Beautiful Soup’s select function, which allows for a liberal search for multiple class names, instead of find_all, which only returns elements which exactly match a given class string. This is important because every other track listing has an alt class.

Command-line interface: beebhijack

By itself bbcradio is inert, with the moving parts contained in a single interface script that has two modes which return an episode’s streaming URL or its details and track list. (Originally these were separate scripts, but there was a lot of duplicate code.)

At the command line you just pick a mode and supply a programme name. Here’s the help message:

$ beebhijack -h
Usage:
    beebhijack url <programme>
    beebhijack details [--clean] <programme>

Options:
    --clean     Use two newlines instead of a pipe
                to separate the episode details

Accepted programmes:
    jazz on 3
    jazz line-up

And, because I’m still blown away how simple docopt is, here’s all the code needed to produce that interface:

python:
 7:  programmes = '''\
 8:  Accepted programmes:
 9:      {}'''.format('\n    '.join(bbcradio.PROG_DICT.keys()))
10:  
11:  usage = '''\
12:  Usage:
13:      {name} url <programme>
14:      {name} details [--clean] <programme>
15:  
16:  Options:
17:      --clean     Use two newlines instead of a pipe
18:                  to separate the episode details
19:  
20:  {prog_list}'''.format(name='beebhijack', prog_list=programmes)
21:  
22:  args = docopt(usage)

If you haven’t used it before, take 20 minutes to watch the docopt video. It’s wonderful: give it a usage message and it hands you back a dictionary. Mine above is a little complicated because it tacks the list of accepted programmes onto the end of the usage message, in case you need a reminder at the command line. (I also print the list if you ask for a programme that’s not in the dictionary, in lines 24–27).

In the dictionary docopt returns, commands (url, details) get a boolean value, so the mode switch is a simple if-else if.

Unless given the --clean argument, the details mode prints the three parts of the tuple it gets from episode_details separated by pipes, which you can split up in AppleScript by changing the text item delimiters.

It’s important to note that I don’t exactly print the details — instead I encode the string into UTF-8 and write it as bytes to sys.stdout.buffer. (Dr Drang’s script does something similar.) The shell invoked by AppleScript’s do shell script command defaults to US-ASCII, causing a UnicodeEncodeError if you try to print a string containing non-Ascii characters.

Generally, I think the key distinction between beebhijack and Dr Drang’s four scripts is that I keep all of the “real work” in the module.

For example, we both fetch the episode ID in our modules, but I also construct the streaming URL there (lines 32–35) while the Doc does that in a separate script.

This isn’t a big deal either way but I think it reveals my thinking about the beebhijack script: it’s a bridge between what you want to achieve and the program that actually does the work. The heaviest lifting done by beebhijack (joining the details) controls how the information is output, not what the information is.

Putting it all together

With the Python code sorted, all that’s left to do is to create individual pre- and post-recording AppleScripts that you get Audio Hijack to run (on the input and recording tabs, respectively).

The pre-recording script just calls beebhijack’s url mode and pass a programme name. I’ve copied Dr Drang’s, but separated out the programme name and path to the script into properties to make them easier to spot and change.

The same applies to the post-recording script, which I stole off the Doc, who adapted one by Rogue Amoeba.

The biggest change I made was to use text-item delimiters to split up the episode date, title and track list as I don’t just put them all into the lyrics field, instead using the title and date to rename the track. (The date is necessary because, although I have Audio Hijack name the files with the date, the recording takes place at least the day after the broadcast day.)

I also don’t use a try block to handle errors fetching the episode details, because I prefer have an error message in the morning to prompt me to sort out the recording by hand.

Once you’ve got the scripts in place, just set an appropriate schedule in Audio Hijack. I have it set to record for much longer than the usual episode length, just in case a longer episode airs that I don’t expect. This doesn’t inflate the file size, because Audio Hijack doesn’t add silence when nothing is output from your source.

Die, bookmarks bar, die

I hate the bookmarks bar. This thing:

The bookmarks bar in Safari

It’s all about the mouse here. Keyboard access is through ⌘1 for the first entry and so on, which often leaves me counting along to find which number I need.

What’s the number for Pinboard: Unread? One… two… three… four!

That’s gross and slow. It’s exactly the same reason why I use InDesign’s quick apply panel at work and don’t even try to remember the per-style keyboard shortcuts (which, handily, only work with the number pad).

It also applies to keyboard shortcuts more generally — if you don’t use them often enough you’ll forget what the shortcut is, annoying you later and often taking more time that it would to grab the mouse.

Assigning less-arbitrary shortcuts can be helpful, but again only if you use them fairly regularly.

Back to that horrid bookmark bar, though. How do we construct a Launchbar-style, search-based panel to get at those bookmarks? We’ll use Launchbar itself. (I guess you can do this in Quicksilver or Alfred, but I haven’t tried.)

Safari’s AppleScript support is fairly poor — it doesn’t expose your bookmarks to scripts. But it does have a handy do JavaScript command, which is all that those bookmarklets do: run JavaScript.

Grab the code (stored as the bookmark’s address) and — importantly — decode it to turn entities such as %7B into {. Lop off the leading javascript: and insert it into this tiny AppleScript:

applescript:
tell application "Safari"
  set bookmarklet to "your_bookmarklet_code_here"
  set current_tab to the current tab of the front window
  do JavaScript bookmarklet in current_tab
end tell

Save that script into a new folder — mine’s in ~/Library/Scripts/Applications/Safari/Bookmarklets/

Lastly add that new folder to Launchbar’s index. I have my entry set to access on sub-search only, so to get to the bookmarklet scripts I type bookm → and I end up with this panel:

Launchbar’s list of my Safari bookmarklet scripts

And we’re set: a quick, searchable, keyboard-friendly list. If, like me, you only use the bookmarks bar for JavaScript bookmarklets you can go ahead and hide it as an added bonus.

Date suffixes in Python

A little while ago I wrote about using TextExpander to write dates, the bulk of which was given over to an Applescript that returned a long date complete with date suffixes.

Since I’ve been writing more and more Python, I thought it would be fun to rewrite it to see the difference between the two.

First, here’s my original Applescript:

applescript:
 1:  set theDate to the day of the (current date)
 2:  set theDay to the weekday of the (current date)
 3:  set theMonth to the month of the (current date)
 4:  set theYear to the year of the (current date)
 5:  
 6:  set lastChar to (the last character of (theDate as string)) as number
 7:  
 8:  if lastChar > 3 or lastChar is 0 or (theDate > 10 and theDate < 21) then
 9:    set theDate to (theDate as string) & "th"
10:  else
11:    set theSuffixes to {"st", "nd", "rd"}
12:    set theDate to (theDate as string) & (item lastChar of theSuffixes)
13:  end if
14:  
15:  return (theDay & ", " & theMonth & " " & theDate & ", " & theYear) as string

And now the Python:

python:
 1:  #!/usr/local/bin/python3
 2:  
 3:  from datetime import date
 4:  
 5:  today = date.today()
 6:  date_string = today.strftime('%A, %B #, %Y')
 7:  day = today.day
 8:  
 9:  if (3 < day < 21) or (23 < day < 31):
10:    day = str(day) + 'th'
11:  else:
12:    suffixes = {1: 'st', 2: 'nd', 3: 'rd'}
13:    day = str(day) + suffixes[day % 10]
14:  
15:  print(date_string.replace('#', day), end='')

Originally the Python script used the /usr/bin/env python3 shebang line, as I do in almost all of my scripts. Unfortunately that doesn’t work with TextExpander as its $PATH is set to /usr/bin:/bin:/usr/sbin:/sbin, so you’ll need to hard-code the path to your Python interpreter.

A date object, kind of similar to Applescript’s current date, is constructed on line 5. This is used to build a formatted date on line 6, which is complete bar the day of the month. I swap it out for a # placeholder because both strftime options for the day fall short: %d is padded with a zero, and %e is padded with a space. So instead I pull out the day separately in line 7.

The date-testing logic on line 9 is simpler and more explicit in this version, testing first for any date that takes a -th. Those dates that require a different suffix get dumped to the else clause.

A bit of modulo arithmetic on line 13 reduces the date integer to the units digit, which is used to key into a dictionary holding the suffixes.

The day and its suffix are concatenated, and are inserted into the formatted date on line 15 using the replace string method (god I love Python’s string methods) and printed, with end='' suppressing the standard newline.

Though the line count is the same between the two, the Python version has about half the characters of the Applescript. I think the Applescript’s verbosity — which I don’t help with that conditional — makes it harder to understand at a glance, while the Python is very clear, excepting the strftime format string.

As a side note, I don’t actually use either of these scripts for my long date, having given in to “Weekday month day year” without a suffix or any commas. This can be constructed using TextExpander’s own date macros, which mirror strftime formatting: %A %B %e %Y. Still, a fun exercise.