wmainfo-rb 0.3

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.
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
+