Friday, October 10, 2008

Netflix API - Parsing the results of an API call - Part 5

One of the first API calls I needed to call gets the user's name and other information. In my case all I need is the first and last names, and the flag that says whether the user can use instant watching. This flag is set true for main accounts, and false for account profiles that don't have an instant queue.

The return data from the API call is in XML format, so this code shows how to use the XML parser to pick information out of the data returned from an API call. I provide a convenience method that combines the first and last names, and I didn't need the other link information, so it is ignored, but the code could easily be extended to pick it out as needed.

Some of the code below doesn't display properly. I'm going to set it up in google code soon to make it easier to manage.


//
// NetflixUser.h
// Instant Flix
//
// Created by Adrian Cockcroft on 10/8/08.
// Copyright 2008 millicomputing.com
// Licenced using Creative Commons Attribution Share-Alike
// http://creativecommons.org/licenses/by-sa/3.0/

#import


@interface NetflixUser : NSObject {
NSData *rawData;
NSString *first_name;
NSString *last_name;
bool can_instant_watch;
NSXMLParser *userParser;
NSString *currentElement;
}

@property(readonly) bool can_instant_watch;

- (NetflixUser *)initWithAPIResponse:(NSData *)response;
- (NSString *)name;

@end






//
// NetflixUser.m
// Instant Flix
//
// Created by Adrian Cockcroft on 10/8/08.
// Copyright 2008 millicomputing.com
// Licenced using Creative Commons Attribution Share-Alike
// http://creativecommons.org/licenses/by-sa/3.0/

#import "NetflixUser.h"

/* Sample returned raw data

[userid]
Adrian
Cockcroft
true













*/

@implementation NetflixUser

@synthesize can_instant_watch;

- (NetflixUser *)initWithAPIResponse:(NSData *)response {
rawData = response;
[rawData retain];
first_name = nil;
last_name = nil;
can_instant_watch = NO;

//NSString *responseBody = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
//NSLog(@"NetflixUser: %@", responseBody);

userParser = [[NSXMLParser alloc] initWithData:rawData];

// Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.
[userParser setDelegate:self];
[userParser setShouldProcessNamespaces:NO];
[userParser setShouldReportNamespacePrefixes:NO];
[userParser setShouldResolveExternalEntities:NO];
[userParser parse];

return self;
}

- (NSString *)name {
return [first_name stringByAppendingFormat:@" %@", last_name];
}

- (void)parserDidStartDocument:(NSXMLParser *)parser{
//NSLog(@"started parsing");
}

- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
NSString * errorString = [NSString stringWithFormat:@"Unable to parse XML (Error code %i)", [parseError code]];
NSLog(@"error: %@", errorString);
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
//NSLog(@"found this element: %@", elementName);
currentElement = [elementName copy];
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
//NSLog(@"ended element: %@", elementName);
currentElement = nil;
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
//NSLog(@"found characters: %@", string);
// save the characters for the current item...
if ([currentElement isEqualToString:@"first_name"]) {
first_name = [string copy];
} else if ([currentElement isEqualToString:@"last_name"]) {
last_name = [string copy];
} else if ([currentElement isEqualToString:@"can_instant_watch"]) {
if ([string isEqualToString:@"true"]) {
can_instant_watch = YES;
} else {
can_instant_watch = NO;
}
}
}

- (void)parserDidEndDocument:(NSXMLParser *)parser {
//NSLog(@"found %@ who %@ instant watch", self.name, (can_instant_watch? @"can": @"cannot"));
}

@end

Wednesday, October 1, 2008

Netflix API - Netflix Specific OAuth iPhone Code - Part 4

I have already discussed how to get OAuth to build for an iPhone in Part 2. To use OAuth to call Netflix there are two small changes needed. The first one is that the way that characters are escaped in URLs needs to be tightened up a bit, otherwise the signature strings will work some of the time and fail when they happen to contain the wrong character sequence. This took a while to figure out...

The file NSString+URLEncoding.m needs to have a few characters (space, plus and asterisk) added as shown below:

Note, I'm having a hard time getting code to look good here, its hard to find a way to render code in a narrow column that doesn't mess up the formatting and works in more than one browser using blogger.com tools and templates.

- (NSString *)encodedURLString {
NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)self,
NULL,
CFSTR("?=& +*"), // legal URL characters to be escaped
kCFStringEncodingUTF8); // encoding
return result;
}

- (NSString *)encodedURLParameterString {
NSString *result = (NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(CFStringRef)self,
NULL,
CFSTR(":/= +*"),
kCFStringEncodingUTF8);
return result;
}


The second thing is that when the authentication is complete, Netflix returns a token that contains an encoded user identifier, as well as a secret and a key. The standard OAuth code only expects the secret and key, so a new NetflixToken class was created to hold the extra information, and to persist it in the iPhone's defaults store along with the secret and key. This means that once the user has signed into OAuth once, this information is saved and they never have to sign in again unless either Netflix or the User revokes the token. One more method was added to remove an entry from the defaults store, for use during logout.

First the NetflixToken.h header file:

//
// NetflixToken.h
// Instant Test
//
// Created by Adrian Cockcroft on 9/11/08.
//

#import
#import "OAToken.h"

@interface NetflixToken : NSObject {
@protected
NSString *key;
NSString *secret;
NSString *user;
}
@property(copy, readwrite) NSString *key;
@property(copy, readwrite) NSString *secret;
@property(copy, readwrite) NSString *user;

- (id)initWithKey:(NSString *)aKey secret:(NSString *)aSecret user:(NSString *)aUser;
- (id)initWithUserDefaultsUsingServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (id)initWithHTTPResponseBody:(NSString *)body;
- (int)storeInUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (int)removeFromUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix;
- (OAToken *)oaToken;

@end


Then the code itself, this is all based on a simple extension of OAToken, which is part of the OAuth code base mentioned in part 2.


//
// NetflixToken.m
// Instant Test
//
// Created by Adrian Cockcroft on 9/11/08.
//

#import "NetflixToken.h"

@implementation NetflixToken

@synthesize key, secret, user;

#pragma mark init

- (id)init {
[super init];
self.key = @"";
self.secret = @"";
self.user = @"";
return self;
}

- (id)initWithKey:(NSString *)aKey secret:(NSString *)aSecret user:(NSString *)aUser {
[super init];
self.key = aKey;
self.secret = aSecret;
self.user = aUser;
return self;
}

- (OAToken *)oaToken {
return [[OAToken alloc] initWithKey:self.key secret:self.secret];
}

- (id)initWithHTTPResponseBody:(NSString *)body {
[super init];
NSArray *pairs = [body componentsSeparatedByString:@"&"];

for (NSString *pair in pairs) {
NSArray *elements = [pair componentsSeparatedByString:@"="];
if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token"]) {
self.key = [elements objectAtIndex:1];
} else if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token_secret"]) {
self.secret = [elements objectAtIndex:1];
} else if ([[elements objectAtIndex:0] isEqualToString:@"user_id"]) {
self.user = [elements objectAtIndex:1];
}
}

return self;
}


- (id)initWithUserDefaultsUsingServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[super init];
NSString *theKey = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
NSString *theSecret = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
NSString *theUser = [[NSUserDefaults standardUserDefaults] stringForKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
if (theKey == NULL || theSecret == NULL)
return(nil);
self.key = theKey;
self.secret = theSecret;
self.user = theUser;
return(self);
}


- (int)storeInUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[[NSUserDefaults standardUserDefaults] setObject:self.key forKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
[[NSUserDefaults standardUserDefaults] setObject:self.secret forKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
[[NSUserDefaults standardUserDefaults] setObject:self.user forKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
[[NSUserDefaults standardUserDefaults] synchronize];
return(0);
}

- (int)removeFromUserDefaultsWithServiceProviderName:(NSString *)provider prefix:(NSString *)prefix
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_KEY", prefix, provider]];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"OAUTH_%@_%@_SECRET", prefix, provider]];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:[NSString stringWithFormat:@"NETFLIX_%@_%@_USER", prefix, provider]];
[[NSUserDefaults standardUserDefaults] synchronize];
return(0);
}
@end

Netflix API - Announcement - Part 3

Here is the official announcement of the API and how to get access to it.

Starting Wednesday, Oct. 1 the Netflix API is open to all:

The Netflix API:

- Allows access to data for 100,000 movie and TV episode titles on DVD as well as Netflix account access on a user’s behalf
Netflix has more than 2 billion ratings in its database
Netflix members rate more than 2 million movies a day
Netflix ships more than 2 million DVDs on a typical day

- Is free

- Allows commercial use
E.g. if a developer creates an iPhone app and wants to sell it for $0.99, that’s ok

Technically, the Netflix API:

- Includes a REST API, a Javascript API, and ATOM feeds

- Uses OAuth standard security to allow the subscriber to control which applications can access the service on his or her behalf

Developers can get access:

- Starting 10/1

- By self sign up at http://developer.netflix.com

Wednesday, September 17, 2008

Netflix API - Getting OAuth to work on iPhone - part 2: Adding the OAuth Code

To start with I found some examples by Nick Dalton that helped me build a simple application that included a Web View, a screen that acts like a web browser but with my custom Objective-C code embedded in it. This is important, because the OAuth sign-in process uses a web page, but on the iPhone, if you spawn a copy of Safari to visit a web page, your application quits first.

Next the Source Code Manager in Xcode was configured to load the OAuthconsumer Objective-C framework via subversion. This was easy and obvious, enter the URL http://oauth.googlecode.com/svn/code/obj-c/ and checkout the code.

When trying to import the framework I discovered that Apple does not allow user specified binary frameworks to be added to iPhone applications. To work around this the source code was copied from the Xcode project for the framework, to the Xcode project for my Instant Test application. I renamed the  framework Classes folder as OAuth and copied to my project via drag and drop, choosing to copy the underlying files. The Cocoa Categories, Protocols and Other Sources>Crypto folder were also copied. The Tests folder did not compile for iPhone so don't bother to copy it over.

The standard system Security.Framework doesn't need to be added to the Frameworks folder. I initially thought it did, but its probably only needed for the KeyChain code.

The iPhone doesn't support the KeyChain functionality, so if you try to build for iPhone it will fail. It does however build for the iPhone Simulator, which is confusing. Open up the OAuth source code, and delete the last two files OAToken_KeychainExtensions.h and OAToken_KeychainExtensions.m.

Since the code is no longer a framework, the header file references need to be changed from #include to #include "file" for all the includes in OAuthConsumer.h apart from the first Foundation one.

At this point, before you try and call anything, try a build, it should compile with no errors. If it doesn't, look for missing files.

At this point, you should have all you need to connect to an OAuth service

Tuesday, September 16, 2008

Netflix API Getting OAuth to work on iPhone - Instant Queue Add part 1: why?

Why develop an iPhone app? Its "the future", a useful skill, and I can carry whatever I develop in my pocket and make it do whatever I want.

I work at Netflix, and I have instant watching on my TV, I built an application "Instant Queue Add" that lets me add a title to my instant queue using my iPhone in a couple of touches. It takes the Top20 and New Choices RSS feeds to find content, and it spawns a copy of Safari to add to instant queue for each pick. The first time it starts, you have to login, then it remembers the Netflix cookie. However I really want to add more features and avoid spawning a copy of Safari with a screen scraped URL.

The Netflix API uses a new standard called OAuth for Open Authentication. There are lots of features, but its complex, and there is no standard off the shelf library for OAuth on the iPhone. However it is a useful building block for more advanced applications.

In this series of posts, I will document the steps I'm making to get OAuth to work on the iPhone using Objective-C and Xcode. My starting point is this code base and tutorial by Jon R. Crosby, which is based on desktop MacOS X, and doesn't directly support the iPhone.

Monday, September 8, 2008

Intel high speed SSD

Here is Tech Report's review of Intel's high speed SSD, which confirms the trend I've been talking about for a while. SSD's were always faster for random read and write, now they are faster for sequential read (250MB/s), and the "extreme" Intel model is faster for sequential write as well (170MB/s). They use less power and have comparable MTBF to the best enterprise disk drives.

The remaining disadvantages of total size and cost are being eaten away over time...

Wednesday, August 6, 2008

The Physical Web

Nat Torkington on O'Reilly Radar declares a familiar theme...

The next step for computing is to move out from the computers. Every device has the potential to become network-connected, delivering information to or from a web service. The mobile phones in our pockets also let us take apps and network service with us wherever we go. Early hackers are building in this space. Big challenges include: take products to the masses and the environmental impacts. We're at early stages yet, but the room for expansion is huge. Projects to watch include Nokia's reinvention as services company, iPhone, and Google's Android platform.