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 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 77 2008-04-16 20:04:35Z moumar $
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.4"
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
- TAGSIZE = 128
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(-TAGSIZE, File::SEEK_END)
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) - TAGSIZE
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 - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
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
- if hastag1?
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
- @hastag1 or @tag2.valid?
315
+ hastag1? or hastag2?
320
316
  end
321
317
 
322
318
  # Does the file has an id3v1 tag?
323
319
  def hastag1?
324
- @hastag1
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
- file.seek(-TAGSIZE, File::SEEK_END)
362
- t = file.read(3)
363
- if t != 'TAG'
364
- #append new tag
365
- file.seek(0, File::SEEK_END)
366
- file.write('TAG')
367
- end
368
- str = [
369
- @tag1_orig["title"]||"",
370
- @tag1_orig["artist"]||"",
371
- @tag1_orig["album"]||"",
372
- ((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
373
- @tag1_orig["comments"]||"",
374
- 0,
375
- @tag1_orig["tracknum"]||0,
376
- @tag1_orig["genre"]||255
377
- ].pack("Z30Z30Z30Z4Z28CCC")
378
- file.write(str)
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 @hastag1
419
- s << "tag2: "+@tag2.inspect+"\n" if @tag2.valid?
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 < TAGSIZE # file is too small
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
- gettag1 if f3 == "TAG" # v1 tag at beginning
432
- begin
433
- @tag2.from_io(@file) if f3 == "ID3" # v2 tag at beginning
434
- rescue RuntimeError => e
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 @hastag1 # v1 tag at end
439
- # this preserves the file pos if tag2 found, since gettag2 leaves
440
- # the file at the best guess as to the first MPEG frame
441
- pos = (@tag2.valid? ? @file.pos : 0)
442
- # seek to where id3v1 tag should be
443
- @file.seek(-TAGSIZE, IO::SEEK_END)
444
- gettag1 if @file.read(3) == "TAG"
445
- @file.seek(pos)
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
- @hastag1 = true
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
- raise Mp3InfoError if @file.eof?
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
@@ -22,6 +22,4 @@ class Mp3Info
22
22
  (getc << 21) + (getc << 14) + (getc << 7) + getc
23
23
  end
24
24
  end
25
-
26
25
  end
27
-
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, value)
232
- puts("decode_tag(#{name.inspect}, #{value.inspect})") if $DEBUG
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 = value.unpack("ca3a*")
237
- out = value.split(0.chr).last
238
+ encoding, lang, str = raw_value.unpack("ca3a*")
239
+ out = raw_value.split(0.chr).last
238
240
  when /^T/
239
- encoding = value[0] # language encoding (see TEXT_ENCODINGS constant)
240
- out = value[1..-1]
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
- value
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
- #size = @io.get_syncsafe #this seems to be a bug
268
- size = @io.get32bits
269
- @io.read(2)
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
- n = ( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
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)
@@ -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
- BASIC_TAG2 = {
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(BASIC_TAG2)
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(BASIC_TAG2)
248
- assert_equal(BASIC_TAG2, w)
249
- id3v2_prog_test(BASIC_TAG2, w)
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 = BASIC_TAG2.dup
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.close
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
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-16 00:00:00 +02:00
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.0.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