Tech Books Deals of the Day: Sinatra, Haml, and Heroku

Originally posted 2010-04-02 16:09:57

I buy books, but I swear I could quit any time. The bookshelves in my home swing low like telephone wires, topped with books perched like crows wing-to-wing and two-deep to maximize the shelf space. I’ve wedged clouds of books into the gaps above the bird-books, distending the shelves even more. Authors and genres span a gamut that rivals public libraries: Tolstoy, Petzold, non-fiction, thrillers, Bryson, Bloch, contemporary fiction, tech books.

The technical books have overflowed from my home to my office, fleeing from my wife’s rubbish bins and donation bags. One weekend, when no office manager was about, I swiped an extra bookshelf to augment my office shelf space by 50%. Another box of books sits under my desk. I read most of what I buy — though I have a deep stack in my \”to read\” pile — and I recoil at the thought of divesting my collection of any of them. As proof: the box of books sitting at my feet contains a book on Windows 3.1 programming using Borland C++ and another called Writing OS/2 REXX Programs. You just never know when you might need to refer to them, y’know?

My wife, on the other hand, abhors clutter and stuff. The aforementioned box sits at my workplace in exile, hiding from her book-burning tyranny. Other books sit in boxes in our garage, and war wages regularly in my home over which ones we should banish forever. In my mind, that’s none. In hers, that’s every single one that no one is actively reading. I try placing bookmarks at varying depths in all my books, but she didn’t fall for that. I guess you could say that she reads me . . . like a book. We argue, we weep, and the books silently witness the maelstrom that decides their fate.

For my birthday this year, we compromised: she bought me a nook, and I’m swooning. I’d already begun hoarding electronic books, but having a nook to tote and read from has fueled a surge in my books-in-bytes collection. I’ve expanded the storage on my nook, of course, and have already read several books and pieces of books on it: A Reliable Wife, my sister’s novel-in-progress (watch for it: Beatrice Andrews), enough of Programming Erlang to question mutability in general — if I can find it in bits, I shove it on there. I can now surreptitiously feed my bibliophilia fix, and my wife has no idea. My nook retains the same size, shape, and heft, whether it contains one book or the weight of the Encyclopedia Britannica. She’ll soon learn to watch the account register diligently, but for now I can buy and download and create no bulky evidence.

Three publishing sites for technical books offer daily deals on books, usually electronic books:

  1. Apress
  2. Manning
  3. O’Reilly

I set my Twitter account to follow their Deal of the Day tweets, but I follow enough folks that tweets are too easy to miss. Yes, I could subscribe to their Twitter feeds, but my track record on missing the \”free day\” for iPhone apps in my RSS reader proves I’d miss too many book deals as well. When I found myself opening a browser and going to three separate sites daily, however, I realized I needed to create a better way to check the daily ebook deals. My solution: Tech Books Deals of the Day.

In last December’s RubyJax meeting, Adam Lowe gave a presentation called Sinatra, Heroku, and You, and You. He introduced me to both Sinatra and Heroku, and I’ve since been looking for an excuse to dance with both. The Sinatra documentation and the Heroku Quickstart Guide are excellent and led me through the process, with a couple exceptions that I’ll note later. You can download the source from http://github.com/hoop33/techbooksdotd.

I encapsulated the data for a \”Deal\” in a class (deal.rb), and the rest of the code is in myapp.rb (I renamed it from techbooksdotd.rb in a misguided attempt to get my Rack app to start on Heroku, and never felt compelled to change it back — more on this later). myapp.rb gathers the data from the three sites, creates Deal objects, and then displays the view (views/index.haml). The way it gathers data differs for each site. Read about each below.

Apress

Apress shows its deal of the day at the same URL daily: http://apress.com/info/dailydeal. The price is always $10, in my experience, but that data is shown only in a graphic:

I decided not to worry about displaying the price. The pertinent information, though, is in a snippet that looks something like this:


1
<div class=‘bookdetails’> 
2         <h3><a href=‘/book/view/1590592662’>Hardening Windows</a></h3> 
3         <div class=‘cover’><a href=“/book/view/1590592662”><img width=‘125’ src=“/resource/bookcover/9781590592663?size=medium” border=‘0’ alt=“Hardening Windows” align=‘left’ /></a></div> 
4

This piece contains the book title, the relative URL for the book, and the relative URL for the book’s cover image. A regular expression snatches these bits nicely and creates our Apress Deal object:


1 matches =
/.*\<div class=’bookdetails’\>.*?\<a href=’(.*?)‘\>(.*?)\<\/a.*\<div class=’cover’\>.*?\<img.*?src=”(.*?).*/m.match(content)
2 if matches.nil?
3   return @@apress_deal
4 end
5
6 url_part, title, image_url_part = matches.captures()
7 Deal.new(:vendor => Apress, :vendor_url => http://www.apress.com/, :title => title,
8   :url => http://apress.com#{url_part}, :image_url => http://apress.com#{image_url_part})

Note the /m at the end of the regex to span the search across multiple lines.

Manning

Manning’s approach is to include a JavaScript file whose contents change daily. This file, found at http://incsrc.manningpublications.com/dotd.js, performs a document.write to write the day’s deal into the home page. Here’s a sample:


1
document.write(“March 28, 2010<BR><BR><a href=’http://www.manning.com/garcia/‘>Ext JS in Action</a><br> Get the MEAP Print edition for $25!  Enter dotd0402 in the Promotional Code box when you check out.”)

My initial approach aggressively scanned the JavaScript using regular expressions to parse various items to display. The very next day, however, their deal had two books, not one, and my regular expression ignored the second. After some thought and tinkering, I decided to capture the guts of the HTML that the JavaScript writes and display that. The code to create the Manning deal looks like this:


1 matches =
/.*?(\<a href=.*?Promotional Code box).*/m.match(content)
2 if matches.nil?
3   return @@manning_deal
4 end
5 title = matches[1]
6 Deal.new(:vendor => Manning, :vendor_url => http://www.manning.com/, :title => title,
7   :image_url => no_cover.png)

I opted not to attempt to display a cover image, though I may some day try to follow the link(s) and pull cover images from there.

O’Reilly

O’Reilly has an RSS feed, in atom format, at http://feeds.feedburner.com/oreilly/ebookdealoftheday. I use Simple RSS to parse the feed. The one challenge is to find the cover image, since it’s not in the feed, but some poking around O’Reilly’s site (ooh! two apostrophes in one word!) revealed that their cover images can be found with a URL in the format:

http://covers.oreilly.com/images/XXXXXXXXXX/cat.gif

where XXXXXXXXXX is the catalog number found in the URL for the book. We can parse it from the URL like this:

entry.link.split(/\//)[-1] # -1 gives us the last item

The code looks like this:


 1
begin
 2   rss = SimpleRSS.parse content
 3   entry = rss.entries.first
 4
 5   return Deal.new(:vendor => O’Reilly, :vendor_url => http://www.oreilly.com/,
 6     :title => entry.title, :url => entry.link,
 7     :image_url => http://covers.oreilly.com/images/#{entry.link.split(/\//)[-1]}/cat.gif)
 8 rescue SimpleRSSError
 9   return @@oreilly_deal
10 end

What about error handling?

The code creates static objects for generic non-deals that it uses if it encounters problems parsing data for any of the publishers:


1
@@manning_deal = Deal.new(:vendor => Manning, :vendor_url => http://www.manning.com/,
2   :title => No results — check Manning site, :url => http://www.manning.com/)
3 @@apress_deal = Deal.new(:vendor => Apress, :vendor_url => http://www.apress.com/,
4   :title => No results — check Apress site, :url => http://www.apress.com/)
5 @@oreilly_deal = Deal.new(:vendor => O’Reilly, :vendor_url => http://www.oreilly.com/,
6   :title => No results — check O’Reilly site, :url => http://www.oreilly.com/)

Heroku

I had a few head-scratching moments trying to push the app to Heroku, but generally the documentation worked perfectly. I had to create a .gems file containing the list of gems my app required. I resolved that issue quickly, but the one that stumped me was the error message I received when I tried to push the app to Heroku:

Heroku push rejected, no Rails or Rack app detected.

One of my attempted fixes was to change techbooksdotd.rb to myapp.rb, thinking perhaps that Heroku relied on a special naming convention to detect Sinatra apps. Convention over configuration, right? That wasn’t it. Google finally led me to a page that told me to create a file: config/rackup.ru. I created the file, but I guess the page was outdated — it directed me to run the app with a line that looks like this:

run Sinatra.application

It should have been:

run Sinatra::Application

which seems obvious now but by this point is was 2:00 AM and I was ready to screech. This was more of a five-minute hardship, though, and my app was soon running on Heroku.

Subsequent Google searches lead me to understand that current convention is not config/rackup.ru, but instead config.ru in the root directory, so I’ll probably get around to changing that soon.

Wrap-up

I enjoy the efficiency of hitting a single site daily to view book deals, and I’ll have to keep my eyes open for other publishers to add to the site. Tech Books Deals of the Day contains a single page, so I didn’t have to learn anything fancy about Sinatra to make it work. I’ll probably want to learn more about Sinatra, though, for possible future projects. Think I can buy a book for that?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.