zencodable 0.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 (55) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +116 -0
  3. data/Rakefile +37 -0
  4. data/lib/generators/zencodable/migration_generator.rb +36 -0
  5. data/lib/generators/zencodable/templates/create_association_table.rb +17 -0
  6. data/lib/generators/zencodable/templates/create_association_thumbnails_table.rb +12 -0
  7. data/lib/zencodable/version.rb +3 -0
  8. data/lib/zencodable.rb +239 -0
  9. data/test/debug.log +3394 -0
  10. data/test/dummy/Rakefile +7 -0
  11. data/test/dummy/app/assets/javascripts/application.js +9 -0
  12. data/test/dummy/app/assets/stylesheets/application.css +7 -0
  13. data/test/dummy/app/controllers/application_controller.rb +3 -0
  14. data/test/dummy/app/helpers/application_helper.rb +2 -0
  15. data/test/dummy/app/models/video.rb +10 -0
  16. data/test/dummy/app/models/video_file.rb +3 -0
  17. data/test/dummy/app/models/video_file_thumbnail.rb +2 -0
  18. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  19. data/test/dummy/config/amazon_s3.yml +4 -0
  20. data/test/dummy/config/application.rb +45 -0
  21. data/test/dummy/config/boot.rb +10 -0
  22. data/test/dummy/config/database.yml +25 -0
  23. data/test/dummy/config/environment.rb +5 -0
  24. data/test/dummy/config/environments/development.rb +30 -0
  25. data/test/dummy/config/environments/production.rb +60 -0
  26. data/test/dummy/config/environments/test.rb +42 -0
  27. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  28. data/test/dummy/config/initializers/inflections.rb +10 -0
  29. data/test/dummy/config/initializers/mime_types.rb +5 -0
  30. data/test/dummy/config/initializers/secret_token.rb +7 -0
  31. data/test/dummy/config/initializers/session_store.rb +8 -0
  32. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  33. data/test/dummy/config/locales/en.yml +5 -0
  34. data/test/dummy/config/routes.rb +58 -0
  35. data/test/dummy/config.ru +4 -0
  36. data/test/dummy/db/development.sqlite3 +0 -0
  37. data/test/dummy/db/migrate/20111130180309_create_dummy_videos.rb +13 -0
  38. data/test/dummy/db/migrate/20111130192331_create_video_files.rb +18 -0
  39. data/test/dummy/db/migrate/20111130193231_create_video_thumbnails.rb +12 -0
  40. data/test/dummy/db/migrate/20111201135457_rename_video_thumbnails.rb +5 -0
  41. data/test/dummy/db/schema.rb +52 -0
  42. data/test/dummy/db/test.sqlite3 +0 -0
  43. data/test/dummy/log/development.log +22 -0
  44. data/test/dummy/log/test.log +42 -0
  45. data/test/dummy/public/404.html +26 -0
  46. data/test/dummy/public/422.html +26 -0
  47. data/test/dummy/public/500.html +26 -0
  48. data/test/dummy/public/favicon.ico +0 -0
  49. data/test/dummy/script/rails +6 -0
  50. data/test/dummy/tmp/db/migrate/20111202054112_create_encoded_kitteh_vids.rb +17 -0
  51. data/test/factories.rb +17 -0
  52. data/test/test_helper.rb +17 -0
  53. data/test/zencodable_generators_test.rb +28 -0
  54. data/test/zencodable_test.rb +64 -0
  55. metadata +226 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2011 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Zencodable
2
+
3
+ Gives you `has_video_encodings` method for your models, that sets up jobs to encode multiple video container/codecs using [Zencoder](http://zencoder.com). It tells Zencoder to place the output files in some bucket in your S3 account. From there, they are yours to enjoy forever.
4
+
5
+ ```ruby
6
+ class Video < ActiveRecord::Base
7
+
8
+ has_video_encodings :video_files, :formats => [:ogg, :mp4, :webm, :flv],
9
+ :output_dimensions => '852x480',
10
+ :s3_config => "#{Rails.root}/config/amazon_s3.yml",
11
+ :path => "videos/zc/:basename/",
12
+ :thumbnails => { :number => 2, :aspect_mode => 'crop', 'size' => '290x160' },
13
+ :options => { :device_profile => 'mobile/advanced' }
14
+
15
+ end
16
+ ```
17
+
18
+ ## Requirements
19
+
20
+ _developed on ruby 1.9.2-p290 and Rails 3.1.3_
21
+
22
+ 1. A [Zencoder][1] account of course, testing or full.
23
+
24
+ 2. A working Amazon S3 account with a shiny new bucket ready to receive video files.
25
+
26
+ 3. this gem (zencodable) in your gemfile, and typhoeus.
27
+
28
+ gem 'zencodable'
29
+ gem 'typhoeus'
30
+
31
+ # Setup
32
+
33
+ ## Zencoder API keys
34
+
35
+ the zencoder/zencoder-rb gem expects access to your Zencoder API keys in some fashion. Also, we need to use dbalatero/typhoeus for HTTP stuffs, that's the only way Zencoder will work with S3. So, I like something in `config/initializers` like
36
+
37
+ # zencoder setup
38
+ if Rails.env == 'production'
39
+ Zencoder.api_key = 'therealdealkey00000000000000000'
40
+ else
41
+ Zencoder.api_key = 'keyfortestingonly00000000000000'
42
+ end
43
+
44
+ Zencoder::HTTP.http_backend = Zencoder::HTTP::Typhoeus
45
+
46
+ ## Bucket policy
47
+
48
+ The bucket needs to have a custom policy to allow Zencoder to place the output videos on it. [Here is a guide on Zencoder's site, follow it](https://app.zencoder.com/docs/guides/getting-started/working-with-s3)
49
+
50
+ (There is currently a branch where an attempt to create a rake task to auto-install this policy was made. Unfortunately it seems the marcel/aws-s3 gem doesn't know how to update a bucket policy after all, it just manages the ACLs. It seems fog can't do that either. Oh well, you'll have to paste in the policy.)
51
+
52
+ ## Run the generator
53
+
54
+ rails g zencodable:migrations <Model> <association_name>
55
+
56
+ e.g.,
57
+
58
+ rails g zencodable:migrations KittehVideo kitteh_video_files
59
+
60
+ This will actually create two `has_many` associations for your model - the `kitteh_video_files` for the output files themselves (one for each format), and the `kitteh_video_file_thumbnails` for the framegrab thumbnails that Zencoder can create (if configured).
61
+
62
+ you can add a `--skip-thumbnails` option if you don't want to use the auto-generated thumbnails.
63
+
64
+ now do a `rake db:migrate`
65
+
66
+ ## How to use
67
+
68
+ ### Configure model and encoding options
69
+
70
+ add something like the above `has_video_encodings` class method to your model (the generator does not try to do this for you).
71
+
72
+ The options should include a `:s3_config` key that gives a location of a YAML file containing your S3 credentials, which should contain a 'bucket' key (you already have one, right?) Actually, all we need from that is the bucket name, so you can instead use a `:bucket` key to give the name of the bucket where the output files should be placed.
73
+
74
+ The `:path` option can be any path within that bucket. It can contain a `:basename` token, which will be replaced with a sanitized, URL-encoded version of the original filename as uploaded.
75
+
76
+ `:formats` is a list of output formats you'd like. [Supported formats and codecs](https://app.zencoder.com/docs/api/encoding/format-and-codecs/format)
77
+
78
+ The other options are all those that can be handled by Zencoder. More info can be found on [:thumbnails](https://app.zencoder.com/docs/api/encoding/thumbnails), [:output_dimensions](https://app.zencoder.com/docs/api/encoding/resolution/size) and other output settings [:options](https://app.zencoder.com/docs/api/encoding)
79
+
80
+ ### Give it a source URL
81
+
82
+ All that's needed to trigger the Zencoder job is to change the `origin_url` value of your model, and then save. That will be picked up, sent to Zencoder, and your job will be started with your desired settings.
83
+
84
+ As the job runs, you can check `Model.job_status` as you see fit, if the job is neither failed nor finished, it will request an update from Zencoder for that Job.
85
+
86
+ Individual files will complete at different times, so you can also check the `state` of each associated output file.
87
+
88
+ vid = Video.new :title => 'Hilarious Kitteh Antics!'
89
+ vid.origin_url = 'http://sourcebucket.s3.amazonaws.com/largevideos/funny_kittehs[HD].mov'
90
+ vid.save
91
+ vid.job_status # "new"
92
+ ...
93
+ vid.job_status # "waiting"
94
+ ...
95
+ vid.job_status # "processing"
96
+ vid.video_encoded_files.collect { |v| [v.format, v.state] }
97
+ ...
98
+ vid.job_status # "finished"
99
+ vid.video_encoded_files.size # 4
100
+ vid.video_encoded_file_thumbnails.size # 2
101
+
102
+
103
+ ## TODO
104
+
105
+ * change name of `origin_url` column and make sure its added in the migrations
106
+ * rake task to generate a working bucket policy (even if it has to be pasted in)
107
+ * background jobs to update the ZC job progress, with events/notifications
108
+ * is s3_url basename sanitization going to be good for non-ASCII filenames? no.
109
+
110
+ ## License
111
+
112
+ Uses MIT-LICENSE. You are free to use this as you like, but don't expect anything.
113
+
114
+ Forking and pull requests would be much appreciated.
115
+
116
+ [1]:http://zencoder.com/
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Zencodable'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task :default => :test
@@ -0,0 +1,36 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/named_base'
3
+
4
+ module Zencodable
5
+ module Generators
6
+ class Migration < ::Rails::Generators::NamedBase
7
+ include Rails::Generators::Migration
8
+
9
+ desc "creates migrations to create tables for the models that hold the encoded video files and thumbnails"
10
+
11
+ argument :association_name, :type => :string, :default => 'video_files'
12
+ class_option :skip_thumbnails, :type => :boolean, :default => false
13
+
14
+ source_root File.expand_path("../templates", __FILE__)
15
+
16
+ def self.next_migration_number(dirname)
17
+ if ActiveRecord::Base.timestamped_migrations
18
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
19
+ migration_number += 1
20
+ migration_number.to_s
21
+ else
22
+ "%.3d" % (current_migration_number(dirname) + 1)
23
+ end
24
+ end
25
+
26
+ def create_migration_files
27
+ migration_template 'create_association_table.rb', "db/migrate/create_#{association_name}"
28
+ unless options.skip_thumbnails?
29
+ migration_template 'create_association_thumbnails_table.rb', "db/migrate/create_#{association_name}_thumbnails"
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,17 @@
1
+ class CreateZencodableOutputFilesAssociationTable < ActiveRecord::Migration
2
+ def change
3
+ create_table "<%= association_name %>" do |t|
4
+ t.string "url"
5
+ t.string "format"
6
+ t.integer "zencoder_file_id"
7
+ t.integer "<%= name.foreign_key %>"
8
+ t.datetime "created_at"
9
+ t.integer "width"
10
+ t.integer "height"
11
+ t.integer "file_size"
12
+ t.string "error_message"
13
+ t.string "state"
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ class CreateZencodableOutputFilesAssociationTable < ActiveRecord::Migration
2
+ def change
3
+ create_table "<%= association_name.singularize %>_thumbnails" do |t|
4
+ t.string "thumbnail_file_name"
5
+ t.string "thumbnail_content_type"
6
+ t.integer "thumbnail_file_size"
7
+ t.datetime "thumbnail_updated_at"
8
+ t.integer "<%= name.foreign_key %>"
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Zencodable
2
+ VERSION = "0.0.1"
3
+ end
data/lib/zencodable.rb ADDED
@@ -0,0 +1,239 @@
1
+ require 'zencoder'
2
+
3
+ module Zencodable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :encoder_definitions
8
+ class_attribute :encoder_output_files_association
9
+ class_attribute :encoder_thumbnails_association
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ def has_video_encodings target_association, options = {}
15
+ self.encoder_definitions = options
16
+ self.encoder_output_files_association = target_association
17
+
18
+ has_many self.encoder_output_files_association, :dependent => :destroy
19
+
20
+ unless options[:thumbnails].blank?
21
+ self.encoder_thumbnails_association = "#{target_association.to_s.singularize}_thumbnails".to_sym
22
+ has_many self.encoder_thumbnails_association, :dependent => :destroy
23
+ end
24
+
25
+ before_save :create_job
26
+
27
+ # TODO cleanup
28
+ #before_destroy :prepare_for_destroy
29
+ #after_destroy :destroy_attached_files
30
+ end
31
+
32
+ end
33
+
34
+ module InstanceMethods
35
+
36
+ def job_status
37
+ unless ['finished','failed'].include? zencoder_job_status
38
+ logger.debug "Unfinished job found. Updating details."
39
+ update_job
40
+ end
41
+ self.zencoder_job_status
42
+ end
43
+
44
+ def create_job
45
+ if self.origin_url_changed?
46
+ logger.debug "Origin URL changed. Creating new ZenCoder job."
47
+ if @job = Encoder::Job.create(origin_url, self.class.encoder_definitions)
48
+ logger.debug "ZenCoder job created, ID = #{@job.id}"
49
+ self.zencoder_job_id = @job.id
50
+ self.zencoder_job_status = 'new'
51
+ self.zencoder_job_created = Time.now
52
+ self.zencoder_job_finished = nil
53
+ end
54
+ end
55
+ end
56
+
57
+ def update_job
58
+ self.zencoder_job_status = encoder_job.status
59
+ self.zencoder_job_finished = encoder_job.finished_at
60
+ self.video_files = encoder_job.files.collect{ |file| video_files_class.new(file) } rescue []
61
+ self.video_thumbnails = encoder_job.thumbnails.collect{ |file| video_thumbnails_class.new(file) } rescue []
62
+ save
63
+ end
64
+
65
+ def source_file_for(fmt)
66
+ self.video_files.where(:format => fmt).first
67
+ end
68
+
69
+ def video_files
70
+ self.send(video_files_method)
71
+ end
72
+
73
+ def video_thumbnails
74
+ self.send(video_files_thumbnails_method)
75
+ end
76
+
77
+ def video_files= *args
78
+ self.send "#{video_files_method}=", *args
79
+ end
80
+
81
+ def video_thumbnails= *args
82
+ self.send("#{video_files_thumbnails_method}=", *args) if self.respond_to?(video_files_thumbnails_method)
83
+ end
84
+
85
+
86
+ private
87
+ def encoder_job
88
+ @job ||= Encoder::Job.new(self.zencoder_job_id)
89
+ end
90
+
91
+ def video_files_method
92
+ self.class.encoder_output_files_association
93
+ end
94
+
95
+ def video_files_thumbnails_method
96
+ self.class.encoder_thumbnails_association
97
+ end
98
+
99
+ # need to know the Class of the associations so we can instantiate some when job is complete.
100
+ def video_files_class
101
+ self.class.reflect_on_all_associations(:has_many).detect{ |reflection| reflection.name == self.class.encoder_output_files_association }.klass
102
+ end
103
+
104
+ def video_thumbnails_class
105
+ self.class.reflect_on_all_associations(:has_many).detect{ |reflection| reflection.name == self.class.encoder_thumbnails_association }.klass
106
+ end
107
+
108
+ end
109
+
110
+
111
+
112
+ module Encoder
113
+ include Zencoder
114
+
115
+ class Job < Zencoder::Job
116
+
117
+ attr_accessor :id
118
+
119
+ class << self
120
+
121
+ def create(origin, encoder_definitions)
122
+ response = super(:input => origin,
123
+ :outputs => build_encoder_output_options(origin, encoder_definitions))
124
+ if response.code == 201
125
+ job_id = response.body['id']
126
+ self.new(job_id)
127
+ end
128
+ end
129
+
130
+ def build_encoder_output_options(origin, definitions)
131
+
132
+ formats = definitions[:formats] || [:ogg]
133
+ size = definitions[:output_dimensions] || '400x300'
134
+ base_url = s3_url(origin, definitions[:s3_config], definitions[:path])
135
+
136
+ defaults = { :public => true,
137
+ :device_profile => "mobile/advanced",
138
+ :size => size
139
+ }
140
+ defaults = defaults.merge(definitions[:options]) if definitions[:options]
141
+
142
+ if definitions[:thumbnails]
143
+ defaults[:thumbnails] = {:aspect_mode => 'crop',
144
+ :base_url => base_url,
145
+ :size => size
146
+ }.merge(definitions[:thumbnails])
147
+ end
148
+
149
+ formats.collect{ |f| defaults.merge( :format => f.to_s, :label => f.to_s, :base_url => base_url ) }
150
+ end
151
+
152
+ def s3_url origin_url, s3_config_file, path
153
+ basename = origin_url.match( %r|([^/][^/\?]+)[^/]*\.[^.]+\z| )[1] # matches filename without extension
154
+ basename = basename.downcase.squish.gsub(/\s+/, '-').gsub(/[^\w\d_.-]/, '') # cheap/ugly to_url
155
+ path = path.gsub(%r|:basename\b|, basename)
156
+ "s3://#{s3_bucket_name(s3_config_file)}.s3.amazonaws.com/#{path}/"
157
+ end
158
+
159
+ def s3_bucket_name s3_config_file
160
+ s3_config_file ||= "#{Rails.root}/config/s3.yml"
161
+ @s3_config ||= YAML.load_file(s3_config_file)[Rails.env].symbolize_keys
162
+ @s3_config[:bucket_name]
163
+ end
164
+
165
+ end
166
+
167
+ def initialize(job_id)
168
+ @id = job_id
169
+ @job_detail = {}
170
+ end
171
+
172
+
173
+ def details
174
+ if @job_detail.empty? and @id
175
+ response = self.class.details @id
176
+ if response.code == 200
177
+ @job_detail = response.body['job']
178
+ end
179
+ end
180
+ @job_detail
181
+ end
182
+
183
+ def status
184
+ self.details['state']
185
+ end
186
+
187
+ def finished_at
188
+ self.details['finished_at']
189
+ end
190
+
191
+ def files
192
+ if outfiles = self.details['output_media_files']
193
+ outfiles.collect { |f| { :url => f['url'],
194
+ :format => f['label'],
195
+ :zencoder_file_id => f['id'],
196
+ :created_at => f['finished_at'],
197
+ :duration_sec => f['duration_in_ms'],
198
+ :width => f['width'],
199
+ :height => f['height'],
200
+ :file_size => f['file_size_bytes'],
201
+ :error_message => f['error_message'],
202
+ :state => f['state'] }
203
+ }
204
+ end
205
+ end
206
+
207
+ # ZC gives thumbnails for each output file format, but gives them the same
208
+ # name and overwrites them at the same S3 location. So if we have f
209
+ # formats, and ask for x thumbnails, we get x*f files described in the
210
+ # details['thumbnails'] API, but there are actually only x on the S3
211
+ # server. So, the inject() here is done to pare that down to unique URLs,
212
+ # and give us the cols/vals that paperclip in VideoThumbnail is going to want
213
+ def thumbnails
214
+ if thumbs = self.details['thumbnails']
215
+
216
+ thumbs.inject([]) do |res,th|
217
+ unless res.map{ |r| r[:thumbnail_file_name] }.include?(th['url'])
218
+ res << { :thumbnail_file_name => th['url'],
219
+ :thumbnail_content_type =>th['format'],
220
+ :thumbnail_file_size => th['file_size_bytes'],
221
+ :thumbnail_updated_at => th['created_at']
222
+ }
223
+ end
224
+ res
225
+ end
226
+
227
+ end
228
+ end
229
+
230
+ end
231
+
232
+ end
233
+ end
234
+
235
+
236
+ class ActiveRecord::Base
237
+ include Zencodable
238
+ end
239
+