ID3v2(Unicode)に統一するScript

mp3のタグの文字コードがそろってないのと、ID3v1だけだったり、ID3v2とID3v1が両方あったりとなんだか美しくないので、統一したくなった。Foobar2000Unicodeに対応しているらしい。そう、もう時代はきっとUnicode
そこで、Rubyでちゃちゃっとできないかなとぐぐるさんに質問。いくつかRubyでID3タグを操作するライブラリがあったが、外部ライブラリ依存の物は、インストールが面倒なので却下。必然的に、mp3info というライブラリになった。Pure Rubyらしい。

まあ、かといってもインストールはしなければならないわけです。
インストールの方法は2通りあって、

  1. Gems経由のインストール
  2. 自前のインストール

Gemsは今後も使うかなと思うので、今回はGems経由で。

  • Gems

http://rubygems.rubyforge.org
から取ってきて,解凍したフォルダで
ruby setup.rb

  • mp3info

$ gem install ruby-mp3info

でインストールできます.

このmp3info、ファイルのタグにハッシュ形式でアクセスできる。便利だ。
そしてできあがった物がこんな物。適当&心配性で執拗なチェックしてるので、汚さとか無駄さは勘弁。使う場合は、UTF-8で保存してください。

#!/bin/ruby
=begin
ruby ID3TagStandardizer.rb [DirPath]

指定したディレクトリにある*.mp3を全部 
1.ID3v2化
2.ID3v1削除(ID3v2もあれば比較しながら長いものをコピー)
3.Unicode化

****************************
Gemsのmp3infoライブラリを必要とします.
=end


require 'rubygems'
require 'mp3info'
require 'nkf'
$KCODE="u"

module Terminal
  def set_initialize()
    case RUBY_PLATFORM
      when "i386-cygwin"
      $TERMINAL_CODE="s"
    else
      $TERMINAL_CODE="e"
    end
  end
  
  def conv(obj)
    begin 
      if obj.class != String.class then
        obj = obj.to_s
      end
    rescue
      return ""
    end
    
    kcode = {nil=>"W", "UTF8"=>"W", "SJIS"=>"S", "EUC"=>"E"}
    return NKF.nkf("-#{kcode[$KCODE]}#{$TERMINAL_CODE}",obj)
  end

  def message(msg="")
    print(conv(msg))
    STDOUT.flush
  end
end
include Terminal
Terminal.set_initialize


def strict_convert_into_utf16(str)
  # ソースがUTF-16でも-w16Bに統一するため、インプット-W16としてエンコードする
  guessCode_to_nkfOption_hash = {
    NKF::JIS => "-J", 
    NKF::EUC => "-E", 
    NKF::SJIS => "--oc=CP932", # naruseさんのご指摘から
    NKF::BINARY => "BINARY", 
    NKF::UNKNOWN => "UNKNOWN(ASCII)", 
    NKF::UTF8 => "-W8", 
    NKF::UTF16 => "-W16"
  }
  
  guess_code = NKF.guess2(str)
  input_option_nkf = guessCode_to_nkfOption_hash[guess_code]
  
  if input_option_nkf == guessCode_to_nkfOption_hash[NKF::UNKNOWN] && 
    input_option_nkf == guessCode_to_nkfOption_hash[NKF::BINARY] then
    raise "文字コードが判別できません"
  end
  
  return NKF.nkf("-m0 #{input_option_nkf} -w16B",str)
end

def main

  path = ARGV[0]
  if path =~ /(.*)\/$/ then
    path = $1
  end
  
  
  # main processing
  non_processed_file_list = Array.new()
  
  dir = Dir.new(path)
  dir.each{|mp3file|
    if mp3file =~ /\.mp3$/ then
      # Beginning Setting
      mp3 = Mp3Info.open(path+"/"+mp3file)
      mp3.tag2.options[:encoding] = 0
      Terminal.message("\nNow :#{mp3file}\n")
      
      
      begin
        # ID3v1のUTF16化
        if mp3.hastag1? then
          Terminal.message("Convert ID3v1...")
          Mp3Info::V1_V2_TAG_MAPPING.each{|id3v1, id3v2|
            Terminal.message(",#{id3v1}")
            if mp3.tag1[id3v1] != nil && mp3.tag1[id3v1].class == String then
              mp3.tag1[id3v1] = strict_convert_into_utf16(mp3.tag1[id3v1])            
            end
          }
          Terminal.message("...Done\n")
        end # mp3.hastag1? then
        
        # ID3v2のUTF16化
        if mp3.hastag2? then
          Terminal.message("Convert ID3v2...")
          ID3v2::TAGS.each{|id3v2, description|
            Terminal.message(",#{id3v2}")
            if mp3.tag2[id3v2] != nil && mp3.tag2[id3v2].class == String then
              mp3.tag2[id3v2] = strict_convert_into_utf16(mp3.tag2[id3v2])
            end
          }
          Terminal.message("...Done\n")
        end # mp3.hastag1? then
        
        # ID3v1とID3v2のマージ
        # ID3v1、ID3v2があるとき:タグ要素で文字列が長い方を優先
        # ID3v1のみある時:ID3v2にコピー
        if mp3.hastag1? && mp3.hastag2? then
          Terminal.message("Marging ID3v1 and ID3v2...\n")
          Mp3Info::V1_V2_TAG_MAPPING.each{|id3v1, id3v2|
            if mp3.tag1[id3v1] != nil && mp3.tag2[id3v2] != nil then
              if mp3.tag1[id3v1].to_s.length > mp3.tag2[id3v2].to_s.length then
                mp3.tag2[id3v2] = mp3.tag1[id3v1]
                Terminal.message("Revised #{id3v2}: =>#{NKF.nkf("-w",mp3.tag2[id3v2].to_s)}\n")
              end
            end
          }
        elsif mp3.hastag1? && !(mp3.hastag2?) then
          Terminal.message("Setting ID3v1 to ID3v2...\n")
          Mp3Info::V1_V2_TAG_MAPPING.each{|id3v1, id3v2|
            Terminal.message("#{id3v2}")
            if mp3.tag1[id3v1] != nil then
              mp3.tag2[id3v2] = mp3.tag1[id3v1].to_s
              Terminal.message(" =>#{NKF.nkf("-w",mp3.tag1[id3v1].to_s)}\n")
            end
          }        
        end # if mp3.hastag1? && mp3.hastag2? then
        
        #write
        mp3.close
        Mp3Info.removetag1(path+"/"+mp3file)

      rescue => e
        Terminal.message("\n処理中にError: \"#{e}\"\n")
        Terminal.message("無視します: #{mp3file}.\n")
        non_processed_file_list.push(mp3file)
        mp3.reload
        mp3.close
        break
      end # begin
    end # if mp3file =~ /\.mp3$/ then
  } # dir
  
  io = open("IgnoreFiles.txt","w")
  non_processed_file_list.each{|line|
    io.print("#{line}\n")
  }
  io.close
  
end #main
main

やってることは、

  1. オプションで指定したフォルダにある拡張子mp3のファイル全てに対して以下をおこなう。
  2. ID3v1があれば中身をNKF.guess2で判定しながらUnicode(UTF-16BigEndian With BOM)へ変換。
  3. ID3v2もあれば同じように変換
  4. ID3v1だけならそれをID3v2へコピー
  5. ID3v1とID3v2があれば、要素を比較し、「文字列の長い方を優先」してID3v2へコピー
  6. ID3v1を削除
  7. 文字コードが判定できないときは、手を付けずIgnoreList.txtにそのファイル名を書き出す

ということです。

ちなみによく分からないんだけれど、ちょっとはまったのが

  mp3.tag2.options[:encoding] = 0

の部分。始め何度やっても文字化けしていた。RDocにも情報がない。
なんで、ソースを眺めると、よく分かってないけど内部で文字コードSJISからUnicodeに変換している部分があった。どうやら、これのせいでこっちが変換した文字をSJIS仮定でUnicodeに再変換されていたっぽい。そのルーチンを上記のようにしておくと外れる様子。
RDocに書いといてよ('A`)

見たら分かると思いますが、一応「フォルダの中全部」、「mp3だけ」を処理します。mp3infoライブラリは、Ogg、Apeその他にも対応しているらしいので(つか、先頭からのバイト数で読み込んでるから同じならいけるんだろな)

    if mp3file =~ /\.mp3$/ then

の辺りとか、あとファイル単位に処理したかったらDirの辺りなどを適当に改造してください。