Silverlight and the Netflix API

Skill

Silverlight and the Netflix API

Posted in:

Even though the Netflix API was released back in October, I just recently discovered it. I enjoy seeing new APIs and was definitely interested in using this one since I'm also a Netflix subscriber. This tutorial will demonstrate how to build a simple Silverlight application on top of the Netflix API.

The Netflix API is very powerful, however it uses OAuth to secure the communications, which is a bit of hurdle starting out. Fortunately for us, the Yedda Dev Blog was nice enough to write a simple C# wrapper around OAuth that was able to be used in Silverlight with only minor changes. Below is an example of the Silverlight application we'll be building today.



The first thing we're going to have to do is figure out how to get data from Netflix. When you register for the Netflix API, you'll be given a consumer key and a shared secret. Let's begin by defining some constants for these values in our C# code.

private const string API_URL = "http://api.netflix.com/" +
    "catalog/titles?max_results=10&term=";
private const string CONSUMER_KEY = "xxxxxxxxxx";
private const string CONSUMER_SECRET = "xxxxxxxxxx";
private const string REQUEST_METHOD = "GET";

The API_URL holds what page we want to request from the API. In this case we want to search all titles in their category for a specific term and only return a maximum of 10 results. The CONSUMER_KEY and CONSUMER_SECRET are provided by Netflix. Netflix can handle POST and GET requests, and for this tutorial, we're using GET.

We'll need a couple of other member variables, so let's define those now as well.

private OAuth.OAuthBase _oauth;
private WebClient _client;

OAuthBase is a class provided by Yedda Dev Blog to help build OAuth requests. I had to modify it because Silverlight doesn't support some things they were using. Basically I had to convert all instances of System.Text.AsciiEncoding to System.Text.Encoding.UTF8. My modified version will be included in the Visual Studio project attached to this tutorial. _client is simply the WebClient object that will be handling all of the requests.

All right, we've got our variables out of the way, now let's build a function that will create a query that we can send off to Netflix.

/// <summary>
/// Builds a URL to be sent to the netflix API
/// </summary>
/// <param name="title">movie to search for</param>
/// <returns>netflix API ready URL</returns>
private Uri BuildQuery(string title)
{
  string normalizedUrl;
  string normalizedReqParams;

  string url = API_URL + title;

  string signature =
      _oauth.GenerateSignature(
          new Uri(url),
          CONSUMER_KEY,
          CONSUMER_SECRET,
          null, null,
          REQUEST_METHOD,
          _oauth.GenerateTimeStamp(),
          _oauth.GenerateNonce(),
          out normalizedUrl,
          out normalizedReqParams);

  //make the signature url ready
  signature = HttpUtility.UrlEncode(signature);

  return new Uri(
      normalizedUrl + "?" +
      normalizedReqParams +
      "&oauth_signature=" +
      signature);
}

This is where things start to get difficult. OAuth requires a signature that is generated from various parts of the request. This ensures that the request cannot be modified between you and Netflix. This is where we use the function GenerateSignature from the OAuthBase object. This function takes all the pieces required by OAuth and Netflix, builds a signature, and outputs a normalized, url friendly, version of our request URL and parameters.

The two null values passed to the function would be used if we were using tokens. You have to first request a token from the user before you can modify their private data. For this app, no tokens are required since we're just searching Netflix's catalog for movies. Lastly, we combine the pieces into a finished URL and return.

Now let's quickly take a look at the Search function that get's called whenever the user hits the "Go" button.

private void Search()
{
  //WebClient can only do one request at a time
  if (!_client.IsBusy)
    _client.DownloadStringAsync(BuildQuery(_tbSearch.Text));
}

We have to make sure the WebClient is not already doing something. If it were, an exception would be thrown when you attempted to use it. Next we call DownloadStringAsync and pass it the Url we just created.

WebClient has an event that gets fired when the download is complete. I hooked the event in the Page constructor after I constructed the WebClient object.

_client = new WebClient();

_client.DownloadStringCompleted +=
    (sender, e) =>
    {
      if (e.Error == null)
        ParseResult(e.Result);
    };

I do a simple check to make sure there are no errors, then I call ParseResult passing it the string containing the result xml. Before we look at the contents of ParseResult, we're going to have to create an object to store information about search results. I called mine SearchResult.

/// <summary>
/// Holds information about a search result
/// </summary>
public class SearchResult
{
  public string Title { get; set; }
  public string Image { get; set; }
  public string Year { get; set; }
  public string Rating { get; set; }
}

Now we can parse the XML returned from Netflix and start populating our user interface. Here's an example XML string returned by the Netflix API.

<catalog_titles>  
  <number_of_results>1140</number_of_results>  
  <start_index>0</start_index>  
  <results_per_page>10</results_per_page>  
  <catalog_title>  
  <id>http://api.netflix.com/catalog/titles/movies/60021896</id><title short="Star"    
   regular="Star"></title>  
  <box_art small="http://alien2.netflix.com/us/boxshots/tiny/60021896.jpg"    
   medium="http://alien2.netflix.com/us/boxshots/small/60021896.jpg"    
   large="http://alien2.netflix.com/us/boxshots/large/60021896.jpg"></box_art>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/synopsis"    
   rel="http://schemas.netflix.com/catalog/titles/synopsis"  title="synopsis"></link>  
  <release_year>2001</release_year>  
  <category scheme="http://api.netflix.com/categories/mpaa_ratings"  label="NR"></category>  
  <category scheme="http://api.netflix.com/categories/genres"  label="Foreign"></category>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/cast"    
   rel="http://schemas.netflix.com/catalog/people.cast"  title="cast"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/directors"    
   rel="http://schemas.netflix.com/catalog/people.directors"  title="directors"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/format_availability"    
   rel="http://schemas.netflix.com/catalog/titles/format_availability"  title="formats"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/screen_formats"  
   rel="http://schemas.netflix.com/catalog/titles/screen_formats"  title="screen  formats"></link  
 <link href="http://api.netflix.com/catalog/titles/movies/60021896/languages_and_audio"
   rel="http://schemas.netflix.com/catalog/title/languages_and_audio"  
   title="languages  and audio"></link>  
  <average_rating>1.9</average_rating>  
  <link href="http://api.netflix.com/catalog/titles/movies/60021896/similars"  
   rel="http://schemas.netflix.com/catalog/titles.similars"  title="similars"></link>  
  <link href="http://www.netflix.com/Movie/Star/60021896"  rel="alternate"  title="webpage"></link>  
  </catalog_title>  
  <catalog_title>  
  <id>http://api.netflix.com/catalog/titles/movies/17985448</id>
  <title short="Lone  Star" regular="Lone Star"></title>  
  <box_art small="http://alien2.netflix.com/us/boxshots/tiny/17985448.jpg"  
 medium="http://alien2.netflix.com/us/boxshots/small/17985448.jpg"  
 large="http://alien2.netflix.com/us/boxshots/large/17985448.jpg"></box_art>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/synopsis"
   rel="http://schemas.netflix.com/catalog/titles/synopsis"  title="synopsis"></link>  
  <release_year>1996</release_year>  
  <category scheme="http://api.netflix.com/categories/mpaa_ratings"  label="R"></category>  
  <category scheme="http://api.netflix.com/categories/genres"  label="Drama"></category>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/cast"
   rel="http://schemas.netflix.com/catalog/people.cast"  title="cast"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/directors"
  rel="http://schemas.netflix.com/catalog/people.directors"  title="directors"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/awards"
   rel="http://schemas.netflix.com/catalog/titles/awards"  title="awards"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/format_availability"
  rel="http://schemas.netflix.com/catalog/titles/format_availability"  title="formats"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/screen_formats"
  rel="http://schemas.netflix.com/catalog/titles/screen_formats"  title="screen  formats"></link>  
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/languages_and_audio"
  rel="http://schemas.netflix.com/catalog/titles/languages_and_audio"
  title="languages  and audio"></link>  
  <average_rating>3.7</average_rating>
  <link href="http://api.netflix.com/catalog/titles/movies/17985448/similars"
   rel="http://schemas.netflix.com/catalog/titles.similars"  title="similars"></link>  
  <link href="http://www.netflix.com/Movie/Lone_Star/17985448"  rel="alternate"
   title="webpage"></link>  
  </catalog_title>  
</catalog_titles>

And here's the function that will parse that into our SearchResult objects.

private void ParseResult(string xml)
{
  try
  {
    XDocument doc = XDocument.Parse(xml);

    _lbResults.ItemsSource =
        from result in doc.Descendants("catalog_title")
        select new SearchResult()
        {
          Title = result.Element("title").Attribute("regular").Value,
          Image = result.Element("box_art").Attribute("large").Value,
          Year = result.Element("release_year").Value,
          Rating = result.Element("average_rating").Value
        };
  }
  catch (Exception)
  {
    //invalid XML
    return;
  }

  //display the details of the first search result
  if (_lbResults.Items.Count > 0)
    _lbResults.SelectedItem = _lbResults.Items[0];
}

I'm really happy Silverlight includes Linq support. If you're not familiar with Linq, I would recommend checking out our introductory tutorial. Netflix has really good example XML in their documentation, so you should be able to use that to quickly parse the XML into data structures. I'm setting the ItemsSource of my ListBox to the output of the Linq expression, which will populate my ListBox with search results. Lastly, I make sure I've received at least one result, and if I have, I select the first result.

That's pretty much it for the code behind. Here's the entire constructor so you can see the rest of my initialization.

public Page()
{
  InitializeComponent();

  _oauth = new OAuth.OAuthBase();
  _client = new WebClient();

  //cause a search to happen when the user
  //presses enter while typing
  _tbSearch.KeyUp +=
      (sender, e) =>
      {
        if (e.Key == Key.Enter)
          Search();
      };

  //search when the 'Go' button is pressed
  _btnGo.Click +=
      (sender, e) =>
          Search();

  //populate the selection details when a
  //search result is selected
  _lbResults.SelectionChanged +=
      (sender, e) =>
          _details.DataContext = _lbResults.SelectedItem;

  _client.DownloadStringCompleted +=
      (sender, e) =>
      {
        if (e.Error == null)
          ParseResult(e.Result);
      };

  //perform an initial search
  Search();
}

We've covered most of this already. What we haven't covered is how to display details for the selected search result. Unfortunately, I couldn't do everything in XAML because the binding support isn't quite as feature-rich as the full WPF framework, so I had to hook the event that's fired whenever the selection changes.

In XAML, I have a Grid control called _details that holds all of the details for a selected result. I simply set the DataContext of the Grid to the selected item (which is a SearchResult object) whenever the selection changes.

All right, time to see some XAML. Here's is the entire source for the example application at the top of this tutorial. The XAML is not the focus of this tutorial so I'm not going to explain it in detail. It's very straight forward and doesn't do anything not covered in one of our other XAML tutorials.

<UserControl x:Class="SilverlightNetflix.Page"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Width="512"
            Height="500"
            Foreground="White">
  <Grid x:Name="LayoutRoot" Background="#b9090b">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="215" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="180" />
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Margin="10">
      <TextBlock Text="Search for Movie: " FontSize="14" VerticalAlignment="Center" />
      <TextBox Width="200" x:Name="_tbSearch" Text="Star Wars" />
      <Button Content="Go" Margin="5,0,0,0" Width="30" x:Name="_btnGo" />
    </StackPanel>
    <TextBlock Text="Results:" FontSize="14" Grid.Row="1" Margin="10,10,0,0" />
    <ListBox Grid.Row="2" Margin="10,0,10,10" x:Name="_lbResults">
      <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
          <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
      </ListBox.ItemsPanel>
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Image Source="{Binding Image}" Stretch="None" Grid.Column="0" Margin="10" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
    <TextBlock Text="Selection Details:" FontSize="14" Grid.Row="3"
              Margin="10,10,0,0" />
    <Grid Margin="10,0,10,10" Background="White" Grid.Row="4" x:Name="_details">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Image Source="{Binding Image}" Stretch="None" Margin="5" />
      <StackPanel Orientation="Vertical" Grid.Column="1" Margin="5">
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="Title: " Foreground="Black" />
          <TextBlock Text="{Binding Title}" Foreground="Black" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="Year: " Foreground="Black" />
          <TextBlock Text="{Binding Year}" Foreground="Black" />
        </StackPanel>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="Rating: " Foreground="Black" />
          <TextBlock Text="{Binding Rating}" Foreground="Black" />
        </StackPanel>
      </StackPanel>
    </Grid>
  </Grid>
</UserControl>

As you can see it's a very basic layout using basic binding to populate the user interface. I did have some difficulty when binding the image path to items in the ListBox. For some reason it didn't work when loaded from my hard drive, however if they're loaded from a web server it works fine.

I think that does it for this tutorial. We've seen how to use OAuth and OAuthBase to create Netflix queries, how to parse the results, and how to populate a user interface containing those results. If you've got any questions or comments, feel free to leave them.

Anonymous
05/18/2009 - 19:15

Very interesting code. Thanks for sharing it.

Can you say why the images load on your site but not when the code is debugged locally in Visual Studio? I'm seeing the following error in IE: 4001, ImageError, AG_E_NETWORK_ERROR.

Any idea what's up?

reply

Anonymous
05/18/2009 - 19:18

Oops. Just noticed your note there at the very end of the article about the image loading. Do you have any idea why this is happening?

Thanks!

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors