flix4r 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ module NetFlix
2
+ class Movie < Title
3
+
4
+ protected
5
+ # the nodes that correspond to the constructor argument
6
+ def self.node_xpath
7
+ "//catalog_title[contains(id/text(),'movies')]"
8
+ end
9
+ end
10
+ end
@@ -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|&amp;|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
+