Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 462 lines (393 sloc) 12.919 kB
a0a5168 @mislav mini app to display popular/per-user photos
authored
1 # encoding: utf-8
2 require 'sinatra'
3 require 'active_support/cache'
4 require 'active_support/core_ext/numeric/time'
5 require 'active_support/core_ext/integer/time'
6 require 'active_support/core_ext/time/acts_like'
81f94f0 @mislav more HTTP caching headers
authored
7 require 'digest/md5'
a0a5168 @mislav mini app to display popular/per-user photos
authored
8 require 'instagram'
9 require 'haml'
49e8b6b @mislav improve styles with compass
authored
10 require 'sass'
11 require 'compass'
12
13 Compass.configuration do |config|
14 config.project_path = settings.root
15 config.sass_dir = 'views'
16 end
a0a5168 @mislav mini app to display popular/per-user photos
authored
17
18 set :haml, format: :html5
49e8b6b @mislav improve styles with compass
authored
19 set :scss, Compass.sass_engine_options.merge(cache_location: File.join(ENV['TMPDIR'], 'sass-cache'))
a0a5168 @mislav mini app to display popular/per-user photos
authored
20
21 set(:cache_dir) { File.join(ENV['TMPDIR'], 'cache') }
22
23 module CachedInstagram
24 extend Instagram
5287e5b @mislav reuse stale cache in case of Instagram downtime
authored
25
26 class FailSafeStore < ActiveSupport::Cache::FileStore
27 # Reuses the stale cache if a known exception occurs while yielding to the block.
28 # The list of exception classes is read from the ":exceptions" array.
29 def fetch(name, options = nil)
30 options = merged_options(options)
31 key = namespaced_key(name, options)
32 entry = !options[:force] && read_entry(key, options)
33
34 if entry and not entry.expired?
35 entry.value
36 else
37 reusing_stale = false
38
39 result = begin
40 yield
41 rescue
42 if entry and ignore_exception?($!)
43 reusing_stale = true
44 entry.value
45 else
46 # TODO: figure out if deleting entries is ever necessary
47 # delete_entry(key, options) if entry
48 raise
49 end
50 end
51
52 write(name, result, options) unless reusing_stale
53 result
54 end
55 end
56
57 private
58
59 def ignore_exception?(ex)
60 options[:exceptions] && options[:exceptions].any? { |klass| ex.is_a? klass }
61 end
62 end
a0a5168 @mislav mini app to display popular/per-user photos
authored
63
64 class << self
5287e5b @mislav reuse stale cache in case of Instagram downtime
authored
65 attr_accessor :cache
66
eea1cdc @mislav help page + user ID discovery
authored
67 def discover_user_id(url)
68 url = Addressable::URI.parse url unless url.respond_to? :host
69 $1.to_i if get_url(url) =~ %r{profiles/profile_(\d+)_}
70 end
71
a0a5168 @mislav mini app to display popular/per-user photos
authored
72 private
73 def get_url(url)
5287e5b @mislav reuse stale cache in case of Instagram downtime
authored
74 cache.fetch(url.to_s) { super }
a0a5168 @mislav mini app to display popular/per-user photos
authored
75 end
76 end
5287e5b @mislav reuse stale cache in case of Instagram downtime
authored
77
78 self.cache = FailSafeStore.new settings.cache_dir, namespace: 'instagram',
79 expires_in: 3.minutes, exceptions: [Net::HTTPServerException, JSON::ParserError]
a0a5168 @mislav mini app to display popular/per-user photos
authored
80 end
81
82 helpers do
83 def img(photo, size)
84 haml_tag :img, src: photo.image_url(size), width: size, height: size
85 end
da36b34 @mislav link back to instagr.am
authored
86
87 def instalink(text)
88 text.sub(/\b(instagram)\b/i, '<a href="http://instagr.am">\1</a>')
89 end
249322a @mislav pagination
authored
90
91 def xhr?
92 !(request.env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
93 end
a006a7e @mislav user's atom feed
authored
94
ab8a0e1 @mislav serve JSONP at "/users/42.json?_callback=foo"
authored
95 def user_photos(params, raw = false)
a006a7e @mislav user's atom feed
authored
96 feed_params = params[:max_id] ? { max_id: params[:max_id].to_s } : {}
ab8a0e1 @mislav serve JSONP at "/users/42.json?_callback=foo"
authored
97 options = raw ? { parse_with: nil } : {}
98 CachedInstagram::by_user(params[:id], feed_params, options)
a006a7e @mislav user's atom feed
authored
99 end
d982194 @mislav add atom feed for popular items
authored
100
101 def user_url(user)
102 absolute_url "/users/#{user.id}"
103 end
104
105 def absolute_url(path)
106 abs_uri = "#{request.scheme}://#{request.host}"
107
108 if request.scheme == 'https' && request.port != 443 ||
109 request.scheme == 'http' && request.port != 80
110 abs_uri << ":#{request.port}"
111 end
112
113 abs_uri << path
114 end
a0a5168 @mislav mini app to display popular/per-user photos
authored
115 end
116
117 get '/' do
118 @photos = CachedInstagram::popular
d982194 @mislav add atom feed for popular items
authored
119 @title = "Instagram popular photos"
a0a5168 @mislav mini app to display popular/per-user photos
authored
120
8efb5fa @mislav shorten cache periods
authored
121 expires 5.minutes, :public
a0a5168 @mislav mini app to display popular/per-user photos
authored
122 haml :index
123 end
124
d982194 @mislav add atom feed for popular items
authored
125 get '/popular.atom' do
126 @photos = CachedInstagram::popular
127 @title = "Instagram popular photos"
128
129 content_type 'application/atom+xml', charset: 'utf-8'
130 expires 15.minutes, :public
131 last_modified @photos.first.taken_at if @photos.any?
132 builder :feed, layout: false
133 end
134
a006a7e @mislav user's atom feed
authored
135 get '/users/:id.atom' do
136 @photos = user_photos params
137 @title = "Photos by #{@photos.first.user.username} on Instagram" if @photos.any?
138
49e8b6b @mislav improve styles with compass
authored
139 content_type 'application/atom+xml', charset: 'utf-8'
8efb5fa @mislav shorten cache periods
authored
140 expires 15.minutes, :public
81f94f0 @mislav more HTTP caching headers
authored
141 last_modified @photos.first.taken_at if @photos.any?
ab8a0e1 @mislav serve JSONP at "/users/42.json?_callback=foo"
authored
142 builder :feed, layout: false
143 end
144
145 get '/users/:id.json' do
146 callback = params.delete('_callback')
147 raw_json = user_photos(params, true)
148
81f94f0 @mislav more HTTP caching headers
authored
149 content_type "application/#{callback ? 'javascript' : 'json'}", charset: 'utf-8'
ab8a0e1 @mislav serve JSONP at "/users/42.json?_callback=foo"
authored
150 expires 15.minutes, :public
81f94f0 @mislav more HTTP caching headers
authored
151 etag Digest::MD5.hexdigest(raw_json)
ab8a0e1 @mislav serve JSONP at "/users/42.json?_callback=foo"
authored
152
153 if callback
154 "#{callback}(#{raw_json.strip})"
155 else
156 raw_json
157 end
a006a7e @mislav user's atom feed
authored
158 end
159
a0a5168 @mislav mini app to display popular/per-user photos
authored
160 get '/users/:id' do
d09698d @mislav more error handling; correct HTTP response codes
authored
161 begin
162 @photos = user_photos params
163 unless xhr?
164 @user = CachedInstagram::user_info params[:id]
165 @title = "Photos by #{@user.username} on Instagram"
166 end
249322a @mislav pagination
authored
167
d09698d @mislav more error handling; correct HTTP response codes
authored
168 expires 5.minutes, :public
81f94f0 @mislav more HTTP caching headers
authored
169 last_modified @photos.first.taken_at if @photos.any?
d09698d @mislav more error handling; correct HTTP response codes
authored
170 haml(xhr? ? :photos : :index)
171 rescue Net::HTTPServerException => e
172 if 404 == e.response.code.to_i
173 status 404
174 haml "%h1 No such user\n%p Instagram couldn't resolve this user ID"
175 else
176 status 500
177 haml "%h1 Error fetching user\n%p The user ID couldn't be discovered because of an error"
178 end
179 end
a0a5168 @mislav mini app to display popular/per-user photos
authored
180 end
181
eea1cdc @mislav help page + user ID discovery
authored
182 get '/help' do
183 @title = "Help page"
184 expires 1.hour, :public
185 haml :help
186 end
187
188 post '/users/discover' do
189 begin
190 user_id = CachedInstagram::discover_user_id(params[:url])
191
192 if user_id
193 redirect "/users/#{user_id}"
194 else
d09698d @mislav more error handling; correct HTTP response codes
authored
195 status 500
eea1cdc @mislav help page + user ID discovery
authored
196 haml "%h1 Sorry\n%p The user ID couldn't be discovered on this page"
197 end
198 rescue
d09698d @mislav more error handling; correct HTTP response codes
authored
199 status 500
eea1cdc @mislav help page + user ID discovery
authored
200 haml "%h1 Error\n%p The user ID couldn't be discovered because of an error"
201 end
202 end
203
a0a5168 @mislav mini app to display popular/per-user photos
authored
204 get '/screen.css' do
205 expires 6.hours, :public
206 scss :style
207 end
208
209 __END__
210 @@ layout
211 !!!
212 %title&= @title
213 %meta{ 'http-equiv' => 'content-type', content: 'text/html; charset=utf-8' }
214 %link{ href: "/screen.css", rel: "stylesheet" }
a006a7e @mislav user's atom feed
authored
215 - if @user
216 %link{ href: "#{request.path}.atom", rel: 'alternate', title: "#{@user.username}'s photos", type: 'application/atom+xml' }
d982194 @mislav add atom feed for popular items
authored
217 - elsif request.path == '/'
218 %link{ href: "/popular.atom", rel: 'alternate', title: @title, type: 'application/atom+xml' }
a0a5168 @mislav mini app to display popular/per-user photos
authored
219 %script{ src: "/zepto.min.js" }
220
221 = yield
222
223 @@ index
224 %header
225 %h1
226 - if @user
227 %img{ src: @user.avatar, class: 'avatar' }
da36b34 @mislav link back to instagr.am
authored
228 = instalink @title
d982194 @mislav add atom feed for popular items
authored
229 - if request.path == '/'
230 %a{ href: "/popular.atom", class: 'feed' }
231 %img{ src: '/feed.png', alt: 'feed' }
a0a5168 @mislav mini app to display popular/per-user photos
authored
232 - if @user
233 %p.stats
234 &= @user.full_name
235 &#8226;
236 = @user.followers
237 followers
0bc4b70 @mislav expose the Atom feed on user page
authored
238 &#8226;
239 %a{ href: "#{request.path}.atom", class: 'feed' }
240 %span photo feed
241 %img{ src: '/feed.png', alt: '' }
a0a5168 @mislav mini app to display popular/per-user photos
authored
242
243 %ol#photos
249322a @mislav pagination
authored
244 = haml :photos
a0a5168 @mislav mini app to display popular/per-user photos
authored
245
246 %footer
247 %p
d588bb2 @mislav link home
authored
248 - if @user
249 &larr; <a href="/">Home</a> &#8226;
eea1cdc @mislav help page + user ID discovery
authored
250 <a href="/help">Help</a> &#8226;
da36b34 @mislav link back to instagr.am
authored
251 App made by <a href="http://twitter.com/mislav">@mislav</a>
252 (<a href="/users/35241" title="Mislav's photos">photos</a>)
a0a5168 @mislav mini app to display popular/per-user photos
authored
253 using <a href="https://github.com/mislav/instagram">Instagram Ruby client</a>
254
255 :javascript
256 $('#photos a.thumb').live('click', function(e) {
257 e.preventDefault()
258 $('#photos').addClass('lightbox')
259 var item = $(this).closest('li').addClass('active')
260 item.find('.full img').attr('src', $(this).attr('href'))
261 })
262
263 $('#photos a[href="#close"], #photos .full img').live('click', function(e) {
264 e.preventDefault()
265 $(this).closest('li').removeClass('active')
266 $('#photos').removeClass('lightbox')
267 })
249322a @mislav pagination
authored
268
269 $('#photos .pagination a').live('click', function(e) {
270 e.preventDefault()
49e8b6b @mislav improve styles with compass
authored
271 $(this).find('span').text('Loading...')
249322a @mislav pagination
authored
272 var item = $(this).closest('.pagination')
273 $.get($(this).attr('href'), function(body) {
274 item.remove()
275 try { $('#photos').append(body) }
276 catch(e) { $('#photos').get(0).innerHTML += body } // for mozilla
277 })
278 })
279
280 @@ photos
281 - for photo in @photos
282 %li
283 %a{ href: photo.image_url(612), class: 'thumb' }
284 - img(photo, 150)
285 .full{ style: 'display:none' }
286 %img{ width: 480, height: 480 }
287 %h2= photo.caption
288 .author
289 by
290 %a{ href: "/users/#{photo.user.id}" }&= photo.user.full_name
291 .close
292 %a{ href: "#close" } close
293 - if @user and @photos.length >= 20
294 %li.pagination
49e8b6b @mislav improve styles with compass
authored
295 %a{ href: request.path + "?max_id=#{@photos.last.id}" } <span>Load more &rarr;</span>
a0a5168 @mislav mini app to display popular/per-user photos
authored
296
eea1cdc @mislav help page + user ID discovery
authored
297 @@ help
298 %article
299 %h1= @title
300 %nav
301 &larr; <a href="/">Home</a>
302
303 %section
304 %h2 What's this site?
305
306 %p This site is the unofficial Instagram front-end on the Web made by querying the <strong>public resources</strong> of the <a href="https://github.com/mislav/instagram/wiki">Instagram API</a>.
307
308 %section
309 %h2 How do I discover my own Instagram photos?
310
311 %p Unfortunately, it isn't straightforward. To fetch your photos this site has to know your user ID, and Instagram doesn't have a method to lookup your ID from your username. Their API has search functionality, but it requires authentication.
312
313 %p There is a way, however. If you have a permalink to one of your photos (for instance, if you setup Instagram to tweet your photo) paste the URL here and your user ID can be detected:
314
315 %form{ action: '/users/discover', method: 'post' }
316 %p
317 %label
318 Instagr.am permalink:
319 %input{ type: 'url', name: 'url', placeholder: 'http://instagr.am/p/..../' }
320 %input{ type: 'submit', value: 'Miracle!' }
321
322 %section
323 %h2 What about geolocated photos?
324
325 %p Some photos have location information, but it isn't visible right now. I might add this functionality.
326
327 %section
328 %h2 Why doesn't Instagram have a real website?
329
330 %p They mentioned that their public site is coming out soon.
331
a006a7e @mislav user's atom feed
authored
332 @@ feed
333 schema_date = 2010
d982194 @mislav add atom feed for popular items
authored
334 popular = request.path.include? 'popular'
a006a7e @mislav user's atom feed
authored
335
4a80fdf @mislav fix atom feed
authored
336 xml.feed "xml:lang" => "en-US", xmlns: 'http://www.w3.org/2005/Atom' do
337 xml.id "tag:#{request.host},#{schema_date}:#{request.path.split(".")[0]}"
d982194 @mislav add atom feed for popular items
authored
338 xml.link rel: 'alternate', type: 'text/html', href: request.url.split(popular ? 'popular' : '.')[0]
4a80fdf @mislav fix atom feed
authored
339 xml.link rel: 'self', type: 'application/atom+xml', href: request.url
a006a7e @mislav user's atom feed
authored
340
341 xml.title @title
4a80fdf @mislav fix atom feed
authored
342
343 if @photos.any?
344 xml.updated @photos.first.taken_at.xmlschema
d982194 @mislav add atom feed for popular items
authored
345 xml.author { xml.name @photos.first.user.full_name } unless popular
4a80fdf @mislav fix atom feed
authored
346 end
a006a7e @mislav user's atom feed
authored
347
348 for photo in @photos
349 xml.entry do
350 xml.title photo.caption || 'Photo'
4a80fdf @mislav fix atom feed
authored
351 xml.id "tag:#{request.host},#{schema_date}:Instagram::Media/#{photo.id}"
352 xml.published photo.taken_at.xmlschema
d982194 @mislav add atom feed for popular items
authored
353
354 if popular
355 xml.link rel: 'alternate', type: 'text/html', href: user_url(photo.user)
356 xml.author { xml.name photo.user.full_name }
357 end
358
4a80fdf @mislav fix atom feed
authored
359 xml.content type: 'xhtml' do |content|
360 content.div xmlns: "http://www.w3.org/1999/xhtml" do
361 content.img src: photo.image_url(306), width: 306, height: 306, alt: photo.caption
d982194 @mislav add atom feed for popular items
authored
362 content.p "#{photo.likers.size} likes" if popular and not photo.likers.empty?
4a80fdf @mislav fix atom feed
authored
363 end
a006a7e @mislav user's atom feed
authored
364 end
365 end
366 end
367 end
368
a0a5168 @mislav mini app to display popular/per-user photos
authored
369 @@ style
49e8b6b @mislav improve styles with compass
authored
370 @import "compass/utilities";
371 @import "compass/css3/text-shadow";
372 @import "compass/css3/border-radius";
373
a0a5168 @mislav mini app to display popular/per-user photos
authored
374 body {
375 font: medium Helvetica, sans-serif;
376 margin: 2em 4em;
377 }
378 h1, h2, h3 {
379 font-family: "Myriad Pro Condensed", "Gill Sans", "Lucida Grande", Helvetica, sans-serif;
380 font-weight: 100;
381 }
0bc4b70 @mislav expose the Atom feed on user page
authored
382 a:link, a:visited { color: darkblue }
383 a:hover, a:active { color: firebrick }
a0a5168 @mislav mini app to display popular/per-user photos
authored
384
385 img { border: none }
da36b34 @mislav link back to instagr.am
authored
386 h1 {
387 color: #333;
388 img.avatar { width: 30px; height: 30px }
0bc4b70 @mislav expose the Atom feed on user page
authored
389 a:link, a:hover, a:active, a:visited { color: #555; font-weight: 400; text-decoration: none }
da36b34 @mislav link back to instagr.am
authored
390 a:hover { text-decoration: underline }
391 }
0bc4b70 @mislav expose the Atom feed on user page
authored
392 p.stats {
393 margin-top: -1.1em;
394 font-style: italic; font-size: 90%;
395 color: gray;
396 a.feed {
397 text-decoration: none;
398 &:link, &:visited { color: inherit; }
399 span { text-decoration: underline }
400 img { vertical-align: middle }
401 }
402 }
eea1cdc @mislav help page + user ID discovery
authored
403 article {
404 h1 + nav { margin-top: -1.1em; font-size: 90%; }
405 max-width: 40em;
406 label { font-weight: bold }
407 input[type=url] { font-size: 1.1em; width: 15em }
408 }
a0a5168 @mislav mini app to display popular/per-user photos
authored
409
410 #photos {
411 list-style: none;
412 padding: 0; margin: 0;
49e8b6b @mislav improve styles with compass
authored
413 @include clearfix;
a0a5168 @mislav mini app to display popular/per-user photos
authored
414 li {
415 display: inline;
49e8b6b @mislav improve styles with compass
authored
416 .thumb img { display: block; float: left; margin: 0 3px 3px 0 }
a0a5168 @mislav mini app to display popular/per-user photos
authored
417 &.active {
418 .thumb { display: none }
419 .full {
420 display: block !important;
421 padding: 15px;
422 color: #F7F4E9;
423 a { color: white }
424 .close { margin-top: -1.15em; text-align: right; width: 480px }
425 }
426 }
249322a @mislav pagination
authored
427 &.pagination {
428 a {
49e8b6b @mislav improve styles with compass
authored
429 display: block; height: 20px; padding: 65px 0; width: 150px;
430 text-align: center; float: left;
431 font-size: 80%; text-decoration: none;
432 span {
433 padding: .2em .7em .3em;
434 color: white; background: #bbb;
435 @include text-shadow(rgba(black, .4));
436 @include border-radius(16px);
437 white-space: nowrap;
438 }
439 }
440 a:hover {
441 background-color: #eee;
442 span { background-color: #999; }
249322a @mislav pagination
authored
443 }
444 }
a0a5168 @mislav mini app to display popular/per-user photos
authored
445 h2 { font-size: 1.2em; margin: .5em 0; }
446 }
447 &.lightbox {
448 background: #3F3831;
449 li { display: none }
450 li.active { display: block }
451 }
452 }
453
454 footer {
455 font-size: 80%;
456 color: gray;
eea1cdc @mislav help page + user ID discovery
authored
457 max-width: 45em;
a0a5168 @mislav mini app to display popular/per-user photos
authored
458 margin: 2em auto;
459 border-top: 1px solid silver;
460 p { text-align: center; text-transform: uppercase; font-family: "Gill Sans", Helvetica, sans-serif; }
0bc4b70 @mislav expose the Atom feed on user page
authored
461 a:link, a:hover, a:active, a:visited { color: #444 }
a0a5168 @mislav mini app to display popular/per-user photos
authored
462 }
Something went wrong with that request. Please try again.