flix4r 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|