wmainfo-rb 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +35 -0
  2. data/lib/wmainfo.rb +407 -0
  3. metadata +46 -0
data/README ADDED
@@ -0,0 +1,35 @@
1
+ :: wmainfo-rb ::
2
+ Author: Darren Kirby
3
+ mailto:bulliver@badcomputer.org
4
+ License: Ruby
5
+
6
+ = Quick API docs =
7
+
8
+ == Initializing ==
9
+
10
+ require 'wmainfo'
11
+ foo = WmaInfo.new("someSong.wma")
12
+ ... or ...
13
+ foo = WmaInfo.new("someVideo.wmv", debug=1)
14
+
15
+ == Public attributes ==
16
+
17
+ @drm :: '1' if DRM present else 'None'
18
+ @tags :: dict of strings (id3 like data)
19
+ @info :: dict of variable types (non-id3 like data)
20
+ @headerObject :: dict of arrays (name, GUID, size and offset of ASF objects)
21
+
22
+ == Public methods ==
23
+
24
+ print_objects :: pretty-print header objects
25
+ hasdrm :: returns True if file has DRM
26
+ hastag('str') :: returns True if @tags['str'] exists
27
+ print_tags :: pretty-print @tags dict
28
+ hasinfo('str') :: returns True if @info['str'] exists
29
+ print_info :: pretty-print @info dict
30
+ parse_stream :: parse Asf_Stream_Property_Object
31
+ ... which will create another public attribute:
32
+ @stream :: dict of variable types (stream properties data)
33
+
34
+ For more/different documentation see http://badcomputer.org/unix/code/wmainfo/
35
+
@@ -0,0 +1,407 @@
1
+ # = Description
2
+ #
3
+ # wmainfo-ruby gives you access to low level information on wma/wmv files.
4
+ # * It identifies all "ASF_..." objects and shows each objects size
5
+ # * It returns info such as bitrate, size, length, creation date etc
6
+ # * It returns meta-tags from ASF_Content_Description_Object
7
+ #
8
+ # = Note:
9
+ #
10
+ # I wrestled with the ASF spec (150 page .doc format!) with no joy for
11
+ # a while, then found Dan Sully's Audio-WMA Perl module:
12
+ # (http://cpants.perl.org/dist/Audio-WMA :: http://www.slimdevices.com/)
13
+ # This entire library is essentially a translation of WMA.pm
14
+ # to Ruby. All credit for the hard work is owed to Dan...
15
+ #
16
+ # License:: Ruby
17
+ # Author:: Darren Kirby (mailto:bulliver@badcomputer.org)
18
+ # Website:: http://badcomputer.org/unix/code/wmainfo/
19
+
20
+
21
+ # raised when errors occur parsing wma header
22
+ class WmaInfoError < StandardError
23
+ end
24
+
25
+ class WmaInfo
26
+ # WmaInfo.tags and WmaInfo.info are hashes of NAME=VALUE pairs
27
+ # WmaInfo.headerObject is a hash of arrays
28
+ attr_reader :tags, :headerObject, :info, :stream
29
+ def initialize(file, debug=nil)
30
+ @drm = nil
31
+ @tags = {}
32
+ @headerObject = {}
33
+ @info = {}
34
+ @file = file
35
+ @debug = debug
36
+ parseWmaHeader
37
+ end
38
+
39
+ # for ASF_Header_Object prints: "name: GUID size num_objects"
40
+ # for others, prints: "name: GUID size offset"
41
+ def print_objects
42
+ @headerObject.each_pair do |key,val|
43
+ puts "#{key}: #{val[0]} #{val[1]} #{val[2]}"
44
+ end
45
+ end
46
+
47
+ # returns true if the file has DRM
48
+ # ie: if a "*Content_Encryption_Object" is found
49
+ def hasdrm?
50
+ @drm ? true : false
51
+ end
52
+
53
+ # returns true if tags["tag"] has a value
54
+ def hastag?(tag)
55
+ @tags["#{tag}"] ? true : false
56
+ end
57
+
58
+ # prettyprint WmaInfo.tags hash
59
+ def print_tags
60
+ @tags.each_pair { |key,val| puts "#{key}: #{val}" }
61
+ end
62
+
63
+ # returns true if info["field"] has a value
64
+ def hasinfo?(field)
65
+ @info["#{field}"] ? true : false
66
+ end
67
+
68
+ # prettyprint WmaInfo.info hash
69
+ def print_info
70
+ @info.each_pair { |key,val| puts "#{key}: #{val}" }
71
+ end
72
+
73
+ # I don't think most people will want/need this info
74
+ # so it is not parsed automatically
75
+ def parse_stream
76
+ begin
77
+ @stream = {}
78
+ offset = @headerObject['ASF_Stream_Properties_Object'][2]
79
+ parseASFStreamPropertiesObject(offset)
80
+ rescue
81
+ raise WmaInfoError, "Cannot grok ASF_Stream_Properties_Object", caller
82
+ end
83
+ end
84
+
85
+ # returns: "filename.wma :: Size: N bytes :: Bitrate: N kbps :: Duration: N seconds"
86
+ # this is useless
87
+ def to_s
88
+ "#{File.basename(@file)} :: Size: #{@size} bytes :: Bitrate: #{@info['bitrate']} kbps :: Duration: #{@info['playtime_seconds']} seconds"
89
+ end
90
+
91
+ private
92
+ def parseWmaHeader
93
+ @size = File.size(@file)
94
+ @fh = File.new(@file, "rb")
95
+ @offset = 0
96
+ @fileOffset = 30
97
+ @guidMapping = knownGUIDs
98
+ @reverseGuidMapping = @guidMapping.invert
99
+ require 'iconv'
100
+ @ic = Iconv.new("ASCII//IGNORE", "UTF-16LE")
101
+
102
+ # read first 30 bytes and parse ASF_Header_Object
103
+ begin
104
+ objectId = byteStringToGUID(@fh.read(16))
105
+ objectSize = @fh.read(8).unpack("V")[0]
106
+ headerObjects = @fh.read(4).unpack("V")[0]
107
+ reserved1 = @fh.read(1).unpack("b*")[0]
108
+ reserved2 = @fh.read(1).unpack("b*")[0]
109
+ objectIdName = @reverseGuidMapping[objectId]
110
+ rescue
111
+ # not getting raised when fed a non-wma file
112
+ # objectSize must be getting value because
113
+ # "Header size reported larger than file size"
114
+ # gets raised instead?
115
+ raise WmaInfoError, "Not a wma header", caller
116
+ end
117
+
118
+ if objectSize > @size
119
+ raise WmaInfoError, "Header size reported larger than file size", caller
120
+ end
121
+
122
+ @headerObject[objectIdName] = [objectId, objectSize, headerObjects, reserved1, reserved2]
123
+
124
+ if @debug
125
+ puts "objectId: #{objectId}"
126
+ puts "objectIdName: #{@reverseGuidMapping[objectId]}"
127
+ puts "objectSize: #{objectSize}"
128
+ puts "headerObjects: #{headerObjects}"
129
+ puts "reserved1: #{reserved1}"
130
+ puts "reserved2: #{reserved2}"
131
+ end
132
+
133
+ @headerData = @fh.read(objectSize - 30)
134
+ @fh.close
135
+ headerObjects.times do
136
+ nextObject = readAndIncrementOffset(16)
137
+ nextObjectText = byteStringToGUID(nextObject)
138
+ nextObjectSize = parse64BitString(readAndIncrementOffset(8))
139
+ nextObjectName = @reverseGuidMapping[nextObjectText];
140
+
141
+ @headerObject[nextObjectName] = [nextObjectText, nextObjectSize, @fileOffset]
142
+ @fileOffset += nextObjectSize
143
+
144
+ if @debug
145
+ puts "nextObjectGUID: #{nextObjectText}"
146
+ puts "nextObjectName: #{nextObjectName}"
147
+ puts "nextObjectSize: #{nextObjectSize}"
148
+ end
149
+
150
+ # start looking at object contents
151
+ if nextObjectName == 'ASF_File_Properties_Object'
152
+ parseASFFilePropertiesObject
153
+ next
154
+ elsif nextObjectName == 'ASF_Content_Description_Object'
155
+ parseASFContentDescriptionObject
156
+ next
157
+ elsif nextObjectName == 'ASF_Extended_Content_Description_Object'
158
+ parseASFExtendedContentDescriptionObject
159
+ next
160
+ elsif nextObjectName == 'ASF_Content_Encryption_Object' || nextObjectName == 'ASF_Extended_Content_Encryption_Object'
161
+ parseASFContentEncryptionObject
162
+ end
163
+
164
+ # set our next object size
165
+ @offset += nextObjectSize - 24
166
+ end
167
+
168
+ # meta-tag like values go to 'tags' all others to 'info'
169
+ @ext_info.each do |k,v|
170
+ if k =~ /WM\/(TrackNumber|AlbumTitle|AlbumArtist|Genre|Year|Composer|Mood|Lyrics|BeatsPerMinute|Publisher)/
171
+ @tags[k.gsub(/WM\//, "")] = v # dump "WM/"
172
+ else
173
+ @info[k] = v
174
+ end
175
+ end
176
+
177
+ # dump empty tags
178
+ @tags.delete_if { |k,v| v == "" || v == nil }
179
+ end
180
+
181
+ def parseASFContentEncryptionObject
182
+ @drm = 1
183
+ end
184
+
185
+ def parseASFFilePropertiesObject
186
+ fileid = readAndIncrementOffset(16)
187
+ @info['fileid_guid'] = byteStringToGUID(fileid)
188
+ @info['filesize'] = parse64BitString(readAndIncrementOffset(8))
189
+ @info['creation_date'] = readAndIncrementOffset(8).unpack("Q")[0]
190
+ @info['creation_date_unix'] = fileTimeToUnixTime(@info['creation_date'])
191
+ @info['creation_string'] = Time.at(@info['creation_date_unix'].to_i)
192
+ @info['data_packets'] = readAndIncrementOffset(8).unpack("V")[0]
193
+ @info['play_duration'] = parse64BitString(readAndIncrementOffset(8))
194
+ @info['send_duration'] = parse64BitString(readAndIncrementOffset(8))
195
+ @info['preroll'] = readAndIncrementOffset(8).unpack("V")[0]
196
+ @info['playtime_seconds'] = (@info['play_duration'] / 10000000) - (@info['preroll'] / 1000)
197
+ flags_raw = readAndIncrementOffset(4).unpack("V")[0]
198
+ if flags_raw & 0x0001 == 0
199
+ @info['broadcast'] = 0
200
+ else
201
+ @info['broadcast'] = 1
202
+ end
203
+ if flags_raw & 0x0002 == 0
204
+ @info['seekable'] = 0
205
+ else
206
+ @info['seekable'] = 1
207
+ end
208
+ @info['min_packet_size'] = readAndIncrementOffset(4).unpack("V")[0]
209
+ @info['max_packet_size'] = readAndIncrementOffset(4).unpack("V")[0]
210
+ @info['max_bitrate'] = readAndIncrementOffset(4).unpack("V")[0]
211
+ @info['bitrate'] = @info['max_bitrate'] / 1000
212
+
213
+ if @debug
214
+ @info.each_pair { |key,val| puts "#{key}: #{val}" }
215
+ end
216
+
217
+ end
218
+
219
+ def parseASFContentDescriptionObject
220
+ lengths = {}
221
+ keys = %w/Title Author Copyright Description Rating/
222
+ keys.each do |key| # read the lengths of each key
223
+ lengths[key] = readAndIncrementOffset(2).unpack("v")[0]
224
+ end
225
+ keys.each do |key| # now pull the data based on length
226
+ @tags[key] = decodeBinaryString(readAndIncrementOffset(lengths[key]))
227
+ end
228
+ end
229
+
230
+ def parseASFExtendedContentDescriptionObject
231
+ @ext_info = {}
232
+ @ext_info['content_count'] = readAndIncrementOffset(2).unpack("v")[0]
233
+ @ext_info['content_count'].times do |n|
234
+ ext = {}
235
+ ext['base_offset'] = @offset + 30
236
+ ext['name_length'] = readAndIncrementOffset(2).unpack("v")[0]
237
+ ext['name'] = decodeBinaryString(readAndIncrementOffset(ext['name_length']))
238
+ ext['value_type'] = readAndIncrementOffset(2).unpack("v")[0]
239
+ ext['value_length'] = readAndIncrementOffset(2).unpack("v")[0]
240
+
241
+ value = readAndIncrementOffset(ext['value_length'])
242
+ if ext['value_type'] <= 1
243
+ ext['value'] = decodeBinaryString(value)
244
+ elsif ext['value_type'] == 4
245
+ ext['value'] = parse64BitString(value)
246
+ else
247
+ valTypeTemplates = ["", "", "V", "V", "", "v"]
248
+ ext['value'] = value.unpack(valTypeTemplates[ext['value_type']])[0]
249
+ end
250
+
251
+ if @debug
252
+ puts "base_offset: #{ext['base_offset']}"
253
+ puts "name length: #{ext['name_length']}"
254
+ puts "name: #{ext['name']}"
255
+ puts "value type: #{ext['value_type']}"
256
+ puts "value length: #{ext['value_length']}"
257
+ puts "value: #{ext['value']}"
258
+ end
259
+
260
+ @ext_info["#{ext['name']}"] = ext['value']
261
+ end
262
+ end
263
+
264
+ def parseASFStreamPropertiesObject(offset)
265
+ @offset = offset - 6 # gained an extra 6 bytes somewhere?!
266
+
267
+ streamType = readAndIncrementOffset(16)
268
+ @stream['stream_type_guid'] = byteStringToGUID(streamType)
269
+ @stream['stream_type_name'] = @reverseGuidMapping[@stream['stream_type_guid']]
270
+ errorType = readAndIncrementOffset(16)
271
+ @stream['error_correct_guid'] = byteStringToGUID(errorType)
272
+ @stream['error_correct_name'] = @reverseGuidMapping[@stream['error_correct_guid']]
273
+
274
+ @stream['time_offset'] = readAndIncrementOffset(8).unpack("4v")[0]
275
+ @stream['type_data_length'] = readAndIncrementOffset(4).unpack("2v")[0]
276
+ @stream['error_data_length'] = readAndIncrementOffset(4).unpack("2v")[0]
277
+ flags_raw = readAndIncrementOffset(2).unpack("v")[0]
278
+ @stream['stream_number'] = flags_raw & 0x007F
279
+ @stream['encrypted'] = flags_raw & 0x8000
280
+
281
+ # reserved - set to zero
282
+ readAndIncrementOffset(4)
283
+
284
+ @stream['type_specific_data'] = readAndIncrementOffset(@stream['type_data_length'])
285
+ @stream['error_correct_data'] = readAndIncrementOffset(@stream['error_data_length'])
286
+
287
+ if @stream['stream_type_name'] == 'ASF_Audio_Media'
288
+ parseASFAudioMediaObject
289
+ end
290
+ end
291
+
292
+ def parseASFAudioMediaObject
293
+ data = @stream['type_specific_data'][0...16]
294
+ @stream['audio_channels'] = data[2...4].unpack("v")[0]
295
+ @stream['audio_sample_rate'] = data[4...8].unpack("2v")[0]
296
+ @stream['audio_bitrate'] = data[8...12].unpack("2v")[0] * 8
297
+ @stream['audio_bits_per_sample'] = data[14...16].unpack("v")[0]
298
+ end
299
+
300
+ # UTF16LE -> ASCII ... am still not happy with this
301
+ def decodeBinaryString(data)
302
+ textString = @ic.iconv(data[0, data.length / 2 * 2] + "\000\000")[0..-2]
303
+ textString.sub!(/\x00$/, '')
304
+ end
305
+
306
+ def readAndIncrementOffset(size)
307
+ value = @headerData[@offset..(@offset + size)]
308
+ @offset += size
309
+ return value
310
+ end
311
+
312
+ def byteStringToGUID(byteString)
313
+ guidString = sprintf("%02X", byteString[3])
314
+ guidString += sprintf("%02X", byteString[2])
315
+ guidString += sprintf("%02X", byteString[1])
316
+ guidString += sprintf("%02X", byteString[0])
317
+ guidString += '-'
318
+ guidString += sprintf("%02X", byteString[5])
319
+ guidString += sprintf("%02X", byteString[4])
320
+ guidString += '-'
321
+ guidString += sprintf("%02X", byteString[7])
322
+ guidString += sprintf("%02X", byteString[6])
323
+ guidString += '-'
324
+ guidString += sprintf("%02X", byteString[8])
325
+ guidString += sprintf("%02X", byteString[9])
326
+ guidString += '-'
327
+ guidString += sprintf("%02X", byteString[10])
328
+ guidString += sprintf("%02X", byteString[11])
329
+ guidString += sprintf("%02X", byteString[12])
330
+ guidString += sprintf("%02X", byteString[13])
331
+ guidString += sprintf("%02X", byteString[14])
332
+ guidString += sprintf("%02X", byteString[15])
333
+ end
334
+
335
+ def parse64BitString(data)
336
+ d = data.unpack('VV')
337
+ d[1] * 2 ** 32 + d[0]
338
+ end
339
+
340
+ def fileTimeToUnixTime(time)
341
+ (time - 116444736000000000) / 10000000
342
+ end
343
+
344
+ def knownGUIDs
345
+ guidMapping = {
346
+ 'ASF_Extended_Stream_Properties_Object' => '14E6A5CB-C672-4332-8399-A96952065B5A',
347
+ 'ASF_Padding_Object' => '1806D474-CADF-4509-A4BA-9AABCB96AAE8',
348
+ 'ASF_Payload_Ext_Syst_Pixel_Aspect_Ratio' => '1B1EE554-F9EA-4BC8-821A-376B74E4C4B8',
349
+ 'ASF_Script_Command_Object' => '1EFB1A30-0B62-11D0-A39B-00A0C90348F6',
350
+ 'ASF_No_Error_Correction' => '20FB5700-5B55-11CF-A8FD-00805F5C442B',
351
+ 'ASF_Content_Branding_Object' => '2211B3FA-BD23-11D2-B4B7-00A0C955FC6E',
352
+ 'ASF_Content_Encryption_Object' => '2211B3FB-BD23-11D2-B4B7-00A0C955FC6E',
353
+ 'ASF_Digital_Signature_Object' => '2211B3FC-BD23-11D2-B4B7-00A0C955FC6E',
354
+ 'ASF_Extended_Content_Encryption_Object' => '298AE614-2622-4C17-B935-DAE07EE9289C',
355
+ 'ASF_Simple_Index_Object' => '33000890-E5B1-11CF-89F4-00A0C90349CB',
356
+ 'ASF_Degradable_JPEG_Media' => '35907DE0-E415-11CF-A917-00805F5C442B',
357
+ 'ASF_Payload_Extension_System_Timecode' => '399595EC-8667-4E2D-8FDB-98814CE76C1E',
358
+ 'ASF_Binary_Media' => '3AFB65E2-47EF-40F2-AC2C-70A90D71D343',
359
+ 'ASF_Timecode_Index_Object' => '3CB73FD0-0C4A-4803-953D-EDF7B6228F0C',
360
+ 'ASF_Metadata_Library_Object' => '44231C94-9498-49D1-A141-1D134E457054',
361
+ 'ASF_Reserved_3' => '4B1ACBE3-100B-11D0-A39B-00A0C90348F6',
362
+ 'ASF_Reserved_4' => '4CFEDB20-75F6-11CF-9C0F-00A0C90349CB',
363
+ 'ASF_Command_Media' => '59DACFC0-59E6-11D0-A3AC-00A0C90348F6',
364
+ 'ASF_Header_Extension_Object' => '5FBF03B5-A92E-11CF-8EE3-00C00C205365',
365
+ 'ASF_Media_Object_Index_Parameters_Obj' => '6B203BAD-3F11-4E84-ACA8-D7613DE2CFA7',
366
+ 'ASF_Header_Object' => '75B22630-668E-11CF-A6D9-00AA0062CE6C',
367
+ 'ASF_Content_Description_Object' => '75B22633-668E-11CF-A6D9-00AA0062CE6C',
368
+ 'ASF_Error_Correction_Object' => '75B22635-668E-11CF-A6D9-00AA0062CE6C',
369
+ 'ASF_Data_Object' => '75B22636-668E-11CF-A6D9-00AA0062CE6C',
370
+ 'ASF_Web_Stream_Media_Subtype' => '776257D4-C627-41CB-8F81-7AC7FF1C40CC',
371
+ 'ASF_Stream_Bitrate_Properties_Object' => '7BF875CE-468D-11D1-8D82-006097C9A2B2',
372
+ 'ASF_Language_List_Object' => '7C4346A9-EFE0-4BFC-B229-393EDE415C85',
373
+ 'ASF_Codec_List_Object' => '86D15240-311D-11D0-A3A4-00A0C90348F6',
374
+ 'ASF_Reserved_2' => '86D15241-311D-11D0-A3A4-00A0C90348F6',
375
+ 'ASF_File_Properties_Object' => '8CABDCA1-A947-11CF-8EE4-00C00C205365',
376
+ 'ASF_File_Transfer_Media' => '91BD222C-F21C-497A-8B6D-5AA86BFC0185',
377
+ 'ASF_Advanced_Mutual_Exclusion_Object' => 'A08649CF-4775-4670-8A16-6E35357566CD',
378
+ 'ASF_Bandwidth_Sharing_Object' => 'A69609E6-517B-11D2-B6AF-00C04FD908E9',
379
+ 'ASF_Reserved_1' => 'ABD3D211-A9BA-11cf-8EE6-00C00C205365',
380
+ 'ASF_Bandwidth_Sharing_Exclusive' => 'AF6060AA-5197-11D2-B6AF-00C04FD908E9',
381
+ 'ASF_Bandwidth_Sharing_Partial' => 'AF6060AB-5197-11D2-B6AF-00C04FD908E9',
382
+ 'ASF_JFIF_Media' => 'B61BE100-5B4E-11CF-A8FD-00805F5C442B',
383
+ 'ASF_Stream_Properties_Object' => 'B7DC0791-A9B7-11CF-8EE6-00C00C205365',
384
+ 'ASF_Video_Media' => 'BC19EFC0-5B4D-11CF-A8FD-00805F5C442B',
385
+ 'ASF_Audio_Spread' => 'BFC3CD50-618F-11CF-8BB2-00AA00B4E220',
386
+ 'ASF_Metadata_Object' => 'C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA',
387
+ 'ASF_Payload_Ext_Syst_Sample_Duration' => 'C6BD9450-867F-4907-83A3-C77921B733AD',
388
+ 'ASF_Group_Mutual_Exclusion_Object' => 'D1465A40-5A79-4338-B71B-E36B8FD6C249',
389
+ 'ASF_Extended_Content_Description_Object' => 'D2D0A440-E307-11D2-97F0-00A0C95EA850',
390
+ 'ASF_Stream_Prioritization_Object' => 'D4FED15B-88D3-454F-81F0-ED5C45999E24',
391
+ 'ASF_Payload_Ext_System_Content_Type' => 'D590DC20-07BC-436C-9CF7-F3BBFBF1A4DC',
392
+ 'ASF_Index_Object' => 'D6E229D3-35DA-11D1-9034-00A0C90349BE',
393
+ 'ASF_Bitrate_Mutual_Exclusion_Object' => 'D6E229DC-35DA-11D1-9034-00A0C90349BE',
394
+ 'ASF_Index_Parameters_Object' => 'D6E229DF-35DA-11D1-9034-00A0C90349BE',
395
+ 'ASF_Mutex_Language' => 'D6E22A00-35DA-11D1-9034-00A0C90349BE',
396
+ 'ASF_Mutex_Bitrate' => 'D6E22A01-35DA-11D1-9034-00A0C90349BE',
397
+ 'ASF_Mutex_Unknown' => 'D6E22A02-35DA-11D1-9034-00A0C90349BE',
398
+ 'ASF_Web_Stream_Format' => 'DA1E6B13-8359-4050-B398-388E965BF00C',
399
+ 'ASF_Payload_Ext_System_File_Name' => 'E165EC0E-19ED-45D7-B4A7-25CBD1E28E9B',
400
+ 'ASF_Marker_Object' => 'F487CD01-A951-11CF-8EE6-00C00C205365',
401
+ 'ASF_Timecode_Index_Parameters_Object' => 'F55E496D-9797-4B5D-8C8B-604DFE9BFB24',
402
+ 'ASF_Audio_Media' => 'F8699E40-5B4D-11CF-A8FD-00805F5C442B',
403
+ 'ASF_Media_Object_Index_Object' => 'FEB103F8-12AD-4C64-840F-2A1D2F7AD48C',
404
+ 'ASF_Alt_Extended_Content_Encryption_Obj' => 'FF889EF1-ADEE-40DA-9E71-98704BB928CE',
405
+ }
406
+ end
407
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: wmainfo-rb
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.3"
7
+ date: 2006-08-05 00:00:00 -07:00
8
+ summary: Pure Ruby lib for accessing info/tags from wma/wmv files
9
+ require_paths:
10
+ - lib
11
+ email: bulliver@badcomputer.org
12
+ homepage: http://badcomputer.org/unix/code/wmainfo/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: wmainfo
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Darren Kirby
30
+ files:
31
+ - README
32
+ - lib/wmainfo.rb
33
+ test_files: []
34
+
35
+ rdoc_options: []
36
+
37
+ extra_rdoc_files:
38
+ - README
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ requirements: []
44
+
45
+ dependencies: []
46
+