#!/usr/bin/ruby
# Amazon sales rank monitor
# by Leonard Richardson (crummy.com)
#
# Authors! Tired of continually reloading the Amazon page for your
# books to check on their sales ranks? Let this Ruby script track your
# authorial worth over time, freeing you for more productive methods of
# procrastination. 
#
# Put this script in your crontab and it will poll Amazon at whatever
# frequency you desire. Once it gets enough data, it will start
# writing graphs and sparklines to graphically track sales of your
# book.
#
# Requires: ImageMagick or RMagick, and the gems 'gruff',
# 'sparklines', 'hpricot', and 'amazon-ecs'.
#
# Usage: SalesRank.rb [AWS Access Key ID] [path to data] [path to graphs] 
#        [ASIN1], [ASIN2], ... [ASINn]
#
# Sample run: ./SalesRank.rb  blahblahblahblah \
#               /home/leonardr/scripts/SalesRank/ \
#               /home/leonardr/public_html/graphics/sales/ \
#               0596523696 0764596543
require 'rubygems'
require 'amazon/ecs'
require 'gruff'
require 'sparklines'

class SalesReport < Array
  attr_accessor :name, :asin
  def initialize(asin, name)
    @asin, @name = asin, name
    open(self.class.filename(@asin)).each do |line| 
      self << line.split.collect { |x| x.to_f }
    end
  end

  def self.filename(asin)
    File.join(@@data_path, asin + '.txt')
  end

  # Updates sales rank for a number of products, and yields a
  # SalesReport object for each one.
  def self.update!(aws_key, data_path, *asins)
    @@data_path ||= data_path
    now = Time.now.to_i

    Amazon::Ecs.options = {:AWS_access_key_id => aws_key }
 
    asins.each() do |asin|
      puts asin
      product = Amazon::Ecs::item_lookup(asin, 
                                         opts={'ResponseGroup' => 
                                           'Small,SalesRank'})
      item = product.items[0]
      asin = item.get('asin')
      name = item.get('title')
      sales_rank = item.get('salesrank')

      # Update the file with the new sales rank.
      open(self.filename(asin), 'a') do |f| 
        f << now << ' ' << sales_rank << "\n"
      end

      # Parse the file into an SalesReport object and yield it.
      yield self.new(asin, name)
    end
  end

  # Make a Gruff graph for the sales of this product.
  def make_graph(graph_path, limit=nil)
    return if size < 2
    if not limit
      limit ||=size
      suffix = ''
    else
      suffix = "-#{limit/24}"
    end
    g = SalesRankGraph.new(800)
    g.title = "Salesrank over time: #{name}"	
    g.theme_37signals
    g.colors = ["black"]
    g.title_font_size = 20
    g.hide_legend = true
    g.hide_title = true
    g.hide_line_markers = true
    g.data(@name, self[-limit..self.size].collect { |date, rank| rank ? 1/rank : nil }.compact)
    label_hash = {}      
    [self.size-limit, (limit/2).round, limit-1].each do |i|
      label_hash[i] = Time.at(self[i][0]).strftime('%m/%d/%Y') if self[i]
    end
    g.labels = label_hash    
    g.write(File.join(graph_path, "#{asin}-salesrank#{suffix}.png"))    
  end

  # Make a sparkline for the sales of this product.
  def make_sparkline(graph_path, time_units=30, samples_per_unit=24)
    path = File.join(graph_path, "#{@asin}-salesrank-sparkline.png")

    # Gather a sample of the data
    sample = []
    (size-(time_units*samples_per_unit)-1).step(size-1, samples_per_unit) do |i| 
	sample << 1/self[i][1] if self[i]
    end
    return if sample.size < 2
    Sparklines.plot_to_file(path, scale(sample), :type => 'smooth', 
                            :line_color => 'black')
  end

  # Scale data so that the smallest item becomes 0 and the largest becomes 
  # 100.
  def scale(data, bottom=0, top=100)
    min, max = data.min, data.max
    scale_ratio = (top-bottom)/(max-min)
    data.collect { |x| bottom + (x-min) * scale_ratio}
  end
end

class SalesRankGraph < Gruff::Line
  def draw_line_markers
  end
end

# Command-line interface
#
amazon_key, data_path, graph_path, *asins = ARGV
SalesReport.update!(amazon_key, data_path, *asins) do |product|
  product.make_graph(graph_path)
  product.make_graph(graph_path,30*24)
  product.make_sparkline(graph_path)
end

