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