aviary 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|