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 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