sneetchalizer.rb

#!/usr/bin/ruby

#  Author :: Darren Kirby <bulliver@badcomputer.org>
#  Website :: http://badcomputer.org/unix/code/sneetchalizer/
#  License :: GPL :: http://www.fsf.org/licensing/licenses/gpl.html
#
#  Copyright (C) 2006-2008 Darren Kirby
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#  Code to convert *.ape (Monkey's Audio) Files was written by
#  Peter H <peterh_hretep [AT] yahoo.com>
#
#  Flac encoding bug and ebuild fixes thanks to
#  Ian Young <youngian [AT] grinnell.edu>
#
#  'rename' and 'copy' support also thanks to
#  Ian Young <youngian [AT] grinnell.edu>
#
#  Oleg Lyashko <olegvl [AT] gmail.com> sent in a simple fix to the
#  '-D' or '--outdir' bug.
#


#
#  Boilerplate to get libs installed via gems to work.
#
begin
  require 'rubygems'
rescue LoadError
end

APPNAME =    'sneetchalizer'
APPVERSION = '0.9.0'
QUIP =       'By far the best software available for turtle-stacking'

#
#  Supported in formats.
#
INTOK = %w/aac aif aifc aiff aiffc ape au caf cdr cdda fap flac gsm ircam m4a m4b mpc mp4 mpp mp3 mp2
           mat mat4 mat5 nist ofr ofs ogg paf pvf raw sds sd2 sf snd spx svx tta voc vox wav wma wv w64 xi/

#
#  Supported out formats.
#
OUTOK = INTOK + ["copy"]

#
# Supported Video formats.
#
VITOK = %w/asf avi divx flv mkv mpg mpeg mov m4v nsv nuv ogm psp qt rv smk svcd vcd vob wmv /

#
#  Print info (green).
#
def info(msg)
  system('echo -en $"\\033[0;32m"')
  puts msg
  system('echo -en $"\\033[0;39m"')
end

#
#  Print warnings (yellow).
#
def caution(msg)
  system('echo -en $"\\033[1;33m"')
  puts msg
  system('echo -en $"\\033[0;39m"')
end

#
#  Print errors (red).
#
def error(msg)
  system('echo -en $"\\033[1;31m"')
  puts "*** #{msg} ***"
  system('echo -en $"\\033[0;39m"')
end

#
#  'short help' serves as a quick reminder of options.
#
def short_help
  print <<USAGE
Usage: #{APPNAME} [options] file1 [file2..x] directory1 [directory2..x]
  Options:
     -h                            --help
     -r   or --recursive           -d or --delete
     -v   or --verbose             -T or --threads[=N]
     -s   or --show-output         -z or --sanitize
     -p   or --pretend             -S or --strict
     -t   or --terminate           -P or --pedantic
     -D X or --out-directory='X'   -b or --bitrate
     -n X or --rename='X'          -c or --compression
     -q   or --quality             -G or --gogo
     -B   or --bladeenc            --stasis
     --in='X'                      --out='X'
     --tt or --title               --ta or --artist
     --tl or --album               --ty or --year
     --tc or --comment             --tg or --genre
     --tn or --trackn
     --in-optionhook='X'           --out-optionhook='X'
USAGE
  exit 0
end

#
#  'long help' gives full usage details.
#
def long_help

  #
  #  Print 13 tokens per line.
  #
  def pp(l)
    s = ""
    l.sort!.each_index do |n|
      s += "'#{l[n]}' "
      s += "\n            " if n % 13 == 0 && n != 0
    end
    s
  end

  print <<USAGE
Usage: #{APPNAME} [options] file1 [file2..x] directory1 [directory2..x]
    General Options:
        '-h'                            Show quick option reminder.
        '--help'                        Show detailed usage.
        '-d'   or '--delete'            Delete input files after successful conversion.
        '-r'   or '--recursive'         Descend recursively into directory args for files to convert.
        '-v'   or '--verbose'           Make #{APPNAME} more chatty.
        '-s'   or '--show-output'       Show conversion command output.
        '-p'   or '--pretend'           Don't convert anything, just show what would be done.
        '-t'   or '--terminate'         Terminate #{APPNAME} options.
        '-D X' or '--out-directory=X'   Write all outfiles to directory 'X'.
        '-T N' or '--threads=N'         EXPERIMENTAL. Run 'N' concurrent jobs. Default 2.
        '-z'   or '--sanitize'          Strip badchars from filename (see manpage).
        '-S'   or '--strict'            Stop running on recoverable errors.
        '-P'   or '--pedantic'          Stop running on tag errors (implies --strict).
        '--stasis'                      Preserve timestamp (mtime) of original file.

    Rename Option:
        '-n X' or '--rename=X'          Rename all outfiles (and temporary wavs) according to string 'X':

        'X' is a string with special designators to be pulled from the file tags.
        The following options are available:
          %t Song title        %b Album title
          %n Track number      %a Artist name
          %y Year              %g Genre
          %c Comment field
        Directories may be created with this command, as in "%b/%n - %t".  The default value of X is "%n %t".

    Bitrate/Quality/Compression Options:
        '-b'   or '--bitrate'           Bitrate. Passed directly to lame/gogo/bladeenc/oggenc/faac.
        '-q'   or '--quality'           Quality. Passed directly to lame/gogo/oggenc/mppenc.
        '-c'   or '--compression'       Flac/mac compression.

    Format Options:
        '--out=X'             Output format. Default is 'wav'.
        '--in=X'              Input format(s). Default is 'wav'.

        Valid output formats are: 
            #{pp(OUTOK)}

        Valid audio input formats are:
            #{pp(INTOK)}

        Valid video input formats are:
            #{pp(VITOK)}

        You can specify multiple input formats using a comma: 'mp3,m4a,wma'.
        The "copy" output option copies files directly, bypassing decoding/encoding steps 
          (useful in conjunction with --rename).
        Input format is only neccesary when passing directory arguments.

    Tag Options:
        '--tt' or '--title'             Set 'title' tag.
        '--ta' or '--artist'            Set 'artist' tag.
        '--tl' or '--album'             Set 'album' tag.
        '--ty' or '--year'              Set 'year' tag.
        '--tc' or '--comment'           Set 'comment' tag.
        '--tg' or '--genre'             Set 'genre' tag.
        '--tn' or '--trackn'            Set 'track number' tag.

        The short and long versions are different in an important way: Using the long version
        will clobber any existing tags. The short version will only set the tag if the existing
        tag has no value. Note that these tags will be placed in _every_ outfile during the run.

    Alternative Encoder/Decoder Options:
        '-G' or '--gogo'                Use gogo to encode mp3 files.
        '-B' or '--bladeenc'            Use bladeenc to encode mp3 files.

    Special Options:
        '--in-optionhook=ARG'           Add ARG to infile conversion command.
        '--out-optionhook=ARG'          Add ARG to outfile conversion command.

USAGE
  exit 0
end

#
#  Genre hash for ID3 tags.
#  Cut 'n' pasted from somewhere ;)
#
def genre_strings
  genre_tags = {
    0x00 => "Blues", 0x01 => "Classic Rock", 0x02 => "Country", 0x03 => "Dance", 0x04 => "Disco",
    0x05 => "Funk", 0x06 => "Grunge", 0x07 => "Hip-Hop", 0x08 => "Jazz", 0x09 => "Metal", 0x0A => "New Age",
    0x0B => "Oldies", 0x0C => "Other", 0x0D => "Pop", 0x0E => "R&B", 0x0F => "Rap", 0x10 => "Reggae",
    0x11 => "Rock", 0x12 => "Techno", 0x13 => "Industrial", 0x14 => "Alternative", 0x15 => "Ska",
    0x16 => "Death Metal", 0x17 => "Pranks", 0x18 => "Soundtrack", 0x19 => "Euro-Techno", 0x1A => "Ambient",
    0x1B => "Trip-Hop", 0x1C => "Vocal", 0x1D => "Jazz+Funk", 0x1E => "Fusion", 0x1F => "Trance",
    0x20 => "Classical", 0x21 => "Instrumental", 0x22 => "Acid", 0x23 => "House", 0x24 => "Game",
    0x25 => "Sound Clip", 0x26 => "Gospel", 0x27 => "Noise", 0x28 => "Alt. Rock", 0x29 => "Bass",
    0x2A => "Soul", 0x2B => "Punk", 0x2C => "Space", 0x2D => "Meditative", 0x2E => "Instrumental Pop",
    0x2F => "Instrumental Rock", 0x30 => "Ethnic", 0x31 => "Gothic", 0x32 => "Darkwave", 0x33 => "Techno-Industrial",
    0x34 => "Electronic", 0x35 => "Pop-Folk", 0x36 => "Eurodance", 0x37 => "Dream", 0x38 => "Southern Rock",
    0x39 => "Comedy", 0x3A => "Cult", 0x3B => "Gangsta", 0x3C => "Top 40", 0x3D => "Christian Rap",
    0x3E => "Pop/Funk", 0x3F => "Jungle", 0x40 => "Native US", 0x41 => "Cabaret", 0x42 => "New Wave",
    0x43 => "Psychadelic", 0x44 => "Rave", 0x45 => "Showtunes", 0x46 => "Trailer", 0x47 => "Lo-Fi",
    0x48 => "Tribal", 0x49 => "Acid Punk", 0x4A => "Acid Jazz", 0x4B => "Polka", 0x4C => "Retro",
    0x4D => "Musical", 0x4E => "Rock & Roll", 0x4F => "Hard Rock", 0x50 => "Folk", 0x51 => "Folk-Rock",
    0x52 => "National Folk", 0x53 => "Swing", 0x54 => "Fast Fusion", 0x55 => "Bebop",
    0x56 => "Latin", 0x57 => "Revival", 0x58 => "Celtic", 0x59 => "Bluegrass", 0x5A => "Avantgarde",
    0x5B => "Gothic Rock", 0x5C => "Progressive Rock", 0x5D => "Psychedelic Rock", 0x5E => "Symphonic Rock",
    0x5F => "Slow Rock", 0x60 => "Big Band", 0x61 => "Chorus", 0x62 => "Easy Listening", 0x63 => "Acoustic",
    0x64 => "Humour", 0x65 => "Speech", 0x66 => "Chanson", 0x67 => "Opera", 0x68 => "Chamber Music",
    0x69 => "Sonata", 0x6A => "Symphony", 0x6B => "Booty Bass", 0x6C => "Primus", 0x6D => "Porn Groove",
    0x6E => "Satire", 0x6F => "Slow Jam", 0x70 => "Club", 0x71 => "Tango", 0x72 => "Samba",
    0x73 => "Folklore", 0x74 => "Ballad", 0x75 => "Power Ballad", 0x76 => "Rhythmic Soul", 0x77 => "Freestyle",
    0x78 => "Duet", 0x79 => "Punk Rock", 0x7A => "Drum Solo", 0x7B => "Acapella", 0x7C => "Euro-House",
    0x7D => "Dance Hall", 0x7E => "Goa", 0x7F => "Drum & Bass", 0x80 => "Club-House", 0x81 => "Hardcore",
    0x82 => "Terror", 0x83 => "Indie", 0x84 => "BritPop", 0x85 => "Negerpunk", 0x86 => "Polsk Punk",
    0x87 => "Beat", 0x88 => "Christian Gangsta Rap", 0x89 => "Heavy Metal", 0x8A => "Black Metal", 0x8B => "Crossover",
    0x8C => "Contemporary Christian", 0x8D => "Christian Rock", 0x8E => "Merengue", 0x8F => "Salsa", 0x90 => "Trash Metal"
  }
end

module TagDisplayer
  ATTRIBUTES = %w{title artist album genre year comment tracknum}
  attr_reader :title, :artist, :album, :genre, :year, :comment, :tracknum

  def to_a
    ATTRIBUTES.map { |x| send(x) }
  end

  def to_s
    space = " "
    ATTRIBUTES.map { |x| "#{space * 5}#{x.capitalize}:#{space * (10 - x.length)}#{send(x)}" }.join("\n")
  end
end

#
# Class for meta tags.
# Provides default values if there is no specialized tag class
#
class MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @opts = opts
    check_tags
  end

  #
  #  Check for tags passed on the CLI.
  #
  def check_tags
    if @opts[:tags_c] != {}
      @title    = @opts[:tags_c]['title']    || @title
      @artist   = @opts[:tags_c]['artist']   || @artist
      @album    = @opts[:tags_c]['album']    || @album
      @year     = @opts[:tags_c]['year']     || @year
      @comment  = @opts[:tags_c]['comment']  || @comment
      @genre    = @opts[:tags_c]['genre']    || @genre
      @tracknum = @opts[:tags_c]['tracknum'] || @tracknum
    end

    if @opts[:tags_nc] != {}
      @title    == nil ? @opts[:tags_nc]['title']    || @title    : @title
      @artist   == nil ? @opts[:tags_nc]['artist']   || @artist   : @artist
      @album    == nil ? @opts[:tags_nc]['album']    || @album    : @album
      @year     == nil ? @opts[:tags_nc]['year']     || @year     : @year
      @comment  == nil ? @opts[:tags_nc]['comment']  || @comment  : @comment
      @genre    == nil ? @opts[:tags_nc]['genre']    || @genre    : @genre
      @tracknum == nil ? @opts[:tags_nc]['tracknum'] || @tracknum : @tracknum
    end

    #
    #  Dump empty tags
    #
    @title    = File.basename(@filename, @filename[-4..-1]) if @title == nil || @title =~ /^(\s)*$/
    @artist   = @title if @artist == nil   || @artist =~  /^(\s)*$/ #  Just whitespace.
    @album    = 'Unknown' if @album == nil || @album =~  /^(\s)*$/  #   "      "
    @genre    = 'Unknown' if @genre == nil || @genre =~ /^(\s)*$/   #   "      "
    @year     = 'Unknown' if @year == nil  || @year =~ /^(\s)*$/    #   "      "
    @comment  = 'Starbellied using sneetchalizer (http://badcomputer.org/code/sneetchalizer/)' if @comment == nil || @comment =~ /^(\s)*$/
    @tracknum = 'Unknown' if @tracknum == nil || @tracknum =~ /^(\s)*$/

    #
    #  Translate GENREID/CONTENTTYPE number to string.
    #  Some are just a number (within a string)
    #  while others are buried in parentheses and whatnot.
    #
    if @genre =~ /\d{1,2}/
      genre_tags = genre_strings()
      @genre = genre_tags[$&.to_i] #  Last match.
    end

    if @opts[:sanitize]
      @title.gsub!(/[,;:'"%@#`]/, '')
      @artist.gsub!(/[,;:'"%@#`]/, '')
      @album.gsub!(/[,;:'"%@#`]/, '')
      @genre.gsub!(/[,;:'"%@#`]/, '')
      @year.gsub!(/[,;:'"%@#`]/, '')
      @comment.gsub!(/[,;:'"%@#`]/, '')

    else
      @title.gsub!('"', "'")      #
      @artist.gsub!('"', "'")     # Normalize tag quote marks.
      @album.gsub!('"', "'")      #
      @genre.gsub!('"', "'")      #
      @year.gsub!('"', "'")       #
      @comment.gsub!('"', "'")    #


      @title.gsub!('`',"'")       #
      @album.gsub!('`',"'")       # Backticks break bash.
      @artist.gsub!('`',"'")      #
      @genre.gsub!('`', "'")      #
      @year.gsub!('`', "'")       #
      @comment.gsub!('`', "'")    #
    end
  end
end


#
# Class for mp3 meta tags.
#
class Mp3MetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts
    @done = nil
    try_ruby_mp3info
    try_id3lib unless @done
    try_id3 unless @done
    check_tags
  end

  #
  # Begin 'ruby-mp3info' (http://rubyforge.org/projects/ruby-mp3info/)
  #
  def try_ruby_mp3info
    begin
      require 'mp3info'
    rescue LoadError
      caution("ruby-mp3info library not found...")
      exit 17 if @pedantic == 1
      return
    end

    begin
      @tags = Mp3Info.open(@filename)
      @title = @tags.tag['title'] || nil
      @artist = @tags.tag['artist'] || nil
      @album = @tags.tag['album'] || nil
      @genre = @tags.tag['genre_s'] || @tags.tag['genre'] ||'Other'
      @year = @tags.tag['year'] || nil
      @comment = @tags.tag['comments'] || nil
      @tracknum = @tags.tag['tracknum'] || nil
    rescue
      exit 18 if @pedantic == 1
      check_tags
    end
    @done = 1
  end # End 'ruby-mp3info'

  #
  # Begin 'id3lib-ruby' (http://id3lib-ruby.rubyforge.org/)
  #
  def try_id3lib
    begin
      require 'id3lib'
    rescue LoadError
      caution("id3lib-ruby library not found...")
      exit 17 if @pedantic == 1
      return
    end

    begin
      @tags = ID3Lib::Tag.new(@filename)
      @title = @tags.title || nil
      @artist = @tags.artist || nil
      @album = @tags.album || nil
      @genre = @tags.genre ||'Other'
      @year = @tags.year || nil
      @comment = @tags.comment || nil
      @tracknum = @tags.track || nil
    rescue
      exit 18 if @pedantic == 1
      check_tags
    end
    @done = 1
  end # End 'id3lib-ruby'

  #
  # Begin 'id3' (http://www.unixgods.org/~tilo/Ruby/ID3-v0.4/docs/index.html)
  #
  def try_id3
    begin
      require 'id3'
    rescue LoadError
      caution("id3.rb library not found... creating scratch tags")
      exit 17 if @pedantic == 1
      return
    end

    def id3v1
      @title = @tags.tagID3v1['TITLE'] || nil
      @artist = @tags.tagID3v1['ARTIST'] || nil
      @album = @tags.tagID3v1['ALBUM'] || nil
      @genre = @tags.tagID3v1['GENREID'] || 'Other'
      @year = @tags.tagID3v1['YEAR'] || nil
      @comment = @tags.tagID3v1['COMMENT'] || nil
      @tracknum = @tags.tagID3v1['TRACKNUM'] || nil
    end

    def id3v2
      @title = @tags.tagID3v2['TITLE']['text'] || nil
      @artist = @tags.tagID3v2['ARTIST']['text'] || @tags.tagID3v1['BAND'] || nil
      @album = @tags.tagID3v2['ALBUM']['text'] || nil
      @genre = @tags.tagID3v2['CONTENTTYPE']['text'] || 'Other' #  No Genre id3 tag in 2.x
      @year = @tags.tagID3v2['YEAR']['text'] || @tags.tagID3v2['DATE']['text'] || nil
      @comment = @tags.tagID3v2['COMMENT']['text'] || nil
      @tracknum = @tags.tagID3v2['TRACKNUM']['text'] || nil
    end

    begin
      @tags = ID3::AudioFile.new(@filename)
      if @tags.version == nil #  Some mp3s with broken tags return nil here...
        exit 18 if @pedantic == 1
        return                #  ...so we bail out.
      end

      if @tags.version.split.size == 2
        id3v2  # if both version 1 and 2 tags are present, prefer 2
      elsif @tags.version.split(".")[0] == "1" #  id3 v1.x
        id3v1
      else     #  id3 v2.x
        id3v2
      end

      check_tags
    rescue
      exit 18 if @pedantic == 1
      check_tags
    end
  end # End 'id3' (http://www.unixgods.org/~tilo/Ruby/ID3-v0.4/docs/index.html)
end


#
#  Class for wma meta tags
#  Requires wmainfo-rb
#  http://badcomputer.org/unix/code/wmainfo/
#
class WmaMetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts

    begin
      require 'wmainfo'
    rescue LoadError
      caution("wmainfo-rb library not found ... creating scratch tags.")
      exit 15 if @pedantic == 1
      check_tags
    end

    begin
      song = WmaInfo.new(@filename)
      @title = song.tags['Title']

      if song.hastag?('Author')
        @artist = song.tags['Author']
      else
        @artist = song.tags['AlbumArtist']
      end

      @album = song.tags['AlbumTitle']
      @genre = song.tags['Genre']
      @year = song.tags['Year']
      @tracknum = song.tags['TrackNumber']

      check_tags
    rescue
      exit 16 if @pedantic == 1
      check_tags
    end
  end
end


#
#  Class for Flac meta tags.
#  Requires flacinfo-rb.
#  http://badcomputer.org/unix/code/flacinfo/
#
class FlacMetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts

    begin
      require 'flacinfo'
    rescue LoadError
      caution("flacinfo-rb library not found ... creating scratch tags.")
      exit 19 if @pedantic == 1
      check_tags
    end

    begin
      song = FlacInfo.new(@filename)
      song.tags.each do |k,v|

        if k.casecmp("artist") == 0
          @artist = v
          next

        elsif k.casecmp("title") == 0
          @title = v
          next

        elsif k.casecmp("album") == 0
          @album = v
          next

        elsif k.casecmp("genre") == 0
          @genre = v
          next

        elsif k.casecmp("date") == 0
          @year = v
          next

        elsif k.casecmp("comment") == 0
          @comment = v
          next

        elsif k.casecmp("tracknumber") == 0
          @tracknum = v
          next
        end

      end
      check_tags

    rescue
      exit 20 if @pedantic == 1
      check_tags
    end
  end
end


#
#  Class for m4a meta tags.
#  Requires MP4Info or faad.
#  http://rubyforge.org/projects/mp4info/
#
class M4aMetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts

    begin
      require 'mp4info'
      use_mp4_info_lib
    rescue LoadError
      caution("mp4info library not found ... trying 'faad' binary.")
      exit 24 if @pedantic == 1
      use_faad_bin
    end

    check_tags
  end

  def use_mp4_info_lib
    begin
      tags = MP4Info.open(@filename)
      @title    = tags.send("NAM")
      @artist   = tags.send("ART")
      @album    = tags.send("ALB")
      @genre    = tags.send("GNRE")
      @year     = tags.send("DAY")
      @comment  = tags.send("CMT")
      num       = tags.send("TRKN")
      @tracknum = "#{num[0]}/#{num[1]}"

    rescue
      use_faad_bin
    end
  end

  def use_faad_bin
    begin
      tags = `faad -i "#@filename" 2>&1`.split("\n")
      tags.delete("")
      xtags = {}
      re = /title:|artist:|album:|genre:|track:|totaltracks:|comment:|date:/i

      #
      #  This ugly line builds a dict of 'tag'=>'value' pairs.
      #
      tags.each { |line| xtags["#{line.split(":")[0]}"] = line.split(":")[1..-1].join("").strip if line =~ re }

      @title   = xtags["title"]
      @artist  = xtags["artist"]
      @album   = xtags["album"]
      @genre   = xtags["genre"]
      @year    = xtags["date"]
      @comment = xtags["comment"]
      num      = xtags["track"]
      total    = xtags["totaltracks"]

      if xtags["totaltracks"] != nil
        @tracknum = "#{num}/#{total}"
      else
        @tracknum = num
      end
    rescue
      exit 14 if @pedantic == 1
    end
  end
end


#
#  Class for ogg meta tags.
#  Requires OggInfo (ruby-lib) or ogginfo (binary).
#  http://rubyforge.org/projects/ruby-ogginfo/
#
class OggMetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts

    begin
      require 'ogginfo'
      use_ogginfo_lib
    rescue LoadError
      caution("ogginfo library not found ... trying 'ogginfo' binary.")
      exit 23 if @pedantic == 1
      use_ogginfo_bin
    end

    check_tags
  end

  def use_ogginfo_lib
    begin
      song = OggInfo.new(@filename)
      song.tag.each do |k,v|
        if k =~ /artist/i
          @artist = v
          next

        elsif k =~ /title/i
          @title = v
          next

        elsif k =~ /album/i
          @album = v
          next

        elsif k =~ /genre/i
          @genre = v
          next

        elsif k =~ /date/i
          @year = v
          next

        elsif k =~ /description/i
          @comment = v
          next

        elsif k =~ /tracknumber/i
          @tracknum = v
          next
        end
      end
    rescue
      exit 13 if @pedantic == 1
      use_ogginfo_bin
    end
  end

  def use_ogginfo_bin
    begin
      tags = `ogginfo "#@filename"`.split("\n")
      tags.delete("")
      xtags = {}
      re = /title=|artist=|album=|genre=|tracknumber=|description=|date=/i

      #
      #  This uglier block builds a dict of 'tag'=>'value' pairs.
      #
      tags.each do |line|
        line.strip!
        xtags["#{line.split("=")[0]}"] = line.split("=")[1..-1].join("").strip if line =~ re
      end

      @title    = xtags["title"]       || xtags["Title"]
      @artist   = xtags["artist"]      || xtags["Artist"]
      @album    = xtags["album"]       || xtags["Album"]
      @genre    = xtags["genre"]       || xtags["Genre"]
      @year     = xtags["date"]        || xtags["Date"]
      @comment  = xtags["description"] || xtags["description"]
      @tracknum = xtags["tracknumber"] || xtags["Tracknumber"]
    rescue
      exit 13 if @pedantic == 1
    end
  end
end


#
#  Class for ape, wv, and mpc meta tags.
#  Requires apetag.
#  http://rubyforge.org/projects/apetag/
#
class ApeMetaTags < MetaTags
  include TagDisplayer

  def initialize(filename, opts)
    @filename = filename
    @pedantic = opts[12]
    @opts = opts

    begin
      require 'apetag'
    rescue LoadError
      caution("apetag library not found ... creating scratch tags.")
      exit 21 if @pedantic == 1
      check_tags
    end

    begin
      song = ApeTag.new(@filename)
      title =    song.fields["title"]   || song.fields["Title"]
      artist =   song.fields["artist"]  || song.fields["Artist"]
      album =    song.fields["album"]   || song.fields["Album"]
      genre =    song.fields["genre"]   || song.fields["Genre"]
      year =     song.fields["date"]    || song.fields["Date"]  || song.fields["year"] || song.fields["Year"]
      comment =  song.fields["comment"] || song.fields["Comment"]
      tracknum = song.fields["track"]   || song.fields["Track"] || song.fields["Tracknumber"]

      #
      #  Fields may have multiple values.
      #
      @title = title.join(" ")
      @artist = artist.join(" ")
      @album = album.join(" ")
      @genre = genre.join(" ")
      @year = year.join(" ")
      @comment = comment.join(" ")
      @tracknum = tracknum.join(" ")

      check_tags
    rescue
      check_tags
      exit 22 if @pedantic == 1
    end
  end
end


class ConversionError < StandardError
end


class Convert
    attr_reader :mtime
  def initialize(filename, fileroot, type, opts)
    @filename = filename
    @fileroot = fileroot
    @type = type
    @opts = opts

    @mtime = File.stat(@filename).mtime if @opts[:stasis]

    begin
    print "Working on: "
    info(File.basename(@filename)) 
    end unless @opts[:threads]

    case @type
      when 'ogg'
        @tags = OggMetaTags.new(@filename, opts)
      when 'm4a', 'm4b', 'mp4', 'aac'
        @tags = M4aMetaTags.new(@filename, opts)
      when 'mp3'
        @tags = Mp3MetaTags.new(@filename, opts)
      when 'wma', 'wmv', 'asf'
        @tags = WmaMetaTags.new(@filename, opts)
      when 'flac'
        @tags = FlacMetaTags.new(@filename, opts)
      when 'ape', 'wv', 'mpc', 'mpp', 'tta', 'ofr', 'ofs'
        @tags = ApeMetaTags.new(@filename, opts)
      else
        @tags = MetaTags.new(@filename, opts)
    end

    if opts[:rename]
      name_conversions = {'t' => @tags.title,
                          'a' => @tags.artist,
                          'b' => @tags.album,
                          'g' => @tags.genre,
                          'y' => @tags.year,
                          'n' => ('0' + @tags.tracknum.to_s)[-2,2],
                          'c' => @tags.comment
                         }
      newfileroot = opts[:rename]
      newfileroot = newfileroot.gsub(/%([tabgync])/) { |match| name_conversions[$1] }

      #
      #  Handle creation of new folders if necessary.
      #
      index = 0
      while (index = newfileroot.index('/', index+1))
        dir = @opts[:outdir] + newfileroot[0,index]
        unless File.exist?(dir) == true
          info("    creating directory #{dir}")
          Dir.mkdir(dir)
        end
      end
      @fileroot = newfileroot
    end

    @t = @tags.to_a
    puts @tags.to_s if @opts[:verbose]
  end

  def deleteWav
    File.delete(@opts[:outdir] + @fileroot + '.wav') unless @opts[:pretend]
  end

  def outputFilename
    if @opts[:outtype] == 'copy'
      outext = @type
    else
      outext = @opts[:outtype]
    end
    return "#{@opts[:outdir]}#{@fileroot}.#{outext}"
  end

  #
  # Decoding ops and commands.
  #
  def copyFile
    require 'fileutils'
    outfile = "#{@opts[:outdir]}#{@fileroot}.#{@type}"
    FileUtils.cp "#@filename", outfile unless @filename == outfile
    return true
  end

  def oggToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/oggdec #{@opts[:inOptionHook]} /
    command += %Q/--quiet / unless @opts[:showoutput]
    command += %Q/-o "#{@opts[:outdir]}#{@fileroot}.wav" "#@filename"/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def m4aToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/faad #{@opts[:inOptionHook]} -o "#{@opts[:outdir]}#{@fileroot}.wav" "#@filename"/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def mp3ToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/lame #{@opts[:inOptionHook]} /
    #
    #  We can't use --silent because it isn't really silent!
    #
    command += %Q/--decode "#@filename" "#{@opts[:outdir]}#{@fileroot}.wav"/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wmaToWav
    info("     converting to wav...") unless @opts[:threads]
    command =  %Q/mplayer #{@opts[:inOptionHook]} -vo null -vc null -af /
    command += %Q/resample=44100 -ao pcm:file="#{@opts[:outdir]}#{@fileroot}.wav" "#@filename"/
    command += ' 1>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def flacToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/flac -f #{@opts[:inOptionHook]} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/-o "#{@opts[:outdir]}#{@fileroot}.wav" --decode "#@filename"/
    runCommand(command)
  end

  def apeToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/mac #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" -d/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wvToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/wvunpack -y #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" -o "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def mpcToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/mppdec #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def ttaToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/ttaenc #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" -do "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def aifToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def auToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def mp2ToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def cdrToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def ofrToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/ofr #{@opts[:inOptionHook]} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/"#@filename" --output "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def ofsToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/ofs #{@opts[:inOptionHook]} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/"#@filename" --output "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def spxToWav
    info("     converting to wav...") unless @opts[:threads]
    command = %Q/speexdec #{@opts[:inOptionHook]} /
    command += %Q/"#@filename" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def vidToWav
    info("     converting audio track to wav...") unless @opts[:threads]
    if not check_for "mplayer"
      error("mplayer not found!")
      puts "Please check your PATH and ensure 'mplayer' is installed"
      exit 43
    end
    command = %Q/mplayer -vc dummy -vo null -nortc -ao pcm:file="#{@opts[:outdir]}#{@fileroot}.wav" /
    command += %Q/"#@filename" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  #
  #  Encoding ops and commands.
  #
  def wavToOgg
    info("     converting to ogg...") unless @opts[:threads]
    if not check_for "oggenc"
      error("No Ogg Vorbis encoder found!")
      puts "Please check your PATH and ensure 'oggenc' is installed"
      exit 26
    end
    command =  %Q/oggenc #{@opts[:outOptionHook]} /
    command += @opts[:qrate] != nil ? "-q #{@opts[:qrate]} " : ""
    command += @opts[:brate] != nil ? "-b #{@opts[:brate]} " : ""
    command += '--quiet ' unless @opts[:showoutput]
    command += %Q/-t "#{@t[0]}" -a "#{@t[1]}"  -l "#{@t[2]}" -G "#{@t[3]}" /
    command += %Q/-d "#{@t[4]}" -c comment="#{@t[5]}" -N "#{@t[6]}" /
    command += %Q/-o "#{@opts[:outdir]}#{@fileroot}.ogg" "#{@opts[:outdir]}#{@fileroot}.wav"/
    runCommand(command)
  end

  def wavToMp3
    info("     converting to mp3...") unless @opts[:threads]
    if @opts[:gogo]
      command = use_gogo
    elsif @opts[:bladeenc]
      command = use_bladeenc
    else
      if check_for "lame"
        lame = true
        command = use_lame
      elsif check_for "bladeenc"
        command = use_bladeenc
      elsif check_for "gogo"
        command = use_gogo
      else
        error("No mp3 encoder found!")
        puts "Please check your PATH and ensure one of 'lame', 'bladeenc' or 'gogo' is installed"
        exit 27
      end
    end
    es = runCommand(command)
    #
    # We cannot pass tags on the CLI unless we use 'lame'.
    #
    write_id3_tags unless lame
    es
  end

  def use_gogo
    command =  %Q/gogo #{@opts[:outOptionHook]} /
    command += @opts[:qrate] != nil ? "-q #{@opts[:qrate]} " : ""
    command += @opts[:brate] != nil ? "-b #{@opts[:brate]} " : ""
    command += '-silent ' unless @opts[:showoutput]
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@opts[:outdir]}#{@fileroot}.mp3"/
    #
    # gogo's '-silent' not silent enough ;)
    #
    command += ' 2>/dev/null' unless @opts[:showoutput]
  end

  def use_bladeenc
    command =  %Q/bladeenc #{@opts[:outOptionHook]} /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" /
    command += @opts[:brate] != nil ? "-br #{@opts[:brate]} " : ""
    command += ' 1>/dev/null' unless @opts[:showoutput]
  end

  def use_lame
    command =  %Q/lame #{@opts[:outOptionHook]} --id3v2-only --ignore-tag-errors /
    command += @opts[:qrate] != nil ? "-q #{@opts[:qrate]} " : ""
    command += @opts[:brate] != nil ? "-b #{@opts[:brate]} " : ""
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/--tt "#{@t[0]}" --ta "#{@t[1]}"  --tl "#{@t[2]}" --tg "#{@t[3]}" --ty /
    command += %Q/"#{@t[4]}" --tc "#{@t[5]}" --tn "#{@t[6]}" /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@opts[:outdir]}#{@fileroot}.mp3"/
  end

  def wavToFlac
    info("     converting to flac...") unless @opts[:threads]
    if not check_for "flac"
      error("No Flac encoder found!")
      puts "Please check your PATH and ensure 'flac' is installed"
      exit 28
    end
    command =  %Q/flac #{@opts[:outOptionHook]} -f --tag="title=#{@t[0]}" --tag="artist=#{@t[1]}" /
    command += %Q/--tag="album=#{@t[2]}" --tag="genre=#{@t[3]}" --tag="date=#{@t[4]}" /
    command += %Q/--tag="comment=#{@t[5]}" --tag="tracknumber=#{@t[6]}" -#{@opts['compression']} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" -o "#{@opts[:outdir]}#{@fileroot + '.flac'}"/
    runCommand(command)
  end

  def wavToM4a
    info("     converting to #{@opts[:outtype]}...") unless @opts[:threads]
    if not check_for "faac"
      error("No Mp4 encoder found!")
      puts "Please check your PATH and ensure 'faac' is installed"
      exit 33
    end
    command =  %Q/faac #{@opts[:outOptionHook]} /
    command += @opts['brate'] != nil ? "-b #{@opts['brate']} " : ""
    command += %Q/-w --title "#{@t[0]}" --artist "#{@t[1]}" --album "#{@t[2]}" --genre "#{@t[3]}" /
    command += %Q/--year "#{@t[4]}" --comment "#{@t[5]}" --track "#{@t[6]}" "#{@opts[:outdir]}#{@fileroot}.wav" /
    command += %Q/-o "#{@opts[:outdir]}#{@fileroot}.#{@opts[:outtype]}"/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    return runCommand(command)
  end

  def wavToApe
    info("     converting to ape...") unless @opts[:threads]
    if not check_for "mac"
      error("No Monkey's Audio encoder found!")
      puts "Please check your PATH and ensure 'mac' is installed"
      exit 34
    end
    command =  %Q/mac #{@opts[:outOptionHook]} /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.#{@opts[:outtype]}"/
    command += @opts[:compression] != nil ? " -c#{@opts[:compression]} " : " -c2000"
    command += ' 2>/dev/null' unless @opts[:showoutput]
    es = runCommand(command)
    #
    # No way to write tags on the CLI with 'mac'
    # so we write them with apetag after encoding.
    #
    write_ape_tags
    es
  end

  def wavToWv
    info("     converting to wv...") unless @opts[:threads]
    if not check_for "wavpack"
      error("No Wavpack encoder found!")
      puts "Please check your PATH and ensure 'wavpack' is installed"
      exit 35
    end
    command =  %Q/wavpack #{@opts[:outOptionHook]} -w "title=#{@t[0]}" -w "artist=#{@t[1]}" /
    command += %Q/-w "album=#{@t[2]}" -w "genre=#{@t[3]}" -w "year=#{@t[4]}" /
    command += %Q/-w "comment=#{@t[5]}" -w "track=#{@t[6]}" /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wavToMpc
    info("     converting to #{@opts[:outtype]}...") unless @opts[:threads]
    if not check_for "mppenc"
      error("No Musepack encoder found!")
      puts "Please check your PATH and ensure 'mppenc' is installed"
      exit 36
    end
    command =  %Q/mppenc #{@opts[:outOptionHook]} --title "#{@t[0]}" --artist "#{@t[1]}" /
    command += %Q/--album "#{@t[2]}" --genre "#{@t[3]}" --year "#{@t[4]}" /
    command += %Q/--comment "#{@t[5]}" --track "#{@t[6]}" --overwrite /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@fileroot}.#{@opts[:outtype]}"/
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wavToTta
    info("     converting to tta...") unless @opts[:threads]
    if not check_for "ttaenc"
      error("No True Audio encoder found!")
      puts "Please check your PATH and ensure 'ttaenc' is installed"
      exit 37
    end
    command = %Q/ttaenc #{@opts[:inOptionHook]} /
    command += %Q/-e "#{@opts[:outdir]}#{@fileroot}.wav" -o "#{@opts[:outdir]}#{@fileroot}.tta" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    es = runCommand(command)
    write_ape_tags
    es
  end

  def wavToMp2
    info("     converting to mp2...") unless @opts[:threads]
    if not check_for "sox"
      error("No Mp2 encoder found!")
      puts "Please check your PATH and ensure 'sox' is installed"
      exit 38
    end
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@opts[:outdir]}#{@fileroot}.mp2" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wavToSox
    info("     converting to #{@opts[:outtype]}...") unless @opts[:threads]
    if not check_for "sox"
      error("No #{@opts[:outtype]} encoder found!")
      puts "Please check your PATH and ensure 'sox' is installed"
      exit 38
    end
    command = %Q/sox #{@opts[:inOptionHook]} /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@opts[:outdir]}#{@fileroot}.#{@opts[:outtype]}" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wavToOfr
    info("     converting to ofr...") unless @opts[:threads]
    if not check_for "ofr"
      error("No OptimFROG encoder found!")
      puts "Please check your PATH and ensure 'ofr' is installed"
      exit 39
    end
    command = %Q/ofr #{@opts[:inOptionHook]} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" --output "#{@opts[:outdir]}#{@fileroot}.ofr" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    es = runCommand(command)
    write_ape_tags
    es
  end

  def wavToOfs
    info("     converting to ofs...") unless @opts[:threads]
    if not check_for "ofs"
      error("No OptimFROG encoder found!")
      puts "Please check your PATH and ensure 'ofs' is installed"
      exit 39
    end
    command = %Q/ofs #{@opts[:inOptionHook]} /
    command += '--silent ' unless @opts[:showoutput]
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" --output "#{@opts[:outdir]}#{@fileroot}.ofs" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    es = runCommand(command)
    write_ape_tags
    es
  end

  def wavToSpx
    info("     converting to spx...") unless @opts[:threads]
    if not check_for "speexenc"
      error("No Speex encoder found!")
      puts "Please check your PATH and ensure 'sox' is installed"
      exit 40
    end
    command = %Q/speexenc #{@opts[:inOptionHook]} -u /
    command += %Q/--author "#{@t[1]}" --title "#{@t[0]}" /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav" "#{@opts[:outdir]}#{@fileroot}.spx" /
    command += ' 2>/dev/null' unless @opts[:showoutput]
    runCommand(command)
  end

  def wavToWma
    info("     converting to wma...") unless @opts[:threads]
    if not check_for "mplayer"
      error("No WMA encoder found!")
      puts "Please check your PATH and ensure 'mplayer' is installed"
      exit 42
    end
    command = %Q/mplayer -vc dummy -vo null -nortc -ao pcm:file="#{@opts[:outdir]}#{@fileroot}}.wma" /
    command += %Q/"#{@opts[:outdir]}#{@fileroot}.wav"/
  end

  private
  def check_for(app)
    bindirs = ENV['PATH'].split(":")
    bindirs.each do |dir|
      if File.exist?("#{dir}/#{app}")
        return true
      end
    end
    return false
  end

  def runCommand(command)
    #
    # FIXME: Strip Null byte from command
    # Unable to track where it originates (suspect Mp3Info)
    #
    command.gsub!(0.chr,'')

    if @opts[:verbose]
      info("     Running:")
      puts "     " + command
    end

    if @opts[:pretend]
      info("     I would run:")
      puts command
    else
      if not system(command)
        error("Error: #{$?}")
        print "Failed to write: "
        info(File.basename(self.outputFilename))
        exit 10 if @opts[:strict]
        caution("Skipping #{@filename}")
        return false
      end
    end
    return true
  end

  def write_ape_tags
    begin
      fh = ApeTag.new("#{@fileroot}.#{@opts[:outtype]}")
    rescue
      require 'apetag'
      fh = ApeTag.new("#{@fileroot}.#{@opts[:outtype]}")
    end

    fh.update { |fields|
      fields['Title']   = "#{@t[0]}"
      fields['Artist']  = "#{@t[1]}"
      fields['Album']   = "#{@t[2]}"
      fields['Genre']   = "#{@t[3]}"
      fields['Year']    = "#{@t[4]}"
      fields['Comment'] = "#{@t[5]}"
      fields['Track']   = "#{@t[6]}"
    }
  end

  def write_id3_tags
    begin
      fh = Mp3Info.open("#{@fileroot}.mp3")
    rescue
      require 'mp3info'
      fh = Mp3Info.open("#{@fileroot}.mp3")
    end
    fh.tag.title    = @t[0]
    fh.tag.artist   = @t[1]
    fh.tag.album    = @t[2]
    fh.tag.genre_s  = @t[3]
    fh.tag.year     = @t[4].to_i
    fh.tag.comments = @t[5]
    fh.tag.tracknum = @t[6].split("/")[0].to_i
    fh.close
  end

  public
  #
  #  MPEG-4 file extensions.
  #
  alias wavToM4b wavToM4a
  alias wavToMp4 wavToM4a
  alias wavToAac wavToM4a

  #
  #  Musepack file extensions.
  #
  alias wavToMpp wavToMpc

  #
  # The rest of these audio formats all use sox.
  #

  #
  #  Audio Interchange file extensions.
  #
  alias wavToAif   wavToSox 
  alias wavToAiff  wavToSox
  alias wavToAifc  wavToSox
  alias wavToAiffc wavToSox

  #
  #  Au file extensions.
  #
  alias wavToAu  wavToSox
  alias wavToSnd wavToSox

  #
  #  Redbook CD audio file extensions.
  #
  alias wavToCdr  wavToSox
  alias wavToCdda wavToSox

  #
  #  Others (audio)....
  #
  alias wavToCaf   wavToSox  #  CAF  (Apple Core Audio File)
  alias wavToSvx   wavToSox  #  IFF  (Amiga IFF/SVX8/SV16)
  alias wavToPaf   wavToSox  #  PAF  (Ensoniq PARIS)
  alias wavToFap   wavToSox  #  PAF  (Ensoniq PARIS)
  alias wavToGsm   wavToSox  #  RAW  (header-less)
  alias wavToNist  wavToSox  #  WAV  (NIST Sphere)
  alias wavToIrcam wavToSox  #  SF   (Berkeley/IRCAM/CARL)
  alias wavToSf    wavToSox  #  SF   (Berkeley/IRCAM/CARL)
  alias wavToVoc   wavToSox  #  VOC  (Creative Labs)
  alias wavToW64   wavToSox  #  W64  (SoundFoundry WAVE 64)
  alias wavToRaw   wavToSox  #  RAW  (header-less)
  alias wavToMat4  wavToSox  #  MAT4 (GNU Octave 2.0 / Matlab 4.2)
  alias wavToMat5  wavToSox  #  MAT5 (GNU Octave 2.1 / Matlab 5.0)
  alias wavToMat   wavToSox  #  MAT4 (GNU Octave 2.0 / Matlab 4.2)
  alias wavToPvf   wavToSox  #  PVF  (Portable Voice Format)
  alias wavToSds   wavToSox  #  SDS  (Midi Sample Dump Standard)
  alias wavToSd2   wavToSox  #  SD2  (Sound Designer II)
  alias wavToVox   wavToSox  #  RAW  (header-less)
  alias wavToXi    wavToSox  #  XI   (FastTracker 2)

  #
  #  Video formats.
  #
  alias rvToWav   vidToWav
  alias asfToWav  vidToWav
  alias aviToWav  vidToWav
  alias divxToWav vidToWav
  alias mkvToWav  vidToWav
  alias mpgToWav  vidToWav
  alias mpegToWav vidToWav
  alias movToWav  vidToWav
  alias ogmToWav  vidToWav
  alias qtToWav   vidToWav
  alias vcdToWav  vidToWav
  alias vobToWav  vidToWav
  alias wmvToWav  vidToWav
  alias flvToWav  vidToWav
  alias svcdToWav vidToWav
  alias m4vToWav  vidToWav
  alias nsvToWav  vidToWav
  alias nuvToWav  vidToWav
  alias pspToWav  vidToWav
  alias smkToWav  vidToWav
end


def sanitize_filename(file, opts)
  begin
    file_clean = file.gsub(/[,;:'"%@\#`]/, '')
    File.rename(file, file_clean) unless opts[:pretend]
  rescue
    puts "Cannot rename #{file}"
    return file
  end
  return file_clean
end

#
#  Instantiate a Convert object for the file, and call the relevant methods.
#
def dispatch_file(file, opts)
  begin
    file = sanitize_filename(file, opts) if opts[:sanitize]
    fileroot, type = /^([^.]*$|.*(?=\.))\.?(.*)$/.match(file)[1..2] # foobar.baz -> foobar, baz
    type.downcase!
  #
  # FIXME: Dirty hack to get threading to work.
  #
  rescue NoMethodError
    return
  end

  if not (INTOK + VITOK).include?(type)
    caution("I don't know how to decode '#{file}'")
    exit 12 if opts[:strict]
    return
  end

  song = Convert.new(file, fileroot, type, opts)

  type = 'copy' if opts[:outtype] == 'copy'

  case type
  when 'copy'
    return unless song.copyFile

  when 'ogg'
    return unless song.oggToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'mp3'
    return unless song.mp3ToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'wma'
    return unless song.wmaToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'flac'
    return unless song.flacToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'ape'
    return unless song.apeToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'wv'
    return unless song.wvToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'wav'
    exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
    return if exit_s == false

  when 'm4a', 'm4b', 'aac', 'mp4'
    return unless song.m4aToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'mpc', 'mpp'
    return unless song.mpcToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'tta'
    return unless song.ttaToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'aif', 'aiff', 'aifc', 'aiffc'
    return unless song.aifToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'au', 'snd'
    return unless song.auToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'mp2'
    return unless song.mp2ToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'cdr', 'cdda'
    return unless song.cdrToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'ofr'
    return unless song.ofrToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'ofs'
    return unless song.ofsToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'spx'
    return unless song.spxToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  when 'asf', 'avi', 'divx', 'flv', 'mkv', 'mpg', 'mpeg', 'mov', 'm4v', 'nsv', 'nuv', 'ogm', 'psp', 'qt', 'rv', 'smk', 'svcd', 'vcd', 'vob', 'wmv'
    return unless song.vidToWav
    if opts[:outtype] != 'wav'
      exit_s = song.send "wavTo#{opts[:outtype].capitalize}"
      song.deleteWav
      return if exit_s == false
    end

  else
    puts "Shouldn't be here!!!"
    puts "Self-destructing..."
    exit 666
  end

  print "Wrote: "
  info("#{song.outputFilename}")
  File.utime(song.mtime,song.mtime,"#{song.outputFilename}") if opts[:stasis]

  if opts[:delete]
    File.delete(file) unless opts[:pretend]
    print "Deleted: "
    info(file)
  end

  puts
end

#
#  Glob all infile type files and feed them to dispatch_file.
#
def dispatch_directory(directory, opts)
  files = []
  pwd = Dir.getwd
  Dir.chdir(directory)
  opts[:intype].each do |t|
    if opts[:recursive]
      files += Dir.glob("**/*.#{t}")            #  ie: foo.mp3 ... foo.m4a ... foo.ogg
      files += Dir.glob("**/*.#{t.upcase}")     #  ie: foo.MP3 ... foo.M4A ... foo.OGG
      files += Dir.glob("**/*.#{t.capitalize}") #  ie: foo.Mp3 ... foo.M4a ... foo.Ogg
                                                #  If you have foo.mP3 fix your damn filenames!
      #
      #  Create subdirectories if '-D' is used.
      #
      if opts[:outdir] != ""
        files.each do |f|
          dir_array = f.split("/")[0...-1]
          unless dir_array == []
            unless opts[:pretend]
              begin
                dir = "#{opts[:outdir] + dir_array.join("/")}"
                unless File.exist?(dir) == true
                  require 'fileutils'
                  FileUtils.mkdirs("#{dir}")
                  info("Created directory #{dir}") if opts[:verbose]
                end
              rescue
                error("Could not create directory: '#{opts[:outdir]}/#{dir_array.join("/")}'")
                exit 25
              end
            end
          end
        end
      end
    else
      files += Dir.glob("*.#{t}")
      files += Dir.glob("*.#{t.upcase}")
      files += Dir.glob("*.#{t.capitalize}")
    end
  end

  if files.length == 0
    caution("No #{opts[:intype].join(" or ")} files to convert in #{directory}")
    exit 9 if opts[:strict]
  end

  files.sort!

  if not opts[:threads]
    files.each { |file| dispatch_file(file, opts) }
  else
    while files != []
      threads = []
      puts "Starting #{opts[:threads]} threads..."
      while threads.size < opts[:threads]
        threads << Thread.new(files.pop) do |file|
          dispatch_file(file, opts)
        end
      end
    threads.each { |thr| thr.join }
    end
  end
  Dir.chdir(pwd)
end

#
#  TODO: allow for default config files.
#  Made difficult due to limitations of GetoptLong.
#
# class ParseConfig
#   #
#   #  Parse config files for default options.
#   #
#   attr_reader :myopts
#   def initialize(myopts)
#     @myopts = myopts
#     files = ['/etc/sneetchrc', "#{ENV['HOME']}/.sneetchrc"]
#   end
# 
#   #private
#   #def  
# end


class ProcessCLI
  #
  #  This is all just a wrapper to encapsulate CLI processing
  #  and populate the option struct.
  #
  attr_reader :myopts

  def initialize
    puts "#{APPNAME} version #{APPVERSION} (#{QUIP}). Use at your own risk"
    puts "Written by Darren Kirby :: d@badcomputer.org :: http://badcomputer.org/unix/code/sneetchalizer/"
    puts "Released under the GPL"
    puts

    optstruct = Struct.new(:verbose, :showoutput, :delete, :recursive, :outdir, :rename, :inOptionHook, :outOptionHook, :outtype, :intype,
                          :brate, :qrate, :strict, :pedantic, :compression, :sanitize, :pretend, :gogo, :bladeenc, :threads, :stasis, :tags_nc, :tags_c)

    #
    #  Assign default values to our option struct.
    #
    @myopts = optstruct.new(nil, nil, nil, nil, "", nil, "", "", "wav", ["wav"], nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, {}, {})

    require 'getoptlong'
    cliopts = GetoptLong.new(
        [ "--verbose",         "-v",  GetoptLong::NO_ARGUMENT ],
        [ "--delete",          "-d",  GetoptLong::NO_ARGUMENT ],
        [ "--show-output",     "-s",  GetoptLong::NO_ARGUMENT ],
        [ "--pretend",         "-p",  GetoptLong::NO_ARGUMENT ],
        [ "--out",             "-i",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--in",              "-o",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--h",               "-h",  GetoptLong::NO_ARGUMENT],
        [ "--help",                   GetoptLong::NO_ARGUMENT],
        [ "--in-optionhook",          GetoptLong::REQUIRED_ARGUMENT ],
        [ "--out-optionhook",         GetoptLong::REQUIRED_ARGUMENT ],
        [ "--terminate",       "-t",  GetoptLong::NO_ARGUMENT],
        [ "--out-directory",   "-D",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--rename",          "-n",  GetoptLong::OPTIONAL_ARGUMENT ],
        [ "--bitrate",         "-b",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--quality",         "-q",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--compression",     "-c",  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--strict",          "-S",  GetoptLong::NO_ARGUMENT],
        [ "--pedantic",        "-P",  GetoptLong::NO_ARGUMENT],
        [ "--recursive",       "-r",  GetoptLong::NO_ARGUMENT],
        [ "--sanitize",        "-z",  GetoptLong::NO_ARGUMENT],
        [ "--bladeenc",        "-B",  GetoptLong::NO_ARGUMENT],
        [ "--gogo",            "-G",  GetoptLong::NO_ARGUMENT],
        [ "--threads",         "-T",  GetoptLong::OPTIONAL_ARGUMENT ],
        [ "--stasis",                 GetoptLong::NO_ARGUMENT],
        # Tag opts
        [ "--tt",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--ta",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--tl",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--ty",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--tc",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--tn",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--tg",                     GetoptLong::REQUIRED_ARGUMENT ],
        [ "--title",                  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--artist",                 GetoptLong::REQUIRED_ARGUMENT ],
        [ "--album",                  GetoptLong::REQUIRED_ARGUMENT ],
        [ "--year",                   GetoptLong::REQUIRED_ARGUMENT ],
        [ "--comment",                GetoptLong::REQUIRED_ARGUMENT ],
        [ "--trackn",                 GetoptLong::REQUIRED_ARGUMENT ],
        [ "--genre",                  GetoptLong::REQUIRED_ARGUMENT ]
    )

    #
    #  Supress error messages, as we'll handle manually.
    #
    cliopts.quiet = TRUE

    begin
      cliopts.each do |opt, arg|

        cliopts.terminate             if ( opt == '-t' )      || ( opt == '--terminate' )
        short_help                    if ( opt == '-h' )      || ( opt == '--h' ) 
        long_help                     if ( opt == '--help' )
        @myopts[:verbose]       = 1   if ( opt == '-v' )      || ( opt == '--verbose' )
        @myopts[:showoutput]    = 1   if ( opt == '-s' )      || ( opt == '--show-output' )
        @myopts[:delete]        = 1   if ( opt == '-d' )      || ( opt == '--delete' )
        @myopts[:stasis]        = 1   if ( opt == '--stasis' )

        @myopts[:recursive]     = 1   if ( opt == '-r' )      || ( opt == '--recursive' )
        @myopts[:outdir]        = arg if ( opt == '-D' )      || ( opt == '--out-directory' )
        @myopts[:rename]        = arg if (( opt == '-n' )     || ( opt == '--rename' )) && arg != ''
        @myopts[:rename]        = "%n %t" if (( opt == '-n' ) || ( opt == '--rename' )) && arg == ''

        @myopts[:outtype]       = arg            if ( opt == '--out' )
        @myopts[:intype]        = arg.split(",") if ( opt == '--in' )

        @myopts[:brate]         = arg.to_i if ( opt == '-b' ) || ( opt == '--bitrate' )
        @myopts[:qrate]         = arg      if ( opt == '-q' ) || ( opt == '--quality' )
        @myopts[:strict]        = 1        if ( opt == '-S' ) || ( opt == '--strict' )
        @myopts[:pedantic]      = 1 && myopts[:strict] = 1 if ( opt == '-P' ) || ( opt == '--pedantic' )

        @myopts[:compression]   = arg if ( opt == '-c' ) || ( opt == '--compression' )
        @myopts[:pretend]       = 1   if ( opt == '-p' ) || ( opt == '--pretend' )
        @myopts[:sanitize]      = 1   if ( opt == '-z' ) || ( opt == '--sanitize' )
        @myopts[:threads]       = arg if ( opt == '-T' ) || ( opt == '--threads' )

        @myopts[:gogo]          = 1   if ( opt == '-G' ) || ( opt == '--gogo' )
        @myopts[:bladeenc]      = 1   if ( opt == '-B' ) || ( opt == '--bladeenc' )

        @myopts[:inOptionHook]  = arg if ( opt == '--in-optionhook' )
        @myopts[:outOptionHook] = arg if ( opt == '--out-optionhook' )

        #  No-clobber
        @myopts[:tags_nc]['title']    = arg if ( opt == '--tt' )
        @myopts[:tags_nc]['artist']   = arg if ( opt == '--ta' )
        @myopts[:tags_nc]['album']    = arg if ( opt == '--tl' )
        @myopts[:tags_nc]['year']     = arg if ( opt == '--ty' )
        @myopts[:tags_nc]['comment']  = arg if ( opt == '--tc' )
        @myopts[:tags_nc]['genre']    = arg if ( opt == '--tg' )
        @myopts[:tags_nc]['tracknum'] = arg if ( opt == '--tn' )
        #  Clobber
        @myopts[:tags_c]['title']    = arg if ( opt == '--title' )
        @myopts[:tags_c]['artist']   = arg if ( opt == '--artist' )
        @myopts[:tags_c]['album']    = arg if ( opt == '--album' )
        @myopts[:tags_c]['year']     = arg if ( opt == '--year' )
        @myopts[:tags_c]['comment']  = arg if ( opt == '--comment' )
        @myopts[:tags_c]['genre']    = arg if ( opt == '--genre' )
        @myopts[:tags_c]['tracknum'] = arg if ( opt == '--trackn' )

      end #  end cliopts.each...

    #
    #  Handle CLI errors.
    #
    rescue GetoptLong::InvalidOption
      caution("#{cliopts.error_message}")
      caution("Hint: try '#{APPNAME} --help' for usage details")
      puts
      exit 1

    rescue GetoptLong::NeedlessArgument
      caution("#{cliopts.error_message}")
      caution("Hint: try '#{APPNAME} --help' for usage details")
      puts
      exit 2

    rescue GetoptLong::MissingArgument
      caution("#{cliopts.error_message}")
      caution("Hint: try '#{APPNAME} --help' for usage details")
      puts
      exit 3
    end

    if not OUTOK.include?(@myopts[:outtype])
      error("I don't know how to encode '.#{@myopts[:outtype]}' format")
      exit 11
    end

    #
    #  Normalize pathname.
    #
    if not @myopts[:outdir] == ""
      begin
        @myopts[:outdir] = File.expand_path(@myopts[:outdir])
        type = File.stat(@myopts[:outdir]).ftype
      rescue Errno::ENOENT
        caution("Output directory: '#{@myopts[:outdir]}' does not exist")
        exit 7 if @myopts['strict']
        print "I will attempt to create it... "
        begin
          Dir.mkdir(@myopts[:outdir]) unless @myopts[:pretend]
        rescue SystemCallError
          error("sorry, no dice")
          exit 7
        end
        puts "OK, done"
        puts
        type = File.stat(@myopts[:outdir]).ftype unless @myopts[:pretend]
      end

      unless @myopts[:pretend]
        if type != 'directory'
          error("Output directory: '#{@myopts[:outdir]}' is not a directory")
          exit 8
        end
      end

      if @myopts[:outdir][-1].chr != "/"
        @myopts[:outdir] += "/"
      end
    end

    #
    #  Threading creates jumbled/random screen output
    #  so we turn off 'verbose' and 'showoutput' flags.
    #
    if @myopts[:threads]
      @myopts[:verbose]    = nil
      @myopts[:showoutput] = nil
      begin # 2 threads by default
        @myopts[:threads] == "" ? @myopts[:threads] = 2 : @myopts[:threads] = @myopts[:threads].to_i
        raise StandardError if @myopts[:threads] <= 0 # "three".to_i returns 0
      rescue
        error("Arg to '--threads' must be an Integer")
        exit 41
      end
    end

    #
    #  Check for bitrate arg.
    #  Valid lame bitrates: 32 40 48 56 64 80 96 112 128 160 192 224 256 320.
    #  lame/oggenc will select a valid bitrate closest to the actual arg.
    #  Valid oggenc bitrate range: 64 - 500!
    #
    if @myopts[:brate]

      if @myopts[:outtype] == "ogg"
        if not (64..500).include? @myopts[:brate]
          error("'-b' or '--bitrate' must be between 64 and 500 inclusive for Ogg Vorbis (OGG) files'")
          exit 29
        end
      end

      if @myopts[:outtype] == "mp3"
        if not (32..320).include? @myopts[:brate]
          error("'-b' or '--bitrate' must be between 32 and 320 inclusive for MPEG-1 Layer III (MP3) files")
          exit 29
        end
      end

      #
      #  'faac' bitrate will accept any integer for bitrate, but round up to 60 kbps
      #  and round down to 152 kbps.
      #
      if %w/aac mp4 m4a m4b/.include? @myopts[:outtype]

        if (60..152).include? @myopts[:brate]
          # no-op
        elsif @myopts[:brate] > 152
          caution("faac's maximum bitrate is 152 kbps")
          caution("faac will scale '#{@myopts[:brate]}' down to '152'")
        elsif @myopts[:brate] < 60
          caution("faac's minimum bitrate is 60 kbps")
          caution("faac will scale '#{@myopts[:brate]}' up to '60'")
        else
          error("Invalid bitrate argument. See 'faac --help'")
          exit 29
        end
      end
    end

    #
    #  Check for quality arg.
    #
    if @myopts[:qrate]
      #
      #  'oggenc' quality ranges from -1 (poorest/fastest) to 10 (best/slowest). Default is 3.
      #
      if @myopts[:outtype] == "ogg"

        if @myopts[:qrate].match(".")
          @myopts[:qrate] = @myopts[:qrate].to_f
        else
          @myopts[:qrate] = @myopts[:qrate].to_i
        end

        if not (-1..10).include? @myopts[:qrate]
          error("'-q' or '--quality' must be -1 to 10 inclusive ... see 'oggenc --help'")
          exit 30
        end
      end

      #
      #  'lame' quality ranges from 0 (best/slowest) to 9 (poorest/fastest). Default is 5.
      #
      if @myopts[:outtype] == "mp3"
        if not (0..9).include? @myopts[:qrate].to_i
          error("'-q' or '--quality' must be 0 to 9 inclusive ... see 'lame --help'")
          exit 30
        end
      end

      #
      #  'mppenc' quality args are 'thumb', 'radio', 'standard' (default), and 'xtreme'.
      #
      if %w/mpc mpp/.include? @myopts[:outtype]
        if not %w/thumb radio standard xtreme/.include? @myopts[:qrate]
          error("'-q' or '--quality' must be one of 'thumb', 'radio', 'standard', or 'xtreme' ... see 'mppenc --help'")
          exit 30
        end
      end
    end

    #
    #  Check for flac compression arg.
    #
    if @myopts[:outtype] == "flac"

      if @myopts[:compression] == nil
        @myopts[:compression] = 8
      end

      if not (0..8).include? @myopts[:compression].to_i
        error("'-c' or '--compression' must be 0-8 inclusive ... see 'flac --help'")
        exit 31
      end
    end

    #
    #  Check for ape compression arg.
    #
    if @myopts[:outtype] == "ape"

      if @myopts[:compression] == nil
        @myopts[:compression] = "2000"
      end

      if not [1000, 2000, 3000, 4000, 5000].include? @myopts[:compression].to_i
        error("'-c' or '--compression' must be '1000', '2000', '3000', '4000', or '5000' ... see 'mac --help'")
        exit 31
      end
    end

    #
    #  Check for arguments.
    #
    if ARGV.size == 0
      error("You must specify a file and/or directory to act upon")
      puts
      exit 4
    end
  end
end


if __FILE__ == $0
  files = []
  directories = []
  opts = ProcessCLI.new.myopts
  ARGV.each do |file|

    begin
      type = File.stat(file).ftype

      if type == 'directory'
        directories << file
      elsif type == 'file'
        files << file
      else
        caution("I don't think I should mess with '#{file}'")
        exit 5 if opts[:strict]
        caution("I am just going to ignore it")
      end

    rescue Errno::ENOENT
      caution("'#{file}' does not seem to exist")
      exit 6 if opts[:strict]
    end
  end

  files.sort! #  Convert files in alphabetical (asciibetical!) order.

  if not opts[:threads]
    files.each { |file| dispatch_file(file, opts) }
    directories.each { |dir| dispatch_directory(dir, opts) }
  else
    while files != []
      threads = []
      puts "Starting #{opts[:threads]} threads..."
      while threads.size < opts[:threads]
        threads << Thread.new(files.pop) do |file|
          dispatch_file(file, opts)
        end
      end
    threads.each { |thr| thr.join }
    end
    directories.each { |dir| dispatch_directory(dir, opts) }
  end

  puts "That is all."
  exit 0
end

__END__

0.9.0

Exit Codes:

0  ==> Success!
1  ==> Invalid option
2  ==> Needless option
3  ==> Missing argument
4  ==> Missing file/directory input argument
5  ==> Input argument not a file or directory
6  ==> Input file/directory does not exist
7  ==> --out-directory/-D argument does not exist
8  ==> --out-directory/-D argument is not a directory
9  ==> Input directory argument contains no files to convert (--strict)
10 ==> Unknown conversion failure (--strict)
11 ==> Unrecognized output token
12 ==> Unrecognized input filetype
13 ==> Ogg Vorbis tag error (--pedantic)
14 ==> AAC/M4A/M4B/MP4 tag error  (--pedantic)
15 ==> WmaInfo LoadError  (--pedantic)
16 ==> WMA tag error  (--pedantic)
17 ==> id3 LoadError  (--pedantic)
18 ==> MP3 tag error  (--pedantic)
19 ==> FlacInfo LoadError  (--pedantic)
20 ==> Flac tag error  (--pedantic)
21 ==> apetag LoadError  (--pedantic)
22 ==> Ape tag error  (--pedantic)
23 ==> OggInfo LoadError (--pedantic)
24 ==> MP4Info LoadError (--pedantic)
25 ==> Unable to create directory
26 ==> Ogg Vorbis encoder not found
27 ==> Mp3 encoder not found
28 ==> Flac encoder not found
29 ==> lame/oggenc/faac --bitrate arg out of range or invalid
30 ==> lame/oggenc/mppenc --quality arg out of range or invalid
31 ==> flac --compression arg out of range or invalid
32 ==> Conversion command not found
33 ==> Mp4 encoder not found
34 ==> Monkey's Audio encoder not found
35 ==> Wavpack encoder not found
36 ==> Musepack encoder not found
37 ==> True Audio encoder not found
38 ==> sox not found (check the dryer!))
39 ==> OptimFROG encoder not found
40 ==> Speex encoder not found
41 ==> Bad argument to '--threads'
42 ==> Wma encoder not found
43 ==> Mplayer not found

666 ==> If you get this, something seriously screwed up.
        Please send me a bug report...