Skip to content
This repository has been archived by the owner on Apr 16, 2018. It is now read-only.

Commit

Permalink
Add instagram gem library to project lib.
Browse files Browse the repository at this point in the history
  • Loading branch information
rummelonp committed Dec 20, 2010
1 parent 2ced22b commit 26c86c2
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 8 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
source 'http://rubygems.org'

gem 'rails', '3.0.3'
gem 'instagram', :require => 'instagram/cached'
gem 'addressable'
gem 'nibbler', '>= 1.2.0'

group :development, :test do
gem 'ruby-debug19'
Expand Down
8 changes: 2 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ GEM
erubis (2.6.6)
abstract (>= 1.0.0)
i18n (0.5.0)
instagram (0.2.0)
addressable
nibbler (>= 1.2.0)
yajl-ruby
linecache19 (0.5.11)
ruby_core_source (>= 0.1.4)
mail (2.2.12)
Expand Down Expand Up @@ -97,13 +93,13 @@ GEM
treetop (1.4.9)
polyglot (>= 0.3.1)
tzinfo (0.3.23)
yajl-ruby (0.7.8)

PLATFORMS
ruby

DEPENDENCIES
instagram
addressable
nibbler (>= 1.2.0)
rails (= 3.0.3)
rspec-rails (>= 2.3.0)
ruby-debug19
2 changes: 1 addition & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Application < Rails::Application
# -- all .rb files in that directory are automatically loaded.

# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
config.autoload_paths += %W(#{config.root}/lib)

# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
Expand Down
47 changes: 47 additions & 0 deletions lib/instagram.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'addressable/uri'
require 'addressable/template'
require 'instagram/models'

module Instagram

extend self

Popular = Addressable::URI.parse 'http://instagr.am/api/v1/feed/popular/'
UserFeed = Addressable::Template.new 'http://instagr.am/api/v1/feed/user/{user_id}/'
UserInfo = Addressable::Template.new 'http://instagr.am/api/v1/users/{user_id}/info/'

def popular(params = {}, options = {})
parse_response(Popular.dup, params, options.fetch(:parse_with, Timeline))
end

def by_user(user_id, params = {}, options = {})
url = UserFeed.expand :user_id => user_id
parse_response(url, params, options.fetch(:parse_with, Timeline))
end

def user_info(user_id, params = {}, options = {})
url = UserInfo.expand :user_id => user_id
parse_response(url, params, options.fetch(:parse_with, UserWrap))
end

private

def parse_response(url, params, parser = nil)
url.query_values = params
body = get_url url
parser ? parser.parse(body) : body
end

def get_url(url)
response = Net::HTTP.start(url.host, url.port) { |http|
http.get url.request_uri
}

if Net::HTTPSuccess === response
response.body
else
response.error!
end
end

end
24 changes: 24 additions & 0 deletions lib/instagram/cached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'instagram/failsafe_store'

module Instagram
module Cached
extend Instagram

class << self
attr_accessor :cache

def setup(cache_dir, options = {})
self.cache = FailsafeStore.new(cache_dir, {
namespace: 'instagram',
exceptions: [Net::HTTPServerException, JSON::ParserError]
}.update(options))
end

private
def get_url(url)
cache.fetch(url.to_s) { super }
end
end

end
end
52 changes: 52 additions & 0 deletions lib/instagram/failsafe_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require 'active_support/cache'

module Instagram
class FailsafeStore < ActiveSupport::Cache::FileStore
# Reuses the stale cache if a known exception occurs while yielding to the block.
# The list of exception classes is read from the ":exceptions" array.
def fetch(name, options = nil)
options = merged_options(options)
key = namespaced_key(name, options)
entry = unless options[:force]
instrument(:read, name, options) do |payload|
payload[:super_operation] = :fetch if payload
read_entry(key, options)
end
end

if entry and not entry.expired?
instrument(:fetch_hit, name, options) { |payload| }
entry.value
else
reusing_stale = false

result = begin
instrument(:generate, name, options) do |payload|
yield
end
rescue
if entry and ignore_exception?($!)
reusing_stale = true
instrument(:reuse_stale, name, options) do |payload|
payload[:exception] = $! if payload
entry.value
end
else
# TODO: figure out if deleting entries is ever necessary
# delete_entry(key, options) if entry
raise
end
end

write(name, result, options) unless reusing_stale
result
end
end

private

def ignore_exception?(ex)
options[:exceptions] && options[:exceptions].any? { |klass| ex.is_a? klass }
end
end
end
134 changes: 134 additions & 0 deletions lib/instagram/models.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
require 'nibbler/json'

module Instagram

class Base < NibblerJSON
# `pk` is such a dumb property name
element 'pk' => :id
end

class User < Base
element :username
element :full_name
element 'profile_pic_url' => :avatar_url
alias avatar avatar_url # `avatar` is deprecated

# extended info
element :media_count
element :following_count
alias following following_count # `following` will return an array of users in future!
element :follower_count
alias followers follower_count # `followers` will return an array of users in future!

def ==(other)
User === other and other.id == self.id
end
end

class UserWrap < NibblerJSON
element :user, :with => User
# return user instead of self when done
def parse() super.user end
end

class Media < Base
# short string used for permalink
element :code
# type is always 1 (other values possibly reserved for video in the future?)
element :media_type
# filter code; use `filter_name` to get human name of the filter used
element :filter_type
# I don't know what "device timestamp" is and how it relates to `taken_at`?
element :device_timestamp
# timestamp of when the picture was taken
element :taken_at, :with => lambda { |sec| Time.at(sec) }
# user who uploaded the media
element :user, :with => User

# array of people who liked this media
elements :likers, :with => User

elements :comments, :with => NibblerJSON do
element :created_at, :with => lambda { |sec| Time.at(sec) }
# content type is always "comment"
element :content_type
# `type` is always 1 (other values possibly reserved for comments in form of media?)
element :type
# the `pk` of parent media
element :media_id
# comment body
element :text
# comment author
element :user, :with => User
end

elements 'image_versions' => :images, :with => NibblerJSON do
element :url
# `type` is 5 for 150px, 6 for 306px and 7 for 612px
element :type
element :width
element :height

alias to_s url
end

# image location
element :lat
element :lng

def geolocated?
self.lat and self.lng
end

element :location, :with => Base do
# ID on a 3rd-party service
element :external_id
# name of 3rd-party service, like "foursquare"
element :external_source
# name of location
element :name
# address in the external service's database
element :address
element :lat
element :lng
end

# author's caption for the image; can be nil
def caption
# caption is implemented as a first comment made by the owner
if comments.first and self.user == comments.first.user
comments.first.text
end
end

# typical sizes: 150px / 306px / 612px square
def image_url(size = 150)
self.images.find { |img| img.width == size }.to_s
end

FILTERS = {
1 => 'X-Pro II',
2 => 'Lomo-fi',
3 => 'Earlybird',
17 => 'Lily',
5 => 'Poprocket',
10 => 'Inkwell',
4 => 'Apollo',
15 => 'Nashville',
13 => 'Gotham',
14 => '1977',
16 => 'Lord Kelvin'
}

def filter_name
FILTERS[filter_type.to_i]
end
end

class Timeline < NibblerJSON
elements :items, :with => Media
# return items instead of self when done
def parse() super.items end
end

end

0 comments on commit 26c86c2

Please sign in to comment.