...making Linux just a little more fun!

<-- prev | next -->

Flickr and Perl

By Jimmy O'Regan

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.

Getting started

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>

Doing something useful

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).

Generating an Image Map

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:

Flickr photo Look - missing his front teeth
at the bottom!

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:

Beata Kennedy's Beata Pat's shift's night out

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);

Flickr::Upload

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.

Creating a montage from Flickr

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):

Default script output

 


[BIO] 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.

Copyright © 2005, Jimmy O'Regan. Released under the Open Publication license unless otherwise noted in the body of the article. Linux Gazette is not produced, sponsored, or endorsed by its prior host, SSC, Inc.

Published in Issue 110 of Linux Gazette, January 2005

<-- prev | next -->
Tux