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 +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: []
|