...making Linux just a little more fun!
Flickr is a photo-sharing service: it allows you to share your photos with friends, family, or the public in general. Flickr caters to "moblogging": photo blogging from mobile phones, which is a great part of the appeal to me. It also comes with an API so you don't have to take apart its pages to scrape it, which is nice.
Flickr::API, which was written by one of Flickr's developers, provides a way to interface to Flickr from Perl. (Flickr's API documentation is available here). There is also Flickr::Upload, which does exactly as the name suggests.
The first step is to get an API key. Flickr is still a relatively new
service, and want to know who is writing software to access their service
and why, and having people register for an API key is a common requirement
of web services anyway. To register for an API key, follow the steps
outlined on this
page (at the time of writing, this simply involved emailing Cal Henderson,
the author of Flickr::API
).
API key at the ready, you can now start using Flickr. Flickr provides
a test method flickr.test.echo
to allow you to check that
everything is working, and this is used in the example given in
Flickr::API
's POD. I've expanded on it slightly to give some
output using the Data::Dumper
module:
use Flickr::API; use Data::Dumper; my $api = new Flickr::API({'key' => ''}); my $response = $api->execute_method('flickr.test.echo', { 'foo' => 'bar', 'baz' => 'quux', }); print "Success: $response->{success}\n"; print "Error code: $response->{error_code}\n"; print Dumper ($response);
The output from this should be
Success: 1 Error code: 0
followed by a lot of output from Data::Dumper
. The part of
this output that we're interested should look something like this:
<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <baz>quux</baz> <method>flickr.test.echo</method> <foo>bar</foo> <api_key>[snip]</api_key> </rsp>
Once everything is up and running, we're ready to start doing something of interest. I'm only really interested in using my own photos, so I first need to get my user id.
There are two ways of doing this: you can call
flickr.urls.lookupUser
with the URL of a user's photo or user
page, or if you know the user's username, with
flickr.people.findByUsername
. Here's an example that uses both:
use Flickr::API; use Data::Dumper; use warnings; use strict; my $api = new Flickr::API({'key' => ''}); my $user = shift; my $response; if ($user =~ m!http://!i) { $response = $api->execute_method ('flickr.urls.lookupUser', { 'url' => $user, }); } else { $response = $api->execute_method ('flickr.urls.findByUsername', { 'username' => $user, }); } my $debug = 1; if ($debug) { print "Success: $response->{success}\n"; print "Error code: $response->{error_code}\n"; print Dumper ($response); }
Cleaning it up to provide useful output is left as an exercise for
the reader (don't worry, I'll get to that later). When called with either
a URL or username, it should have (among the usual Data::Dumper
output) something that looks like this:
<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <user id="49502976979@N01"> <username>jimregan</username> </user> </rsp>
So... I mentioned that I was going to do something useful. What I'm looking to build is a little script that gives me a montage of the last few photos I posted, and a script that takes the coordinates of a photo note and generates an image map (at some point, I'd like to change that to be RDF, so I can use it in FOAF or what have you, but for now, an image map is easier).
First, let's take a look at how we get the information, and what it looks like:
use Flickr::API; use Data::Dumper; use warnings; use strict; # Test photo: http://flickr.com/photos/jimregan/120856/ # Photo url: http://photos1.flickr.com/120856_01b51464c0.jpg # http://www.flickr.com/services/api/flickr.photos.getInfo.html my $api = new Flickr::API({'key' => ''}); my $response; $response = $api->execute_method ('flickr.photos.getInfo', { 'photo_id' => '120856', 'secret' => '01b51464c0' }); my $debug = 1; if ($debug) { print "Success: $response->{success}\n"; print "Error code: $response->{error_code}\n"; print Dumper ($response); }
output:
<?xml version="1.0" encoding="utf-8" ?> <rsp stat="ok"> <photo id="120856" secret="01b51464c0" server="1" dateuploaded="1090965387" isfavorite="0" license="4"> <owner nsid="49502976979@N01" username="jimregan" realname="Jimmy O\'Regan" location="Ireland" /> <title>IMAGE0006</title> <description>Mark, May 2002</description> <visibility ispublic="1" isfriend="0" isfamily="0" /> <dates posted="1090965387" taken="2004-07-27 14:56:27" takengranularity="0" /> <editability cancomment="0" canaddmeta="0" /> <comments>0</comments> <notes> <note id="10840" author="49502976979@N01" authorname="jimregan" x="96" y="103" w="38" h="24">Look - missing his front teeth at the bottom!</note> </notes> <tags> <tag id="283784" author="49502976979@N01" raw="Mark">mark</tag> <tag id="283785" author="49502976979@N01" raw="2002">2002</tag> </tags> </photo> </rsp>
So, how do we turn that rather useless code example into something that
will generate a simple HTML page with an image map? I could have tried
accessing $response->tree
directly, but life's too short for
that. The author of Flickr::API
and
XML::Parser::Lite::Tree
seems to have thought the same, because
he also wrote XML::Parser::Lite::Tree::XPath
, which allows some
simple XPath expressions to be used on XML::Parser::Lite::Tree
's
output.
With a look at the XML above, we want the contents of the
<note>
tags: /photo/notes/note
#!/usr/bin/perl use Flickr::API; use Data::Dumper; use XML::Parser::Lite::Tree::XPath; use warnings; use strict; # Test photo: http://flickr.com/photos/jimregan/120856/ my $photo = "http://photos1.flickr.com/120856_01b51464c0.jpg"; # http://www.flickr.com/services/api/flickr.photos.getInfo.html my $api = new Flickr::API({'key' => ''}); my $response; $response = $api->execute_method ('flickr.photos.getInfo', { 'photo_id' => '120856', 'secret' => '01b51464c0' }); my $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); my @notes = $xpath->select_nodes('/photo/notes/note'); print "<html>\n<head>\n<title>Flickr Photo</title>\n</head>\n"; print "<img src=\"$photo\" alt=\"Flickr photo\" usemap=\"#genmap\">\n"; print "<map name=\"genmap\">\n"; foreach (@notes) { print "<area shape=\"rect\" coords=\""; print "$_->{attributes}->{x}, "; print "$_->{attributes}->{y}, "; print $_->{attributes}->{x} + $_->{attributes}->{w} .", "; print $_->{attributes}->{y} + $_->{attributes}->{h} ."\" "; print "alt=\"$_->{children}[0]->{content}\" "; print "title=\"$_->{children}[0]->{content}\" nohref>\n"; } print "</map>\n</html>\n";
Now we're getting somewhere. The output is pretty shoddy HTML, but it works:
<html> <head> <title>Flickr Photo</title> </head> <img src="http://photos1.flickr.com/120856_01b51464c0.jpg" alt="Flickr photo" usemap="#genmap"> <map name="genmap"> <area shape="rect" coords="96, 103, 134, 127" alt="Look - missing his front teeth at the bottom!" title="Look - missing his front teeth at the bottom!" nohref> </map> </html>
Let's go one better, and show what it looks like:
Here's an improved version of that script that takes one or two parameters from the command line (photo ID, and secret if available) and creates a web page with more information (text version):
#!/usr/bin/perl use Flickr::API; use XML::Parser::Lite::Tree::XPath; use Date::Format qw(time2str); use warnings; use strict; my $api = new Flickr::API({'key' => ''}); my $response; my $photo_id = $ARGV[0]; my ($desc, $date, $title, $taken, $photo); if ($#ARGV == 1) { $response = $api->execute_method ('flickr.photos.getInfo', { 'photo_id' => $ARGV[0], 'secret' => $ARGV[1] }); } else { $response = $api->execute_method ('flickr.photos.getInfo', { 'photo_id' => $ARGV[0], }); } my $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); my @notes = $xpath->select_nodes('/photo/notes/note'); my @tmp = $xpath->select_nodes('/photo/dates'); $taken = $tmp[0]->{attributes}->{taken}; @tmp = $xpath->select_nodes('/photo/dates'); $date = time2str "%a %b %e %H:%M:%S %Y", $tmp[0]->{attributes}->{posted}; @tmp = $xpath->select_nodes('/photo/description'); $desc = $tmp[0]->{children}[0]->{content}; @tmp = $xpath->select_nodes('/photo/title'); $title = $tmp[0]->{children}[0]->{content}; @tmp = $xpath->select_nodes('/photo'); $photo = "http://photos" . $tmp[0]->{attributes}->{server} . ".flickr.com/" . $tmp[0]->{attributes}->{id} . "_" . $tmp[0]->{attributes}->{secret} . ".jpg"; print "<html>\n<head>\n<title>$title</title>\n</head>\n"; print "<img src=\"$photo\" alt=\"$title\" usemap=\"#genmap\">\n"; print "<map name=\"genmap\">\n"; foreach (@notes) { print "<area shape=\"rect\" coords=\""; print "$_->{attributes}->{x}, "; print "$_->{attributes}->{y}, "; print $_->{attributes}->{x} + $_->{attributes}->{w} .", "; print $_->{attributes}->{y} + $_->{attributes}->{h} ."\" "; print "alt=\"$_->{children}[0]->{content}\" "; print "title=\"$_->{children}[0]->{content}\" nohref>\n"; } print "</map>\n"; print "<p>$desc</p>\n"; print "<p>Taken: $taken, Uploaded: $date</p>\n"; print "</html>\n";
Let's look at the output of that:
Taken: 2004-12-12 01:09:16, Uploaded: Sun Dec 12 01:09:16 2004
I had a script earlier that did the basics of finding a userid, and said that I was going to leave making it useful as an exercise for the reader. Well, the bulk of this article was written on Christmas Day, so Merry Christmas: (text version)
use Flickr::API; use XML::Parser::Lite::Tree::XPath; use warnings; use strict; my $theuser = shift; sub finduser { my $fuser = shift; my ($xpath, @username, $userid); if ($fuser =~ m!http://!i) { $response = $api->execute_method ('flickr.urls.lookupUser', { 'url' => $fuser, }); $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); @username = $xpath->select_nodes('/user'); $userid = $username[0]->{attributes}->{id}; } else { $response = $api->execute_method ('flickr.people.findByUsername', { 'username' => $fuser, }); $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); @username = $xpath->select_nodes('/user'); $userid = $username[0]->{attributes}->{nsid}; } return $userid; } print finduser ($theuser);
So how do we upload images? We use Flickr::Upload
. There
isn't much to using this module: the following script is based on the example
from the POD, but with two minor differences.
First, the script takes the location of the image as a parameter, so it can be used more than once; second, it tells Mozilla to open a page so the uploader can edit the details of the photo (as the POD and Flickr's API documentation say it should). (text version)
use LWP::UserAgent; use Flickr::Upload qw(upload); my $image = shift; my $ua = LWP::UserAgent->new; my $photoid = upload ($ua, 'photo' => $image, 'email' => '', 'password' => '', 'tags' => 'mobile', 'is_public' => 1, 'is_friend' => 1, 'is_family' => 1 ) or die "Failed to upload $image"; `mozilla -remote \"openURL(http://www.flickr.com/tools/uploader_edit.gne?ids=$photoid)\"`;
The only required parameters are $ua
, email
,
and password
. These last two are left blank, for obvious
reasons.
Here it is, the pièste de résistance: a script to generate a montage from Flickr. (text version)
use Flickr::API; use XML::Parser::Lite::Tree::XPath; use Getopt::Long; use Data::Dumper; use Image::Magick; use LWP::Simple; use warnings; use strict; # Getopt vars. All arguments with default values. # You probably want to set this a bit lower my $count = 24; my $theuser = "http://flickr.com/photos/jimregan"; my $type = 'photos'; my $email = ''; my $pass = ''; my $xpath; my $result = GetOptions ("user=s" => \$theuser, "type=s" => \$type, "count=i" => \$count, "password=s" => \$pass, "email=s" => \$email); # For some reason Image::Magick doesn't read the # last image on the list. <shrug> $count++; my $api = new Flickr::API({'key' => ''}); my $response; my $debug = 1; my $user = finduser ($theuser); if ($type eq 'photos') { $response = $api->execute_method ('flickr.people.getPublicPhotos', { 'user_id' => $user, 'per_page' => $count, 'page' => 1}); } elsif ($type eq 'favourites'||$type eq 'favorites') { $response = $api->execute_method ('flickr.favorites.getList', { 'user_id' => $user, 'per_page' => $count, 'email' => $email, 'password' => $pass, 'page' => 1}); } elsif ($type eq 'contacts') { $response = $api->execute_method ('flickr.photos.getContactsPhotos', { 'count' => $count, 'email' => $email, 'password' => $pass,}); } else { die "--type must be 'photos', 'contacts' or 'favo[u]rites'\n"; } if ($response->{success} == 0) { die "Error $response->{error_code}: $response->{error_message}" . "\nDid you remember to pass --email and --password?\n"; } my $photolist = new XML::Parser::Lite::Tree::XPath($response->{tree}); my @bphoto = $photolist->select_nodes('/photos/photo'); my ($photo, $photofile, @photofiles); # Set up the image for our montage my $image=Image::Magick->new; foreach (@bphoto) { $photo = "http://photos" . $_->{attributes}->{server} . ".flickr.com/" . $_->{attributes}->{id} . "_" . $_->{attributes}->{secret} . ".jpg"; $photofile = "tmp-$_->{attributes}->{id}.jpg"; push @photofiles, $photofile; open (FILE, ">$photofile"); my $g = get($photo); print FILE $g; } foreach (@photofiles) { $image->Read($_); } if ($debug) { warn "$image\n" if "$image"; print 0+$image; print "\n"; } print Dumper ($image); my $montage = $image->Montage; $montage->Write ('output.jpg'); foreach (@photofiles) { unlink $_; } sub finduser { my $fuser = shift; my ($xpath, @username, $userid); if ($fuser =~ m!http://!i) { $response = $api->execute_method ('flickr.urls.lookupUser', { 'url' => $fuser, }); $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); @username = $xpath->select_nodes('/user'); $userid = $username[0]->{attributes}->{id}; } else { $response = $api->execute_method ('flickr.people.findByUsername', { 'username' => $fuser, }); $xpath = new XML::Parser::Lite::Tree::XPath($response->{tree}); @username = $xpath->select_nodes('/user'); $userid = $username[0]->{attributes}->{nsid}; } return $userid; }
This does quite a bit more than the other scripts, and is a bit more neat too. Note that, because Flickr requires authentication, you need to pass your email and password if you are looking for a montage of images from your Favourites or Contacts.
I'll leave you with the default output of that script (though shrunk a bit):
Jimmy is a single father of one, who enjoys long walks... Oh, right.
Jimmy has been using computers from the tender age of seven, when his father
inherited an Amstrad PCW8256. After a few brief flirtations with an Atari ST
and numerous versions of DOS and Windows, Jimmy was introduced to Linux in 1998
and hasn't looked back.
In his spare time, Jimmy likes to play guitar and read: not at the same time,
but the picks make handy bookmarks.