Saturday, July 25, 2009

Sort your flickr albums in Ruby with Flickr_fu

I have tons of photos in flickr organized by photo sets. Each photo set name has a date prefix like '20090725 Chicago downtown'. I use the prefix to sort album folders in my local drive. However when I upload them to flickr, if I don't upload them in the same order, I will have to use Flickr's Organize to manually drag and drop ones to the right place. It is tedious and there is no quick and easy way to automate it.

I know that flickr provides APIs that allow developers to manage photos programatially in pratically any modern language. Since I am a fan of Ruby, I chose a ruby libray, flickr_fu from commontread.

So I take an hour to set up and write a ruby class to re-order my 1000+ photosets. The class name is FlickrPhotoSetSorter. Here is the step that I did and you can follow, assuming you have ruby 1.8+ installed.

1. Install flickr_fu
sudo gem install flickr-fu

You may have to install a required gem; xml-magic separately.

2. Create flickr.yml to store my flickr API key and secret key. Replace "your key" and "your secret" with the yours. You may obtain them from Flickr API keys. The token_cache.yml is a file flickr_fu uses to store your session token (flickr calls it 'frob').
## YAML Template.
--- !map:Hash
key: "your key"
secret: "your secret"
token_cache: "token_cache.yml"
3. Authorize your program to read/write your albums. I defined a method, authorize, in my FlickrPhotoSetSorter class.

require 'flickr_fu'

class FlickrPhotoSetSorter
def initialize
@flickr = Flickr.new('flickr.yml')
end

def authorize
puts "visit the following url, then click once you have authorized:"
# request write permissions
puts @flickr.auth.url(:write)
gets
flickr.auth.cache_token
end
end

FlickrPhotoSetSorter.new.authorize
The method generates and displays a URL to flickr.com that you will have to visit in order to ensure that you have rights to your account. Here is an example of the result. I replaced all ids with a dummy 12345.

visit the following url, then click once you have authorize
http://flickr.com/services/auth/?frob=12345&auth_token=12345&api_key=12345&perms=write&api_sig=12345
4. Once I authorized the application, I can start loading a list of my photosets using flickr_fu's Photosets class.

  def photosets_list
Flickr::Photosets.new(@flickr).get_list
end
5. Since I have a large collection of my photosets, I don't want to load them from flickr.com every time I run the script. So I chose to store the result into a file that I can load later. I created a file, list.txt and store each photoset with id, number of photos, title and description separated by a pipe symbol '|'
  def store_photoset_list(file_name = 'list.txt')
File.open(file_name, "w") do |file|
photosets_list.map do |photoset|
file.puts("#{photoset.id}|#{photoset.num_photos}|#{photoset.title}|#{photoset.description}")
end
end
end

FlickrPhotoSetSorter.new.store_photoset_list
6. To load it back, firstly I created a method to read a line and returns a hash that contain the information from a photoset.
  def photoset_from(line)
id, num_photos, title, description = line.split('|')
return nil unless id.to_i > 0
{ :id => id,
:num_photos => num_photos,
:title => title,
:description => description }
end


Then, I defined a method to load the file and build an photoset array.

  def load_photoset_list(file_name = 'list.txt')
return @photosets_list if @photosets_list
@photosets_list = []
lines = IO.readlines(file_name)
lines.each do |line|
@photosets_list << photoset_from(line)
end
@photosets_list.compact
end
7. Since flickr_fu does not have a method to sort or update the order of photosets, I have to do it by myself. It is super easy since I already have everything I need. Flickr has an API to order photo sets, flickr.photosets.orderSets and it requires a list of photoset ids separated by commas.

So I sort the photoset by title descending and collect ids and join them with ','. At the end, I use flickr_fu's send_request and pass the API name with the list of photoset ids using HTTP post.

  def order_photosets_by_title
ordered_list = load_photoset_list.sort { |a,b| (b[:title] || "") <=> (a[:title] || "") }
ordered_ids = ordered_list.map { |set| set[:id] }.join(',')
@flickr.send_request('flickr.photosets.orderSets', { :photoset_ids => ordered_ids }, :post)
end
8. To run the script, just use:

FlickrPhotoSetSorter.new.order_photosets_by_title


That's it. Now my photoset list are sorted by title descending order. I can upload older photosets, rerun the script and have them sort again and never worry that my photosets will be out of order again.

Here is the entire source code of the FlickrPhotoSetSorter class.
require 'flickr_fu'

class FlickrPhotoSetSorter
def initialize
@flickr = Flickr.new('flickr.yml')
end

def authorize
puts "visit the following url, then click once you have authorized:"
# request write permissions
puts @flickr.auth.url(:write)
gets
flickr.auth.cache_token
end

def photosets_list
Flickr::Photosets.new(@flickr).get_list
end

def store_photoset_list(file_name = 'list.txt')
File.open(file_name, "w") do |file|
photosets_list.map do |photoset|
file.puts("#{photoset.id}|#{photoset.num_photos}|#{photoset.title}|#{photoset.description}")
end
end
end

def load_photoset_list(file_name = 'list.txt')
return @photosets_list if @photosets_list
@photosets_list = []
lines = IO.readlines(file_name)
lines.each do |line|
@photosets_list << photoset_from(line)
end
@photosets_list.compact
end

def photoset_from(line)
id, num_photos, title, description = line.split('|')
return nil unless id.to_i > 0
{ :id => id,
:num_photos => num_photos,
:title => title,
:description => description }
end

def order_photosets_by_title
ordered_list = load_photoset_list.sort { |a,b| (b[:title] || "") <=> (a[:title] || "") }
ordered_ids = ordered_list.map { |set| set[:id] }.join(',')
@flickr.send_request('flickr.photosets.orderSets', { :photoset_ids => ordered_ids }, :post)
end

end


Feel free to use the code for you needs. If you want to reorder with different criteria, update the sort block in the order_photosets_by_title method. Rename the method as appropriate.

Enjoy.