ruby-mp3info 0.6.4 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -0
- data/lib/mp3info.rb +85 -49
- data/lib/mp3info/extension_modules.rb +0 -2
- data/lib/mp3info/id3v2.rb +19 -17
- data/test/test_ruby-mp3info.rb +55 -24
- metadata +3 -3
data/History.txt
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
=== 0.6.5 / 2008-04-19
|
2
|
+
|
3
|
+
* added Mp3Info#audio_content method, to return "audio-only" boundaries from mp3, i.e. data without tags
|
4
|
+
(closes feature request #17230)
|
5
|
+
* bugfix on reading id3 2.4 (size not syncsafed)
|
6
|
+
* more robust tag decoding with bad tags
|
7
|
+
|
1
8
|
=== 0.6.4 / 2008-04-16
|
2
9
|
|
3
10
|
* added @tag2["disc_number"] and @tag2["disc_total"] mirroring TPOS attribute (thanks to Harry Ohlsen)
|
data/lib/mp3info.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# $Id: mp3info.rb
|
1
|
+
# $Id: mp3info.rb 82 2008-04-19 10:59:02Z moumar $
|
2
2
|
# License:: Ruby
|
3
3
|
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
4
4
|
# Website:: http://ruby-mp3info.rubyforge.org/
|
@@ -17,7 +17,7 @@ end
|
|
17
17
|
|
18
18
|
class Mp3Info
|
19
19
|
|
20
|
-
VERSION = "0.6.
|
20
|
+
VERSION = "0.6.5"
|
21
21
|
|
22
22
|
LAYER = [ nil, 3, 2, 1]
|
23
23
|
BITRATE = [
|
@@ -65,7 +65,7 @@ class Mp3Info
|
|
65
65
|
"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
|
66
66
|
"SynthPop" ]
|
67
67
|
|
68
|
-
|
68
|
+
TAG1_SIZE = 128
|
69
69
|
#MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
|
70
70
|
V1_V2_TAG_MAPPING = {
|
71
71
|
"title" => "TIT2",
|
@@ -132,7 +132,7 @@ class Mp3Info
|
|
132
132
|
# Test the presence of an id3v1 tag in file +filename+
|
133
133
|
def self.hastag1?(filename)
|
134
134
|
File.open(filename) { |f|
|
135
|
-
f.seek(-
|
135
|
+
f.seek(-TAG1_SIZE, File::SEEK_END)
|
136
136
|
f.read(3) == "TAG"
|
137
137
|
}
|
138
138
|
end
|
@@ -148,7 +148,7 @@ class Mp3Info
|
|
148
148
|
# Remove id3v1 tag from +filename+
|
149
149
|
def self.removetag1(filename)
|
150
150
|
if self.hastag1?(filename)
|
151
|
-
newsize = File.size(filename) -
|
151
|
+
newsize = File.size(filename) - TAG1_SIZE
|
152
152
|
File.open(filename, "rb+") { |f| f.truncate(newsize) }
|
153
153
|
end
|
154
154
|
end
|
@@ -172,7 +172,6 @@ class Mp3Info
|
|
172
172
|
# reload (or load for the first time) the file from disk
|
173
173
|
def reload
|
174
174
|
raise(Mp3InfoError, "empty file") unless File.size?(@filename)
|
175
|
-
@hastag1 = false
|
176
175
|
|
177
176
|
@tag1 = {}
|
178
177
|
@tag1.extend(HashKeys)
|
@@ -262,7 +261,7 @@ class Mp3Info
|
|
262
261
|
@vbr = true
|
263
262
|
else
|
264
263
|
# for cbr, calculate duration with the given bitrate
|
265
|
-
@streamsize = @file.stat.size - (
|
264
|
+
@streamsize = @file.stat.size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
|
266
265
|
@length = ((@streamsize << 3)/1000.0)/@bitrate
|
267
266
|
if @tag2["TLEN"]
|
268
267
|
# but if another duration is given and it isn't close (within 5%)
|
@@ -301,10 +300,7 @@ class Mp3Info
|
|
301
300
|
|
302
301
|
# Remove id3v1 from mp3
|
303
302
|
def removetag1
|
304
|
-
|
305
|
-
Mp3Info.removetag1(@filename)
|
306
|
-
@tag1.clear
|
307
|
-
end
|
303
|
+
@tag1.clear
|
308
304
|
self
|
309
305
|
end
|
310
306
|
|
@@ -316,12 +312,12 @@ class Mp3Info
|
|
316
312
|
|
317
313
|
# Does the file has an id3v1 or v2 tag?
|
318
314
|
def hastag?
|
319
|
-
|
315
|
+
hastag1? or hastag2?
|
320
316
|
end
|
321
317
|
|
322
318
|
# Does the file has an id3v1 tag?
|
323
319
|
def hastag1?
|
324
|
-
|
320
|
+
!@tag1.empty?
|
325
321
|
end
|
326
322
|
|
327
323
|
# Does the file has an id3v2 tag?
|
@@ -333,6 +329,23 @@ class Mp3Info
|
|
333
329
|
def rename(new_filename)
|
334
330
|
@filename = new_filename
|
335
331
|
end
|
332
|
+
|
333
|
+
# this method returns the "audio-only" data boundaries of the file,
|
334
|
+
# i.e. content stripped form tags. Useful to compare 2 files with the same
|
335
|
+
# audio content but with differents tags. Returned value is an array
|
336
|
+
# [position_in_the_file, length_of_the_data]
|
337
|
+
def audio_content
|
338
|
+
pos = 0
|
339
|
+
length = File.size(@filename)
|
340
|
+
if hastag1?
|
341
|
+
length -= TAG1_SIZE
|
342
|
+
end
|
343
|
+
if hastag2?
|
344
|
+
pos = @tag2.io_position
|
345
|
+
length -= @tag2.io_position
|
346
|
+
end
|
347
|
+
[pos, length]
|
348
|
+
end
|
336
349
|
|
337
350
|
# Flush pending modifications to tags and close the file
|
338
351
|
def close
|
@@ -358,24 +371,29 @@ class Mp3Info
|
|
358
371
|
#@tag1_orig.update(@tag1)
|
359
372
|
@tag1_orig = @tag1.dup
|
360
373
|
File.open(@filename, 'rb+') do |file|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
374
|
+
if @tag1_orig.empty?
|
375
|
+
newsize = file.stat.size - TAG1_SIZE
|
376
|
+
file.truncate(newsize)
|
377
|
+
else
|
378
|
+
file.seek(-TAG1_SIZE, File::SEEK_END)
|
379
|
+
t = file.read(3)
|
380
|
+
if t != 'TAG'
|
381
|
+
#append new tag
|
382
|
+
file.seek(0, File::SEEK_END)
|
383
|
+
file.write('TAG')
|
384
|
+
end
|
385
|
+
str = [
|
386
|
+
@tag1_orig["title"]||"",
|
387
|
+
@tag1_orig["artist"]||"",
|
388
|
+
@tag1_orig["album"]||"",
|
389
|
+
((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
|
390
|
+
@tag1_orig["comments"]||"",
|
391
|
+
0,
|
392
|
+
@tag1_orig["tracknum"]||0,
|
393
|
+
@tag1_orig["genre"]||255
|
394
|
+
].pack("Z30Z30Z30Z4Z28CCC")
|
395
|
+
file.write(str)
|
396
|
+
end
|
379
397
|
end
|
380
398
|
end
|
381
399
|
|
@@ -412,11 +430,18 @@ class Mp3Info
|
|
412
430
|
end
|
413
431
|
end
|
414
432
|
|
433
|
+
# close and reopen the file, i.e. commit changes to disk and
|
434
|
+
# reload it
|
435
|
+
def flush
|
436
|
+
close
|
437
|
+
reload
|
438
|
+
end
|
439
|
+
|
415
440
|
# inspect inside Mp3Info
|
416
441
|
def to_s
|
417
442
|
s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. error protection #{@error_protection} "
|
418
|
-
s << "tag1: "+@tag1.inspect+"\n" if
|
419
|
-
s << "tag2: "+@tag2.inspect+"\n" if
|
443
|
+
s << "tag1: "+@tag1.inspect+"\n" if hastag1?
|
444
|
+
s << "tag2: "+@tag2.inspect+"\n" if hastag2?
|
420
445
|
s
|
421
446
|
end
|
422
447
|
|
@@ -425,24 +450,28 @@ private
|
|
425
450
|
|
426
451
|
### parses the id3 tags of the currently open @file
|
427
452
|
def parse_tags
|
428
|
-
return if @file.stat.size <
|
453
|
+
return if @file.stat.size < TAG1_SIZE # file is too small
|
454
|
+
@tag1_parsed = false
|
429
455
|
@file.seek(0)
|
430
456
|
f3 = @file.read(3)
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
raise(Mp3InfoError, e.message)
|
457
|
+
# v1 tag at beginning
|
458
|
+
if f3 == "TAG"
|
459
|
+
gettag1
|
460
|
+
@tag1_parsed = true
|
436
461
|
end
|
462
|
+
|
463
|
+
@tag2.from_io(@file) if f3 == "ID3" # v2 tag at beginning
|
437
464
|
|
438
|
-
unless @
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
465
|
+
unless @tag1_parsed # v1 tag at end
|
466
|
+
# this preserves the file pos if tag2 found, since gettag2 leaves
|
467
|
+
# the file at the best guess as to the first MPEG frame
|
468
|
+
pos = (@tag2.valid? ? @file.pos : 0)
|
469
|
+
# seek to where id3v1 tag should be
|
470
|
+
@file.seek(-TAG1_SIZE, IO::SEEK_END)
|
471
|
+
if @file.read(3) == "TAG"
|
472
|
+
gettag1
|
473
|
+
end
|
474
|
+
@file.seek(pos)
|
446
475
|
end
|
447
476
|
end
|
448
477
|
|
@@ -465,7 +494,14 @@ private
|
|
465
494
|
### gets id3v1 tag information from @file
|
466
495
|
### assumes @file is pointing to char after "TAG" id
|
467
496
|
def gettag1
|
468
|
-
@
|
497
|
+
@tag1_parsed = true
|
498
|
+
=begin
|
499
|
+
# FIXME remove that
|
500
|
+
pos = @file.pos
|
501
|
+
p @file.read
|
502
|
+
@file.seek(pos)
|
503
|
+
# FIXME remove that
|
504
|
+
=end
|
469
505
|
@tag1["title"] = read_id3_string(30)
|
470
506
|
@tag1["artist"] = read_id3_string(30)
|
471
507
|
@tag1["album"] = read_id3_string(30)
|
@@ -500,7 +536,7 @@ private
|
|
500
536
|
dummyproof.times do |i|
|
501
537
|
if @file.getc == 0xff
|
502
538
|
data = @file.read(3)
|
503
|
-
|
539
|
+
raise(Mp3InfoError, "end of file reached") if @file.eof?
|
504
540
|
head = 0xff000000 + (data[0] << 16) + (data[1] << 8) + data[2]
|
505
541
|
if check_head(head)
|
506
542
|
return head
|
data/lib/mp3info/id3v2.rb
CHANGED
@@ -3,6 +3,8 @@ require "iconv"
|
|
3
3
|
|
4
4
|
require "mp3info/extension_modules"
|
5
5
|
|
6
|
+
class ID3v2Error < StandardError ; end
|
7
|
+
|
6
8
|
# This class is not intended to be used directly
|
7
9
|
class ID3v2 < DelegateClass(Hash)
|
8
10
|
|
@@ -146,7 +148,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
146
148
|
@io.extend(Mp3Info::Mp3FileMethods)
|
147
149
|
version_maj, version_min, flags = @io.read(3).unpack("CCB4")
|
148
150
|
@unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
|
149
|
-
raise("can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
|
151
|
+
raise(ID3v2Error, "can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
|
150
152
|
@version_maj, @version_min = version_maj, version_min
|
151
153
|
@valid = true
|
152
154
|
@tag_length = @io.get_syncsafe
|
@@ -196,7 +198,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
196
198
|
tag_str = "ID3"
|
197
199
|
#version_maj, version_min, unsync, ext_header, experimental, footer
|
198
200
|
tag_str << [ WRITE_VERSION, 0, "0000" ].pack("CCB4")
|
199
|
-
tag_str << to_syncsafe(tag.size)
|
201
|
+
tag_str << [to_syncsafe(tag.size)].pack("N")
|
200
202
|
tag_str << tag
|
201
203
|
p tag_str if $DEBUG
|
202
204
|
tag_str
|
@@ -228,19 +230,19 @@ class ID3v2 < DelegateClass(Hash)
|
|
228
230
|
end
|
229
231
|
|
230
232
|
### Read a tag from file and perform UNICODE translation if needed
|
231
|
-
def decode_tag(name,
|
232
|
-
puts("decode_tag(#{name.inspect}, #{
|
233
|
+
def decode_tag(name, raw_value)
|
234
|
+
puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
|
233
235
|
case name
|
234
236
|
when "COMM"
|
235
237
|
#FIXME improve this
|
236
|
-
encoding, lang, str =
|
237
|
-
out =
|
238
|
+
encoding, lang, str = raw_value.unpack("ca3a*")
|
239
|
+
out = raw_value.split(0.chr).last
|
238
240
|
when /^T/
|
239
|
-
encoding =
|
240
|
-
out =
|
241
|
+
encoding = raw_value[0] # language encoding (see TEXT_ENCODINGS constant)
|
242
|
+
out = raw_value[1..-1]
|
241
243
|
# we need to convert the string in order to match
|
242
244
|
# the requested encoding
|
243
|
-
if encoding != @text_encoding_index
|
245
|
+
if out && encoding != @text_encoding_index
|
244
246
|
begin
|
245
247
|
Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out)[0]
|
246
248
|
rescue Iconv::Failure
|
@@ -250,7 +252,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
250
252
|
out
|
251
253
|
end
|
252
254
|
else
|
253
|
-
|
255
|
+
raw_value
|
254
256
|
end
|
255
257
|
end
|
256
258
|
|
@@ -264,11 +266,13 @@ class ID3v2 < DelegateClass(Hash)
|
|
264
266
|
seek_to_v2_end
|
265
267
|
break
|
266
268
|
else
|
267
|
-
|
268
|
-
|
269
|
-
|
269
|
+
if @version_maj == 4
|
270
|
+
size = @io.get_syncsafe
|
271
|
+
else
|
272
|
+
size = @io.get32bits
|
273
|
+
end
|
274
|
+
@io.seek(2, IO::SEEK_CUR) # skip flags
|
270
275
|
puts "name '#{name}' size #{size}" if $DEBUG
|
271
|
-
#@io.seek(2, IO::SEEK_CUR) # skip flags
|
272
276
|
add_value_to_tag2(name, size)
|
273
277
|
end
|
274
278
|
break if @io.pos >= @tag_length # 2. reach length from header
|
@@ -297,7 +301,6 @@ class ID3v2 < DelegateClass(Hash)
|
|
297
301
|
### create an array if the key already exists in the tag
|
298
302
|
def add_value_to_tag2(name, size)
|
299
303
|
puts "add_value_to_tag2" if $DEBUG
|
300
|
-
raise("tag size too big for tag #{name.inspect} unsync #{@unsync} ") if size > 50_000_000
|
301
304
|
data_io = @io.read(size)
|
302
305
|
data = decode_tag(name, data_io)
|
303
306
|
|
@@ -328,8 +331,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
328
331
|
|
329
332
|
### convert an 32 integer to a syncsafe string
|
330
333
|
def to_syncsafe(num)
|
331
|
-
|
332
|
-
[n].pack("N")
|
334
|
+
( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
|
333
335
|
end
|
334
336
|
|
335
337
|
# def method_missing(meth, *args)
|
data/test/test_ruby-mp3info.rb
CHANGED
@@ -11,7 +11,8 @@ require "tempfile"
|
|
11
11
|
class Mp3InfoTest < Test::Unit::TestCase
|
12
12
|
|
13
13
|
TEMP_FILE = File.join(File.dirname($0), "test_mp3info.mp3")
|
14
|
-
|
14
|
+
|
15
|
+
DUMMY_TAG2 = {
|
15
16
|
"COMM" => "comments",
|
16
17
|
#"TCON" => "genre_s"
|
17
18
|
"TIT2" => "title",
|
@@ -20,7 +21,17 @@ class Mp3InfoTest < Test::Unit::TestCase
|
|
20
21
|
"TYER" => "year",
|
21
22
|
"TRCK" => "tracknum"
|
22
23
|
}
|
23
|
-
|
24
|
+
|
25
|
+
DUMMY_TAG1 = {
|
26
|
+
"title" => "toto",
|
27
|
+
"artist" => "artist 123",
|
28
|
+
"album" => "ALBUMM",
|
29
|
+
"year" => 1934,
|
30
|
+
"tracknum" => 14,
|
31
|
+
"comments" => "comment me",
|
32
|
+
"genre" => 233
|
33
|
+
}
|
34
|
+
|
24
35
|
def setup
|
25
36
|
# Command to create a dummy MP3
|
26
37
|
# dd if=/dev/zero bs=1024 count=15 | lame --preset cbr 128 -r -s 44.1 --bitwidth 16 - - | ruby -rbase64 -e 'print Base64.encode64($stdin.read)'
|
@@ -229,7 +240,7 @@ EOF
|
|
229
240
|
end
|
230
241
|
|
231
242
|
def test_id3v2_version
|
232
|
-
written_tag = write_temp_file(
|
243
|
+
written_tag = write_temp_file(DUMMY_TAG2)
|
233
244
|
assert_equal( "2.#{ID3v2::WRITE_VERSION}.0", written_tag.version )
|
234
245
|
end
|
235
246
|
|
@@ -244,9 +255,9 @@ EOF
|
|
244
255
|
end
|
245
256
|
|
246
257
|
def test_id3v2_basic
|
247
|
-
w = write_temp_file(
|
248
|
-
assert_equal(
|
249
|
-
id3v2_prog_test(
|
258
|
+
w = write_temp_file(DUMMY_TAG2)
|
259
|
+
assert_equal(DUMMY_TAG2, w)
|
260
|
+
id3v2_prog_test(DUMMY_TAG2, w)
|
250
261
|
end
|
251
262
|
|
252
263
|
#test the tag with the "id3v2" program
|
@@ -295,7 +306,7 @@ EOF
|
|
295
306
|
end
|
296
307
|
|
297
308
|
def test_leading_char_gets_chopped
|
298
|
-
tag2 =
|
309
|
+
tag2 = DUMMY_TAG2.dup
|
299
310
|
tag2["WOAR"] = "http://foo.bar"
|
300
311
|
w = write_temp_file(tag2)
|
301
312
|
assert_equal("http://foo.bar", w["WOAR"])
|
@@ -316,27 +327,11 @@ EOF
|
|
316
327
|
mp3.reload
|
317
328
|
assert !mp3.tag1.empty?, "tag is empty"
|
318
329
|
mp3.removetag1
|
319
|
-
mp3.
|
320
|
-
mp3.reload
|
330
|
+
mp3.flush
|
321
331
|
assert mp3.tag1.empty?, "tag is not empty"
|
322
332
|
end
|
323
333
|
end
|
324
334
|
|
325
|
-
#test the tag with php getid3
|
326
|
-
# prog = %{
|
327
|
-
# <?php
|
328
|
-
# require("/var/www/root/netjuke/lib/getid3/getid3.php");
|
329
|
-
# $mp3info = GetAllFileInfo('#{TEMP_FILE}');
|
330
|
-
# echo $mp3info;
|
331
|
-
# ?>
|
332
|
-
# }
|
333
|
-
#
|
334
|
-
# open("|php", "r+") do |io|
|
335
|
-
# io.puts(prog)
|
336
|
-
# io.close_write
|
337
|
-
# p io.read
|
338
|
-
# end
|
339
|
-
|
340
335
|
def test_good_parsing_of_a_pathname
|
341
336
|
fn = "Freak On `(Stone´s Club Mix).mp3"
|
342
337
|
File.rename(TEMP_FILE, fn)
|
@@ -394,6 +389,42 @@ EOF
|
|
394
389
|
end
|
395
390
|
end
|
396
391
|
|
392
|
+
def test_audio_content
|
393
|
+
require "digest/md5"
|
394
|
+
|
395
|
+
expected_digest = nil
|
396
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
397
|
+
mp3.tag1.update(DUMMY_TAG1)
|
398
|
+
mp3.tag2.update(DUMMY_TAG2)
|
399
|
+
mp3.flush
|
400
|
+
assert mp3.hastag1?
|
401
|
+
assert mp3.hastag2?
|
402
|
+
assert mp3.tag2.io_position != 0
|
403
|
+
expected_digest = compute_audio_content_mp3_digest(mp3)
|
404
|
+
end
|
405
|
+
|
406
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
407
|
+
mp3.removetag1
|
408
|
+
mp3.removetag2
|
409
|
+
mp3.flush
|
410
|
+
assert !mp3.hastag1?
|
411
|
+
assert !mp3.hastag2?
|
412
|
+
got_digest = compute_audio_content_mp3_digest(mp3)
|
413
|
+
assert_equal expected_digest, got_digest
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def compute_audio_content_mp3_digest(mp3)
|
418
|
+
pos, size = mp3.audio_content
|
419
|
+
data = File.open(mp3.filename) do |f|
|
420
|
+
f.seek(pos, IO::SEEK_SET)
|
421
|
+
f.read(size)
|
422
|
+
end
|
423
|
+
Digest::MD5.new.update(data).hexdigest
|
424
|
+
end
|
425
|
+
def compute_md5(content)
|
426
|
+
end
|
427
|
+
|
397
428
|
def write_temp_file(tag)
|
398
429
|
Mp3Info.open(TEMP_FILE) do |mp3|
|
399
430
|
mp3.tag2.update(tag)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-mp3info
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Pierronnet
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-04-
|
12
|
+
date: 2008-04-19 00:00:00 +02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -64,7 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements: []
|
65
65
|
|
66
66
|
rubyforge_project: ruby-mp3info
|
67
|
-
rubygems_version: 1.
|
67
|
+
rubygems_version: 1.1.1
|
68
68
|
signing_key:
|
69
69
|
specification_version: 2
|
70
70
|
summary: ruby-mp3info is a pure-ruby library to retrieve low level informations on mp3 files and manipulate id3v1 and id3v2 tags
|