#!/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