Affiliate links via the iTunes Search API

I started writing these scripts a couple of weeks ago, and they weren’t supposed to take very long to put together. But I had to learn some things along the way, and then I got caught up in the statusbar cleaning scripts, so they kept getting delayed. Better late than never, though; I expect to use these scripts lot.

I’ve long had a TextExpander snippet that gets the URL of the frontmost Safari window and, if it’s an link, turns it into an affiliate link. My usual workflow for putting affiliate links into a blog post was

  1. Find the page for the product in Safari.
  2. In BBEdit, call my Markdown reference link script, which puts up a dialog box for the URL.
  3. Use the TextExpander snippet to insert the iTunes affiliate link.

This works pretty well if it’s easy to find the product’s iTunes web page. For quite a while, I was using Google’s site-specific searching to speed that process. I even made a TextExpander snippet for, so I could add the site-specific search terms quickly.

A couple of months ago, though, I noticed that Google’s search results weren’t turning up useful pages. The most common problem was that Google would find the product, but would return the link for the Canadian or UK iTunes Store, which isn’t very useful to me. This seemed fishy. Given the population disparity, I find it hard to believe that those stores should be ranked higher than the US store. But maybe Google’s noticed how often I watch YouTube videos of QI and decided I’m a British ex-pat.

Whatever the reason, I changed my site-specific search string to with the hope that that would fix the problem. It worked, inasmuch as the results were now limited to the US store, but the top link, assuming it was to the correct product at all, was almost always to a Spanish-language page. An English-language link to the product was seldom on the front page of results.

Don’t believe me? Here’s a search for Pythonista:

Google search for Pythonista in iTunes

I suppose I could’ve switched search engines, but I decided to use the iTunes Search API instead. I’d already used the python-itunes module in another script, so I figured it’d be simple to use it again.

Unfortunately, python-itunes had some bugs and limitations that I soon bumped up against. I had to fork it and make the changes and additions necessary to do what I wanted. In the process, I had to learn more about the iTunes Search API than I originally intended. Oh, well. That which does not kill us makes us stronger.1

Once I got python-itunes tamed, writing the searching and link-making scripts was pretty easy, mainly because I was able to crib code from other scripts I’d written. First came a Python script, isearch, that takes a search string as an argument and returns the URL of a match. It finds the top 25 matches for the search string and presents them to the user in a dropdown menu (via CocoaDialog). Whichever item the user selects is the one whose URL is returned.

Because the iTunes stores are so big, the search can be limited to specific media types through command line options:

Here’s the script:

 1:  #!/usr/bin/python
 3:  import sys
 4:  from subprocess import *
 5:  import itunes
 6:  from getopt import getopt
 8:  # My affiliate ID.
 9:  myID = '10l4Fv'
11:  # Set the environment for calling CocoaDialog.
12:  dialogpath = '/Applications/'
13:  dialogenv = {'PATH': dialogpath}
15:  # Search for specified item.
16:  opts, args = getopt(sys.argv[1:],
17:                      'bimsaf',
18:                      ['book', 'ios', 'mac', 'song', 'album', 'movie'])
19:  if not opts:
20:    items =[0], order='popular', limit=25)
21:  for o, a in opts:
22:    if o in ('-b', '--book'):
23:      items = itunes.search_book(args[0], order='popular', limit=25)
24:    elif o in ('-i', '--ios'):
25:      items = itunes.search_ios(args[0], order='popular', limit=25)
26:    elif o in ('-m', '--mac'):
27:      items = itunes.search_mac(args[0], order='popular', limit=25)
28:    elif o in ('-s', '--song'):
29:      items = itunes.search_track(args[0], order='popular', limit=25)
30:    elif o in ('-a', '--album'):
31:      items = itunes.search_album(args[0], order='popular', limit=25)
32:    elif o in ('-f', '--movie'):
33:      items = itunes.search_movie(args[0], order='popular', limit=25)
35:  # Construct lists of names and URLs.
36:  names = [ '%2d  %s' % (n+1, for (n, i) in enumerate(items) ]
37:  urls = [ i.url for i in items ]
39:  # Display a dialog asking for which link to use.
40:  chosen = Popen(['CocoaDialog', 'standard-dropdown', '--exit-onchange', '--title', 'iTunes Search Results', '--text', 'Choose an item', '--items'] + names, stdout=PIPE, env =dialogenv).communicate()[0].split('\n')
42:  # Print the URL for the chosen item with my affiliate ID added.
43:  if int(chosen[0]) == 1 or int(chosen[0]) == 4:
44:    itemURL = urls[int(chosen[1])]
45:    if '?' in itemURL:
46:      idURL = '%s&at=%s' % (itemURL, myID)
47:    else:
48:      idURL = '%s?at=%s' % (itemURL, myID)
49:    sys.stdout.write(idURL) 

If you want to adapt this for your own use, the first thing you should do is change the affiliate code in Line 9. Unless, of course, you want me to get the commission for your sales.

There are probably only a couple of things about this script that warrant an explanation:

  1. Why do I use the old-fashioned getopt module when the much more feature-filled argparse is available? The answer is twofold. First, getopt’s simplicity makes it easy to understand. Second, argparse is not Python’s first attempt to create a super whizzy option parsing library, and I worry that it won’t be the last. I don’t want to put the time in to learn a module only to have it deprecated a few years later. Been there, done that.
  2. Why do I go to the bother of putting numbers in front of the search results before presenting them to the user? The numbers don’t get used elsewhere else in the script.

    Search results dropdown

    As I was testing the code, I learned that CocoaDialog’s standard-dropdown would “eat” identically named items in the list it was given, presenting only the item that came later in the list. This could eliminate the top search results from the menu—definitely not what I wanted. That, in fact, is exactly what happened when I tested it on Revolver—the Beatles album wouldn’t show up in the menu, even though I knew it was the top hit.

    Putting a number in front of the item name isn’t the best way to solve this problem, I’ll grant you, but it was easy to implement so I could get the script working. I’ll try to come up with a better way to distinguish items after I’ve used the script longer.

Update 11/18/13
I’ve made it easier to decide which one to choose by adding the artist (author, developer, director) to the item’s name in the dropdown menu.

Improved search dropdown menu

Notice that I’ve retained the leading number because sometimes, even with the artist included, there will still be results that look identical but are, in fact, different. I’m not sure how I would go about distinguishing one of T-Pain’s non-deluxe versions of rEVOLVEr from the other, but I’m not going to lose sleep over it.

Another improvement is that the dropdown menu doesn’t appear at all if there’s only one search result. It’s a waste of time.

The improved version of isearch is in its GitHub repository.

OK, with isearch written, it’s time to make an AppleScript that uses it to create the link in BBEdit. It follows the general outline of my other link-making scripts, taking the selected text, feeding it to isearch, and using the result to make the link.

 1:  tell application "Finder" to set cPath to home as text
 2:  set binPath to (quoted form of POSIX path of cPath) & "Dropbox/bin/"
 4:  set searchOptions to {"--mac", "--ios", "--album", "--song", "--book", "--movie"}
 5:  set searchNames to {"Mac software", "iOS software", "Album", "Song", "Book", "Movie"}
 7:  tell application "BBEdit"
 8:    set myText to contents of front document
 9:    set myRef to do shell script binPath & "bbstdin " & quoted form of myText & " | " & binPath & "nextreflink"
11:    if length of selection is 0 then
12:      beep
13:    else
14:      -- Ask what kind of search to perform.
15:      set searchType to (choose from list searchNames with title "iTunes Search" with prompt "Media type:") as text
16:      if searchType is "false" then
17:        beep
18:      else
19:        repeat with i from 1 to count of searchNames
20:          if item i of searchNames is searchType then
21:            set option to item i of searchOptions
22:            exit repeat
23:          end if
24:        end repeat
26:        -- Get the selection and find the iTunes result.
27:        set myTerms to selection as text
28:        set myURL to do shell script binPath & "isearch " & option & " " & quoted form of myTerms
30:        -- Turn selected text into link and put cursor after the reference.
31:        add prefix and suffix of selection prefix "[" suffix "]" & "[" & myRef & "]"
32:        select insertion point after last character of selection
33:      end if
35:    end if
37:    -- Add the reference at the bottom of the document and reset cursor.
38:    set savePt to selection
39:    select insertion point after last character of front document
40:    set selection to "[" & myRef & "]: " & myURL & return
41:    select savePt
43:  end tell

The choose from list in Line 15 and the repeat block in Lines 19–24 set the media type option for isearch. It brings up a dialog that looks like this:

Choose from list

If you think there’s something clunky about using this type of selection UI in one place and a dropdown menu in another, you’re not alone. But I’m lazy and this was the quickest way to get a working script.2

The AppleScript, named iTunes Search Link, is saved in BBEdit’s Scripts folder, and I’ve assigned it a keyboard shortcut of ⌃⌥⌘I. My workflow for adding an affiliate link is now

  1. Select the text, usually the name of the app, book, or album.
  2. Hit ⌃⌥⌘I.
  3. Select the media type to search on.
  4. Select the search result I want.

This has four steps instead of three, but goes much faster because there’s no switching from Safari to BBEdit. And, of course, I get results that actually work.

These scripts, along with the bbstin and nextreflink programs that are called by the AppleScript, are in this GitHub repository. I’ll be updating the repository as I make improvements. Error handling would be a nice addition, don’t you think?

  1. ♫ There’s nothin’ Nietzsche couldn’t teach ya ’bout the raisin’ of the wrist. ♫ 

  2. Have you noticed a theme developing throughout this post?