File Coverage

blib/lib/Flickr/Upload/Dopplr.pm
Criterion Covered Total %
statement 34 62 54.8
branch 0 2 0.0
condition 0 3 0.0
subroutine 12 17 70.5
pod n/a
total 46 84 54.7


line stmt bran cond sub pod time code
1             # $Id: Dopplr.pm,v 1.15 2008/03/13 16:35:15 asc Exp $
2              
3 1     1   3303 use strict;
  1         3  
  1         69  
4              
5             package FUDException;
6 1     1   6 use base qw (Error);
  1         3  
  1         4281  
7              
8             # http://www.perl.com/lpt/a/690
9              
10 1     1   10684 use overload ('""' => 'stringify');
  1         2  
  1         10  
11              
12             sub new {
13 0     0     my $self = shift;
14 0           my $text = shift;
15 0           my $prev = shift;
16              
17 0 0         if (UNIVERSAL::can($prev, "stacktrace")){
18 0           $text .= "\n";
19 0           $text .= $prev->stacktrace();
20             }
21              
22 0           local $Error::Depth = $Error::Depth + 1;
23 0           local $Error::Debug = 1; # Enables storing of stacktrace
24              
25 0           $self->SUPER::new(-text => $text);
26             }
27              
28             sub stringify {
29 0     0     my $self = shift;
30 0   0       my $pkg = ref($self) || $self;
31 0           return sprintf("[%s] %s", $pkg, $self->stacktrace());
32             }
33              
34             package FlickrUploadException;
35 1     1   1437 use base qw (FUDException);
  1         3  
  1         1869  
36              
37             package FlickrAPIException;
38 1     1   12 use base qw (FUDException);
  1         2  
  1         736  
39              
40             sub new {
41 0     0     my $pkg = shift;
42 0           my $text = shift;
43 0           my $prev = shift;
44 0           my $code = shift;
45 0           my $msg = shift;
46              
47 0           $text .= " Error code $code";
48              
49 0           my $self = $pkg->SUPER::new($text, $prev);
50              
51 0           $self->{'api_error_code'} = $code;
52 0           $self->{'api_error_message'} = $msg;
53 0           return $self;
54             }
55              
56             sub error_code {
57 0     0     my $self = shift;
58 0           return $self->{'api_error_code'};
59             }
60              
61             sub error_message {
62 0     0     my $self = shift;
63 0           return $self->{'api_error_message'};
64             }
65              
66             package NetDopplrException;
67 1     1   7 use base qw (FUDException);
  1         2  
  1         13327  
68              
69             package Flickr::Upload::Dopplr;
70 1     1   22 use base qw (Flickr::Upload);
  1         3  
  1         10265  
71              
72             $Flickr::Upload::Dopplr::VERSION = '0.3';
73              
74             =head1 NAME
75              
76             Flickr::Upload::Dopplr - Flickr::Upload subclass to assign location information using Dopplr
77              
78             =head1 SYNOPSIS
79              
80             use Flickr::Upload::Dopplr;
81              
82             my %dp_args = ('auth_token' => 'JONES!!!!',
83             'tagify' => 'delicious');
84              
85             my %fl_args = ('key' => 'OH HAI',
86             'secret' => 'OH NOES',,
87             'dopplr' => \%dp_args);
88              
89             my $uploadr = Flickr::Upload::Dopplr->new(\%fl_args);
90              
91             my $photo_id = $uploadr->upload('photo' => "/path/to/photo",
92             'auth_token' => 'O RLY');
93              
94             =head1 DESCRIPTION
95              
96             Flickr::Upload subclass to assign location information using Dopplr.
97              
98             Specifically, the package will query Dopplr for the current location of the
99             user associated with I<$dopplr_authtoken> and assign the following tags for
100             the name of the city a machinetag representing the Geonames.org ID for that
101             city.
102              
103             If the package is able to query a photo's EXIF data and read the I
104             field that value will be used to query Dopplr for your location on that day.
105              
106             It will also try to resolve a corresponding Flickr Places ID for the Geonames
107             city ID returned by Dopplr. For example, Geonames ID I<5391959> becomes
108             I which becomes Flickr Places ID I.
109              
110             (Or in machinetag-speak, I)
111              
112             If, when the photo is uploaded, the Dopplr API thinks that it is a "travel
113             day" another machine tag (dopplr:trip=) will be added containing the numeric
114             identifier for that trip.
115              
116             If an upload is successful, the package will attempt to assign latitude and
117             longitude information for the photo with a Flickr accuracy of 11 (or "city").
118              
119             =head1 ERROR HANDLING
120              
121             Flickr::Upload::Dopplr subclasses Error.pm to catch and throw exceptions. Although
122             this is still a mostly un-Perl-ish way of doing things, it seemed like the most sensible
123             way to handle the variety of error cases. I don't love it but we'll see.
124              
125             This means that the library B and you will need to
126             code around it using either I or - even better - I and I blocks.
127              
128             There are four package specific exception handlers :
129              
130             =over 4
131              
132             =item * B
133              
134             An error condition specific to I was triggered.
135              
136             =item * B
137              
138             An error condition specific to I was triggered.
139              
140             =item * B
141              
142             An error condition specific to calling the Flickr API (read : I)
143             was triggered.
144              
145             This is the only exception handler that defines its own additional methods. They
146             are :
147              
148             =over 4
149              
150             =item * B
151              
152             The numeric error code returned by the Flickr API.
153              
154             =item * B
155              
156             The textual error message returned by the Flickr API.
157              
158             =back
159              
160             =item * B
161              
162             An error condition specific to I was triggered.
163              
164             =back
165              
166             =head1 CAVEATS
167              
168             =over 4
169              
170             =item *
171              
172             Asynchronous uploads are not support and will trigger an exception.
173              
174             =item *
175              
176             At the moment, the package does not check to see whether geo information was
177             already assigned (for example, via GPS EXIF data) nor does it issue and error
178             reporting if there was a problem assigning geo information.
179              
180             =back
181              
182             =cut
183              
184 1     1   110058 use Net::Dopplr;
  1         151611  
  1         37  
185 1     1   2658 use Image::Info qw (image_info);
  1         2454  
  1         83  
186 1     1   10 use Error qw(:try);
  1         1  
  1         12  
187 1     1   2315 use LWP::Simple;
  1         10873  
  1         9  
188 1     1   1697 use XML::XPath;
  0            
  0            
189              
190             $Error::Debug = 1;
191              
192             =head1 PACKAGE METHODS
193              
194             =head2 __PACKAGE__->new(\%args)
195              
196             All the same arguments required by the I constructor plus the
197             following :
198              
199             =over 4
200              
201             =item * B
202              
203             A hash reference containing the following keys :
204              
205             =over 4
206              
207             =item * B
208              
209             String. I
210              
211             A valid Dopplr API authentication token.
212              
213             =item * B
214              
215             String.
216              
217             An optional flag to format tags for cities, specific to a service. Valid
218             services are :
219              
220             =over 4
221              
222             =item * B
223              
224             City names are lower-cased and spaces are removed.
225              
226             =item * B
227              
228             City names are wrapped in double-quotes if they contain spaces.
229              
230             =back
231              
232             The default value is I
233              
234             =back
235              
236             =back
237              
238             Returns a I object.
239              
240             =cut
241              
242             sub new {
243             my $pkg = shift;
244             my $args = shift;
245              
246             my $dargs = $args->{'dopplr'};
247             delete($args->{'dopplr'});
248              
249             my $self = undef;
250              
251             try {
252             $self = $pkg->SUPER::new($args);
253             }
254            
255             catch Error with {
256             my $e = shift;
257             throw FlickrUploadException("Failed to instantiate Flickr::Upload", $e);
258             };
259              
260             my $token = $dargs->{'auth_token'};
261              
262             try {
263             $self->{'__dopplr'} = Net::Dopplr->new($token);
264             }
265            
266             catch Error with {
267             my $e = shift;
268             throw NetDopplrException("Failed to instantiate Net::Dopplr", $e);
269             };
270              
271             $self->{'__dargs'} = $dargs;
272             $self->{'__places'} = {};
273              
274             return $self;
275             }
276              
277             =head1 OBJECT METHODS YOU SHOULD CARE ABOUT
278              
279             =head2 $obj->upload(%args)
280              
281             Nothing you wouldn't pass the Flickr::Upload I method. Except the
282             I flag which is not honoured yet. I'm working on it.
283              
284             In additional, you may pass an optional I parameter. It must be a hash
285             reference with the following keys :
286              
287             =over 4
288              
289             =item * B
290              
291             Itself a hash reference containing is_public, is_contact, is_family and is_friend
292             keys and their boolean values to set the geo permissions on your uploaded photo.
293              
294             If this is not defined then your default viewing settings for geo data will be left
295             in place.
296              
297             =back
298              
299             Returns a photo ID!
300              
301             =cut
302              
303             sub upload {
304             my $self = shift;
305             my %args = @_;
306              
307             if ($args{'async'}){
308             throw FUDException("Asynchronous uploads are not supported yet");
309             }
310            
311             #
312              
313             my $city = undef;
314             my $id = 0;
315             my $geo = undef;
316              
317             if (ref($args{'geo'}) eq "HASH"){
318             $geo = $args{'geo'};
319             delete($args{'geo'});
320             }
321              
322             #
323              
324             $city = $self->where_am_i($args{'photo'});
325            
326             if (! $city){
327             throw FUDException("No city data returned from Dopplr");
328             }
329              
330             $args{'tags'} .= sprintf(" \"%s\"", $self->tagify($city->{'name'}));
331             $args{'tags'} .= sprintf(" geonames:locality=%d", $city->{'geoname_id'});
332            
333             if (my $place = $self->geonames_id_to_places_id($city->{'geoname_id'})){
334             $args{'tags'} .= sprintf(" places:%s=%s", $place->{'type'}, $place->{'id'});
335             }
336              
337             if ($city->{'tripid'}){
338             $args{'tags'} .= sprintf(" dopplr:trip=%d", $city->{'tripid'});
339             }
340              
341             try {
342             $id = $self->SUPER::upload(%args);
343             }
344              
345             catch Error with {
346             throw FlickrUploadException("Failed to upload photo to Flickr", shift);
347             };
348              
349             if (! $id){
350             throw FlickrUploadException("Flickr::Upload did not return a photo ID");
351             }
352              
353             #
354             # Check to see if the photo has GPS info
355             # (this will be picked up by the Flickr upload wah-wah)
356             #
357              
358             my $has_gps = 0;
359              
360             eval {
361             my $info = image_info($args{'photo'});
362              
363             if (($info) && (ref($info->{'GPSLatitude'}))){
364             $has_gps = 1;
365             }
366             };
367              
368             #
369             # Set GPS by city
370             #
371              
372             if (! $has_gps){
373             my %set = ('accuracy' => 11,
374             'lat' => $city->{'latitude'},
375             'lon' => $city->{'longitude'},
376             'auth_token' => $args{'auth_token'},
377             'photo_id' => $id);
378            
379             $self->flickr_api_call('flickr.photos.geo.setLocation', \%set);
380             }
381              
382             #
383              
384             if (exists($geo->{'perms'})){
385              
386             my %perms = %{$geo->{'perms'}};
387             $perms{'auth_token'} = $args{'auth_token'};
388             $perms{'photo_id'} = $id;
389              
390             if ($perms{'is_public'}){
391             foreach my $other ('is_contact', 'is_family', 'is_friend'){
392             if (! exists($perms{$other})){
393             $perms{$other} = 1;
394             }
395             }
396             }
397              
398             $self->flickr_api_call('flickr.photos.geo.setPerms', \%perms);
399             }
400              
401             #
402              
403             return $id;
404             }
405              
406             sub where_am_i {
407             my $self = shift;
408             my $photo = shift;
409              
410             if (my $when = $self->when_was_that($photo)){
411             return $self->where_was_i_then($when);
412             }
413            
414             return $self->where_am_i_now();
415             }
416              
417             sub where_was_i_then {
418             my $self = shift;
419             my $ymd = shift;
420              
421             my $info = undef;
422              
423             try {
424             $info = $self->{'__dopplr'}->location_on_date('', 'date' => $ymd);
425             }
426              
427             catch Error with {
428             throw NetDopplrException("Failed to call location_on_date", shift);
429             };
430              
431             if (! $info){
432             return undef;
433             }
434              
435             my $city = $info->{'location'}->{'home'};
436              
437             if ($info->{'location'}->{'trip'}){
438             $city = $info->{'location'}->{'trip'}->{'city'};
439             $city->{'tripid'} = $info->{'location'}->{'trip'}->{'id'};
440             }
441              
442             return $city;
443             }
444              
445             sub where_am_i_now {
446             my $self = shift;
447             my $info = undef;
448              
449             try {
450             $info = $self->{'__dopplr'}->traveller_info();
451             }
452              
453             catch Error with {
454             throw NetDopplrException("Failed to call traveller_info", shift);
455             };
456              
457             if (! $info){
458             return undef;
459             }
460            
461             my $city = $info->{'traveller'}->{'current_city'};
462              
463             if ($info->{'traveller'}->{'travel_today'}){
464             $city->{'tripid'} = $info->{'traveller'}->{'current_trip'}->{'id'};
465             }
466              
467             return $city;
468             }
469              
470             sub when_was_that {
471             my $self = shift;
472             my $photo = shift;
473              
474             my $info = undef;
475              
476             eval {
477             $info = image_info($photo);
478             };
479              
480             if (($info) && ($info->{'DateTimeOriginal'})){
481             if ($info->{'DateTimeOriginal'} =~ /^(\d{4})[\:-](\d{2})[\:-](\d{2})/){
482             return join("-", $1, $2, $3);
483             }
484             }
485              
486             return undef;
487             }
488              
489             #
490             # Please for someone to write Text::Tagify...
491             #
492              
493             sub tagify {
494             my $self = shift;
495             my $tag = shift;
496              
497             if ($self->{'__dargs'}->{'tagify'} eq "delicious"){
498             return $self->tagify_like_delicious($tag);
499             }
500              
501             return $self->tagify_like_flickr($tag);
502             }
503              
504             sub tagify_like_flickr {
505             my $self = shift;
506             my $tag = shift;
507              
508             if ($tag =~ /\s/){
509             $tag = "\"$tag\"";
510             }
511              
512             return $tag;
513             }
514              
515             sub tagify_like_delicious {
516             my $self = shift;
517             my $tag = shift;
518              
519             $tag =~ s/\s//g;
520             return lc($tag);
521             }
522              
523             sub geonames_id_to_places_id {
524             my $self = shift;
525             my $geonames_id = shift;
526              
527             if (exists($self->{'__places'}->{$geonames_id})){
528             return $self->{'__places'}->{$geonames_id};
529             }
530              
531             # sort out error handling...not that important, really...
532              
533             my $url = "http://ws.geonames.org/hierarchy?geonameId=" . $geonames_id;
534              
535             my $gn_xml = get($url);
536             my $gn_xp = undef;
537              
538             eval {
539             $gn_xp = XML::XPath->new('xml' => $gn_xml);
540             };
541              
542             if ($@){
543             warn $@;
544             return undef;
545             }
546            
547             my $locality = ($gn_xp->findnodes("*//geoname[fcode='PPL']"))[0];
548             my $region = ($gn_xp->findnodes("*//geoname[fcode='ADM1']"))[0];
549             my $country = ($gn_xp->findnodes("*//geoname[fcode='PCLI']"))[0];
550              
551             my @parts = ();
552              
553             foreach my $pl ($locality, $region, $country){
554              
555             if ($pl){
556             push @parts, $pl->findvalue("name");
557             }
558             }
559              
560             my $query = join(", ", @parts);
561              
562             if (! $query){
563             return undef;
564             }
565              
566             my $place_id = undef;
567             my $place_type = undef;
568              
569             eval {
570             my $fl = Flickr::API->new({'key' => $self->{'api_key'}});
571             my $res = $fl->execute_method('flickr.places.find', {'query' => $query});
572            
573             my $fl_xml = $res->decoded_content();
574             my $fl_xp = XML::XPath->new('xml' => $fl_xml);
575              
576             # Wait to see if any more actual magic is required...
577              
578             my @places = $fl_xp->findnodes("/rsp/places/place");
579             my $place = $places[0];
580              
581             $place_id = $place->getAttribute("place_id");
582             $place_type = $place->getAttribute("place_type");
583             };
584              
585             if ($@){
586             warn $@;
587             return undef;
588             }
589              
590             $self->{'__places'}->{$geonames_id} = {'id' => $place_id, 'type' => $place_type};
591             return $self->{'__places'}->{$geonames_id};
592             }
593              
594             sub flickr_api_call {
595             my $self = shift;
596             my $meth = shift;
597             my $args = shift;
598              
599             my $res;
600              
601             try {
602             $res = $self->execute_method($meth, $args);
603             }
604            
605             catch Error with {
606             my $e = shift;
607             throw FlickrAPIException("API call $meth failed", 999, "Unknown API error");
608             };
609              
610             if (! $res->{success}){
611             my $e = shift;
612             throw FlickrAPIException("API call $meth failed", $e, $res->{error_code}, $res->{error_message});
613             }
614              
615             return $res;
616             }
617              
618              
619             =head1 VERSION
620              
621             0.3
622              
623             =head1 DATE
624              
625             $Date: 2008/03/13 16:35:15 $
626              
627             =head1 AUTHOR
628              
629             Aaron Straup Cope
630              
631             =head1 SEE ALSO
632              
633             L
634              
635             L
636              
637             L
638              
639             L
640              
641             L
642              
643             L
644              
645             =head1 BUGS
646              
647             Please report all bugs via http://rt.cpan.org/
648              
649             =head1 LICENSE
650              
651             Copyright (c) 2007-2008 Aaron Straup Cope. All Rights Reserved.
652              
653             This is free software. You may redistribute it and/or
654             modify it under the same terms as Perl itself.
655              
656             =cut
657              
658             return 1;