ramazon_advertising 0.3.2
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 +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +46 -0
- data/Rakefile +84 -0
- data/VERSION +1 -0
- data/features/friendly_errors.feature +15 -0
- data/features/generate_root_browse_nodes.feature +13 -0
- data/features/getting_offer_details.feature +30 -0
- data/features/getting_search_bins.feature +22 -0
- data/features/retrieve_browse_node_information.feature +16 -0
- data/features/retrieving_a_product.feature +32 -0
- data/features/searching_for_products.feature +47 -0
- data/features/step_definitions/auth_steps.rb +8 -0
- data/features/step_definitions/browse_node_steps.rb +55 -0
- data/features/step_definitions/error_steps.rb +14 -0
- data/features/step_definitions/product_collection_steps.rb +20 -0
- data/features/step_definitions/product_steps.rb +75 -0
- data/features/step_definitions/ramazon_advertising_steps.rb +0 -0
- data/features/support/env.rb +7 -0
- data/features/support/ramazon_advertising.example.yml +2 -0
- data/lib/ramazon/abstract_element.rb +18 -0
- data/lib/ramazon/browse_node.rb +69 -0
- data/lib/ramazon/configuration.rb +66 -0
- data/lib/ramazon/error.rb +12 -0
- data/lib/ramazon/image.rb +18 -0
- data/lib/ramazon/merchant.rb +21 -0
- data/lib/ramazon/offer.rb +27 -0
- data/lib/ramazon/price.rb +18 -0
- data/lib/ramazon/product.rb +303 -0
- data/lib/ramazon/product_collection.rb +30 -0
- data/lib/ramazon/rails_additions.rb +99 -0
- data/lib/ramazon/request.rb +82 -0
- data/lib/ramazon/search_bin.rb +11 -0
- data/lib/ramazon/search_bin_parameter.rb +9 -0
- data/lib/ramazon/search_bin_set.rb +11 -0
- data/lib/ramazon/signatory.rb +10 -0
- data/lib/ramazon_advertising.rb +41 -0
- data/lib/root_nodes.yml +64 -0
- data/lib/tasks/ramazon.rake +9 -0
- data/spec/ramazon/configuration_spec.rb +21 -0
- data/spec/spec_helper.rb +11 -0
- metadata +157 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
When /^I try to find the asin "([^\"]*)"$/ do |asin|
|
2
|
+
@products = Ramazon::Product.find(:item_id => asin, :response_group => ["Medium", "BrowseNodes", "Offers"])
|
3
|
+
@product = @products[0]
|
4
|
+
end
|
5
|
+
|
6
|
+
Given /^I am searching with the "([^\"]*)" of "([^\"]*)"$/ do |attr, value|
|
7
|
+
@search_options ||= {}
|
8
|
+
@search_options[attr.to_sym] = value
|
9
|
+
end
|
10
|
+
|
11
|
+
When /^I perform the product search$/ do
|
12
|
+
begin
|
13
|
+
@search_options ||= {}
|
14
|
+
@products = Ramazon::Product.find(@search_options)
|
15
|
+
rescue Ramazon::Error => e
|
16
|
+
@error = e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Then /^I should get a list of products$/ do
|
21
|
+
raise @error if @error
|
22
|
+
@products.should_not be_empty
|
23
|
+
end
|
24
|
+
|
25
|
+
Then /^the list of products should have more than (\d+) product$/ do |count|
|
26
|
+
@products.should have_at_least(count.to_i + 1).items
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
Then /^each product should have the "([^\"]*)" "([^\"]*)"$/ do |attr, value|
|
31
|
+
@products.each do |p|
|
32
|
+
p.send(attr).should eql(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Then /^I should get a product$/ do
|
37
|
+
raise @error if @error
|
38
|
+
@product = @products[0] unless @product
|
39
|
+
@product.should_not be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
Then /^the product should have (the\s)?"([^\"]*)" "([^\"]*)"$/ do |the, attr, value|
|
43
|
+
@product.send(attr).should eql(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
Then /^the product should have (a\s)?"([^\"]*)"$/ do |a, attr|
|
47
|
+
@product.send(attr).should_not be_nil
|
48
|
+
end
|
49
|
+
|
50
|
+
Then /^each of the product's "([^\"]*)" should have a "([^\"]*)"$/ do |collection_name, attr|
|
51
|
+
@product.send(collection_name).should_not be_empty
|
52
|
+
@product.send(collection_name).each do |i|
|
53
|
+
i.send(attr).should_not be_nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Then /^each of the product's "([^\"]*)" should have a "([^\"]*)" of "([^\"]*)"$/ do |collection_name, attr, value|
|
58
|
+
@product.send(collection_name).should_not be_empty
|
59
|
+
@product.send(collection_name).each do |i|
|
60
|
+
i.send(attr).should_not be_nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
Then /^the product should have a category tree for "([^\"]*)"$/ do |category_name|
|
66
|
+
@product.category_tree[category_name].should_not be_nil
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
Then /^each product should have (a\s)?"([^\"]*)"$/ do |a, attr|
|
71
|
+
@products.each do |p|
|
72
|
+
p.send(attr).should_not be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
File without changes
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Ramazon
|
2
|
+
module AbstractElement
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def abstract_element(name, type, options = {})
|
9
|
+
element(name, type, options.merge(:parser => :abstract_parse, :raw => true))
|
10
|
+
end
|
11
|
+
|
12
|
+
def abstract_parse(xml, options = {})
|
13
|
+
tag XML::Parser.string(xml).parse.root.name
|
14
|
+
parse(xml)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Ramazon
|
2
|
+
class BrowseNode
|
3
|
+
include HTTParty
|
4
|
+
include HappyMapper
|
5
|
+
|
6
|
+
tag "BrowseNode"
|
7
|
+
|
8
|
+
element :name, String, :tag => "Name"
|
9
|
+
element :node_id, String, :tag => "BrowseNodeId"
|
10
|
+
element :children, Ramazon::BrowseNode, :tag => "Children/BrowseNode"
|
11
|
+
element :is_category_root, Boolean, :tag => "IsCategoryRoot"
|
12
|
+
|
13
|
+
attr_accessor :child
|
14
|
+
|
15
|
+
DEFAULT_ROOT_FILE = File.join(File.dirname(__FILE__), '..', 'root_nodes.yml')
|
16
|
+
|
17
|
+
def self.generate_root_nodes(file_name = DEFAULT_ROOT_FILE)
|
18
|
+
if Ramazon::Configuration.locale == :us
|
19
|
+
doc = Nokogiri::HTML(get('http://www.amazon.com').body)
|
20
|
+
root_nodes = {}
|
21
|
+
doc.search(".navSaMenu .navSaChildItem a").each do |element|
|
22
|
+
if element["href"] =~ /node=(\d+)\&/
|
23
|
+
root_nodes[element.content] = $1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
unless root_nodes.empty?
|
28
|
+
FileUtils.rm_f(file_name)
|
29
|
+
File.open(file_name, 'w') do |f|
|
30
|
+
f.write(root_nodes.to_yaml)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else
|
34
|
+
#todo correlate ECS locales to actual amazon.* urls
|
35
|
+
raise "generating root nodes for locale's other than the US is not supported"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.root_nodes(file_name = DEFAULT_ROOT_FILE)
|
40
|
+
@root_nodes ||= File.open(file_name) { |yf| YAML::load(yf) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# find a browse node based on its id
|
44
|
+
# @param node_id [String] the browse node you're looking for
|
45
|
+
# @returns [Ramazon::BrowseNode] the node you're looking for
|
46
|
+
def self.find(node_id)
|
47
|
+
req = Ramazon::Request.new(:operation => "BrowseNodeLookup", :browse_node_id => node_id)
|
48
|
+
res = req.submit
|
49
|
+
parse(res.to_s)[0]
|
50
|
+
end
|
51
|
+
|
52
|
+
# get a hash of name -> child browse_nodes
|
53
|
+
# @returns [Hash] stringified hash of names => Ramazon::BrowseNode objects
|
54
|
+
def child_hash
|
55
|
+
if !@child_hash
|
56
|
+
@child_hash = {}
|
57
|
+
self.children.each do |i|
|
58
|
+
@child_hash[i.name] = i
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
@child_hash
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.parse(xml, options = {})
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# the class that persists configuration information for Amazon requests
|
3
|
+
class Configuration
|
4
|
+
class << self
|
5
|
+
LOCALES = [:ca, :de, :fr, :jp, :uk, :us]
|
6
|
+
|
7
|
+
# set the locale for future requests
|
8
|
+
# will raise an exception if the locale isn't in the Configuration::LOCALES collection
|
9
|
+
def locale=(locale)
|
10
|
+
if LOCALES.include?(locale)
|
11
|
+
configatron.ramazon.locale = locale
|
12
|
+
else
|
13
|
+
raise "unknown locale"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# get the current locale (defaults to the us)
|
18
|
+
def locale
|
19
|
+
configatron.ramazon.locale || :us
|
20
|
+
end
|
21
|
+
|
22
|
+
# get the current access key
|
23
|
+
def access_key
|
24
|
+
configatron.ramazon.access_key
|
25
|
+
end
|
26
|
+
|
27
|
+
# set the current access key
|
28
|
+
# @param key [String] access key you're using to access the advertising api
|
29
|
+
def access_key=(key)
|
30
|
+
configatron.ramazon.access_key = key
|
31
|
+
end
|
32
|
+
|
33
|
+
# get the current secret key that is used for request signing
|
34
|
+
def secret_key
|
35
|
+
configatron.ramazon.secret_key
|
36
|
+
end
|
37
|
+
|
38
|
+
# set the secret key so that requests can be appropriately signed
|
39
|
+
# @param key [String] secret key you're using to sign advertising requests
|
40
|
+
def secret_key=(key)
|
41
|
+
configatron.ramazon.secret_key = key
|
42
|
+
end
|
43
|
+
|
44
|
+
# get the correct host based on your locale
|
45
|
+
def base_uri
|
46
|
+
if locale == :us
|
47
|
+
"http://ecs.amazonaws.com"
|
48
|
+
else
|
49
|
+
"http://ecs.amazonaws.#{locale}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# get the full path including locale specific host and /onca/xml path
|
54
|
+
def uri
|
55
|
+
"#{base_uri}#{path}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# get the path where requests should be dispatched to
|
59
|
+
def path
|
60
|
+
"/onca/xml"
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# An image object returned by Amazon's product data
|
3
|
+
# Available accessors include
|
4
|
+
# +url+::
|
5
|
+
# The url of the image
|
6
|
+
# +height+::
|
7
|
+
# The height of the image
|
8
|
+
# +width+::
|
9
|
+
# The width of the image
|
10
|
+
class Image
|
11
|
+
include HappyMapper
|
12
|
+
include Ramazon::AbstractElement
|
13
|
+
tag "Image"
|
14
|
+
element :url, String, :tag => "URL"
|
15
|
+
element :height, Integer, :tag => "Height"
|
16
|
+
element :width, Integer, :tag => "Width"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# Merchant Details
|
3
|
+
# Current supports the following accessors
|
4
|
+
# +merchant_id+::
|
5
|
+
# The id of the merchant
|
6
|
+
# +glance_page_url+::
|
7
|
+
# The url of the merchant
|
8
|
+
# +average_feedback_rating+::
|
9
|
+
# Average Feedback Rating
|
10
|
+
# +total_feedback+::
|
11
|
+
# Total Feedback
|
12
|
+
class Merchant
|
13
|
+
include HappyMapper
|
14
|
+
tag "Merchant"
|
15
|
+
|
16
|
+
element :merchant_id, String, :tag => "MerchantId"
|
17
|
+
element :glance_page_url, String, :tag => "GlancePage"
|
18
|
+
element :average_feedback_rating, Float, :tag => "AverageFeedbackRating"
|
19
|
+
element :total_feedback, Integer, :tag => "TotalFeedback"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# Offer Details
|
3
|
+
# Currently supports the following accessors
|
4
|
+
# +merchant+::
|
5
|
+
# The merchant that is listing this product (NOTE: Returns Ramazon::Merchant object)
|
6
|
+
# +condition+::
|
7
|
+
# The condition of the product
|
8
|
+
# +sub_condition+::
|
9
|
+
# The subcondition of the product
|
10
|
+
# +listing_id+::
|
11
|
+
# The id of the listing
|
12
|
+
# +price+::
|
13
|
+
# The asking price of the listing (NOTE: Returns Ramazon::Price object)
|
14
|
+
class Offer
|
15
|
+
include HappyMapper
|
16
|
+
include Ramazon::AbstractElement
|
17
|
+
|
18
|
+
tag "Offer"
|
19
|
+
|
20
|
+
has_one :merchant, Ramazon::Merchant, :tag => "Merchant"
|
21
|
+
element :condition, String, :tag => "OfferAttributes/Condition"
|
22
|
+
element :sub_condition, String, :tag => "OfferAttributes/SubCondition"
|
23
|
+
element :listing_id, String, :tag => "OfferListing/OfferListingId"
|
24
|
+
abstract_element :price, Ramazon::Price, :tag => "OfferListing/Price"
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# A price object returned by Amazon's product data
|
3
|
+
# Available accessors include
|
4
|
+
# +amount+::
|
5
|
+
# The unformatted ammount of the price
|
6
|
+
# +currency_code+::
|
7
|
+
# The currency of the offer
|
8
|
+
# +formatted_price+::
|
9
|
+
# The formatted price of the offer
|
10
|
+
class Price
|
11
|
+
include HappyMapper
|
12
|
+
include Ramazon::AbstractElement
|
13
|
+
|
14
|
+
element :amount, String, :tag => "Amount"
|
15
|
+
element :currency_code, String, :tag => "CurrencyCode"
|
16
|
+
element :formatted_price, String, :tag => "FormattedPrice"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
module Ramazon
|
2
|
+
# Find and get product details with this class
|
3
|
+
# Currently supports the following accessors
|
4
|
+
# (all other elements can be accessed via nokogiri selectors and the get method)
|
5
|
+
# +asin+::
|
6
|
+
# Amazon Identifier
|
7
|
+
# +upc+::
|
8
|
+
# UPC ID
|
9
|
+
# +title+::
|
10
|
+
# Title of the product
|
11
|
+
# +product_group+::
|
12
|
+
# The category/product_group of the product
|
13
|
+
# +manufacturer+::
|
14
|
+
# The manufacturer of the product
|
15
|
+
# +brand+::
|
16
|
+
# The brand of the product
|
17
|
+
# +url+::
|
18
|
+
# The Amazon URL of the product
|
19
|
+
# +small_image+::
|
20
|
+
# The small image that Amazon provides (NOTE: Returns Ramazon::Image object)
|
21
|
+
# +medium_image+::
|
22
|
+
# The medium image that Amazon provides (NOTE: Returns Ramazon::Image object)
|
23
|
+
# +large_image+::
|
24
|
+
# The large image that Amazon provides (NOTE: Returns Ramazon::Image object)
|
25
|
+
# +list_price+::
|
26
|
+
# The list price of the item (NOTE: Returns Ramazon::Price object)
|
27
|
+
# +lowest_new_price+::
|
28
|
+
# The lowest new price from the offer summary (NOTE: Returns Ramazon::Price object)
|
29
|
+
# +sales_rank+::
|
30
|
+
# The sales rank of the product
|
31
|
+
# +new_count+::
|
32
|
+
# The quantity of new item offers
|
33
|
+
# +used_count+::
|
34
|
+
# The quantity of used item offers
|
35
|
+
# +collectible_count+::
|
36
|
+
# The quantity of collectible item offers
|
37
|
+
# +refurbished_count+::
|
38
|
+
# The quantity of refurbished item offers
|
39
|
+
# +release_date+::
|
40
|
+
# The release date of the product
|
41
|
+
# +original_release_date+::
|
42
|
+
# The original release date of the product
|
43
|
+
# +offers+::
|
44
|
+
# The collection of offers available for the given product
|
45
|
+
# @example find an individual item
|
46
|
+
# @products = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")
|
47
|
+
# @products[0].title
|
48
|
+
# @products[0].asin
|
49
|
+
# @products[0].upc
|
50
|
+
# @products[0].large_image.url
|
51
|
+
# @products[0].url
|
52
|
+
#
|
53
|
+
|
54
|
+
class Product
|
55
|
+
include HappyMapper
|
56
|
+
include Ramazon::AbstractElement
|
57
|
+
tag 'Item'
|
58
|
+
|
59
|
+
element :asin, String, :tag => 'ASIN'
|
60
|
+
element :upc, String, :tag => 'ItemAttributes/UPC'
|
61
|
+
element :title, String, :tag => 'Title', :deep => true
|
62
|
+
element :product_group, String, :tag => 'ItemAttributes/ProductGroup'
|
63
|
+
element :manufacturer, String, :tag => 'ItemAttributes/Manufacturer'
|
64
|
+
element :brand, String, :tag => 'ItemAttributes/Brand'
|
65
|
+
element :url, String, :tag => 'DetailPageURL'
|
66
|
+
abstract_element :small_image, Ramazon::Image, :tag => "SmallImage"
|
67
|
+
abstract_element :large_image, Ramazon::Image, :tag => "LargeImage"
|
68
|
+
abstract_element :medium_image, Ramazon::Image, :tag => "MediumImage"
|
69
|
+
abstract_element :tiny_image, Ramazon::Image, :tag => "TinyImage"
|
70
|
+
abstract_element :thumb_image, Ramazon::Image, :tag => "ThumbImage"
|
71
|
+
abstract_element :list_price, Ramazon::Price, :tag => "ItemAttributes/ListPrice"
|
72
|
+
abstract_element :lowest_new_price, Ramazon::Price, :tag => "OfferSummary/LowestNewPrice"
|
73
|
+
has_many :offers, Ramazon::Offer, :tag => "Offers/Offer"
|
74
|
+
|
75
|
+
element :sales_rank, Integer, :tag => "SalesRank"
|
76
|
+
|
77
|
+
element :new_count, Integer, :tag => "OfferSummary/TotalNew"
|
78
|
+
element :used_count, Integer, :tag => "OfferSummary/TotalUsed"
|
79
|
+
element :collectible_count, Integer, :tag => "OfferSummary/TotalCollectible"
|
80
|
+
element :refurbished_count, Integer, :tag => "OfferSummary/TotalRefurbished"
|
81
|
+
|
82
|
+
element :release_date, Date, :tag => 'ItemAttributes/ReleaseDate'
|
83
|
+
element :original_release_date, Date, :tag => 'ItemAttributes/OriginalReleaseDate'
|
84
|
+
|
85
|
+
attr_accessor :has_first_page_of_full_offers
|
86
|
+
|
87
|
+
# Creates the worker that performs the delta indexing
|
88
|
+
# @param options Amazon request options (you can use an underscore convention)
|
89
|
+
# (ie. passing the :response_group option will be converted to "ResponseGroup")
|
90
|
+
# <tt>:item_id</tt> - the ASIN or UPC you're looking for
|
91
|
+
# @return [Array] array of Ramazon::Product objects
|
92
|
+
def self.find(*args)
|
93
|
+
options = args.extract_options!
|
94
|
+
if options[:item_id]
|
95
|
+
item_lookup(options[:item_id], options)
|
96
|
+
else
|
97
|
+
options[:operation] ||= "ItemSearch"
|
98
|
+
options[:search_index] ||= "Blended"
|
99
|
+
options[:item_page] ||= 1
|
100
|
+
res = Ramazon::Request.new(options).submit
|
101
|
+
products = Ramazon::ProductCollection.create_from_results(options[:item_page] || 1, 10, res)
|
102
|
+
if find_options_retrieve_all_offers?(options)
|
103
|
+
products.each do |p|
|
104
|
+
p.has_first_page_of_full_offers = true
|
105
|
+
p.offer_pages = offer_pages_for(p)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
products
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Performs an item lookup
|
113
|
+
# @param item_id the ASIN or UPC you're looking for
|
114
|
+
# @options additional Amazon request options (i.e. :response_group)
|
115
|
+
def self.item_lookup(item_id, options = {})
|
116
|
+
req = Ramazon::Request.new({:item_id => item_id,
|
117
|
+
:operation => "ItemLookup"}.merge(options))
|
118
|
+
res = req.submit
|
119
|
+
|
120
|
+
Ramazon::ProductCollection.create_from_results(1,1,res)
|
121
|
+
end
|
122
|
+
|
123
|
+
# assembles the available images for the object
|
124
|
+
# @return [Hash] hash of symbolized image_name => Ramazon::Image pairs
|
125
|
+
def images
|
126
|
+
if !@images
|
127
|
+
@images = {}
|
128
|
+
@images[:thumb] = self.thumb_image if self.thumb_image
|
129
|
+
@images[:tiny_image] = self.tiny_image if self.tiny_image
|
130
|
+
@images[:small] = self.small_image if self.small_image
|
131
|
+
@images[:medium] = self.medium_image if self.medium_image
|
132
|
+
@images[:large] = self.large_image if self.large_image
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
attr_accessor :xml_doc
|
137
|
+
def self.parse(xml, options = {})
|
138
|
+
node = XML::Parser.string(xml.to_s).parse.root
|
139
|
+
node.find("//Item").collect do |n|
|
140
|
+
p = super(n.to_s)
|
141
|
+
p.xml_doc = Nokogiri::XML.parse(n.to_s)
|
142
|
+
p
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# perform a nokogiri search on the product's XML
|
147
|
+
# @param args Passes directly to a Nokogiri::Xml.parse(xml).search method
|
148
|
+
# @example find the actor
|
149
|
+
# @product = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")[0]
|
150
|
+
# @product.get("ItemAttributes Actor").collect{|a| a.content}
|
151
|
+
def get(*args)
|
152
|
+
result = @xml_doc.search(args)
|
153
|
+
end
|
154
|
+
|
155
|
+
# returns a hash of category browse nodes from the top down
|
156
|
+
def category_tree
|
157
|
+
@category_tree = {}
|
158
|
+
get_category_browse_nodes.each do |n|
|
159
|
+
build_category_tree(n)
|
160
|
+
end
|
161
|
+
@category_tree
|
162
|
+
end
|
163
|
+
|
164
|
+
# a sorted list of used offers
|
165
|
+
def used_offers
|
166
|
+
if @used_offers.nil?
|
167
|
+
@used_offers = []
|
168
|
+
self.offers.each do |o|
|
169
|
+
if o.condition.downcase == "used"
|
170
|
+
@used_offers << o
|
171
|
+
end
|
172
|
+
end
|
173
|
+
@used_offers.sort!{|a,b| a.price.amount <=> b.price.amount}
|
174
|
+
end
|
175
|
+
@used_offers
|
176
|
+
end
|
177
|
+
|
178
|
+
# breaks down all the offers in a nested hash of [condition][subcondition]
|
179
|
+
# note: this will load ALL offers into memory
|
180
|
+
# @return [Hash] a nest hash of offers [condition][subcondition] => Array of Ramazon::Offer objects
|
181
|
+
def offers_by_condition
|
182
|
+
@offer_hash = {}
|
183
|
+
offer_page = 1
|
184
|
+
|
185
|
+
offers = offer_page(offer_page)
|
186
|
+
while offer_page <= offer_pages
|
187
|
+
offers.each do |o|
|
188
|
+
@offer_hash[o.condition.downcase] ||= {}
|
189
|
+
@offer_hash[o.condition.downcase][o.sub_condition] ||= []
|
190
|
+
@offer_hash[o.condition.downcase][o.sub_condition] << o
|
191
|
+
end
|
192
|
+
|
193
|
+
offer_page += 1
|
194
|
+
offers = offer_page(offer_page)
|
195
|
+
end
|
196
|
+
|
197
|
+
@offer_hash
|
198
|
+
end
|
199
|
+
|
200
|
+
# gets the number of offer pages for the specified product
|
201
|
+
# @param [Ramazon::Product] product the we want to get offer pages for
|
202
|
+
# @returns [Integer] number of offer pages
|
203
|
+
def self.offer_pages_for(product)
|
204
|
+
if !@offer_pages
|
205
|
+
offer_page_tags = product.get("//Offers/TotalOfferPages")
|
206
|
+
if offer_page_tags.size > 0
|
207
|
+
offer_pages = offer_page_tags[0].content.to_i
|
208
|
+
else
|
209
|
+
offer_pages = 1
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
offer_pages
|
214
|
+
end
|
215
|
+
|
216
|
+
# get the lowest offers broken down by subcondition
|
217
|
+
# @return [Hash] a nested hash of prices ie ["new"]["mint"] => Ramazon::Offer
|
218
|
+
def lowest_offers
|
219
|
+
if @lowest_offers.nil?
|
220
|
+
@lowest_offers = {}
|
221
|
+
offers_by_condition.each do |condition, sub_conditions|
|
222
|
+
@lowest_offers[condition] = {}
|
223
|
+
sub_conditions.each do |sub_condition, col|
|
224
|
+
sorted_offers = col.sort{|a,b| a.price.amount.to_i <=> b.price.amount.to_i}
|
225
|
+
@lowest_offers[condition][sub_condition] = sorted_offers.first
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
@lowest_offers
|
231
|
+
end
|
232
|
+
|
233
|
+
def offer_pages=(pages)
|
234
|
+
@offer_pages = pages.to_i
|
235
|
+
end
|
236
|
+
|
237
|
+
def offer_pages
|
238
|
+
@offer_pages
|
239
|
+
end
|
240
|
+
|
241
|
+
#get offers from a given page
|
242
|
+
# @param page [Integer] the page number you want to get
|
243
|
+
# @return [Array] Array of Offers returned from the page
|
244
|
+
def offer_page(page = 1)
|
245
|
+
#get all offers
|
246
|
+
if page == 1 && has_first_page_of_full_offers
|
247
|
+
self.offers
|
248
|
+
else
|
249
|
+
products = self.class.find(:item_id => self.asin,
|
250
|
+
:response_group => "OfferListings",
|
251
|
+
:merchant_id => "All",
|
252
|
+
:condition => "All",
|
253
|
+
:offer_page => page)
|
254
|
+
|
255
|
+
if products
|
256
|
+
product = products[0]
|
257
|
+
self.offer_pages = self.class.offer_pages_for(product)
|
258
|
+
product.offers
|
259
|
+
else
|
260
|
+
[]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
# recursive function used to generate a topdown category tree
|
267
|
+
def build_category_tree(n, child = nil)
|
268
|
+
amz_node = BrowseNode.parse(n.to_s)
|
269
|
+
amz_node.child = child unless child.nil?
|
270
|
+
|
271
|
+
if n.search("./IsCategoryRoot").size > 0
|
272
|
+
@category_tree[amz_node.name] ||= []
|
273
|
+
@category_tree[amz_node.name] << amz_node
|
274
|
+
else
|
275
|
+
parents = n.search("./Ancestors/BrowseNode")
|
276
|
+
if parents.size > 0
|
277
|
+
build_category_tree(parents[0], amz_node)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
def get_category_browse_nodes
|
285
|
+
self.get("BrowseNodes BrowseNode IsCategoryRoot").collect do |n|
|
286
|
+
n.ancestors("//BrowseNodes/BrowseNode")
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.find_options_retrieve_all_offers?(options = {})
|
291
|
+
if options[:response_group].is_a?(Array)
|
292
|
+
groups = options[:response_group].join(" ")
|
293
|
+
else
|
294
|
+
groups = options[:response_group] || ""
|
295
|
+
end
|
296
|
+
|
297
|
+
groups =~ /OfferListings/ &&
|
298
|
+
(options[:offer_page].nil? || options[:offer_page].to_i == 1) &&
|
299
|
+
(options[:merchant_id] == "All") &&
|
300
|
+
(options[:condition] == "All")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|