Amazon, Python, and Ellison

Let’s start with an apology. If you follow me on Twitter, you’ll find me tweeting links to Kindle books more frequently than I did in the past. This is not because I’m desperate for the 8¢ commission on each $2 sale book you buy, but because Amazon turns off my ability to use its product advertising API if I go more than 30 days without a sale. Apparently, this policy has been in place for years,

Note that your account will lose access to Product Advertising API if it has not generated referring sales for a consecutive 30-day period.

but I don’t think Amazon was enforcing it.

Anyway, I ran into this a week or so ago when my shortcut for tweeting affiliate links failed because it had been a couple of months since I last used it. I had to create the tweet by hand (yes, like an animal) and promised myself I’d never let that happen again.

It also reminded me that I’d never written about the changes I had to make to my Kindle tweeting shortcut after iOS 13 was released. So here goes.

In early October, I wanted to tweet a link to a book that was on sale, but my shortcut, which used a combination of Shortcuts and Pythonista, kept failing. I traced the problem to a bug in passing parameters (in this case, the Amazon product ID code of the book) to Pythonista. Whether this was a bug in the new version of Shortcuts or a change in Shortcuts that exposed a bug in Pythonista, I didn’t know and didn’t care to dig into. I just wanted a Shortcut that worked.

The Pythonista part of this workflow handled the communication with the Amazon API and included HMAC encoding of the request with my API account keys. I thought about rewriting this in JavaScript using Scriptable, but I didn’t want to rewrite a bunch of code in a language I don’t much like. And besides, it wasn’t Python that was failing me, it was Pythonista and its connection to Shortcuts. The easier and more reliable solution was to run the Python script somewhere other than iOS.1

I set up a CGI script on a web server to take the place of Pythonista. Scoff if you like at my invocation of 90s web tech, but it was the perfect solution, requiring only a few edits to the Pythonista script and fitting my out-of-date web development skillset. Here’s the script:

python:
 1:  #!/usr/bin/python3
 2:  
 3:  import cgi
 4:  import requests
 5:  from datetime import datetime
 6:  import base64, hashlib, hmac
 7:  import urllib.parse
 8:  from bs4 import BeautifulSoup
 9:  import sys
10:  import re
11:  import json
12:  
13:  # Get the Amazon ASIN
14:  form = cgi.FieldStorage()
15:  itemASIN = form['asin'].value
16:  
17:  # Date and time
18:  t = datetime.utcnow()
19:  timeStamp = urllib.parse.quote(t.strftime('%Y-%m-%dT%H:%M:%SZ'))
20:  
21:  # Parameters
22:  associateTag = 'my-associate-tag'
23:  accessKey = 'my-access-key'
24:  secretKey = b'my-secret-key'
25:  parameters = ['Service=AWSECommerceService',
26:    'Operation=ItemLookup',
27:    'ResponseGroup=Large',
28:    'ItemId={}'.format(itemASIN),
29:    'AWSAccessKeyId={}'.format(accessKey),
30:    'AssociateTag={}'.format(associateTag),
31:    'Timestamp={}'.format(timeStamp)]
32:  parameters.sort()
33:  paramString = '&'.join(parameters)
34:  
35:  # Generate signature from parameters and secret key
36:  unsignedString = '''GET
37:  webservices.amazon.com
38:  /onca/xml
39:  {}'''.format(paramString)
40:  signedString = hmac.new(secretKey, msg=unsignedString.encode(), digestmod=hashlib.sha256).digest()
41:  sig = urllib.parse.quote(base64.b64encode(signedString))
42:  
43:  # Generate URL from parameters and signature
44:  url = 'http://webservices.amazon.com/onca/xml?{}&Signature={}'.format(paramString, sig)
45:  
46:  # Get image information
47:  resp = requests.get(url)
48:  xml = resp.text
49:  # print(xml)
50:  
51:  # Extract the information from from the XML
52:  # response and build a dictionary
53:  soup = BeautifulSoup(xml, 'html5lib')
54:  info = {}
55:  info['imgURL'] = soup.item.largeimage.url.text
56:  info['author'] = soup.item.itemattributes.author.text
57:  info['title'] = soup.item.itemattributes.title.text
58:  info['link'] = 'https://www.amazon.com/dp/{}/?tag={}'.format(itemASIN, associateTag)
59:  
60:  print('Content-Type: application/json')
61:  print()
62:  print(json.dumps(info))

I won’t describe the fundamental logic—you can get that in this post from last year. The only changes I had to make were at the beginning and end of the script to get the input and output to conform with the requirements of web use.

  1. The ID code for the book (what Amazon calls an ASIN) is passed to the script through a GET request, like this,

    https://server.com/cgi-bin/amazon-script.cgi?asin=0691181624
    

    and Lines 14–15 use Python’s cgi module to extract the ASIN string.

  2. The JSON output is preceded by a Content-Type header in Lines 60–62.

As before, the JSON output includes the title, author, a link to the book’s cover image, and an affiliate link to the book itself.

The script is called by this shortcut:

Tweet Kindle Book shortcut

The shortcut differs from the original in more ways than just the call to a CGI script. Instead of trying to build and send the tweet directly, this new version puts the tweet text in Drafts, which makes it easier for me to edit the tweet before sending it. Also, I can write the tweet when I first see that an interesting book is on sale—typically early in the morning—and send it out later in the day. The downside is that the book’s cover image is saved in Photos and I have to add it to the tweet manually before sending. Overall, though, I think this is a better solution.

The logic of the Shortcut is fairly simple. It’s meant to be called from the Share Sheet in Safari when the book of interest is the current page. The first two steps extract the ASIN from the URL of the page. I know of three places the ASIN can be within the URL, and the regular expression in the first step accounts for all three of those ways. The shortcut then calls the CGI script using the ASIN, collects its output, and converts the JSON into a dictionary for later reference.

One of the items in the dictionary is the URL of the book’s cover image. The next two steps get that image and save it to the Recents folder in Photos.

The shortcut then goes interactive, asking me for the price of the book (getting this from the Amazon API is a lot more difficult than getting the other information, and I decided it wasn’t worth the effort). It then constructs the tweet text using my answer combined with the other information from the dictionary. Finally, it saves the tweet text in Drafts.

After running the script, I can edit the tweet in Drafts to add any extra comments I want and then use the the Tweet with Tweetbot action to send the text to Tweetbot. That’s where I add the the image and send it off. It is, I think, a reasonable combination of automation and manual work.


Moving slightly off-topic…

After a few sales last week unlocked my access to the Amazon API, I used the shortcut yesterday for this tweet:

Shatterday tweet

Of the five retweets you see at the bottom of the screenshot, two came from friends Jason Snell and David J. Loehr, whose appreciation of Ellison (and “Jeffty is Five”) you can hear on this episode of The Incomparable. There’s also a lovely Jeffty/Ellison-based pun David wrote in the very first Incomparable Radio Theater episode.

But with all due respect to Jason and David, the retweet I appreciated the most was the one that came from The Magazine of Fantasy and Science Fiction, a magazine I devoured in my prime sf-reading days. As I said in the tweet, I read the story “Jeffty is Five” when it was initially published in F&SF back in 1977. This issue with a fun cover by Kelly Freas:

Harlan Ellison issue of Fantasy and Science Fiction

Image from the Interactive Speculative Fiction Database.

Set me back a whole dollar, but it was worth it.

“Jeffty is Five” struck a chord with me because of my father. He was about Ellison’s age and was a big fan of the golden age of radio, a bit of which he passed on to me. So even though I was just 16 when I read “Jeffty,” I knew all the references because of Dad and saw him in the narrator.

I say Dad was a fan of the golden age of radio, but he was a fan of radio, period, even when it wasn’t what it had been. He always preferred it to TV and movies and spent a lot of time and money in his later years trying to find decent shows to listen to. He would have loved the age of podcasts.


  1. This is a common theme in my use of iOS as a “real computer.” I like my iPad Pro and don’t have a laptop anymore, but if I couldn’t use Prompt and the terminal emulator in Textastic to run commands on Macs and Linux servers, it would be impossible for me to get my work done on it.