hydra-derivatives 3.2.2 → 3.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 505c538dd6ded3a65f826eb0d93040bc4edf2a1e
4
- data.tar.gz: 0323253a69f02a91b84e07f546e42debd822ea7f
3
+ metadata.gz: b300c88a53fe0b8e79f9ace11145653e878f8614
4
+ data.tar.gz: dfe48e84c0bd64e74d77d3a4c9cb103f2aaee0dc
5
5
  SHA512:
6
- metadata.gz: c27070a785686b16bb37c60d0a30cf175da384e2818942efb3d7994ff0eacf2a61b84aa0f1189216d4242ed235bda6bda1a43001b3dcb7ad09efcb5a9a085254
7
- data.tar.gz: 453cec1e1d0b1f5940ad251f6271771e0f062f37e53342f80e31f783005b1a87615868d615e569389dcef1635e2332ef59e7a7f92048c93018e0a6e4f0876349
6
+ metadata.gz: 2f1f0183aae8af14487552ebf3c5f9453db4fa64c7f51fa9f1ac6bc4453cf58d3b9bf0a25c5296450c4006eb7b45b4fb6d6093f1cf46a7548d0a189854cfdb19
7
+ data.tar.gz: '058e7057eae73e19451e8d9926576305f85416bd1c3db0bdba279acc97bd629fa27d9ac6200dd56f84505a780d1371937e74f8eab970983a3276af1d3092fd27'
data/.travis.yml CHANGED
@@ -6,8 +6,8 @@ before_install:
6
6
  - gem install bundler
7
7
  rvm:
8
8
  - 2.2.5
9
- - 2.3.1
10
- - 2.4.0
9
+ - 2.3.4
10
+ - 2.4.1
11
11
  env:
12
12
  global:
13
13
  - NOKOGIRI_USE_SYSTEM_LIBRARIES=true
data/README.md CHANGED
@@ -52,6 +52,8 @@ Then when you call `obj.create_derivatives` two new files, 'thumbnail' and 'cont
52
52
 
53
53
  We recommend you run `obj.create_derivatives` in a background worker, because some derivative creation (especially videos) can take a long time.
54
54
 
55
+ The ActiveEncode runner provides support for using external video transcoding services like Amazon Elastic Transcoder. See [ActiveEncode](https://github.com/samvera-labs/active_encode) for details on specific adapters supported.
56
+
55
57
  ## Configuration
56
58
 
57
59
  ### Retrieving from a basic container in Fedora
@@ -74,6 +76,7 @@ Hydra::Derivatives::Processors::Video::Processor.timeout = 10.minutes
74
76
  Hydra::Derivatives::Processors::Document.timeout = 5.minutes
75
77
  Hydra::Derivatives::Processors::Audio.timeout = 10.minutes
76
78
  Hydra::Derivatives::Processors::Image.timeout = 5.minutes
79
+ Hydra::Derivatives::Processors::ActiveEncode.timeout = 5.minutes
77
80
 
78
81
  ```
79
82
 
@@ -88,6 +91,28 @@ Hydra::Derivatives::Processors::Video::Processor.config.mkv.codec = '-vcodec ffv
88
91
  Hydra::Derivatives::Processors::Video::Processor.config.jpeg.codec = '-vcodec mjpeg'
89
92
  ```
90
93
 
94
+ ### Configuration for Audio/Video Processing with ActiveEncode
95
+
96
+ ```ruby
97
+ # Set the transcoding engine
98
+ ActiveEncode::Base.engine_adapter = :elastic_transcoder
99
+
100
+ # Sleep time (in seconds) to poll for status of encoding job
101
+ Hydra::Derivatives.active_encode_poll_time = 10
102
+
103
+ # If you want to use a different class for the source file service
104
+ Hydra::Derivatives::ActiveEncodeDerivatives.source_file_service = MyCustomSourceFileService
105
+
106
+ # If you want to use a different class for the output file service
107
+ Hydra::Derivatives::ActiveEncodeDerivatives.output_file_service = MyCustomOutputFileService
108
+ ```
109
+
110
+ Note: Please don't confuse these methods with the similar methods in the parent class:
111
+ `Hydra::Derivatives.source_file_service` and `Hydra::Derivatives.output_file_service`
112
+
113
+ For additional documentation on using ActiveEncode, see:
114
+ * [Using Amazon Elastic Transcoder](doc/amazon_elastic_transcoder.md)
115
+
91
116
  ### Additional Directives
92
117
 
93
118
  #### Layers
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.2
1
+ 3.3.0
@@ -0,0 +1,116 @@
1
+ # Create Audio and Video Derivatives using Amazon Elastic Transcoder
2
+
3
+ `hydra-derivatives` uses the
4
+ [active\_encode gem](https://github.com/projecthydra-labs/active_encode)
5
+ to allow you to use different encoding services.
6
+ These instructions are for Amazon's Elastic Transcoder service.
7
+
8
+ ## Prerequsites
9
+
10
+ ### Set up the Elastic Transcoder Pipeline
11
+
12
+ Set up a pipeline on AWS Elastic Transcoder that defines:
13
+
14
+ * input bucket
15
+ * bucket for transcoded files
16
+ * bucket for thumbnails
17
+
18
+ ### Configure AWS credentials
19
+
20
+ Optional: If you don't want to pass these values in your ruby code using `Aws.config`, you can set environment variables instead:
21
+
22
+ * AWS\_ACCESS\_KEY\_ID
23
+ * AWS\_SECRET\_ACCESS\_KEY
24
+ * AWS\_REGION
25
+
26
+ ### Install gems
27
+
28
+ Add to your `Gemfile`:
29
+
30
+ * aws-sdk
31
+
32
+ ### Configure initializer
33
+
34
+ In an initializer file such as `config/initializers/active_encode.rb`, make sure you have the following code:
35
+
36
+ ```ruby
37
+ # Use Amazon's Elastic Transcoder
38
+ ActiveEncode::Base.engine_adapter = :elastic_transcoder
39
+ ```
40
+
41
+ ## How to create derivatives (Multiple derivatives per Elastic Transcoder job)
42
+
43
+ ```ruby
44
+ # Access config for AWS
45
+ Aws.config[:access_key_id] = 'put your access key here'
46
+ Aws.config[:secret_access_key] = 'put your secret key here'
47
+ Aws.config[:region] = 'us-east-1'
48
+
49
+ # The pipeline that I set up in Elastic Transcoder
50
+ pipeline_id = '1490715200916-25b08y'
51
+
52
+ # The file "sample_data.mp4" has already been uploaded to the input bucket for my pipeline.
53
+ input_file = 'sample_data.mp4'
54
+
55
+ # Choose a name for the output files
56
+ base_file_name = 'output_file_17'
57
+
58
+ # Settings for a low-res video derivative using a preset for a 320x240 resolution mp4 file
59
+ low_res_video = { key: "#{base_file_name}.mp4", preset_id: '1351620000001-000061' }
60
+
61
+ # Settings for a flash video derivative
62
+ flash_video = { key: "#{base_file_name}.flv", preset_id: '1351620000001-100210' }
63
+
64
+ # Settings to send to the Elastic Transcoder job
65
+ job_settings = { pipeline_id: pipeline_id, output_key_prefix: "active_encode-demo_app/", outputs: [low_res_video, flash_video] }
66
+
67
+ # Run the encoding
68
+ Hydra::Derivatives::ActiveEncodeDerivatives.create(input_file, outputs: [job_settings])
69
+
70
+ # Note: Your rails console will not return to the prompt until the encoding is complete,
71
+ # so it might sit there for several minutes with no feedback.
72
+ # Use the AWS console to see the current status of the encoding.
73
+ ```
74
+
75
+ ## How to create derivatives (One derivative per Elastic Transcoder job)
76
+
77
+ If you want to run a separate Elastic Transcoder job for each derivative file, you could do something like this:
78
+
79
+ ```ruby
80
+ # Settings for a low-res video derivative using a preset for a 320x240 resolution mp4 file.
81
+ low_res_preset_id = '1351620000001-000061'
82
+ low_res_output_file = 'output_15.mp4'
83
+ low_res_video = { pipeline_id: pipeline_id, output_key_prefix: "active_encode-demo_app/", outputs: [{ key: low_res_output_file, preset_id: low_res_preset_id }] }
84
+
85
+ # Settings for a flash video derivative
86
+ flash_preset_id = '1351620000001-100210'
87
+ flash_output_file = 'output_15.flv'
88
+ flash_video = { pipeline_id: pipeline_id, output_key_prefix: "active_encode-demo_app/", outputs: [{ key: flash_output_file, preset_id: flash_preset_id }] }
89
+
90
+ Hydra::Derivatives::ActiveEncodeDerivatives.create(input_file, outputs: [low_res_video, flash_video])
91
+ ```
92
+
93
+ ## How to pass in a ruby object
94
+
95
+ If you want to pass in an `ActiveFedora::Base` object (or some other record) instead of just a String for the input file name, you need to set the `source` option to specify which method to call on your object to get the file name. For example:
96
+
97
+ ```ruby
98
+ # Some object that contains the source file name
99
+ class Video
100
+ attr_accessor :source_file_name
101
+ end
102
+
103
+ video_record = Video.new
104
+ video_record.source_file_name = 'sample_data.mp4'
105
+
106
+ Hydra::Derivatives::ActiveEncodeDerivatives.create(video_record, source: :source_file_name, outputs: [low_res_video])
107
+ ```
108
+
109
+ ## How to pass in a custom encode class
110
+
111
+ If you don't want to use the default encode class `::ActiveEncode::Base`, you can pass in `encode_class`:
112
+
113
+ ```ruby
114
+ Hydra::Derivatives::ActiveEncodeDerivatives.create(video_record, encode_class: MyCustomEncode, source: :source_file_name, outputs: [low_res_video])
115
+ ```
116
+
@@ -23,10 +23,12 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "solr_wrapper", "~> 0.4"
24
24
  spec.add_development_dependency 'fcrepo_wrapper', '~> 0.2'
25
25
 
26
- spec.add_dependency 'active-fedora', '>= 9.0', '< 12'
26
+ spec.add_dependency 'active-fedora', '>= 11.3.1', '< 12'
27
27
  spec.add_dependency 'mini_magick', '>= 3.2', '< 5'
28
28
  spec.add_dependency 'activesupport', '>= 4.0', '< 6'
29
29
  spec.add_dependency 'mime-types', '> 2.0', '< 4.0'
30
+ spec.add_dependency 'active_encode', '~>0.1'
31
+ spec.add_dependency 'addressable', '~>2.5'
30
32
  spec.add_dependency 'deprecation'
31
33
  end
32
34
 
@@ -11,6 +11,7 @@ module Hydra
11
11
  # Runners take a single input and produce one or more outputs
12
12
  # The runner typically accomplishes this by using one or more processors
13
13
  autoload_under 'runners' do
14
+ autoload :ActiveEncodeDerivatives
14
15
  autoload :AudioDerivatives
15
16
  autoload :DocumentDerivatives
16
17
  autoload :FullTextExtract
@@ -30,8 +31,10 @@ module Hydra
30
31
 
31
32
  autoload_under 'services' do
32
33
  autoload :RetrieveSourceFileService
34
+ autoload :RemoteSourceFile
33
35
  autoload :PersistOutputFileService
34
36
  autoload :PersistBasicContainedOutputFileService
37
+ autoload :PersistExternalFileOutputFileService
35
38
  autoload :TempfileService
36
39
  autoload :MimeTypeService
37
40
  end
@@ -48,7 +51,7 @@ module Hydra
48
51
  end
49
52
 
50
53
  CONFIG_METHODS = [:ffmpeg_path, :libreoffice_path, :temp_file_base, :fits_path, :kdu_compress_path,
51
- :kdu_compress_recipes, :enable_ffmpeg, :source_file_service, :output_file_service].freeze
54
+ :kdu_compress_recipes, :enable_ffmpeg, :source_file_service, :output_file_service, :active_encode_poll_time].freeze
52
55
  CONFIG_METHODS.each do |method|
53
56
  module_eval <<-RUBY
54
57
  def self.#{method}
@@ -5,7 +5,8 @@ module Hydra
5
5
  class Config
6
6
  attr_writer :ffmpeg_path, :libreoffice_path, :temp_file_base,
7
7
  :source_file_service, :output_file_service, :fits_path,
8
- :enable_ffmpeg, :kdu_compress_path, :kdu_compress_recipes
8
+ :enable_ffmpeg, :kdu_compress_path, :kdu_compress_recipes,
9
+ :active_encode_poll_time
9
10
 
10
11
  def ffmpeg_path
11
12
  @ffmpeg_path ||= 'ffmpeg'
@@ -72,6 +73,13 @@ module Hydra
72
73
  "Stiles={1024,1024}" ).gsub(/\s+/, " ").strip
73
74
  }
74
75
  end
76
+
77
+ # The poll time (in seconds) that the active encode
78
+ # processor will sleep before it checks the status of an
79
+ # encoding job.
80
+ def active_encode_poll_time
81
+ @active_encode_poll_time ||= 10
82
+ end
75
83
  end
76
84
  end
77
85
  end
@@ -6,6 +6,7 @@ module Hydra::Derivatives
6
6
  autoload :Processor
7
7
  end
8
8
 
9
+ autoload :ActiveEncode
9
10
  autoload :Audio
10
11
  autoload :Document
11
12
  autoload :Ffmpeg
@@ -0,0 +1,56 @@
1
+ require 'active_encode'
2
+
3
+ module Hydra::Derivatives::Processors
4
+ class ActiveEncodeError < StandardError
5
+ def initialize(status, source_path, errors = [])
6
+ msg = "ActiveEncode status was \"#{status}\" for #{source_path}"
7
+ msg = "#{msg}: #{errors.join(' ; ')}" if errors.any?
8
+ super(msg)
9
+ end
10
+ end
11
+
12
+ class ActiveEncode < Processor
13
+ class_attribute :timeout
14
+ attr_accessor :encode_class
15
+ attr_reader :encode_job
16
+
17
+ def initialize(source_path, directives, opts = {})
18
+ super
19
+ @encode_class = opts.delete(:encode_class) || ::ActiveEncode::Base
20
+ end
21
+
22
+ def process
23
+ @encode_job = encode_class.create(source_path, directives)
24
+ timeout ? wait_for_encode_job_with_timeout : wait_for_encode_job
25
+ encode_job.output.each do |output|
26
+ output_file_service.call(output, directives)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def wait_for_encode_job_with_timeout
33
+ Timeout.timeout(timeout) { wait_for_encode_job }
34
+ rescue Timeout::Error
35
+ cleanup_after_timeout
36
+ end
37
+
38
+ # Wait until the encoding job is finished. If the status
39
+ # is anything other than 'completed', raise an error.
40
+ def wait_for_encode_job
41
+ sleep Hydra::Derivatives.active_encode_poll_time while encode_job.reload.running?
42
+ raise ActiveEncodeError.new(encode_job.state, source_path, encode_job.errors) unless encode_job.completed?
43
+ end
44
+
45
+ # After a timeout error, try to cancel the encoding.
46
+ def cleanup_after_timeout
47
+ encode_job.cancel!
48
+ rescue => e
49
+ cancel_error = e
50
+ ensure
51
+ msg = "Unable to process ActiveEncode derivative: The command took longer than #{timeout} seconds to execute. Encoding will be cancelled."
52
+ msg = "#{msg} An error occurred while trying to cancel encoding: #{cancel_error}" if cancel_error
53
+ raise Hydra::Derivatives::TimeoutError, msg
54
+ end
55
+ end
56
+ end
@@ -3,7 +3,7 @@ module Hydra::Derivatives::Processors
3
3
  include ShellBasedProcessor
4
4
 
5
5
  def self.encode(path, format, outdir)
6
- execute "#{Hydra::Derivatives.libreoffice_path} --invisible --headless --convert-to #{format} --outdir #{outdir} #{path}"
6
+ execute "#{Hydra::Derivatives.libreoffice_path} --invisible --headless --convert-to #{format} --outdir #{outdir} #{Shellwords.escape(path)}"
7
7
  end
8
8
 
9
9
  # Converts the document to the format specified in the directives hash.
@@ -14,7 +14,7 @@ module Hydra::Derivatives::Processors
14
14
  def encode(path, options, output_file)
15
15
  inopts = options[INPUT_OPTIONS] ||= "-y"
16
16
  outopts = options[OUTPUT_OPTIONS] ||= ""
17
- execute "#{Hydra::Derivatives.ffmpeg_path} #{inopts} -i \"#{path}\" #{outopts} #{output_file}"
17
+ execute "#{Hydra::Derivatives.ffmpeg_path} #{inopts} -i #{Shellwords.escape(path)} #{outopts} #{output_file}"
18
18
  end
19
19
  end
20
20
  end
@@ -75,7 +75,7 @@ module Hydra::Derivatives::Processors
75
75
 
76
76
  def encode(path, recipe, output_file)
77
77
  kdu_compress = Hydra::Derivatives.kdu_compress_path
78
- execute "#{kdu_compress} -quiet -i #{path} -o #{output_file} #{recipe}"
78
+ execute "#{kdu_compress} -quiet -i #{Shellwords.escape(path)} -o #{output_file} #{recipe}"
79
79
  end
80
80
 
81
81
  def tmp_file(ext)
@@ -0,0 +1,44 @@
1
+ module Hydra::Derivatives
2
+ class ActiveEncodeDerivatives < Runner
3
+ # @param [String, ActiveFedora::Base] object_or_filename source file name (or path), or an object that has a method that will return the file name
4
+ # @param [Hash] options options to pass to the encoder
5
+ # @option options [Symbol] :source a method that can be called on the object to retrieve the source file's name
6
+ # @option options [Symbol] :encode_class class name of the encode object (usually a subclass of ::ActiveEncode::Base)
7
+ # @options options [Array] :outputs a list of desired outputs
8
+ def self.create(object_or_filename, options)
9
+ processor_opts = processor_options(options)
10
+ source_file(object_or_filename, options) do |file_name|
11
+ transform_directives(options.delete(:outputs)).each do |instructions|
12
+ processor = processor_class.new(file_name, instructions, processor_opts)
13
+ processor.process
14
+ end
15
+ end
16
+ end
17
+
18
+ # Use the source service configured for this class or default to the remote file service
19
+ def self.source_file_service
20
+ @source_file_service || RemoteSourceFile
21
+ end
22
+
23
+ # Use the output service configured for this class or default to the external file service
24
+ def self.output_file_service
25
+ @output_file_service || PersistExternalFileOutputFileService
26
+ end
27
+
28
+ def self.processor_class
29
+ Processors::ActiveEncode
30
+ end
31
+
32
+ class << self
33
+ private
34
+
35
+ # Build an options hash specifically for the processor isolated from the runner options
36
+ def processor_options(options)
37
+ opts = { output_file_service: output_file_service }
38
+ encode_class = options.delete(:encode_class)
39
+ opts = opts.merge(encode_class: encode_class) if encode_class
40
+ opts
41
+ end
42
+ end
43
+ end
44
+ end
@@ -9,31 +9,55 @@ module Hydra::Derivatives
9
9
  #
10
10
  # NOTE: Uses basic containment. If you want to use direct containment (ie. with PCDM) you must use a different service (ie. Hydra::Works::AddFileToGenericFile Service)
11
11
  #
12
- # @param [#read] stream the data to be persisted
12
+ # @param [IO,String] content the data to be persisted
13
13
  # @param [Hash] directives directions which can be used to determine where to persist to.
14
14
  # @option directives [String] url This can determine the path of the object.
15
- def self.call(stream, directives)
16
- file = Hydra::Derivatives::IoDecorator.new(stream, new_mime_type(directives.fetch(:format)))
17
- o_name = determine_original_name(file)
18
- m_type = determine_mime_type(file)
19
- uri = URI(directives.fetch(:url))
20
- raise ArgumentError, "#{uri} is not an http uri" unless uri.scheme == 'http'
21
- remote_file = ActiveFedora::File.new(uri.to_s)
15
+ # @option directives [String] format The file extension (e.g. 'jpg')
16
+ def self.call(content, directives)
17
+ file = io(content, directives)
18
+ remote_file = retrieve_remote_file(directives)
22
19
  remote_file.content = file
23
- remote_file.mime_type = m_type
24
- remote_file.original_name = o_name
20
+ remote_file.mime_type = determine_mime_type(file)
21
+ remote_file.original_name = determine_original_name(file)
25
22
  remote_file.save
26
23
  end
27
24
 
28
- def self.new_mime_type(format)
29
- case format
25
+ # Override this implementation if you need a remote file from a different location
26
+ # @return [ActiveFedora::File]
27
+ def self.retrieve_remote_file(directives)
28
+ uri = URI(directives.fetch(:url))
29
+ raise ArgumentError, "#{uri} is not an http uri" unless uri.scheme == 'http'
30
+ ActiveFedora::File.new(uri.to_s)
31
+ end
32
+ private_class_method :retrieve_remote_file
33
+
34
+ def self.io(content, directives)
35
+ Hydra::Derivatives::IoDecorator.new(content, new_mime_type(directives.fetch(:format), charset(content)))
36
+ end
37
+ private_class_method :io
38
+
39
+ def self.new_mime_type(extension, charset = nil)
40
+ fmt = mime_format(extension)
41
+ fmt += "; charset=#{charset}" if charset
42
+ fmt
43
+ end
44
+
45
+ # Strings (from FullText) have encoding. Retrieve it
46
+ def self.charset(content)
47
+ content.encoding.name if content.respond_to?(:encoding)
48
+ end
49
+ private_class_method :charset
50
+
51
+ def self.mime_format(extension)
52
+ case extension
30
53
  when 'mp4'
31
54
  'video/mp4' # default is application/mp4
32
55
  when 'webm'
33
56
  'video/webm' # default is audio/webm
34
57
  else
35
- MIME::Types.type_for(format).first.to_s
58
+ MIME::Types.type_for(extension).first.to_s
36
59
  end
37
60
  end
61
+ private_class_method :mime_format
38
62
  end
39
63
  end
@@ -0,0 +1,20 @@
1
+ require 'addressable'
2
+
3
+ module Hydra::Derivatives
4
+ class PersistExternalFileOutputFileService < PersistOutputFileService
5
+ # Persists a new file at specified location that points to external content
6
+ # @param [Hash] output information about the external derivative file
7
+ # @option output [String] url the location of the external content
8
+ # @param [Hash] directives directions which can be used to determine where to persist to.
9
+ # @option directives [String] url This can determine the path of the object.
10
+ def self.call(output, directives)
11
+ external_file = ActiveFedora::File.new(directives[:url])
12
+ # TODO: Replace the following two lines with the shorter call to #external_url once active_fedora/pull/1234 is merged
13
+ external_file.content = ''
14
+ external_file.mime_type = "message/external-body; access-type=URL; URL=\"#{output[:url]}\""
15
+ # external_file.external_url = output[:url]
16
+ external_file.original_name = Addressable::URI.parse(output[:url]).path.split('/').last
17
+ external_file.save
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # For the case where the source file is a remote file, and we
2
+ # don't want to download the file locally, just return the
3
+ # file name or file path (or whatever we need to pass to the
4
+ # encoding service so that it can find the file).
5
+
6
+ module Hydra::Derivatives
7
+ class RemoteSourceFile
8
+ # Finds the file name of the remote source file.
9
+ # @param [String, ActiveFedora::Base] object file name, or an object that has a method that will return the file name
10
+ # @param [Hash] options
11
+ # @option options [Symbol] :source a method that can be called on the object to retrieve the source file's name
12
+ # @yield [String] the file name
13
+ def self.call(object, options, &_block)
14
+ source_name = options.fetch(:source, :to_s)
15
+ yield(object.send(source_name))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hydra::Derivatives::Processors::ActiveEncode do
4
+ let(:file_path) { File.join(fixture_path, 'videoshort.mp4') }
5
+ let(:directives) { { url: '12345/derivative' } }
6
+ let(:output_file_service) { Hydra::Derivatives::PersistExternalFileOutputFileService }
7
+ let(:options) { { output_file_service: output_file_service } }
8
+ let(:processor) { described_class.new(file_path, directives, options) }
9
+
10
+ describe '#process' do
11
+ subject { processor.process }
12
+
13
+ # Mock out the actual encoding, just pretend that the
14
+ # encode finished and returned a certain status.
15
+ let(:failed_status) { false }
16
+ let(:cancelled_status) { false }
17
+ let(:completed_status) { false }
18
+ let(:state) { :completed }
19
+ let(:errors) { [] }
20
+ let(:external_url) { 'http://www.example.com/external/content' }
21
+ let(:output) { [{ url: external_url }] }
22
+ let(:encode_job_double) do
23
+ enc = double('encode_job',
24
+ state: state,
25
+ errors: errors,
26
+ output: output,
27
+ running?: false,
28
+ completed?: completed_status,
29
+ failed?: failed_status,
30
+ cancelled?: cancelled_status)
31
+ allow(enc).to receive(:reload).and_return(enc)
32
+ enc
33
+ end
34
+
35
+ context 'with a custom encode class' do
36
+ before do
37
+ class TestEncode < ::ActiveEncode::Base; end
38
+
39
+ # For this spec we don't care what happens with output,
40
+ # so stub it out to speed up the spec.
41
+ allow(output_file_service).to receive(:call)
42
+ end
43
+
44
+ after { Object.send(:remove_const, :TestEncode) }
45
+
46
+ let(:completed_status) { true }
47
+ let(:state) { :completed }
48
+ let(:options) do
49
+ {
50
+ encode_class: TestEncode,
51
+ output_file_service: output_file_service
52
+ }
53
+ end
54
+
55
+ it 'uses the configured encode class' do
56
+ expect(TestEncode).to receive(:create).and_return(encode_job_double)
57
+ subject
58
+ end
59
+ end
60
+
61
+ context 'when the encoding failed' do
62
+ let(:state) { :failed }
63
+ let(:failed_status) { true }
64
+ let(:errors) { ['error 1', 'error 2'] }
65
+
66
+ before do
67
+ # Don't really encode the file during specs
68
+ allow(::ActiveEncode::Base).to receive(:create).and_return(encode_job_double)
69
+ end
70
+
71
+ it 'raises an exception' do
72
+ expect { subject }.to raise_error(Hydra::Derivatives::Processors::ActiveEncodeError, "ActiveEncode status was \"failed\" for #{file_path}: error 1 ; error 2")
73
+ end
74
+ end
75
+
76
+ context 'when the encoding was cancelled' do
77
+ let(:state) { :cancelled }
78
+ let(:cancelled_status) { true }
79
+
80
+ before do
81
+ # Don't really encode the file during specs
82
+ allow(::ActiveEncode::Base).to receive(:create).and_return(encode_job_double)
83
+ end
84
+
85
+ it 'raises an exception' do
86
+ expect { subject }.to raise_error(Hydra::Derivatives::Processors::ActiveEncodeError, "ActiveEncode status was \"cancelled\" for #{file_path}")
87
+ end
88
+ end
89
+
90
+ context 'when the timeout is set' do
91
+ before do
92
+ processor.timeout = 0.01
93
+ allow(processor).to receive(:wait_for_encode_job) { sleep 0.1 }
94
+ end
95
+
96
+ it 'raises a timeout exception' do
97
+ msg = "Unable to process ActiveEncode derivative: The command took longer than 0.01 seconds to execute. Encoding will be cancelled."
98
+ expect { processor.process }.to raise_error Hydra::Derivatives::TimeoutError, msg
99
+ end
100
+ end
101
+
102
+ context 'when the timeout is not set' do
103
+ before do
104
+ processor.timeout = nil
105
+ # Don't really encode the file during specs
106
+ allow(::ActiveEncode::Base).to receive(:create).and_return(encode_job_double)
107
+ end
108
+
109
+ it 'processes the encoding without a timeout' do
110
+ expect(processor).not_to receive(:wait_for_encode_job_with_timeout)
111
+ expect(processor).to receive(:wait_for_encode_job).once
112
+ processor.process
113
+ end
114
+ end
115
+
116
+ context 'when error occurs during timeout cleanup' do
117
+ let(:error) { StandardError.new('some error message') }
118
+
119
+ before do
120
+ processor.timeout = 0.01
121
+ allow(processor).to receive(:wait_for_encode_job) { sleep 0.1 }
122
+ allow(::ActiveEncode::Base).to receive(:create).and_return(encode_job_double)
123
+ allow(encode_job_double).to receive(:cancel!).and_raise(error)
124
+ end
125
+
126
+ it 'doesnt lose the timeout error, but adds the new error message' do
127
+ msg = "Unable to process ActiveEncode derivative: The command took longer than 0.01 seconds to execute. Encoding will be cancelled. An error occurred while trying to cancel encoding: some error message"
128
+ expect { processor.process }.to raise_error Hydra::Derivatives::TimeoutError, msg
129
+ end
130
+ end
131
+ end
132
+ end
@@ -7,7 +7,10 @@ describe Hydra::Derivatives::Processors::FullText do
7
7
 
8
8
  describe "#process" do
9
9
  it 'extracts fulltext and stores the results' do
10
- expect(processor.output_file_service).to receive(:call).with(/Project Charter for E-Content Delivery Platform Review/, directives)
10
+ expect(processor.output_file_service).to receive(:call) do |first, second|
11
+ expect(first).to match(/Project Charter for E-Content Delivery Platform Review/)
12
+ expect(second).to eq directives
13
+ end
11
14
  processor.process
12
15
  end
13
16
  end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hydra::Derivatives::ActiveEncodeDerivatives do
4
+ context '.create' do
5
+ before do
6
+ class TestVideo < ActiveFedora::Base
7
+ attr_accessor :remote_file_name
8
+ end
9
+ end
10
+
11
+ after { Object.send(:remove_const, :TestVideo) }
12
+
13
+ let(:file_path) { 'some/path/to/my_video.mp4' }
14
+ let(:video_record) { TestVideo.new(remote_file_name: file_path) }
15
+ let(:options) { { source: :remote_file_name, outputs: [low_res_video] } }
16
+ let(:low_res_video) { { some_key: 'some options to pass to my encoder service' } }
17
+ let(:processor) { double('processor') }
18
+
19
+ it 'calls the processor with the right arguments' do
20
+ expect(Hydra::Derivatives::Processors::ActiveEncode).to receive(:new).with(file_path, low_res_video, output_file_service: Hydra::Derivatives::PersistExternalFileOutputFileService).and_return(processor)
21
+ expect(processor).to receive(:process)
22
+ described_class.create(video_record, options)
23
+ end
24
+
25
+ context 'with a custom encode class' do
26
+ before { class TestEncode < ::ActiveEncode::Base; end }
27
+ after { Object.send(:remove_const, :TestEncode) }
28
+
29
+ let(:options) { { encode_class: TestEncode, source: :remote_file_name, outputs: [low_res_video] } }
30
+
31
+ it 'calls the processor with the right arguments' do
32
+ expect(Hydra::Derivatives::Processors::ActiveEncode).to receive(:new).with(file_path, low_res_video, output_file_service: Hydra::Derivatives::PersistExternalFileOutputFileService, encode_class: TestEncode).and_return(processor)
33
+ expect(processor).to receive(:process)
34
+ described_class.create(video_record, options)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -17,11 +17,24 @@ describe Hydra::Derivatives::PersistBasicContainedOutputFileService do
17
17
  # not be sufficient.
18
18
  context "when file is basic contained (default assumption)" do
19
19
  let(:object) { BasicContainerObject.create }
20
- let(:stream) { StringIO.new("fake file content") }
21
- it "persists the file to the specified destination on the given object" do
22
- described_class.call(stream, format: 'jpg', url: "#{object.uri}/the_derivative_name")
23
- expect(object.send(destination_name.to_sym).content).to eq("fake file content")
24
- expect(object.send(destination_name.to_sym).content_changed?).to eq false
20
+ let(:content) { StringIO.new("fake file content") }
21
+ let(:resource) { object.public_send(destination_name.to_sym) }
22
+ context "and the content is a stream" do
23
+ it "persists the file to the specified destination on the given object" do
24
+ described_class.call(content, format: 'jpg', url: "#{object.uri}/the_derivative_name")
25
+ expect(resource.content).to start_with("fake file content")
26
+ expect(resource.content_changed?).to eq false
27
+ expect(resource.mime_type).to eq 'image/jpeg'
28
+ end
29
+ end
30
+
31
+ context "and content is a string" do
32
+ let(:content) { "fake file content - ÅÄÖ" }
33
+ it "persists the file to the specified destination on the given object" do
34
+ described_class.call(content, format: 'txt', url: "#{object.uri}/the_derivative_name")
35
+ expect(resource.content).to eq("fake file content - ÅÄÖ")
36
+ expect(resource.mime_type).to eq 'text/plain;charset=UTF-8'
37
+ end
25
38
  end
26
39
  end
27
40
  end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hydra::Derivatives::PersistExternalFileOutputFileService do
4
+ before do
5
+ class ExternalDerivativeContainerObject < ActiveFedora::Base
6
+ has_subresource "external_derivative"
7
+ end
8
+ end
9
+ after do
10
+ Object.send(:remove_const, :ExternalDerivativeContainerObject)
11
+ end
12
+
13
+ let(:object) { ExternalDerivativeContainerObject.create }
14
+ let(:directives) { { url: "#{object.uri}/external_derivative" } }
15
+ let(:external_url) { 'http://www.example.com/external/content' }
16
+ let(:output) { { url: external_url } }
17
+ let(:destination_name) { 'external_derivative' }
18
+
19
+ describe '.call' do
20
+ it "persists the external file to the specified destination on the given object" do
21
+ described_class.call(output, directives)
22
+ expect(object.send(destination_name.to_sym).mime_type).to eq "message/external-body;access-type=URL;url=\"http://www.example.com/external/content\""
23
+ expect(object.send(destination_name.to_sym).content).to eq ''
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ class TestObject < ActiveFedora::Base
4
+ attr_accessor :source_file_name
5
+ end
6
+
7
+ describe Hydra::Derivatives::RemoteSourceFile do
8
+ describe '.call' do
9
+ let(:file_name) { 'my_source_file.mp4' }
10
+
11
+ context 'when you pass in a String file name' do
12
+ let(:input_obj) { file_name }
13
+ let(:options) { Hash.new }
14
+
15
+ it 'it yields the file name' do
16
+ expect do |blk|
17
+ described_class.call(input_obj, options, &blk)
18
+ end.to yield_with_args(file_name)
19
+ end
20
+ end
21
+
22
+ context 'when you pass in an ActiveFedora::Base object ' do
23
+ let(:input_obj) { TestObject.new(source_file_name: file_name) }
24
+ let(:options) { { source: :source_file_name } }
25
+
26
+ it 'it yields the file name' do
27
+ expect do |blk|
28
+ described_class.call(input_obj, options, &blk)
29
+ end.to yield_with_args(file_name)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -41,4 +41,10 @@ describe "the configuration" do
41
41
  subject.source_file_service = source_file_service
42
42
  expect(subject.source_file_service).to eq(source_file_service)
43
43
  end
44
+
45
+ it "lets you set the poll time for ActiveEncode jobs" do
46
+ expect(subject.active_encode_poll_time).to eq 10
47
+ subject.active_encode_poll_time = 15
48
+ expect(subject.active_encode_poll_time).to eq 15
49
+ end
44
50
  end
@@ -53,6 +53,10 @@ describe Hydra::Derivatives do
53
53
  subject.kdu_compress_path = '/usr/local/bin/kdu_compress'
54
54
  subject.reset_config!
55
55
  expect(subject.kdu_compress_path).to eq('kdu_compress')
56
+
57
+ subject.active_encode_poll_time = 2
58
+ subject.reset_config!
59
+ expect(subject.active_encode_poll_time).to eq 10
56
60
  end
57
61
  end
58
62
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hydra-derivatives
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.2
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-07 00:00:00.000000000 Z
11
+ date: 2017-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -86,7 +86,7 @@ dependencies:
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '9.0'
89
+ version: 11.3.1
90
90
  - - "<"
91
91
  - !ruby/object:Gem::Version
92
92
  version: '12'
@@ -96,7 +96,7 @@ dependencies:
96
96
  requirements:
97
97
  - - ">="
98
98
  - !ruby/object:Gem::Version
99
- version: '9.0'
99
+ version: 11.3.1
100
100
  - - "<"
101
101
  - !ruby/object:Gem::Version
102
102
  version: '12'
@@ -160,6 +160,34 @@ dependencies:
160
160
  - - "<"
161
161
  - !ruby/object:Gem::Version
162
162
  version: '4.0'
163
+ - !ruby/object:Gem::Dependency
164
+ name: active_encode
165
+ requirement: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - "~>"
168
+ - !ruby/object:Gem::Version
169
+ version: '0.1'
170
+ type: :runtime
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: '0.1'
177
+ - !ruby/object:Gem::Dependency
178
+ name: addressable
179
+ requirement: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: '2.5'
184
+ type: :runtime
185
+ prerelease: false
186
+ version_requirements: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: '2.5'
163
191
  - !ruby/object:Gem::Dependency
164
192
  name: deprecation
165
193
  requirement: !ruby/object:Gem::Requirement
@@ -195,6 +223,7 @@ files:
195
223
  - TODO
196
224
  - VERSION
197
225
  - config/jetty.yml
226
+ - doc/amazon_elastic_transcoder.md
198
227
  - hydra-derivatives.gemspec
199
228
  - lib/color_profiles/license.txt
200
229
  - lib/color_profiles/sRGB_IEC61966-2-1_no_black_scaling.icc
@@ -204,6 +233,7 @@ files:
204
233
  - lib/hydra/derivatives/io_decorator.rb
205
234
  - lib/hydra/derivatives/logger.rb
206
235
  - lib/hydra/derivatives/processors.rb
236
+ - lib/hydra/derivatives/processors/active_encode.rb
207
237
  - lib/hydra/derivatives/processors/audio.rb
208
238
  - lib/hydra/derivatives/processors/document.rb
209
239
  - lib/hydra/derivatives/processors/ffmpeg.rb
@@ -216,6 +246,7 @@ files:
216
246
  - lib/hydra/derivatives/processors/video.rb
217
247
  - lib/hydra/derivatives/processors/video/config.rb
218
248
  - lib/hydra/derivatives/processors/video/processor.rb
249
+ - lib/hydra/derivatives/runners/active_encode_derivatives.rb
219
250
  - lib/hydra/derivatives/runners/audio_derivatives.rb
220
251
  - lib/hydra/derivatives/runners/document_derivatives.rb
221
252
  - lib/hydra/derivatives/runners/full_text_extract.rb
@@ -227,7 +258,9 @@ files:
227
258
  - lib/hydra/derivatives/services/capability_service.rb
228
259
  - lib/hydra/derivatives/services/mime_type_service.rb
229
260
  - lib/hydra/derivatives/services/persist_basic_contained_output_file_service.rb
261
+ - lib/hydra/derivatives/services/persist_external_file_output_file_service.rb
230
262
  - lib/hydra/derivatives/services/persist_output_file_service.rb
263
+ - lib/hydra/derivatives/services/remote_source_file.rb
231
264
  - lib/hydra/derivatives/services/retrieve_source_file_service.rb
232
265
  - lib/hydra/derivatives/services/tempfile_service.rb
233
266
  - solr/config/_rest_managed.json
@@ -261,6 +294,7 @@ files:
261
294
  - spec/fixtures/test.xls
262
295
  - spec/fixtures/test.xlsx
263
296
  - spec/fixtures/world.png
297
+ - spec/processors/active_encode_spec.rb
264
298
  - spec/processors/document_spec.rb
265
299
  - spec/processors/full_text_spec.rb
266
300
  - spec/processors/image_spec.rb
@@ -268,8 +302,11 @@ files:
268
302
  - spec/processors/processor_spec.rb
269
303
  - spec/processors/shell_based_processor_spec.rb
270
304
  - spec/processors/video_spec.rb
305
+ - spec/runners/active_encode_derivatives_spec.rb
271
306
  - spec/services/audio_derivatives_spec.rb
272
307
  - spec/services/persist_basic_contained_output_file_service_spec.rb
308
+ - spec/services/persist_external_file_output_file_service_spec.rb
309
+ - spec/services/remote_source_file_spec.rb
273
310
  - spec/services/retrieve_source_file_service_spec.rb
274
311
  - spec/services/tempfile_service_spec.rb
275
312
  - spec/spec_helper.rb
@@ -299,7 +336,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
299
336
  version: '0'
300
337
  requirements: []
301
338
  rubyforge_project:
302
- rubygems_version: 2.6.10
339
+ rubygems_version: 2.6.12
303
340
  signing_key:
304
341
  specification_version: 4
305
342
  summary: Derivative generation plugin for hydra
@@ -319,6 +356,7 @@ test_files:
319
356
  - spec/fixtures/test.xls
320
357
  - spec/fixtures/test.xlsx
321
358
  - spec/fixtures/world.png
359
+ - spec/processors/active_encode_spec.rb
322
360
  - spec/processors/document_spec.rb
323
361
  - spec/processors/full_text_spec.rb
324
362
  - spec/processors/image_spec.rb
@@ -326,8 +364,11 @@ test_files:
326
364
  - spec/processors/processor_spec.rb
327
365
  - spec/processors/shell_based_processor_spec.rb
328
366
  - spec/processors/video_spec.rb
367
+ - spec/runners/active_encode_derivatives_spec.rb
329
368
  - spec/services/audio_derivatives_spec.rb
330
369
  - spec/services/persist_basic_contained_output_file_service_spec.rb
370
+ - spec/services/persist_external_file_output_file_service_spec.rb
371
+ - spec/services/remote_source_file_spec.rb
331
372
  - spec/services/retrieve_source_file_service_spec.rb
332
373
  - spec/services/tempfile_service_spec.rb
333
374
  - spec/spec_helper.rb