bazaar_sources 0.2.1.1.1.4
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/README.rdoc +7 -0
- data/init.rb +1 -0
- data/lib/api_helpers/amazon.rb +606 -0
- data/lib/api_helpers/api_helper.rb +9 -0
- data/lib/api_helpers/external_url.rb +127 -0
- data/lib/api_helpers/httparty_nokogiri_parser.rb +14 -0
- data/lib/api_helpers/reseller_ratings_api.rb +174 -0
- data/lib/api_helpers/shopping.rb +224 -0
- data/lib/api_helpers/shopping_bulk_api.rb +514 -0
- data/lib/api_helpers/shopzilla_api.rb +230 -0
- data/lib/bazaar_sources.rb +35 -0
- data/lib/sources/amazon_source.rb +94 -0
- data/lib/sources/buy_source.rb +34 -0
- data/lib/sources/ebay_source.rb +35 -0
- data/lib/sources/epinions_source.rb +80 -0
- data/lib/sources/google_source.rb +119 -0
- data/lib/sources/price_grabber_source.rb +94 -0
- data/lib/sources/reseller_ratings_source.rb +47 -0
- data/lib/sources/shopping_source.rb +136 -0
- data/lib/sources/shopzilla_source.rb +108 -0
- data/lib/sources/simple_sources.yml +71 -0
- data/lib/sources/source.rb +242 -0
- metadata +137 -0
@@ -0,0 +1,514 @@
|
|
1
|
+
module ShoppingBulkAPI
|
2
|
+
require 'hpricot'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
class SearchType
|
7
|
+
SHOPPING_PRODUCT_ID = 'SHOPPING_PRODUCT_ID' # Shopping Product ID-based search
|
8
|
+
PRODUCT = 'PRODUCT' # Search by our products
|
9
|
+
KEYWORDS = 'KEYWORDS' # Search by a 1 or more keywords
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.get_all_categories
|
13
|
+
params = {
|
14
|
+
'categoryId' => 0,
|
15
|
+
'showAllDescendants' => true
|
16
|
+
}
|
17
|
+
result = make_v3_request :CategoryTree, params
|
18
|
+
parse_category(result.at('category[@id="0"]'))
|
19
|
+
end
|
20
|
+
|
21
|
+
# parse a category, then look for sub-categories and parse those too!
|
22
|
+
def self.parse_category(category, parent_id=nil)
|
23
|
+
categories = []
|
24
|
+
id = category.attributes['id'].to_i
|
25
|
+
# main category, does not count as a parent and should not be added
|
26
|
+
if id == 0
|
27
|
+
name = nil
|
28
|
+
id = nil
|
29
|
+
else
|
30
|
+
name = category.at('name').innerText
|
31
|
+
end
|
32
|
+
hash = {
|
33
|
+
:banned => banned_categories.include?(name),
|
34
|
+
:id => id,
|
35
|
+
:name => name,
|
36
|
+
:parent_id => parent_id
|
37
|
+
}
|
38
|
+
if sub_categories = category.at('categories')
|
39
|
+
categories << hash.merge({:end_point => false}) unless name == nil || id == nil
|
40
|
+
# if there are sub categories, we don't want to add the parent to the list
|
41
|
+
# that we'll be searching.
|
42
|
+
(sub_categories / '> category').each do |sub_category|
|
43
|
+
categories += parse_category(sub_category, id)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
categories << hash.merge({:end_point => true})
|
47
|
+
end
|
48
|
+
|
49
|
+
categories
|
50
|
+
end
|
51
|
+
|
52
|
+
# batch lookup products
|
53
|
+
# takes a search hash, which should have a :search_type key (one member from SearchType Enum above)
|
54
|
+
# for searching products, pass an array of products in :products (:search_type => ShoppingBulkAPI::SearchType::PRODUCT)
|
55
|
+
# for searching from shopping product ids, pass an array of shopping product ids in :shopping_product_ids (:search_type => ShoppingBulkAPI::SearchType::SHOPPING_PRODUCT_ID)
|
56
|
+
# for those two above, you can pass :batch_lookup, which is how many we should look up w/ shopping at once
|
57
|
+
# for searching a keyword, pass a :keywords array of strings (any you want to be included in results, ordered) (:search_type => ShoppingBulkAPI::SearchType::KEYWORDS)
|
58
|
+
def self.batch_search_v3(search_hash, sandbox=false)
|
59
|
+
search_hash[:batch_lookup] ||= 20
|
60
|
+
|
61
|
+
case search_hash[:search_type]
|
62
|
+
when SearchType::SHOPPING_PRODUCT_ID
|
63
|
+
items = search_hash[:shopping_product_ids]
|
64
|
+
search_hash[:get_extra_product_info] ||= false
|
65
|
+
when SearchType::PRODUCT
|
66
|
+
# list of shopping product ids to their associated product
|
67
|
+
# useful to bring back the shopping_product_ids into products
|
68
|
+
search_hash[:product_ids_hash] = search_hash[:products].inject({}) do |ha, product|
|
69
|
+
shopping_product_source = product.shopping_ids.detect{|product_source| !product_source.questionable?}
|
70
|
+
unless shopping_product_source.nil?
|
71
|
+
shopping_id = shopping_product_source.source_id
|
72
|
+
if ha.has_key?(shopping_id)
|
73
|
+
puts "DUPLICATE KEY FOR #{shopping_id} !! #{ha[shopping_id].inspect} VS #{product.id}"
|
74
|
+
end
|
75
|
+
ha[shopping_id] = product.id
|
76
|
+
end
|
77
|
+
ha
|
78
|
+
end
|
79
|
+
# just the shopping product ids
|
80
|
+
search_hash[:product_ids] = search_hash[:product_ids_hash].keys
|
81
|
+
items = search_hash[:product_ids]
|
82
|
+
search_hash[:get_extra_product_info] ||= false
|
83
|
+
when SearchType::KEYWORDS
|
84
|
+
items = search_hash[:keywords]
|
85
|
+
search_hash[:get_extra_product_info] = true # force extra info, how else will we get the name/etc. ?!
|
86
|
+
else
|
87
|
+
raise ArgumentError, "Invalid :search_type specified: #{search_hash[:search_type].inspect}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# defaults
|
91
|
+
all_offers = self.default_offers
|
92
|
+
all_product_infos = self.default_product_infos(search_hash[:get_extra_product_info])
|
93
|
+
missed_ids = []
|
94
|
+
second_misses = []
|
95
|
+
# puts "SEARCH HASH: #{search_hash.inspect}"
|
96
|
+
# look 'em up in batches!
|
97
|
+
items.each_slice(search_hash[:batch_lookup]) do |batch_items|
|
98
|
+
search_hash[:batch_items] = batch_items
|
99
|
+
misses, offers, product_infos = self.single_batch_search_v3(search_hash, sandbox)
|
100
|
+
all_product_infos.update(product_infos)
|
101
|
+
all_offers.update(offers)
|
102
|
+
missed_ids += misses unless misses.empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
# for the ones we missed, we're going to try looking them up one more time
|
106
|
+
# before giving up entirely.
|
107
|
+
# (only applies to non-category/keyword searches)
|
108
|
+
if missed_ids.length > 0
|
109
|
+
# now look up the missed IDs in their own batch
|
110
|
+
missed_ids.each_slice(search_hash[:batch_lookup]) do |batch_items|
|
111
|
+
search_hash[:batch_items] = batch_items
|
112
|
+
misses, offers, product_infos = self.single_batch_search_v3(search_hash, sandbox)
|
113
|
+
all_product_infos.update(product_infos)
|
114
|
+
all_offers.update(offers)
|
115
|
+
second_misses += misses unless misses.empty?
|
116
|
+
offers = nil
|
117
|
+
product_infos = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# only care to look up one-by-one if we're going to do something with the data
|
122
|
+
# (for product lookups, then, to hide or update shopping ids)
|
123
|
+
if !second_misses.empty? && search_hash[:search_type] == SearchType::PRODUCT
|
124
|
+
# missed again? gotta look up one-by-one!
|
125
|
+
products_to_hide = []
|
126
|
+
second_misses.each do |product_id|
|
127
|
+
our_product_id = search_hash[:product_ids_hash][product_id]
|
128
|
+
search_hash[:batch_items] = [product_id]
|
129
|
+
final_miss, offers, product_infos = self.single_batch_search_v3(search_hash, sandbox)
|
130
|
+
all_product_infos.update(product_infos)
|
131
|
+
all_offers.update(offers)
|
132
|
+
if !final_miss.empty?
|
133
|
+
puts "****** COULDN'T LOOK UP INFO FOR #{our_product_id} ( SHOPPING ID #{product_id}) !! Adding to hide queue..."
|
134
|
+
products_to_hide << product_id
|
135
|
+
else
|
136
|
+
# shopping gave us a product ID back that doesn't match our shopping product ID! gotta update!
|
137
|
+
new_shopping_id = product_infos[our_product_id][:reported_product_id]
|
138
|
+
if ps = ProductSource.find_by_source_name_and_source_id(ProductSource::Name::SHOPPING, new_shopping_id)
|
139
|
+
puts "SHOPPING PRODUCT ID ALREADY EXISTS AT #{ps.product_id} -- HIDING DUPLICATE #{our_product_id}"
|
140
|
+
products_to_hide << product_id
|
141
|
+
else
|
142
|
+
puts "UPDATING SOURCE ID: FROM #{product_id.inspect} TO #{new_shopping_id.inspect} FOR product id##{our_product_id}"
|
143
|
+
ProductSource.update_all("source_id = E'#{new_shopping_id}'", "source_id = E'#{product_id}' AND product_id = #{our_product_id}")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
puts "HIDING PRODUCTS: #{products_to_hide.inspect}"
|
148
|
+
if products_to_hide.length > 0
|
149
|
+
products_to_hide.each do |shopping_product_id|
|
150
|
+
ProductSource.increment_not_found_count(ProductSource::Name::SHOPPING, shopping_product_id)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
# return all that jazz
|
155
|
+
[all_offers, all_product_infos]
|
156
|
+
end
|
157
|
+
|
158
|
+
# find a single batch of offers and shove them info all_offers hash
|
159
|
+
# this is just a helper for batch_search_v3 and shouldn't be called directly
|
160
|
+
# for looking up a whole lot of product offers, look at batch_search_v3 above
|
161
|
+
def self.single_batch_search_v3(search_hash, sandbox=false)
|
162
|
+
misses, offers, product_infos = do_search_v3(search_hash, sandbox)
|
163
|
+
|
164
|
+
# turn shopping ids into product ids for the returned results ( both offers and product_infos )
|
165
|
+
# (only if they initially gave us a set of products)
|
166
|
+
if search_hash[:search_type] == SearchType::PRODUCT
|
167
|
+
search_hash[:batch_items].each do |product_id|
|
168
|
+
[offers, product_infos].each do |item|
|
169
|
+
our_id = search_hash[:product_ids_hash][product_id]
|
170
|
+
# if OUR product id is the same as shopping's, we don't delete. obvi.
|
171
|
+
next if our_id == product_id
|
172
|
+
if our_id.nil?
|
173
|
+
puts "******** NIL FOR #{product_id}"
|
174
|
+
end
|
175
|
+
item[our_id] = item[product_id]
|
176
|
+
item.delete(product_id)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
[misses, offers, product_infos]
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.default_offers
|
184
|
+
Hash.new([]).clone
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.default_product_infos get_extra_product_info
|
188
|
+
# smart defaults for error handling
|
189
|
+
if get_extra_product_info
|
190
|
+
product_infos = Hash.new({
|
191
|
+
:avg_secondary_cpcs => nil,
|
192
|
+
:primary_cpc => nil,
|
193
|
+
:reported_product_id => nil,
|
194
|
+
:images => {},
|
195
|
+
:manufacturer => nil,
|
196
|
+
:name => nil,
|
197
|
+
:description => nil,
|
198
|
+
:review_url => nil,
|
199
|
+
:review_count => nil,
|
200
|
+
:rating => nil
|
201
|
+
})
|
202
|
+
else # even if they don't ask for it! BAM!
|
203
|
+
product_infos = Hash.new({
|
204
|
+
:avg_secondary_cpcs => nil,
|
205
|
+
:primary_cpc => nil,
|
206
|
+
:reported_product_id => nil
|
207
|
+
})
|
208
|
+
end
|
209
|
+
|
210
|
+
product_infos.clone
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.do_search_v3(search_hash, sandbox=false)
|
214
|
+
# defaults
|
215
|
+
offers = self.default_offers
|
216
|
+
product_infos = self.default_product_infos(search_hash[:get_extra_product_info])
|
217
|
+
misses = []
|
218
|
+
|
219
|
+
case search_hash[:search_type]
|
220
|
+
when SearchType::PRODUCT, SearchType::SHOPPING_PRODUCT_ID
|
221
|
+
search_hash[:batch_items].compact! # remove nils
|
222
|
+
if search_hash[:batch_items].empty?
|
223
|
+
# nothing to look for! dummy.
|
224
|
+
puts "NO PRODUCT ID PASSED!"
|
225
|
+
# return blanks
|
226
|
+
return [misses, offers, product_infos]
|
227
|
+
end
|
228
|
+
params = {
|
229
|
+
'productId' => search_hash[:batch_items],
|
230
|
+
'showProductOffers' => true,
|
231
|
+
'trackingId' => search_hash[:tracking_id],
|
232
|
+
'numItems' => 20
|
233
|
+
}
|
234
|
+
when SearchType::KEYWORDS
|
235
|
+
params = {
|
236
|
+
#'categoryId' => search_hash[:category],
|
237
|
+
'keyword' => Array(search_hash[:keywords].collect{|x| CGI::escape(x) }), # can be an array, thass coo' wit me.
|
238
|
+
'showProductOffers' => true,
|
239
|
+
'trackingId' => search_hash[:tracking_id],
|
240
|
+
'numOffersPerProduct' => 20,
|
241
|
+
'pageNumber' => 1,
|
242
|
+
'numItems' => (search_hash[:num_items].nil? || search_hash[:num_items].to_s.empty?) ? 1 : search_hash[:num_items],
|
243
|
+
# 'productSortType' => 'price',
|
244
|
+
# 'productSortOrder' => 'asc'
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
result = make_v3_request :GeneralSearch, params, sandbox
|
249
|
+
|
250
|
+
|
251
|
+
if search_hash[:search_type] == SearchType::PRODUCT || search_hash[:search_type] == SearchType::SHOPPING_PRODUCT_ID
|
252
|
+
if search_hash[:batch_items].length == 1 && result.at('product')
|
253
|
+
# if we're looking up one ID, it doesn't matter if the ID they returned doesn't match ours
|
254
|
+
misses = []
|
255
|
+
elsif result.at('product') # if we got ANY products back
|
256
|
+
misses = search_hash[:batch_items] - (result / 'product').collect{|x| x.attributes['id']}
|
257
|
+
else # probably an error happened
|
258
|
+
misses = search_hash[:batch_items]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
errors = result.search('exception[@type=error]')
|
263
|
+
if errors.length > 0
|
264
|
+
# we got an error, or more than one!
|
265
|
+
if (search_hash[:search_type] == SearchType::PRODUCT || search_hash[:search_type] == SearchType::SHOPPING_PRODUCT_ID) && search_hash[:batch_items].length == 1 && errors.length == 1 && errors.first.at('message').innerText == "Could not find ProductIDs #{search_hash[:batch_items].first}"
|
266
|
+
# happens when we look up one product id and it's not a valid product id according to shopping. we ignore this kind of error.
|
267
|
+
else
|
268
|
+
puts "*** ERROR *** Could not look up offers by product ids:"
|
269
|
+
errors.each do |error|
|
270
|
+
puts " - #{error.at('message').innerText}"
|
271
|
+
end
|
272
|
+
# notify hoptoad of this shit!
|
273
|
+
HoptoadNotifier.notify(
|
274
|
+
:error_class => "ShoppingOfferError",
|
275
|
+
:error_message => %{
|
276
|
+
We got error(s) while trying to get the shopping offers! #{search_hash[:batch_items].inspect}
|
277
|
+
},
|
278
|
+
:request => { :params => Hash[*errors.collect{|x| ["Error ##{errors.index(x)}", x.at('message').innerText] }.flatten] }
|
279
|
+
)
|
280
|
+
end
|
281
|
+
|
282
|
+
# return blanks
|
283
|
+
return [misses, offers, product_infos]
|
284
|
+
end
|
285
|
+
|
286
|
+
(result / 'product').each do |product|
|
287
|
+
product_id = product.attributes['id']
|
288
|
+
if (search_hash[:search_type] == SearchType::PRODUCT || search_hash[:search_type] == SearchType::SHOPPING_PRODUCT_ID)
|
289
|
+
# this happens when they give us back an ID that we didn't ask for
|
290
|
+
# (if we are only looking at one product, we don't care what ID they give us back,
|
291
|
+
# we know it's the product we were looking for)
|
292
|
+
if search_hash[:batch_items].length == 1 && !search_hash[:batch_items].include?(product_id)
|
293
|
+
product_id = search_hash[:batch_items].first # revert back to the ID we asked for, we put the other in product_infos[x][:reported_product_id]
|
294
|
+
elsif !search_hash[:batch_items].include?(product_id)
|
295
|
+
# skip it, already included in the misses ( hopefully ... )
|
296
|
+
next
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
offers[product_id]={}
|
301
|
+
product_infos[product_id] = {
|
302
|
+
:reported_product_id => product.attributes['id'] # their reported ID doesn't necessarily match up with our ID
|
303
|
+
}
|
304
|
+
if search_hash[:get_extra_product_info]
|
305
|
+
product_infos[product_id][:name] = product.at('name').innerText
|
306
|
+
product_infos[product_id][:review_url] = (product.at('reviewURL').innerText rescue nil)
|
307
|
+
product_infos[product_id][:review_count] = (product.at('rating/reviewCount').innerText rescue nil)
|
308
|
+
product_infos[product_id][:rating] = (product.at('rating/rating').innerText rescue nil)
|
309
|
+
|
310
|
+
try_description = product.at('fullDescription').innerText
|
311
|
+
if try_description.nil? || try_description.empty?
|
312
|
+
try_description = product.at('shortDescription').innerText
|
313
|
+
end
|
314
|
+
product_infos[product_id][:description] = (try_description.nil? || try_description.empty?) ? '' : try_description[0...255]
|
315
|
+
|
316
|
+
images = (product / 'images' / 'image[@available="true"]').collect{|x|
|
317
|
+
{
|
318
|
+
:width => x.attributes['width'].to_i,
|
319
|
+
:height => x.attributes['height'].to_i,
|
320
|
+
:url => x.at('sourceURL').innerText
|
321
|
+
}
|
322
|
+
}.sort_by{|x| x[:width] * x[:height] }
|
323
|
+
|
324
|
+
product_infos[product_id][:images] = {
|
325
|
+
:small_image => images[0],
|
326
|
+
:medium_image => images[1],
|
327
|
+
:large_image => images[2]
|
328
|
+
}
|
329
|
+
|
330
|
+
# possible_manufacturers = (product / 'offer > manufacturer').collect{|x| x.innerText}.compact.uniq
|
331
|
+
#
|
332
|
+
# if possible_manufacturers.length == 1
|
333
|
+
# product_infos[product_id][:manufacturer] = possible_manufacturers.first # easy peasy lemon squezy
|
334
|
+
# elsif possible_manufacturers.length > 1
|
335
|
+
# # figure out which manufacturer is the most popular
|
336
|
+
# manufacturers_popularity_index = possible_manufacturers.inject({}) {|ha, manufacturer| ha[manufacturer] ||= 0; ha[manufacturer] += 1; ha }
|
337
|
+
# product_infos[product_id][:manufacturer] = manufacturers_popularity_index.sort_by{|key, val| val }.last.first
|
338
|
+
# else
|
339
|
+
# product_infos[product_id][:manufacturer] = nil # zip, zero, doodad :(
|
340
|
+
# end
|
341
|
+
end
|
342
|
+
|
343
|
+
(product.at('offers') / 'offer').each do |offer|
|
344
|
+
store = offer.at('store')
|
345
|
+
store_hash = {
|
346
|
+
:name => store.at('name').innerText,
|
347
|
+
:trusted => store.attributes['trusted'] == "true",
|
348
|
+
:id => store.attributes['id'].to_i,
|
349
|
+
:authorized_reseller => store.attributes['authorizedReseller'] == "true"
|
350
|
+
}
|
351
|
+
store_logo = store.at('logo')
|
352
|
+
if store_logo.attributes['available'] == "true"
|
353
|
+
store_hash[:logo] = {
|
354
|
+
:width => store_logo.attributes['width'],
|
355
|
+
:height => store_logo.attributes['height'],
|
356
|
+
:url => store_logo.at('sourceURL').innerText
|
357
|
+
}
|
358
|
+
else
|
359
|
+
store_hash[:logo] = nil
|
360
|
+
end
|
361
|
+
|
362
|
+
# store rating
|
363
|
+
store_rating = store.at('ratingInfo')
|
364
|
+
store_hash[:rating] = {
|
365
|
+
:number => store_rating.at('rating').nil? ? nil : normalize_merchant_rating(store_rating.at('rating').innerText.to_f),
|
366
|
+
:count => store_rating.at('reviewCount').innerText.to_i,
|
367
|
+
:url => store_rating.at('reviewURL').nil? ? nil : store_rating.at('reviewURL').innerText
|
368
|
+
}
|
369
|
+
shipping_info = offer.at('shippingCost').attributes['checkSite'] == "true" ? nil : to_d_or_nil(offer.at('shippingCost').innerText)
|
370
|
+
price_info = to_d_or_nil(offer.at('basePrice').innerText)
|
371
|
+
if shipping_info && price_info
|
372
|
+
total_price = shipping_info + price_info
|
373
|
+
else
|
374
|
+
total_price = price_info
|
375
|
+
end
|
376
|
+
|
377
|
+
# in-stock
|
378
|
+
stock_status = offer.at('stockStatus').innerText
|
379
|
+
in_stock = stock_status != 'out-of-stock' && stock_status != 'back-order'
|
380
|
+
|
381
|
+
if in_stock
|
382
|
+
offers[product_id][store_hash[:id]] = { :merchant_code => store_hash[:id].to_s,
|
383
|
+
:merchant_name => store_hash[:name],
|
384
|
+
:merchant_logo_url => store_hash[:logo].nil? ? nil : store_hash[:logo][:url],
|
385
|
+
:cpc => offer.at('cpc').nil? ? nil : (offer.at('cpc').innerText.to_f*100).to_i,
|
386
|
+
:price => to_d_or_nil(offer.at('basePrice').innerText),
|
387
|
+
:shipping => offer.at('shippingCost').attributes['checkSite'] == "true" ? nil : to_d_or_nil(offer.at('shippingCost').innerText),
|
388
|
+
:offer_url => offer.at('offerURL').innerText,
|
389
|
+
:offer_tier => 1,
|
390
|
+
:merchant_rating => store_hash[:rating][:number],
|
391
|
+
:num_merchant_reviews => store_hash[:rating][:count] }
|
392
|
+
end
|
393
|
+
end
|
394
|
+
# return an array, don't care about the hash. was used for dup checking.
|
395
|
+
offers[product_id] = offers[product_id].values.sort_by{|x| x[:price] + (x[:shipping] || 0) }
|
396
|
+
end
|
397
|
+
|
398
|
+
[misses, offers, product_infos]
|
399
|
+
end
|
400
|
+
|
401
|
+
def self.normalize_merchant_rating(merchant_rating)
|
402
|
+
merchant_rating.nil? ? nil : (merchant_rating * 20.0).round
|
403
|
+
end
|
404
|
+
|
405
|
+
# get any ol' random attribute from a shopping id
|
406
|
+
# for instance, 'Screen Size' is a good'un.
|
407
|
+
def self.get_attribute_from_shopping_id_v3 shopping_id, attribute
|
408
|
+
product_info = find_by_product_id_v3 shopping_id
|
409
|
+
values = product_info[:specifications].values.flatten
|
410
|
+
index = values.index(attribute)
|
411
|
+
# we +1 here because the flattened values are [name, value] oriented
|
412
|
+
index.nil? ? nil : values[index+1]
|
413
|
+
end
|
414
|
+
|
415
|
+
def self.parse_images_v3 images_element
|
416
|
+
images_element.inject({}) do |ha,obj|
|
417
|
+
ha["#{obj.attributes['width']}x#{obj.attributes['height']}"] = [obj.attributes['available'] == 'true', obj.at('sourceURL').innerText]
|
418
|
+
ha
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def self.find_related_terms_v3 keyword, sandbox=false
|
423
|
+
result = make_v3_request :GeneralSearch, {'keyword' => keyword}, sandbox
|
424
|
+
(result / 'relatedTerms > term').collect{|x| x.innerText}
|
425
|
+
end
|
426
|
+
|
427
|
+
private
|
428
|
+
|
429
|
+
def self.make_v3_request(action, user_params, sandbox=false)
|
430
|
+
params = {
|
431
|
+
'trackingId' => '8039097',
|
432
|
+
'apiKey' => '21e3f349-c5f4-4783-8354-6ff75371ae22'
|
433
|
+
}
|
434
|
+
params = params.merge(user_params) # merge in the user params
|
435
|
+
# sort 'em for the caching
|
436
|
+
params = params.sort
|
437
|
+
|
438
|
+
query_string = params.collect{|x|
|
439
|
+
if x[1].class == Array
|
440
|
+
x[1].collect{|y| "#{x[0]}=#{y}" }.join '&'
|
441
|
+
else
|
442
|
+
"#{x[0]}=#{x[1]}"
|
443
|
+
end
|
444
|
+
}.join "&" # build the api url
|
445
|
+
|
446
|
+
# do we already have a cached version of this API call?
|
447
|
+
# key = "shopping-api-v3-#{action}-#{sandbox}-#{Digest::MD5.hexdigest(query_string)}-v2"
|
448
|
+
#result = CACHE.get(key)
|
449
|
+
#if !result # nope.. gotta get a new one.
|
450
|
+
url = sandbox ? "http://sandbox.api.shopping.com/publisher/3.0/rest/#{action}?#{query_string}" : "http://publisher.api.shopping.com/publisher/3.0/rest/#{action}?#{query_string}"
|
451
|
+
# puts "Shopping.com API request URL: #{url}"
|
452
|
+
result = do_api_request(url)
|
453
|
+
#begin
|
454
|
+
# CACHE.set(key, result, Source.shopping_source.offer_ttl_seconds)
|
455
|
+
#rescue MemCache::MemCacheError => e
|
456
|
+
# raise e unless e.message == 'Value too large, memcached can only store 1MB of data per key'
|
457
|
+
#end
|
458
|
+
#end
|
459
|
+
Hpricot.XML(result)
|
460
|
+
end
|
461
|
+
|
462
|
+
# create the Net::HTTP object to actually do the request
|
463
|
+
def self.do_api_request(url, retry_num=0, max_retries=4)
|
464
|
+
# print '~.~'
|
465
|
+
if retry_num >= max_retries
|
466
|
+
raise StandardError, "Failed to get Shopping URL with after #{max_retries} tries for url: #{url.inspect}"
|
467
|
+
end
|
468
|
+
|
469
|
+
req_url = URI.safe_parse(url)
|
470
|
+
http = Net::HTTP.new(req_url.host, req_url.port)
|
471
|
+
http.read_timeout = 5 # 5 second timeout
|
472
|
+
resp = nil
|
473
|
+
begin
|
474
|
+
http.start do |web|
|
475
|
+
resp = web.get("#{req_url.path}?#{req_url.query}")
|
476
|
+
end
|
477
|
+
rescue Timeout::Error, Errno::EPIPE, Errno::ECONNRESET
|
478
|
+
puts "Timeout, broken pipe, or connection reset. Trying again."
|
479
|
+
# timed out, try again.
|
480
|
+
retry_num += 1
|
481
|
+
do_api_request(url, retry_num, max_retries)
|
482
|
+
end
|
483
|
+
|
484
|
+
case resp
|
485
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
486
|
+
resp.body
|
487
|
+
when Net::HTTPInternalServerError
|
488
|
+
puts "GOT Net::HTTPInternalServerError FROM Shopping; SLEEPING AND TRYING IN 0.5 SECONDS. RETRY NUM #{retry_num}."
|
489
|
+
sleep(0.5)
|
490
|
+
retry_num += 1
|
491
|
+
do_api_request(url, retry_num, max_retries)
|
492
|
+
when Net::HTTPServiceUnavailable
|
493
|
+
puts "GOT Net::HTTPServiceUnavailable FROM Shopping; SLEEPING AND TRYING IN TWO SECONDS. RETRY NUM #{retry_num}."
|
494
|
+
sleep(2)
|
495
|
+
retry_num += 1
|
496
|
+
do_api_request(url, retry_num, max_retries)
|
497
|
+
when nil
|
498
|
+
puts "GOT nil FROM Shopping; SLEEPING AND TRYING IN 0.5 SECONDS. RETRY NUM #{retry_num}."
|
499
|
+
sleep(0.5)
|
500
|
+
retry_num += 1
|
501
|
+
do_api_request(url, retry_num, max_retries)
|
502
|
+
else
|
503
|
+
raise StandardError, "Failed to get Shopping URL with unknown error: #{resp.inspect} For url: #{url.inspect}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def self.to_i_or_nil(value)
|
508
|
+
value.nil? ? nil : value.strip.to_i rescue nil
|
509
|
+
end
|
510
|
+
|
511
|
+
def self.to_d_or_nil(value)
|
512
|
+
value.nil? ? nil : BigDecimal(value.strip) rescue nil
|
513
|
+
end
|
514
|
+
end
|