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