line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
################################################################### |
2
|
|
|
|
|
|
|
package Net::Amazon; |
3
|
|
|
|
|
|
|
###################################################################### |
4
|
|
|
|
|
|
|
# Mike Schilli , 2003 |
5
|
|
|
|
|
|
|
###################################################################### |
6
|
|
|
|
|
|
|
|
7
|
26
|
|
|
26
|
|
1039718
|
use 5.006; |
|
26
|
|
|
|
|
102
|
|
|
26
|
|
|
|
|
1142
|
|
8
|
26
|
|
|
26
|
|
164
|
use strict; |
|
26
|
|
|
|
|
48
|
|
|
26
|
|
|
|
|
1125
|
|
9
|
26
|
|
|
26
|
|
142
|
use warnings; |
|
26
|
|
|
|
|
112
|
|
|
26
|
|
|
|
|
9646
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
our $VERSION = '0.62'; |
12
|
|
|
|
|
|
|
our $WSDL_DATE = '2011-08-01'; |
13
|
|
|
|
|
|
|
our $Locale = 'us'; |
14
|
|
|
|
|
|
|
our @CANNED_RESPONSES = (); |
15
|
|
|
|
|
|
|
our $IS_CANNED = 0; |
16
|
|
|
|
|
|
|
|
17
|
26
|
|
|
26
|
|
75788
|
use LWP::UserAgent; |
|
26
|
|
|
|
|
2211330
|
|
|
26
|
|
|
|
|
1052
|
|
18
|
26
|
|
|
26
|
|
310
|
use HTTP::Message; |
|
26
|
|
|
|
|
52
|
|
|
26
|
|
|
|
|
612
|
|
19
|
26
|
|
|
26
|
|
47030
|
use HTTP::Request::Common; |
|
26
|
|
|
|
|
100367
|
|
|
26
|
|
|
|
|
2391
|
|
20
|
26
|
|
|
26
|
|
27816
|
use XML::Simple; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
use Data::Dumper; |
22
|
|
|
|
|
|
|
use URI; |
23
|
|
|
|
|
|
|
use Log::Log4perl qw(:easy get_logger); |
24
|
|
|
|
|
|
|
use Time::HiRes qw(usleep gettimeofday tv_interval); |
25
|
|
|
|
|
|
|
use Digest::SHA qw(hmac_sha256_base64); |
26
|
|
|
|
|
|
|
use URI::Escape qw(uri_escape); |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# Each key represents a search() type, and each value indicates which |
29
|
|
|
|
|
|
|
# Net::Amazon::Request:: class to use to handle it. |
30
|
|
|
|
|
|
|
use constant SEARCH_TYPE_CLASS_MAP => { |
31
|
|
|
|
|
|
|
actor => 'Actor', |
32
|
|
|
|
|
|
|
artist => 'Artist', |
33
|
|
|
|
|
|
|
all => 'All', |
34
|
|
|
|
|
|
|
author => 'Author', |
35
|
|
|
|
|
|
|
asin => 'ASIN', |
36
|
|
|
|
|
|
|
blended => 'Blended', |
37
|
|
|
|
|
|
|
browsenode => 'BrowseNode', |
38
|
|
|
|
|
|
|
director => 'Director', |
39
|
|
|
|
|
|
|
ean => 'EAN', |
40
|
|
|
|
|
|
|
exchange => 'Exchange', |
41
|
|
|
|
|
|
|
isbn => 'ISBN', |
42
|
|
|
|
|
|
|
keyword => 'Keyword', |
43
|
|
|
|
|
|
|
manufacturer => 'Manufacturer', |
44
|
|
|
|
|
|
|
musiclabel => 'MusicLabel', |
45
|
|
|
|
|
|
|
power => 'Power', |
46
|
|
|
|
|
|
|
publisher => 'Publisher', |
47
|
|
|
|
|
|
|
seller => 'Seller', |
48
|
|
|
|
|
|
|
similar => 'Similar', |
49
|
|
|
|
|
|
|
textstream => 'TextStream', |
50
|
|
|
|
|
|
|
title => 'Title', |
51
|
|
|
|
|
|
|
upc => 'UPC', |
52
|
|
|
|
|
|
|
}; |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
################################################## |
55
|
|
|
|
|
|
|
sub new { |
56
|
|
|
|
|
|
|
################################################## |
57
|
|
|
|
|
|
|
my($class, %options) = @_; |
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
if(! exists $options{token}) { |
60
|
|
|
|
|
|
|
die "Mandatory paramter 'token' not defined"; |
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
if(! exists $options{secret_key}) { |
64
|
|
|
|
|
|
|
die "Mandatory paramter 'secret_key' not defined"; |
65
|
|
|
|
|
|
|
} |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
if(! exists $options{associate_tag}) { |
68
|
|
|
|
|
|
|
die "Mandatory paramter 'associate_tag' not defined"; |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
my $self = { |
72
|
|
|
|
|
|
|
strict => 1, |
73
|
|
|
|
|
|
|
response_dump => 0, |
74
|
|
|
|
|
|
|
rate_limit => 1.0, # 1 req/sec |
75
|
|
|
|
|
|
|
max_pages => 5, |
76
|
|
|
|
|
|
|
ua => LWP::UserAgent->new(), |
77
|
|
|
|
|
|
|
compress => 1, |
78
|
|
|
|
|
|
|
%options, |
79
|
|
|
|
|
|
|
}; |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
# XXX: this has to be set as soon as possible to ensure |
82
|
|
|
|
|
|
|
# the validators pick up the correct locale. I don't |
83
|
|
|
|
|
|
|
# like the way this works, and need to think of a better |
84
|
|
|
|
|
|
|
# solution. |
85
|
|
|
|
|
|
|
if (exists $self->{locale}) { |
86
|
|
|
|
|
|
|
$Net::Amazon::Locale = $self->{locale}; |
87
|
|
|
|
|
|
|
} |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
help_xml_simple_choose_a_parser(); |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
bless $self, $class; |
92
|
|
|
|
|
|
|
} |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
################################################## |
95
|
|
|
|
|
|
|
sub search { |
96
|
|
|
|
|
|
|
################################################## |
97
|
|
|
|
|
|
|
my($self, %params) = @_; |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
foreach my $key ( keys %params ) { |
100
|
|
|
|
|
|
|
next unless ( my $class = SEARCH_TYPE_CLASS_MAP->{$key} ); |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
return $self->_make_request($class, \%params); |
103
|
|
|
|
|
|
|
} |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# FIX? |
106
|
|
|
|
|
|
|
# This seems like it really should be a die() instead...this is |
107
|
|
|
|
|
|
|
# indicative of a programming problem. Generally speaking, it's |
108
|
|
|
|
|
|
|
# best to issue warnings from a module--you can't be sure that the |
109
|
|
|
|
|
|
|
# client has a stderr to begin with, or that he wants errors |
110
|
|
|
|
|
|
|
# spewed to it. |
111
|
|
|
|
|
|
|
warn "No Net::Amazon::Request type could be determined"; |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
return undef; |
114
|
|
|
|
|
|
|
} |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
################################################## |
117
|
|
|
|
|
|
|
sub intl_url { |
118
|
|
|
|
|
|
|
################################################## |
119
|
|
|
|
|
|
|
my($self, $url) = @_; |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
if(! exists $self->{locale}) { |
122
|
|
|
|
|
|
|
return $url; |
123
|
|
|
|
|
|
|
} |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
if (0) { |
126
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "ca") { |
127
|
|
|
|
|
|
|
$url =~ s/\.com/.ca/; |
128
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "de") { |
129
|
|
|
|
|
|
|
$url =~ s/\.com/.de/; |
130
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "es") { |
131
|
|
|
|
|
|
|
$url =~ s/\.com/.es/; |
132
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "fr") { |
133
|
|
|
|
|
|
|
$url =~ s/\.com/.fr/; |
134
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "jp") { |
135
|
|
|
|
|
|
|
$url =~ s/\.com/.co.jp/; |
136
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "it") { |
137
|
|
|
|
|
|
|
$url =~ s/\.com/.it/; |
138
|
|
|
|
|
|
|
} elsif ($self->{locale} eq "uk") { |
139
|
|
|
|
|
|
|
$url =~ s/\.com/.co.uk/; |
140
|
|
|
|
|
|
|
} |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
return $url; |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
################################################## |
146
|
|
|
|
|
|
|
sub request { |
147
|
|
|
|
|
|
|
################################################## |
148
|
|
|
|
|
|
|
my($self, $request) = @_; |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
my $resp_class = $request->response_class(); |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
eval "require $resp_class;" or |
153
|
|
|
|
|
|
|
die "Cannot find '$resp_class'"; |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
my $res = $resp_class->new(); |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
my $url = URI->new($self->intl_url($request->amzn_xml_url())); |
158
|
|
|
|
|
|
|
my $page = (defined $request->page()) ? |
159
|
|
|
|
|
|
|
($request->page() - 1) * $self->{max_pages} + 1 : |
160
|
|
|
|
|
|
|
0; |
161
|
|
|
|
|
|
|
my $ref; |
162
|
|
|
|
|
|
|
my $max_pages_in_this_search = $self->{max_pages} + $page - 1; |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
REQUEST: { |
165
|
|
|
|
|
|
|
my %params = $request->params(page => $page); |
166
|
|
|
|
|
|
|
$params{locale} = $self->{locale} if exists $self->{locale}; |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
$url->query_form( |
169
|
|
|
|
|
|
|
'Service' => 'AWSECommerceService', |
170
|
|
|
|
|
|
|
'AWSAccessKeyId' => $self->{token}, |
171
|
|
|
|
|
|
|
'Version' => $WSDL_DATE, |
172
|
|
|
|
|
|
|
'AssociateTag' => $self->{associate_tag}, |
173
|
|
|
|
|
|
|
map { $_, $params{$_} } sort keys %params, |
174
|
|
|
|
|
|
|
); |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
# Signed requests will have different URLs, which breaks caching. |
177
|
|
|
|
|
|
|
# Get a cachable URL before signing the request. |
178
|
|
|
|
|
|
|
my $url_cachablestr = $url->as_string; |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
# New signature for >=2009-03-31. Do not alter URL after this! |
181
|
|
|
|
|
|
|
$url = $self->_sign_request($url) if exists $self->{secret_key}; |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
DEBUG(sub { "request: params = " . Dumper(\%params) . "\n"}); |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
my $urlstr = $url->as_string; |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
DEBUG(sub { "urlstr=" . $urlstr }); |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
my $xml = fetch_url($self, $urlstr, $url_cachablestr, $res); |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
if(!defined $xml) { |
192
|
|
|
|
|
|
|
return $res; |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
DEBUG(sub { "Received [ " . $xml . "]" }); |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
# Let the response class parse the XML |
198
|
|
|
|
|
|
|
$ref = $res->xml_parse($xml); |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
# DEBUG(sub { Data::Dumper::Dumper($ref) }); |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
if(! defined $ref) { |
203
|
|
|
|
|
|
|
ERROR("Invalid XML"); |
204
|
|
|
|
|
|
|
$res->messages( [ "Invalid XML" ]); |
205
|
|
|
|
|
|
|
$res->status(""); |
206
|
|
|
|
|
|
|
return $res; |
207
|
|
|
|
|
|
|
} |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
$res->current_page($ref, $page); |
210
|
|
|
|
|
|
|
$res->set_total_results($ref); |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
my $rc = $res->is_page_error($ref); |
213
|
|
|
|
|
|
|
if ($rc == 0) { |
214
|
|
|
|
|
|
|
return $res; |
215
|
|
|
|
|
|
|
} elsif ($rc == -1) { |
216
|
|
|
|
|
|
|
last; |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
my $new_items = $res->xmlref_add($ref); |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
DEBUG("Received valid XML ($new_items items)"); |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
# Stop if we've fetched max_pages already |
224
|
|
|
|
|
|
|
if(defined $page && $max_pages_in_this_search <= $page) { |
225
|
|
|
|
|
|
|
DEBUG("Fetched max_pages ($max_pages_in_this_search) -- stopping"); |
226
|
|
|
|
|
|
|
last; |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
if($res->is_page_available($ref, $new_items, $page)) { |
230
|
|
|
|
|
|
|
$page++; |
231
|
|
|
|
|
|
|
redo REQUEST; |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# We're gonna fall out of this loop here. |
235
|
|
|
|
|
|
|
} |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
$res->status(1); |
238
|
|
|
|
|
|
|
# We have a valid response, so if TotalResults isn't set, |
239
|
|
|
|
|
|
|
# we most likely have a single response |
240
|
|
|
|
|
|
|
$res->total_results(1) unless defined $res->total_results(); |
241
|
|
|
|
|
|
|
return $res; |
242
|
|
|
|
|
|
|
} |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
################################################## |
245
|
|
|
|
|
|
|
sub fetch_url { |
246
|
|
|
|
|
|
|
################################################## |
247
|
|
|
|
|
|
|
my($self, $url, $url_cachablestr, $res) = @_; |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
my $max_retries = 2; |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
INFO("Fetching $url"); |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
if(@CANNED_RESPONSES) { |
254
|
|
|
|
|
|
|
$IS_CANNED = 1; |
255
|
|
|
|
|
|
|
INFO("Serving canned response (testing)"); |
256
|
|
|
|
|
|
|
return shift @CANNED_RESPONSES; |
257
|
|
|
|
|
|
|
} |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
if(exists $self->{cache}) { |
260
|
|
|
|
|
|
|
my $resp = $self->{cache}->get($url_cachablestr); |
261
|
|
|
|
|
|
|
if(defined $resp) { |
262
|
|
|
|
|
|
|
INFO("Serving from cache"); |
263
|
|
|
|
|
|
|
return $resp; |
264
|
|
|
|
|
|
|
} |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
INFO("Cache miss"); |
267
|
|
|
|
|
|
|
} |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
my $ua = $self->{ua}; |
270
|
|
|
|
|
|
|
$ua->env_proxy(); |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
my $resp; |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
{ |
275
|
|
|
|
|
|
|
# wait up to a second before the next request so |
276
|
|
|
|
|
|
|
# as to not violate Amazon's 1 query per second |
277
|
|
|
|
|
|
|
# rule (or the configured rate_limit). |
278
|
|
|
|
|
|
|
$self->pause() if $self->{strict}; |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
{ |
281
|
|
|
|
|
|
|
my $req = GET $url; |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
$req->header("Accept-Encoding" => [ HTTP::Message::decodable() ]) |
284
|
|
|
|
|
|
|
if $self->{compress}; |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
$resp = $ua->request($req); |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
$self->reset_timer() if $self->{strict}; |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
if($resp->is_error) { |
292
|
|
|
|
|
|
|
# retry on 503 Service Unavailable errors |
293
|
|
|
|
|
|
|
if ($resp->code == 503) { |
294
|
|
|
|
|
|
|
if ($max_retries-- >= 0) { |
295
|
|
|
|
|
|
|
INFO("Temporary Amazon error 503, retrying"); |
296
|
|
|
|
|
|
|
redo; |
297
|
|
|
|
|
|
|
} else { |
298
|
|
|
|
|
|
|
INFO("Out of retries, giving up"); |
299
|
|
|
|
|
|
|
$res->status(""); |
300
|
|
|
|
|
|
|
$res->messages( [ "Too many temporary Amazon errors" ] ); |
301
|
|
|
|
|
|
|
return undef; |
302
|
|
|
|
|
|
|
} |
303
|
|
|
|
|
|
|
} else { |
304
|
|
|
|
|
|
|
$res->status(""); |
305
|
|
|
|
|
|
|
$res->messages( [ $resp->message ] ); |
306
|
|
|
|
|
|
|
return undef; |
307
|
|
|
|
|
|
|
} |
308
|
|
|
|
|
|
|
} |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
if($self->{response_dump}) { |
311
|
|
|
|
|
|
|
my $dumpfile = "response-$self->{response_dump}.txt"; |
312
|
|
|
|
|
|
|
open FILE, ">$dumpfile" or die "Cannot open $dumpfile"; |
313
|
|
|
|
|
|
|
print FILE $resp->decoded_content(); |
314
|
|
|
|
|
|
|
close FILE; |
315
|
|
|
|
|
|
|
$self->{response_dump}++; |
316
|
|
|
|
|
|
|
} |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
if($resp->decoded_content =~ // && |
319
|
|
|
|
|
|
|
# Is this the same value of AWS4? |
320
|
|
|
|
|
|
|
$resp->decoded_content =~ /Please retry/i) { |
321
|
|
|
|
|
|
|
if($max_retries-- >= 0) { |
322
|
|
|
|
|
|
|
INFO("Temporary Amazon error, retrying"); |
323
|
|
|
|
|
|
|
redo; |
324
|
|
|
|
|
|
|
} else { |
325
|
|
|
|
|
|
|
INFO("Out of retries, giving up"); |
326
|
|
|
|
|
|
|
$res->status(""); |
327
|
|
|
|
|
|
|
$res->messages( [ "Too many temporary Amazon errors" ] ); |
328
|
|
|
|
|
|
|
return undef; |
329
|
|
|
|
|
|
|
} |
330
|
|
|
|
|
|
|
} |
331
|
|
|
|
|
|
|
} |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
if(exists $self->{cache}) { |
334
|
|
|
|
|
|
|
$self->{cache}->set($url_cachablestr, $resp->decoded_content()); |
335
|
|
|
|
|
|
|
} |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
return $resp->decoded_content(); |
338
|
|
|
|
|
|
|
} |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
################################################## |
341
|
|
|
|
|
|
|
# Poor man's Class::Struct |
342
|
|
|
|
|
|
|
################################################## |
343
|
|
|
|
|
|
|
sub make_accessor { |
344
|
|
|
|
|
|
|
################################################## |
345
|
|
|
|
|
|
|
my($package, $name) = @_; |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
no strict qw(refs); |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
my $code = <
|
350
|
|
|
|
|
|
|
*{"$package\\::$name"} = sub { |
351
|
|
|
|
|
|
|
my(\$self, \$value) = \@_; |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
if(defined \$value) { |
354
|
|
|
|
|
|
|
\$self->{$name} = \$value; |
355
|
|
|
|
|
|
|
} |
356
|
|
|
|
|
|
|
if(exists \$self->{$name}) { |
357
|
|
|
|
|
|
|
return (\$self->{$name}); |
358
|
|
|
|
|
|
|
} else { |
359
|
|
|
|
|
|
|
return ""; |
360
|
|
|
|
|
|
|
} |
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
EOT |
363
|
|
|
|
|
|
|
if(! defined *{"$package\::$name"}) { |
364
|
|
|
|
|
|
|
eval $code or die "$@"; |
365
|
|
|
|
|
|
|
} |
366
|
|
|
|
|
|
|
} |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
# An accessor for backward compatability with AWS3. |
369
|
|
|
|
|
|
|
################################################## |
370
|
|
|
|
|
|
|
sub make_compatible_accessor{ |
371
|
|
|
|
|
|
|
################################################## |
372
|
|
|
|
|
|
|
my($package, $old_name, $new_name) = @_; |
373
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
no strict qw(refs); |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
my $code = <
|
377
|
|
|
|
|
|
|
*{"$package\\::$old_name"} = sub { |
378
|
|
|
|
|
|
|
my(\$self, \$value) = \@_; |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
if(defined \$value) { |
381
|
|
|
|
|
|
|
\$self->{$new_name} = \$value; |
382
|
|
|
|
|
|
|
} |
383
|
|
|
|
|
|
|
if(exists \$self->{$new_name}) { |
384
|
|
|
|
|
|
|
return (\$self->{$new_name}); |
385
|
|
|
|
|
|
|
} else { |
386
|
|
|
|
|
|
|
return ""; |
387
|
|
|
|
|
|
|
} |
388
|
|
|
|
|
|
|
} |
389
|
|
|
|
|
|
|
EOT |
390
|
|
|
|
|
|
|
if(! defined *{"$package\::$old_name"}) { |
391
|
|
|
|
|
|
|
eval $code or die "$@"; |
392
|
|
|
|
|
|
|
} |
393
|
|
|
|
|
|
|
} |
394
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
################################################## |
396
|
|
|
|
|
|
|
# Make accessors for arrays |
397
|
|
|
|
|
|
|
################################################## |
398
|
|
|
|
|
|
|
sub make_array_accessor { |
399
|
|
|
|
|
|
|
################################################## |
400
|
|
|
|
|
|
|
my($package, $name) = @_; |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
no strict qw(refs); |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
my $code = <
|
405
|
|
|
|
|
|
|
*{"$package\\::$name"} = sub { |
406
|
|
|
|
|
|
|
my(\$self, \$nameref) = \@_; |
407
|
|
|
|
|
|
|
if(defined \$nameref) { |
408
|
|
|
|
|
|
|
if(ref \$nameref eq "ARRAY") { |
409
|
|
|
|
|
|
|
\$self->{$name} = \$nameref; |
410
|
|
|
|
|
|
|
} else { |
411
|
|
|
|
|
|
|
\$self->{$name} = [\$nameref]; |
412
|
|
|
|
|
|
|
} |
413
|
|
|
|
|
|
|
} |
414
|
|
|
|
|
|
|
# Return a list |
415
|
|
|
|
|
|
|
if(exists \$self->{$name} and |
416
|
|
|
|
|
|
|
ref \$self->{$name} eq "ARRAY") { |
417
|
|
|
|
|
|
|
return \@{\$self->{$name}}; |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
return undef; |
421
|
|
|
|
|
|
|
} |
422
|
|
|
|
|
|
|
EOT |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
if(! defined *{"$package\::$name"}) { |
425
|
|
|
|
|
|
|
eval $code or die "$@"; |
426
|
|
|
|
|
|
|
} |
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
################################################## |
430
|
|
|
|
|
|
|
sub walk_hash_ref { |
431
|
|
|
|
|
|
|
################################################## |
432
|
|
|
|
|
|
|
my ($package, $href, $aref) = @_; |
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
return $href if scalar(@$aref) == 0; |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
my @a; |
437
|
|
|
|
|
|
|
push @a, $_ for @$aref; |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
my $tail = pop @a; |
440
|
|
|
|
|
|
|
my $ref = $href; |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
for my $part (@a) { |
443
|
|
|
|
|
|
|
$ref = $ref->{$part}; |
444
|
|
|
|
|
|
|
} |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
return $ref->{$tail}; |
447
|
|
|
|
|
|
|
} |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
################################################## |
451
|
|
|
|
|
|
|
sub artist { |
452
|
|
|
|
|
|
|
################################################## |
453
|
|
|
|
|
|
|
my($self, $nameref) = @_; |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
# Only return the first artist |
456
|
|
|
|
|
|
|
return ($self->artists($nameref))[0]; |
457
|
|
|
|
|
|
|
} |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
################################################## |
460
|
|
|
|
|
|
|
sub version { |
461
|
|
|
|
|
|
|
################################################## |
462
|
|
|
|
|
|
|
my($self) = @_; |
463
|
|
|
|
|
|
|
return $self->{Version}; |
464
|
|
|
|
|
|
|
} |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
################################################## |
467
|
|
|
|
|
|
|
sub current_page { |
468
|
|
|
|
|
|
|
################################################## |
469
|
|
|
|
|
|
|
my($self, $ref, $page) = @_; |
470
|
|
|
|
|
|
|
if(exists $ref->{Items}->{TotalPages}) { |
471
|
|
|
|
|
|
|
INFO("Page $page/$ref->{Items}->{TotalPages}"); |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
} |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
################################################## |
476
|
|
|
|
|
|
|
sub set_total_results { |
477
|
|
|
|
|
|
|
################################################## |
478
|
|
|
|
|
|
|
my($self, $ref) = @_; |
479
|
|
|
|
|
|
|
if(exists $ref->{Items}->{TotalResults}) { |
480
|
|
|
|
|
|
|
$self->total_results( $ref->{Items}->{TotalResults} ); |
481
|
|
|
|
|
|
|
} |
482
|
|
|
|
|
|
|
} |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
################################################## |
485
|
|
|
|
|
|
|
sub is_page_error { |
486
|
|
|
|
|
|
|
################################################## |
487
|
|
|
|
|
|
|
my($self, $ref) = @_; |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
if(exists $ref->{Items}->{Request}->{Errors}) { |
490
|
|
|
|
|
|
|
my $errref = $ref->{Items}->{Request}->{Errors}; |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
if (ref($errref->{Error}) eq "ARRAY") { |
493
|
|
|
|
|
|
|
my @errors; |
494
|
|
|
|
|
|
|
for my $e (@{$errref->{Error}}) { |
495
|
|
|
|
|
|
|
push @errors, $e->{Message}; |
496
|
|
|
|
|
|
|
} |
497
|
|
|
|
|
|
|
# multiple errors, set arrary ref |
498
|
|
|
|
|
|
|
$self->messages( \@errors ); |
499
|
|
|
|
|
|
|
} else { |
500
|
|
|
|
|
|
|
# single error, create array |
501
|
|
|
|
|
|
|
$self->messages( [ $errref->{Error}->{Message} ] ); |
502
|
|
|
|
|
|
|
} |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
ERROR("Fetch Error: " . $self->message ); |
505
|
|
|
|
|
|
|
$self->status(""); |
506
|
|
|
|
|
|
|
return 0; |
507
|
|
|
|
|
|
|
} |
508
|
|
|
|
|
|
|
return 1; |
509
|
|
|
|
|
|
|
} |
510
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
################################################## |
512
|
|
|
|
|
|
|
sub is_page_available { |
513
|
|
|
|
|
|
|
################################################## |
514
|
|
|
|
|
|
|
my($self, $ref, $new_items, $page) = @_; |
515
|
|
|
|
|
|
|
if(exists $ref->{Items}->{TotalPages} and |
516
|
|
|
|
|
|
|
$ref->{Items}->{TotalPages} > $page and |
517
|
|
|
|
|
|
|
$IS_CANNED ne 1) { |
518
|
|
|
|
|
|
|
DEBUG("Page $page of $ref->{Items}->{TotalPages} fetched - continuing"); |
519
|
|
|
|
|
|
|
return 1; |
520
|
|
|
|
|
|
|
} |
521
|
|
|
|
|
|
|
return 0; |
522
|
|
|
|
|
|
|
} |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
################################################## |
525
|
|
|
|
|
|
|
sub xmlref_add { |
526
|
|
|
|
|
|
|
################################################## |
527
|
|
|
|
|
|
|
my($self, $xmlref) = @_; |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
my $nof_items_added = 0; |
530
|
|
|
|
|
|
|
return $nof_items_added unless defined $xmlref; |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
# Push a nested hash structure, retrieved via XMLSimple, onto the |
533
|
|
|
|
|
|
|
# object's internal 'xmlref' entry, which holds a ref to an array, |
534
|
|
|
|
|
|
|
# whichs elements are refs to hashes holding an item's attributes |
535
|
|
|
|
|
|
|
# (like OurPrice etc.) |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
#DEBUG("xmlref_add ", Data::Dumper::Dumper($xmlref)); |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
unless(ref($self->{xmlref}) eq "HASH" && |
540
|
|
|
|
|
|
|
ref($self->{xmlref}->{Items}) eq "ARRAY") { |
541
|
|
|
|
|
|
|
$self->{xmlref}->{Items} = []; |
542
|
|
|
|
|
|
|
} |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
if(ref($xmlref->{Items}->{Item}) eq "ARRAY") { |
545
|
|
|
|
|
|
|
push @{$self->{xmlref}->{Items}}, @{$xmlref->{Items}->{Item}}; |
546
|
|
|
|
|
|
|
$nof_items_added = scalar @{$xmlref->{Items}->{Item}}; |
547
|
|
|
|
|
|
|
} else { |
548
|
|
|
|
|
|
|
if (exists $xmlref->{Items}->{Item}->{ItemAttributes}) { |
549
|
|
|
|
|
|
|
push @{$self->{xmlref}->{Items}}, $xmlref->{Items}->{Item}; |
550
|
|
|
|
|
|
|
$nof_items_added = 1; |
551
|
|
|
|
|
|
|
} |
552
|
|
|
|
|
|
|
} |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
DEBUG("xmlref_add (after):", Data::Dumper::Dumper($self)); |
555
|
|
|
|
|
|
|
return $nof_items_added; |
556
|
|
|
|
|
|
|
} |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
################################################## |
559
|
|
|
|
|
|
|
sub help_xml_simple_choose_a_parser { |
560
|
|
|
|
|
|
|
################################################## |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
eval "require XML::Parser"; |
563
|
|
|
|
|
|
|
unless($@) { |
564
|
|
|
|
|
|
|
$XML::Simple::PREFERRED_PARSER = "XML::Parser"; |
565
|
|
|
|
|
|
|
return; |
566
|
|
|
|
|
|
|
} |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
eval "require XML::SAX::PurePerl"; |
569
|
|
|
|
|
|
|
unless($@) { |
570
|
|
|
|
|
|
|
$XML::Simple::PREFERRED_PARSER = "XML::SAX::PurePerl"; |
571
|
|
|
|
|
|
|
return; |
572
|
|
|
|
|
|
|
} |
573
|
|
|
|
|
|
|
} |
574
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
################################################## |
576
|
|
|
|
|
|
|
# This timer makes sure we don't query Amazon more |
577
|
|
|
|
|
|
|
# than once a second. |
578
|
|
|
|
|
|
|
################################################## |
579
|
|
|
|
|
|
|
sub reset_timer { |
580
|
|
|
|
|
|
|
################################################## |
581
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
my $self = shift; |
583
|
|
|
|
|
|
|
$self->{t0} = [gettimeofday]; |
584
|
|
|
|
|
|
|
} |
585
|
|
|
|
|
|
|
|
586
|
|
|
|
|
|
|
################################################## |
587
|
|
|
|
|
|
|
# Pause for up to a second if necessary. |
588
|
|
|
|
|
|
|
################################################## |
589
|
|
|
|
|
|
|
sub pause { |
590
|
|
|
|
|
|
|
################################################## |
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
my $self = shift; |
593
|
|
|
|
|
|
|
return unless ($self->{t0}); |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
my $t1 = [gettimeofday]; |
596
|
|
|
|
|
|
|
my $dur = (1.0/$self->{rate_limit} - |
597
|
|
|
|
|
|
|
tv_interval($self->{t0}, $t1)) * 1000000; |
598
|
|
|
|
|
|
|
if($dur > 0) { |
599
|
|
|
|
|
|
|
# Use a pseudo subclass for the logger, since the app |
600
|
|
|
|
|
|
|
# might not want to log that as 'ERROR'. Log4perl's |
601
|
|
|
|
|
|
|
# inheritance mechanism makes sure it does the right |
602
|
|
|
|
|
|
|
# thing for the current class. |
603
|
|
|
|
|
|
|
my $logger = get_logger(__PACKAGE__ . "::RateLimit"); |
604
|
|
|
|
|
|
|
$logger->error("Ratelimiting: Sleeping $dur microseconds"); |
605
|
|
|
|
|
|
|
usleep($dur); |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
} |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
## |
610
|
|
|
|
|
|
|
## 'PRIVATE' METHODS |
611
|
|
|
|
|
|
|
## |
612
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
# $self->_make_request( TYPE, PARAMS ) |
614
|
|
|
|
|
|
|
# |
615
|
|
|
|
|
|
|
# Takes a TYPE that corresponds to a Net::Amazon::Request |
616
|
|
|
|
|
|
|
# class, require()s that class, instantiates it, and returns |
617
|
|
|
|
|
|
|
# the result of that instance's request() method. |
618
|
|
|
|
|
|
|
# |
619
|
|
|
|
|
|
|
sub _make_request { |
620
|
|
|
|
|
|
|
my ($self, $type, $params) = @_; |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
my $class = "Net::Amazon::Request::$type"; |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
# XXX: change me back, this makes debugging a little difficult. |
625
|
|
|
|
|
|
|
eval "require $class"; |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
my $req = $class->new(%{$params}); |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
return $self->request($req); |
630
|
|
|
|
|
|
|
} |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
# $self->_sign_request( URI ) |
633
|
|
|
|
|
|
|
# |
634
|
|
|
|
|
|
|
# Takes a URI object that corresponds to a Net::Amazon::Request |
635
|
|
|
|
|
|
|
# adds the required Timestamp and Signature parameters, and returns it |
636
|
|
|
|
|
|
|
# See http://docs.amazonwebservices.com/AWSECommerceService/2009-03-31/DG/Query_QueryAuth.html |
637
|
|
|
|
|
|
|
sub _sign_request { |
638
|
|
|
|
|
|
|
my ($self,$uri) = @_; |
639
|
|
|
|
|
|
|
return $uri unless exists $self->{secret_key}; |
640
|
|
|
|
|
|
|
# This assumes no duplicated keys. Safe assumption? |
641
|
|
|
|
|
|
|
my %query = $uri->query_form; |
642
|
|
|
|
|
|
|
my @now = gmtime; |
643
|
|
|
|
|
|
|
$query{Timestamp} ||= sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ',$now[5]+1900,$now[4]+1,@now[3,2,1,0]); |
644
|
|
|
|
|
|
|
my $qstring = join '&', map {"$_=". uri_escape($query{$_},"^A-Za-z0-9\-_.~")} sort keys %query; |
645
|
|
|
|
|
|
|
# Use chr(10), not "\n" which varies by platform |
646
|
|
|
|
|
|
|
my $signme = join chr(10),"GET",$uri->host,$uri->path,$qstring; |
647
|
|
|
|
|
|
|
my $sig = hmac_sha256_base64($signme, $self->{secret_key}); |
648
|
|
|
|
|
|
|
# Digest does not properly pad b64 strings |
649
|
|
|
|
|
|
|
$sig .= '=' while length($sig) % 4; |
650
|
|
|
|
|
|
|
$sig = uri_escape($sig,"^A-Za-z0-9\-_.~"); |
651
|
|
|
|
|
|
|
$qstring .= "&Signature=$sig"; |
652
|
|
|
|
|
|
|
$uri->query( $qstring ); |
653
|
|
|
|
|
|
|
return $uri; |
654
|
|
|
|
|
|
|
} |
655
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
1; |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
__END__ |