File Coverage

blib/lib/Net/Flickr/Backup.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             # $Id: Backup.pm,v 1.107 2010/12/19 19:06:56 asc Exp $
2             # -*-perl-*-
3              
4 1     1   3214 use strict;
  1         2  
  1         39  
5 1     1   7 use warnings;
  1         1  
  1         283  
6              
7             package Net::Flickr::Backup;
8 1     1   16 use base qw (Net::Flickr::RDF);
  1         2  
  1         1455  
9              
10             $Net::Flickr::Backup::VERSION = '3.1';
11              
12             =head1 NAME
13              
14             Net::Flickr::Backup - OOP for backing up your Flickr photos locally
15              
16             =head1 SYNOPSIS
17              
18             use Net::Flickr::Backup;
19             use Log::Dispatch::Screen;
20            
21             my $flickr = Net::Flickr::Backup->new($cfg);
22              
23             my $feedback = Log::Dispatch::Screen->new('name' => 'info',
24             'min_level' => 'info');
25              
26             $flickr->log()->add($feedback);
27             $flickr->backup();
28              
29             =head1 DESCRIPTION
30              
31             OOP for backing up your Flickr photos locally.
32              
33             =head1 OPTIONS
34              
35             Options are passed to Net::Flickr::Backup using a Config::Simple object or
36             a valid Config::Simple config file. Options are grouped by "block".
37              
38             =head2 flickr
39              
40             =over 4
41              
42             =item * B
43              
44             String. I
45              
46             A valid Flickr API key.
47              
48             =item * B
49              
50             String. I
51              
52             A valid Flickr Auth API secret key.
53              
54             =item * B
55              
56             String. I
57              
58             A valid Flickr Auth API token.
59              
60             The B defines which XML/XPath handler to use to process API responses.
61              
62             =over 4
63              
64             =item * B
65              
66             Use XML::LibXML.
67              
68             =item * B
69              
70             Use XML::XPath.
71              
72             =back
73              
74             =back
75              
76             =head2 backup
77              
78             =over 4
79              
80             =item * B
81              
82             String. I
83              
84             The root folder where you want photographs to be stored. Individual
85             files are named using the following pattern :
86              
87             B/B/B/B
/B-B-B_B.jpg
88              
89             Where the various components are :
90              
91             =over 4
92              
93             =item * B
94              
95             photo[@id=123]/dates/@taken
96              
97             =item * B
98              
99             photo/@id
100              
101             =item * B
102              
103             photo[@id=123]/title
104              
105             Unicode characters translated in to ASCII (using Text::Unidecode) and the
106             entire string is stripped anything that is not an alphanumeric, underbar,
107             dash or a square bracket.
108              
109             =item * B
110              
111             Net::Flickr::Backup will attempt to fetch not only the original file uploaded
112             to Flickr but also, depending on your config options, the medium and square
113             versions. Filenames will be modified as follows :
114              
115             =over 4
116              
117             =item * B
118              
119             The original photo you uploaded to the Flickr servers. No extension is
120             added.
121              
122             =item * B
123              
124             These photos are scaled to 500 pixels at the longest dimension. A B<_m>
125             extension is added.
126              
127             =item * B
128              
129             These photos are scaled to 640 pixels at the longest dimension. A B<_z>
130             extension is added.
131              
132             =item * B
133              
134             These photos are to cropped to 75 x 75 pixels at the center. A B<_s>
135             extension is added.
136              
137             =item * B
138              
139             The MP4 version of a video uploaded to Flickr. A B<_site> extension is added.
140              
141             =item * B
142              
143             An original video uploaded to Flickr. No extentsion is added.
144              
145             =back
146              
147             =back
148              
149             =item * B
150              
151             Boolean.
152              
153             Retrieve the "original" version of a photo from the Flickr servers.
154              
155             Default is true.
156              
157             =item * B
158              
159             Boolean.
160              
161             Retrieve the "original" version of a video from the Flickr servers.
162              
163             Default is true.
164              
165             =item * B
166              
167             Boolean.
168              
169             Retrieve the "medium" version of a photo from the Flickr servers; these photos
170             have been scaled to 500 pixels at the longest dimension.
171              
172             Default is false.
173              
174             =item * B
175              
176             Boolean.
177              
178             Retrieve the "medium" version of a photo from the Flickr servers; these photos
179             have been scaled to 640 pixels at the longest dimension.
180              
181             Default is false.
182              
183             =item * B
184              
185             Boolean.
186              
187             Retrieve the "square" version of a photo from the Flickr servers; these photos
188             have been cropped to 75 x 75 pixels at the center.
189              
190             Default is false.
191              
192             =item * B
193              
194             Boolean.
195              
196             Retrieve the "site MP4" version of a video from the Flickr servers;
197              
198             Default is false.
199              
200             =item * B
201              
202             Boolean.
203              
204             If true then, for each Flickr photo ID backed up, the library will check
205             B for images (and metadata files) with a matching ID but
206             a different name. Matches will be deleted.
207              
208             =item * B
209              
210             Boolean.
211              
212             Force a photograph to be backed up even if it has not changed.
213              
214             Default is false.
215              
216             =back
217              
218             =head2 rdf
219              
220             =over 4
221              
222             =item * B
223              
224             Boolean.
225              
226             Generate an RDF description for each photograph. Descriptions
227             are written to disk in separate files.
228              
229             Default is false.
230              
231             =item * B
232              
233             String.
234              
235             The path where RDF data dumps for a photo should be written. The default
236             is the same path as B.
237              
238             File names are generated with the same pattern used to name
239             photographs.
240              
241             =item * B
242              
243             Boolean.
244              
245             Set to true if you want the RDF dump for a photo to be stored in the file's
246             JPEG COM block. RDF data will only be stored (for the time being) in the original
247             image file and not any of the scaled versions.
248              
249             This option will only work for JPEG files and is still B. It may change
250             or, you know, not always work. Using Adobe's XMP spec is on the list of things to poke
251             at so if you've got any suggestions on the subject, they'd be welcome.
252              
253             Default is false.
254              
255             =item * B
256              
257             String.
258              
259             If defined this string is applied as regular expression substitution to
260             B.
261              
262             Default is to append the B URI protocol to a path.
263              
264             =item * B
265              
266             Boolean.
267              
268             If true and a photo has geodata (latitude, longitude) associated with it, then
269             the geonames.org database will be queried for a corresponding match. Data will be
270             added as properties of the photo's geo:Point description. For example :
271              
272            
273             -122.025151
274             16
275             visbility
276             37.417839
277             public
278            
279            
280              
281            
282             PPLX
283             US
284             CA
285             California
286             Santa Clara
287             2
288            
289              
290             =back
291              
292             =head2 iptc
293              
294             =over 4
295              
296             =item * B
297              
298             Boolean.
299              
300             If true, then a limited set of metadata associated with a photo will be stored
301             as IPTC information.
302              
303             A photo's title is stored as the IPTC B, description as B
304             and tags are stored in one or more B headers. Per the IPTC 7901 spec,
305             all text is converted to the ISO-8859-1 character encoding.
306              
307             For example :
308              
309             exiv2 -pi /home/asc/photos/2006/06/20/20060620-171674319-mie.jpg
310             Iptc.Application2.RecordVersion Short 1 2
311             Iptc.Application2.Keywords String 11 cameraphone
312             Iptc.Application2.Keywords String 15 "san francisco"
313             Iptc.Application2.Keywords String 5 filtr
314             Iptc.Application2.Keywords String 3 mie
315             Iptc.Application2.Keywords String 20 upcoming:event=77752
316             Iptc.Application2.Headline String 3 Mie
317              
318             Default is false.
319              
320             =back
321              
322             =head2 search
323              
324             Any valid parameter that can be passed to the I
325             method B 'user_id' which is pre-filled with the user_id that
326             corresponds to the B token.
327              
328             =head2 modified_since
329              
330             String.
331              
332             This specifies a time-based limiting criteria for fetching photos.
333              
334             The syntax is B<(n)(modifier)> where B<(n)> is a positive integer and B<(modifier)>
335             may be one of the following :
336              
337             =over 4
338              
339             =item * B
340              
341             Fetch photos that have been modified in the last B<(n)> hours.
342              
343             =item * B
344              
345             Fetch photos that have been modified in the last B<(n)> days.
346              
347             =item * B
348              
349             Fetch photos that have been modified in the last B<(n)> weeks.
350              
351             =item * B
352              
353             Fetch photos that have been modified in the last B<(n)> months.
354              
355             =back
356              
357             =cut
358              
359             use utf8;
360             use Encode;
361             use English;
362             use Data::Dumper;
363              
364             use Text::Unidecode;
365              
366             use File::Basename;
367             use File::Path;
368             use File::Spec;
369             use File::Find::Rule;
370              
371             use DirHandle;
372              
373             use IO::AtomicFile;
374             use IO::Scalar;
375             use LWP::Simple;
376             use LWP::UserAgent;
377             use HTTP::Request;
378              
379             use Memoize;
380             use Sys::Hostname;
381              
382             Readonly::Hash my %FETCH_SIZES => (
383             'Original' => '',
384             'Medium' => '_m',
385             'Medium 640' => '_z',
386             'Square' => '_s',
387             'Video Original' => '',
388             'Site MP4' => '_site',
389             );
390              
391             Readonly::Scalar my $FLICKR_URL => "http://www.flickr.com/";
392             Readonly::Scalar my $FLICKR_URL_PHOTOS => $FLICKR_URL . "photos/";
393              
394             =head1 PACKAGE METHODS
395              
396             =cut
397              
398             =head2 __PACKAGE__->new($cfg)
399              
400             Returns a I object.
401              
402             =cut
403              
404             # Defined in Net::Flickr::API
405              
406             sub init {
407             my $self = shift;
408             my $cfg = shift;
409            
410             if (! $self->SUPER::init($cfg)) {
411             return undef;
412             }
413            
414             #
415             # Ensure that we have 'flickr' and 'backup'
416             # config blocks
417             #
418              
419             foreach my $block ('flickr', 'backup') {
420            
421             my $test = $self->{cfg}->param(-block=>$block);
422            
423             if (! keys %$test) {
424             $self->log()->error("unable to find any properties for $block block in config file");
425             return undef;
426             }
427             }
428              
429             #
430              
431             $self->{'__lastmod_since'} = 0;
432             $self->{'__callbacks'} = {};
433             $self->{'__cancel'} = 0;
434              
435             $self->{'__hostname'} = undef;
436              
437             #
438              
439             memoize("_clean");
440             return 1;
441             }
442              
443             =head1 OBJECTS METHODS YOU SHOULD CARE ABOUT
444              
445             =cut
446              
447             =head2 $obj->backup()
448              
449             Returns true or false.
450              
451             =cut
452              
453             sub backup {
454             my $self = shift;
455             my $args = shift;
456            
457             my $auth = $self->get_auth();
458            
459             if (! $auth) {
460             return 0;
461             }
462            
463             #
464             #
465             #
466              
467             my $photos_root = $self->{cfg}->param("backup.photos_root");
468            
469             if (! $photos_root) {
470             $self->log()->error("no photo root defined, exiting");
471             return 0;
472             }
473            
474             #
475             #
476             #
477              
478             my $poll_meth = "flickr.photos.search";
479             my $poll_args = $self->{cfg}->param(-block=>"search");
480              
481             $poll_args->{'user_id'} = $auth->find("/rsp/auth/user/\@nsid")->string_value();
482              
483             if (my $min_date = $self->{cfg}->param("search.modified_since")) {
484              
485             if ($min_date !~ /^\d+$/) {
486             $min_date = &_mk_mindate($min_date);
487              
488             if (! $min_date) {
489             $self->log()->error("unable to parse min date criteria, exiting");
490             return 0;
491             }
492             }
493              
494             $poll_meth = "flickr.photos.recentlyUpdated";
495             $poll_args = {min_date => $min_date};
496              
497             $self->{'__lastmod_since'} = $min_date;
498             }
499              
500             #
501             #
502             #
503              
504             $self->log()->info("search args ($poll_meth) : " . Dumper($poll_args));
505              
506             #
507             #
508             #
509              
510             my $num_pages = 0;
511             my $current_page = 1;
512            
513             my $poll = 1;
514            
515             while ($poll) {
516            
517             if ($self->{'__cancel'}) {
518             last;
519             }
520              
521             $poll_args->{page} = $current_page;
522            
523             #
524            
525             my $photos = $self->api_call({"method" => $poll_meth,
526             args => $poll_args});
527            
528             if (! $photos) {
529             return 0;
530             }
531              
532             #
533              
534             if (($current_page == 1) && ($self->_has_callback("start_backup_queue"))) {
535             $self->_execute_callback("start_backup_queue", $photos);
536             }
537            
538             $num_pages = $photos->find("/rsp/photos/\@pages")->string_value();
539              
540             #
541            
542             foreach my $node ($photos->findnodes("/rsp/photos/photo")) {
543            
544             if ($self->{'__cancel'}) {
545             last;
546             }
547              
548             $self->{'__files'} = {};
549            
550             my $id = $node->getAttribute("id");
551             my $secret = $node->getAttribute("secret");
552            
553             $self->log()->info(sprintf("process image %s (%s)",
554             $id, &_clean($node->getAttribute("title"))));
555            
556             #
557              
558             if ($self->_has_callback("start_backup_photo")) {
559             $self->_execute_callback("start_backup_photo", $node);
560             }
561            
562             my $ok = $self->backup_photo($id, $secret);
563              
564             if ($self->_has_callback("finish_backup_photo")) {
565             $self->_execute_callback("finish_backup_photo", $node, $ok);
566             }
567              
568             }
569            
570             if ($current_page >= $num_pages) {
571             $poll = 0;
572             }
573            
574             $current_page ++;
575             }
576            
577             #
578              
579             if ($self->_has_callback("finish_backup_queue")) {
580             $self->_execute_callback("finish_backup_queue");
581             }
582            
583             #
584            
585             if ((! $self->{'__cancel'}) && ($self->{cfg}->param("backup.scrub_backups"))) {
586             $self->log()->info("scrubbing backups");
587             $self->scrub();
588             }
589            
590             return 1;
591             }
592              
593             =head1 OBJECT METHODS YOU MAY CARE ABOUT
594              
595             =cut
596              
597             =head2 $obj->backup_photo($id,$secret)
598              
599             Backup an individual photo. This method is called internally by
600             I.
601              
602             =cut
603              
604             sub backup_photo {
605             my $self = shift;
606             my $id = shift;
607             my $secret = shift;
608            
609             # FIX ME : add 'skip' hash containing id+secret
610             # If there is a problem storing photo data, ensure
611             # that it is not accidentally scrubbed.
612              
613             if (! $self->get_auth()) {
614             return 0;
615             }
616            
617             #
618            
619             my $force = $self->{cfg}->param("backup.force");
620             my $photos_root = $self->{cfg}->param("backup.photos_root");
621            
622             if (! $photos_root) {
623             $self->log()->error("no photo root defined, exiting");
624             return 0;
625             }
626            
627             #
628              
629             my $info = $self->api_call({method =>"flickr.photos.getInfo",
630             args => {'photo_id' => $id,
631             'secret' => $secret}});
632            
633             if (! $info) {
634             return 0;
635             }
636            
637             $self->{'_scrub'}->{$id} = [];
638            
639             my $img = ($info->findnodes("/rsp/photo"))[0];
640            
641             if (! $img) {
642             return 0;
643             }
644            
645             my $dates = ($img->findnodes("dates"))[0];
646            
647             my $last_update = $dates->getAttribute("lastupdate");
648             my $has_changed = 1;
649            
650             #
651            
652             my %data = (photo_id => $id,
653             user_id => $img->find("owner/\@nsid")->string_value(),
654             title => $img->find("title")->string_value(),
655             taken => $dates->getAttribute("taken"),
656             posted => $dates->getAttribute("posted"),
657             lastmod => $last_update);
658            
659             #
660            
661             my $title = &_clean($data{title}) || "untitled";
662              
663             my $dt = $data{taken};
664            
665             $dt =~ /^(\d{4})-(\d{2})-(\d{2})/;
666             my ($yyyy,$mm,$dd) = ($1,$2,$3);
667            
668             #
669            
670             my $sizes = $self->api_call({method => "flickr.photos.getSizes",
671             args => {photo_id => $id}});
672            
673             if (! $sizes) {
674             return 0;
675             }
676            
677             #
678            
679             my $fetch_cfg = $self->{cfg}->param(-block=>"backup");
680            
681             my $files_modified = 0;
682              
683             foreach my $label (keys %FETCH_SIZES) {
684            
685             my $fetch_label = lc($label);
686             $fetch_label =~ s/ /_/g;
687              
688             my $fetch_param = "fetch_" . $fetch_label;
689             my $do_fetch = 1;
690            
691             if (($label !~ /Original/) || (exists($fetch_cfg->{$fetch_param}))) {
692             $do_fetch = $fetch_cfg->{$fetch_param};
693             }
694            
695             if (! $do_fetch) {
696             $self->log()->debug("$fetch_param option is false, skipping");
697             next;
698             }
699            
700             #
701            
702             my $sz = ($sizes->findnodes("/rsp/sizes/size[\@label='$label']"))[0];
703            
704             if (! $sz) {
705             $self->log()->warning("Unable to locate size info for key $label\n");
706             next;
707             }
708            
709             my $source = $sz->getAttribute("source");
710              
711             my $ext = 'jpg';
712              
713             if (($label eq 'Site MP4') || ($label eq 'Video Original')){
714              
715             my $ua = LWP::UserAgent->new();
716             my $req = HTTP::Request->new('HEAD' => $source);
717             my $res = $ua->request($req);
718             my $headers = $res->headers();
719             my $disp = $headers->{"content-disposition"};
720              
721             $disp =~ /\.([^\.]+)$/;
722             $ext = $1;
723              
724             $self->log()->info("video! $source has $disp becomes $ext");
725             }
726              
727             my $img_root = File::Spec->catdir($photos_root, $yyyy, $mm, $dd);
728             my $img_fname = sprintf("%04d%02d%02d-%s-%s%s.%s", $yyyy, $mm, $dd, $id, $title, $FETCH_SIZES{$label}, $ext);
729            
730             $self->log()->info("scrub-store $img_fname");
731             push @{$self->{'_scrub'}->{$id}}, $img_fname;
732            
733             my $img_bak = File::Spec->catfile($img_root, $img_fname);
734             $self->{'__files'}->{$label} = $img_bak;
735            
736             #
737            
738             if ((-s $img_bak) && (! $force)){
739              
740             if (! $has_changed){
741             $self->log()->info("$img_bak has not changed, skipping\n");
742             next;
743             }
744            
745             my $mtime = (stat($img_bak))[9];
746            
747             if ((-f $img_bak) && ($last_update) && ($mtime >= $last_update)){
748             $self->log()->info("$img_bak has not changed ($mtime/$last_update), skipping\n");
749             $has_changed = 0;
750             next;
751             }
752             }
753            
754             #
755            
756             if (! -d $img_root) {
757            
758             $self->log()->info("create $img_root");
759            
760             if (! mkpath([$img_root], 0, 0755)) {
761             $self->log()->error("failed to create $img_root, $!");
762             next;
763             }
764             }
765            
766             if (! getstore($source, $img_bak)) {
767             $self->log()->error("failed to store '$source' as '$img_bak', $!\n");
768             next;
769             }
770            
771             $self->log()->info("stored $img_bak");
772            
773             #
774            
775             $files_modified ++;
776             }
777              
778             #
779             # Ensure that we don't accidentally purge any metafiles
780             #
781              
782             my $meta_bak = $self->path_rdf_dumpfile($info);
783             push @{$self->{'_scrub'}->{$id}}, basename($meta_bak);
784              
785             #
786             # Do we need to keep going...
787             #
788              
789             $has_changed = ($files_modified) ? 1 : 0;
790              
791             $self->log()->info("has changed (filemod) : $has_changed");
792              
793             if ((! $has_changed) && (! $force)) {
794              
795             my $lastmod = $self->{'__lastmod_since'};
796             $self->log()->info("last mod : $lastmod");
797              
798             if (($lastmod) && ($last_update >= $lastmod)) {
799             $has_changed = 1;
800             $self->log()->info("has changed (update) : $has_changed ($last_update - $lastmod)");
801             }
802              
803             #
804             # Ensure the RDF file is there and up to date
805             #
806              
807             if (! $self->{cfg}->param("rdf.rdfdump_inline")) {
808            
809             my $dump = $self->path_rdf_dumpfile($info);
810             $self->log()->info("test for rdf dump : $dump");
811              
812             if (($has_changed) && (-f $dump)) {
813              
814             my $dumpmod = (stat($dump))[9];
815             $self->log()->info("rdf dump : $dump");
816              
817             if ($dumpmod >= $lastmod) {
818             $has_changed = 0;
819             $self->log()->info("has changed (rdf) : $has_changed ($last_update - $dumpmod)");
820             }
821             }
822              
823             else {
824             if (! -f $dump) {
825             $self->log()->info("rdf dump does not exist : $dump");
826             $has_changed = 1;
827             }
828             }
829             }
830              
831             }
832              
833             $self->log()->info("has changed (final) : $has_changed");
834              
835             #
836             # Is that RDF in your pants?
837             #
838            
839             if ($self->{cfg}->param("rdf.do_dump")) {
840             $self->store_rdf($info, $has_changed, $force);
841             }
842              
843             #
844             # JPEG/IPTC
845             #
846              
847             if ($self->{cfg}->param("iptc.do_dump")) {
848             $self->store_iptc($info, $has_changed, $force);
849             }
850              
851             return 1;
852             }
853              
854             sub store_rdf {
855             my $self = shift;
856             my $photo = shift;
857             my $has_changed = shift;
858             my $force = shift;
859              
860             if (! $force){
861             $force = $self->{'cfg'}->param("rdf.force");
862             }
863              
864             my $rdf_root = $self->{cfg}->param("rdf.rdfdump_root");
865             my $rdf_inline = $self->{cfg}->param("rdf.rdfdump_inline");
866             my $rdf_str = "";
867              
868             if ((! $rdf_inline) && (! $rdf_root)) {
869             $rdf_root = $self->{cfg}->param("backup.photos_root");
870             }
871              
872             my $secret = $photo->find("/rsp/photo/\@originalsecret")->string_value();
873             my $id = $photo->find("/rsp/photo/\@id")->string_value();
874              
875             my $meta_bak = $self->path_rdf_dumpfile($photo);
876             my $meta_str = "";
877              
878             if ((! $force) && (! $has_changed) && (! $rdf_inline) && (-f $meta_bak)) {
879             return 1;
880             }
881            
882             #
883             #
884             #
885              
886             my $meta_root = dirname($meta_bak);
887              
888             if ((! -d $meta_root) && (! $rdf_inline)) {
889            
890             $self->log()->info("create $meta_root");
891            
892             if (! mkpath([$meta_root], 0, 0755)) {
893             $self->log()->error("failed to create $meta_root, $!");
894             next;
895             }
896             }
897            
898             #
899             #
900             #
901              
902             $self->log()->info("fetching RDF data for photo");
903            
904             my $fh = undef;
905              
906             if ($rdf_inline) {
907             $fh = IO::Scalar->new(\$rdf_str);
908             }
909              
910             else {
911             $fh = IO::AtomicFile->open($meta_bak, "w");
912             }
913              
914             if (! $fh) {
915             $self->log()->error("failed to open '$meta_bak', $!");
916             return 0;
917             }
918            
919             #
920             #
921             #
922              
923             my $desc_ok = $self->describe_photo({photo_id => $id,
924             secret => $secret,
925             fh => \*$fh});
926            
927             if (! $desc_ok) {
928             $self->log()->error("failed to describe photo $id:$secret\n");
929              
930             if (! $rdf_inline){
931             $fh->delete();
932             }
933              
934             return 0;
935             }
936            
937             #
938             # JPEG/RDF COM
939             #
940              
941             if ($rdf_inline) {
942             if (! $self->store_rdf_inline(\$rdf_str, $self->{'__files'}->{'Original'})) {
943             return 0;
944             }
945             }
946              
947             else {
948             if (! $fh->close()) {
949             $self->log()->error("failed to write '$meta_bak', $!");
950             return 0;
951             }
952             }
953              
954             return 1;
955             }
956              
957             sub store_iptc {
958             my $self = shift;
959             my $photo = shift;
960             my $has_changed = shift;
961             my $force = shift;
962              
963             if ((! $has_changed) && (! $force)) {
964             return 1;
965             }
966              
967             return $self->store_iptc_inline($photo, $self->{'__files'}->{'Original'});
968             }
969              
970             sub store_iptc_inline {
971             my $self = shift;
972             my $photo = shift;
973             my $original = shift;
974              
975             my $im = $self->_jpeg_handler($original);
976              
977             if (! $im) {
978             return 0;
979             }
980              
981             my %iptc = ('Headline' => $self->_iptcify($photo->find("/rsp/photo/title")->string_value()),
982             'Caption/Abstract' => $self->_iptcify($photo->find("/rsp/photo/description")->string_value()),
983             'Keywords' => []);
984              
985             my @tags = ();
986              
987             foreach my $tag ($photo->findnodes("/rsp/photo/tags/tag")) {
988             my $raw = $self->_iptcify($tag->getAttribute("raw"));
989              
990             if ($raw =~ /\s/) {
991             $raw = "\"$raw\"";
992             }
993              
994             push @{$iptc{'Keywords'}}, $raw;
995             }
996              
997             if (! $im->set_app13_data(\%iptc, 'UPDATE', 'IPTC')) {
998             $self->log()->error("Failed to updated IPTC");
999             return 0;
1000             }
1001              
1002             if (! $im->save($original)) {
1003             $self->log()->error("Failed store IPTC, $!");
1004             return 0;
1005             }
1006              
1007             return 1;
1008             }
1009              
1010             sub store_rdf_inline {
1011             my $self = shift;
1012             my $str_rdf = shift;
1013             my $path_jpg = shift;
1014              
1015             my $im = $self->_jpeg_handler($path_jpg, "COM");
1016              
1017             if (! $im) {
1018             return 0;
1019             }
1020              
1021             $im->add_comment($$str_rdf);
1022              
1023             if (! $im->save("$path_jpg")) {
1024             $self->log()->error("Failed store COM block, $!");
1025             return 0;
1026             }
1027              
1028             return 1;
1029             }
1030              
1031             =head2 $obj->scrub()
1032              
1033             Returns true or false.
1034              
1035             =cut
1036              
1037             sub scrub {
1038             my $self = shift;
1039            
1040             if (! keys %{$self->{'_scrub'}}) {
1041             return 1;
1042             }
1043            
1044             #
1045            
1046             my $rule = File::Find::Rule->new();
1047             $rule->file();
1048            
1049             $rule->exec(sub {
1050             my ($shortname, $path, $fullname) = @_;
1051            
1052             # $self->log()->info("test $shortname");
1053            
1054             $shortname =~ /^\d{8}-(\d+)-/;
1055             my $id = $1;
1056            
1057             if (! $id) {
1058             return 0;
1059             }
1060              
1061             if (! exists($self->{'_scrub'}->{$id})) {
1062             return 0;
1063             }
1064            
1065             if (grep /$shortname/, @{$self->{'_scrub'}->{$id}}) {
1066             return 0;
1067             }
1068            
1069             $self->log()->info("mark $fullname for scrubbing");
1070             return 1;
1071             });
1072            
1073             #
1074            
1075             foreach my $root ($rule->in($self->{'cfg'}->param("backup.photos_root"))) {
1076            
1077             $self->log()->info("unlink $root");
1078              
1079             if (! unlink($root)) {
1080             $self->log()->error("failed to unlink $root, $!");
1081             next;
1082             }
1083            
1084             # next unlink empty parent directories
1085            
1086             my $dd_dir = dirname($root);
1087             my $mm_dir = dirname($dd_dir);
1088             my $yyyy_dir = dirname($mm_dir);
1089            
1090             foreach my $path ($dd_dir, $mm_dir, $yyyy_dir) {
1091             if (&_has_children($path)) {
1092             last;
1093             }
1094            
1095             else {
1096              
1097             $self->log()->info("unlink $path");
1098              
1099             if (! rmtree([$path], 0, 1)) {
1100             $self->log()->error("failed to unlink, $path");
1101             last;
1102             }
1103             }
1104             }
1105             }
1106            
1107             #
1108            
1109             $self->{'_scrub'} = {};
1110             return 1;
1111             }
1112              
1113             =head2 $obj->cancel_backup()
1114              
1115             Cancel the backup process as soon as the current photo backup
1116             is complete.
1117              
1118             =cut
1119              
1120             sub cancel_backup {
1121             my $self = shift;
1122             $self->{'__cancel'} = 1;
1123             }
1124              
1125             =head2 $obj->register_callback($name, \&func)
1126              
1127             B
1128              
1129             Valid callback triggers are :
1130              
1131             =over 4
1132              
1133             =item * B
1134              
1135             The list of photos to be backed up is pulled from the Flickr servers
1136             is done in batches. This trigger is invoked for the first successful
1137             result set.
1138              
1139             The callback function will be passed a I representation
1140             of the result document returned by the Flickr API.
1141              
1142             =item * B
1143              
1144             This trigger is invoked after the last photo has been backed up.
1145              
1146             =item * B
1147              
1148             This trigger is invoked before the object's B method is
1149             called.
1150              
1151             The callback function will be passed a I representation
1152             of the current photo, as returned by the Flickr API.
1153              
1154             =item * B
1155              
1156             This trigger is invoked after the object's B method is
1157             called.
1158              
1159             The callback function will be passed a I representation
1160             of the current photo, as returned by the Flickr API, followed by a
1161             boolean indicating whether or not the backup was successful.
1162              
1163             =back
1164              
1165             Returns true or false, if I<$func> is not a valid code
1166             reference.
1167              
1168             =cut
1169              
1170             sub register_callback {
1171             my $self = shift;
1172             my $name = shift;
1173             my $func = shift;
1174              
1175             if (ref($func) ne "CODE") {
1176             return 0;
1177             }
1178              
1179             $self->{'__callbacks'}->{$name} = $func;
1180             return 1;
1181             }
1182              
1183              
1184             =head2 $obj->namespaces()
1185              
1186             Returns a hash ref of the prefixes and namespaces used by I
1187              
1188             The default key/value pairs are :
1189              
1190             =over 4
1191              
1192             =item B
1193              
1194             http://www.w3.org/2000/10/annotation-ns
1195              
1196             =item B
1197              
1198             http://www.w3.org/2001/02/acls#
1199              
1200             =item B
1201              
1202             http://purl.org/dc/elements/1.1/
1203              
1204             =item B
1205              
1206             http://purl.org/dc/terms/
1207              
1208             =item B
1209              
1210             http://nwalsh.com/rdf/exif#
1211              
1212             =item B
1213              
1214             http://nwalsh.com/rdf/exif-intrinsic#
1215              
1216             =item B
1217              
1218             x-urn:flickr:
1219              
1220             =item B
1221              
1222             http://xmlns.com/foaf/0.1/#
1223              
1224             =item B
1225              
1226             http://www.w3.org/2003/01/geo/wgs84_pos#
1227              
1228             =item B
1229              
1230             http://www.w3.org/2004/02/image-regions#
1231              
1232             =item B
1233              
1234             http://www.w3.org/1999/02/22-rdf-syntax-ns#
1235              
1236             =item B
1237              
1238             http://www.w3.org/2000/01/rdf-schema#
1239              
1240             =item B
1241              
1242             http://www.w3.org/2004/02/skos/core#
1243              
1244             =back
1245              
1246             I adds the following namespaces :
1247              
1248             =over 4
1249              
1250             =item B
1251              
1252             x-urn:B<$OSNAME>: (where $OSNAME is the value of the English.pm
1253             $OSNAME variable.
1254              
1255             =back
1256              
1257             =cut
1258              
1259             sub namespaces {
1260             my $self = shift;
1261             my %ns = %{$self->SUPER::namespaces()};
1262             $ns{computer} = sprintf("x-urn:%s:",$OSNAME);
1263             return (wantarray) ? %ns : \%ns;
1264             }
1265              
1266             =head2 $obj->namespace_prefix($uri)
1267              
1268             Return the namespace prefix for I<$uri>
1269              
1270             =cut
1271              
1272             # Defined in Net::Flickr::RDF
1273              
1274             =head2 $obj->uri_shortform($prefix,$name)
1275              
1276             Returns a string in the form of I:I. The property is
1277             the value of $name. The prefix passed may or may be the same as the prefix
1278             returned depending on whether or not the user has defined or redefined their
1279             own list of namespaces.
1280              
1281             The prefix passed to the method is assumed to be one of prefixes in the
1282             B list of namespaces.
1283              
1284             =cut
1285              
1286             # Defined in Net::Flickr::RDF
1287              
1288             =head2 $obj->make_photo_triples(\%data)
1289              
1290             Returns an array ref of array refs of the meta data associated with a
1291             photo (I<%data>).
1292              
1293             If any errors are unencounter an error is recorded via the B
1294             method and the method returns undef.
1295              
1296             =cut
1297              
1298             sub make_photo_triples {
1299             my $self = shift;
1300             my $data = shift;
1301            
1302             my $triples = $self->SUPER::make_photo_triples($data);
1303            
1304             if (! $triples) {
1305             return undef;
1306             }
1307            
1308             my $user_id = (getpwuid($EUID))[0];
1309             my $os_uri = sprintf("x-urn:%s:",$OSNAME);
1310             my $user_uri = $os_uri."user";
1311            
1312             my $creator_uri = sprintf("x-urn:%s#%s", $self->hostname_short(), $user_id);
1313            
1314             push @$triples, [$user_uri, $self->uri_shortform("rdfs", "subClassOf"), "http://xmlns.com/foaf/0.1/Person"];
1315            
1316             foreach my $label (keys %{$self->{'__files'}}) {
1317            
1318             my $uri = "file://".$self->{'__files'}->{$label};
1319             my $photo = sprintf("%s%s/%s", $FLICKR_URL_PHOTOS, $data->{user_id}, $data->{photo_id});
1320            
1321             push @$triples, [$uri, $self->uri_shortform("rdfs", "seeAlso"), $photo];
1322             push @$triples, [$uri, $self->uri_shortform("dc", "creator"), $creator_uri];
1323             push @$triples, [$uri, $self->uri_shortform("dcterms", "created"), &_w3cdtf()];
1324             }
1325            
1326             push @$triples, [$creator_uri, $self->uri_shortform("foaf", "name"), (getpwuid($EUID))[6]];
1327             push @$triples, [$creator_uri, $self->uri_shortform("foaf", "nick"), $user_id];
1328             push @$triples, [$creator_uri, $self->uri_shortform("rdf", "type"), "computer:user"];
1329            
1330             return $triples;
1331             }
1332              
1333             sub hostname_short {
1334             my $self = shift;
1335              
1336             if ($self->{'__hostname'}){
1337             return $self->{'__hostname'};
1338             }
1339              
1340             my @parts = split(/\./, hostname());
1341             my $short = $parts[0];
1342            
1343             $self->{'__hostname'} = $short;
1344             return $short;
1345             }
1346              
1347             =head2 $obj->namespace_prefix($uri)
1348              
1349             Return the namespace prefix for I<$uri>
1350              
1351             =cut
1352              
1353             =head2 $obj->uri_shortform($prefix,$name)
1354              
1355             Returns a string in the form of I:I. The property is
1356             the value of $name. The prefix passed may or may be the same as the prefix
1357             returned depending on whether or not the user has defined or redefined their
1358             own list of namespaces.
1359              
1360             The prefix passed to the method is assumed to be one of prefixes in the
1361             B list of namespaces.
1362              
1363             =cut
1364              
1365             # Defined in Net::Flickr::RDF
1366              
1367             =head2 $obj->api_call(\%args)
1368              
1369             Valid args are :
1370              
1371             =over 4
1372              
1373             =item * B
1374              
1375             A string containing the name of the Flickr API method you are
1376             calling.
1377              
1378             =item * B
1379              
1380             A hash ref containing the key value pairs you are passing to
1381             I
1382              
1383             =back
1384              
1385             If the method encounters any errors calling the API, receives an API error
1386             or can not parse the response it will log an error event, via the B method,
1387             and return undef.
1388              
1389             Otherwise it will return a I object (if XML::LibXML is
1390             installed) or a I object.
1391              
1392             =cut
1393              
1394             # Defined in Net::Flickr::API
1395              
1396             =head2 $obj->log()
1397              
1398             Returns a I object.
1399              
1400             =cut
1401              
1402             # Defined in Net::Flickr::API
1403              
1404             sub path_rdf_dumpfile {
1405             my $self = shift;
1406             my $photo = shift;
1407              
1408             my $rdf_root = $self->{cfg}->param("rdf.rdfdump_root");
1409             my $rdf_inline = $self->{cfg}->param("rdf.rdfdump_inline");
1410             my $rdf_str = "";
1411              
1412             if ((! $rdf_inline) && (! $rdf_root)) {
1413             $rdf_root = $self->{cfg}->param("backup.photos_root");
1414             }
1415              
1416             my $id = $photo->find("/rsp/photo/\@id")->string_value();
1417             my $secret = $photo->find("/rsp/photo/\@secret")->string_value();
1418             my $title = $photo->find("/rsp/photo/title")->string_value() || "untitled";
1419             $title = &_clean($title);
1420              
1421             my $dt = $photo->find("/rsp/photo/dates/\@taken")->string_value();
1422            
1423             $dt =~ /^(\d{4})-(\d{2})-(\d{2})/;
1424             my ($yyyy,$mm,$dd) = ($1,$2,$3);
1425              
1426             my $meta_root = File::Spec->catdir($rdf_root, $yyyy, $mm, $dd);
1427             my $meta_fname = sprintf("%04d%02d%02d-%s-%s.xml", $yyyy, $mm, $dd, $id, $title);
1428             my $meta_path = File::Spec->catfile($meta_root, $meta_fname);
1429              
1430             return $meta_path;
1431             }
1432              
1433             sub _clean {
1434             my $str = shift;
1435            
1436             $str = lc($str);
1437            
1438             $str =~ s/\.jpg$//;
1439            
1440             # unidecode to convert everything to
1441             # happy happy ASCII
1442            
1443             # see also : http://perladvent.org/2004/12th/
1444            
1445             $str = unidecode(&_unescape(&_decode($str)));
1446            
1447             $str =~ s/@/at/g;
1448             $str =~ s/&/and/g;
1449             $str =~ s/\*/star/g;
1450            
1451             $str =~ s/[^a-z0-9-_]/ /ig;
1452             $str =~ s/'//g;
1453             $str =~ s/\^//g;
1454            
1455             # make all whitespace single spaces
1456             $str =~ s/\s+/ /g;
1457            
1458             # remove starting or trailing whitespace
1459             $str =~ s/^\s+//;
1460             $str =~ s/\s+$//;
1461              
1462             # remove trailing periods
1463             $str =~ s/\.+$//;
1464              
1465             # make all spaces underscores
1466             $str =~ s/ /_/g;
1467            
1468             return $str;
1469             }
1470              
1471             sub _decode {
1472             my $str = shift;
1473            
1474             if (! utf8::is_utf8($str)) {
1475             $str = decode_utf8($str);
1476             }
1477            
1478             $str =~ s/(?:%([a-fA-F0-9]{2})%([a-fA-F0-9]{2}))/pack("U0U*", hex($1), hex($2))/eg;
1479             return $str;
1480             }
1481              
1482             # Borrowed from URI::Escape
1483              
1484             sub _unescape {
1485             my $str = shift;
1486            
1487             if (defined($str)) {
1488             $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
1489             }
1490            
1491             return $str;
1492             }
1493              
1494             sub _has_children {
1495             my $path = shift;
1496             my $dh = DirHandle->new($path);
1497             my $has = grep { $_ !~ /^\.+$/ } $dh->read();
1498             return $has;
1499             }
1500              
1501             # Borrowed from LWP::Authen::Wsse
1502              
1503             sub _w3cdtf {
1504             my ($sec, $min, $hour, $mday, $mon, $year) = gmtime();
1505             $mon++; $year += 1900;
1506            
1507             return sprintf("%04s-%02s-%02sT%02s:%02s:%02sZ",
1508             $year, $mon, $mday, $hour, $min, $sec);
1509             }
1510              
1511             sub _has_callback {
1512             my $self = shift;
1513             my $name = shift;
1514              
1515             my $cb = $self->{'__callbacks'};
1516              
1517             if (! exists($cb->{$name})) {
1518             return 0;
1519             }
1520              
1521             elsif (ref($cb->{$name} ne "CODE")) {
1522             return 0;
1523             }
1524              
1525             else {
1526             return 1;
1527             }
1528             }
1529              
1530             sub _execute_callback {
1531             my $self = shift;
1532             my $name = shift;
1533             $self->{'__callbacks'}->{$name}->(@_);
1534             }
1535              
1536             sub _mk_mindate {
1537             my $str = shift;
1538            
1539             $str =~ /^(\d+)([hdwM])$/;
1540            
1541             my $count = $1;
1542             my $period = $2;
1543            
1544             # print "count $count : period $period\n";
1545            
1546             if ((! $count) || (! $period)) {
1547             return 0;
1548             }
1549            
1550             #
1551            
1552             if ($period eq "h") {
1553             return time() - ($count * (60 * 60));
1554             }
1555            
1556             elsif ($period eq "d") {
1557             return time() - ($count * (24 * (60 * 60)));
1558             }
1559            
1560             elsif ($period eq "w") {
1561             return time() - ($count * (7 * (24 * (60 * 60))));
1562             }
1563            
1564             elsif ($period eq "M") {
1565             return time() - ($count * (4 * (7 * (24 * (60 * 60)))));
1566             }
1567            
1568             else {
1569             return 0;
1570             }
1571             }
1572              
1573             sub _jpeg_handler {
1574             my $self = shift;
1575             my $img = shift;
1576              
1577             eval "require Image::MetaData::JPEG";
1578            
1579             if ($@) {
1580             $self->log()->error("Failed to load Image::MetaData::JPEG, $@");
1581             return undef;
1582             }
1583              
1584             my $im = Image::MetaData::JPEG->new($img, @_);
1585              
1586             if (! $im) {
1587             $self->log()->error("Failed to read $img, " . Image::MetaData::JPEG::Error());
1588             return undef;
1589             }
1590              
1591             return $im;
1592             }
1593              
1594             sub _iptcify {
1595             my $self = shift;
1596             return encode("iso-8859-1", &_decode($_[0]));
1597             }
1598              
1599             =head1 EXAMPLES
1600              
1601             =cut
1602              
1603             =head2 CONFIG FILES
1604              
1605             This is an example of a Config::Simple file used to back up photos tagged
1606             with 'cameraphone' from Flickr
1607              
1608             [flickr]
1609             api_key=asd6234kjhdmbzcxi6e323
1610             api_secret=s00p3rs3k3t
1611             auth_token=123-omgwtf4u
1612             api_handler=LibXML
1613              
1614             [search]
1615             tags=cameraphone
1616             per_page=500
1617              
1618             [backup]
1619             photos_root=/home/asc/photos
1620             scrub_backups=1
1621             fetch_medium=1
1622             fetch_square=1
1623             force=0
1624              
1625             [rdf]
1626             do_dump=1
1627             rdfdump_root=/home/asc/photos
1628              
1629             =head2 RDF
1630              
1631             This is an example of an RDF dump for a photograph backed up from
1632             Flickr (using Net::Flickr::RDF) :
1633              
1634              
1635            
1636            
1637             xmlns:geoname="http://www.geonames.org/onto#"
1638             xmlns:a="http://www.w3.org/2000/10/annotation-ns"
1639             xmlns:ph="http://www.machinetags.org/wiki/ph#camera"
1640             xmlns:filtr="http://www.machinetags.org/wiki/filtr#process"
1641             xmlns:nfr_geo="http://www.machinetags.org/wiki/geo#debug"
1642             xmlns:place="x-urn:flickr:place:"
1643             xmlns:exif="http://nwalsh.com/rdf/exif#"
1644             xmlns:mt="x-urn:flickr:machinetag:"
1645             xmlns:exifi="http://nwalsh.com/rdf/exif-intrinsic#"
1646             xmlns:geonames="http://www.machinetags.org/wiki/geonames#feature"
1647             xmlns:dcterms="http://purl.org/dc/terms/"
1648             xmlns:dc="http://purl.org/dc/elements/1.1/"
1649             xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
1650             xmlns:acl="http://www.w3.org/2001/02/acls#"
1651             xmlns:skos="http://www.w3.org/2004/02/skos/core#"
1652             xmlns:foaf="http://xmlns.com/foaf/0.1/"
1653             xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
1654             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1655             xmlns:flickr="x-urn:flickr:"
1656             >
1657              
1658            
1659             2fc2c76d7634d1a6446b1898bf5471205ed3d0cb
1660            
1661             thincvox
1662            
1663              
1664            
1665             filtr
1666            
1667            
1668            
1669            
1670              
1671            
1672             PPLX
1673             US
1674             CA
1675             58
1676             State of California
1677             San Francisco County
1678            
1679            
1680              
1681            
1682             feature
1683             geonames
1684            
1685            
1686              
1687            
1688             Original
1689             1944
1690             2592
1691            
1692            
1693            
1694              
1695            
1696             cameraphone
1697            
1698              
1699            
1700             filtr
1701            
1702            
1703            
1704              
1705            
1706             Flash did not fire, auto mode
1707             100/100
1708             100
1709             2592
1710             297/100
1711             1944
1712             5.6 mm
1713             2007-05-30T15:10:01PDT
1714             sRGB
1715             f/2.8
1716             2007-05-30T15:10:01PDT
1717             4351/1000
1718             0.049 sec (49/1000)
1719            
1720              
1721            
1722             san francisco
1723             sanfrancisco
1724            
1725            
1726            
1727              
1728            
1729             sanfrancisco
1730            
1731              
1732            
1733             Medium
1734             375
1735             500
1736            
1737            
1738            
1739            
1740              
1741            
1742             5405296
1743            
1744            
1745            
1746              
1747            
1748             cameraphone
1749            
1750            
1751            
1752              
1753            
1754            
1755             LOG (2007)
1756            
1757            
1758              
1759            
1760             6065-522214395-72157600293655654
1761             2007-05-31T14:54:25
1762             Kittens!
1763            
1764            
1765            
1766              
1767            
1768             587a68f90c4030a9b0c7d8ca6ff8549a8b40e5cd
1769             Aaron Straup Cope
1770             straup
1771            
1772              
1773            
1774             n95
1775            
1776            
1777            
1778            
1779              
1780            
1781            
1782            
1783              
1784            
1785             6065-522214395-72157600295486776
1786             2007-06-01T00:19:05
1787             here kitty, kitty, <a href="http://thincvox.com/audio_recordings/meow.mp3">meow</a>
1788            
1789            
1790            
1791              
1792            
1793             5405296
1794            
1795            
1796            
1797            
1798              
1799            
1800             process
1801             filtr
1802            
1803            
1804              
1805            
1806             debug
1807             geo
1808            
1809            
1810              
1811            
1812             filtr
1813             namespace test
1814             visbility
1815             Untitled #1180563722
1816             n95
1817             All rights reserved.
1818             public
1819            
1820             2007-05-30T15:10:01-0700
1821             2007-05-30T15:18:39-0700
1822             5405296
1823            
1824            
1825            
1826            
1827            
1828            
1829            
1830            
1831            
1832            
1833            
1834            
1835            
1836              
1837            
1838             Thumbnail
1839             75
1840             100
1841            
1842            
1843            
1844            
1845              
1846            
1847            
1848            
1849              
1850            
1851             -122.401937
1852             visbility
1853             37.794694
1854             16
1855             public
1856            
1857            
1858            
1859              
1860            
1861             filtr
1862            
1863            
1864            
1865              
1866            
1867             n95
1868            
1869            
1870            
1871              
1872            
1873             2.0:1180823550
1874             2007-06-02T15:32:30-0700
1875            
1876            
1877            
1878              
1879            
1880             filtr
1881            
1882              
1883            
1884             namespace test
1885            
1886            
1887            
1888            
1889            
1890              
1891            
1892             Small
1893             180
1894             240
1895            
1896            
1897            
1898            
1899              
1900            
1901            
1902            
1903              
1904            
1905             camera
1906             ph
1907            
1908            
1909              
1910            
1911             Square
1912             75
1913             75
1914            
1915            
1916            
1917            
1918              
1919            
1920             Large
1921             768
1922             1024
1923            
1924            
1925            
1926            
1927              
1928            
1929             San Francisco
1930             United States
1931             California
1932             San Francisco
1933            
1934            
1935              
1936            
1937             namespace test
1938            
1939            
1940            
1941              
1942            
1943              
1944             =head1 VERSION
1945              
1946             3.1
1947              
1948             =head1 DATE
1949              
1950             $Date: 2010/12/19 19:06:56 $
1951              
1952             =head1 AUTHOR
1953              
1954             Aaron Straup Cope Eascope@cpan.orgE
1955              
1956             =head1 CONTRIBUTORS
1957              
1958             Thomas Sibley Etsibley@cpan.orgE
1959              
1960             =head1 SEE ALSO
1961              
1962             L
1963              
1964             L
1965              
1966             L
1967              
1968             http://www.flickr.com/services/api/misc.userauth.html
1969              
1970             =head1 BUGS
1971              
1972             Please report all bugs via http://rt.cpan.org
1973              
1974             =head1 LICENSE
1975              
1976             Copyright (c) 2005-2008 Aaron Straup Cope. All Rights Reserved.
1977              
1978             This is free software. You may redistribute it and/or
1979             modify it under the same terms as Perl itself.
1980              
1981             =cut
1982              
1983             return 1;