package WWW::Netflix::API;
use warnings;
use strict;
our $VERSION = '0.03';
use base qw(Class::Accessor);
use Net::OAuth;
use HTTP::Request::Common;
use LWP::UserAgent;
use WWW::Mechanize;
use URI::Escape;
__PACKAGE__->mk_accessors(qw/
consumer_key
consumer_secret
content_filter
access_token
access_secret
user_id
content
original_content
content_error
_levels
rest_url
_url
_params
/);
sub _set_content {
my $self = shift;
my $content = shift;
$self->content_error(undef);
$self->original_content( $content );
$self->content( $self->original_content && $self->content_filter
? &{$self->content_filter}($self->original_content, @_)
: $self->original_content
);
return $self->content;
}
sub REST {
my $self = shift;
my $url = shift;
$self->_levels([]);
$self->_set_content(undef);
if( $url ){
my ($url, $querystring) = split '\?', $url, 2;
$self->_url($url);
$self->_params({
map {
my ($k,$v) = split /=/, $_, 2;
$k !~ /^oauth_/
? ( $k => uri_unescape($v) )
: ()
}
split /&/, $querystring||''
});
return $self->url;
}
$self->_url(undef);
$self->_params({});
return WWW::Netflix::API::_UrlAppender->new( stack => $self->_levels, append => {users=>$self->user_id} );
}
sub url {
my $self = shift;
return $self->_url if $self->_url;
return join '/', 'http://api.netflix.com', @{ $self->_levels || [] };
}
sub _submit {
my $self = shift;
my $method = shift;
my %options = ( %{$self->_params || {}}, @_ );
$self->_set_content(undef);
my $request = Net::OAuth->request("protected resource")->new(
consumer_key => $self->consumer_key,
consumer_secret => $self->consumer_secret,
request_url => $self->url,
token => $self->access_token,
token_secret => $self->access_secret,
request_method => $method,
signature_method => 'HMAC-SHA1',
timestamp => time,
nonce => join('::', $0, $$),
version => '1.0',
extra_params => \%options,
);
$request->sign;
my $url = $request->to_url->as_string;
$self->rest_url( $url );
my $ua = LWP::UserAgent->new;
my $req;
if( $method eq 'GET' ){
$req = GET $url;
}elsif( $method eq 'POST' ){
$req = POST $url;
}elsif( $method eq 'DELETE' ){
$req = HTTP::Request->new( 'DELETE', $url );
}else{
$self->content_error( "Unknown method '$method'" );
return;
}
my $res = $ua->request($req);
if ( ! $res->is_success ) {
$self->content_error( sprintf '%s Request to "%s" failed (%s): "%s"', $method, $url, $res->status_line, $res->content );
return;
}
$self->_set_content( $res->content );
return 1;
}
sub Get {
my $self = shift;
return $self->_submit('GET', @_);
}
sub Post {
my $self = shift;
return $self->_submit('POST', @_);
}
sub Delete {
my $self = shift;
return $self->_submit('DELETE', @_);
}
sub rest2sugar {
my $self = shift;
my $url = shift;
my @stack = ( '$netflix', 'REST' );
my @params;
$url =~ s#^http://api.netflix.com##;
$url =~ s#(/users/)(\w|-){30,}/#$1#i;
$url =~ s#/(\d+)(?=/|$)#('$1')#;
if( $url =~ s#\?(.+)## ){
my $querystring = $1;
@params = map {
my ($k,$v) = split /=/, $_, 2;
[ $k, uri_unescape($v) ]
}
split /&/, $querystring;
}
push @stack, map {
join '_', map { ucfirst } split '_', lc $_
}
grep { length($_) }
split '/', $url
;
return (
join('->', @stack),
sprintf('$netflix->Get(%s)',
join( ', ', map { sprintf "'%s' => '%s'", @$_ } @params ),
),
);
}
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sub __get_token_response {
my $self = shift;
my $which = shift;
my $token = shift;
my $secret = shift;
my %urls = qw(
request http://api.netflix.com/oauth/request_token
access http://api.netflix.com/oauth/access_token
);
my $request = Net::OAuth->request("$which token")->new(
consumer_key => $self->consumer_key,
consumer_secret => $self->consumer_secret,
($token ? ( token => $token,
token_secret => $secret, ) : () ),
request_url => $urls{$which},
request_method => 'POST',
signature_method => 'HMAC-SHA1',
timestamp => time,
nonce => join('::', $0, $$),
version => '1.0',
);
$request->sign;
my $ua = LWP::UserAgent->new;
my $res = $ua->request(POST $request->to_url); # Post message to the Service Provider
if (! $res->is_success) {
warn sprintf 'Request for %s token failed (%s): "%s"', $which, $res->status_line, $res->content;
return;
}
my $response = Net::OAuth->response("$which token")->from_post_body($res->content);
return $response;
}
sub __get_request_token {
my $self = shift;
my $response = $self->__get_token_response('request');
return (
$response->token,
$response->token_secret,
$response->extra_params->{login_url},
$response->extra_params->{application_name},
);
}
sub __get_access_token {
my $self = shift;
my $response = $self->__get_token_response('access', @_);
return (
$response->token,
$response->token_secret,
$response->extra_params->{user_id},
);
}
sub RequestAccess {
my $self = shift;
my ($user, $pass) = @_;
my ($request_token, $request_secret, $login_url, $application_name) = $self->__get_request_token;
my $mech = WWW::Mechanize->new;
my $url = sprintf '%s&oauth_callback=%s&oauth_consumer_key=%s&application_name=%s',
$login_url,
map { uri_escape($_) }
'',
$self->consumer_key,
$application_name,
;
$mech->get($url);
$mech->submit_form(
form_number => 1,
fields => {
login => $user,
password => $pass,
},
);
return unless $mech->content =~ /successfully/i && $mech->content !~ /failed/i;
my ($access_token, $access_secret, $user_id) = $self->__get_access_token( $request_token, $request_secret );
$self->access_token( $access_token );
$self->access_secret( $access_secret );
$self->user_id( $user_id );
return ($self->access_token, $self->access_secret, $self->user_id);
}
########################################
package WWW::Netflix::API::_UrlAppender;
use strict;
use warnings;
our $AUTOLOAD;
sub new {
my $self = shift;
my $params = { @_ };
return bless { stack => $params->{stack}, append => $params->{append}||{} }, $self;
}
sub AUTOLOAD {
my $self = shift;
my $dir = lc $AUTOLOAD;
$dir =~ s/.*:://;
if( $dir ne 'destroy' ){
push @{ $self->{stack} }, $dir;
push @{ $self->{stack} }, @_ if scalar @_;
push @{ $self->{stack} }, $self->{append}->{$dir} if exists $self->{append}->{$dir};
}
return $self;
}
1; # End of WWW::Netflix::API
__END__
=pod
=head1 NAME
WWW::Netflix::API - Interface for Netflix's API
=head1 VERSION
Version 0.03
=head1 OVERVIEW
This module is to provide your perl applications with easy access to the
Netflix API (L).
The Netflix API allows access to movie and user information, including queues, rating, rental history, and more.
=head1 SYNOPSIS
use WWW::Netflix::API;
use Data::Dumper;
my %auth = Your::Custom::getAuthFromCache();
# consumer key/secret values below are fake
my $netflix = WWW::Netflix::API->new({
consumer_key => '4958gj86hj6g99',
consumer_secret => 'QWEas1zxcv',
access_token => $auth{access_token},
access_secret => $auth{access_secret},
user_id => $auth{user_id},
content_filter => sub { use XML::Simple; XMLin(@_) }, # optional
});
if( ! $auth{user_id} ){
my ( $user, $pass ) = .... ;
@auth{qw/access_token access_secret user_id/} = $netflix->RequestAccess( $user, $pass );
Your::Custom::storeAuthInCache( %auth );
}
$netflix->REST->Users->Feeds;
$netflix->Get() or die 'request failed';
print Dumper $netflix->content;
$netflix->REST->Catalog->Titles->Movies('18704531');
$netflix->Get() or die 'request failed';
print Dumper $netflix->content;
=head1 GETTING STARTED
The first step to using this module is to register at L -- you will need to register your application, for which you'll receive a consumer_key and consumer_secret keypair.
Any applications written with the Netflix API must adhere to the
Terms of Use (L)
and
Branding Requirements (L).
=head2 Usage
This module provides access to the REST API via perl syntactical sugar. For example, to find a user's queue, the REST url is of the form users/I/feeds :
http://api.netflix.com/users/T1tareQFowlmc8aiTEXBcQ5aed9h_Z8zdmSX1SnrKoOCA-/queues/disc
Using this module, the syntax would be:
$netflix->REST->Users->Queues->Disc;
$netflix->Get(%$params) or die;
print $netflix->content;
Other examples include:
$netflix->REST->Users;
$netflix->REST->Users->At_Home;
$netflix->REST->Catalog->Titles->Movies('18704531');
$netflix->REST->Users->Feeds;
$netflix->REST->Users->Rental_History;
All of the possibilities (and parameter details) are outlined here:
L
There is a helper method L<"rest2sugar"> included that will provide the proper syntax given a url. This is handy for translating the example urls in the REST API Reference.
=head2 Resources
The following describe the authentication that's happening under the hood and were used heavily in writing this module:
L
L
L
=head1 EXAMPLES
The examples/ directory in the distribution has several examples to use as starting points.
=head1 METHODS
=head2 new
This is the constructor.
Takes a hashref of L<"ATTRIBUTES">.
Inherited from L
Most important options to pass are the L<"consumer_key"> and L<"consumer_secret">.
=head2 REST
This is used to change the resource that is being accessed. Some examples:
# The user-friendly way:
$netflix->REST->Users->Feeds;
# Including numeric parts:
$netflix->REST->Catalog->Titles->Movies('60021896');
# Load a pre-formed url (e.g. a title_ref from a previous query)
$netflix->REST('http://api.netflix.com/users/T1tareQFowlmc8aiTEXBcQ5aed9h_Z8zdmSX1SnrKoOCA-/queues/disc?feed_token=T1u.tZSbY9311F5W0C5eVQXaJ49.KBapZdwjuCiUBzhoJ_.lTGnmES6JfOZbrxsFzf&oauth_consumer_key=v9s778n692e9qvd83wfj9t8c&output=atom');
=head2 RequestAccess
This is used to login as a netflix user in order to get an access token.
my ($access_token, $access_secret, $user_id) = $netflix->RequestAccess( $user, $pass );
=head2 Get
=head2 Post
=head2 Delete
=head2 rest2sugar
=head1 ATTRIBUTES
=head2 consumer_key
=head2 consumer_secret
=head2 access_token
=head2 access_secret
=head2 user_id
=head2 content_filter
The content returned by the REST calls is POX (plain old XML). Setting this attribute to a code ref will cause the content to be "piped" through it.
use XML::Simple;
$netflix->content_filter( sub { XMLin(@_) } ); # Parse the XML into a perl data structure
=head2 content
Read-Only.
=head2 original_content
Read-Only.
=head2 content_error
Read-Only.
=head2 url
Read-Only.
=head2 rest_url
Read-Only.
=head1 INTERNAL
=head2 _url
=head2 _params
=head2 _levels
=head2 _submit
=head2 __get_token_response
=head2 __get_request_token
=head2 __get_access_token
=head2 WWW::Netflix::API::_UrlAppender
=head1 AUTHOR
David Westbrook (CPAN: davidrw), C<< >>
=head1 BUGS
Please report any bugs or feature requests to C, or through
the web interface at L. I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::Netflix::API
You can also look for information at:
=over 4
=item * RT: CPAN's request tracker
L
=item * AnnoCPAN: Annotated CPAN documentation
L
=item * CPAN Ratings
L
=item * Search CPAN
L
=back
=head1 COPYRIGHT & LICENSE
Copyright 2008 David Westbrook, all rights reserved.
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut