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 +11 -0
- data/EXAMPLES +3 -0
- data/lib/mp3info.rb +55 -20
- data/lib/mp3info/extension_modules.rb +1 -10
- data/lib/mp3info/id3v2.rb +30 -6
- data/test.rb +67 -0
- metadata +10 -7
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
|
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
|
-
|
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[
|
221
|
+
@layer = LAYER[bits(head, 18,17)]
|
209
222
|
next if @layer.nil?
|
210
|
-
@bitrate = BITRATE[@mpeg_version-1][@layer-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][
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
325
|
-
|
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
|
-
|
337
|
-
|
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
|
-
|
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", "
|
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 = @
|
231
|
+
#size = @io.get_syncsafe #this seems to be a bug
|
228
232
|
size = @io.get32bits
|
229
|
-
|
230
|
-
|
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
|
-
|
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.
|
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:
|
7
|
-
date:
|
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: []
|