File Coverage

blib/lib/WebService/DMM.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package WebService::DMM;
2 5     5   240776 use strict;
  5         12  
  5         218  
3 5     5   26 use warnings;
  5         8  
  5         147  
4 5     5   137 use 5.008_001;
  5         27  
  5         211  
5              
6 5     5   26 use Carp ();
  5         18  
  5         128  
7 5     5   6069 use URI;
  5         32389  
  5         167  
8 5     5   5181 use POSIX qw/strftime/;
  5         42840  
  5         34  
9 5     5   10901 use Furl;
  5         180278  
  5         161  
10 5     5   5655 use Encode ();
  5         62472  
  5         133  
11 5     5   583474 use XML::LibXML;
  0            
  0            
12              
13             use WebService::DMM::Item;
14             use WebService::DMM::Response;
15             use WebService::DMM::Person::Actor;
16             use WebService::DMM::Person::Author;
17             use WebService::DMM::Person::Director;
18             use WebService::DMM::Person::Fighter;
19             use WebService::DMM::Delivery;
20             use WebService::DMM::Label;
21             use WebService::DMM::Maker;
22             use WebService::DMM::Series;
23              
24             use utf8;
25              
26             our $VERSION = '0.11';
27              
28             my $agent_name = __PACKAGE__ . "/$VERSION";
29             our $UserAgent = Furl->new(agent => $agent_name);
30              
31             my $ROOT_NODE;
32             my @supported_api_versions = ('2.00', '1.00');
33              
34             sub __ua {
35             $UserAgent ||= Furl->new(agent => $agent_name);
36             $UserAgent;
37             }
38              
39             sub new {
40             my ($class, %args) = @_;
41              
42             for my $param (qw/affiliate_id api_id/) {
43             unless (exists $args{$param}) {
44             Carp::croak("missing mandatory parameter '$param'");
45             }
46             }
47              
48             _validate_affiliate_id($args{affiliate_id});
49              
50             bless {
51             %args,
52             }, $class;
53             }
54              
55             sub _validate_affiliate_id {
56             my $account = shift;
57              
58             unless ($account =~ m{9[0-9]{2}$}) {
59             Carp::croak("Postfix of affiliate_id is '900--999'");
60             }
61              
62             return 1;
63             }
64              
65             my %validate_table = (
66             hits => \&_validate_hits_param,
67             offset => \&_validate_offset_param,
68             sort => \&_validate_sort_param,
69             );
70              
71             sub search {
72             my ($self, %args) = @_;
73              
74             my %param;
75              
76             # mandatory parameters
77             $param{affiliate_id} = $self->{affiliate_id};
78             $param{api_id} = $self->{api_id};
79             $param{operation} = $args{operation} || 'ItemList';
80             $param{version} = _validate_version_param($args{version});
81             $param{timestamp} = $args{timestamp} || _format_current_time();
82             $param{site} = _validate_site_param($args{site});
83              
84             # optional parameters
85             for my $p (qw/hits offset sort/) {
86             if ($args{$p}) {
87             $param{$p} = $validate_table{$p}->($args{$p});
88             }
89             }
90              
91             if ($args{service}) {
92             my ($service, $floor)
93             = _validate_service_floor(@args{'site', 'service', 'floor'});
94              
95             $param{service} = $service;
96             $param{floor} = $floor if defined $floor;
97             }
98              
99             if ($args{keyword}) {
100             $param{keyword} = Encode::encode('euc-jp', $args{keyword});
101             }
102              
103             _set_root_node_name($param{version});
104              
105             $self->_send_request(%param);
106             }
107              
108             sub _set_root_node_name {
109             my $version = shift;
110              
111             # API 1.00 mistake spelling 'response'
112             $ROOT_NODE = $version eq '1.00' ? 'responce' : 'response';
113             }
114              
115             sub _validate_version_param {
116             my $version = shift;
117             return '2.00' unless defined $version;
118              
119             unless (grep { $version eq $_ } @supported_api_versions) {
120             Carp::croak("Invalid version '$version'");
121             }
122              
123             return $version;
124             }
125              
126             sub _validate_sort_param {
127             my $sort = shift;
128             my @sort_values = qw(rank +price -price date review);
129              
130             unless (grep {$sort eq $_} @sort_values) {
131             Carp::croak("'sort' parameter should be (@sort_values)");
132             }
133              
134             return $sort;
135             }
136              
137             sub _validate_site_param {
138             my $site = shift;
139              
140             unless (defined $site) {
141             Carp::croak("'site' parameter is mandatory parameter");
142             }
143              
144             unless ($site eq 'DMM.co.jp' || $site eq 'DMM.com') {
145             Carp::croak("'site' parameter should be 'DMM.co.jp' or 'DMM.com'");
146             }
147              
148             return $site;
149             }
150              
151             sub _validate_hits_param {
152             my $hits = shift;
153              
154             unless ($hits >= 1 && $hits <= 100) {
155             Carp::croak("'hits' parameter should be 1 <= n <= 100");
156             }
157              
158             return $hits;
159             }
160              
161             sub _validate_offset_param {
162             my $offset = shift;
163              
164             unless ($offset >= 1) {
165             Carp::croak("'offset' parameter should be positive number(n >= 1)");
166             }
167              
168             return $offset;
169             }
170              
171             sub _format_current_time {
172             strftime '%Y-%m-%d %T', localtime;
173             }
174              
175             sub last_response {
176             my $self = shift;
177             $self->{_last_response};
178             }
179              
180             sub _send_request {
181             my ($self, %args) = @_;
182              
183             my $uri = URI->new('http://affiliate-api.dmm.com/');
184             $uri->query_form(%args);
185              
186             my $res = __ua()->get( $uri->as_string );
187             unless ($res->is_success) {
188             Carp::croak("Download failed: " . $uri->as_string);
189             }
190              
191             $self->{_site} = $args{site}; # need for parsing actor information
192              
193             my $response = $self->_parse_response( \$res->content );
194             $self->{_last_response} = $res;
195             return $response;
196             }
197              
198             sub _parse_response {
199             my ($self, $content_ref) = @_;
200             my $decoded = _decode_xml_utf8( $content_ref );
201              
202             my $dom = XML::LibXML->load_xml(string => $decoded);
203              
204             my $res = WebService::DMM::Response->new();
205             my $message = _get_text_content(
206             node => $dom, path => "/$ROOT_NODE/result/message"
207             );
208             if (defined $message) {
209             my $cause = _get_text_content(
210             node => $dom, path => "/$ROOT_NODE/result/errors/error/value"
211             );
212             $res->cause($cause);
213             $res->is_success(0);
214             return $res;
215             }
216             $res->is_success(1);
217              
218             for my $p (qw/result_count total_count first_position/) {
219             my $text = _get_text_content(
220             node => $dom, path => "/$ROOT_NODE/result/$p"
221             );
222             $res->$p($text);
223             }
224              
225             $res->items( $self->_parse_items($dom) );
226             return $res;
227             }
228              
229             sub _parse_items {
230             my ($self, $dom) = @_;
231              
232             my @items;
233             for my $item_node ($dom->findnodes("/$ROOT_NODE/result/items/item")) {
234             my %param;
235              
236             for my $p (qw/service_name floor_name category_name/) {
237             $param{$p} = _get_text_content(node => $item_node, path => "$p");
238             }
239              
240             for my $p (qw/content_id product_id URL affiliateURL title date/) {
241             $param{$p} = $item_node->findvalue($p);
242             }
243              
244             # for Smart Phone
245             for my $p (qw/URLsp affiliateURLsp/) {
246             $param{$p} = _get_text_content(node => $item_node, path => $p);
247             }
248              
249             $param{image} = _image_urls($item_node);
250             $param{sample_images} = _sample_images($item_node);
251              
252             ## item/prices/*
253             for my $p (qw/price price_all list_price/) {
254             $param{$p} = _get_text_content(node => $item_node, path => "prices/$p");
255             }
256              
257             ## item/prices/deriveries/*
258             $param{deliveries} = _delivery_info($item_node);
259              
260             ## item/iteminfo
261             $param{keywords} = [
262             map { $_->findvalue('name') } $item_node->findnodes('iteminfo/keyword')
263             ];
264              
265             my ($actor_path, $node_num);
266             if ($self->{_site} eq 'DMM.co.jp') {
267             $actor_path = 'iteminfo/actress';
268             $node_num = 3; # actress node has another information
269             } else {
270             $actor_path = 'iteminfo/actor';
271             $node_num = 2;
272             }
273             $param{actors} = _personal_info('Actor', $item_node, $actor_path, $node_num);
274              
275             for my $p (qw/author director fighter/) {
276             my $class = ucfirst $p;
277             my $key = $p . 's';
278             $param{$key} = _personal_info($class, $item_node, "iteminfo/$p", 2);
279             }
280              
281             for my $p (qw/series maker label/) {
282             my $class = 'WebService::DMM::' . ucfirst $p;
283             my @nodes = $item_node->findnodes("iteminfo/$p");
284             next unless @nodes;
285              
286             $param{$p} = $class->new(
287             id => $nodes[0]->findvalue('id'),
288             name => $nodes[0]->findvalue('name')
289             );
290             }
291              
292             for my $p (qw/jancode maker_product isbn stock/) {
293             $param{$p} = _get_text_content(
294             node => $item_node, path => "iteminfo/$p"
295             );
296             }
297              
298             push @items, WebService::DMM::Item->new(%param);
299             }
300              
301             return \@items;
302             }
303              
304             sub _get_text_content {
305             my (%args) = @_;
306              
307             my @nodes = $args{node}->findnodes($args{path});
308             return unless @nodes;
309              
310             return $nodes[0]->textContent;
311             }
312              
313             sub _image_urls {
314             my $item_node = shift;
315              
316             my %image_url;
317             for my $p (qw/list small large/) {
318             $image_url{$p} = $item_node->findvalue("imageURL/$p");
319             }
320              
321             return \%image_url;
322             }
323              
324             sub _sample_images {
325             my $item_node = shift;
326              
327             my @image_nodes = $item_node->findnodes('sampleImageURL/sample_s/image');
328             return [ map { $_->textContent } @image_nodes ];
329             }
330              
331             sub _delivery_info {
332             my $item_node = shift;
333              
334             my @deliveries;
335             for my $node ($item_node->findnodes('iteminfo/prices/deliveries')) {
336             push @deliveries, WebService::DMM::Delivery->new(
337             type => $node->findvalue('type'),
338             price => $node->findvalue('price'),
339             );
340             }
341              
342             return scalar @deliveries != 0 ? \@deliveries : [];
343             }
344              
345             sub _personal_info {
346             my ($type, $node, $path, $node_num) = @_;
347              
348             my $class = 'WebService::DMM::Person::' . $type;
349              
350             my @persons;
351             my @person_nodes = $node->findnodes($path);
352             while (my ($name_node, $ruby_node) = splice @person_nodes, 0, $node_num) {
353             my $name_str = $name_node->findvalue('name');
354             my $id = $name_node->findvalue('id');
355             my $ruby_str = $ruby_node->findvalue('name');
356             my $ruby_id = $ruby_node->findvalue('id');
357              
358             unless ($ruby_id eq "${id}_ruby") {
359             Carp::croak("Internal Error(ruby_id=$ruby_id, id=${id})");
360             }
361              
362             my ($name, $name_aliases) = _separate_name($name_str);
363             my ($ruby, $ruby_aliases) = _separate_name($ruby_str);
364              
365             my %param = ( id => $id, name => $name, ruby => $ruby );
366              
367             if (defined $name_aliases) {
368             my @aliases;
369             my $length = scalar @{$name_aliases};
370             for my $i (0..($length - 1)) {
371             my $ruby_alias = defined $ruby_aliases->[$i] ? $ruby_aliases->[$i] : '';
372             push @aliases, {
373             name => $name_aliases->[$i],
374             ruby => $ruby_alias,
375             },
376             }
377              
378             $param{aliases} = \@aliases;
379             } else {
380             $param{aliases} = [];
381             }
382              
383             push @persons, $class->new( %param );
384             }
385              
386             my $retval = scalar @persons ? \@persons : [];
387             return $retval;
388             }
389              
390             sub _separate_name {
391             my $name_str = shift;
392              
393             # Name paramter may have Zenkaku/Hankaku spaces and comma.
394             if ($name_str =~ m{(.+?)[((](.+?)[))]}) {
395             my ($name, $aliases_str) = ($1, $2);
396              
397             if ($aliases_str) {
398             return ($name, [ split /[,、]/, $aliases_str ]);
399             } else {
400             return ($name, []);
401             }
402             } else {
403             return ($name_str);
404             }
405             }
406              
407             # parsing XML encoded EUC-jp is difficult.
408             sub _decode_xml_utf8 {
409             my $content_ref = shift;
410             $$content_ref =~ s{encoding="euc-jp"}{encoding="utf-8"};
411              
412             return Encode::decode('euc-jp', $$content_ref);
413             }
414              
415             sub items {
416             my $self = shift;
417             return @{$self->{items}};
418             }
419              
420             my %service_floor = (
421             'DMM.com' => {
422             lod => [qw/akb48 ske48/],
423             digital => [qw/bandai anime video idol cinema fight/],
424             monthly => [qw/toei animate shochikugeino idol cinepara dgc fleague/],
425             digital_book => [qw/comic novel photo otherbooks/],
426             pcsoft => [qw/pcgame pcsoft/],
427             mono => [qw/dvd cd book game hobby kaden houseware gourmet/],
428             rental => [qw/rental_dvd ppr_dvd rental_cd ppr_cd comic/],
429             nandemo => [qw/fashion_ladies fashion_mems rental_iroiro/],
430             },
431              
432             'DMM.co.jp' => {
433             digital => [qw/videoa videoc nikkatsu anime photo/],
434             monthly => [qw/shirouto nikkatsu paradisetv animech dream avstation
435             playgirl alice crystal hmp waap momotarobb moodyz
436             prestige jukujo sod mania s1 kmp mousouzoku/],
437             ppm => [qw/video videoc/],
438             pcgame => [qw/pcgame/],
439             doujin => [qw/doujin/],
440             book => [qw/book/],
441             mono => [qw/dvd goods anime pcgame book doujin/],
442             rental => [qw/rental_dvd ppr_dvd/],
443             },
444             );
445              
446             sub _validate_service_floor {
447             my ($site, $service, $floor) = @_;
448              
449             unless (defined $floor) {
450             return ($service, undef);
451             }
452              
453             unless (exists $service_floor{$site}->{$service}) {
454             my @keys = keys %service_floor;
455             Carp::croak("Invalid service '$service': (@keys)");
456             }
457              
458             my @floors = @{$service_floor{$site}->{$service}};
459             unless (grep { $floor eq $_ } @floors) {
460             Carp::croak("Invalid floor '$floor'(service $service): (@floors)");
461             }
462              
463             return ($service, $floor);
464             }
465              
466             1;
467             __END__