alula 0.2.3 → 0.4.0b

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 (182) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +7 -0
  3. data/Guardfile +9 -0
  4. data/Rakefile +12 -1
  5. data/VERSION +1 -1
  6. data/alula.gemspec +20 -4
  7. data/lib/alula/attachment_processor.rb +77 -0
  8. data/lib/alula/cdn.rb +30 -0
  9. data/lib/alula/cdns/edgecast.rb +16 -0
  10. data/lib/alula/cdns/hosts.rb +14 -0
  11. data/lib/alula/cli.rb +90 -39
  12. data/lib/alula/compressors.rb +22 -10
  13. data/lib/alula/config.rb +141 -0
  14. data/lib/alula/content.rb +113 -0
  15. data/lib/alula/contents/attachment.rb +8 -0
  16. data/lib/alula/contents/item.rb +409 -0
  17. data/lib/alula/contents/metadata.rb +73 -0
  18. data/lib/alula/contents/page.rb +9 -0
  19. data/lib/alula/contents/post.rb +32 -0
  20. data/lib/alula/context.rb +72 -0
  21. data/lib/alula/core_ext.rb +5 -0
  22. data/lib/alula/core_ext/environment.rb +20 -0
  23. data/lib/alula/core_ext/filter.rb +20 -0
  24. data/lib/alula/core_ext/filters/smilies.rb +36 -0
  25. data/lib/alula/core_ext/manifest.rb +30 -0
  26. data/lib/alula/core_ext/tag.rb +100 -0
  27. data/lib/alula/core_ext/tags/attachment.rb +28 -0
  28. data/lib/alula/core_ext/tags/blockquote.rb +21 -0
  29. data/lib/alula/core_ext/tags/image.rb +48 -0
  30. data/lib/alula/core_ext/tags/locale.rb +17 -0
  31. data/lib/alula/core_ext/tags/video.rb +103 -0
  32. data/lib/alula/generator.rb +31 -0
  33. data/lib/alula/generators/feedbuilder.rb +44 -0
  34. data/lib/alula/generators/paginate.rb +88 -0
  35. data/lib/alula/generators/sitemap.rb +26 -0
  36. data/lib/alula/helpers.rb +2 -0
  37. data/lib/alula/helpers/addons.rb +12 -0
  38. data/lib/alula/helpers/assets.rb +56 -0
  39. data/lib/alula/helpers/url_helpers.rb +16 -0
  40. data/lib/alula/plugin.rb +32 -0
  41. data/lib/alula/processor.rb +86 -0
  42. data/lib/alula/processors/dummy.rb +24 -0
  43. data/lib/alula/processors/image.rb +52 -0
  44. data/lib/alula/processors/magick.rb +83 -0
  45. data/lib/alula/processors/video.rb +97 -0
  46. data/lib/alula/processors/zencoder.rb +199 -0
  47. data/lib/alula/progress.rb +95 -0
  48. data/lib/alula/progressbar.rb +66 -0
  49. data/lib/alula/site.rb +331 -262
  50. data/lib/alula/storage.rb +46 -0
  51. data/lib/alula/storages/file_item.rb +43 -0
  52. data/lib/alula/storages/filestorage.rb +96 -0
  53. data/lib/alula/storages/item.rb +12 -0
  54. data/lib/alula/support/commonlogger.rb +30 -0
  55. data/lib/alula/theme.rb +70 -13
  56. data/lib/alula/theme/layout.rb +56 -0
  57. data/lib/alula/theme/view.rb +43 -0
  58. data/lib/alula/version.rb +1 -1
  59. data/locales/en.yml +9 -0
  60. data/locales/fi.yml +10 -0
  61. data/locales/l10n/ar.yml +199 -0
  62. data/locales/l10n/az.yml +199 -0
  63. data/locales/l10n/bg.yml +199 -0
  64. data/locales/l10n/bn-IN.yml +182 -0
  65. data/locales/l10n/bs.yml +242 -0
  66. data/locales/l10n/ca.yml +199 -0
  67. data/locales/l10n/cs.yml +198 -0
  68. data/locales/l10n/csb.yml +210 -0
  69. data/locales/l10n/cy.yml +199 -0
  70. data/locales/l10n/da.yml +199 -0
  71. data/locales/l10n/de-AT.yml +203 -0
  72. data/locales/l10n/de-CH.yml +203 -0
  73. data/locales/l10n/de.yml +203 -0
  74. data/locales/l10n/dsb.yml +215 -0
  75. data/locales/l10n/el.yml +199 -0
  76. data/locales/l10n/en-AU.yml +205 -0
  77. data/locales/l10n/en-CA.yml +214 -0
  78. data/locales/l10n/en-GB.yml +205 -0
  79. data/locales/l10n/en-IN.yml +205 -0
  80. data/locales/l10n/en-US.yml +205 -0
  81. data/locales/l10n/en.yml +205 -0
  82. data/locales/l10n/eo.yml +201 -0
  83. data/locales/l10n/es-AR.yml +205 -0
  84. data/locales/l10n/es-CL.yml +199 -0
  85. data/locales/l10n/es-CO.yml +205 -0
  86. data/locales/l10n/es-MX.yml +205 -0
  87. data/locales/l10n/es-PE.yml +181 -0
  88. data/locales/l10n/es-VE.yml +205 -0
  89. data/locales/l10n/es.yml +199 -0
  90. data/locales/l10n/et.yml +199 -0
  91. data/locales/l10n/eu.yml +199 -0
  92. data/locales/l10n/fa.yml +199 -0
  93. data/locales/l10n/fi.yml +199 -0
  94. data/locales/l10n/fr-CA.yml +207 -0
  95. data/locales/l10n/fr-CH.yml +207 -0
  96. data/locales/l10n/fr.yml +222 -0
  97. data/locales/l10n/fur.yml +199 -0
  98. data/locales/l10n/gl-ES.yml +178 -0
  99. data/locales/l10n/gsw-CH.yml +199 -0
  100. data/locales/l10n/he.yml +201 -0
  101. data/locales/l10n/hi-IN.yml +199 -0
  102. data/locales/l10n/hi.yml +199 -0
  103. data/locales/l10n/hr.yml +237 -0
  104. data/locales/l10n/hsb.yml +214 -0
  105. data/locales/l10n/hu.yml +199 -0
  106. data/locales/l10n/id.yml +200 -0
  107. data/locales/l10n/is.yml +213 -0
  108. data/locales/l10n/it.yml +205 -0
  109. data/locales/l10n/ja.yml +197 -0
  110. data/locales/l10n/kn.yml +199 -0
  111. data/locales/l10n/ko.yml +197 -0
  112. data/locales/l10n/lo.yml +186 -0
  113. data/locales/l10n/lt.yml +182 -0
  114. data/locales/l10n/lv.yml +215 -0
  115. data/locales/l10n/mk.yml +170 -0
  116. data/locales/l10n/mn.yml +205 -0
  117. data/locales/l10n/nb.yml +207 -0
  118. data/locales/l10n/nl.yml +199 -0
  119. data/locales/l10n/nn.yml +160 -0
  120. data/locales/l10n/pl.yml +221 -0
  121. data/locales/l10n/pt-BR.yml +207 -0
  122. data/locales/l10n/pt-PT.yml +207 -0
  123. data/locales/l10n/quotes.yml +24 -0
  124. data/locales/l10n/rm.yml +182 -0
  125. data/locales/l10n/ro.yml +199 -0
  126. data/locales/l10n/ru.yml +257 -0
  127. data/locales/l10n/sk.yml +213 -0
  128. data/locales/l10n/sl.yml +210 -0
  129. data/locales/l10n/sr-Latn.yml +170 -0
  130. data/locales/l10n/sr.yml +170 -0
  131. data/locales/l10n/sv-SE.yml +199 -0
  132. data/locales/l10n/sw.yml +197 -0
  133. data/locales/l10n/th.yml +173 -0
  134. data/locales/l10n/tl.yml +229 -0
  135. data/locales/l10n/tr.yml +199 -0
  136. data/locales/l10n/uk.yml +257 -0
  137. data/locales/l10n/vi.yml +201 -0
  138. data/locales/l10n/wo.yml +205 -0
  139. data/locales/l10n/zh-CN.yml +199 -0
  140. data/locales/l10n/zh-TW.yml +199 -0
  141. data/template/Gemfile.erb +14 -4
  142. data/template/README +16 -0
  143. data/template/config.yml.erb +42 -38
  144. data/test/fixtures/config_001_simple.yml +2 -0
  145. data/test/fixtures/config_002_l10n.yml +5 -0
  146. data/test/fixtures/pages/invalid-page.markdown +1 -0
  147. data/test/fixtures/pages/multilingual-page.markdown +20 -0
  148. data/test/fixtures/pages/section/subpage.markdown +5 -0
  149. data/test/fixtures/pages/simple-page.markdown +7 -0
  150. data/test/fixtures/posts/2012-07-02-invalid-post.markdown +1 -0
  151. data/test/fixtures/posts/2012-07-02-simple.markdown +7 -0
  152. data/test/fixtures/posts/2012-07-03-full-metadata.markdown +8 -0
  153. data/test/fixtures/posts/2012-07-03-multilingual-full-metadata.markdown +20 -0
  154. data/test/fixtures/theme/test/layouts/default.html.erb +1 -0
  155. data/test/fixtures/theme/test/views/page.html.erb +1 -0
  156. data/test/fixtures/theme/test/views/post.html.erb +1 -0
  157. data/test/minitest_helper.rb +14 -0
  158. data/test/test_config.rb +33 -0
  159. data/test/test_content.rb +30 -0
  160. data/test/test_metadata.rb +83 -0
  161. data/test/test_page.rb +81 -0
  162. data/test/test_post.rb +123 -0
  163. data/test/test_storage.rb +23 -0
  164. data/test/test_storage_file.rb +32 -0
  165. data/test/test_theme.rb +45 -0
  166. data/vendor/assets/images/favicon.png +0 -0
  167. data/vendor/assets/images/grey.gif +0 -0
  168. data/vendor/assets/javascripts/jquery.alula.js.coffee +16 -0
  169. data/vendor/{javascripts → assets/javascripts}/jquery.js +0 -0
  170. data/vendor/assets/javascripts/jquery.lazyload.js +210 -0
  171. data/vendor/assets/javascripts/lazyload.js.coffee +15 -0
  172. data/vendor/layouts/feed.xml.builder +19 -0
  173. data/vendor/layouts/sitemap.xml.builder +10 -0
  174. data/vendor/views/feed_post.html.haml +1 -0
  175. metadata +529 -50
  176. data/lib/alula.rb +0 -5
  177. data/lib/alula/assethelper.rb +0 -75
  178. data/lib/alula/plugins.rb +0 -23
  179. data/lib/alula/plugins/assets.rb +0 -82
  180. data/lib/alula/plugins/pagination.rb +0 -121
  181. data/lib/alula/rake_tasks.rb +0 -42
  182. data/lib/alula/tasks.rb +0 -2
@@ -0,0 +1,32 @@
1
+ require 'hashie/mash'
2
+
3
+ module Alula
4
+ class Plugin
5
+ def self.register(name, klass); plugins[name.to_s] = klass; end
6
+ def self.plugins; @@plugins ||= {}; end
7
+ def plugins; self.class.plugins; end
8
+
9
+ def self.load(name, options)
10
+ if plugins[name] and !(!!options == options and !options)
11
+ plugin = plugins[name]
12
+ return plugin if plugin.install(options || Hashie::Mash.new)
13
+ end
14
+ end
15
+
16
+ def self.addons; @@addons ||= Hash.new {|hash, key| hash[key] = []}; end
17
+ def self.addon(type, content_or_block); addons[type] << content_or_block; end
18
+
19
+ def self.script_load_mode=(mode)
20
+ @@script_load_mode = case mode
21
+ when :sync
22
+ :sync
23
+ when :defer
24
+ self.script_load_mode == :sync ? :sync : :defer
25
+ end
26
+ end
27
+
28
+ def self.script_load_mode
29
+ @@script_load_mode ||= :async
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,86 @@
1
+ require 'mimemagic'
2
+ require 'mini_exiftool'
3
+
4
+ module Alula
5
+ class Processor
6
+ attr_reader :item
7
+ attr_reader :options
8
+ attr_reader :site
9
+ attr_reader :attachments
10
+
11
+ def self.mimetype(*mimetypes)
12
+ (class << self; self; end).send(:define_method, "mimetypes") do
13
+ mimetypes.collect do |m|
14
+ if m.kind_of?(String)
15
+ Regexp.new("^#{m}$")
16
+ else
17
+ m
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.available?(options)
24
+ true
25
+ end
26
+
27
+ def self.process?(item, opts)
28
+ if self.respond_to?(:mimetypes)
29
+ mimetype = if opts[:fast]
30
+ MimeMagic.by_extension(item.extension)
31
+ else
32
+ MimeMagic.by_magic(File.open(item.filepath))
33
+ end
34
+
35
+ return !(self.mimetypes.select{|re| re.match(mimetype.type)}.empty?)
36
+ end
37
+
38
+ return false
39
+ end
40
+
41
+ def initialize(item, opts)
42
+ @item = item
43
+ @site = opts.delete(:site)
44
+ @attachments = opts.delete(:attachments)
45
+
46
+ @options = opts.delete(:options)
47
+
48
+ # Networked processors, create global 'queues' to prevent multiple simultanous upload/downloads
49
+ @@upload ||= Mutex.new
50
+ @@download ||= Mutex.new
51
+ end
52
+
53
+ def cleanup
54
+ @item = nil
55
+ end
56
+
57
+ def process
58
+ end
59
+
60
+ def asset_path(name, type)
61
+ asset_name = self.attachments.asset_name(name, type.to_s)
62
+ output = File.join(self.site.storage.path(:cache, "attachments"), asset_name)
63
+ # Make sure our directory exists
64
+ output_dir = self.site.storage.path(:cache, "attachments", File.dirname(asset_name))
65
+
66
+ output
67
+ end
68
+
69
+ def info
70
+ @info ||= begin
71
+ info = Dimensions.dimensions(self.item.filepath)
72
+ info ||= begin
73
+ _info = MiniExiftool.new self.item.filepath
74
+ [_info.imagewidth, _info.imageheight]
75
+ end
76
+ Hashie::Mash.new({
77
+ width: info[0],
78
+ height: info[1],
79
+ })
80
+ end
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+ Dir[File.dirname(__FILE__) + '/processors/*.rb'].each { |f| require f }
@@ -0,0 +1,24 @@
1
+ module Alula
2
+ class DummyProcessor < Processor
3
+
4
+ def process
5
+ super
6
+
7
+ asset_name = self.attachments.asset_name(item.name)
8
+ output = File.join(self.site.storage.path(:cache, "attachments"), asset_name)
9
+ # Make sure our directory exists
10
+ output_dir = self.site.storage.path(:cache, "attachments", File.dirname(asset_name))
11
+
12
+ # Skip processing if output already exists...
13
+ unless File.exists?(output)
14
+ # Just simply copy attachement
15
+ FileUtils.cp(item.filepath, output)
16
+ end
17
+
18
+ # Cleanup ourself
19
+ cleanup
20
+ end
21
+ end
22
+ end
23
+
24
+ Alula::AttachmentProcessor.register('dummy', Alula::DummyProcessor)
@@ -0,0 +1,52 @@
1
+ module Alula
2
+ class ImageProcessor < Processor
3
+ def process
4
+ super
5
+
6
+ sizes.each do |size|
7
+ width, height = size[:size]
8
+ # output_dir = self.site.storage.path(:cache, "attachments", size[:type].to_s)
9
+
10
+ # Generate attachment hash
11
+ name = File.join size[:type].to_s, if size[:hires]
12
+ ext = File.extname(item.name)
13
+ item.name.gsub(/#{ext}$/, "-hires#{ext}")
14
+ else
15
+ item.name
16
+ end
17
+
18
+ output = asset_path(name, size[:type].to_s)
19
+ # asset_name = self.attachments.asset_name(name, size[:type].to_s)
20
+ # output = File.join(self.site.storage.path(:cache, "attachments"), asset_name)
21
+ # # Make sure our directory exists
22
+ # output_dir = self.site.storage.path(:cache, "attachments", File.dirname(asset_name))
23
+
24
+ # Skip image processing if output already exists...
25
+ next if File.exists?(output)
26
+
27
+ # Skip if our original resolution isn't enough for hires images
28
+ next if (width > self.info.width and height > self.info.height) and size[:hires]
29
+
30
+ # Generate resized image to output path
31
+ resize_image(output: output, size: size[:size])
32
+ end
33
+
34
+ # Cleanup ourself
35
+ cleanup
36
+ end
37
+
38
+ def sizes
39
+ @sizes ||= begin
40
+ sizes = []
41
+ sizes << { type: :image, hires: false, size: self.site.config.attachments["image"]["size"].split("x").collect{|i| i.to_i} }
42
+ sizes << { type: :thumbnail, hires: false, size: self.site.config.attachments["image"]["thumbnail"].split("x").collect{|i| i.to_i} }
43
+
44
+ if self.site.config.attachments["image"]["hires"]
45
+ sizes += sizes.collect {|s| { type: s[:type], hires: true, size: s[:size].collect{|x| x*2} } }
46
+ end
47
+
48
+ sizes
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,83 @@
1
+ require 'alula/processors/image'
2
+ require 'hashie/mash'
3
+ require 'RMagick'
4
+ require 'mini_exiftool'
5
+
6
+ module Alula
7
+ class Magick < ImageProcessor
8
+ # Register mimetypes
9
+ mimetype "image/jpeg", "image/png", "image/gif"
10
+
11
+ def resize_image(opts)
12
+ output = opts.delete(:output)
13
+ width, height = opts.delete(:size)
14
+
15
+ # Generate resized image
16
+ resized = self.image.resize_to_fit(width, height)
17
+ # Make it progressive
18
+ resized.interlace = ::Magick::PlaneInterlace
19
+
20
+ # Strip unwanted properties
21
+ tags = Hash[*(self.site.config.attachments["image"]["keep_tags"].collect{|t| [t, self.exif[t]]}).flatten]
22
+ resized.strip!
23
+
24
+ resized.write(output)
25
+
26
+ # Save our EXIF info
27
+ exif = MiniExiftool.new output
28
+ tags.each {|key, value| exif[key] = value }
29
+ exif.save
30
+ end
31
+
32
+ def cleanup
33
+ super
34
+
35
+ @info = nil
36
+ @image = nil
37
+ @exif = nil
38
+ end
39
+
40
+ def info
41
+ @info ||= begin
42
+ info = ::Magick::Image.ping(self.item.filepath).first
43
+ Hashie::Mash.new({
44
+ width: info.columns,
45
+ height: info.rows,
46
+ })
47
+ end
48
+ end
49
+
50
+ def exif
51
+ @exif ||= MiniExiftool.new self.item.filepath
52
+ end
53
+
54
+ def image
55
+ @image ||= begin
56
+ image = ::Magick::Image.read(self.item.filepath).first
57
+ unless self.options[:no_rotate]
58
+ case image.orientation.to_i
59
+ when 2
60
+ image.flop!
61
+ when 3
62
+ image.rotate!(180)
63
+ when 4
64
+ image.flip!
65
+ when 5
66
+ image.transpose!
67
+ when 6
68
+ image.rotate!(90)
69
+ when 7
70
+ image.transverse!
71
+ when 8
72
+ image.rotate!(270)
73
+ end
74
+
75
+ image
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+
83
+ Alula::AttachmentProcessor.register('magick', Alula::Magick)
@@ -0,0 +1,97 @@
1
+ module Alula
2
+ class VideoProcessor < Processor
3
+ def process
4
+ # Select variants we need to generate
5
+ generate = variants.select do |name, format|
6
+ size = format[:size].split("x").collect{|i| i.to_i}
7
+
8
+ # Check if video requires rotating
9
+ if self.info.rotation == 90 or self.info.rotation == 270
10
+ size.reverse!
11
+ end
12
+ width, height = size
13
+
14
+ # Generate attachment name hash
15
+ ext = (format[:mobile] ? "-mobile" : "") + (format[:hires] ? "-hires" : "") + ".#{format[:format]}"
16
+ name = File.join "video", item.name.gsub(/\.#{item.extension}$/, "#{ext}")
17
+
18
+ output = asset_path(name, :video)
19
+
20
+ format[:output] = output
21
+
22
+ # Skip video processing if output already exists...
23
+ !File.exists?(output) and !((width > self.info.width and height > self.info.height) and format[:hires])
24
+ end
25
+
26
+ # Detect if thumbnail is required
27
+ size = self.site.config.attachments.video["thumbnail"].split("x").collect{|x|x.to_i}
28
+ thumbnails = ([{size: size, hires: false}] + [{size: size.collect{|x| x*2}, hires: true}])
29
+ .collect {|tn|
30
+ if self.info.rotation == 90 or self.info.rotation == 270
31
+ tn[:size].reverse!
32
+ end
33
+ tn[:size] = tn[:size].join("x")
34
+ name = File.join "thumbnail", item.name.gsub(/\.#{item.extension}$/, "#{tn[:hires] ? "-hires" : ""}.png")
35
+ tn[:output] = asset_path(name, :thumbnail)
36
+ tn[:label] = "thumbnail#{tn[:hires] ? "-hires" : ""}"
37
+ tn
38
+ }
39
+ .select {|tn| !tn[:hires] or (tn[:hires] and self.site.config.attachments.image.hires)}
40
+ .select{|tn|
41
+ width, height = tn[:size].split("x").collect{|i| i.to_i};
42
+ !File.exists?(tn[:output]) and !((width > self.info.width and height > self.info.height) and format[:hires])
43
+ }
44
+
45
+ thumbnails = Hash[thumbnails.collect{|tn| [tn[:label], tn]}]
46
+
47
+ encode(generate, thumbnails)
48
+ end
49
+
50
+ private
51
+ def variants
52
+ @variants ||= begin
53
+ # Collect all formats
54
+ variants = Hash[
55
+ self.site.config.attachments.video.formats.collect { |format|
56
+ [format, {
57
+ format: format,
58
+ size: self.site.config.attachments.video["size-sd"],
59
+ mobile: false,
60
+ hires: false }]
61
+ }
62
+ ]
63
+ # Generate mobile variants?
64
+ if self.site.config.attachments.video.mobile
65
+ variants.merge!(Hash[ variants.collect {|name, fmt| ["#{name}-mobile", fmt.merge({
66
+ size: self.site.config.attachments.video["size-mobile-sd"],
67
+ mobile: true,
68
+ })] } ])
69
+ end
70
+
71
+ # Generate HD versions
72
+ if self.site.config.attachments.video.hires
73
+ variants.merge!(Hash[ variants.collect {|name, fmt| ["#{name}-hires", fmt.merge({
74
+ size: (self.site.config.attachments.video[(fmt[:mobile] ? "size-mobile-hd" : "size-hd")]),
75
+ hires: true,
76
+ })] } ])
77
+ end
78
+
79
+
80
+ # Sort by preferred order
81
+ formats = self.site.config.attachments.video.formats
82
+ variants.sort {|a, b|
83
+ # Sort by preferred format order
84
+ c = formats.index(a.last[:format]) <=> formats.index(b.last[:format])
85
+
86
+ # Sort HD videos on top
87
+ c == 0 and c = (a.last[:hires] == b.last[:hires]) ? 0 : (a.last[:hires] ? -1 : 1)
88
+
89
+ # Put mobile low
90
+ c == 0 and c = (a.last[:mobile] == b.last[:mobile]) ? 0 : (a.last[:mobile] ? 1 : -1)
91
+
92
+ c
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,199 @@
1
+ require 'alula/processors/video'
2
+ require 'aws-sdk'
3
+ require 'zencoder'
4
+
5
+ module Alula
6
+ class Zencoder < VideoProcessor
7
+ # Register mimetypes
8
+ mimetype /^video\//
9
+
10
+ def self.available?(options)
11
+ options.has_key?("token") and options.has_key?("key_id") and options.has_key?("access_key")
12
+ end
13
+
14
+ def initialize(item, opts)
15
+ super
16
+
17
+ ::Zencoder.api_key = options.token
18
+ @s3 = ::AWS::S3.new({
19
+ :access_key_id => options.key_id,
20
+ :secret_access_key => options.access_key,
21
+ })
22
+ end
23
+
24
+ def cleanup
25
+ super
26
+
27
+ @s3 = nil
28
+ @bucket = nil
29
+ @object = nil
30
+ ::Zencoder.api_key = nil
31
+ end
32
+
33
+ def encode(variants, thumbnails)
34
+ return if variants.empty? and thumbnails.empty?
35
+
36
+ @variants = variants
37
+ @thumbnails = thumbnails
38
+
39
+ # Upload attachment to S3 bucket
40
+ upload_item unless item_uploaded
41
+
42
+ # Create encoding profiles
43
+ job = {input: "s3://#{@bucket.name}/#{@object.key}", outputs: [], test: !!self.site.config.testing}
44
+
45
+ profiles.each { |name, profile| job[:outputs] << profile }
46
+
47
+ job = zencoder_encode(job)
48
+ success = zencoder_download(job)
49
+ end
50
+
51
+ private
52
+ def item_uploaded
53
+ @bucket ||= @s3.buckets[self.site.config.attachments.zencoder.bucket]
54
+ unless @bucket.exists?
55
+ @bucket = @s3.buckets.create(self.site.config.attachments.zencoder.bucket)
56
+ end
57
+
58
+ folder = @bucket.objects["#{File.dirname(item.name)}/"]
59
+ unless folder.exists?
60
+ folder = @bucket.objects["#{File.dirname(item.name)}/"].write(nil)
61
+ end
62
+
63
+ @object = @bucket.objects[item.name]
64
+ return @object.exists?
65
+ end
66
+
67
+ def upload_item
68
+ # Fetch upload lock to guarantee that we're onlyones to upload
69
+ @@upload.synchronize do
70
+ self.site.progress.create :upload, title: "Uploading #{item.name}", total: File.size(item.filepath)
71
+ self.site.progress.set_file_transfer(:upload)
72
+
73
+ min_chunk_size = 5 * 1024 * 1024 # S3 minimum chunk size (5Mb)
74
+ @object.multipart_upload do |upload|
75
+ io = File.open(item.filepath)
76
+
77
+ parts = []
78
+
79
+ bufsize = (io.size > 2 * min_chunk_size) ? min_chunk_size : io.size
80
+ while buf = io.read(bufsize)
81
+ md5 = Digest::MD5.base64digest(buf)
82
+
83
+ part = upload.add_part(buf)
84
+ parts << part
85
+
86
+ self.site.progress.set(:upload, io.pos)
87
+ if (io.size - (io.pos + bufsize)) < bufsize
88
+ bufsize = (io.size - io.pos) if (io.size - io.pos) > 0
89
+ end
90
+ end
91
+
92
+ upload.complete(parts)
93
+ end
94
+
95
+ self.site.progress.finish(:upload)
96
+
97
+ # Give Zencoder rights to read file
98
+ @object.acl.change do |acl|
99
+ acl.grant(:read).to(:canonical_user_id => '6c8583d84664a381db0c6af0e79b285ede571885fbe768e7ea50e5d3760597dd')
100
+ end
101
+ end
102
+ end
103
+
104
+ def zencoder_encode(job)
105
+ response = ::Zencoder::Job.create(job)
106
+ return false if response.code != "201"
107
+
108
+ job_id = response.body["id"]
109
+ self.site.progress.create "encode-#{job_id}", title: "Encoding #{item.name}", total: 100
110
+ while (%w{pending waiting processing}.include?((job = ::Zencoder::Job.progress(job_id)).body['state']))
111
+ self.site.progress.set("encode-#{job_id}", job.body['progress'].to_f)
112
+ sleep(1)
113
+ end
114
+ self.site.progress.finish "encode-#{job_id}"
115
+
116
+ job = ::Zencoder::Job.details(job_id)
117
+ if job.body['job']['state'] != "finished"
118
+ return false
119
+ end
120
+ return job
121
+ end
122
+
123
+ def zencoder_download(job)
124
+ @@download.synchronize do
125
+ unless @variants.empty?
126
+ job.body['job']['output_media_files'].each_with_index do |output, idx|
127
+ output_name = profiles[output['label']][:filename]
128
+ output_io = File.open(output_name, 'w')
129
+
130
+ open(output['url'], {
131
+ :content_length_proc => lambda {|t|
132
+ if t && 0 < t
133
+ self.site.progress.create output['label'],
134
+ title: "Downloading (%i/%i)" % [idx + 1, job.body['job']['output_media_files'].count],
135
+ total: t
136
+ self.site.progress.set_file_transfer(output['label'])
137
+ end
138
+ },
139
+ :progress_proc => lambda {|s|
140
+ self.site.progress.set(output['label'], s)
141
+ }
142
+ }) do |io|
143
+ output_io.write(io.read)
144
+ end
145
+ self.site.progress.finish(output['label'])
146
+ end
147
+ end
148
+
149
+ if job.body['job']['thumbnails']
150
+ # Download thumbnails
151
+ job.body['job']['thumbnails'].each do |tn|
152
+ output_file = @thumbnails[tn['group_label']][:output]
153
+
154
+ open(tn['url']) { |i| File.open(output_file, 'w') {|o| o.write(i.read) } }
155
+ end
156
+ end
157
+
158
+ return true
159
+ end
160
+ end
161
+
162
+ def profiles
163
+ @profiles ||= begin
164
+ profiles = Hash[@variants.collect { |variant, opts|
165
+ profile = {
166
+ label: variant,
167
+ filename: opts[:output],
168
+ format: opts[:format],
169
+ size: opts[:size],
170
+ }
171
+ if opts[:mobile]
172
+ profile[:device_profile] = opts[:hires] ? "mobile/advanced" : "mobile/baseline"
173
+ end
174
+ [variant, profile]
175
+ }]
176
+
177
+ unless profiles.empty?
178
+ profiles[profiles.keys.first][:thumbnails] = thumbnail_profiles
179
+ end
180
+ if profiles.empty? and !thumbnail_profiles.empty?
181
+ profiles = {thumbnails: { thumbnails: thumbnail_profiles }}
182
+ end
183
+ profiles
184
+ end
185
+ end
186
+
187
+ def thumbnail_profiles
188
+ @thumbnail_profiles ||= @thumbnails.collect do |label, tn|
189
+ {
190
+ label: tn[:label],
191
+ number: 1,
192
+ size: tn[:size],
193
+ }
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ Alula::AttachmentProcessor.register('zencoder', Alula::Zencoder)