File Coverage

blib/lib/Net/Flickr/API.pm
Criterion Covered Total %
statement 27 200 13.5
branch 0 56 0.0
condition 0 6 0.0
subroutine 9 21 42.8
pod 6 10 60.0
total 42 293 14.3


line stmt bran cond sub pod time code
1 1     1   997 use strict;
  1         2  
  1         85  
2              
3             # $Id: API.pm,v 1.35 2009/08/02 17:16:12 asc Exp $
4             # -*-perl-*-
5              
6             package Net::Flickr::API;
7              
8             $Net::Flickr::API::VERSION = '1.7';
9              
10             =head1 NAME
11              
12             Net::Flickr::API - base API class for Net::Flickr::* libraries
13              
14             =head1 SYNOPSIS
15              
16             package Net::Flickr::RDF;
17             use base qw (Net::Flickr::API);
18              
19             =head1 DESCRIPTION
20              
21             Base API class for Net::Flickr::* libraries
22              
23             Net::Flickr::API is a wrapper for Flickr::API that provides support for throttling
24             API calls (per second), retries if the API is disabled and marshalling of API responses
25             into XML::LibXML or XML::XPath objects.
26              
27             =head1 OPTIONS
28              
29             Options are passed to Net::Flickr::Backup using a Config::Simple object or
30             a valid Config::Simple config file. Options are grouped by "block".
31              
32             =head2 flick
33              
34             =over 4
35              
36             =item * B
37              
38             String. I
39              
40             A valid Flickr API key.
41              
42             =item * B
43              
44             String. I
45              
46             A valid Flickr Auth API secret key.
47              
48             =item * B
49              
50             String. I
51              
52             A valid Flickr Auth API token.
53              
54             =item * B
55              
56             String. I
57              
58             The B defines which XML/XPath handler to use to process API responses.
59              
60             =over 4
61              
62             =item * B
63              
64             Use XML::LibXML.
65              
66             =item * B
67              
68             Use XML::XPath.
69              
70             =back
71              
72             =back
73              
74             =head2 reporting
75              
76             =over
77              
78             =item * B
79              
80             Boolean.
81              
82             Default is false.
83              
84             =item * B
85              
86             String.
87              
88             The default handler is B, as in C
89              
90             =item * B
91              
92             For example, the following :
93              
94             reporting_handler_args=name:foobar;min_level=info
95              
96             Would be converted as :
97              
98             (name => "foobar",
99             min_level => "info");
100              
101             The default B argument is "__report". The default B argument
102             is "info".
103              
104             =back
105              
106             =cut
107              
108 1     1   1124 use Config::Simple;
  1         20931  
  1         12  
109              
110 1     1   975 use Flickr::API;
  1         80342  
  1         32  
111 1     1   889 use Flickr::Upload;
  1         4315  
  1         30  
112              
113 1     1   873 use Readonly;
  1         3165  
  1         54  
114 1     1   1076 use Data::Dumper;
  1         6892  
  1         74  
115              
116 1     1   926 use Log::Dispatch;
  1         12892  
  1         30  
117 1     1   734 use Log::Dispatch::Screen;
  1         2224  
  1         654  
118              
119             Readonly::Scalar my $PAUSE_SECONDS_OK => 2;
120             Readonly::Scalar my $PAUSE_SECONDS_UNAVAILABLE => 4;
121             Readonly::Scalar my $PAUSE_MAXTRIES => 10;
122             Readonly::Scalar my $PAUSE_ONSTATUS => 503;
123              
124             Readonly::Scalar my $RETRY_MAXTRIES => 10;
125              
126             =head1 PACKAGE METHODS
127              
128             =cut
129              
130             =head2 __PACKAGE__->new($cfg)
131              
132             Where B<$cfg> is either a valid I object or the path
133             to a file that can be parsed by I.
134              
135             Returns a I object.
136              
137             =cut
138              
139             sub new {
140 0     0 1   my $pkg = shift;
141 0           my $cfg = shift;
142            
143 0           my $self = {'__wait' => time() + $PAUSE_SECONDS_OK,
144             '__paused' => 0,
145             '__retries' => 0,};
146            
147 0           bless $self,$pkg;
148            
149 0 0         if (! $self->init($cfg)) {
150 0           undef $self;
151             }
152            
153 0           return $self;
154             }
155              
156             sub init {
157 0     0 0   my $self = shift;
158 0           my $cfg = shift;
159            
160 0 0         $self->{cfg} = (UNIVERSAL::isa($cfg, "Config::Simple")) ? $cfg : Config::Simple->new($cfg);
161            
162 0 0         if ($self->{cfg}->param("flickr.api_handler") !~ /^(?:XPath|LibXML)$/) {
163 0           warn "Invalid API handler";
164 0           return 0;
165             }
166            
167             #
168            
169             my $log_fmt = sub {
170 0     0     my %args = @_;
171            
172 0           my $msg = $args{'message'};
173 0           chomp $msg;
174            
175 0 0         if ($args{'level'} eq "error") {
176            
177 0           my ($ln, $sub) = (caller(4))[2,3];
178 0           $sub =~ s/.*:://;
179            
180 0           return sprintf("[%s][%s, ln%d] %s\n",
181             $args{'level'}, $sub, $ln, $msg);
182             }
183            
184 0           return sprintf("[%s] %s\n", $args{'level'}, $msg);
185 0           };
186            
187 0           my $logger = Log::Dispatch->new(callbacks=>$log_fmt);
188 0           my $error = Log::Dispatch::Screen->new(name => '__error',
189             min_level => 'error',
190             stderr => 1);
191            
192 0           $logger->add($error);
193              
194             #
195             # Custom report logging
196             #
197              
198 0 0         if ($self->{cfg}->param("reporting.enable")) {
199              
200 0   0       my $report_handler = $self->{cfg}->param("reporting.handler") || "Screen";
201 0           $report_handler =~ s/:://g;
202              
203 0           my $report_pkg = "Log::Dispatch::$report_handler";
204 0           eval "require $report_pkg";
205              
206 0 0         if ($@) {
207 0           warn "Failed to load $report_pkg, $@";
208 0           return 0;
209             }
210              
211 0           my %report_args = ();
212              
213 0 0         if (my $args = $self->{cfg}->param("reporting.handler_args")) {
214              
215 0           foreach my $part (split(",", $args)) {
216 0           my ($key, $value) = split(":", $part);
217 0           $report_args{$key} = $value;
218             }
219             }
220              
221 0   0       $report_args{'name'} ||= "__report";
222 0   0       $report_args{'min_level'} ||= "info";
223              
224 0           my $reporter = $report_pkg->new(%report_args);
225              
226 0 0         if (! $reporter) {
227 0           warn "Failed to instantiate $report_pkg, $!";
228 0           return 0;
229             }
230              
231 0           $logger->add($reporter);
232             }
233              
234 0           $self->{'__logger'} = $logger;
235              
236             #
237            
238 0           $self->{api} = Flickr::API->new({key => $self->{cfg}->param("flickr.api_key"),
239             secret => $self->{cfg}->param("flickr.api_secret"),
240             handler => $self->{cfg}->param("flickr.api_handler")});
241            
242 0           my $pkg = ref($self);
243 0           my $version = undef;
244            
245 0           do {
246 0           my $ref = join("::", $pkg, "VERSION");
247            
248 1     1   6 no strict "refs";
  1         2  
  1         1337  
249 0           $version = ${$ref};
  0            
250             };
251            
252 0           my $agent_string = sprintf("%s/%s", $pkg, $version);
253            
254 0           $self->{api}->agent($agent_string);
255 0           return 1;
256             }
257              
258             =head1 OBJECT METHODS
259              
260             =cut
261              
262             =head2 $obj->api_call(\%args)
263              
264             Valid args are :
265              
266             =over 4
267              
268             =item * B
269              
270             A string containing the name of the Flickr API method you are
271             calling.
272              
273             =item * B
274              
275             A hash ref containing the key value pairs you are passing to
276             I
277              
278             =back
279              
280             If the method encounters any errors calling the API, receives an API error
281             or can not parse the response it will log an error event, via the B method,
282             and return undef.
283              
284             Otherwise it will return a I object (if XML::LibXML is
285             installed) or a I object.
286              
287             =cut
288              
289             sub api_call {
290 0     0 1   my $self = shift;
291 0           my $args = shift;
292            
293             #
294            
295             # check to see if we need to take
296             # breather (are we pounding or are
297             # we not?)
298              
299 0           while (time < $self->{'__wait'}) {
300              
301 0           my $debug_msg = sprintf("trying not to beat up the Flickr servers, pause for %.2f seconds\n",
302             $PAUSE_SECONDS_OK);
303              
304 0           $self->log()->debug($debug_msg);
305 0           sleep($PAUSE_SECONDS_OK);
306             }
307            
308             # send request
309            
310 0 0         if (exists($args->{'args'}->{'api_sig'})) {
311 0           delete $args->{'args'}->{'api_sig'};
312             }
313              
314 0           $args->{'args'}->{'auth_token'} = $self->{cfg}->param("flickr.auth_token");
315              
316             #
317              
318 0           my $req = Flickr::API::Request->new($args);
319 0           my $res = undef;
320              
321 0           $self->log()->debug("calling $args->{method} : " . Dumper($args->{args}));
322            
323 0           eval {
324 0           $res = $self->{'api'}->execute_request($req);
325             };
326              
327 0 0         if ($@) {
328 0           $self->log()->error("Fatal error calling the Flickr API, $@");
329              
330 0           $self->{'__wait'} = time + $PAUSE_SECONDS_OK;
331 0           $self->{'__paused'} = 0;
332 0           return undef;
333             }
334              
335             #
336             # check for 503 status
337             #
338              
339 0 0         if ($res->code() eq $PAUSE_ONSTATUS) {
340 0           $res = $self->retry_api_call($args, $res);
341             }
342            
343 0           $self->{'__wait'} = time + $PAUSE_SECONDS_OK;
344 0           $self->{'__paused'} = 0;
345              
346 0           return $self->parse_api_call($args, $res);
347             }
348              
349             =head2 $obj->get_auth()
350              
351             Return an XML I element containing the Flickr auth token information for
352             the current object.
353              
354             Returns undef if no token information is present.
355              
356             =cut
357              
358             sub get_auth {
359 0     0 1   my $self = shift;
360            
361 0 0         if (! $self->{'__auth'}) {
362 0           my $auth = $self->api_call({"method" => "flickr.auth.checkToken"});
363            
364 0 0         if (! $auth) {
365 0           return undef;
366             }
367            
368 0           my $nsid = $auth->find("/rsp/auth/user/\@nsid")->string_value();
369            
370 0 0         if (! $nsid) {
371 0           $self->log()->error("unabled to determine ID for token");
372 0           return undef;
373             }
374            
375 0           $self->{'__auth'} = $auth;
376             }
377            
378 0           return $self->{'__auth'};
379             }
380              
381             =head2 $obj->get_auth_nsid()
382              
383             Return the Flickr NSID of the user associated with the Flickr auth token information
384             for the current object.
385              
386             Returns undef if no token information is present.
387              
388             =cut
389              
390             sub get_auth_nsid {
391 0     0 1   my $self = shift;
392              
393 0 0         if (my $auth = $self->get_auth()){
394 0           return $auth->find("/rsp/auth/user/\@nsid")->string_value();
395             }
396              
397 0           return undef;
398             }
399              
400             sub parse_api_call {
401 0     0 0   my $self = shift;
402 0           my $args = shift;
403 0           my $res = shift;
404              
405 0           $self->log()->debug($res->decoded_content());
406              
407 0           my $xml = $self->_parse_results_xml($res);
408              
409 0 0         if (! $xml) {
410 0           $self->log()->error("failed to parse API response, calling $args->{method}");
411 0           $self->log()->error($res->decoded_content());
412 0           return undef;
413             }
414              
415 0           my $stat = $xml->find("/rsp/\@stat")->string_value();
416              
417 0 0         if ($stat eq "fail") {
418 0           my $code = $xml->findvalue("/rsp/err/\@code");
419 0           my $msg = $xml->findvalue("/rsp/err/\@msg");
420              
421 0           $self->log()->error(sprintf("[%s] %s (calling $args->{method})\n",
422             $code,
423             $msg));
424              
425 0 0         if ($code==0) {
426 0           $self->log()->info(sprintf("api disabled attempting %s/%s tries to see if it's come back up", $self->{'__retries'}, $RETRY_MAXTRIES));
427 0           return $self->api_disabled($args, $res);
428             }
429             }
430              
431 0           $self->{'__retries'} = 0;
432              
433 0 0         return ($@) ? undef : $xml;
434             }
435              
436             sub _parse_results_xml {
437 0     0     my $self = shift;
438 0           my $res = shift;
439              
440 0           my $xml = undef;
441              
442             #
443             # Please for Cal to someday accept the patch to add
444             # response handlers to Flickr::API...
445             #
446              
447 0 0         if ($self->{cfg}->param("flickr.api_handler") eq "XPath") {
448 0           eval "require XML::XPath";
449              
450 0 0         if (! $@) {
451 0           eval {
452 0           $xml = XML::XPath->new(xml=>$res->decoded_content());
453             };
454             }
455             }
456            
457             else {
458 0           eval "require XML::LibXML";
459              
460 0 0         if (! $@) {
461 0           eval {
462 0           my $parser = XML::LibXML->new();
463 0           $xml = $parser->parse_string($res->decoded_content());
464             };
465             }
466             }
467            
468             #
469              
470 0 0         if (! $xml) {
471 0           $self->log()->error("XML parse error : $@");
472 0           return undef;
473             }
474            
475             #
476              
477 0           return $xml;
478             }
479              
480             sub api_disabled {
481 0     0 0   my $self = shift;
482 0           my $args = shift;
483 0           my $res = shift;
484              
485 0           $self->{'__retries'} ++;
486              
487 0 0         if ($self->{'__retries'} > $RETRY_MAXTRIES) {
488 0           $self->log()->critical(sprintf("API still down after %s tries - exiting", $RETRY_MAXTRIES));
489 0           exit;
490             }
491              
492 0           $res = $self->retry_api_call($args, $res);
493              
494 0 0         if (! $res) {
495 0           $self->log()->critical("Returned false during 'api disabled' retry. That can only be bad - exiting");
496 0           exit;
497             }
498              
499 0           return $res;
500             }
501              
502             sub retry_api_call {
503 0     0 0   my $self = shift;
504 0           my $args = shift;
505 0           my $res = shift;
506              
507             # you are in a dark and twisty corridor
508             # where all the errors look the same -
509             # just give up if we hit this ceiling
510            
511 0           $self->{'__paused'} ++;
512            
513 0 0         if ($self->{'__paused'} > $PAUSE_MAXTRIES) {
514            
515 0           my $errmsg = sprintf("service returned '%d' status %d times; exiting",
516             $PAUSE_ONSTATUS, $PAUSE_MAXTRIES);
517            
518 0           $self->log()->error($errmsg);
519 0           return undef;
520             }
521            
522 0           my $retry_after = $res->header("Retry-After");
523 0           my $debug_msg = undef;
524            
525 0 0         if ($retry_after ) {
526 0           $debug_msg = sprintf("service unavailable, requested to retry in %d seconds",
527             $retry_after);
528             }
529            
530             else {
531 0           $retry_after = $PAUSE_SECONDS_UNAVAILABLE * $self->{'__paused'};
532 0           $debug_msg = sprintf("service unavailable, pause for %.2f seconds",
533             $retry_after);
534             }
535            
536 0           $self->log()->debug($debug_msg);
537 0           sleep($retry_after);
538            
539             # try, try again
540            
541 0           return $self->api_call($args);
542             }
543              
544             =head2 $obj->upload(\%args)
545              
546             This is a helper method that simply wraps calls to the I upload
547             method. All the arguments are the same. For complete documentation please consult:
548              
549             L
550              
551             (Note: There's no need to pass an auth_token argument as the wrapper will take care
552             of for you.)
553              
554             Returns a photo ID (or a ticket ID if the call is asynchronous) on success or false
555             if there was a problem.
556              
557             =cut
558              
559             sub upload {
560 0     0 1   my $self = shift;
561 0           my $args = shift;
562              
563 0           $args->{'auth_token'} = $self->{cfg}->param("flickr.auth_token");
564              
565 0           my $ua = Flickr::Upload->new({'key' => $self->{cfg}->param("flickr.api_key"),
566             'secret' => $self->{cfg}->param("flickr.api_secret")});
567            
568 0           my $id = undef;
569              
570 0           eval {
571 0           $id = $ua->upload(%$args);
572             };
573              
574 0 0         if ($@){
575 0           $self->log()->error("upload failed: $@");
576 0           return 0;
577             }
578              
579 0           return $id;
580             }
581              
582             =head2 $obj->log()
583              
584             Returns a I object.
585              
586             =cut
587              
588             sub log {
589 0     0 1   my $self = shift;
590 0           return $self->{'__logger'};
591             }
592              
593             =head1 VERSION
594              
595             1.7
596              
597             =head1 DATE
598              
599             $Date: 2009/08/02 17:16:12 $
600              
601             =head1 AUTHOR
602              
603             Aaron Straup Cope Eascope@cpan.orgE
604              
605             =head1 SEE ALSO
606              
607             L
608              
609             L
610              
611             L
612              
613             L
614              
615             =head1 BUGS
616              
617             Please report all bugs via http://rt.cpan.org/
618              
619             =head1 LICENSE
620              
621             Copyright (c) 2005-2008 Aaron Straup Cope. All Rights Reserved.
622              
623             This is free software. You may redistribute it and/or
624             modify it under the same terms as Perl itself.
625              
626             =cut
627              
628             return 1;
629              
630             __END__