ruby-mp3info 0.6.16 → 0.7
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 +6 -0
- data/Manifest.txt +0 -1
- data/README.txt +7 -9
- data/Rakefile +1 -1
- data/lib/mp3info.rb +17 -12
- data/lib/mp3info/extension_modules.rb +46 -4
- data/lib/mp3info/id3v2.rb +102 -74
- data/test/test_ruby-mp3info.rb +57 -100
- metadata +47 -55
- data/install.rb +0 -1022
data/History.txt
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
=== 0.7 / 2012-02-29
|
2
|
+
|
3
|
+
* removed iconv for ruby >= 1.9
|
4
|
+
* default encoding for decoded and encoded tags is UTF-8 now (removed :encoding params for ID3v2)
|
5
|
+
* come back to a non senseless version numbering
|
6
|
+
|
1
7
|
=== 0.6.16 / 2011-11-10
|
2
8
|
|
3
9
|
* fixed type error when inspecting mp3info (thanks to Jacob Lichner)
|
data/Manifest.txt
CHANGED
data/README.txt
CHANGED
@@ -14,9 +14,12 @@ mp3 files.
|
|
14
14
|
* read, write, remove id3v1 and id3v2 tags
|
15
15
|
* correctly read VBR files (with or without Xing header)
|
16
16
|
* only 2.3 version is supported for writings id3v2 tags
|
17
|
+
* id3v2 tags are always written in UTF-16 encoding
|
17
18
|
|
18
19
|
== SYNOPSIS:
|
19
20
|
|
21
|
+
### read and display infos & tags
|
22
|
+
|
20
23
|
require "mp3info"
|
21
24
|
# read and display infos & tags
|
22
25
|
Mp3Info.open("myfile.mp3") do |mp3info|
|
@@ -35,6 +38,10 @@ mp3 files.
|
|
35
38
|
mp3.tag.artist = "artist name"
|
36
39
|
end
|
37
40
|
|
41
|
+
# tags are written when calling Mp3Info#close or at the end of the #open block
|
42
|
+
|
43
|
+
### access id3v2 tags
|
44
|
+
|
38
45
|
Mp3Info.open("myfile.mp3") do |mp3|
|
39
46
|
# you can access four letter v2 tags like this
|
40
47
|
puts mp3.tag2.TIT2
|
@@ -47,21 +54,12 @@ mp3 files.
|
|
47
54
|
mp3.tag2.COMM = "my comment in french, correctly handled when reading and writing"
|
48
55
|
end
|
49
56
|
|
50
|
-
# tags v2 will be read and written according to the :encoding settings
|
51
|
-
mp3 = Mp3Info.open("myfile.mp3", :encoding => 'utf-8')
|
52
|
-
|
53
57
|
== REQUIREMENTS:
|
54
58
|
|
55
59
|
* iconv
|
56
60
|
|
57
61
|
== INSTALL:
|
58
62
|
|
59
|
-
$ ruby install.rb config
|
60
|
-
$ ruby install.rb setup
|
61
|
-
# ruby install.rb install
|
62
|
-
|
63
|
-
or
|
64
|
-
|
65
63
|
* gem install ruby-mp3info
|
66
64
|
|
67
65
|
== DEVELOPERS:
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'hoe'
|
|
5
5
|
Hoe.plugin :yard
|
6
6
|
|
7
7
|
Hoe.spec('ruby-mp3info') do
|
8
|
-
developer "Guillaume Pierronnet", "
|
8
|
+
developer "Guillaume Pierronnet", "guillaume.pierronnet@gmail.com"
|
9
9
|
remote_rdoc_dir = ''
|
10
10
|
rdoc_locations << "rubyforge.org:/var/www/gforge-projects/ruby-mp3info/"
|
11
11
|
end
|
data/lib/mp3info.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
# Website:: http://ruby-mp3info.rubyforge.org/
|
5
5
|
|
6
6
|
require "fileutils"
|
7
|
+
require "stringio"
|
7
8
|
require "mp3info/extension_modules"
|
8
9
|
require "mp3info/id3v2"
|
9
10
|
|
@@ -17,7 +18,7 @@ end
|
|
17
18
|
|
18
19
|
class Mp3Info
|
19
20
|
|
20
|
-
VERSION = "0.
|
21
|
+
VERSION = "0.7"
|
21
22
|
|
22
23
|
LAYER = [ nil, 3, 2, 1]
|
23
24
|
BITRATE = {
|
@@ -219,6 +220,9 @@ class Mp3Info
|
|
219
220
|
def initialize(filename_or_io, options = {})
|
220
221
|
warn("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
|
221
222
|
@filename_or_io = filename_or_io
|
223
|
+
if @filename_or_io.nil?
|
224
|
+
raise ArgumentError, "filename is nil"
|
225
|
+
end
|
222
226
|
options = {:parse_mp3 => true, :parse_tags => true}.update(options)
|
223
227
|
@tag_parsing_enabled = options.delete(:parse_tags)
|
224
228
|
@mp3_parsing_enabled = options.delete(:parse_mp3)
|
@@ -230,22 +234,21 @@ class Mp3Info
|
|
230
234
|
def reload
|
231
235
|
@header = {}
|
232
236
|
|
233
|
-
if @filename_or_io.is_a?(
|
234
|
-
@io_is_a_file = true
|
235
|
-
@io = File.new(@filename_or_io, "rb")
|
236
|
-
@io_size = @io.stat.size
|
237
|
-
@filename = @filename_or_io
|
238
|
-
elsif @filename_or_io.is_a?(StringIO)
|
237
|
+
if @filename_or_io.is_a?(StringIO)
|
239
238
|
@io_is_a_file = false
|
240
239
|
@io = @filename_or_io
|
241
240
|
@io_size = @io.size
|
242
241
|
@filename = nil
|
242
|
+
else
|
243
|
+
@io_is_a_file = true
|
244
|
+
@io = File.new(@filename_or_io, "rb")
|
245
|
+
@io_size = @io.stat.size
|
246
|
+
@filename = @filename_or_io
|
243
247
|
end
|
244
248
|
|
245
249
|
if @io_size == 0
|
246
250
|
raise(Mp3InfoError, "empty file or IO")
|
247
251
|
end
|
248
|
-
|
249
252
|
|
250
253
|
@io.extend(Mp3FileMethods)
|
251
254
|
@tag1 = @tag = @tag1_orig = @tag_orig = {}
|
@@ -554,9 +557,10 @@ private
|
|
554
557
|
### assumes @io is pointing to char after "TAG" id
|
555
558
|
def gettag1
|
556
559
|
@tag1_parsed = true
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
+
%w{title artist album}.each do |tag|
|
561
|
+
v = @io.read(30).unpack("A*").first
|
562
|
+
@tag1[tag] = Mp3Info::EncodingHelper.convert_from_iso_8859_1(v)
|
563
|
+
end
|
560
564
|
year_t = @io.read(4).to_i
|
561
565
|
@tag1["year"] = year_t unless year_t == 0
|
562
566
|
comments = @io.read(30)
|
@@ -564,7 +568,8 @@ private
|
|
564
568
|
@tag1["tracknum"] = comments.getbyte(-1).to_i
|
565
569
|
comments.chop! #remove the last char
|
566
570
|
end
|
567
|
-
|
571
|
+
comment = comments.unpack("A*").first
|
572
|
+
@tag1["comments"] = Mp3Info::EncodingHelper.convert_from_iso_8859_1(comment)
|
568
573
|
@tag1["genre"] = @io.getbyte
|
569
574
|
@tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
|
570
575
|
|
@@ -21,10 +21,6 @@ class Mp3Info
|
|
21
21
|
class ::String
|
22
22
|
if RUBY_VERSION < "1.9.0"
|
23
23
|
alias getbyte []
|
24
|
-
else
|
25
|
-
def getbyte(i)
|
26
|
-
self[i].ord unless self[i].nil?
|
27
|
-
end
|
28
24
|
end
|
29
25
|
end
|
30
26
|
|
@@ -43,4 +39,50 @@ class Mp3Info
|
|
43
39
|
(getbyte << 21) + (getbyte << 14) + (getbyte << 7) + getbyte
|
44
40
|
end
|
45
41
|
end
|
42
|
+
|
43
|
+
class EncodingHelper
|
44
|
+
def self.convert_to(value, from, to)
|
45
|
+
if RUBY_1_8
|
46
|
+
if to == "iso-8859-1"
|
47
|
+
to = to + "//TRANSLIT"
|
48
|
+
end
|
49
|
+
ruby_18_encode(from, to, value)
|
50
|
+
else
|
51
|
+
if to == "utf-16"
|
52
|
+
("\uFEFF" + value).encode("UTF-16BE")
|
53
|
+
else
|
54
|
+
value.encode(to)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.convert_from_iso_8859_1(value)
|
60
|
+
if RUBY_1_8
|
61
|
+
ruby_18_encode("utf-8", "iso-8859-1", value)
|
62
|
+
else
|
63
|
+
value.force_encoding("iso-8859-1").encode("utf-8")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.ruby_18_encode(from, to, value)
|
68
|
+
begin
|
69
|
+
Iconv.iconv(to, from, value).first
|
70
|
+
rescue Iconv::Failure
|
71
|
+
value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.decode_utf16(out)
|
76
|
+
if RUBY_1_8
|
77
|
+
convert_to(out, "UTF-8", "UTF-16")
|
78
|
+
else
|
79
|
+
if out.bytes.first == 0xff
|
80
|
+
tag_encoding = "UTF-16LE"
|
81
|
+
else
|
82
|
+
tag_encoding = "UTF-16BE"
|
83
|
+
end
|
84
|
+
out = out.dup.force_encoding(tag_encoding)[1..-1]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
46
88
|
end
|
data/lib/mp3info/id3v2.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# encoding: utf-8
|
2
2
|
# License:: Ruby
|
3
3
|
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
4
4
|
# Website:: http://ruby-mp3info.rubyforge.org/
|
5
5
|
|
6
6
|
require "delegate"
|
7
|
-
|
7
|
+
|
8
|
+
if RUBY_VERSION[0..2] == "1.8"
|
9
|
+
require "iconv"
|
10
|
+
RUBY_1_8 = true
|
11
|
+
else
|
12
|
+
RUBY_1_8 = false
|
13
|
+
end
|
8
14
|
|
9
15
|
require "mp3info/extension_modules"
|
10
16
|
|
@@ -16,9 +22,6 @@ class ID3v2Error < StandardError ; end
|
|
16
22
|
# Written version is always 2.3.
|
17
23
|
class ID3v2 < DelegateClass(Hash)
|
18
24
|
|
19
|
-
# Major version used when writing tags
|
20
|
-
WRITE_VERSION = 3
|
21
|
-
|
22
25
|
TAGS = {
|
23
26
|
"AENC" => "Audio encryption",
|
24
27
|
"APIC" => "Attached picture",
|
@@ -169,26 +172,22 @@ class ID3v2 < DelegateClass(Hash)
|
|
169
172
|
|
170
173
|
# :+lang+: for writing comments
|
171
174
|
#
|
172
|
-
# :+encoding+: one of the string of +TEXT_ENCODINGS+,
|
173
|
-
#
|
174
|
-
#
|
175
|
+
# [DEPRECATION] :+encoding+: one of the string of +TEXT_ENCODINGS+,
|
176
|
+
# use of :encoding parameter is DEPRECATED. In ruby 1.8, use utf-8 encoded strings for tags.
|
177
|
+
# In ruby >= 1.9, strings are automatically transcoded from their originaloriginal encoding.
|
175
178
|
attr_reader :options
|
176
179
|
|
177
180
|
# possible options are described above ('options' attribute)
|
178
181
|
# you can access this object like an hash, with [] and []= methods
|
179
182
|
# special cases are ["disc_number"] and ["disc_total"] mirroring TPOS attribute
|
180
183
|
def initialize(options = {})
|
181
|
-
@options = {
|
182
|
-
|
183
|
-
:encoding
|
184
|
-
|
184
|
+
@options = { :lang => "ENG" }
|
185
|
+
if @options[:encoding]
|
186
|
+
warn("use of :encoding parameter is DEPRECATED. In ruby 1.8, use utf-8 encoded strings for tags.\n" +
|
187
|
+
"In ruby >= 1.9, strings are automatically transcoded from their original encoding.")
|
188
|
+
end
|
185
189
|
|
186
190
|
@options.update(options)
|
187
|
-
@text_encoding_index = TEXT_ENCODINGS.index(@options[:encoding])
|
188
|
-
|
189
|
-
unless @text_encoding_index
|
190
|
-
raise(ArgumentError, "bad id3v2 text encoding specified")
|
191
|
-
end
|
192
191
|
|
193
192
|
@hash = {}
|
194
193
|
#TAGS.keys.each { |k| @hash[k] = nil }
|
@@ -250,7 +249,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
250
249
|
@io = nil
|
251
250
|
end
|
252
251
|
|
253
|
-
# dump tag for writing. Version is always 2
|
252
|
+
# dump tag for writing. Version is always 2.3.0
|
254
253
|
def to_bin
|
255
254
|
#TODO handle of @tag2[TLEN"]
|
256
255
|
#TODO add of crc
|
@@ -269,13 +268,13 @@ class ID3v2 < DelegateClass(Hash)
|
|
269
268
|
|
270
269
|
# Output one flag for each array element, or one only if it's not an array
|
271
270
|
[v].flatten.each do |value|
|
272
|
-
data = encode_tag(k, value.to_s
|
271
|
+
data = encode_tag(k, value.to_s)
|
273
272
|
#data << "\x00"*2 #End of tag
|
274
273
|
|
275
274
|
tag << k[0,4] #4 characte max for a tag's key
|
276
275
|
#tag << to_syncsafe(data.size) #+1 because of the language encoding byte
|
277
276
|
size = data.size
|
278
|
-
|
277
|
+
unless RUBY_1_8
|
279
278
|
size = data.dup.force_encoding("binary").size
|
280
279
|
end
|
281
280
|
tag << [size].pack("N") #+1 because of the language encoding byte
|
@@ -286,7 +285,7 @@ class ID3v2 < DelegateClass(Hash)
|
|
286
285
|
|
287
286
|
tag_str = "ID3"
|
288
287
|
#version_maj, version_min, unsync, ext_header, experimental, footer
|
289
|
-
tag_str << [
|
288
|
+
tag_str << [ 3, 0, "0000" ].pack("CCB4")
|
290
289
|
tag_str << [to_syncsafe(tag.size)].pack("N")
|
291
290
|
tag_str << tag
|
292
291
|
puts "tag in binary format: #{tag_str.inspect}" if $DEBUG
|
@@ -295,60 +294,83 @@ class ID3v2 < DelegateClass(Hash)
|
|
295
294
|
|
296
295
|
private
|
297
296
|
|
298
|
-
def encode_tag(name, value
|
299
|
-
puts "encode_tag(#{name.inspect}, #{value.inspect}
|
300
|
-
|
301
|
-
text_encoding_index = @text_encoding_index
|
302
|
-
if (name.index("T") == 0 || name == "COMM" ) && version == 3
|
303
|
-
# in id3v2.3 tags, there is only 2 encodings possible
|
304
|
-
transcoded_value = value
|
305
|
-
if text_encoding_index >= 2
|
306
|
-
begin
|
307
|
-
transcoded_value = Iconv.iconv(TEXT_ENCODINGS[1],
|
308
|
-
TEXT_ENCODINGS[text_encoding_index],
|
309
|
-
value).first
|
310
|
-
rescue Iconv::Failure
|
311
|
-
transcoded_value = value
|
312
|
-
end
|
313
|
-
text_encoding_index = 1
|
314
|
-
end
|
315
|
-
end
|
297
|
+
def encode_tag(name, value)
|
298
|
+
puts "encode_tag(#{name.inspect}, #{value.inspect})" if $DEBUG
|
299
|
+
name = name.to_s
|
316
300
|
|
301
|
+
if name =~ /^(COM|T)/
|
302
|
+
transcoded_value = Mp3Info::EncodingHelper.convert_to(value, "utf-8", "utf-16")
|
303
|
+
end
|
317
304
|
case name
|
318
305
|
when "COMM"
|
319
|
-
|
320
|
-
[
|
306
|
+
puts "encode COMM: lang: #{@options[:lang]}, value #{transcoded_value.inspect}" if $DEBUG
|
307
|
+
s = [ 1, @options[:lang], "\xFE\xFF\x00\x00", transcoded_value].pack("ca3a*a*")
|
308
|
+
return s
|
321
309
|
when /^T/
|
322
|
-
|
310
|
+
unless RUBY_1_8
|
311
|
+
transcoded_value.force_encoding("BINARY")
|
312
|
+
end
|
313
|
+
return "\x01" + transcoded_value
|
323
314
|
else
|
324
|
-
value
|
315
|
+
return value
|
325
316
|
end
|
326
317
|
end
|
327
318
|
|
328
319
|
### Read a tag from file and perform UNICODE translation if needed
|
329
320
|
def decode_tag(name, raw_value)
|
330
321
|
puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
|
331
|
-
|
332
|
-
|
322
|
+
if name =~ /^(T|COM)/
|
323
|
+
if name =~ /^COM/
|
333
324
|
#FIXME improve this
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
return out
|
325
|
+
encoding_index, lang, raw_tag = raw_value.unpack("ca3a*")
|
326
|
+
if encoding_index == 1
|
327
|
+
=begin
|
328
|
+
comment = Mp3Info::EncodingHelper.decode_utf16(raw_tag)
|
329
|
+
e = comment.encoding
|
330
|
+
out = comment.force_encoding("BINARY").split("\x00\x00").last.force_encoding(e)
|
331
|
+
p out
|
332
|
+
=end
|
333
|
+
comment = Mp3Info::EncodingHelper.decode_utf16(raw_tag)
|
334
|
+
split_val = RUBY_1_8 ? "\x00\x00" : "\x00".encode(comment.encoding)
|
335
|
+
out = comment.split(split_val).last rescue ""
|
336
|
+
else
|
337
|
+
comment, out = raw_tag.split("\x00", 2)
|
338
|
+
end
|
339
|
+
puts "COM tag found. encoding: #{encoding_index} lang: #{lang} str: #{out.inspect}" if $DEBUG
|
350
340
|
else
|
351
|
-
|
341
|
+
encoding_index = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant)
|
342
|
+
out = raw_value[1..-1]
|
343
|
+
end
|
344
|
+
# we need to convert the string in order to match
|
345
|
+
# the requested encoding
|
346
|
+
if encoding_index && TEXT_ENCODINGS[encoding_index] && out
|
347
|
+
if RUBY_1_8
|
348
|
+
out = Mp3Info::EncodingHelper.convert_to(out, TEXT_ENCODINGS[encoding_index], "utf-8")
|
349
|
+
else
|
350
|
+
if encoding_index == 1
|
351
|
+
out = Mp3Info::EncodingHelper.decode_utf16(out)
|
352
|
+
else
|
353
|
+
out.force_encoding(TEXT_ENCODINGS[encoding_index])
|
354
|
+
end
|
355
|
+
if out
|
356
|
+
out.encode!("utf-8")
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
if out
|
362
|
+
# remove padding zeros for textual tags
|
363
|
+
if RUBY_1_8
|
364
|
+
r = /\0*$/
|
365
|
+
else
|
366
|
+
r = Regexp.new("\x00*$".encode(out.encoding))
|
367
|
+
end
|
368
|
+
out.sub!(r, '')
|
369
|
+
end
|
370
|
+
|
371
|
+
return out
|
372
|
+
else
|
373
|
+
return raw_value
|
352
374
|
end
|
353
375
|
end
|
354
376
|
|
@@ -404,21 +426,26 @@ class ID3v2 < DelegateClass(Hash)
|
|
404
426
|
|
405
427
|
data_io = @io.read(size)
|
406
428
|
data = decode_tag(name, data_io)
|
429
|
+
if data && !data.empty?
|
430
|
+
if self.keys.include?(name)
|
431
|
+
if self[name].is_a?(Array)
|
432
|
+
unless self[name].include?(data)
|
433
|
+
self[name] << data
|
434
|
+
end
|
435
|
+
else
|
436
|
+
self[name] = [ self[name], data ]
|
437
|
+
end
|
438
|
+
else
|
439
|
+
self[name] = data
|
440
|
+
end
|
407
441
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
end
|
412
|
-
|
413
|
-
if self.keys.include?(name)
|
414
|
-
unless self[name].is_a?(Array)
|
415
|
-
self[name] = [ self[name] ]
|
442
|
+
if name == "TPOS" && data =~ /(\d+)\s*\/\s*(\d+)/
|
443
|
+
self["disc_number"] = $1.to_i
|
444
|
+
self["disc_total"] = $2.to_i
|
416
445
|
end
|
417
|
-
self[name] << data
|
418
|
-
else
|
419
|
-
self[name] = data
|
420
446
|
end
|
421
|
-
|
447
|
+
|
448
|
+
puts "self[#{name.inspect}] = #{self[name].inspect}" if $DEBUG
|
422
449
|
end
|
423
450
|
|
424
451
|
### runs thru @file one char at a time looking for best guess of first MPEG
|
@@ -434,5 +461,6 @@ class ID3v2 < DelegateClass(Hash)
|
|
434
461
|
def to_syncsafe(num)
|
435
462
|
( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
|
436
463
|
end
|
464
|
+
|
437
465
|
end
|
438
466
|
|