active_encode 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aae1d452186e5101e0e42b4211fd25da561b068caee41d8839f019067309048a
4
- data.tar.gz: d54cc90a08b0907885281ada0ad0c8a62aae0806213b36010282e279f85803bc
3
+ metadata.gz: e8c41200059443abc28d64ef9748086b36a4a1c97537138da24bd1b5016e902d
4
+ data.tar.gz: 3fec8ac243d8e7ff3c96abaa26b08c23e5c409337c2f63463f465096e29e7cba
5
5
  SHA512:
6
- metadata.gz: c1080a31e991e8784dcbf9a8b1f7740fedf299a3eb5075d3ff9f665589fe0f73560679f592f3e97925a9424277a6e12a845187501858d520e2564f3e2a078302
7
- data.tar.gz: 77f94da2c42eeaff56e9298138b1c04a5a79a7fc7b256bd6fa65ba691cb25b3b216dd1cfbad414ad22f3180aceb934a07518a0a2fb062ce63dc8c6f37a3ab6e1
6
+ metadata.gz: 5cae3d56d8c068e3cb73e01b4d15d7ba99d305f6c7bd09e7de51def230d247fa3b423d8b32674cadbf66ad7845ce235399b6b53fa068ef305b65bfad1c829f85
7
+ data.tar.gz: 7265200be814de1969887dd1275a8e89da3e0befe6a92542800c42119f93307f0bf7584a70dcfe8127ef11c7a7a76f5f14fab7b9a5f47ef9408cb474f64d2039
data/.rubocop_todo.yml CHANGED
@@ -37,6 +37,7 @@ Layout/LineLength:
37
37
  Metrics/MethodLength:
38
38
  Exclude:
39
39
  - 'lib/active_encode/engine_adapters/*'
40
+ - 'lib/active_encode/engine_adapters/ffmpeg_adapter/*'
40
41
  - 'lib/file_locator.rb'
41
42
 
42
43
  Metrics/PerceivedComplexity:
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ module ActiveEncode
3
+ module EngineAdapters
4
+ class FfmpegAdapter
5
+ module Cleaner
6
+ # This method is to clean up files leftover from the ffmpeg encode process.
7
+ # File names for the pass_through adapter are the same, so this will clean up
8
+ # pass_through encodes as well.
9
+ def remove_old_files!(options = {})
10
+ default_options = {
11
+ older_than: 2.weeks,
12
+ no_outputs: ['input_metadata', 'duration_input_metadata', 'error.log', 'exit_status.code', 'progress', 'completed', 'pid', 'output_metadata-*'],
13
+ outputs: false,
14
+ all: false
15
+ }
16
+ options.reverse_merge!(default_options)
17
+
18
+ if options[:all]
19
+ files = build_file_list(WORK_DIR, "*")
20
+ directories = remove_files(files, options[:older_than])
21
+ remove_empty_directories(directories)
22
+ elsif options[:outputs]
23
+ output_directories = build_file_list(WORK_DIR, "outputs")
24
+ remove_child_files(output_directories, options[:older_than])
25
+ remove_empty_directories(output_directories)
26
+ else
27
+ files = []
28
+ options[:no_outputs].each { |fn| files += build_file_list(WORK_DIR, fn) }
29
+ remove_files(files, options[:older_than])
30
+ end
31
+ end
32
+
33
+ def build_file_list(directory, filename)
34
+ file_path = File.join(directory, "**", filename)
35
+ # Some of the files generated by the ffmpeg encode seem to be hidden files.
36
+ # This uses File::FNM_DOTMATCH to include them in the results.
37
+ Dir.glob(file_path, File::FNM_DOTMATCH)
38
+ end
39
+
40
+ def file_check(path, older_than)
41
+ File.mtime(path) < DateTime.now - older_than && File.file?(path)
42
+ end
43
+
44
+ def remove_files(files, older_than)
45
+ files_to_delete = files.select { |f| file_check(f, older_than) }
46
+ FileUtils.rm(files_to_delete) unless files_to_delete.empty?
47
+
48
+ # Return a list of any directories that were included in the files list for further processing.
49
+ # The files list can include directories such as "/tmp/.." which should not be included in directory list.
50
+ files.select { |f| File.directory?(f) unless f.end_with?(".") }
51
+ end
52
+
53
+ def remove_empty_directories(directories)
54
+ directories_to_delete = directories.select { |d| Dir.empty?(d) }
55
+ non_empty_directories = directories - directories_to_delete
56
+ directories_to_delete += non_empty_directories.select { |ned| Dir.children(ned) == ["outputs"] && directories_to_delete.include?(File.join(ned, "outputs")) }
57
+ FileUtils.rmdir(directories_to_delete) unless directories_to_delete.empty?
58
+ end
59
+
60
+ def remove_child_files(directories, older_than)
61
+ files_to_delete = []
62
+ directories.each do |d|
63
+ files = Dir.children(d).select { |ch| file_check(File.join(d, ch), older_than) }
64
+ files_to_delete += files.collect { |f| File.join(d, f) }
65
+ end
66
+ FileUtils.rm(files_to_delete) unless files_to_delete.empty?
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
+ require 'English'
2
3
  require 'fileutils'
3
4
  require 'nokogiri'
4
5
  require 'shellwords'
5
6
  require 'addressable/uri'
7
+ require 'active_encode/engine_adapters/ffmpeg_adapter/cleaner'
6
8
 
7
9
  module ActiveEncode
8
10
  module EngineAdapters
9
11
  class FfmpegAdapter
12
+ extend ActiveEncode::EngineAdapters::FfmpegAdapter::Cleaner
13
+
10
14
  WORK_DIR = ENV["ENCODE_WORK_DIR"] || "encodes" # Should read from config
11
15
  MEDIAINFO_PATH = ENV["MEDIAINFO_PATH"] || "mediainfo"
12
16
  FFMPEG_PATH = ENV["FFMPEG_PATH"] || "ffmpeg"
@@ -51,18 +55,38 @@ module ActiveEncode
51
55
 
52
56
  new_encode.input = build_input new_encode
53
57
 
54
- if new_encode.input.duration.blank?
55
- new_encode.state = :failed
56
- new_encode.percent_complete = 1
58
+ # Log error if file is empty or inaccessible
59
+ if new_encode.input.duration.blank? && new_encode.input.file_size.blank?
60
+ file_error(new_encode, input_url)
61
+ return new_encode
62
+ # If file is not empty, try copying file to generate missing metadata
63
+ elsif new_encode.input.duration.blank? && new_encode.input.file_size.present?
64
+
65
+ # This regex removes the query string from URIs. This is necessary to
66
+ # properly process files originating from S3 or similar providers.
67
+ filepath = clean_url.to_s.gsub(/\?.*/, '')
68
+ copy_url = filepath.gsub(filepath, "#{File.basename(filepath, File.extname(filepath))}_temp#{File.extname(filepath)}")
69
+ copy_path = working_path(copy_url, new_encode.id)
70
+
71
+ # -map 0 sets ffmpeg to copy all available streams.
72
+ # -c copy sets ffmpeg to copy all codecs
73
+ # -y automatically overwrites the temp file if one already exists
74
+ `#{FFMPEG_PATH} -loglevel level+fatal -i \"#{input_url}\" -map 0 -c copy -y \"#{copy_path}\"`
75
+
76
+ # If ffmpeg copy fails, log error because file is either not a media file
77
+ # or the file extension does not match the codecs used to encode the file
78
+ unless $CHILD_STATUS.success?
79
+ file_error(new_encode, input_url)
80
+ return new_encode
81
+ end
57
82
 
58
- new_encode.errors = if new_encode.input.file_size.blank?
59
- ["#{input_url} does not exist or is not accessible"]
60
- else
61
- ["Error inspecting input: #{input_url}"]
62
- end
83
+ # Write the mediainfo output to a separate file to preserve metadata from original file
84
+ `#{MEDIAINFO_PATH} #{curl_option} --Output=XML --LogFile=#{working_path("duration_input_metadata", new_encode.id)} "#{copy_path}"`
63
85
 
64
- write_errors new_encode
65
- return new_encode
86
+ File.delete(copy_path)
87
+
88
+ # Assign duration to the encode created for the original file.
89
+ new_encode.input.duration = fixed_duration(working_path("duration_input_metadata", new_encode.id))
66
90
  end
67
91
 
68
92
  new_encode.state = :running
@@ -99,6 +123,7 @@ module ActiveEncode
99
123
  encode.output = []
100
124
  encode.created_at, encode.updated_at = get_times encode.id
101
125
  encode.input = build_input encode
126
+ encode.input.duration ||= fixed_duration(working_path("duration_input_metadata", encode.id)) if File.exist?(working_path("duration_input_metadata", encode.id))
102
127
  encode.percent_complete = calculate_percent_complete encode
103
128
 
104
129
  pid = get_pid(id)
@@ -302,6 +327,24 @@ module ActiveEncode
302
327
  def get_xpath_text(doc, xpath, cast_method)
303
328
  doc.xpath(xpath).first&.text&.send(cast_method)
304
329
  end
330
+
331
+ def fixed_duration(path)
332
+ get_tech_metadata(path)[:duration]
333
+ end
334
+
335
+ def file_error(new_encode, input_url)
336
+ new_encode.state = :failed
337
+ new_encode.percent_complete = 1
338
+
339
+ new_encode.errors = if new_encode.input.file_size.blank?
340
+ ["#{input_url} does not exist or is not accessible"]
341
+ else
342
+ ["Error inspecting input: #{input_url}"]
343
+ end
344
+
345
+ write_errors new_encode
346
+ new_encode
347
+ end
305
348
  end
306
349
  end
307
350
  end
@@ -15,6 +15,7 @@ module ActiveEncode
15
15
  class PassThroughAdapter
16
16
  WORK_DIR = ENV["ENCODE_WORK_DIR"] || "encodes" # Should read from config
17
17
  MEDIAINFO_PATH = ENV["MEDIAINFO_PATH"] || "mediainfo"
18
+ FFMPEG_PATH = ENV["FFMPEG_PATH"] || "ffmpeg"
18
19
 
19
20
  def create(input_url, options = {})
20
21
  # Decode file uris for ffmpeg (mediainfo works either way)
@@ -36,18 +37,38 @@ module ActiveEncode
36
37
  new_encode.input.id = new_encode.id
37
38
  new_encode.created_at, new_encode.updated_at = get_times new_encode.id
38
39
 
39
- if new_encode.input.duration.blank?
40
- new_encode.state = :failed
41
- new_encode.percent_complete = 1
42
-
43
- new_encode.errors = if new_encode.input.file_size.blank?
44
- ["#{input_url} does not exist or is not accessible"]
45
- else
46
- ["Error inspecting input: #{input_url}"]
47
- end
48
-
49
- write_errors new_encode
40
+ # Log error if file is empty or inaccessible
41
+ if new_encode.input.duration.blank? && new_encode.input.file_size.blank?
42
+ file_error(new_encode, input_url)
50
43
  return new_encode
44
+ # If file is not empty, try copying file to generate missing metadata
45
+ elsif new_encode.input.duration.blank? && new_encode.input.file_size.present?
46
+
47
+ # This regex removes the query string from URIs. This is necessary to
48
+ # properly process files originating from S3 or similar providers.
49
+ filepath = input_url.to_s.gsub(/\?.*/, '')
50
+ copy_url = filepath.gsub(filepath, "#{File.basename(filepath, File.extname(filepath))}_temp#{File.extname(input_url)}")
51
+ copy_path = working_path(copy_url, new_encode.id)
52
+
53
+ # -map 0 sets ffmpeg to copy all available streams.
54
+ # -c copy sets ffmpeg to copy all codecs
55
+ # -y automatically overwrites the temp file if one already exists
56
+ `#{FFMPEG_PATH} -loglevel level+fatal -i \"#{input_url}\" -map 0 -c copy -y \"#{copy_path}\"`
57
+
58
+ # If ffmpeg copy fails, log error because file is either not a media file
59
+ # or the file extension does not match the codecs used to encode the file
60
+ unless $CHILD_STATUS.success?
61
+ file_error(new_encode, input_url)
62
+ return new_encode
63
+ end
64
+
65
+ # Write the mediainfo output to a separate file to preserve metadata from original file
66
+ `#{MEDIAINFO_PATH} --Output=XML --LogFile=#{working_path("duration_input_metadata", new_encode.id)} \"#{copy_path}\"`
67
+
68
+ File.delete(copy_path)
69
+
70
+ # Assign duration to the encode created for the original file.
71
+ new_encode.input.duration = fixed_duration(working_path("duration_input_metadata", new_encode.id))
51
72
  end
52
73
 
53
74
  # For saving filename to label map used to find the label when building outputs
@@ -86,6 +107,7 @@ module ActiveEncode
86
107
  encode.created_at, encode.updated_at = get_times encode.id
87
108
  encode.input = build_input encode
88
109
  encode.input.id = encode.id
110
+ encode.input.duration ||= fixed_duration(working_path("duration_input_metadata", encode.id)) if File.exist?(working_path("duration_input_metadata", encode.id))
89
111
  encode.output = []
90
112
  encode.current_operations = []
91
113
 
@@ -231,6 +253,24 @@ module ActiveEncode
231
253
  def get_xpath_text(doc, xpath, cast_method)
232
254
  doc.xpath(xpath).first&.text&.send(cast_method)
233
255
  end
256
+
257
+ def fixed_duration(path)
258
+ get_tech_metadata(path)[:duration]
259
+ end
260
+
261
+ def file_error(new_encode, input_url)
262
+ new_encode.state = :failed
263
+ new_encode.percent_complete = 1
264
+
265
+ new_encode.errors = if new_encode.input.file_size.blank?
266
+ ["#{input_url} does not exist or is not accessible"]
267
+ else
268
+ ["Error inspecting input: #{input_url}"]
269
+ end
270
+
271
+ write_errors new_encode
272
+ new_encode
273
+ end
234
274
  end
235
275
  end
236
276
  end
@@ -24,7 +24,9 @@ module ActiveEncode
24
24
  when /^file\:\/\/\//
25
25
  input_url.to_s.gsub(/file:\/\//, '')
26
26
  when /^s3\:\/\//
27
- input_url.to_s.gsub(/#{input_url.normalized_site}/, '')
27
+ input_url.to_s.gsub(/#{Addressable::URI.parse(input_url).normalized_site}/, '')
28
+ when /^https?:\/\//
29
+ input_url
28
30
  end
29
31
  end
30
32
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ActiveEncode
3
- VERSION = '1.1.2'
3
+ VERSION = '1.2.0'
4
4
  end
@@ -112,6 +112,62 @@ describe ActiveEncode::EngineAdapters::FfmpegAdapter do
112
112
  end
113
113
  end
114
114
 
115
+ context "input file format does not match extension" do
116
+ let(:improper_format_file) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.mp4').to_s }
117
+ let(:improper_format_job) { ActiveEncode::Base.create(improper_format_file, outputs: [{ label: "low", ffmpeg_opt: "-s 640x480", extension: 'mp4' }]) }
118
+
119
+ it "returns the encode with correct error" do
120
+ expect(improper_format_job.errors).to include("Error inspecting input: #{improper_format_file}")
121
+ expect(improper_format_job.percent_complete).to be 1
122
+ end
123
+ end
124
+
125
+ context "input file with missing metadata" do
126
+ let(:file_without_metadata) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.webm').to_s }
127
+ let!(:create_without_metadata_job) { ActiveEncode::Base.create(file_without_metadata, outputs: [{ label: "low", ffmpeg_opt: "-s 640x480", extension: 'mp4' }]) }
128
+ let(:find_without_metadata_job) { ActiveEncode::Base.find create_without_metadata_job.id }
129
+
130
+ it "does not have errors" do
131
+ sleep 2
132
+ expect(find_without_metadata_job.errors).to be_empty
133
+ end
134
+
135
+ it "has the input technical metadata in a file" do
136
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/input_metadata")).not_to be_empty
137
+ end
138
+
139
+ it "has the pid in a file" do
140
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/pid")).not_to be_empty
141
+ end
142
+
143
+ it "assigns the correct duration to the encode" do
144
+ expect(create_without_metadata_job.input.duration).to eq 68_653
145
+ expect(find_without_metadata_job.input.duration).to eq 68_653
146
+ end
147
+
148
+ context 'when uri encoded' do
149
+ let(:file_without_metadata) { Addressable::URI.encode("file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.webm').to_s) }
150
+
151
+ it "does not have errors" do
152
+ sleep 2
153
+ expect(find_without_metadata_job.errors).to be_empty
154
+ end
155
+
156
+ it "has the input technical metadata in a file" do
157
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/input_metadata")).not_to be_empty
158
+ end
159
+
160
+ it "has the pid in a file" do
161
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/pid")).not_to be_empty
162
+ end
163
+
164
+ it "assigns the correct duration to the encode" do
165
+ expect(create_without_metadata_job.input.duration).to eq 68_653
166
+ expect(find_without_metadata_job.input.duration).to eq 68_653
167
+ end
168
+ end
169
+ end
170
+
115
171
  context "input filename with spaces" do
116
172
  let(:file_with_space) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file with space.mp4').to_s }
117
173
  let!(:create_space_job) { ActiveEncode::Base.create(file_with_space, outputs: [{ label: "low", ffmpeg_opt: "-s 640x480", extension: 'mp4' }]) }
@@ -372,4 +428,108 @@ describe ActiveEncode::EngineAdapters::FfmpegAdapter do
372
428
  expect { running_job.cancel! }.to raise_error(ActiveEncode::CancelError)
373
429
  end
374
430
  end
431
+
432
+ describe "#remove_old_files!" do
433
+ subject { created_job }
434
+ # 'exit_status.code' and 'progress' seem to be hidden files so rspec does not see them.
435
+ # That is why they are not explicitly included in the tests even though they are in the filenames list.
436
+ # If they were not being deleted they would cause other tests to fail.
437
+ let(:base_path) { "#{work_dir}/#{subject.id}" }
438
+ let(:input_metadata_file) { "#{base_path}/input_metadata" }
439
+ let(:error_log_file) { "#{base_path}/error.log" }
440
+ let(:pid_file) { "#{base_path}/pid" }
441
+ let(:exit_status_file) { "#{base_path}/exit_status.code" }
442
+ let(:progress_file) { "#{base_path}/progress" }
443
+ let(:pathnames) { [input_metadata_file, error_log_file, pid_file, exit_status_file, progress_file] }
444
+
445
+ # There was some flaky behavior with the file creation for created_job that
446
+ # would cause tests to fail. This ensures the files are created.
447
+ before :each do
448
+ FileUtils.touch(pathnames)
449
+ end
450
+
451
+ context ":no_outputs" do
452
+ it "deletes files created from encode process older than 2 weeks" do
453
+ # Another measure to give files time to be created.
454
+ sleep 1
455
+ travel 3.weeks do
456
+ expect { described_class.remove_old_files! }
457
+ .to change { File.exist?(input_metadata_file) }.from(true).to(false)
458
+ .and change { File.exist?(error_log_file) }.from(true).to(false)
459
+ .and change { File.exist?(pid_file) }.from(true).to(false)
460
+ .and not_change { Dir.children("#{work_dir}/#{subject.id}/outputs").count }.from(2)
461
+ end
462
+ end
463
+
464
+ it "does not delete files younger than 2 weeks" do
465
+ sleep 1
466
+ expect { described_class.remove_old_files! }
467
+ .to not_change { File.exist?(input_metadata_file) }.from(true)
468
+ .and not_change { File.exist?(error_log_file) }.from(true)
469
+ .and not_change { File.exist?(pid_file) }.from(true)
470
+ .and not_change { Dir.children("#{work_dir}/#{subject.id}/outputs").count }.from(2)
471
+ end
472
+ end
473
+
474
+ context ":outputs" do
475
+ it "deletes outputs created from encode process older than 2 weeks" do
476
+ sleep 1
477
+ travel 3.weeks do
478
+ expect { described_class.remove_old_files!(outputs: true) }
479
+ .to not_change { File.exist?(input_metadata_file) }.from(true)
480
+ .and not_change { File.exist?(error_log_file) }.from(true)
481
+ .and not_change { File.exist?(pid_file) }.from(true)
482
+ .and change { Dir.exist?("#{work_dir}/#{subject.id}/outputs") }.from(true).to(false)
483
+ end
484
+ end
485
+
486
+ it "does not delete outputs younger than 2 weeks" do
487
+ sleep 1
488
+ expect { described_class.remove_old_files!(outputs: true) }
489
+ .to not_change { File.exist?(input_metadata_file) }.from(true)
490
+ .and not_change { File.exist?(error_log_file) }.from(true)
491
+ .and not_change { File.exist?(pid_file) }.from(true)
492
+ .and not_change { Dir.children("#{work_dir}/#{subject.id}/outputs").count }.from(2)
493
+ end
494
+
495
+ it "does not delete outputs directory containing files younger than 2 weeks" do
496
+ sleep 1
497
+ travel 3.weeks do
498
+ allow(File).to receive(:mtime).and_call_original
499
+ allow(File).to receive(:mtime).with("#{work_dir}/#{subject.id}/outputs/fireworks-low.mp4").and_return(DateTime.now)
500
+ expect { described_class.remove_old_files!(outputs: true) }
501
+ .to not_change { Dir.exist?("#{work_dir}/#{subject.id}/outputs") }.from(true)
502
+ expect(Dir.children("#{work_dir}/#{subject.id}/outputs")).to eq(["fireworks-low.mp4"])
503
+ end
504
+ end
505
+ end
506
+
507
+ context ":all" do
508
+ it "deletes all files and directories older than 2 weeks" do
509
+ sleep 1
510
+ travel 3.weeks do
511
+ expect { described_class.remove_old_files!(all: true) }
512
+ .to change { Dir.exist?("#{work_dir}/#{subject.id}") }.from(true).to(false)
513
+ end
514
+ end
515
+
516
+ it "does not delete files and directories younger than 2 weeks" do
517
+ sleep 1
518
+ expect { described_class.remove_old_files!(all: true) }
519
+ .to not_change { Dir.exist?("#{work_dir}/#{subject.id}") }.from(true)
520
+ .and not_change { Dir.children("#{work_dir}/#{subject.id}").count }
521
+ end
522
+
523
+ it "does not delete directories containing files younger than 2 weeks" do
524
+ sleep 1
525
+ travel 3.weeks do
526
+ allow(File).to receive(:mtime).and_call_original
527
+ allow(File).to receive(:mtime).with("#{work_dir}/#{subject.id}/input_metadata").and_return(DateTime.now)
528
+ expect { described_class.remove_old_files!(all: true) }
529
+ .to not_change { Dir.exist?("#{work_dir}/#{subject.id}") }.from(true)
530
+ expect(Dir.children("#{work_dir}/#{subject.id}")).to eq(["input_metadata"])
531
+ end
532
+ end
533
+ end
534
+ end
375
535
  end
@@ -109,6 +109,54 @@ describe ActiveEncode::EngineAdapters::PassThroughAdapter do
109
109
  end
110
110
  end
111
111
 
112
+ context "input file format does not match extension" do
113
+ let(:improper_format_file) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.mp4').to_s }
114
+ let(:improper_format_job) { ActiveEncode::Base.create(improper_format_file, outputs: [{ label: "low", url: improper_format_file }]) }
115
+
116
+ it "returns the encode with correct error" do
117
+ expect(improper_format_job.errors).to include("Error inspecting input: #{improper_format_file}")
118
+ expect(improper_format_job.percent_complete).to be 1
119
+ end
120
+ end
121
+
122
+ context "input file without metadata" do
123
+ let(:file_without_metadata) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.webm').to_s }
124
+ let(:file_without_metadata_derivative) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.low.webm').to_s }
125
+ let(:create_without_metadata_job) { ActiveEncode::Base.create(file_without_metadata, outputs: [{ label: "low", url: file_without_metadata_derivative }]) }
126
+ let(:find_without_metadata_job) { ActiveEncode::Base.find create_without_metadata_job.id }
127
+
128
+ it "does not have errors" do
129
+ expect(find_without_metadata_job.errors).to be_empty
130
+ end
131
+
132
+ it "has the input technical metadata in a file" do
133
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/input_metadata")).not_to be_empty
134
+ end
135
+
136
+ it "assigns the correct duration to the encode" do
137
+ expect(create_without_metadata_job.input.duration).to eq 68_653
138
+ expect(find_without_metadata_job.input.duration).to eq 68_653
139
+ end
140
+
141
+ context 'when uri encoded' do
142
+ let(:file_without_metadata) { Addressable::URI.encode("file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.webm').to_s) }
143
+ let(:file_without_metadata_derivative) { Addressable::URI.encode("file://" + Rails.root.join('..', 'spec', 'fixtures', 'file_without_metadata.low.webm').to_s) }
144
+
145
+ it "does not have errors" do
146
+ expect(find_without_metadata_job.errors).to be_empty
147
+ end
148
+
149
+ it "has the input technical metadata in a file" do
150
+ expect(File.read("#{work_dir}/#{create_without_metadata_job.id}/input_metadata")).not_to be_empty
151
+ end
152
+
153
+ it "assigns the correct duration to the encode" do
154
+ expect(create_without_metadata_job.input.duration).to eq 68_653
155
+ expect(find_without_metadata_job.input.duration).to eq 68_653
156
+ end
157
+ end
158
+ end
159
+
112
160
  context "input filename with spaces" do
113
161
  let(:file_with_space) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file with space.mp4').to_s }
114
162
  let(:file_with_space_derivative) { "file://" + Rails.root.join('..', 'spec', 'fixtures', 'file with space.low.mp4').to_s }
data/spec/rails_helper.rb CHANGED
@@ -20,4 +20,6 @@ RSpec.configure do |config|
20
20
  config.after do |example|
21
21
  DatabaseCleaner.clean if example.metadata[:db_clean]
22
22
  end
23
+
24
+ config.include ActiveSupport::Testing::TimeHelpers
23
25
  end
data/spec/spec_helper.rb CHANGED
@@ -16,3 +16,5 @@ RSpec::Matchers.define :be_the_same_time_as do |expected|
16
16
  expect(Time.parse(expected).utc).to eq(Time.parse(actual).utc)
17
17
  end
18
18
  end
19
+
20
+ RSpec::Matchers.define_negated_matcher :not_change, :change
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'addressable'
4
+
5
+ describe ActiveEncode::FilenameSanitizer do
6
+ describe '#sanitize_uri' do
7
+ it 'removes file scheme' do
8
+ uri = "file:///path/to/file"
9
+ expect(ActiveEncode.sanitize_uri(uri)).to eq "/path/to/file"
10
+ end
11
+ it 'relativizes s3 uris' do
12
+ uri = "s3://mybucket/guitar.mp4"
13
+ expect(ActiveEncode.sanitize_uri(uri)).to eq "/guitar.mp4"
14
+ end
15
+ it 'does nothing for http(s) uris' do
16
+ uri = "https://www.googleapis.com/drive/v3/files/1WkWJ12WecI9hX-PmEbuKDGLPK_mN3kYP?alt=media"
17
+ expect(ActiveEncode.sanitize_uri(uri)).to eq uri
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_encode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Klein, Chris Colvard, Phuong Dinh
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-30 00:00:00.000000000 Z
11
+ date: 2023-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -278,6 +278,7 @@ files:
278
278
  - lib/active_encode/engine_adapters.rb
279
279
  - lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb
280
280
  - lib/active_encode/engine_adapters/ffmpeg_adapter.rb
281
+ - lib/active_encode/engine_adapters/ffmpeg_adapter/cleaner.rb
281
282
  - lib/active_encode/engine_adapters/matterhorn_adapter.rb
282
283
  - lib/active_encode/engine_adapters/media_convert_adapter.rb
283
284
  - lib/active_encode/engine_adapters/media_convert_output.rb
@@ -364,6 +365,9 @@ files:
364
365
  - spec/fixtures/file with space.mp4
365
366
  - spec/fixtures/file.with :=+%sp3c!l-ch4cts().mp4
366
367
  - spec/fixtures/file.with...periods.mp4
368
+ - spec/fixtures/file_without_metadata.low.webm
369
+ - spec/fixtures/file_without_metadata.mp4
370
+ - spec/fixtures/file_without_metadata.webm
367
371
  - spec/fixtures/fireworks.low.mp4
368
372
  - spec/fixtures/fireworks.mp4
369
373
  - spec/fixtures/matterhorn/cancelled_response.xml
@@ -421,6 +425,7 @@ files:
421
425
  - spec/units/core_spec.rb
422
426
  - spec/units/engine_adapter_spec.rb
423
427
  - spec/units/file_locator_spec.rb
428
+ - spec/units/filename_sanitizer_spec.rb
424
429
  - spec/units/global_id_spec.rb
425
430
  - spec/units/input_spec.rb
426
431
  - spec/units/output_spec.rb
@@ -433,7 +438,7 @@ licenses:
433
438
  - Apache-2.0
434
439
  metadata:
435
440
  rubygems_mfa_required: 'true'
436
- post_install_message:
441
+ post_install_message:
437
442
  rdoc_options: []
438
443
  require_paths:
439
444
  - lib
@@ -448,8 +453,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
448
453
  - !ruby/object:Gem::Version
449
454
  version: '0'
450
455
  requirements: []
451
- rubygems_version: 3.2.32
452
- signing_key:
456
+ rubygems_version: 3.1.6
457
+ signing_key:
453
458
  specification_version: 4
454
459
  summary: Declare encode job classes that can be run by a variety of encoding services
455
460
  test_files:
@@ -520,6 +525,9 @@ test_files:
520
525
  - spec/fixtures/file with space.mp4
521
526
  - spec/fixtures/file.with :=+%sp3c!l-ch4cts().mp4
522
527
  - spec/fixtures/file.with...periods.mp4
528
+ - spec/fixtures/file_without_metadata.low.webm
529
+ - spec/fixtures/file_without_metadata.mp4
530
+ - spec/fixtures/file_without_metadata.webm
523
531
  - spec/fixtures/fireworks.low.mp4
524
532
  - spec/fixtures/fireworks.mp4
525
533
  - spec/fixtures/matterhorn/cancelled_response.xml
@@ -577,6 +585,7 @@ test_files:
577
585
  - spec/units/core_spec.rb
578
586
  - spec/units/engine_adapter_spec.rb
579
587
  - spec/units/file_locator_spec.rb
588
+ - spec/units/filename_sanitizer_spec.rb
580
589
  - spec/units/global_id_spec.rb
581
590
  - spec/units/input_spec.rb
582
591
  - spec/units/output_spec.rb