ruby-mp3info 0.6.16 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,7 +2,6 @@ History.txt
2
2
  Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
- install.rb
6
5
  lib/mp3info.rb
7
6
  lib/mp3info/extension_modules.rb
8
7
  lib/mp3info/id3v2.rb
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", "moumar@rubyforge.org"
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.6.16"
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?(String)
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
- @tag1["title"] = @io.read(30).unpack("A*").first
558
- @tag1["artist"] = @io.read(30).unpack("A*").first
559
- @tag1["album"] = @io.read(30).unpack("A*").first
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
- @tag1["comments"] = comments.unpack("A*").first
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
- # coding:utf-8
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
- require "iconv"
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
- # used as a source and destination encoding respectively
174
- # for read and write tag2 values.
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
- :lang => "ENG",
183
- :encoding => "iso-8859-1"
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.#{WRITE_VERSION}.0.
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, WRITE_VERSION)
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
- if RUBY_VERSION >= "1.9.0"
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 << [ WRITE_VERSION, 0, "0000" ].pack("CCB4")
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, version)
299
- puts "encode_tag(#{name.inspect}, #{value.inspect}, #{version})" if $DEBUG
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
- puts "encode COMM: enc: #{text_encoding_index}, lang: #{@options[:lang]}, str: #{transcoded_value.dump}" if $DEBUG
320
- [ text_encoding_index , @options[:lang], 0, transcoded_value ].pack("ca3ca*")
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
- text_encoding_index.chr + transcoded_value
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
- case name
332
- when /^COM/
322
+ if name =~ /^(T|COM)/
323
+ if name =~ /^COM/
333
324
  #FIXME improve this
334
- encoding, lang, str = raw_value.unpack("ca3a*")
335
- out = raw_value.split(0.chr).last
336
- when /^T/
337
- encoding = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant)
338
- out = raw_value[1..-1]
339
- # we need to convert the string in order to match
340
- # the requested encoding
341
- if encoding && TEXT_ENCODINGS[encoding] && out && encoding != @text_encoding_index
342
- begin
343
- out = Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out).first
344
- rescue Iconv::Failure
345
- end
346
- end
347
- # remove padding zeros for textual tags
348
- out.sub!(/\0*$/, '') unless out.nil?
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
- return raw_value
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
- if self["TPOS"] =~ /(\d+)\s*\/\s*(\d+)/
409
- self["disc_number"] = $1.to_i
410
- self["disc_total"] = $2.to_i
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
- p data if $DEBUG
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