ruby-mp3info 0.5 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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: []