Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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