mediainfo 0.5.1

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/Changelog ADDED
@@ -0,0 +1,24 @@
1
+ v0.5.1 Packaging Fix
2
+ - Gemspec was referencing a missing file
3
+ - Behold: THREE DIGIT VERSION NUMBER. Serious business now!
4
+
5
+ v0.5 Raw, Son
6
+ - You can now initialize an instance from raw CLI output
7
+
8
+ v0.4 Mediainfo Respects Your Existing Customs
9
+ - Define String#shell_escape only if it is not already defined
10
+
11
+ v0.3 Mediainfo Is High Class
12
+ - Class instead of instance-level customization of mediainfo binary path.
13
+ Set Mediainfo.path = /path/to/binary if mediainfo is not in your shell
14
+ path.
15
+ - Instances retain the :last_command run. The API to rerun that command is
16
+ still private.
17
+ - Started defining custom Mediainfo exception classes and are now raising
18
+ Mediainfo::ExecutionError instead of RuntimeError if the system command
19
+ exits with anything other than 0.
20
+
21
+ v0.2 Escape from the Shell
22
+ - Added proper escaping for funky filenames. Thanks to Peter Vandenberk!
23
+
24
+ v0.1 Initial Release
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ This program is free software. It comes without any warranty, to
2
+ the extent permitted by applicable law. You can redistribute it
3
+ and/or modify it under the terms of the Do What The Fuck You Want
4
+ To Public License, Version 2, as published by Sam Hocevar. See
5
+ http://sam.zoy.org/wtfpl/COPYING for more details.
6
+
7
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
8
+ Version 2, December 2004
9
+
10
+ Copyright (C) 2009 Seth Thomas Rasmussen
11
+ Everyone is permitted to copy and distribute verbatim or modified
12
+ copies of this license document, and changing it is allowed as long
13
+ as the name is changed.
14
+
15
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
16
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
17
+
18
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
data/Manifest ADDED
@@ -0,0 +1,9 @@
1
+ Changelog
2
+ LICENSE
3
+ Manifest
4
+ README.markdown
5
+ Rakefile
6
+ lib/mediainfo.rb
7
+ lib/mediainfo/attr_readers.rb
8
+ lib/mediainfo/string.rb
9
+ mediainfo.gemspec
data/README.markdown ADDED
@@ -0,0 +1,40 @@
1
+ # Mediainfo
2
+
3
+ Mediainfo is a class wrapping [the mediainfo CLI](http://mediainfo.sourceforge.net).
4
+
5
+ ## Usage
6
+
7
+ info = Mediainfo.new "/path/to/file"
8
+
9
+ That will issue the system call to `mediainfo` and parse the output.
10
+ From there, you can call numerous methods to get a variety of information
11
+ about a file. Some attributes may be present for some files where others
12
+ are not.
13
+
14
+ For a list of all possible attributes supported:
15
+
16
+ Mediainfo.supported_attributes
17
+
18
+ In addition to the stock arguments provided by parsing `mediainfo` output,
19
+ some convenience methods and added behavior is added.
20
+
21
+ Mediainfo is inspired by RVideo::Inspector, part of the rvideo gem.
22
+ The rvideo inspector is based on output from ffmpeg which is not
23
+ intended to be machine parseable. I spent a little while chasing
24
+ the ffmpeg development team, and decided finally that perhaps other
25
+ tools were better. As such, some of the API for Mediainfo is straight
26
+ from RVideo::Inspector. Some is not. Just saying.
27
+
28
+ ## Requirements
29
+
30
+ This library is compatible with:
31
+
32
+ MediaInfo Command line,
33
+ MediaInfoLib - v0.7.11
34
+
35
+ to the extent that is documented in the tests.
36
+
37
+ ## Contributors
38
+
39
+ * Seth Thomas Rasmussen - [http://greatseth.com](http://greatseth.com)
40
+ * Peter Vandenberk - [http://github.com/pvdb](http://github.com/pvdb)
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require "rake/testtask"
2
+ load "Rakefile.cloud" if File.exist? "Rakefile.cloud"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList["test/*_test.rb"]
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ namespace :mediainfo do
13
+ task :fixture do
14
+ unless file = ENV["file"]
15
+ puts "Usage: rake mediainfo:fixture file=/path/to/file"
16
+ exit
17
+ end
18
+ fixture = File.expand_path "./test/fixtures/#{File.basename file}.txt"
19
+ system "mediainfo #{file} > #{fixture}"
20
+ if File.exist? fixture
21
+ puts "Generated fixture #{fixture}."
22
+ else
23
+ puts "Error generating fixture. #{fixture} not created."
24
+ end
25
+ end
26
+ end
27
+
28
+ require "rubygems"
29
+ require "echoe"
30
+
31
+ Echoe.new "mediainfo" do |p|
32
+ p.description = "Mediainfo is a class wrapping the mediainfo CLI (http://mediainfo.sourceforge.net)"
33
+ p.author = "Seth Thomas Rasmussen"
34
+ p.email = "sethrasmussen@gmail.com"
35
+ p.url = "http://greatseth.com"
36
+ p.ignore_pattern = %w( test/**/* )
37
+ end
data/lib/mediainfo.rb ADDED
@@ -0,0 +1,260 @@
1
+ require "mediainfo/string"
2
+ require "mediainfo/attr_readers"
3
+
4
+ # Mediainfo is a class encapsulating the ability to run `mediainfo`
5
+ # on a file and expose the information it produces in a straightforward
6
+ # manner.
7
+ #
8
+ # Basic usage:
9
+ #
10
+ # info = Mediainfo.new "/path/to/file"
11
+ #
12
+ # That will issue the system call to `mediainfo` and parse the output.
13
+ # From there, you can call numerous methods to get a variety of information
14
+ # about a file. Some attributes may be present for some files where others
15
+ # are not.
16
+ #
17
+ # You may also initialize a Mediainfo instance using raw CLI output
18
+ # you have saved for some reason.
19
+ #
20
+ # info = Mediainfo.new
21
+ # info.raw_response = cli_output
22
+ #
23
+ # Setting the raw_response triggers the system call, and from that point on
24
+ # the object should behave the same as if you'd initialized it with the path
25
+ # to a file.
26
+ #
27
+ # For a list of all possible attributes supported:
28
+ #
29
+ # Mediainfo.supported_attributes
30
+ #
31
+ # In addition to the stock arguments provided by parsing `mediainfo` output,
32
+ # some convenience methods and added behavior is added.
33
+ class Mediainfo
34
+ extend AttrReaders
35
+
36
+ SECTIONS = %w( Audio Video Image ) # and General
37
+
38
+ ### GENERAL
39
+
40
+ mediainfo_attr_reader :codec_id, "Codec ID"
41
+
42
+ mediainfo_duration_reader :duration
43
+
44
+ mediainfo_attr_reader :format
45
+ mediainfo_attr_reader :format_profile
46
+ mediainfo_attr_reader :format_info
47
+ mediainfo_attr_reader :overall_bit_rate
48
+ mediainfo_attr_reader :writing_application
49
+ mediainfo_attr_reader :writing_library
50
+
51
+ def size; File.size(@full_filename); end
52
+
53
+ mediainfo_date_reader :mastered_date
54
+ mediainfo_date_reader :tagged_date
55
+ mediainfo_date_reader :encoded_date
56
+
57
+ ### VIDEO
58
+
59
+ mediainfo_section_query :video
60
+
61
+ # XXX this breaks from RVideo::Inspector which returns
62
+ # something like "#0.1" instead of "1"
63
+ mediainfo_attr_reader :video_stream_id, "ID"
64
+
65
+ mediainfo_duration_reader :video_duration
66
+
67
+ mediainfo_attr_reader :video_stream_size
68
+ mediainfo_attr_reader :video_bit_rate
69
+ mediainfo_attr_reader :video_nominal_bit_rate
70
+
71
+ mediainfo_attr_reader :video_bit_rate_mode
72
+ def cbr?; video? and "Constant" == video_bit_rate_mode; end
73
+ def vbr?; video? and not cbr?; end
74
+
75
+ mediainfo_attr_reader :video_scan_order
76
+ mediainfo_attr_reader :video_scan_type
77
+ def interlaced?; video? and "Interlaced" == video_scan_type; end
78
+ def progressive?; video? and not interlaced? end
79
+
80
+ mediainfo_int_reader :video_resolution
81
+
82
+ mediainfo_attr_reader :video_colorimetry
83
+ alias_method :video_colorspace, :video_colorimetry
84
+
85
+ mediainfo_attr_reader :video_format
86
+ mediainfo_attr_reader :video_format_profile
87
+ mediainfo_attr_reader :video_format_version
88
+ mediainfo_attr_reader :video_format_settings_cabac, "Format settings, CABAC"
89
+ mediainfo_attr_reader :video_format_settings_reframes, "Format settings, ReFrames"
90
+ mediainfo_attr_reader :video_format_settings_matrix, "Format settings, Matrix"
91
+ # Format settings, BVOP : Yes
92
+ # Format settings, QPel : No
93
+ # Format settings, GMC : No warppoints
94
+ # mediainfo_attr_reader :video_format_settings_qpel, "Format settings, QPel"
95
+
96
+ mediainfo_attr_reader :video_codec_id, "Codec ID"
97
+ mediainfo_attr_reader :video_codec_info, "Codec ID/Info"
98
+
99
+ mediainfo_attr_reader :video_frame_rate
100
+ def fps; video_frame_rate[/[\d.]+/].to_f if video?; end
101
+ alias_method :framerate, :fps
102
+
103
+ mediainfo_attr_reader :video_frame_rate_mode
104
+
105
+ mediainfo_attr_reader :video_display_aspect_ratio
106
+ alias_method :display_aspect_ratio, :video_display_aspect_ratio
107
+
108
+ mediainfo_attr_reader :video_bits_pixel_frame, "Bits/(Pixel*Frame)"
109
+
110
+ mediainfo_int_reader :video_width
111
+ mediainfo_int_reader :video_height
112
+
113
+ def resolution; "#{width}x#{height}" if video? or image?; end
114
+ def width; if video?; video_width; elsif image?; image_width; end; end
115
+ def height; if video?; video_height; elsif image?; image_height; end; end
116
+
117
+ mediainfo_date_reader :video_encoded_date
118
+ mediainfo_date_reader :video_tagged_date
119
+
120
+ ### AUDIO
121
+
122
+ mediainfo_section_query :audio
123
+
124
+ # XXX this breaks from RVideo::Inspector which returns
125
+ # something like "#0.1" instead of "1"
126
+ mediainfo_attr_reader :audio_stream_id, "ID"
127
+
128
+ mediainfo_duration_reader :audio_duration
129
+
130
+ mediainfo_attr_reader :audio_sampling_rate
131
+ def audio_sample_rate
132
+ return unless rate = audio_sampling_rate_before_type_cast
133
+ number = rate.gsub(/[^\d.]+/, "").to_f
134
+ number = case rate
135
+ when /KHz/ then number * 1000
136
+ when /Hz/ then number
137
+ else
138
+ raise "unhandled sample rate! please report bug!"
139
+ end
140
+ number.to_i
141
+ end
142
+ alias_method :audio_sampling_rate, :audio_sample_rate
143
+
144
+ mediainfo_attr_reader :audio_stream_size
145
+ mediainfo_attr_reader :audio_bit_rate
146
+ mediainfo_attr_reader :audio_bit_rate_mode
147
+ mediainfo_attr_reader :audio_interleave_duration, "Interleave, duration"
148
+
149
+ mediainfo_int_reader :audio_resolution
150
+ alias_method :audio_sample_bit_depth, :audio_resolution
151
+
152
+ mediainfo_attr_reader :audio_format
153
+ mediainfo_attr_reader :audio_format_info, "Format/Info"
154
+ mediainfo_attr_reader :audio_format_settings_endianness, "Format settings, Endianness"
155
+ mediainfo_attr_reader :audio_format_settings_sign, "Format settings, Sign"
156
+ mediainfo_attr_reader :audio_codec_id, "Codec ID"
157
+ mediainfo_attr_reader :audio_codec_info, "Codec ID/Info"
158
+ mediainfo_attr_reader :audio_channel_positions
159
+
160
+ # XXX this breaks from RVideo::Inspector which returns
161
+ # strings like "mono" or "stereo" for this method.
162
+ mediainfo_int_reader :audio_channels, "Channel(s)"
163
+ def stereo?; 2 == audio_channels; end
164
+ def mono?; 1 == audio_channels; end
165
+
166
+ mediainfo_date_reader :audio_encoded_date
167
+ mediainfo_date_reader :audio_tagged_date
168
+
169
+ ### IMAGE
170
+
171
+ mediainfo_section_query :image
172
+ mediainfo_attr_reader :image_resolution
173
+ mediainfo_attr_reader :image_format
174
+
175
+ mediainfo_int_reader :image_width
176
+ mediainfo_int_reader :image_height
177
+
178
+ ###
179
+
180
+ attr_reader :raw_response, :parsed_response,
181
+ :full_filename, :filename, :path, :escaped_full_filename
182
+
183
+ def initialize(full_filename = nil)
184
+ if full_filename
185
+ @full_filename = File.expand_path full_filename
186
+ @path = File.dirname @full_filename
187
+ @filename = File.basename @full_filename
188
+
189
+ raise ArgumentError, "need a path to a video file, got nil" unless @full_filename
190
+ raise ArgumentError, "need a path to a video file, #{@full_filename} does not exist" unless File.exist? @full_filename
191
+
192
+ @escaped_full_filename = @full_filename.shell_escape
193
+
194
+ self.raw_response = mediainfo!
195
+ end
196
+ end
197
+
198
+ def raw_response=(mediainfo_cli_output)
199
+ @raw_response = mediainfo_cli_output
200
+ parse!
201
+ @raw_response
202
+ end
203
+
204
+ class << self; attr_accessor :path; end
205
+ def path; self.class.path; end
206
+
207
+ def self.default_mediainfo_path!; self.path = "mediainfo"; end
208
+ default_mediainfo_path! unless path
209
+
210
+ def mediainfo_version
211
+ `#{path} --Version`[/v([\d.]+)/, 1]
212
+ end
213
+
214
+ attr_reader :last_command
215
+
216
+ class Error < StandardError; end
217
+ class ExecutionError < Error; end
218
+
219
+ def inspect
220
+ super.sub /@raw_response=".+?", @/, %{@raw_response="...", @}
221
+ end
222
+
223
+ private
224
+ def mediainfo!
225
+ # for bash, see: http://www.faqs.org/docs/bashman/bashref_12.html
226
+ # but appears to be working for other shells: sh, zsh, ksh, dash
227
+ @last_command = "#{path} #{@escaped_full_filename}"
228
+ run_last_command!
229
+ end
230
+
231
+ def run_last_command!
232
+ raw_response = `#{@last_command}`
233
+ unless $? == 0
234
+ raise ExecutionError, "Execution of `#{@last_command}` failed: #{raw_response.inspect}"
235
+ end
236
+ raw_response
237
+ end
238
+
239
+ def parse!
240
+ @parsed_response = {}
241
+ subsection = nil
242
+
243
+ @raw_response.to_s.split("\n").map { |x| x.strip }.each do |line|
244
+ next if line.empty? || line == "General"
245
+
246
+ if SECTIONS.include? line
247
+ subsection = line
248
+ @parsed_response[subsection] = {}
249
+ next
250
+ end
251
+
252
+ bucket = @parsed_response
253
+ bucket = bucket[subsection] if subsection
254
+
255
+ key, value = line.split(":", 2).map { |x| x.strip }
256
+
257
+ bucket[key] = value
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,76 @@
1
+ require "rubygems"
2
+ require "active_support" # TODO selective includes once they are released
3
+
4
+ class Mediainfo
5
+ module AttrReaders
6
+ def supported_attributes
7
+ @supported_attributes ||= []
8
+ end
9
+
10
+ def mediainfo_attr_reader(name, mediainfo_key = nil)
11
+ supported_attributes << name
12
+ attr_name = "#{name}_before_type_cast"
13
+ define_method attr_name do
14
+ if v = instance_variable_get("@#{attr_name}")
15
+ v
16
+ else
17
+ v = if md = name.to_s.match(/^(#{SECTIONS.map { |x| x.underscore } * "|"})_(.+)$/)
18
+ k = mediainfo_key ? mediainfo_key : md[2].humanize.capitalize
19
+ if subsection = @parsed_response[md[1].capitalize]
20
+ subsection[k]
21
+ end
22
+ else
23
+ k = mediainfo_key ? mediainfo_key : name.to_s.humanize.capitalize
24
+ @parsed_response[k]
25
+ end
26
+
27
+ instance_variable_set "@#{attr_name}", v
28
+ v
29
+ end
30
+ end
31
+
32
+ define_method name do
33
+ if v = instance_variable_get("@#{name}")
34
+ v
35
+ else
36
+ v = send "#{name}_before_type_cast"
37
+ v = yield v if v and block_given?
38
+
39
+ instance_variable_set "@#{name}", v
40
+ v
41
+ end
42
+ end
43
+ end
44
+
45
+ def mediainfo_duration_reader(*a)
46
+ mediainfo_attr_reader *a do |v|
47
+ t = 0
48
+ v.split(/\s+/).each do |tf|
49
+ case tf
50
+ # XXX haven't actually seen hot they represent hours yet
51
+ # but hopefully this is ok.. :\
52
+ when /\d+h/ then t += tf.to_i * 60 * 60 * 1000
53
+ when /\d+mn/ then t += tf.to_i * 60 * 1000
54
+ when /\d+ms/ then t += tf.to_i
55
+ when /\d+s/ then t += tf.to_i * 1000
56
+ else
57
+ raise "unexpected time fragment! please report bug!"
58
+ end
59
+ end
60
+ t
61
+ end
62
+ end
63
+
64
+ def mediainfo_date_reader(*a)
65
+ mediainfo_attr_reader(*a) { |v| Time.parse v }
66
+ end
67
+
68
+ def mediainfo_int_reader(*a)
69
+ mediainfo_attr_reader(*a) { |v| v.gsub(/\D+/, "").to_i }
70
+ end
71
+
72
+ def mediainfo_section_query(name)
73
+ define_method("#{name}?") { @parsed_response.key? name.to_s.capitalize }
74
+ end
75
+ end
76
+ end