media_processing_tool 1.0.0

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