ruby-mp3info 0.5 → 0.5.1

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/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ [0.5.1 10/09/2007]
2
+
3
+ * ADDED: Mp3Info#reload method to reload the file from the disk
4
+ * FIXED: bug [#2604] Not able to delete tag1
5
+ * FIXED: bug #3401 'id3v2.rb dies when trying to read a certain mp3'
6
+ * FIXED: bug #2957 'Error message "Can't define singleton"'
7
+ * FIXED: bug #3068 'require_gem ("ruby-mp3info") doesn't works'
8
+ * FIXED: bug #11967 "Leading 'h' from 'http://' gets chopped on URL fields"
9
+ * PATCHED: with patch #3157 'Fix for 64 bit Ruby'
10
+
11
+
1
12
  [0.5 06/12/2005]
2
13
 
3
14
  * id3v2 writing and removing support added. tag2 attribute is r/w now
data/EXAMPLES CHANGED
@@ -1,5 +1,7 @@
1
1
  = Examples
2
2
 
3
+ # a good exercise is to read the test.rb to understand how the library works deeper
4
+
3
5
  require "mp3info"
4
6
 
5
7
  # read and display infos & tags
@@ -38,3 +40,4 @@
38
40
  mp3.tag2.options[:lang] = "FRE"
39
41
  mp3.tag2.COMM = "my comment in french, correctly handled when reading and writing"
40
42
  end
43
+
data/lib/mp3info.rb CHANGED
@@ -1,4 +1,4 @@
1
- # $Id: mp3info.rb,v 1.5 2005/04/26 13:41:41 moumar Exp $
1
+ # $Id: mp3info.rb 51 2007-09-10 11:53:52Z moumar $
2
2
  # License:: Ruby
3
3
  # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
4
4
  # Website:: http://ruby-mp3info.rubyforge.org/
@@ -18,7 +18,7 @@ end
18
18
 
19
19
  class Mp3Info
20
20
 
21
- VERSION = "0.5"
21
+ VERSION = "0.5.1"
22
22
 
23
23
  LAYER = [ nil, 3, 2, 1]
24
24
  BITRATE = [
@@ -77,7 +77,13 @@ class Mp3Info
77
77
  "comments" => "COMM",
78
78
  "genre_s" => "TCON"
79
79
  }
80
-
80
+
81
+ # http://www.codeproject.com/audio/MPEGAudioInfo.asp
82
+ SAMPLES_PER_FRAME = [
83
+ [384, 384, 384], # Layer I
84
+ [1152, 1152, 1152], # Layer II
85
+ [1152, 576, 576] # Layer III
86
+ ]
81
87
 
82
88
  # mpeg version = 1 or 2
83
89
  attr_reader(:mpeg_version)
@@ -97,6 +103,9 @@ class Mp3Info
97
103
  # variable bitrate => true or false
98
104
  attr_reader(:vbr)
99
105
 
106
+ # only used in vbr mode
107
+ attr_reader(:samples_per_frame)
108
+
100
109
  # length in seconds as a Float
101
110
  attr_reader(:length)
102
111
 
@@ -155,8 +164,13 @@ class Mp3Info
155
164
  # Instantiate a new Mp3Info object with name +filename+
156
165
  def initialize(filename)
157
166
  $stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
158
- raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
159
167
  @filename = filename
168
+ reload
169
+ end
170
+
171
+ # reload the file from disk
172
+ def reload
173
+ raise(Mp3InfoError, "empty file") unless File.size?(@filename)
160
174
  @hastag1 = false
161
175
 
162
176
  @tag1 = {}
@@ -199,19 +213,18 @@ class Mp3Info
199
213
  ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
200
214
  ### head (fixnum) = valid 4 byte MPEG header
201
215
 
202
- found = false
216
+ found = false
203
217
 
204
218
  5.times do
205
219
  head = find_next_frame()
206
- head.extend(NumericBits)
207
220
  @mpeg_version = [2, 1][head[19]]
208
- @layer = LAYER[head.bits(18,17)]
221
+ @layer = LAYER[bits(head, 18,17)]
209
222
  next if @layer.nil?
210
- @bitrate = BITRATE[@mpeg_version-1][@layer-1][head.bits(15,12)-1]
223
+ @bitrate = BITRATE[@mpeg_version-1][@layer-1][bits(head, 15,12)-1]
211
224
  @error_protection = head[16] == 0 ? true : false
212
- @samplerate = SAMPLERATE[@mpeg_version-1][head.bits(11,10)]
225
+ @samplerate = SAMPLERATE[@mpeg_version-1][bits(head, 11,10)]
213
226
  @padding = (head[9] == 1 ? true : false)
214
- @channel_mode = CHANNEL_MODE[@channel_num = head.bits(7,6)]
227
+ @channel_mode = CHANNEL_MODE[@channel_num = bits(head, 7,6)]
215
228
  @copyright = (head[3] == 1 ? true : false)
216
229
  @original = (head[2] == 1 ? true : false)
217
230
  @vbr = false
@@ -240,7 +253,10 @@ class Mp3Info
240
253
  # currently this just skips the TOC entries if they're found
241
254
  @file.seek(100, IO::SEEK_CUR) if flags[0] == 1
242
255
  @vbr_quality = @file.get32bits if flags[3] == 1
243
- @length = (26/1000.0)*@frames
256
+
257
+ @samples_per_frame = SAMPLES_PER_FRAME[@layer-1][@mpeg_version-1]
258
+ @length = @frames * @samples_per_frame / Float(@samplerate)
259
+
244
260
  @bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
245
261
  @vbr = true
246
262
  else
@@ -285,8 +301,7 @@ class Mp3Info
285
301
  # Remove id3v1 from mp3
286
302
  def removetag1
287
303
  if hastag1?
288
- newsize = @file.stat.size(filename) - TAGSIZE
289
- @file.truncate(newsize)
304
+ Mp3Info.removetag1(@filename)
290
305
  @tag1.clear
291
306
  end
292
307
  self
@@ -294,6 +309,7 @@ class Mp3Info
294
309
 
295
310
  def removetag2
296
311
  @tag2.clear
312
+ self
297
313
  end
298
314
 
299
315
  # Does the file has an id3v1 or v2 tag?
@@ -315,14 +331,18 @@ class Mp3Info
315
331
  def rename(new_filename)
316
332
  @filename = new_filename
317
333
  end
318
-
334
+
319
335
  # Flush pending modifications to tags and close the file
320
336
  def close
321
337
  puts "close" if $DEBUG
322
338
  if @tag != @tag_orig
323
339
  puts "@tag has changed" if $DEBUG
324
- @tag.each do |k, v|
325
- @tag1[k] = v
340
+
341
+ # @tag1 has precedence over @tag
342
+ if @tag1 == @tag1_orig
343
+ @tag.each do |k, v|
344
+ @tag1[k] = v
345
+ end
326
346
  end
327
347
 
328
348
  V1_V2_TAG_MAPPING.each do |key1, key2|
@@ -333,8 +353,8 @@ class Mp3Info
333
353
  if @tag1 != @tag1_orig
334
354
  puts "@tag1 has changed" if $DEBUG
335
355
  raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
336
- @tag1_orig.update(@tag1)
337
- #puts "@tag1_orig: #{@tag1_orig.inspect}"
356
+ #@tag1_orig.update(@tag1)
357
+ @tag1_orig = @tag1.dup
338
358
  File.open(@filename, 'rb+') do |file|
339
359
  file.seek(-TAGSIZE, File::SEEK_END)
340
360
  t = file.read(3)
@@ -389,7 +409,6 @@ class Mp3Info
389
409
  end
390
410
  File.rename(tempfile_name, @filename)
391
411
  end
392
- @file = nil
393
412
  end
394
413
 
395
414
  # inspect inside Mp3Info
@@ -409,7 +428,11 @@ private
409
428
  @file.seek(0)
410
429
  f3 = @file.read(3)
411
430
  gettag1 if f3 == "TAG" # v1 tag at beginning
412
- @tag2.from_io(@file) if f3 == "ID3" # v2 tag at beginning
431
+ begin
432
+ @tag2.from_io(@file) if f3 == "ID3" # v2 tag at beginning
433
+ rescue RuntimeError => e
434
+ raise(Mp3InfoError, e.message)
435
+ end
413
436
 
414
437
  unless @hastag1 # v1 tag at end
415
438
  # this preserves the file pos if tag2 found, since gettag2 leaves
@@ -456,6 +479,11 @@ private
456
479
  @tag1["comments"] = comments.strip
457
480
  @tag1["genre"] = @file.getc
458
481
  @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
482
+
483
+ # clear empty tags
484
+ @tag1.delete_if { |k, v| v.respond_to?(:empty?) && v.empty? }
485
+ @tag1.delete("genre") if @tag1["genre"] == 255
486
+ @tag1.delete("tracknum") if @tag1["tracknum"] == 0
459
487
  end
460
488
 
461
489
  ### reads through @file from current pos until it finds a valid MPEG header
@@ -496,6 +524,13 @@ private
496
524
  true
497
525
  end
498
526
 
527
+ ### returns the selected bit range (b, a) as a number
528
+ ### NOTE: b > a if not, returns 0
529
+ def bits(n, b, a)
530
+ t = 0
531
+ b.downto(a) { |i| t += t + n[i] }
532
+ t
533
+ end
499
534
  end
500
535
 
501
536
  if $0 == __FILE__
@@ -13,20 +13,11 @@ class Mp3Info
13
13
  end
14
14
  end
15
15
 
16
- module NumericBits #:nodoc:
17
- ### returns the selected bit range (b, a) as a number
18
- ### NOTE: b > a if not, returns 0
19
- def bits(b, a)
20
- t = 0
21
- b.downto(a) { |i| t += t + self[i] }
22
- t
23
- end
24
- end
25
-
26
16
  module Mp3FileMethods #:nodoc:
27
17
  def get32bits
28
18
  (getc << 24) + (getc << 16) + (getc << 8) + getc
29
19
  end
20
+
30
21
  def get_syncsafe
31
22
  (getc << 21) + (getc << 14) + (getc << 7) + getc
32
23
  end
data/lib/mp3info/id3v2.rb CHANGED
@@ -121,7 +121,7 @@ class ID3v2 < DelegateClass(Hash)
121
121
  def from_io(io)
122
122
  @io = io
123
123
  version_maj, version_min, flags = @io.read(3).unpack("CCB4")
124
- unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
124
+ @unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
125
125
  raise("can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
126
126
  @version_maj, @version_min = version_maj, version_min
127
127
  @valid = true
@@ -178,6 +178,8 @@ class ID3v2 < DelegateClass(Hash)
178
178
  case name
179
179
  when "COMM"
180
180
  [ @options[:encoding] == :iso ? 0 : 1, @options[:lang], 0, value ].pack("ca3ca*")
181
+ when /^W/ # URL link frames
182
+ value
181
183
  else
182
184
  if @options[:encoding] == :iso
183
185
  "\x00"+value
@@ -195,6 +197,8 @@ class ID3v2 < DelegateClass(Hash)
195
197
  #FIXME improve this
196
198
  encoding, lang, str = value.unpack("ca3a*")
197
199
  out = value.split(0.chr).last
200
+ when /^W/ # URL link frames
201
+ out = value
198
202
  else
199
203
  encoding = value[0] # language encoding bit 0 for iso_8859_1, 1 for unicode
200
204
  out = value[1..-1]
@@ -206,7 +210,7 @@ class ID3v2 < DelegateClass(Hash)
206
210
  #strip byte-order bytes at the beginning of the unicode string if they exists
207
211
  out[0..3] =~ /^[\xff\xfe]+$/ and out = out[2..-1]
208
212
  begin
209
- out = Iconv.iconv("ISO-8859-1", "UNICODE", out)[0]
213
+ out = Iconv.iconv("ISO-8859-1", "UTF-16", out)[0]
210
214
  rescue Iconv::IllegalSequence, Iconv::InvalidCharacter
211
215
  end
212
216
  end
@@ -224,10 +228,26 @@ class ID3v2 < DelegateClass(Hash)
224
228
  seek_to_v2_end
225
229
  break
226
230
  else
227
- #size = @file.get_syncsafe #this seems to be a bug
231
+ #size = @io.get_syncsafe #this seems to be a bug
228
232
  size = @io.get32bits
229
- puts "name '#{name}' size #{size} " if $DEBUG
230
- @io.seek(2, IO::SEEK_CUR) # skip flags
233
+ @io.read(2)
234
+ =begin
235
+ size_str = @io.read(4)
236
+
237
+ @io.getc #flags part 1
238
+ # just read the unsync bit
239
+ b = @io.getc
240
+ unsync = ((b >> 1) & 1) == 1
241
+
242
+ if unsync
243
+ size = (size_str[0] << 21) + (size_str[1] << 14) + (size_str[2]<< 7) + size_str[3]
244
+ else
245
+ size = size_str.unpack("N").first
246
+ end
247
+ require "to_b"
248
+ =end
249
+ puts "name '#{name}' size #{size}" if $DEBUG
250
+ #@io.seek(2, IO::SEEK_CUR) # skip flags
231
251
  add_value_to_tag2(name, size)
232
252
  # case name
233
253
  # when /^T/
@@ -277,7 +297,9 @@ class ID3v2 < DelegateClass(Hash)
277
297
  ### create an array if the key already exists in the tag
278
298
  def add_value_to_tag2(name, size)
279
299
  puts "add_value_to_tag2" if $DEBUG
280
- data = decode_tag(name, @io.read(size))
300
+ raise("tag size too big for tag #{name.inspect} unsync #{@unsync} ") if size > 50_000_000
301
+ data_io = @io.read(size)
302
+ data = decode_tag(name, data_io)
281
303
  if self.keys.include?(name)
282
304
  unless self[name].is_a?(Array)
283
305
  self[name] = self[name].to_a
@@ -286,12 +308,14 @@ class ID3v2 < DelegateClass(Hash)
286
308
  else
287
309
  self[name] = data
288
310
  end
311
+ p data if $DEBUG
289
312
  end
290
313
 
291
314
  ### runs thru @file one char at a time looking for best guess of first MPEG
292
315
  ### frame, which should be first 0xff byte after id3v2 padding zero's
293
316
  def seek_to_v2_end
294
317
  until @io.getc == 0xff
318
+ raise EOFError if @io.eof?
295
319
  end
296
320
  @io.seek(-1, IO::SEEK_CUR)
297
321
  end
data/test.rb CHANGED
@@ -6,6 +6,7 @@ require "test/unit"
6
6
  require "base64"
7
7
  require "mp3info"
8
8
  require "fileutils"
9
+ require "tempfile"
9
10
 
10
11
  class Mp3InfoTest < Test::Unit::TestCase
11
12
 
@@ -126,6 +127,25 @@ EOF
126
127
  assert_equal(info.length, 0.1305625)
127
128
  end
128
129
  end
130
+
131
+ def test_vbr_mp3_length
132
+ temp_file_vbr = File.join(File.dirname($0), "vbr.mp3")
133
+
134
+ unless File.exists?(temp_file_vbr)
135
+ tempfile = Tempfile.new("ruby-mp3info_test")
136
+ tempfile.close
137
+ system("dd if=/dev/zero of=#{tempfile.path} bs=1024 count=30000")
138
+ raise "cannot find lame binary in path" unless system("which lame")
139
+ system("lame -h -v -b 112 -r -s 44.1 --bitwidth 16 #{tempfile.path} #{temp_file_vbr}")
140
+ tempfile.close!
141
+ end
142
+
143
+ Mp3Info.open(temp_file_vbr) do |info|
144
+ assert(info.vbr)
145
+ assert_equal(1152, info.samples_per_frame)
146
+ assert_in_delta(174.210612, info.length, 0.000001)
147
+ end
148
+ end
129
149
 
130
150
  def test_removetag1
131
151
  Mp3Info.open(TEMP_FILE) { |info| info.tag1 = @tag }
@@ -251,6 +271,38 @@ EOF
251
271
  assert_equal(tag, write_temp_file(tag))
252
272
  end
253
273
 
274
+ def test_infinite_loop_on_seek_to_v2_end
275
+
276
+ end
277
+
278
+ def test_leading_char_gets_chopped
279
+ tag2 = BASIC_TAG2.dup
280
+ tag2["WOAR"] = "http://foo.bar"
281
+ w = write_temp_file(tag2)
282
+ assert_equal("http://foo.bar", w["WOAR"])
283
+
284
+ system(%(id3v2 --WOAR "http://foo.bar" "#{TEMP_FILE}"))
285
+
286
+ Mp3Info.open(TEMP_FILE) do |mp3|
287
+ assert_equal "http://foo.bar", mp3.tag2["WOAR"]
288
+ end
289
+ end
290
+
291
+ def test_remove_tag
292
+ Mp3Info.open(TEMP_FILE) do |mp3|
293
+ tag = mp3.tag
294
+ tag.title = "title"
295
+ tag.artist = "artist"
296
+ mp3.close
297
+ mp3.reload
298
+ assert !mp3.tag1.empty?, "tag is empty"
299
+ mp3.removetag1
300
+ mp3.close
301
+ mp3.reload
302
+ assert mp3.tag1.empty?, "tag is not empty"
303
+ end
304
+ end
305
+
254
306
  #test the tag with php getid3
255
307
  # prog = %{
256
308
  # <?php
@@ -266,6 +318,21 @@ EOF
266
318
  # p io.read
267
319
  # end
268
320
 
321
+ def test_good_parsing_of_a_pathname
322
+ fn = "Freak On `(Stone�s Club Mix).mp3"
323
+ File.rename(TEMP_FILE, fn)
324
+ begin
325
+ mp3 = Mp3Info.new(fn)
326
+ mp3.tag.title = fn
327
+ mp3.close
328
+ mp3.reload
329
+ assert_equal fn, mp3.tag.title
330
+ mp3.close
331
+ ensure
332
+ File.delete(fn)
333
+ end
334
+ end
335
+
269
336
  #test the tag with the "id3v2" program
270
337
  def id3v2_prog_test(tag, written_tag)
271
338
  return if PLATFORM =~ /win32/
metadata CHANGED
@@ -1,10 +1,10 @@
1
- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.11
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: ruby-mp3info
5
5
  version: !ruby/object:Gem::Version
6
- version: "0.5"
7
- date: 2005-12-09 00:00:00 +01:00
6
+ version: 0.5.1
7
+ date: 2007-09-10 00:00:00 +02:00
8
8
  summary: ruby-mp3info is a pure-ruby library to retrieve low level informations on mp3 files and manipulate id3v1 and id3v2 tags
9
9
  require_paths:
10
10
  - lib
@@ -12,7 +12,7 @@ email: moumar@rubyforge.org
12
12
  homepage: http://ruby-mp3info.rubyforge.org
13
13
  rubyforge_project: ruby-mp3info
14
14
  description:
15
- autorequire:
15
+ autorequire: mp3info
16
16
  default_executable:
17
17
  bindir: bin
18
18
  has_rdoc: true
@@ -25,6 +25,7 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
25
25
  platform: ruby
26
26
  signing_key:
27
27
  cert_chain:
28
+ post_install_message:
28
29
  authors:
29
30
  - Guillaume Pierronnet
30
31
  files:
@@ -39,8 +40,10 @@ test_files: []
39
40
 
40
41
  rdoc_options: []
41
42
 
42
- extra_rdoc_files: []
43
-
43
+ extra_rdoc_files:
44
+ - README
45
+ - CHANGELOG
46
+ - EXAMPLES
44
47
  executables: []
45
48
 
46
49
  extensions: []