# = Flickr
# An insanely easy interface to the Flickr photo-sharing service. By Scott Raymond.
#
# Author:: Scott Raymond <sco@redgreenblu.com>
# Copyright:: Copyright (c) 2005 Scott Raymond <sco@redgreenblu.com>. Additional content by Patrick Plattes and Chris Taggart (http://pushrod.wordpress.com)
# License:: MIT <http://www.opensource.org/licenses/mit-license.php>
#
# BASIC USAGE:
# require 'flickr'
# flickr = Flickr.new('some_flickr_api_key') # create a flickr client (get an API key from http://www.flickr.com/services/api/)
# user = flickr.users('sco@scottraymond.net') # lookup a user
# user.getInfo.name # get the user's name
# user.location # and location
# user.photos # grab their collection of Photo objects...
# user.groups # ...the groups they're in...
# user.contacts # ...their contacts...
# user.favorites # ...favorite photos...
# user.photosets # ...their photo sets...
# user.tags # ...and their tags
# recentphotos = flickr.photos # get the 100 most recent public photos
# photo = recent.first # or very most recent one
# photo.getInfo.url # see its URL,
# photo.title # title,
# photo.description # and description,
# photo.owner # and its owner.
# File.open(photo.filename, 'w') do |file|
# file.puts p.file # save the photo to a local file
# end
# flickr.photos.each do |p| # get the last 100 public photos...
# File.open(p.filename, 'w') do |f|
# f.puts p.file('Square') # ...and save a local copy of their square thumbnail
# end
# end
require 'cgi'
require 'net/http'
require 'xmlsimple'
require 'digest/md5'
# Flickr client class. Requires an API key
class Flickr
attr_reader :api_key, :auth_token
attr_accessor :user
HOST_URL = 'http://flickr.com'
API_PATH = '/services/rest'
# Flickr, annoyingly, uses a number of representations to specify the size
# of a photo, depending on the context. It gives a label such a "Small" or
# "Medium" to a size of photo, when returning all possible sizes. However,
# when generating the uri for the page that features that size of photo, or
# the source url for the image itself it uses a single letter. Bizarrely,
# these letters are different depending on whether you want the Flickr page
# for the photo or the source uri -- e.g. a "Small" photo (240 pixels on its
# longest side) may be viewed at
# "http://www.flickr.com/photos/sco/2397458775/sizes/s/"
# but its source is at
# "http://farm4.static.flickr.com/3118/2397458775_2ec2ddc324_m.jpg".
# The VALID_SIZES hash associates the correct letter with a label
VALID_SIZES = { "Square" => ["s", "sq"],
"Thumbnail" => ["t", "t"],
"Small" => ["m", "s"],
"Medium" => [nil, "m"],
"Large" => ["b", "l"]
}
# To use the Flickr API you need an api key
# (see http://www.flickr.com/services/api/misc.api_keys.html), and the flickr
# client object shuld be initialized with this. You'll also need a shared
# secret code if you want to use authentication (e.g. to get a user's
# private photos)
# There are two ways to initialize the Flickr client. The preferred way is with
# a hash of params, e.g. 'api_key' => 'your_api_key', 'shared_secret' =>
# 'shared_secret_code'. The older (deprecated) way is to pass an ordered series of
# arguments. This is provided for continuity only, as several of the arguments
# are no longer usable ('email', 'password')
def initialize(api_key_or_params=nil, email=nil, password=nil, shared_secret=nil)
@host = HOST_URL
@api = API_PATH
if api_key_or_params.is_a?(Hash)
@api_key = api_key_or_params['api_key']
@shared_secret = api_key_or_params['shared_secret']
@auth_token = api_key_or_params['auth_token']
else
@api_key = api_key_or_params
@shared_secret = shared_secret
login(email, password) if email and password
end
end
# Gets authentication token given a Flickr frob, which is returned when user
# allows access to their account for the application with the api_key which
# made the request
def get_token_from(frob)
auth_response = request("auth.getToken", :frob => frob)['auth']
@auth_token = auth_response['token']
@user = User.new( 'id' => auth_response['user']['nsid'],
'username' => auth_response['user']['username'],
'name' => auth_response['user']['fullname'],
'client' => self)
@auth_token
end
# Stores authentication credentials to use on all subsequent calls.
# If authentication succeeds, returns a User object.
# NB This call is no longer in API and will result in an error if called
def login(email='', password='')
@email = email
@password = password
user = request('test.login')['user'] rescue fail
@user = User.new(user['id'], nil, nil, nil, @api_key)
end
# Implements flickr.urls.lookupGroup and flickr.urls.lookupUser
def find_by_url(url)
response = urls_lookupUser('url'=>url) rescue urls_lookupGroup('url'=>url) rescue nil
(response['user']) ? User.new(response['user']['id'], nil, nil, nil, @api_key) : Group.new(response['group']['id'], @api_key) unless response.nil?
end
# Implements flickr.photos.getRecent and flickr.photos.search
def photos(*criteria)
photos = (criteria[0]) ? photos_search(criteria[0]) : photos_getRecent
# At this point, search criterias with per_page => has structure
# {"photos => {"photo" => {...}}"}
# While per_page values with > 1 have
# {"photos => {"photo" => [{...}]}"}
collection = photos['photos']['photo']
collection = [collection] if collection.is_a? Hash
collection.collect { |photo| Photo.new(photo.delete('id'), @api_key, photo) }
end
def photos_search(params={})
photos = request('photos.search', params)
collection = photos['photos']['photo']
collection = [collection] if collection.is_a? Hash
collection.collect { |photo| Photo.new(photo.delete('id'), @api_key, photo) }
end
# Gets public photos with a given tag
def tag(tag)
photos('tags'=>tag)
end
# Implements flickr.people.findByEmail and flickr.people.findByUsername.
def users(lookup=nil)
user = people_findByEmail('find_email'=>lookup)['user'] rescue people_findByUsername('username'=>lookup)['user']
return User.new("id" => user["nsid"], "username" => user["username"], "client" => self)
end
# Implements flickr.groups.search
def groups(group_name, options={})
collection = groups_search({"text" => group_name}.merge(options))['groups']['group']
collection = [collection] if collection.is_a? Hash
collection.collect { |group| Group.new( "id" => group['nsid'],
"name" => group['name'],
"eighteenplus" => group['eighteenplus'],
"client" => self) }
end
# Implements flickr.tags.getRelated
def related_tags(tag)
tags_getRelated('tag_id'=>tag)['tags']['tag']
end
# Implements flickr.photos.licenses.getInfo
def licenses
photos_licenses_getInfo['licenses']['license']
end
# Returns url for user to login in to Flickr to authenticate app for a user
def login_url(perms)
"http://flickr.com/services/auth/?api_key=#{@api_key}&perms=#{perms}&api_sig=#{signature_from('api_key'=>@api_key, 'perms' => perms)}"
end
# Implements everything else.
# Any method not defined explicitly will be passed on to the Flickr API,
# and return an XmlSimple document. For example, Flickr#test_echo is not
# defined, so it will pass the call to the flickr.test.echo method.
def method_missing(method_id, params={})
request(method_id.id2name.gsub(/_/, '.'), params)
end
protected
# Does an HTTP GET on a given URL and returns the response body
def http_get(url)
Net::HTTP.get_response(URI.parse(url)).body.to_s
end
# Takes a Flickr API method name and set of parameters; returns an XmlSimple object with the response
def request(method, params={})
url = request_url(method, params)
response = XmlSimple.xml_in(http_get(url), { 'ForceArray' => false })
raise response['err']['msg'] if response['stat'] != 'ok'
response
end
# Builds url for Flickr API REST request from given the flickr method name
# (exclusing the 'flickr.' that begins each method call) and params (where
# applicable) which should be supplied as a Hash (e.g 'user_id' => "foo123")
def request_url(method, params={})
method = 'flickr.' + method
url = "#{@host}#{@api}/?api_key=#{@api_key}&method=#{method}"
params.merge!('api_key' => @api_key, 'method' => method, 'auth_token' => @auth_token)
signature = signature_from(params)
url = "#{@host}#{@api}/?" + params.merge('api_sig' => signature).collect { |k,v| "#{k}=" + CGI::escape(v.to_s) unless v.nil? }.compact.join("&")
end
def signature_from(params={})
return unless @shared_secret # don't both getting signature if no shared_secret
request_str = params.reject {|k,v| v.nil?}.collect {|p| "#{p[0].to_s}#{p[1]}"}.sort.join # build key value pairs, sort in alpha order then join them, ignoring those with nil value
return Digest::MD5.hexdigest("#{@shared_secret}#{request_str}")
end
# Todo:
# logged_in?
# if logged in:
# flickr.blogs.getList
# flickr.favorites.add
# flickr.favorites.remove
# flickr.groups.browse
# flickr.photos.getCounts
# flickr.photos.getNotInSet
# flickr.photos.getUntagged
# flickr.photosets.create
# flickr.photosets.orderSets
# flickr.tags.getListUserPopular
# flickr.test.login
# uploading
class User
attr_reader :client, :id, :name, :location, :photos_url, :url, :count, :firstdate, :firstdatetaken
# A Flickr::User can be instantiated in two ways. The old (deprecated)
# method is with an ordered series of values. The new method is with a
# params Hash, which is easier when a variable number of params are
# supplied, which is the case here, and also avoids having to constantly
# supply nil values for the email and password, which are now irrelevant
# as authentication is no longer done this way.
# An associated flickr client will also be generated if an api key is
# passed among the arguments or in the params hash. Alternatively, and
# most likely, an existing client object may be passed in the params hash
# (e.g. 'client' => some_existing_flickr_client_object), and this is
# what happends when users are initlialized as the result of a method
# called on the flickr client (e.g. flickr.users)
def initialize(id_or_params_hash=nil, username=nil, email=nil, password=nil, api_key=nil)
if id_or_params_hash.is_a?(Hash)
id_or_params_hash.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
else
@id = id_or_params_hash
@username = username
@email = email
@password = password
@api_key = api_key
end
@client ||= Flickr.new('api_key' => @api_key, 'shared_secret' => @shared_secret, 'auth_token' => @auth_token) if @api_key
@client.login(@email, @password) if @email and @password # this is now irrelevant as Flickr API no longer supports authentication this way
end
def username
@username.nil? ? getInfo.username : @username
end
def name
@name.nil? ? getInfo.name : @name
end
def location
@location.nil? ? getInfo.location : @location
end
def count
@count.nil? ? getInfo.count : @count
end
def firstdate
@firstdate.nil? ? getInfo.firstdate : @firstdate
end
def firstdatetaken
@firstdatetaken.nil? ? getInfo.firstdatetaken : @firstdatetaken
end
def photos_url
@photos_url.nil? ? getInfo.photos_url : @photos_url
end
def url
@url.nil? ? getInfo.url : @url
end
# Implements flickr.people.getPublicGroups
def groups
collection = @client.people_getPublicGroups('user_id'=>@id)['groups']['group']
collection = [collection] if collection.is_a? Hash
collection.collect { |group| Group.new( "id" => group['nsid'],
"name" => group['name'],
"eighteenplus" => group['eighteenplus'],
"client" => @client) }
end
# Implements flickr.people.getPublicPhotos. Options hash allows you to add
# extra restrictions as per flickr.people.getPublicPhotos docs, e.g.
# user.photos('per_page' => '25', 'extras' => 'date_taken')
def photos(options={})
collection = @client.people_getPublicPhotos({'user_id'=>@id}.merge(options))['photos']['photo']
collection = [collection] if collection.is_a? Hash
collection.collect { |photo| Photo.new(photo.delete('id'), @api_key, photo.merge('owner' => self)) }
# what about non-public photos?
end
# Gets photos with a given tag
def tag(tag)
@client.photos('user_id'=>@id, 'tags'=>tag)
end
# Implements flickr.contacts.getPublicList and flickr.contacts.getList
def contacts
@client.contacts_getPublicList('user_id'=>@id)['contacts']['contact'].collect { |contact| User.new(contact['nsid'], contact['username'], nil, nil, @api_key) }
#or
end
# Implements flickr.favorites.getPublicList and flickr.favorites.getList
def favorites
@client.favorites_getPublicList('user_id'=>@id)['photos']['photo'].collect { |photo| Photo.new(photo.delete('id'), @api_key, photo) }
#or
end
# Implements flickr.photosets.getList
def photosets
@client.photosets_getList('user_id'=>@id)['photosets']['photoset'].collect { |photoset| Photoset.new(photoset['id'], @api_key) }
end
# Implements flickr.tags.getListUser
def tags
@client.tags_getListUser('user_id'=>@id)['who']['tags']['tag'].collect { |tag| tag }
end
# Implements flickr.photos.getContactsPublicPhotos and flickr.photos.getContactsPhotos
def contactsPhotos
@client.photos_getContactsPublicPhotos('user_id'=>@id)['photos']['photo'].collect { |photo| Photo.new(photo.delete('id'), @api_key, photo) }
# or
#@client.photos_getContactsPhotos['photos']['photo'].collect { |photo| Photo.new(photo['id'], @api_key) }
end
def to_s
@name
end
private
# Implements flickr.people.getInfo, flickr.urls.getUserPhotos, and flickr.urls.getUserProfile
def getInfo
info = @client.people_getInfo('user_id'=>@id)['person']
@username = info['username']
@name = info['realname']
@location = info['location']
@count = info['photos']['count']
@firstdate = info['photos']['firstdate']
@firstdatetaken = info['photos']['firstdatetaken']
@photos_url = @client.urls_getUserPhotos('user_id'=>@id)['user']['url']
@url = @client.urls_getUserProfile('user_id'=>@id)['user']['url']
self
end
end
class Photo
attr_reader :id, :client, :title
def initialize(id=nil, api_key=nil, extra_params={})
@id = id
@api_key = api_key
extra_params.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
@client = Flickr.new @api_key
end
# Allows access to all photos instance variables through hash like
# interface, e.g. photo["datetaken"] returns @datetaken instance
# variable. Useful for accessing any weird and wonderful parameter
# that may have been returned by Flickr when finding the photo,
# e.g. those returned by the extras argument in
# flickr.people.getPublicPhotos
def [](param_name)
instance_variable_get("@#{param_name}")
end
def title
@title.nil? ? getInfo.title : @title
end
# Returns the owner of the photo as a Flickr::User. If we have no info
# about the owner, we make an API call to get it. If we already have
# the owner's id, create a user based on that. Either way, we cache the
# result so we don't need to check again
def owner
case @owner
when Flickr::User
@owner
when String
@owner = Flickr::User.new(@owner, nil, nil, nil, @api_key)
else
getInfo.owner
end
end
def server
@server.nil? ? getInfo.server : @server
end
def isfavorite
@isfavorite.nil? ? getInfo.isfavorite : @isfavorite
end
def license
@license.nil? ? getInfo.license : @license
end
def rotation
@rotation.nil? ? getInfo.rotation : @rotation
end
def description
@description.nil? ? getInfo.description : @description
end
def notes
@notes.nil? ? getInfo.notes : @notes
end
# Returns the URL for the photo page (default or any specified size).
# NB This method returns the main page for the photo by default. However,
# it also returns this page for "Medium" size images, instead of the
# specific page for the "Medium" size, which should be
# http://www.flickr.com/photos/#{username}/#{photo_id}/sizes/m/
# It may make sense to correct this in future
def url(size='Medium')
if size=='Medium'
"http://flickr.com/photos/#{owner.username}/#{@id}"
else
uri_for_photo_from_self(size) || sizes(size)['url']
end
end
# Returns the URL for the image (default or any specified size)
def source(size='Medium')
image_source_uri_from_self(size) || sizes(size)['source']
end
# Returns the photo file data itself, in any specified size. Example: File.open(photo.title, 'w') { |f| f.puts photo.file }
def file(size='Medium')
Net::HTTP.get_response(URI.parse(source(size))).body
end
# Unique filename for the image, based on the Flickr NSID
def filename
"#{@id}.jpg"
end
# Implements flickr.photos.getContext
def context
context = @client.photos_getContext('photo_id'=>@id)
@previousPhoto = Photo.new(context['prevphoto'].delete('id'), @api_key, context['prevphoto']) if context['prevphoto']['id']!='0'
@nextPhoto = Photo.new(context['nextphoto'].delete('id'), @api_key, context['nextphoto']) if context['nextphoto']['id']!='0'
return [@previousPhoto, @nextPhoto]
end
# Implements flickr.photos.getExif
def exif
@client.photos_getExif('photo_id'=>@id)['photo']
end
# Implements flickr.photos.getPerms
def permissions
@client.photos_getPerms('photo_id'=>@id)['perms']
end
# Implements flickr.photos.getSizes
def sizes(size=nil)
sizes = @client.photos_getSizes