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.
@@ -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
+