flix4r 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/flix4r.gemspec +75 -0
- data/init.rb +2 -0
- data/lib/flix4r.rb +22 -0
- data/lib/net_flix.rb +36 -0
- data/lib/net_flix/authenticator.rb +78 -0
- data/lib/net_flix/builders/actor_builder.rb +29 -0
- data/lib/net_flix/builders/format_builder.rb +33 -0
- data/lib/net_flix/credentials.rb +41 -0
- data/lib/net_flix/movie.rb +10 -0
- data/lib/net_flix/request.rb +82 -0
- data/lib/net_flix/television.rb +14 -0
- data/lib/net_flix/title.rb +113 -0
- data/lib/valuable.rb +82 -0
- data/test/actor_builder_test.rb +34 -0
- data/test/authenticator_test.rb +69 -0
- data/test/credentials_test.rb +27 -0
- data/test/fixtures/autocomplete.xml +15 -0
- data/test/fixtures/cast.xml +10 -0
- data/test/fixtures/directors.xml +15 -0
- data/test/fixtures/movies.xml +56 -0
- data/test/fixtures/synopsis.xml +3 -0
- data/test/fixtures/titles.xml +69 -0
- data/test/format_builder_test.rb +67 -0
- data/test/movie_test.rb +54 -0
- data/test/request_test.rb +39 -0
- data/test/test_helper.rb +40 -0
- data/test/title_test.rb +59 -0
- metadata +95 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
module NetFlix
|
2
|
+
class Request < Valuable
|
3
|
+
|
4
|
+
RESERVED_CHARACTERS = /[^A-Za-z0-9\-\._~]/
|
5
|
+
|
6
|
+
has_value :http_method, :default => 'GET'
|
7
|
+
has_value :url, :default => 'http://api.netflix.com/catalog/titles/index'
|
8
|
+
has_value :parameters, :klass => HashWithIndifferentAccess, :default => {}
|
9
|
+
has_value :cache_options, :default => {}
|
10
|
+
|
11
|
+
def self.default_cache_options
|
12
|
+
{
|
13
|
+
:cache => 600, # 10 minutes After this time fetch new data
|
14
|
+
:valid => 86400, # 1 day Maximum time to use old data
|
15
|
+
# :forever is a valid option
|
16
|
+
:period => 60, # 1 minute Maximum frequency to call API
|
17
|
+
:timeout => 5 # 5 seconds API response timeout
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def ordered_keys
|
23
|
+
parameters.keys.sort
|
24
|
+
end
|
25
|
+
def parameter_string
|
26
|
+
ordered_keys.map do |key, value|
|
27
|
+
"#{key}=#{ ( key == 'term' )? URI.escape( parameters[key] ) : parameters[key]}"
|
28
|
+
end.join('&')
|
29
|
+
end
|
30
|
+
|
31
|
+
def authenticator
|
32
|
+
@auth ||= NetFlix::Authenticator.new(:request => self, :credentials => NetFlix.credentials)
|
33
|
+
end
|
34
|
+
|
35
|
+
def target
|
36
|
+
URI.parse "#{url}?#{parameter_string}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def log
|
40
|
+
NetFlix.log(target.to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def send
|
44
|
+
merged_cache_options = self.class.default_cache_options.merge(cache_options)
|
45
|
+
APICache.get(target.to_s, merged_cache_options ) do
|
46
|
+
authenticator.sign!
|
47
|
+
log
|
48
|
+
Net::HTTP.get(target)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_to_file(file_name)
|
53
|
+
authenticator.sign!
|
54
|
+
Net::HTTP.start(target.host, target.port) do |http|
|
55
|
+
File.open( file_name, 'w') do |file|
|
56
|
+
http.request_get(target.request_uri) do |response|
|
57
|
+
response.read_body do |body|
|
58
|
+
file.write(body)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.encode(value)
|
66
|
+
URI.escape( value.to_s, RESERVED_CHARACTERS ) if value
|
67
|
+
end
|
68
|
+
|
69
|
+
# validation stuff
|
70
|
+
has_collection :errors
|
71
|
+
|
72
|
+
def valid?
|
73
|
+
errors.clear
|
74
|
+
validate_http_method
|
75
|
+
errors.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_http_method
|
79
|
+
errors << "HTTP method must be POST or GET, but I got #{http_method}" unless ['POST', 'GET'].include? http_method
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module NetFlix
|
2
|
+
class Television < Title
|
3
|
+
|
4
|
+
def actors
|
5
|
+
@actors ||= ActorBuilder.from_movie(@xdoc)
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
# the nodes that correspond to the constructor argument
|
10
|
+
def self.node_xpath
|
11
|
+
"//catalog_title[not(contains(id/text(),'movies'))]"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module NetFlix
|
2
|
+
class Title
|
3
|
+
RATING_PREDICATE = %w{ G PG PG-13 R NC-17 NR }.map do |rating|
|
4
|
+
"@term=\"#{rating}\""
|
5
|
+
end.join(' or ')
|
6
|
+
|
7
|
+
def initialize(xml)
|
8
|
+
@xdoc = xml.is_a?(String) ? Nokogiri.parse( xml ) : xml
|
9
|
+
end
|
10
|
+
|
11
|
+
def actors
|
12
|
+
@actors ||= ActorBuilder.from_movie(@xdoc)
|
13
|
+
end
|
14
|
+
|
15
|
+
# not every title has a director!
|
16
|
+
def directors
|
17
|
+
@directors ||= ( Nokogiri.parse(fetch_link('directors')) / "/people/person/name/text()" ).to_a.map(&:to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
def rating
|
21
|
+
( @xdoc / "//catalog_title/category[#{RATING_PREDICATE}]/@term" ).to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def release_year
|
25
|
+
( @xdoc / "//catalog_title/release_year/text()" ).to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
# suppported title lengths are :short (the default) and :regular.
|
29
|
+
def title(length=:short)
|
30
|
+
( @xdoc / "//catalog_title/title/@#{length}" ).to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def images
|
34
|
+
HashWithIndifferentAccess.new(Crack::XML.parse(@xdoc.xpath('//catalog_title/box_art').to_s)['box_art'])
|
35
|
+
end
|
36
|
+
|
37
|
+
def synopsis
|
38
|
+
@synopsis ||= begin
|
39
|
+
Crack::XML.parse(fetch_link('synopsis'))['synopsis']
|
40
|
+
rescue
|
41
|
+
''
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def id
|
46
|
+
@id ||= begin
|
47
|
+
node = @xdoc.search('id').first
|
48
|
+
node.content if node
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def movie
|
53
|
+
@movie ||= NetFlix::Movie.new(NetFlix::Request.new(:url => id ).send) if is_movie?
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_movie?
|
57
|
+
id =~ /movies\/(\d+)/
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
title || 'unknown title'
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def fetch_link(title)
|
66
|
+
link_url = ( @xdoc / "//catalog_title/link[@title='#{title}']/@href" ).to_s
|
67
|
+
NetFlix::Request.new(:url => link_url ).send unless link_url.blank?
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def base_url
|
72
|
+
'http://api.netflix.com/catalog/titles'
|
73
|
+
end
|
74
|
+
|
75
|
+
def autocomplete(params)
|
76
|
+
(Nokogiri.parse(
|
77
|
+
NetFlix::Request.new(:url => base_url << '/autocomplete', :parameters => params).send
|
78
|
+
) / '//title/@short').to_a.map(&:to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def index(file_name)
|
82
|
+
NetFlix::Request.new( :url => NetFlix::Title.base_url + '/index').write_to_file(file_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def search(params)
|
86
|
+
parse(NetFlix::Request.new(:url => base_url, :parameters => params).send)
|
87
|
+
end
|
88
|
+
|
89
|
+
def find( params )
|
90
|
+
if params[:id]
|
91
|
+
new( NetFlix::Request.new(:url => params[:id]).send )
|
92
|
+
elsif params[:term]
|
93
|
+
search(params)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse(xml)
|
98
|
+
return [] unless xml
|
99
|
+
|
100
|
+
nxml = Nokogiri.XML(xml)
|
101
|
+
|
102
|
+
(nxml / node_xpath).map do |data|
|
103
|
+
self.new(data.to_s)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
protected
|
107
|
+
def node_xpath
|
108
|
+
'//catalog_title'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end # class Title
|
112
|
+
end # module NetFlix
|
113
|
+
|
data/lib/valuable.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
class Valuable
|
4
|
+
|
5
|
+
def attributes
|
6
|
+
@attributes ||= deep_duplicate_of(self.class.defaults)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(atts = {})
|
10
|
+
atts.each { |name, value| __send__("#{name}=", value ) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def deep_duplicate_of(value)
|
14
|
+
Marshal.load(Marshal.dump(value))
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
def attributes
|
20
|
+
@attributes ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
def defaults
|
24
|
+
@defaults ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_value(name, options={})
|
28
|
+
attributes << name
|
29
|
+
defaults[name] = options[:default] unless options[:default].nil?
|
30
|
+
|
31
|
+
create_accessor_for(name)
|
32
|
+
create_setter_for(name, options[:klass], options[:default])
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_setter_for(name, klass, default)
|
36
|
+
|
37
|
+
if klass == nil
|
38
|
+
define_method "#{name}=" do |value|
|
39
|
+
attributes[name] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
elsif klass == Integer
|
43
|
+
|
44
|
+
define_method "#{name}=" do |value|
|
45
|
+
value_as_integer = value && value.to_i
|
46
|
+
attributes[name] = value_as_integer
|
47
|
+
end
|
48
|
+
|
49
|
+
elsif klass == String
|
50
|
+
|
51
|
+
define_method "#{name}=" do |value|
|
52
|
+
value_as_string = value && value.to_s
|
53
|
+
attributes[name] = value_as_string
|
54
|
+
end
|
55
|
+
|
56
|
+
else
|
57
|
+
|
58
|
+
define_method "#{name}=" do |value|
|
59
|
+
if value.nil?
|
60
|
+
attributes[name] = nil
|
61
|
+
elsif value.is_a? klass
|
62
|
+
attributes[name] = value
|
63
|
+
else
|
64
|
+
attributes[name] = klass.new(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_accessor_for(name)
|
71
|
+
define_method name do
|
72
|
+
attributes[name]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_collection(name)
|
77
|
+
has_value(name, :default => [] )
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
class ActorBuilderTest < Test::Unit::TestCase
|
3
|
+
|
4
|
+
def test_that_actors_are_parsed
|
5
|
+
xml = %|<title_index_item>
|
6
|
+
<link href="http://api.netflix.com/catalog/people/98401" rel="http://schemas.netflix.com/catalog/person.actor" title="George Wendt"></link>
|
7
|
+
<link href="http://api.netflix.com/catalog/people/35954" rel="http://schemas.netflix.com/catalog/person.actor" title="Robert Hy Gorman"></link>
|
8
|
+
</title_index_item>
|
9
|
+
|
|
10
|
+
data = Nokogiri.XML(xml).search('//.')
|
11
|
+
assert_equal ['George Wendt', 'Robert Hy Gorman'], ActorBuilder.from_movie(data).sort
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_that_cast_reference_is_pulled
|
15
|
+
|
16
|
+
NetFlix::Request.expects(:new).with(:url => 'http://api.netflix.com/catalog/titles/movies/60024073/cast').returns(stub_everything(:send => '<xml/>'))
|
17
|
+
|
18
|
+
xml = %|
|
19
|
+
<title_index_item>
|
20
|
+
<link href="http://api.netflix.com/catalog/titles/movies/60024073/cast" rel="http://schemas.netflix.com/catalog/people.cast" title="cast"></link>
|
21
|
+
</title_index_item>
|
22
|
+
|
|
23
|
+
data = Nokogiri.XML(xml).search('//title_index_item')
|
24
|
+
|
25
|
+
ActorBuilder.from_movie(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_that_cast_list_is_parsable
|
29
|
+
xml = %|<people><person><id>http://api.netflix.com/catalog/people/20037237</id><name>Vanessa Bell Calloway</name><link href="http://api.netflix.com/catalog/people/20037237/filmography" rel="http://schemas.netflix.com/catalog/titles.filmography" title="filmography"></link><link href="http://www.netflix.com/RoleDisplay/Vanessa_Bell_Calloway/20037237" rel="alternate" title="web page"></link></person></people>|
|
30
|
+
|
31
|
+
assert_equal ['Vanessa Bell Calloway'], ActorBuilder.from_xml(xml)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
class AuthenticatorTest < Test::Unit::TestCase
|
3
|
+
|
4
|
+
def test_that_url_is_properly_encoded
|
5
|
+
fake_request = stub_everything(:url => 'http://photos.example.net/a pic')
|
6
|
+
assert_equal 'http%3A%2F%2Fphotos.example.net%2Fa%20pic', NetFlix::Authenticator.new(:request => fake_request).encoded_url
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_that_authentication_corresponds_to_known_values
|
10
|
+
#based on http://developer.netflix.com/docs/Security#0_pgfId-1015486
|
11
|
+
#based on http://term.ie/oauth/example/client.php
|
12
|
+
|
13
|
+
cred = stub_everything(
|
14
|
+
:key => '123456789012345678901234',
|
15
|
+
:secret => '1234567890',
|
16
|
+
:valid? => true)
|
17
|
+
|
18
|
+
expected_sbs = 'GET&http%3A%2F%2Fapi.netflix.com%2Fcatalog%2Ftitles&max_results%3D5%26oauth_consumer_key%3D123456789012345678901234%26oauth_nonce%3D24aea97b0b5dd516145966aaf2945c6a%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1233950836%26oauth_version%3D1.0%26term%3Dsneakers'
|
19
|
+
|
20
|
+
expected_signature = 'vlROEST9UXwI+zc9fld82giaYPE='
|
21
|
+
|
22
|
+
request = NetFlix::Request.new(
|
23
|
+
:url => 'http://api.netflix.com/catalog/titles',
|
24
|
+
:http_method => 'GET',
|
25
|
+
:parameters => {'term' => 'sneakers', 'max_results' => '5'})
|
26
|
+
|
27
|
+
auth = NetFlix::Authenticator.new(
|
28
|
+
:request => request,
|
29
|
+
:timestamp => 1233950836,
|
30
|
+
:nonce => '24aea97b0b5dd516145966aaf2945c6a',
|
31
|
+
:credentials => cred
|
32
|
+
)
|
33
|
+
|
34
|
+
assert_equal expected_sbs, auth.signature_base_string
|
35
|
+
assert_equal expected_signature, auth.signature
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_that_signature_key_includes_consumer_secret_and_ampersand
|
39
|
+
cred = stub_everything(:secret => 'shhh', :key => 'letmein')
|
40
|
+
assert_equal 'shhh&', NetFlix::Authenticator.new(:credentials => cred).signature_key
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_that_access_token_is_included_in_signature_key_if_it_is_available
|
44
|
+
cred = stub_everything(:secret => 'shhh', :key => 'letmein', :access_token => 'they_like_me')
|
45
|
+
request = stub_everything
|
46
|
+
|
47
|
+
assert_equal 'shhh&they_like_me', NetFlix::Authenticator.new(:request => request, :credentials => cred).signature_key
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_that_signature_is_added_to_parameters_during_signing
|
51
|
+
cred = stub_everything(:secred => 'shhh', :key => 'letmein', :valid? => true)
|
52
|
+
params = {}
|
53
|
+
|
54
|
+
request = stub_everything(:parameters => params)
|
55
|
+
auth = NetFlix::Authenticator.new(:request => request, :credentials => cred)
|
56
|
+
auth.sign!
|
57
|
+
assert params['oauth_signature']
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_that_exception_is_raised_if_secret_is_missing
|
61
|
+
credentials = stub_everything(:valid? => false)
|
62
|
+
authenticator = NetFlix::Authenticator.new(:credentials => credentials)
|
63
|
+
|
64
|
+
assert_raises ArgumentError do
|
65
|
+
authenticator.require_credentials
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CredentialsTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def file_contents
|
6
|
+
{:key => :value}.to_yaml
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_that_credentials_file_is_not_loaded_if_it_dne
|
10
|
+
File.stubs(:exist?).returns(false)
|
11
|
+
File.expects(:open).never
|
12
|
+
NetFlix::Credentials.from_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_that_credentials_file_is_loaded_if_present
|
16
|
+
File.stubs(:exist?).returns(true)
|
17
|
+
File.expects(:open).returns(file_contents)
|
18
|
+
NetFlix::Credentials.from_file
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_that_values_from_file_are_present
|
22
|
+
File.stubs(:exist?).returns(true)
|
23
|
+
File.stubs(:open).returns({:key => :my_key, :secret => 'quiet!'}.to_yaml)
|
24
|
+
assert_equal :my_key, NetFlix::Credentials.from_file.key
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<?xml version="1.0" standalone="yes"?>
|
2
|
+
|
3
|
+
<autocomplete>
|
4
|
+
<url_template>http://api.netflix.com/catalog/titles/autocomplete?{-join|&|term}</url_template>
|
5
|
+
<autocomplete_item>
|
6
|
+
<title short="Love Wrecked"></title>
|
7
|
+
</autocomplete_item>
|
8
|
+
<autocomplete_item>
|
9
|
+
<title short="Lovesickness"></title>
|
10
|
+
</autocomplete_item>
|
11
|
+
<autocomplete_item>
|
12
|
+
<title short="Loverboy"></title>
|
13
|
+
</autocomplete_item>
|
14
|
+
</autocomplete>
|
15
|
+
|