mediainfo 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +24 -0
- data/LICENSE +18 -0
- data/Manifest +9 -0
- data/README.markdown +40 -0
- data/Rakefile +37 -0
- data/lib/mediainfo.rb +260 -0
- data/lib/mediainfo/attr_readers.rb +76 -0
- data/lib/mediainfo/string.rb +9 -0
- data/mediainfo.gemspec +31 -0
- data/test/mediainfo_awaywego_encoded_test.rb +271 -0
- data/test/mediainfo_awaywego_test.rb +275 -0
- data/test/mediainfo_dinner_test.rb +257 -0
- data/test/mediainfo_hats_test.rb +257 -0
- data/test/mediainfo_string_test.rb +12 -0
- data/test/mediainfo_test.rb +112 -0
- data/test/mediainfo_vimeo_test.rb +257 -0
- data/test/mediainfo_vimeoimage_test.rb +234 -0
- data/test/test_helper.rb +18 -0
- metadata +80 -0
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
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
|