zencodable 0.0.1

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