alula 0.2.3 → 0.4.0b

Sign up to get free protection for your applications and to get access to all the features.
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)