ruby-mp3info 0.6.4 → 0.6.5
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/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
|