DIY – Your Rottentomatoes Ratings System for Redbox Movies

NOTE: Finally developed a Chrome plugin called Tomato Box out of this post! You can download it from Chrome Web Store, here. 

Update: My friend Pemba has made several enhancements to it, and ported the plugin for Firefox browser. You can download the source code of the Firefox add-on from here.

Every Friday, I rent a dvd from a Redbox kiosk near our place. I would always find a good movie, provided I spent a lot of time looking up the ratings of the available movies in rottentomatoes.com or imdb. It would have been nice if Redbox.com provided imdb or rottentomatoes ratings side by side.

To cut short the time I spent finding the right movie, I created a chrome plugin that displayed the ratings from rottentomatoes.com on the redbox.com movie pages. The image above is the screenshot of the ratings created by the plugin.

Rottentomatoes.com provides a few easy-to-use RESTful APIs for accessing movie information including the ratings from their data store.(here). They call their ratings as critics_score([C] in the screenshot) and audience_score([A]). One of the APIs searches the database and returns ratings, casts, year of release, and several other information for a movie. The API is invoked passing the name of the movie as a parameter. All API invocations are authenticated based on an API key passed with each call. The API key is obtained by registering with the rottentomatoes’ developers’ website.

I used a content script to parse the name of the movie from Redbox’s movie pages, and passed it to a node.js http server runnning locally. The call rate for my API key for rottentomatoes.com is limited to 10 calls/sec and 10000 calls per day. So I used the node.js server as a poor man cache so as to return the ratings locally before hitting the rottentomatoes datastore. I also used a simple scheduler in the content script to schedule less than 4 calls per second.

The complete sourcecode of this prototype is available in Git, here. The instructions to install the plugin are similar to those in my previous post, here.

Here is the code for the node server. The poor man cache is a set of files after the name of the movie.

//change API_KEY to your auth key obtained from rottentomatoes.com
var API_KEY = "YOUR_API_KEY";

var sys = require('util'),
    http = require('http'),
    fs = require('fs'),
    index;

function respond(content, response, movie, stringify)
{
    var data;
    if(content == null){
        content = new Object();
        content.critics_score = "none";
        content.audience_score = "none";
        content.id = "";
    }
    if(stringify == false){
        data = content;
    }else{
        data = JSON.stringify(content);
    }
    console.log("Sending data:"+ data);
    response.writeHead(200, {'Content-Length': data.length, 'Content-Type': 'text/json'});
    response.write(data);
    response.end();
    return data;
}

function findRanking(movie, response)
{
    try{
    var moviePath = "./movies/" + movie;
    fs.lstat(moviePath, function(err, stats){
      //return the ranking by reading the file
      if(stats && stats.isFile()){
        var content = fs.readFileSync(moviePath);
        var data = respond(content, response, movie, false);
        console.log("Found Movie:" +movie +" data:" + data);
      }else{
         //else query rotten tomatoes directly
         getRankings(movie, function(id, ratings){
              if(id == null){
                   respond(null, response, movie, true);
                   return;
              }
              if(ratings){
                   if(ratings == null){
                      respond(null, response, movie, true);
                      return;
                   }
                   console.log("critics ranking is:" + ratings.critics_score + ". audience ranking is:" + ratings.audience_score);
                   var content = new Object();
                   content.critics_score = ratings.critics_score;
                   content.audience_score = ratings.audience_score;
                   content.id = id;
                   content.name = movie;
                   var jsonContent = respond(content, response, movie, true);
                   try{
                       var ifile = fs.openSync(moviePath, "w");
                       fs.writeSync(ifile, jsonContent);
                       fs.close(ifile);
                   }catch(err){
                       console.log("Exception caught:" + err);
                   }
              }
         });
      }
    });
    }catch(err){
        console.log("Exception:" + err);
        respond(null, response, movie, true);
    }
}

function getRankings(movie, callback) {
    console.log("movie is :" + movie);
    var urlPath = "/api/public/v1.0/movies.json?q=" + encodeURIComponent(movie) + "&page_limit=5&page=1&apikey=" + API_KEY;
    var options = {
      host: 'api.rottentomatoes.com',
      port: 80,
      path: urlPath,
      method: 'GET'
    };
    var msgBody = "";
    var req = http.request(options, function(res) {
            console.log('STATUS: ' + res.statusCode);
            res.setEncoding('utf8');
            res.on('data', function (chunk) {
                msgBody += chunk;
            });
            res.on('end', function(){
                if(msgBody!= "" && res.statusCode == 200){
                    var movieObject = JSON.parse(msgBody);
                    if(movieObject.movies == null || movieObject.movies.length == 0){
                       callback(null, null);
                       return;
                    }
                    if(movieObject.movies.length == 1){
                      // only a single movie - lets assume that this is THE movie we are looking for
                      var item = movieObject.movies[0];
                      callback(item.id, item.ratings);
                      return;
                    }

                    // tallying the results with year of release, etc.  will yeild more accurate information
                    for(var i = 0; i<movieObject.movies.length; i++){
                        var item = movieObject.movies[i];
                        if(movie == item.title.toUpperCase()){
                           var id = item.id;
                           console.log("Id of the movie is:" + id);
                           callback(id, item.ratings);
                           return;
                        }else{
                          console.log("title=[" + movie + "] and movie=[" + item.title + "] does not match.");
                        }
                    }
                    callback(null, null);
                }else{
                    callback(null, null);
                }
            });
    });

    req.on('error', function(e) {
            console.log('problem with request: ' + e.message);
    });

    req.end();
}

function normalize(name)
{
    var decoded  = decodeURIComponent(name);
    var replaced = decoded.replace(/\(.*\)/,"");
    var trimmed = replaced.trim().toUpperCase();
    return trimmed;
}
http.createServer(function (request, response) {
  console.log(request.url);
  var result = request.url.match(/^\/(.*\.js)/);
  if(result) {
      response.writeHead(200, {'Content-Type': 'text/javascript'});
      response.write(readFile(result[1]));
      response.end();
  } else if(request.url.indexOf("add?&id=") != -1){
      var vals = request.url.split("=");
      console.log(vals[vals.length-1]);
      var nMovie = normalize(vals[vals.length-1]);
      findRanking(nMovie, response);
  }
  else{
      response.writeHead(200, {'Content-Type': 'text/html'});
      response.write(readFile('index.html'));
      response.end();
  }
}).listen(8989);

console.log('Server running at http://127.0.0.1:8989/');

Here is the code for the content script. The trick was to control the rate of calls to the server, which was achieved using windows.setTimeout API with a queue of parsed movies.

function MovieHandler(target){
    this.target = target;
}

MovieHandler.prototype.getTarget = function(){
    return this.target;
}

MovieHandler.prototype.callback = function(content){
     if(content){
        console.log("critics_score is :" + content.critics_score );
        console.log("audience_score is :" + content.audience_score);
        $(this).append("<em><strong>[C]</strong>" + content.critics_score + " <strong>[A]</strong>" + content.audience_score + "</em>");
     }
};
var doAjax = function(dataString, onSuccess){
    $.ajax({
        type: "GET",
        url: "http://127.0.0.1:8989/add?",
        data: dataString,
        context: onSuccess.getTarget(),
        success: onSuccess.callback
    });
};

var movieHandlers = new Array();
$.each( $(".box-wrapper"), function(index, obj){
        var movieHandler = new MovieHandler(obj);
        movieHandlers.push(movieHandler);
});
function schedule(){
    console.log("schedule invoked. queue size=" + movieHandlers.length);
    var i =0;
    var movieHandler;
    while((i < 2) && (movieHandlers.length > 0)){
        movieHandler = movieHandlers.shift();
        var name = $(movieHandler.getTarget()).attr('name');
        if(name != null){
            console.log("Querying movie:" + name);
            doAjax("id=" + name, movieHandler);
            i++;
        }
    }
    if(i == 2){
        setTimeout(schedule, 2000);
    }
}
console.log("scheduling timer 2 sec");
//found that ideally 2 calls/per sec is optimal.
//playing safe.
setTimeout(schedule, 2000);

Finally, the code for the plugin’s manifest. I added the URL of the node server ( http://127.0.0.1:8989) in the permissions section which was required for cross-domain XHR.

{
    "name": "REDBOX ranking",
        "description": "Parses videos out of Redbox and ranks",
        "version": "0.1",
        "permissions": ["contextMenus", "http://127.0.0.1:8989/"],
        "content_security_policy": "default-src 'self'",
        "content_scripts": [
            {
                "matches": ["*://*.redbox.com/*"],
                "js": ["jquery.min.js","parser.js"]
            }
        ]

}
Advertisements

About Amar Deka

Software Engineer
This entry was posted in Open Source, Technology, Thoughts and tagged , , , . Bookmark the permalink.

5 Responses to DIY – Your Rottentomatoes Ratings System for Redbox Movies

  1. Matt says:

    Fabulous. I’ve been trying to write my own chrome plugin to do some simple text search and replace. I know nothing about javascript but I think this will help me get started.

  2. Amar Deka says:

    Thanks Matt. Here is the trunk for the prod version- https://github.com/cvrepos/Tomato-Box-Plugin

  3. Matt says:

    Looking at the latest code, seems like you moved away from your node.js “poor man’s cache” and instead are using a generic JavaScript object (i.e.: movie_cache = {};). Is that accurate?

    I was just reading about HTML5 localStorage. Do you think that would be better? I’m not sure but I’m guessing your current cache gets erased when the user closes all browser windows. Perhaps localStorage would result in less frequent API calls to rottentomatoes? And you could expire old localStored ratings after X number of hours/days.

  4. Amar Deka says:

    Yes you are right. We now have a local cache for the rankings to avoid some of the http calls. I did not know about localStorage feature of HTML5. Thanks for pointing it out, Matt.

  5. Ricardo says:

    You share interesting things here. I think that your page can go viral easily,
    but you must give it initial boost and i know how to do it, just type in google (with quotes) for –
    “mundillo traffic increase make your website go viral”

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