jump to navigation

Book info script May 26, 2007

Posted by samwyse in Scripting.
trackback

The Problem

I like books. I like them a lot. Over the years, I’ve collected a large number of books. And now I want to get rid of some of them.

A lot of them I’m giving to Goodwill, but there are several sets that all deal with one topic or another. As I was packing some of these for transport, it occured to me that someone else may want to buy some of my books, so I needed to write an ad. Of course, good ads are packed with useful information about the product being sold, and collecting that data for a collection of random books is a bit labor intensive.

So, I wrote a Python script.

The idea is, you enter a bunch of ISBN numbers (10 digits, easy to find on any book with a barcode) and the script runs out to Amazon and finds out as much as it can about them. It then assembles the text of an ad: One line saying how many books there are, one line for each book detailing its title, author and original cost, and a final line saying how much you want to sell them for. (The latter is based on the total of the books’ used prices.) Next, the script downloads cover art for all of the books and assembles it into a single image showing the books laid out in a row. (This is because craigslist allows no more than four images per posting, and I often have more than four books in a set.)

What’s important is what this script doesn’t do. I decided not to load it up with a bunch of command line options; I realized that any changes that I make would be used going forward, and source code isn’t that hard to modify. If I ever need a different version for eBay, then I’ll just fork the source.

The Script

#!/usr/bin/python

"""
Creates a 'for sale' posting (suitable for craigslist, or maybe eBay) for a
list of books using information gathered from amazon.com.  Simply supply a
list of ISBN numbers on the command line, then cut and paste the results into
your web browser.

Expected errors:

*** No module named amazon
This script needs the 'amazon.py' module.  It may be obtained from
http://svn.plone.org/svn/collective/ATAmazon/trunk/amazon.py or
http://www.josephson.org/projects/pyamazon/files/pyamazon-0.65.zip

*** No module named PIL
This script needs the Python Imaging Library.  It may be obtained from
http://www.pythonware.com/products/pil/

"""

import sys

AMZN_LICENSE_KEY = 'XXXXXXXXXXXXXXXXXXXX'  # must get your own key!
OUTPUT_FILENAME = 'posting'
IMAGE_BACKGROUND = (0xAA, 0xAA, 0xAA)  # light gray
FRAME_SIZE = 8
WIDTH_LIMIT = 640
HEIGHT_LIMIT = 960

def die(msg):
  """die(msg) -> Doesn't return.
  Just like Perl, this prints a message to stderr and exits, but with a
  twist!  Before printing, we scan __doc__ for the message for a more
  verbose version to print instead.
  """
  docList = __doc__.splitlines()
  try:
    del docList[0:docList.index('*** %s' % msg)]
  except ValueError:
    docList = ('', msg)
  try:
    del docList[docList.index(''):]
  except:
    pass
  print >>sys.stderr, '\n'.join(docList[1:])
  sys.exit(1)

def joinEn(theList):
  """joinEn(list) -> string
  Join the elements of a list using normal English syntax. 
  """
  if hasattr(theList, '__iter__'):
    if len(theList) > 1:
      last = theList.pop()
      return ', '.join(theList) + " and " + last
    return theList[0]
  return theList

def stepOne(isbnList):
  """stepOne(isbnList) -> imageUrls
  Looks up a list of ISBN numbers at amazon.com and returns a list of URLs
  of cover images.  As an important side-effect, it also prints a few facts
  about each book to the file 'posting.txt' in the current directory.
  """
  from time import sleep
  try:
    from amazon import setLicense, searchByPower
  except ImportError, msg:
    die(msg)

  # set the Amazon developer key
  setLicense(AMZN_LICENSE_KEY)

  totalPrice = 0
  posting = []
  imageUrls = []
  for isbn in isbnList:
    sleep(1.1)
    searchResults = searchByPower('isbn:'+isbn)
    for book in searchResults:
      totalPrice += float(book.UsedPrice[1:])
      posting.append("\"%s\" by %s (%s new)" % (
        book.ProductName,
        joinEn(book.Authors.Author),
        book.ListPrice,
      ))
      imageUrls.append(book.ImageUrlMedium)

  # create a description of what we found
  postingText = open(OUTPUT_FILENAME + '.txt', 'wt')
  print >>postingText, '\n'.join(
	["%d books\n" % len(posting)] + posting +
	["\nThis set can be yours for just $%.2f." % totalPrice]
  )
  postingText.close()

  # return the list of URLs
  return imageUrls

def stepTwo(imageUrls):
  """stepTwo(imageUrls) -> imageFiles
  Given a list of URLs, saves each as a file in the current directory, and
  returns the list of filenames.
  """
  from urllib2 import urlopen
  from urlparse import urlparse
  # parse each url, keeping only the path
  # split the path, keeping only the last piece
  imageFiles = [ urlparse(url)[2].rsplit('/', 2)[-1] for url in imageUrls ]
  # download the files
  for url, fname in zip(imageUrls, imageFiles):
    try:
      img_bin_data = urlopen(url).read()
      new_img_file = open(fname, 'wb')
      new_img_file.write(img_bin_data)
      new_img_file.close()
    except: pass
  # return the list of names
  return imageFiles

def stepThree(imageFiles):
  """stepThree(imageFiles) -> None
  Given a list of image files, creates a single gridded image consisting
  of the images arranged in columns and rows.  The resulting image is
  saved to the file 'posting.jpg' in the current directory.
  """
  try:
    from PIL import Image
  except ImportError, msg:
    die(msg)
  # load all of the images
  imageList = [ Image.open(fname) for fname in imageFiles ]
  # deduce an optimum number of rows and columns
  for nr in xrange(1, len(imageList)):
    nc = (len(imageList) + nr - 1) // nr
    maxWidth  = max([i.size[0] for i in imageList]) + FRAME_SIZE
    maxHeight = max([i.size[1] for i in imageList]) + FRAME_SIZE
    size = (maxWidth * nc + FRAME_SIZE, maxHeight * nr + FRAME_SIZE)
    if size[0] < WIDTH_LIMIT: break
    if size[1] > HEIGHT_LIMIT: break
  # create the enclosing image
  combo = Image.new('RGB', size, IMAGE_BACKGROUND)
  offsetList = [ (maxWidth * x + FRAME_SIZE//2, maxHeight * y + FRAME_SIZE//2)
		for y in xrange(nr) for x in xrange(nc) ]
  for image, offset in zip(imageList, offsetList):
    combo.paste(image, (
		offset[0] + (maxWidth  - image.size[0]) // 2,
		offset[1] + (maxHeight - image.size[1]) // 2,
    ))
  # save the results
  combo.save(OUTPUT_FILENAME + '.jpg')

# For testing purposes, here are some lists of ISBNs and the names
# (as of 5/25/07) of their associated cover images.
threeBooks = ( '0345333926', '0671741926', '0671695320' )
threeImages = ( '21O8IWsJopL.jpg', '214NPWFP3NL.jpg', '21R15WKVYBL.jpg' )
fiveBooks = ( '0688149529', '0671541390', '0914728490',
		'0939616173', '0960607013' )
fiveImages = ( '21EDRZPZP0L.jpg', '21VNTBV2SVL.jpg', '21FF9NXVSBL.jpg',
		'21CA607WJ1L.jpg', '41W5GRS54AL.gif' )

def main(isbnList=None):
  """main(isbnList) -> None
  The idea is, you enter a bunch of ISBN numbers (10 digits, easy to find
  on any book with a barcode) and the script runs out to Amazon and finds
  out as much as it can about them. It then assembles the text of an ad:
  One line saying how many books there are, one line for each book detailing
  its title, author and original cost, and a final line saying how much you
  want to sell them for. (The latter is based on the total of the books’
  used prices.) Next, the script downloads cover art for all of the books
  and assembles it into a single image showing the books laid out in a grid.
  (This is because craigslist allows no more than four images per posting,
  and I often have more than four books in a set.)
  """
  if isbnList is None: isbnList = sys.argv
  imageUrls = stepOne(isbnList)
  imageFiles = stepTwo(imageUrls)
  stepThree(imageFiles)

if __name__ == '__main__':
  sys.exit(main())
Advertisements

Comments»

No comments yet — be the first.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: