Using Ruby on Rails and the Lulu Publication API [UPDATED]

5 minute read

19 May 2010

In the past week or so I’ve been working on a little side project that is making fairly heavy use of Lulu and their new Publication API. It’s a brilliant system, and although I can’t blame them as the Lulu API is a new release, the documentation is a little thin. After a bit of poking around their Python example on Github and more into Ruby’s HTTP class, I’ve managed to find a pretty neat and easy to implement method which I’ve written about here. To get the communication between your site and the Lulu Publication API the main libraries we need are;

require 'net/https'
require 'uri'
require 'curb'

…and 4 URLs, along with the API Key (free to create one) which should be defined as constants (perhaps in config/environment.rb);

LULU_AUTHENTICATE = "https://www.lulu.com/account/endpoints/authenticator.php"
LULU_UPLOAD_TOKEN = "http://apps.lulu.com/api/publish/v1/request_upload_token"
LULU_UPLOAD = "https://pubapp.lulu.com/api/publish/v1/upload"
LULU_PUBLISH = "http://apps.lulu.com/api/publish/v1/create"
API_KEY = "ABCDEF123" # replace this with your api key

Curb is simply a hook into the brilliant cURL utility and can be installed as a gem. It’s used here as it makes it dead easy to do a multipart form post, which is required for the ‘upload’ phase of the integration.

[sudo] gem install curb

Step 1 – Obtain your authToken from the Lulu API

To use the Lulu API, all you need is a normal Lulu account, which is free. You’ll need those details to get the ‘authToken’ from the Lulu API which simply authenticates all the requests. We also need to be using https for every call to the API. Anyway, here’s the code you need for step 1 (I’ve defined my LULU_AUTH_USER and LULU_AUTH_PASS in config/environment.rb for neatness).

require 'net/https'
require 'uri'
require 'curb'

# LULU_AUTHENTICATE WITH LULU TO GET OUR auth_token
url = URI.parse(LULU_AUTHENTICATE+'?api_key='+LULU_API_KEY)
auth_args = {
'username' => LULU_AUTH_USER,
'password' => LULU_AUTH_PASS,
'responseType' => 'json'
}

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url.path)
request.set_form_data(auth_args)

response = http.start {|http| http.request(request) }
json = ActiveSupport::JSON.decode(response.body)

Now all we need to do is deal with that response. The last line of the code above simply gives us a decoded JSON object, which can be used like this:

if json['authenticated'] == true
  # we're logged in!
  auth_token = json['authToken']
else
 # something went wrong, let's review what came back and try again
 puts response.body
end

Step 2 – Upload the PDFs

This bit probably took me the longest to work out, but as I mentioned earlier, the Curb gem came to the rescue. Make sure you’ve installed that and then we can form a new request to send our cover PDF and the book PDF. Remember that this will only work if you’ve successfully received an authToken from the authentication stage of the Lulu API.

UPDATE: Since June 2010, you now need to request an Upload Token before being able to upload files:-

url = URI.parse(LULU_UPLOAD_TOKEN+'?api_key='+LULU_API_KEY)

publish_args = {
  'auth_user' => LULU_AUTH_USER,
  'auth_token' => auth_token,
  'api_key' => LULU_API_KEY
}
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url.path)
request.set_form_data(publish_args)

response = http.start {|http| http.request(request) }
responsebody = response.body.gsub("'", "\"") # Sadly our JSON parser won't work unless we do this (single quotes aren't valid JSON)

tokendata = ActiveSupport::JSON.decode(responsebody)

# LULU_UPLOAD THEM
pdf_cover = "/tmp/cover.pdf"
pdf_book = "/tmp/book.pdf"

c = Curl::Easy.new(LULU_UPLOAD)
c.multipart_form_post = true
c.http_post(
       Curl::PostField.content('auth_user', LULU_AUTH_USER.to_s),
       Curl::PostField.content('auth_token', auth_token.to_s),
       Curl::PostField.content('api_key', LULU_API_KEY), # note the new API key being sent across
       Curl::PostField.content('upload_token', tokendata['token'].to_s)# add our upload_token to the post fields
       Curl::PostField.file('cover', pdf_cover),
       Curl::PostField.file('book', pdf_book)     )

json = ActiveSupport::JSON.decode(c.body_str)

if json['written_files']
  # the files were uploaded!
  ... business logic here ...
else
  # something went wrong, review the output below
  puts c.body_str
end

It really is as simple as that! Now all we need to do is send the fairly large (but still, easy to interpret) JSON data structure to the final stage of the Lulu API.

Step 3 – Send Publish Data to Lulu Publication API

So this will probably look like the most complicated part, but it’s simply where we configure exactly what sort of book we want; meaning the size, paper/hardback, quality, availability etc etc. Let’s get straight into the data structure.

create_input =
{
  "allow_ratings" => true,
  "project_type" => "hardcover",
  "access" => "direct",

  "bibliography" =>
  {
    "title" => "My First Book",
    "authors" => [
      {
        "first_name" => "Ben",
        "last_name" => "Barnett"
      }
    ],
    "category" => 4,
    "description" => "What a fabulous book!",
    "copyright_year" => "2010",
    "publisher" => "Brushfire Design Ltd",
    "language" => "EN",
    "country_code" => "GB"

  },

  "physical_attributes" =>
  {
    "binding_type" => "casewrap-hardcover",
    "trim_size" => "SIZE_825x1075",
    "paper_type" => "regular",
    "color" => "true"
  },

  "pricing" =>
  [
      {
        "product" => "print",
        "currency_code" => "GBP",
        "total_price" => "34.99"
      }
  ],

  "file_info" =>
  {
    "cover" => [{"mimetype" => "application/pdf", "filename" => "cover.pdf"}],
    "contents" => [{"mimetype" => "application/pdf", "filename" => "book.pdf"}]
  }
}

The only couple of bits to really worry about are to ensure that you provide just the name of your PDFs, i.e. not the full path (mine were /tmp/cover.pdf) and to set the access type. Supported access levels are ‘private’, ‘public’ (available through search/browse), and ‘direct’.

Hopefully the above layout makes it clear how the JSON will be structured. Obviously at this stage we have a standard Object in Ruby, which we need to convert to a JSON and then send to the Publication API:

url = URI.parse(LULU_PUBLISH)
publish_args = {
  'auth_user' => LULU_AUTH_USER,
  'auth_token' => auth_token,
  'project' => create_input.to_json,
  'api_key' => LULU_API_KEY # note the new API key being sent across
}

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url.path)
request.set_form_data(publish_args)

response = http.start {|http| http.request(request) }

json = ActiveSupport::JSON.decode(response.body)

if json['content_id']
  # we have a book!
  ... store the json['content_id'] for reference
else
  # something went wrong
  ... review output in response.body
end

That’s it!

It may look like a lot of code above, but that really is all there is to it. I found the last stage quite fiddly, but it was more to do in terms of getting my PDF to the right DPI and exact sizes etc. You can find some sample PDFs from Lulu’s Book Specifications (at the bottom of the page).

Feel free to get in touch if you need any more help getting up and running.

Updated: