aviary 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/Rakefile +10 -0
- data/aviary.gemspec +32 -0
- data/bin/aviary +87 -0
- data/generator/_assets/aviary.css +95 -0
- data/generator/_assets/aviary.js +38 -0
- data/generator/_assets/loader.gif +0 -0
- data/generator/template.erb +45 -0
- data/lib/aviary.rb +28 -0
- data/lib/aviary/configuration.rb +36 -0
- data/lib/aviary/generator.rb +40 -0
- data/lib/aviary/image_host.rb +74 -0
- data/lib/aviary/image_host/flickr.rb +53 -0
- data/lib/aviary/image_host/plixi.rb +27 -0
- data/lib/aviary/image_host/twitpic.rb +13 -0
- data/lib/aviary/image_host/yfrog.rb +13 -0
- data/lib/aviary/page.rb +26 -0
- data/lib/aviary/paginator.rb +54 -0
- data/lib/aviary/search.rb +34 -0
- data/lib/aviary/site.rb +52 -0
- data/lib/aviary/version.rb +3 -0
- data/test/aviary/configuration_test.rb +30 -0
- data/test/aviary/generator_test.rb +33 -0
- data/test/aviary/image_host/flickr_test.rb +60 -0
- data/test/aviary/image_host/plixi_test.rb +30 -0
- data/test/aviary/image_host/twitpic_test.rb +15 -0
- data/test/aviary/image_host/yfrog_test.rb +15 -0
- data/test/aviary/image_host_test.rb +65 -0
- data/test/aviary/page_test.rb +29 -0
- data/test/aviary/paginator_test.rb +48 -0
- data/test/aviary/search_test.rb +43 -0
- data/test/aviary/site_test.rb +66 -0
- data/test/fixtures/flickr.xml +19 -0
- data/test/fixtures/plixi.xml +1 -0
- data/test/fixtures/source/_assets/static +0 -0
- data/test/fixtures/source/_assets/subdir/static +0 -0
- data/test/fixtures/source/template.erb +5 -0
- data/test/fixtures/twitter.json +1 -0
- data/test/helper.rb +9 -0
- metadata +271 -0
data/lib/aviary.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Standard library
|
2
|
+
require 'erb'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
# Third pary
|
7
|
+
require 'rubygems'
|
8
|
+
require 'base58'
|
9
|
+
require 'dm-core'
|
10
|
+
require 'dm-sqlite-adapter'
|
11
|
+
require 'dm-migrations'
|
12
|
+
require 'dm-validations'
|
13
|
+
require 'nokogiri'
|
14
|
+
require 'twitter'
|
15
|
+
|
16
|
+
# Internal
|
17
|
+
require 'aviary/configuration'
|
18
|
+
require 'aviary/generator'
|
19
|
+
require 'aviary/image_host'
|
20
|
+
require 'aviary/image_host/flickr'
|
21
|
+
require 'aviary/image_host/plixi'
|
22
|
+
require 'aviary/image_host/twitpic'
|
23
|
+
require 'aviary/image_host/yfrog'
|
24
|
+
require 'aviary/page'
|
25
|
+
require 'aviary/paginator'
|
26
|
+
require 'aviary/search'
|
27
|
+
require 'aviary/site'
|
28
|
+
require 'aviary/version'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Configuration
|
3
|
+
def initialize(envrionment, config = {})
|
4
|
+
@config = {}
|
5
|
+
@config[:source] = config[:source] || Dir.pwd
|
6
|
+
@config[:dest] = config[:dest] || File.join(@config[:source], '_site')
|
7
|
+
@config[:hashtag] = config[:hashtag]
|
8
|
+
@config[:per_page] = config[:per_page]
|
9
|
+
@config[:limit] = config[:limit]
|
10
|
+
|
11
|
+
ImageHost::Flickr.api_key(config[:flickr_api_key])
|
12
|
+
|
13
|
+
send(envrionment)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the value for the given key.
|
17
|
+
#
|
18
|
+
# Returns value.
|
19
|
+
def [](key)
|
20
|
+
@config[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def default
|
26
|
+
DataMapper.setup(:default, "sqlite://#{File.join(self[:source], 'db.sqlite3')}")
|
27
|
+
DataMapper.finalize
|
28
|
+
end
|
29
|
+
|
30
|
+
def test
|
31
|
+
DataMapper.setup(:default, 'sqlite::memory:')
|
32
|
+
DataMapper.finalize
|
33
|
+
DataMapper.auto_migrate!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Generator
|
3
|
+
attr_reader :source, :hashtag
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@source = config[:source]
|
7
|
+
@hashtag = config[:hashtag]
|
8
|
+
end
|
9
|
+
|
10
|
+
def process
|
11
|
+
copy_template
|
12
|
+
migrate
|
13
|
+
end
|
14
|
+
|
15
|
+
# Migrates the database for the first time.
|
16
|
+
#
|
17
|
+
# Returns nothing.
|
18
|
+
def migrate
|
19
|
+
DataMapper.auto_migrate!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Copies the contents of the +generator+ directory into
|
23
|
+
# the +source+ directory for setting up a new aviary.
|
24
|
+
#
|
25
|
+
# Returns nothing.
|
26
|
+
def copy_template
|
27
|
+
FileUtils.mkdir_p(self.source) unless File.exists?(self.source)
|
28
|
+
File.open(File.join(self.source, 'template.erb'), 'w') do |file|
|
29
|
+
erb = File.read(File.join(generator_path, 'template.erb'))
|
30
|
+
erb.gsub!('{{hashtag}}', self.hashtag) if self.hashtag
|
31
|
+
file.write(erb)
|
32
|
+
end
|
33
|
+
FileUtils.cp_r File.join(generator_path, '_assets'), self.source
|
34
|
+
end
|
35
|
+
|
36
|
+
def generator_path
|
37
|
+
File.join(File.dirname(__FILE__), '..', '..', 'generator')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Aviary
|
2
|
+
class ImageHost
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
property :type, Discriminator
|
7
|
+
|
8
|
+
# Unique identifier to the photo
|
9
|
+
property :token, String, :unique => true, :auto_validation => true
|
10
|
+
|
11
|
+
# Twitter status
|
12
|
+
property :status, Object
|
13
|
+
|
14
|
+
# Store additional data for building +href+ and +src+
|
15
|
+
property :meta, Object
|
16
|
+
|
17
|
+
# Get descendants which are available for searching. Descendant
|
18
|
+
# classes should implement this method if they require additional
|
19
|
+
# configuration before they are available. Eg: ImageHost::Flickr
|
20
|
+
# requires an API key to be set.
|
21
|
+
#
|
22
|
+
# Returns array of descendants.
|
23
|
+
def self.available
|
24
|
+
@available ||= descendants.select { |d| d.available? }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.available?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set and get regular expressions for matching against links to image
|
32
|
+
# hosts. Passing an argument appends the it to the array. Passing nothing
|
33
|
+
# gets the regular expressions.
|
34
|
+
#
|
35
|
+
# Returns array of regular expressions.
|
36
|
+
def self.matches(regex = nil)
|
37
|
+
@matches = (@matches || []) << regex if regex
|
38
|
+
@matches
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build an array of captures from matching text. Will capture multiple
|
42
|
+
# times. An empty array means there were no captures.
|
43
|
+
#
|
44
|
+
# Returns array of captured strings.
|
45
|
+
def self.match(text)
|
46
|
+
text.scan(Regexp.union(matches)).flatten.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
# Like match, but instead of returning an array of captures strings
|
50
|
+
# it creates new records with the captured token. Status is an object
|
51
|
+
# which has a text attribute returned by Twitter.
|
52
|
+
#
|
53
|
+
# Returns nothing.
|
54
|
+
def self.match_and_create(status)
|
55
|
+
match(status.text).each do |capture|
|
56
|
+
create(:token => capture, :status => status)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Link to the original photo. Descendant classes must implement this method.
|
61
|
+
#
|
62
|
+
# Raises exception until implemented.
|
63
|
+
def href
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# Link to the image. Descendant classes must implement this method.
|
68
|
+
#
|
69
|
+
# Raises exception until implemented.
|
70
|
+
def src
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Aviary
|
2
|
+
class ImageHost::Flickr < ImageHost
|
3
|
+
before :create, :set_meta
|
4
|
+
|
5
|
+
matches /flic\.kr\/p\/(\w+)/
|
6
|
+
matches /flickr\.com\/photos\/\w+\/(\d+)/
|
7
|
+
|
8
|
+
# Get and set the API key. Passing an argument sets the
|
9
|
+
# API key. Passing nothing gets the API key.
|
10
|
+
#
|
11
|
+
# Returns api key.
|
12
|
+
def self.api_key(key = nil)
|
13
|
+
@api_key = key if key
|
14
|
+
@api_key
|
15
|
+
end
|
16
|
+
|
17
|
+
# True if the API key has been set.
|
18
|
+
#
|
19
|
+
# Returns boolean.
|
20
|
+
def self.available?
|
21
|
+
!api_key.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.match_and_create(status)
|
25
|
+
match(status.text).each do |capture|
|
26
|
+
create :token => (capture =~ /^\d+$/ ? Base58.encode(capture.to_i) : capture),
|
27
|
+
:status => status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def href
|
32
|
+
"http://flic.kr/p/#{self.token}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def src
|
36
|
+
"http://farm#{self.meta[:farm_id]}.static.flickr.com/" +
|
37
|
+
"#{self.meta[:server_id]}/#{self.meta[:id]}_#{self.meta[:secret]}_z.jpg"
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_meta
|
41
|
+
uri = URI.parse "http://api.flickr.com/services/rest/?method=flickr" +
|
42
|
+
".photos.getInfo&api_key=#{self.class.api_key}" +
|
43
|
+
"&photo_id=#{Base58.decode(self.token)}"
|
44
|
+
photo = Nokogiri::XML(open(uri)).css('photo')
|
45
|
+
self.meta = {
|
46
|
+
:farm_id => photo.first['farm'],
|
47
|
+
:server_id => photo.first['server'],
|
48
|
+
:id => photo.first['id'],
|
49
|
+
:secret => photo.first['secret']
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Aviary
|
2
|
+
class ImageHost::Plixi < ImageHost
|
3
|
+
before :create, :set_meta
|
4
|
+
|
5
|
+
matches /plixi\.com\/p\/(\d+)/
|
6
|
+
|
7
|
+
def href
|
8
|
+
"http://plixi.com/p/#{self.token}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def src
|
12
|
+
self.meta[:medium_image_url]
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_meta
|
16
|
+
uri = URI.parse("http://api.plixi.com/api/tpapi.svc/photos/#{self.token}")
|
17
|
+
doc = Nokogiri::XML(open(uri))
|
18
|
+
self.meta = {
|
19
|
+
:big_image_url => doc.css('BigImageUrl').text,
|
20
|
+
:large_image_url => doc.css('LargeImageUrl').text,
|
21
|
+
:medium_image_url => doc.css('MediumImageUrl').text,
|
22
|
+
:small_image_url => doc.css('SmallImageUrl').text,
|
23
|
+
:thumbnail_image_url => doc.css('ThumbnailUrl').text
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/aviary/page.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Page
|
3
|
+
# Photos for the current page
|
4
|
+
attr_reader :image_hosts
|
5
|
+
|
6
|
+
# Next page, previous page and current page
|
7
|
+
attr_reader :paginator
|
8
|
+
|
9
|
+
def initialize(paginator)
|
10
|
+
@paginator = paginator
|
11
|
+
@image_hosts = ImageHost.all(@paginator.query_options.merge(:order => :id.desc))
|
12
|
+
end
|
13
|
+
|
14
|
+
# Escapes text by replacing < and > with their HTML character entity
|
15
|
+
# reference.
|
16
|
+
#
|
17
|
+
# Returns escaped string.
|
18
|
+
def h(string)
|
19
|
+
string.gsub('<', '<').gsub('>', '>')
|
20
|
+
end
|
21
|
+
|
22
|
+
def binding
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Paginator
|
3
|
+
attr_reader :per_page # Number of photos per page
|
4
|
+
attr_reader :current_page # Number for the current page
|
5
|
+
attr_reader :first_page # Minimum number of pages
|
6
|
+
attr_reader :last_page # Maximum number of pages
|
7
|
+
|
8
|
+
def initialize(per_page)
|
9
|
+
@per_page = per_page
|
10
|
+
@current_page = 1
|
11
|
+
@first_page = 1
|
12
|
+
@last_page = ImageHost.count / self.per_page
|
13
|
+
end
|
14
|
+
|
15
|
+
# True if this is not the last page.
|
16
|
+
#
|
17
|
+
# Returns boolean.
|
18
|
+
def next_page?
|
19
|
+
self.current_page < self.last_page
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the page number for the next page.
|
23
|
+
#
|
24
|
+
# Returns page number.
|
25
|
+
def next_page
|
26
|
+
self.current_page + 1
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the page number for the next page and increment the +current_page+.
|
30
|
+
#
|
31
|
+
# Returns page number.
|
32
|
+
def next_page!
|
33
|
+
@current_page = next_page
|
34
|
+
end
|
35
|
+
|
36
|
+
# True if this is not the first page.
|
37
|
+
#
|
38
|
+
# Returns boolean.
|
39
|
+
def prev_page?
|
40
|
+
self.current_page > self.first_page
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get the page number for the previous page.
|
44
|
+
#
|
45
|
+
# Returns page number.
|
46
|
+
def prev_page
|
47
|
+
self.current_page - 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def query_options
|
51
|
+
{:limit => self.per_page, :offset => self.per_page * (self.current_page - 1)}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Search
|
3
|
+
attr_reader :twitter, :limit, :current_page
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@twitter = Twitter::Search.new.filter('links').no_retweets.per_page(100).hashtag(config[:hashtag])
|
7
|
+
@limit = config[:limit] || 50
|
8
|
+
@current_page = 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def process
|
12
|
+
return unless next_page?
|
13
|
+
self.twitter.each do |status|
|
14
|
+
ImageHost.available.each do |image_host|
|
15
|
+
image_host.match_and_create(status)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
next_page!
|
19
|
+
process
|
20
|
+
end
|
21
|
+
|
22
|
+
# True if there is another page to fetch from Twitter and
|
23
|
+
# we haven't exceeded the limit.
|
24
|
+
#
|
25
|
+
# Returns boolean.
|
26
|
+
def next_page?
|
27
|
+
self.twitter.next_page? && self.current_page < self.limit
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_page!
|
31
|
+
@current_page += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/aviary/site.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Aviary
|
2
|
+
class Site
|
3
|
+
attr_reader :source, :dest, :paginator, :template
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@source = config[:source]
|
7
|
+
@dest = config[:dest]
|
8
|
+
@paginator = Paginator.new(config[:per_page] || 25)
|
9
|
+
@template = ERB.new(File.read(File.join(self.source, 'template.erb')))
|
10
|
+
end
|
11
|
+
|
12
|
+
def process
|
13
|
+
render
|
14
|
+
if self.paginator.next_page?
|
15
|
+
self.paginator.next_page!
|
16
|
+
process
|
17
|
+
else
|
18
|
+
copy_index
|
19
|
+
copy_assets
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def render
|
24
|
+
FileUtils.mkdir_p(current_page_path) unless File.exists?(current_page_path)
|
25
|
+
File.open(File.join(current_page_path, "index.htm"), "w") do |file|
|
26
|
+
file.write self.template.result(Page.new(self.paginator).binding)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Copies the first page and makes it the index at the root
|
31
|
+
# of the +dest+ directory.
|
32
|
+
#
|
33
|
+
# Returns nothing.
|
34
|
+
def copy_index
|
35
|
+
FileUtils.cp File.join(self.dest, "page1", "index.htm"),
|
36
|
+
File.join(self.dest, "index.htm")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Recursively copy the contents of the +_assets+ directory into
|
40
|
+
# the root of the +dest+ directory. Useful for sharing CSS,
|
41
|
+
# JavaScript or images between pages.
|
42
|
+
#
|
43
|
+
# Returns nothing.
|
44
|
+
def copy_assets
|
45
|
+
FileUtils.cp_r File.join(self.source, '_assets', '.'), self.dest
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_page_path
|
49
|
+
File.join(self.dest, "page#{self.paginator.current_page}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|