File Coverage

blib/lib/Flickr/Upload.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Flickr::Upload;
2              
3 6     6   140070 use strict;
  6         15  
  6         161  
4 6     6   31 use warnings;
  6         12  
  6         167  
5              
6 6     6   5788 use LWP::UserAgent;
  6         330846  
  6         209  
7 6     6   4730 use HTTP::Request::Common;
  6         14237  
  6         456  
8 6     6   5248 use Flickr::API;
  0            
  0            
9             use XML::Simple qw(:strict);
10             use Digest::MD5 qw(md5_hex);
11             use Encode qw(encode_utf8);
12             use Carp;
13              
14             our $VERSION = '1.5';
15              
16             our @ISA = qw(Flickr::API);
17              
18             =head1 NAME
19              
20             Flickr::Upload - Upload images to C
21              
22             =head1 SYNOPSIS
23              
24             use Flickr::Upload;
25              
26             my $ua = Flickr::Upload->new(
27             {
28             'key' => '90909354',
29             'secret' => '37465825'
30             });
31             $ua->upload(
32             'photo' => '/tmp/image.jpg',
33             'auth_token' => $auth_token,
34             'tags' => 'me myself eye',
35             'is_public' => 1,
36             'is_friend' => 1,
37             'is_family' => 1
38             ) or die "Failed to upload /tmp/image.jpg";
39              
40             =head1 DESCRIPTION
41              
42             Upload an image to L.
43              
44             =head1 METHODS
45              
46             =head2 new
47              
48             my $ua = Flickr::Upload->new(
49             {
50             'key' => '90909354',
51             'secret' => '37465825'
52             });
53              
54             Instantiates a L instance. The C argument is your
55             API key and the C is the API secret associated with it. To get an
56             API key and secret, go to L.
57              
58             The resulting L instance is a subclass of L
59             and can be used for any other Flickr API calls. As such,
60             L is also a subclass of L.
61              
62             =head2 upload
63              
64             my $photoid = $ua->upload(
65             'photo' => '/tmp/image.jpg',
66             'auth_token' => $auth_token,
67             'tags' => 'me myself eye',
68             'is_public' => 1,
69             'is_friend' => 1,
70             'is_family' => 1
71             'async' => 0,
72             );
73              
74             Taking a L instance C<$ua> as an argument, this is
75             basically a direct interface to the Flickr Photo Upload API. Required
76             parameters are C and C. Note that the C
77             must have been issued against the API key and secret used to instantiate
78             the uploader.
79              
80             Returns the resulting identifier of the uploaded photo on success,
81             C on failure. According to the API documentation, after an upload the
82             user should be directed to the page
83             L.
84              
85             If the C option is non-zero, the photo will be uploaded
86             asynchronously and a successful upload returns a ticket identifier. See
87             L. The caller can then
88             periodically poll for a photo id using the C method. Note
89             that photo and ticket identifiers aren't necessarily numeric.
90              
91             =cut
92              
93             sub upload {
94             my $self = shift;
95             die '$self is not a Flickr::Upload' unless $self->isa('Flickr::Upload');
96             my %args = @_;
97              
98             # these are the only things _required_ by the uploader.
99             die "Can't read photo '$args{'photo'}'" unless $args{'photo'} and -f $args{'photo'};
100             die "Missing 'auth_token'" unless defined $args{'auth_token'};
101              
102             # create a request object and execute it
103             my $req = $self->make_upload_request( %args );
104             return undef unless defined $req;
105              
106             return $self->upload_request( $req );
107             }
108              
109             =head2 check_upload
110              
111             my %status2txt = (0 => 'not complete', 1 => 'completed', 2 => 'failed');
112             my @rc = $ua->check_upload( @ticketids );
113             for( @rc ) {
114             print "Ticket $_->{id} has $status2txt{$_->{complete}}\n";
115             print "\tPhoto id is $_->{photoid}\n" if exists $_->{photoid};
116             }
117              
118             This function will check the status of one or more asynchronous uploads. A
119             list of ticket identifiers are provided (C<@ticketids>) and each is
120             checked. This is basically just a wrapper around the Flickr API
121             C method.
122              
123             On success, a list of hash references is returned. Each
124             hash contains a C (the ticket id), C and, if
125             completed, C members. C may also be returned.
126             Status codes (for C) are as documented at
127             L and, actually, the
128             returned fields are identical to those listed in the C tag of the
129             response. The returned list isn't guaranteed to be in any particular order.
130              
131             This function polls a web server, so avoid calling it too frequently.
132              
133             =cut
134              
135             sub check_upload {
136             my $self = shift;
137             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
138              
139             return () unless @_; # no tickets
140              
141             my $res = $self->execute_method( 'flickr.photos.upload.checkTickets',
142             { 'tickets' => ((@_ == 1) ? $_[0] : join(',', @_)) } );
143             return () unless defined $res and $res->{success};
144              
145             # FIXME: better error feedback
146              
147             my @rc;
148             return undef unless defined $res->{tree} and exists $res->{tree}->{'children'};
149             for my $n ( @{$res->{tree}->{'children'}} ) {
150             next unless defined $n and exists $n->{'name'} and $n->{'children'};
151             next unless $n->{'name'} eq "uploader";
152              
153             for my $m (@{$n->{'children'}} ) {
154             next unless exists $m->{'name'}
155             and $m->{'name'} eq 'ticket'
156             and exists $m->{'attributes'};
157              
158             # okay, this is maybe a little lazy...
159             push @rc, $m->{'attributes'};
160             }
161             }
162              
163             return @rc;
164             }
165              
166             =head2 make_upload_request
167              
168             my $req = $uploader->make_upload_request(
169             'auth_token' => '82374523',
170             'tags' => 'me myself eye',
171             'is_public' => 1,
172             'is_friend' => 1,
173             'is_family' => 1
174             );
175             $req->header( 'X-Greetz' => 'hi cal' );
176             my $resp = $ua->request( $req );
177              
178             Creates an L object loaded with all the flick upload
179             parameters. This will also sign the request, which means you won't be able to
180             mess any further with the upload request parameters.
181              
182             Takes all the same parameters as L, except that the photo argument
183             isn't required. This in intended so that the caller can include it by
184             messing directly with the HTTP content (via C<$DYNAMIC_FILE_UPLOAD> or
185             the L class, among other things). See C directory from
186             the source distribution for examples.
187              
188             Returns a standard L POST object. The caller can manually
189             do the upload or just call the L function.
190              
191             =cut
192              
193             sub make_upload_request {
194             my $self = shift;
195             die '$self is not a Flickr::Upload' unless $self->isa('Flickr::Upload');
196             my %args = @_;
197              
198             # _required_ by the uploader.
199             die "Missing 'auth_token' argument" unless $args{'auth_token'};
200              
201             my $uri = $args{'uri'} || 'https://api.flickr.com/services/upload/';
202              
203             # passed in separately, so remove from the hash
204             delete $args{uri};
205              
206             # Flickr::API includes this with normal requests, but we're building a custom
207             # message.
208             $args{'api_key'} = $self->{'api_key'};
209              
210             # photo is _not_ included in the sig
211             my $photo = $args{photo};
212             delete $args{photo};
213              
214             $args{'api_sig'} = $self->_sign_args(\%args);
215              
216             # unlikely that the caller would set up the photo as an array,
217             # but...
218             if( defined $photo ) {
219             $photo = [ $photo ] if ref $photo ne "ARRAY";
220             $args{photo} = $photo;
221             }
222              
223             my $req = POST $uri, 'Content_Type' => 'form-data', 'Content' => \%args;
224              
225             return $req;
226             }
227              
228             =head2 upload_request
229              
230             my $photoid = upload_request( $ua, $request );
231              
232             Taking (at least) L and L objects as
233             arguments, this executes the request and processes the result as a
234             flickr upload. It's assumed that the request looks a lot like something
235             created with L. Note that the request must be signed
236             according to the Flickr API authentication rules.
237              
238             Returns the resulting identifier of the uploaded photo (or ticket for
239             asynchronous uploads) on success, C on failure. According to the
240             API documentation, after an upload the user should be directed to the
241             page L.
242              
243             =cut
244              
245             sub upload_request {
246             my $self = shift;
247             die "$self is not a LWP::UserAgent" unless $self->isa('LWP::UserAgent');
248             my $req = shift;
249             die "expecting a HTTP::Request" unless $req->isa('HTTP::Request');
250              
251             # Try 3 times to upload data. Without this flickr_upload is bound
252             # to die on large uploads due to some miscellaneous network
253             # issues. Timeouts on flickr or something else.
254             my ($res, $xml);
255             my $tries = 3;
256             for my $try (1 .. $tries) {
257             # Try to upload
258             $res = $self->request( $req );
259             return () unless defined $res;
260              
261             if ($res->is_success) {
262             $xml = XMLin($res->decoded_content, KeyAttr=>[], ForceArray=>0);
263             return () unless defined $xml;
264             last;
265             } else {
266             my $what_next = ($try == $tries ? "giving up" : "trying again");
267             my $status = $res->status_line;
268              
269             print STDERR "Failed uploading attempt attempt $try/$tries, $what_next. Message from server was: '$status'\n";
270             next;
271             }
272             }
273              
274             my $photoid = $xml->{photoid};
275             my $ticketid = $xml->{ticketid};
276             unless( defined $photoid or defined $ticketid ) {
277             print STDERR "upload failed:\n", $res->decoded_content(), "\n";
278             return undef;
279             }
280              
281             return (defined $photoid) ? $photoid : $ticketid;
282             }
283              
284             =head2 file_length_in_encoded_chunk
285              
286             $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
287             my $photo = 'image.jpeg';
288             my $photo_size = (stat($photo))[7];
289             my $req = $ua->make_upload_request( ... );
290             my $gen = $req->content();
291             die unless ref($gen) eq "CODE";
292              
293             my $state;
294             my $size;
295              
296             $req->content(
297             sub {
298             my $chunk = &$gen();
299              
300             $size += Flickr::Upload::file_length_in_encoded_chunk(\$chunk, \$state, $photo_size);
301              
302             warn "$size bytes have now been uploaded";
303              
304             return $chunk;
305             }
306             );
307              
308             $rc = $ua->upload_request( $req );
309              
310             This subroutine is tells you how much of a chunk in a series of
311             variable size multipart HTTP chunks contains a single file being
312             uploaded given a reference to the current chunk, a reference to a
313             state variable that lives between calls, and the size of the file
314             being uploaded.
315              
316             It can be used used along with L's
317             $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD facility to implement
318             upload progress bars or other upload monitors, see L
319             for a practical example and F for tests.
320              
321             =cut
322              
323             sub file_length_in_encoded_chunk
324             {
325             my ($chunk, $s, $img_size) = @_;
326              
327             $$s = {} unless ref $$s eq 'HASH';
328              
329             # If we've run past the end of the image there's nothing to do but
330             # report no image content in this sector.
331             return 0 if $$s->{done};
332              
333             unless ($$s->{in}) {
334             # Since we haven't found the image yet append this chunk to
335             # our internal data store, we do this because we have to do a
336             # regex match on m[Content-Type...] which might be split
337             # across multiple chunks
338             $$s->{data} .= defined $$chunk ? $$chunk : '';
339              
340             if ($$s->{data} =~ m[Content-Type: .*?\r\n\r\n]g) {
341             # We've found the image inside the stream, record this,
342             # delete ->{data} since we don't need it, and see how much
343             # of the image this particular chunk gives us.
344             $$s->{in} = 1;
345             my $size = length substr($$s->{data}, pos($$s->{data}), -1);
346             delete $$s->{data};
347              
348             $$s->{size} = $size;
349              
350             if ($$s->{size} >= $img_size) {
351             # The image could be so small that we've already run
352             # through it in chunk it starts in, mark as done and
353             # return the total image size
354              
355             $$s->{done} = 1;
356             return $img_size;
357             } else {
358             return $$s->{size};
359             }
360             } else {
361             # Are we inside the image yet? No!
362             return 0;
363             }
364             } else {
365             my $size = length $$chunk;
366              
367             if (($$s->{size} + $size) >= $img_size) {
368             # This chunk finishes the image
369              
370             $$s->{done} = 1;
371              
372             # Return what we had left
373             return $img_size - $$s->{size};
374             } else {
375             # This chunk isn't the last one
376              
377             $$s->{size} += $size;
378              
379             return $size;
380             }
381             }
382             }
383              
384             =head2 photosets_create
385              
386             Calls Flickr's "flickr.photosets.create" method,
387             to create a new Set.
388              
389             The set will use the PrimaryPhotoID as the thumbnail photo.
390              
391             returns: UNDEF on failure, PhotosetID on success.
392              
393             my $photoset_id = $ua->photosets_create( title => 'title',
394             description => 'description',
395             primary_photo_id => ID,
396             auth_token => AUTH_TOKEN );
397              
398             $ua->photosets_addphoto ( photoset_id => $photoset_id,
399             photo_id => ID );
400              
401             =cut
402             sub photosets_create {
403             my $self = shift;
404             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
405              
406             my %args = @_;
407             carp "Missing 'auth_token' parameter for photosets_create()"
408             unless exists $args{'auth_token'};
409             my $auth_token = $args{'auth_token'};
410             carp "Missing 'title' parameter for photosets_create()"
411             unless exists $args{'title'} && length($args{'title'})>0;
412             my $title = $args{'title'};
413             carp "Missing 'primary_photo_id' parameter for photosets_create()"
414             unless exists $args{'primary_photo_id'};
415             my $primary_photo_id = $args{'primary_photo_id'};
416             carp "Invalid primary_photo_id ($primary_photo_id) value (expecting numeric ID)" unless $primary_photo_id =~ /^[0-9]+$/;
417             my $description = ( exists $args{'description'} ) ? $args{'description'} : "" ;
418              
419             my $res = $self->execute_method( 'flickr.photosets.create',
420             { 'title' => $title,
421             'description' => $description,
422             'primary_photo_id' => $primary_photo_id,
423             'auth_token' => $auth_token,
424             } ) ;
425             #TODO: Add detailed error messages
426             return undef unless defined $res and $res->{success};
427              
428             my $hash = XMLin($res->decoded_content(), KeyAttr=>[], ForceArray=>0);
429             my $photoset_id = $hash->{photoset}->{id};
430             if ( ! defined $photoset_id ) {
431             warn "Failed to extract photoset ID from response:\n" .
432             $res->decoded_content() . "\n\n";
433             return undef;
434             }
435             return $photoset_id ;
436             }
437              
438             =head2 photosets_addphoto
439              
440             Calls Flickr's "flickr.photosets.addPhoto" method,
441             to add a (existing) photo to an existing set.
442              
443             returns: UNDEF on failure, TRUE on success.
444              
445             my $photoset_id = $ua->photosets_create( title => 'title',
446             description => 'description',
447             primary_photo_id => ID,
448             auth_token => AUTH_TOKEN );
449              
450             $ua->photosets_addphoto ( photoset_id => $photoset_id,
451             photo_id => ID );
452              
453             =cut
454             sub photosets_addphoto {
455             my $self = shift;
456             die '$self is not a Flickr::API' unless $self->isa('Flickr::API');
457              
458             my %args = @_;
459             carp "Missing 'auth_token' parameter for photosets_addphoto()"
460             unless exists $args{'auth_token'};
461             my $auth_token = $args{'auth_token'};
462             carp "Missing 'photoset_id' parameter for photosets_addphoto()"
463             unless exists $args{'photoset_id'};
464             my $photoset_id = $args{'photoset_id'};
465             carp "Missing 'photo_id' parameter for photosets_addphoto()"
466             unless exists $args{'photo_id'};
467             my $photo_id = $args{'photo_id'};
468              
469             my $res = $self->execute_method( 'flickr.photosets.addPhoto',
470             { 'photoset_id' => $photoset_id,
471             'photo_id' => $photo_id,
472             'auth_token' => $auth_token,
473             } ) ;
474             #TODO: Add detailed error messages
475             return undef unless defined $res;
476              
477             return $res->{success};
478             }
479              
480             # Private method adapted from Flickr::API
481             # See: https://www.flickr.com/services/api/auth.howto.web.html
482             sub _sign_args {
483             my $self = shift;
484             my $args = shift;
485              
486             my $sig = $self->{api_secret};
487              
488             for(sort { $a cmp $b } keys %$args) {
489             $sig .= $_ . (defined($args->{$_}) ? $args->{$_} : "");
490             }
491              
492             return md5_hex($self->{unicode} ? encode_utf8($sig) : $sig);
493             }
494              
495             1;
496             __END__