media_processing_tool 1.0.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.
data/bin/mig_http ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ if %w(start stop restart reload run zap status).include?((command = ARGV.first) ? command.downcase : command)
3
+ require 'daemons'
4
+ Daemons.run($0)
5
+ exit
6
+ end
7
+
8
+ lib_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+ $:.unshift(lib_path) if !$:.include?(lib_path) and File.exists?(lib_path)
10
+ require 'json'
11
+
12
+ require 'cli'
13
+ require 'mig'
14
+ require 'mig/http'
15
+
16
+ options[:binding] = '0.0.0.0'
17
+ options[:local_port] = 4567
18
+ options[:ffmpeg_cmd_path] = FFMPEG::DEFAULT_EXECUTABLE_PATH
19
+ options[:mediainfo_cmd_path] = Mediainfo::DEFAULT_EXECUTABLE_PATH
20
+ options[:exiftool_cmd_path] = ExifTool::DEFAULT_EXECUTABLE_PATH
21
+
22
+ op = common_option_parser
23
+ op.banner = "Usage: #{File.basename(__FILE__)} [options] \n\t#{File.basename(__FILE__)} [start|stop|status] [daemon options] -- [application options]"
24
+ op.on('--ffmpeg-bin-path PATH', 'The path to the FFMPEG executable.', "\tdefault: #{options[:ffmpeg_cmd_path]}") { |v| options[:ffmpeg_cmd_path] = v }
25
+ op.on('--mediainfo-bin-path PATH', 'The path to the MediaInfo executable.', "\tdefault: #{options[:mediainfo_cmd_path]}") { |v| options[:mediainfo_cmd_path] = v }
26
+ op.on('--exiftool-bin-path PATH', 'The path to the exiftool executable.', "\tdefault: #{options[:exiftool_cmd_path]}") { |v| options[:exiftool_cmd_path] = v }
27
+ op.on('--binding BINDING', 'The address to bind the server to.',
28
+ "\tdefault: #{options[:binding]}") do |v|
29
+ options[:binding] = v
30
+ end
31
+ op.on('--port PORT', 'The port that the server should listen on.',
32
+ "\tdefault: #{options[:local_port]}") do |v|
33
+ options[:local_port] = v
34
+ end
35
+ op.on('--[no-]options-file [FILENAME]', "\tdefault: #{options[:options_file_path]}" ) { |v| options[:options_file_path] = v }
36
+ add_common_options
37
+ op.parse_common
38
+
39
+ logger = Logger.new(options[:log_to] || STDERR)
40
+ logger.level = Logger::DEBUG
41
+ options[:logger] = logger
42
+
43
+ mig = MediaInformationGatherer.new(options)
44
+
45
+ app = MediaInformationGatherer::HTTP
46
+ app.set(:logger, logger)
47
+ app.set(:bind, options.delete(:binding))
48
+ app.set(:port, options.delete(:local_port))
49
+ app.set(:mig, mig)
50
+ app.set(:initial_options, options)
51
+ app.run!
52
+
data/bin/xml_processor ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ lib_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ $:.unshift(lib_path) if !$:.include?(lib_path) and File.exists?(lib_path)
4
+ require 'optparse'
5
+ require 'pp'
6
+
7
+ require 'media_processing_tool/xml_processor'
8
+
9
+ options_file_path = nil
10
+ config_file_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'default', "#{File.basename(__FILE__, '.*')}_config"))
11
+ options = { }
12
+ ARGV << '--help' if ARGV.empty?
13
+ op = OptionParser.new
14
+ op.banner = "#{File.basename(__FILE__)} [options] file"
15
+ options[:config_file_path] = config_file_path
16
+ op.on('--config-file-path PATH', 'The path to the configuration file containing the @publish_map variable definition.', "default: #{config_file_path}") { |v| options[:config_file_path] = v }
17
+ op.on('--[no-]output-results', 'Outputs the Result as a JSON object.') { |v| options[:output_results] = v }
18
+ op.on('--[no-]pretty-print', 'Pretty Prints the Results.') { |v| options[:pretty_print] = v }
19
+ #op.on('--path-to-mig PATH', '') { |v| options[:path_to_mig] = v }
20
+ options[:log_level] = Logger::WARN
21
+ op.on('--log-level LEVEL', [:debug, :DEBUG, :info, :INFO, :warn, :WARN, :error, :ERROR, :fatal, :FATAL], 'The logging level.', 'default: WARN') do |v|
22
+ v = v.upcase # Symbol doesn't support upcase!
23
+ options[:log_level] = Logger.const_defined?(v) ? Logger.const_get(v) : Logger::WARN
24
+ end
25
+ options[:log_to] = STDERR
26
+ op.on('--log-to PATH', 'Log output location (ex: stderr, stdout, device path, file path)', "\tdefault: STDERR") do |v|
27
+ options[:log_to] = case v.downcase
28
+ when 'stderr'; STDERR
29
+ when 'stdout'; STDOUT
30
+ else; v
31
+ end
32
+ end
33
+ op.on('--help', 'Display this menu.') { puts op; exit }
34
+ op.load(options_file_path)
35
+ op.parse!
36
+
37
+ xp = MediaProcessingTool::XMLProcessor.new(options)
38
+
39
+ file_paths = ARGV
40
+ results = file_paths.map { |file_path| { file: file_path, results: xp.process(file_path) } }
41
+ results = results.pop[:results] if file_paths.length == 1
42
+
43
+ if options[:output_results]
44
+ json_options = { allow_nan: true }
45
+
46
+ if options[:pretty_print]
47
+ puts JSON.pretty_generate(results, json_options)
48
+ else
49
+ puts JSON.fast_generate(results, json_options)
50
+ end
51
+ end
@@ -0,0 +1,49 @@
1
+ # The Following Instance Variables are Available During Evaluation
2
+ #
3
+ # @object The Hash that is being Processed
4
+ # @full_file_path_field_name defaults to :path_on_file_system
5
+ # @full_file_path = object[@full_file_path_field_name]
6
+ #
7
+ # @metadata_sources = object.fetch(:metadata_sources, { })
8
+ # @exiftool = @metadata_sources[:exiftool] ||= { }
9
+ # @mediainfo = @metadata_sources[:mediainfo] ||= { }
10
+ # @ffmpeg = @metadata_sources[:ffmpeg] ||= { }
11
+ # @filemagic = @metadata_sources[:filemagic] ||= { }
12
+ # @media = @metadata_sources[:filemagic] ||= { }
13
+ # @common_media_info = @metadata_sources[:common] ||= { }
14
+ #
15
+ # @media = entity.fetch('media', { })
16
+ # @media_type = @media[:type] || @media['type']
17
+ # @media_subtype = @media[:subtype] || @media['subtype']
18
+ @publish_maps = [
19
+ {
20
+ type: :glob,
21
+ map: {
22
+ '*.*' => {
23
+ publish_executable: 'echo',
24
+ eval_publish_executable: false,
25
+ publish_arguments: %q("#{@full_file_path} #{@media_type}/#{@media_subtype} >> /tmp/mpt_xml_processor_test"),
26
+ eval_publish_arguments: true,
27
+ #workflow: {
28
+ # name: 'WORKFLOW_ONLY',
29
+ # parameters: {
30
+ #
31
+ # } # parameters
32
+ #} # workflow
33
+ }
34
+ } # map
35
+ },
36
+ {
37
+ type: :eval,
38
+ map: {
39
+ 'true' => {
40
+ workflow: {
41
+ name: 'WORKFLOW_ONLY',
42
+ parameters: {
43
+
44
+ } # parameters
45
+ } # workflow
46
+ }
47
+ } # map
48
+ },
49
+ ] # @publish_maps
data/lib/axml.rb ADDED
@@ -0,0 +1,59 @@
1
+ # Abstracted XML Module
2
+ module AXML
3
+
4
+ begin
5
+ require 'libxml'
6
+ XML_PARSER = :libxml unless defined?(XML_PARSER)
7
+ rescue LoadError
8
+ require 'rexml/document'
9
+ XML_PARSER = :rexml unless defined?(XML_PARSER)
10
+ end
11
+
12
+ module REXMLAbstraction
13
+ class << self
14
+ def self.xml_as_document(xml)
15
+ if xml.is_a?(String)
16
+ xml_clean = xml.chomp.strip.chomp
17
+ unless xml_clean.start_with?('<')
18
+ file_name = File.expand_path(xml)
19
+ xml_document = REXML::Document.new(File.new(file_name)) if !file_name.start_with?('<') and File.exists?(file_name)
20
+ end
21
+ xml_document ||= REXML::Document.new(xml)
22
+ else
23
+ xml_document = xml
24
+ end
25
+ xml_document
26
+ end # xml_as_document
27
+ end # self
28
+ end
29
+
30
+ module LibXMLAbstraction
31
+
32
+ def self.xml_as_document(xml)
33
+ if xml.is_a?(String)
34
+ xml_clean = xml.chomp.strip.chomp
35
+ unless xml_clean.start_with?('<')
36
+ file_name = File.expand_path(xml)
37
+ xml_document = LibXML::XML::Document.file(file_name) if File.exists?(file_name)
38
+ end
39
+ xml_document ||= LibXML::XML::Document.string(xml)
40
+ else
41
+ xml_document = xml
42
+ end
43
+ xml_document
44
+ end
45
+
46
+ end
47
+
48
+ # if XML_PARSER == :libxml
49
+ # #puts 'Including LibXML'
50
+ # include LibXMLAbstraction
51
+ # else
52
+ # #puts 'Including REXML'
53
+ # include REXMLAbstraction
54
+ # end
55
+
56
+ # Force LibXML Usage
57
+ def self.xml_as_document(xml); LibXMLAbstraction.xml_as_document(xml); end # self.xml_as_document
58
+
59
+ end # AXML
data/lib/cli.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'logger'
2
+ require 'optparse'
3
+ module CLI
4
+
5
+ LOGGING_LEVELS = {
6
+ :debug => Logger::DEBUG,
7
+ :info => Logger::INFO,
8
+ :warn => Logger::WARN,
9
+ :error => Logger::ERROR,
10
+ :fatal => Logger::FATAL
11
+ }
12
+
13
+ class <<self
14
+
15
+ attr_accessor :options
16
+
17
+ end
18
+
19
+ class CommonOptionParser < ::OptionParser
20
+
21
+ attr_accessor :options
22
+
23
+ #def options=(value)
24
+ # #puts "Setting #{self.class.name}[#{self.object_id}] options => (#{value.class.name}[#{value.object_id}]) #{value}"
25
+ # @options = value
26
+ #end
27
+ #
28
+ #def options
29
+ # #puts "Getting #{self.class.name}[#{self.object_id}] options. #{@options}"
30
+ # @options
31
+ #end
32
+
33
+ def parse_common
34
+ #puts "Parsing #{self.class.name}[#{self.object_id}] options. #{@options}"
35
+ parse!(ARGV.dup)
36
+
37
+ options_file_path = options[:options_file_path]
38
+ # Make sure that options from the command line override those from the options file
39
+ parse!(ARGV.dup) if options_file_path and load(options_file_path)
40
+
41
+ check_required_arguments
42
+ end
43
+
44
+ def required_arguments; @required_arguments ||= [ ] end
45
+ def add_required_argument(*args) [*args].each { |arg| required_arguments << arg } end
46
+ alias :add_required_arguments :add_required_argument
47
+
48
+ def missing_required_arguments
49
+ puts "Options #{options}"
50
+ required_arguments.dup.delete_if { |a| options.has_key?(a.is_a?(Hash) ? a.keys.first : a) }
51
+ end
52
+
53
+ def check_required_arguments
54
+ _missing_arguments = missing_required_arguments
55
+ unless _missing_arguments.empty?
56
+ abort "Missing Required Arguments: #{_missing_arguments.map { |v| (v.is_a?(Hash) ? v.values.first : v).to_s.sub('_', '-')}.join(', ')}\n#{self.to_s}"
57
+ end
58
+ end
59
+ end # CommonOptionParser
60
+
61
+ def self.new_common_option_parser(*args)
62
+ op = CommonOptionParser.new(*args)
63
+ op.options = options
64
+ op
65
+ end
66
+
67
+ end
68
+
69
+ @options = options ||= { } #HashTap.new
70
+ def options; @options end
71
+ def common_option_parser
72
+ @common_option_parser ||= begin
73
+ CLI.options ||= options
74
+ CLI.new_common_option_parser
75
+ end
76
+ end
77
+ def add_common_options
78
+ options[:log_level] ||= 1
79
+ common_option_parser.on('--[no-]options-file [FILENAME]', "\tdefault: #{options[:options_file_path]}" ) { |v| options[:options_file_path] = v }
80
+ common_option_parser.on('--log-to FILENAME', 'Log file location.', "\tdefault: STDERR") { |v| options[:log_to] = v }
81
+ common_option_parser.on('--log-level LEVEL', CLI::LOGGING_LEVELS.keys, "Logging level. Available Options: #{CLI::LOGGING_LEVELS.keys.join(', ')}",
82
+ "\tdefault: #{CLI::LOGGING_LEVELS.invert[options[:log_level]]}") { |v| options[:log_level] = CLI::LOGGING_LEVELS[v] }
83
+ common_option_parser.on('-h', '--help', 'Show this message.') { puts common_option_parser; exit }
84
+ end
85
+
86
+ default_options_file_path = File.join(File.expand_path('.'), "#{File.basename($0, '.rb')}_options")
87
+ options[:options_file_path] = default_options_file_path if File.exists?(default_options_file_path)
88
+ options[:options_file_path] ||= File.expand_path(File.basename($0, '.*'), '~/.options')
@@ -0,0 +1,31 @@
1
+ require 'logger'
2
+ module FinalCutPro
3
+
4
+ attr_writer :logger
5
+
6
+ # @param [Hash] options
7
+ # @option options [Object|nil] :logger (Logger)
8
+ # @option options [String|Object] :log_to (STDERR)
9
+ # @option options [Fixnum] :log_level (3)
10
+ def self.process_options_for_logger(options = { })
11
+ _logger = options[:logger]
12
+ unless _logger
13
+ if options[:log_to] or options[:log_level]
14
+ _logger = Logger.new(options[:log_to] || STDERR)
15
+ _logger.level = options[:log_level] if options[:log_level]
16
+ else
17
+ _logger = logger
18
+ end
19
+ end
20
+ @logger ||= _logger
21
+ _logger
22
+ end # process_options_for_logger
23
+
24
+ def self.logger
25
+ return @logger if @logger
26
+ @logger = Logger.new(STDERR)
27
+ @logger.level = Logger::ERROR
28
+ @logger
29
+ end # self.logger
30
+
31
+ end # FinalCutPro
@@ -0,0 +1,135 @@
1
+ require 'logger'
2
+ require 'uri'
3
+ require 'timecode_methods'
4
+
5
+ module FinalCutPro
6
+ class SequenceProcessor
7
+
8
+ class << self
9
+ include TimecodeMethods
10
+
11
+ attr_writer :logger
12
+ def logger; @logger ||= Logger.new(STDOUT) end
13
+
14
+ def parse_xml(xml)
15
+ doc = FinalCutPro::XMLParser.load(xml)
16
+ sequences = doc.parse_sequences(xml)
17
+ process(sequences)
18
+ end
19
+
20
+ def process(sequences)
21
+ files = { }
22
+ clips = [ ]
23
+ _sequences = { }
24
+ [*sequences].each do |sequence|
25
+ sequence_id = sequence[:id]
26
+ clip_items = sequence[:clip_items]
27
+ next unless clip_items
28
+ clip_item_counter = 0
29
+ clip_items_count = clip_items.count
30
+ clip_items.each do |clip_item|
31
+ clip_item_counter += 1
32
+ #logger.debug { "Clip Item #{clip_item_counter} of #{clip_items_count}"}
33
+
34
+ clip_rate = clip_item[:rate]
35
+ clip_time_base = clip_rate[:timebase].to_f
36
+ clip_rate_ntsc = clip_rate[:ntsc] == 'TRUE' ? true : false
37
+ clip_frame_rate = convert_time_base(clip_time_base, clip_rate_ntsc)
38
+
39
+ clip_duration_frames = clip_item[:duration].to_i
40
+ clip_duration_seconds = clip_duration_frames / clip_frame_rate
41
+
42
+ clip_start_frame = clip_item[:start].to_i
43
+ clip_end_frame = clip_item[:end].to_i
44
+
45
+ clip_in_frame = clip_item[:in].to_i
46
+ clip_out_frame = clip_item[:out].to_i
47
+
48
+
49
+ # File tags may just be a reference to an earlier file tag. A reference only includes an id attribute which we
50
+ # can use to lookup the file from previous parsing.
51
+ file = clip_item[:file] || { }
52
+ file_id = file[:id]
53
+ if file.keys.count == 1
54
+ # We only got the id with this file tag so look it up in our file cache
55
+ file = files[file_id]
56
+ else
57
+ # This file has more than just an ID so it's not a reference to a previous file. Store it in the file cache
58
+ files[file_id] = file
59
+ end
60
+ next unless file
61
+
62
+ file_timecode = file[:timecode] || { }
63
+ #file_timecode_value = file_timecode[:string] || '00:00:00:00'
64
+ file_timecode_frame = file_timecode[:frame].to_i
65
+
66
+ file_rate = file[:rate] || { }
67
+ file_time_base = file_rate[:timebase]
68
+ file_rate_ntsc = (file_rate[:ntsc] == 'TRUE') ? true : false
69
+ file_frame_rate = convert_time_base(file_time_base, file_rate_ntsc)
70
+ #puts "TB: #{file_time_base} NTSC: #{file_rate_ntsc} FPS: #{file_frame_rate}"
71
+ file_in_frame = clip_in_frame # + file_timecode_frame
72
+ file_out_frame = clip_out_frame # + file_timecode_frame
73
+
74
+ # Use the time base and not the frame rate
75
+ file_to_clip_rate_ratio = (file_time_base.to_f / clip_time_base.to_f)
76
+
77
+ #file_in_frame_at_clip_frame_rate = clip_in_frame * file_to_clip_rate_ratio
78
+ #file_out_frame_at_clip_frame_rate = clip_out_frame * file_to_clip_rate_ratio
79
+
80
+ file_in_timecode = frames_to_timecode(file_in_frame, clip_frame_rate, clip_rate_ntsc, ':')
81
+ file_out_timecode = frames_to_timecode(file_out_frame, clip_frame_rate, clip_rate_ntsc, ':')
82
+
83
+ #file_in_timecode_with_offset = TimecodeHelper.timecode_calculator(file_in_timecode, file_out_timecode, file_timecode_value, file_frame_rate)
84
+ file_in_seconds = (file_in_frame/clip_frame_rate) # TimecodeHelper.convert_frames_to_seconds(file_frame_rate, file_in_frame)
85
+ file_out_seconds = (file_out_frame/clip_frame_rate) # TimecodeHelper.convert_frames_to_seconds(file_frame_rate, file_out_frame)
86
+
87
+ file_path_url = file[:pathurl] || ''
88
+ file_path = URI.unescape(file_path_url).scan(/.*:\/\/\w*(\/.*)/).flatten.first
89
+
90
+
91
+ file_in_frame_with_offset = file_timecode_frame + (file_in_frame * file_to_clip_rate_ratio)
92
+ file_out_frame_with_offset = file_timecode_frame + (file_out_frame * file_to_clip_rate_ratio)
93
+ file_in_timecode_with_offset = frames_to_timecode(file_in_frame_with_offset, file_frame_rate, file_rate_ntsc, ';')
94
+ file_out_timecode_with_offset = frames_to_timecode(file_out_frame_with_offset, file_frame_rate, file_rate_ntsc, ';')
95
+
96
+ clips << {
97
+ :start_frame => clip_start_frame,
98
+ :end_frame => clip_end_frame,
99
+ :timebase => clip_time_base,
100
+ :ntsc => clip_rate[:ntsc],
101
+ :frame_rate => clip_frame_rate,
102
+ :sequence => { :id => sequence_id },
103
+ :duration_in_seconds => clip_duration_seconds,
104
+ :duration_in_frames => clip_duration_frames,
105
+ :file => {
106
+ :id => file_id,
107
+ :pathurl => file_path_url,
108
+ :path => file_path,
109
+ :timecode => file_timecode,
110
+ :timebase => file_time_base,
111
+ :ntsc => file_rate_ntsc,
112
+ :frame_rate => file_frame_rate,
113
+
114
+ :in_frame => file_in_frame,
115
+ :out_frame => file_out_frame,
116
+ :in_seconds => file_in_seconds,
117
+ :out_seconds => file_out_seconds,
118
+ :in_timecode => file_in_timecode,
119
+ :out_timecode => file_out_timecode,
120
+ :in_frame_with_offset => file_in_frame_with_offset,
121
+ :out_frame_with_offset => file_out_frame_with_offset,
122
+ :in_timecode_with_offset => file_in_timecode_with_offset,
123
+ :out_timecode_with_offset => file_out_timecode_with_offset,
124
+ },
125
+ }
126
+ _sequences[sequence_id] = clips
127
+ end
128
+ end
129
+ _sequences
130
+ end # process
131
+
132
+ end # << self
133
+
134
+ end # SequenceProcessor
135
+ end # FinalCutPro