TYLER DAVIS

Working at a startup leaves me feeling warm and fuzzy - also slightly hysterical.

Hbizzle

This article is an account of my work on http://www.hbizzle.com.

The project code is available on Github, licensed under MIT.

The problem.

I have HBO GO. I pay for it. I know, crazy. I don’t use my mom’s. I don’t use my neighbors’ mom’s. So for that reason – because I pay for it – it pisses me off. I’ll admit it. I’m a user experience snob. The projects that I work on at home and at work, I tend to nitpick endlessly until they’re just right. So when a service that I pay for doesn’t live up to my standards, I slowly but surely begin to hate it. Sure enough, as of last week, I was hating on HBO GO pretty hard.

HBO GO Interface

The interface has an obvious, ‘I really tried to be sexy’ feel to it, but it’s cumbersome, slow to navigate, and next to impossible to quickly find something good to watch. What ends up happening in our household is we navigate to the movie section, pull up IMDB and search for each featured film to find something that doesn’t have a terrible score.

There’s a huge problem with this system, though. If you watch a decent amount of their content, you end up in the A-Z section, using the same HBO > IMDB > HBO system. That, in a nutshell, made me crack.

The solution.

So, I set out to recreate the HBO GO user experience in a way that made sense to me. You hear all the time that the best products are solutions to the developer’s own problems. Well, Hbizzle is just that – a selfish solution to one of my own problems.

The requirements were simple:

  • It needs to take care of the research for me. I don’t want to go to IMDB. I want all the data right there.
  • It needs to be intuitive, meaning, me, my roommate and my mom need to be able to get it and start using it as quickly as possible.
  • It needs to be fast.
  • It needs to look good.
  • I need to learn from it. I want to do something that I haven’t done before.

The Prototype

So, last week, while watching an HBO movie, I started researching. I opened up dev tools and watched the network requests in various views of the HBO GO app, and discovered a nice, full XML listing of all of their movies. No tokens, open to the public, it was exactly what I needed. I then moved on to IMDB, which sure enough, someone has created a gem to access their data.

The prototype:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require 'active_support/core_ext/hash/conversions'
require 'imdb'
require 'nokogiri'
require 'open-uri'

module HBO
  def get_ratings
    xml = Nokogiri::XML(open('http://catalog.lv3.hbogo.com/apps/mediacatalog/rest/productBrowseService/HBO/category/INDB487'))

    hbo_features = Hash.from_xml(xml.to_s)['response']['body']['productResponses']['featureResponse']

    hbo_features.each do |feature|
      self.get_imdb_data(feature['title'], feature['year'].to_s, feature['imageResponses'].first['resourceUrl'])
    end
  end

  def get_imdb_data(title, year, image)
    p "Searching for #{title}"
    imdb_movies = Imdb::Movie.search(title)
    movie = imdb_movies.select { |m| m.title.include? year }.first
    Movie.create(
      title: title,
      year: year,
      image: image,
      rating: movie.rating.to_s,
      imdb_link: movie.url
    )
  end
end

So, I had what I needed. I refactored the script in to a simple Sinatra app and rendered the results to a table. I knew I’d need to persist the data so I wasn’t pinging hbo and imdb every time someone made a request, so I installed Datamapper and set up a table in PostgreSQL. This brought to light more areas for improvement, mainly the need to handle the API requests asynchronously. For that, I chose my favorite, Sidekiq. I broke the actions in to individual tasks, one for fetching the HBO listings, and another for searching IMDB for each movie.

Rails

At this point, Sinatra and it’s simplicity started slowing me down. Don’t get me wrong – I love Sinatra and it’s simplicity, but I wanted to move quickly and get this project up as quickly as possible, so I generated a rails app and moved the necessary code over.

I had my index route, which served my table of data, and a secondary route for fetching new listings and updating existing listings.

Realizing I didn’t want to have to ping this update route every day to get fresh data, I started looking at automating the updates. I needed to start thinking about deployment, since the service I chose would dictate how that was implemented.

Heroku

I didn’t want to pay much for hosting. It’s a project that in all likelihood will only be used by me and my friends, so it didn’t make sense to use a dedicated Digital Ocean (love you guys) box for a 20 visits per day site. Heroku seemed like a reasonable solution.

Heroku has a great, fairly powerful, free tier. You can host small apps behind custom domains with little deployment effort. There are limitations, one being, you can’t rely on scheduling tasks in your app, but using their scheduler allows you to spin up a temporary Dyno that will run a rake task, and it’s super cheap to do so.

I again refactored, creating a rake task that handled the fetching and updating of the movie listings. I deployed the app, set up the scheduler and everything worked!

Now, I had a hosted app. When I hit the app, it printed a 265 row table of every movie currently available on HBO GO, but it had problems. If I tried to view the page on my iPhone 5, it would crash the browser. Investigating, I found the problem. The images I was linking to from HBO were big. So, I created yet another task, using the DragonFly gem, which resized the images from HBO GO and persisted them on S3. Pushing those changes up to Heroku, the app finally ran on my phone, but was still not very user friendly. I had a huge table, with no sorting.

Enter, AngularJS

I love Angular. I’ve been working exclusively with it for the past five months at work and it has been a huge win in my book. Its modules and dependency injection allows for clean organization of code and separation of concerns. For this type of project, that’s not a priority, but I like working within that type of structure.

I reimplemented the table in Angular. Knowing that the amount of data I’d be working with would be relatively static, I chose to continue having rails print the table data to the index page, but instead, printing it as a javascript object in the global scope. That way, the app only makes one request, and angular has access to the data as soon as possible.

1
2
3
<script>
  window.movies = <%= raw @movies.to_json(except: [:id, :expire, :created_at, :updated_at], methods: [:hbo_link, :meta_score, :image_url]) %>;
</script>

Sorting and Pagination

I wanted sorting and pagination, so with some research I found an implementation that made sense to me. HubSpot’s Sortable jQuery plugin was a great example of the sorting behavior I was after, so I borrowed the patterns I wanted from them and reimplemented them in angular. I added a pagination filter and pager ala angular-ui’s bootstrap library that completed the pagination aspect.

Now I really had something that I could use. I had the ability to sort by imdb score, title, year and even rating. At this point I was getting pretty amped about what I had accomplished in a couple nights of tinkering, so I started thinking about clever ways to extend my app.

Meet Hbizzle

Hbizzle Desktop UI

The Hbizzle Score

I spent a couple days getting user feedback and found that a lot of people actually use Rotten Tomatoes for their movie ratings, so added yet another task to pull RT ratings for my listings. At this point, my simple table was becoming not so simple. As an experiment, I created an aggregate score using IMDB and RT.

1
2
3
4
5
6
7
8
9
class Movie
  def meta_score
    if (self.imdb_rating && self.rotten_critics_score && self.rotten_audience_score)
      return ((self.imdb_rating.to_f * 10 + self.rotten_critics_score.to_f + self.rotten_audience_score.to_f) / 3).round
    else
      return 0
    end
  end
end

I went back to my users, who loved the new Hbizzle score.

Extending the hbizzle score

So, this is where I’m at right now. I wanted to track what users were watching through my app and how many plays each movie was getting, but I didn’t want to require any type of registration or login. The simplest solution I’ve come up with is to use cookies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular.module('controllers.index').controller('IndexCtrl', ['$cookies', '$scope', function($cookies, $scope) {

  var cookies = (!!$cookies.hbizzle) ? JSON.parse($cookies.hbizzle) : [];

  $scope.play = function (movie, event) {
    cookies.push(movie.hbo_id);
    $cookies.hbizzle = JSON.stringify(cookies);
    $http({
      data : { hbo_id : movie.hbo_id },
      method : 'POST',
      url : '/play'
    });
  };
}])

Every time a movie is played, two things are happening. I’m updating the play count on the server and I’m appending the movie’s ID to the new Hbizzle cookie on the client. With the new plays attribute, I’m going to incorporate some type of recency score to further extend the Hbizzle score.

I’d also like to extend my model to support movie genres, actors and directors. I’m thinking about adding an anonymous user id to the Hbizzle cookie that would persist on my database, that in combination with the previously watched movies cookie data, will allow me to establish a distance score which I can then incorporate in to the Hbizzle score. That will make everyone’s Hbizzle score their own, and ultimately – I hope, anyways – make finding great movies even easier and more personal.

For anyone that has experience implementing distance scoring of this type, I’d love to get your feedback on my ideas. Thank you for taking the time to read this!

Project source is available on Github, licensed under MIT.

Update:

I’ve implemented the movie play count in to the Hbizzle score along with a recency score. For both, I chose to calculate a percentile based on a subset of movies.

1
2
3
4
5
6
7
8
9
10
11
class Movie
  def self.percentile_map(movies, action)
    movie_array = movies.map {|m| m[action]}.uniq.sort
    count = movie_array.count
    percentile_map = {}
    movie_array.each_with_index do |key, index|
      percentile_map[key] = ((index + 1) / count.to_f * 100).round
    end
    return percentile_map
  end
end

As you can see, I’m creating a unique array of all play/created_at values and sorting them. From there, I’m mapping those values to a percentile score. I’m then mapping the percentiles to the individual records.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def set_meta_score
  if (self.imdb_rating && self.rotten_critics_score && self.rotten_audience_score)
    self.meta_score = (
      (
        self.imdb_rating.to_f * 10 +
        self.rotten_critics_score.to_f +
        self.rotten_audience_score.to_f +
        self.play_rating +
        self.recency_rating * 1.2
      ) / 5
    ).round
  else
    self.meta_score = 0
  end
end

The weight set on the recency score is an arbitrary one, one which I will be adjusting as time goes on.