aviary 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +110 -0
  5. data/Rakefile +10 -0
  6. data/aviary.gemspec +32 -0
  7. data/bin/aviary +87 -0
  8. data/generator/_assets/aviary.css +95 -0
  9. data/generator/_assets/aviary.js +38 -0
  10. data/generator/_assets/loader.gif +0 -0
  11. data/generator/template.erb +45 -0
  12. data/lib/aviary.rb +28 -0
  13. data/lib/aviary/configuration.rb +36 -0
  14. data/lib/aviary/generator.rb +40 -0
  15. data/lib/aviary/image_host.rb +74 -0
  16. data/lib/aviary/image_host/flickr.rb +53 -0
  17. data/lib/aviary/image_host/plixi.rb +27 -0
  18. data/lib/aviary/image_host/twitpic.rb +13 -0
  19. data/lib/aviary/image_host/yfrog.rb +13 -0
  20. data/lib/aviary/page.rb +26 -0
  21. data/lib/aviary/paginator.rb +54 -0
  22. data/lib/aviary/search.rb +34 -0
  23. data/lib/aviary/site.rb +52 -0
  24. data/lib/aviary/version.rb +3 -0
  25. data/test/aviary/configuration_test.rb +30 -0
  26. data/test/aviary/generator_test.rb +33 -0
  27. data/test/aviary/image_host/flickr_test.rb +60 -0
  28. data/test/aviary/image_host/plixi_test.rb +30 -0
  29. data/test/aviary/image_host/twitpic_test.rb +15 -0
  30. data/test/aviary/image_host/yfrog_test.rb +15 -0
  31. data/test/aviary/image_host_test.rb +65 -0
  32. data/test/aviary/page_test.rb +29 -0
  33. data/test/aviary/paginator_test.rb +48 -0
  34. data/test/aviary/search_test.rb +43 -0
  35. data/test/aviary/site_test.rb +66 -0
  36. data/test/fixtures/flickr.xml +19 -0
  37. data/test/fixtures/plixi.xml +1 -0
  38. data/test/fixtures/source/_assets/static +0 -0
  39. data/test/fixtures/source/_assets/subdir/static +0 -0
  40. data/test/fixtures/source/template.erb +5 -0
  41. data/test/fixtures/twitter.json +1 -0
  42. data/test/helper.rb +9 -0
  43. 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
@@ -0,0 +1,13 @@
1
+ module Aviary
2
+ class ImageHost::Twitpic < ImageHost
3
+ matches /twitpic\.com\/(\w+)/
4
+
5
+ def href
6
+ "http://twitpic.com/#{self.token}"
7
+ end
8
+
9
+ def src
10
+ "http://twitpic.com/show/large/#{self.token}.jpg"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Aviary
2
+ class ImageHost::Yfrog < ImageHost
3
+ matches /yfrog\.com\/(\w+)/
4
+
5
+ def href
6
+ "http://yfrog.com/#{self.token}"
7
+ end
8
+
9
+ def src
10
+ "http://yfrog.com/#{self.token}:medium"
11
+ end
12
+ end
13
+ end
@@ -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('<', '&lt;').gsub('>', '&gt;')
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
@@ -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