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.
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