diff options
| -rwxr-xr-x | omptagger | 708 | 
1 files changed, 318 insertions, 390 deletions
@@ -1,6 +1,6 @@  #!/usr/bin/env ruby  # -# omptagger [version 0.8.1] +# omptagger [version 1.0]  # http://dev.gentoo.org/~omp/omptagger/  #  # Copyright 2007 David Shakaryan <omp@gentoo.org> @@ -12,113 +12,132 @@  # on the format of the file being worked with.  #  # Dependencies: -# * ruby [ http://www.ruby-lang.org/ ] -# * flac [ http://flac.sourceforge.net/ ] -# * id3lib-ruby [ http://id3lib-ruby.rubyforge.org/ ] -# * vorbis-tools [ http://www.vorbis.com/ ] +# * ruby           http://www.ruby-lang.org/ +# * flac           http://flac.sourceforge.net/ +# * id3lib-ruby    http://id3lib-ruby.rubyforge.org/ +# * vorbis-tools   http://www.vorbis.com/  require 'getoptlong'  require 'id3lib' -# List of valid options. -getopt = GetoptLong.new( -  ['--view',       '-v', GetoptLong::NO_ARGUMENT], -  ['--view-tag',   '-t', GetoptLong::REQUIRED_ARGUMENT], -  ['--set-tag',    '-s', GetoptLong::REQUIRED_ARGUMENT], -  ['--generate',   '-g', GetoptLong::NO_ARGUMENT], -  ['--preview',    '-p', GetoptLong::NO_ARGUMENT], -  ['--remove',     '-r', GetoptLong::NO_ARGUMENT], -  ['--remove-tag', '-d', GetoptLong::REQUIRED_ARGUMENT], -  ['--rename',     '-m', GetoptLong::NO_ARGUMENT], -  ['--scheme',     '-n', GetoptLong::REQUIRED_ARGUMENT], -  ['--no-colour',  '-c', GetoptLong::NO_ARGUMENT], -  ['--help',       '-h', GetoptLong::NO_ARGUMENT] -) - -# Store any options set in a hash. Create a tracker variable to determine -# whether an action has been set. -action = false -$opts = Hash['scheme' => '%a - %t'] -getopt.each do |opt, arg| -  opt.sub!('--', '') - -  case opt -  # Options which may be used only once. -  when 'view', 'generate', 'preview', 'remove', 'rename', 'help' -    $opts[opt] = arg -    action = true -  when 'no-colour', 'scheme' -    $opts[opt] = arg - -  # Options which may be used more than once. -  when 'view-tag', 'set-tag', 'remove-tag' -    if $opts.has_key?(opt) -      $opts[opt].push(arg) -    else -      $opts[opt] = [arg] -    end -    action = true -  end -end - -# Define colours unless the no-colour option is set. -if $opts.has_key?('no-colour') -  def colred(str); return str; end -  def colgrn(str); return str; end -  def colyel(str); return str; end -  def colcyn(str); return str; end -else -  def colred(str); return "\e[31m#{str}\e[0m"; end -  def colgrn(str); return "\e[32m#{str}\e[0m"; end -  def colyel(str); return "\e[33m#{str}\e[0m"; end -  def colcyn(str); return "\e[36m#{str}\e[0m"; end -end +# Define colours. +def colred(str) return "\e[31m#{str}\e[0m" end +def colgrn(str) return "\e[32m#{str}\e[0m" end +def colyel(str) return "\e[33m#{str}\e[0m" end +def colcyn(str) return "\e[36m#{str}\e[0m" end -# Methods for outputting warning and status messages. -def warn(message) -  puts '  ' + colred('ERROR:') + ' ' + message -end -def status(message) -  puts '  ' + colgrn(message) -end - -# Method for escaping single quotes for use within single quotes in shell -# commands. Substitutes all instances of ' with '\''. +# Escape single quotes for use within single quotes in shell commands.  def esc(str) +  # Substitute all instances of ' with '\''.    return str.gsub("'", "'\\\\''")  end -# Method for dynamic spacing; used to correctly align tag and value output. -def spacing(tags, tag) -  return ' ' * (tags.map do |str| str.size end.max + 2 - tag.length) +# Output a tag name and value. +def output(tag, val) +  # Alter capitalisation of tag name for aesthetic purposes. +  tag = tag.downcase.capitalize unless tag == 'ISRC' +  tag = 'TrackNumber' if tag == 'Tracknumber' + +  # The number statically defined here should be two greater than the length of +  # the longest possible tag name. +  puts '    ' + colcyn(tag) + ' ' * (14 - tag.length) + val  end -# Method for splitting filename based on a naming scheme. -def namingscheme(scheme, name) -  regexp = /\A#{Regexp.escape(scheme).gsub(/%[a-z]/, '(.+?)')}\Z/ -  tags = scheme.scan(regexp).flatten -  values = name.scan(regexp).flatten +# Split a filename based on a naming scheme. An asterisk is a wildcard key. +def schemesplit(scheme, file, keystr) +  # Determine keys and values using a regular expression. +  regexp = /\A#{Regexp.escape(scheme).gsub(/%([#{keystr}]|\\\*)/, '(.*?)')}\Z/ +  keys = scheme.scan(regexp).flatten +  vals = file.scan(regexp).flatten -  unless tags.length == values.length -    raise 'Filename does not match naming scheme.' -  end +  raise 'Filename does not match naming scheme.' if keys.length != vals.length +  # Map scheme keys to values.    result = Hash.new -  tags.zip(values).each do |tag, value| -    result[tag.delete('%')] = value +  keys.zip(vals).each do |key, val| +    result[key[1,1]] = val unless key == '%*'    end    return result  end +# Display help information. +def help +  options = [['--view        -v', 'View all tags'], +             ['--view-tag    -t', 'View a tag'], +             ['--set-tag     -s', 'Set a tag'], +             ['--remove      -r', 'Remove all tags'], +             ['--remove-tag  -d', 'Remove a tag'], +             ['--generate    -g', 'Generate tags based on filename'], +             ['--rename      -m', 'Generate filename based on tags'], +             ['--scheme      -n', 'Specify a file naming scheme'], +             ['--pretend     -p', 'Do not make any actual changes'], +             ['--no-colour   -c', 'Disable use of colour in output'], +             ['--list        -l', 'Display list of available tags'], +             ['--help        -h', 'Display help information'], +             ['--rename      -m', 'Generate filename based on tags']] +  notes = ['The default file naming scheme is Artist - Title.', +           'Schemes must be specified prior to actions that use them.', +           'Underscores in filenames are converted to spaces in tags.'] + +  puts colyel('Usage:') + ' omptagger ' + colgrn('[options] [files]') +  puts +  puts colyel('Options:') +  options.each do |option, description| +    puts '  ' + colgrn(option) + '  ' + description +  end +  puts +  puts colyel('Notes:') +  notes.each do |note| +    puts '  ' + colgrn('*') + ' ' + note +  end +  exit +end + +# Display list of available tags. +def list +  puts '┌───────────────────────────────────┐', +       '│ Keys   Vorbis Comments   ID3 Tags │', +       '├───────────────────────────────────┤', +       '│ %a     Artist            Artist   │', +       '│ %b     Album             Album    │', +       '│ %d     Date              Year     │', +       '│ %n     TrackNumber       Track    │', +       '│ %t     Title             Title    │', +       '│ %*     Wildcard          Wildcard │', +       '├───────────────────────────────────┤', +       '│        Contact                    │', +       '│        Copyright                  │', +       '│        Description                │', +       '│        Genre                      │', +       '│        ISRC                       │', +       '│        License                    │', +       '│        Location                   │', +       '│        Organization               │', +       '│        Performer                  │', +       '│        Version                    │', +       '└───────────────────────────────────┘' +  exit +end +  # Class for Vorbis comments; used by FLAC and Vorbis files.  class VorbisComments -  attr_reader :hash, :file -    # Possible tags. -  TAGS = ['TITLE', 'VERSION', 'ALBUM', 'TRACKNUMBER', 'ARTIST', 'PERFORMER', -          'COPYRIGHT', 'LICENSE', 'ORGANIZATION', 'DESCRIPTION', 'GENRE', -          'DATE', 'LOCATION', 'CONTACT', 'ISRC'] +  TAGS = ['ALBUM', +          'ARTIST', +          'CONTACT', +          'COPYRIGHT', +          'DATE', +          'DESCRIPTION', +          'GENRE', +          'ISRC', +          'LICENSE', +          'LOCATION', +          'ORGANIZATION', +          'PERFORMER', +          'TITLE', +          'TRACKNUMBER', +          'VERSION']    # Map scheme keys to tags.    KEYS = Hash['a' => 'ARTIST', @@ -127,206 +146,160 @@ class VorbisComments                'n' => 'TRACKNUMBER',                't' => 'TITLE'] -  # Method for outputting a tag and its corresponding value. -  def output(tag, value) -    puts '  ' + colcyn(tag) + spacing(TAGS, tag) + value -  end - -  # Method for initialising a new object.    def initialize(file) -    @file = file +    @file = @origfile = file      @tags = Hash.new +    @write = false      TAGS.each do |tag| -      value = read_tag(tag) -      next if value.empty? - -      @tags[tag] = value.split("\n").map do |str| str.sub(tag + '=', '') end -    end -  end - -  # Method for writing tags. -  def write_tags -    clear_tags +      val = read_tag(tag) +      next if val.empty? -    @tags.each do |tag, value| -      value.each do |value| -        write_tag(tag, value) -      end +      @tags[tag] = val.gsub(/^#{tag}=/i, '').split("\n")      end    end -  # Method for displaying all tags.    def view      raise 'No tags are set.' if @tags.empty? -    @tags.each do |tag, value| -      value.each do |value| -        output(tag, value) +    @tags.each do |tag, val| +      val.each do |val| +        output(tag, val)        end      end    end -  # Method for displaying certain tags.    def view_tag(arg) -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        tag = arg.upcase -        raise tag + ' is not a valid tag.' unless TAGS.include?(tag) +    tag = arg.upcase +    raise tag + ' is not a valid tag.' unless TAGS.include?(tag) -        value = @tags[tag] -        raise tag + ' tag is not set.' if value.nil? +    val = @tags[tag] +    raise tag + ' tag is not set.' if val.nil? -        value.each do |value| -          output(tag, value) -        end -      rescue RuntimeError => message -        warn(message) -      end +    val.each do |val| +      output(tag, val)      end    end -  # Method for setting tags.    def set_tag(arg) -    status('Setting tags...') +    arg = arg.split('=', 2) +    tag = arg.shift.upcase +    raise tag + ' is not a valid tag.' unless TAGS.include?(tag) -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        arg = arg.split('=', 2) -        tag = arg.first.upcase -        raise tag + ' is not a valid tag.' unless TAGS.include?(tag) - -        if arg.length == 1 -          value = '' -        else -          value = arg.last -        end +    @tags[tag] = arg +    view_tag(tag) -        @tags[tag] = [value] -        view_tag(tag) -      rescue RuntimeError => message -        warn(message) -      end -    end +    @write = true    end -  # Method for generating new tags based on filename. -  def generate -    value = File.basename(@file).gsub('_', ' ').sub(/\.(flac|ogg)$/, '') -    scheme = namingscheme($opts['scheme'].gsub('_', ' '), value) - -    tags = Array.new -    values = Array.new - -    scheme.each do |tag, value| -      raise 'Naming scheme contains an invalid key.' unless KEYS.has_key?(tag) - -      tags.push(KEYS[tag]) -      values.push(value) -    end - -    status('Generating tags...') - -    tags.zip(values).each do |arr| -      @tags[arr.first] = [arr.last] -      view_tag(arr.first) -    end -  end - -  # Method for removing all tags.    def remove      raise 'No tags are set.' if @tags.empty? -    status('Removing tags...')      @tags.clear +    @write = true    end -  # Method for removing certain tags.    def remove_tag(arg) -    status('Removing tags...') +    tag = arg.upcase +    raise tag + ' is not a valid tag.' unless TAGS.include?(tag) +    raise tag + ' tag is not set.' unless @tags.include?(tag) -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        tag = arg.upcase -        raise tag + ' is not a valid tag.' unless TAGS.include?(tag) -        raise tag + ' tag is not set.' unless @tags.include?(tag) +    @tags.delete(tag) +    @write = true +  end -        @tags.delete(tag) -      rescue RuntimeError => message -        warn(message) -      end +  def generate(scheme) +    val = File.basename(@file, File.extname(@file)).gsub('_', ' ') +    scheme = schemesplit(scheme.gsub('_', ' '), val, KEYS.keys.join) + +    tagval = Array.new + +    scheme.each do |tag, val| +      tagval << [KEYS[tag], val]      end + +    tagval.each do |arr| +      @tags[arr.first] = [arr.last] +      view_tag(arr.first) +    end + +    @write = true    end -  # Method for renaming files based on tags. -  def rename -    file = $opts['scheme'] +  def rename(scheme) +    file = scheme -    file.scan(/%([a-z])/).flatten.each do |key| -      raise 'Naming scheme contains an invalid key.' unless KEYS.has_key?(key) +    file.scan(/%([#{KEYS.keys.join}])/).flatten.each do |key|        raise 'Missing ' + KEYS[key] + ' tag.' unless @tags[KEYS[key]] -        file = file.gsub('%' + key, @tags[KEYS[key]].first)      end      file = File.dirname(@file) + '/' + file.gsub('/', '_') + -      @file.sub(/.*\./, '.') +      File.extname(@file) + +    raise 'Generated filename and current filename are identical.' if +      File.basename(@file) == File.basename(file) + +    output('FILENAME', file.sub(/^\.\//, '')) +    raise 'File with generated name already exists.' if File.exist?(file) + +    @file = file +  end + +  def finalise +    if @write +      clear_tags -    raise 'Generated filename and current filename are identical.' \ -      if File.basename(@file) == File.basename(file) +      @tags.each do |tag, val| +        val.each do |val| +          write_tag(tag, val) +        end +      end +    end -    status('Renaming to ' + file.sub(/^\.\//, '') + '...') -    File.rename(@file, file) +    File.rename(@origfile, @file) unless @file == @origfile    end  end  # Subclass of VorbisComments containing methods unique to FLAC files.  class FLAC < VorbisComments    def read_tag(tag) -    %x(metaflac --show-tag=#{tag} -- '#{esc(@file)}').chomp +    %x(metaflac --show-tag=#{tag} -- '#{esc(@origfile)}').chomp    end    def clear_tags -    %x{metaflac --remove-all-tags -- '#{esc(@file)}'} +    %x{metaflac --remove-all-tags -- '#{esc(@origfile)}'}    end -  def write_tag(tag, value) -    %x{metaflac --set-tag=#{tag}='#{esc(value)}' -- '#{esc(@file)}'} +  def write_tag(tag, val) +    %x{metaflac --set-tag=#{tag}='#{esc(val)}' -- '#{esc(@origfile)}'}    end  end  # Subclass of VorbisComments containing methods unique to Vorbis files.  class Vorbis < VorbisComments    def read_tag(tag) -    %x(vorbiscomment -l -- '#{esc(@file)}' | grep '#{tag}').chomp +    %x(vorbiscomment -l -- '#{esc(@origfile)}' | grep '^#{tag}=').chomp    end    def clear_tags -    %x{vorbiscomment -w -t '' -- '#{esc(@file)}' 2>/dev/null} +    %x{vorbiscomment -w -t '' -- '#{esc(@origfile)}' 2>/dev/null}    end -  def write_tag(tag, value) -    %x{vorbiscomment -a -t #{tag}='#{esc(value)}' -- '#{esc(@file)}'} +  def write_tag(tag, val) +    %x{vorbiscomment -a -t #{tag}='#{esc(val)}' -- '#{esc(@origfile)}'}    end  end  # Class for ID3 tags; used by MP3 files.  class ID3 -  attr_reader :file, :tags -    # Possible tags. -  TAGS = Hash['TITLE'   => :TIT2, -              'ALBUM'   => :TALB, -              'TRACK'   => :TRCK, +  TAGS = Hash['ALBUM'   => :TALB,                'ARTIST'  => :TPE1, -              'YEAR'    => :TYER, -              'COMMENT' => :COMM] +              'COMMENT' => :COMM, +              'TITLE'   => :TIT2, +              'TRACK'   => :TRCK, +              'YEAR'    => :TYER]    # Map scheme keys to tags.    KEYS = Hash['a' => 'ARTIST', @@ -335,252 +308,207 @@ class ID3                'n' => 'TRACK',                't' => 'TITLE'] -  # Method for outputting a tag and its corresponding value. -  def output(tag, value) -    puts '  ' + colcyn(tag) + spacing(TAGS.keys, tag) + value -  end - -  # Method for initialising a new object.    def initialize(file) -    @file = file +    @file = @origfile = file      @tags = ID3Lib::Tag.new(@file) +    @write = false    end -  # Method for writing tags. -  def write_tags -    if @tags.empty? or (@tags.length == 1 and @tags.frame_text(:TLEN)) -      @tags.strip! -    else -      @tags.update! -    end -  end - -  # Method for displaying all tags.    def view      raise 'No tags are set.' if @tags.empty?      TAGS.each do |tag, frame| -      value = @tags.frame_text(frame) -      output(tag, value) unless value.nil? +      val = @tags.frame_text(frame) +      output(tag, val) unless val.nil?      end    end -  # Method for displaying certain tags.    def view_tag(arg) -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        tag = arg.upcase -        raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) +    tag = arg.upcase +    raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) -        value = @tags.frame_text(TAGS[tag]) -        raise tag + ' tag is not set.' if value.nil? +    val = @tags.frame_text(TAGS[tag]) +    raise tag + ' tag is not set.' if val.nil? -        output(tag, value) -      rescue RuntimeError => message -        warn(message) -      end -    end +    output(tag, val)    end -  # Method for setting tags.    def set_tag(arg) -    status('Setting tags...') +    arg = arg.split('=', 2) +    tag = arg.shift.upcase +    raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        arg = arg.split('=', 2) -        tag = arg.first.upcase -        raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) - -        if arg.length == 1 -          value = '' -        else -          value = arg.last -        end +    @tags.set_frame_text(TAGS[tag], arg.to_s) +    view_tag(tag) -        @tags.set_frame_text(TAGS[tag], value) -        view_tag(tag) -      rescue RuntimeError => message -        warn(message) -      end -    end +    @write = true    end -  # Method for generating new tags based on filename. -  def generate -    value = File.basename(@file).gsub('_', ' ').sub(/\.mp3$/, '') -    scheme = namingscheme($opts['scheme'].gsub('_', ' '), value) - -    tags = Array.new -    values = Array.new +  def generate(scheme) +    val = File.basename(@file, File.extname(@file)).gsub('_', ' ') +    scheme = schemesplit(scheme.gsub('_', ' '), val, KEYS.keys.join) -    scheme.each do |tag, value| -      raise 'Naming scheme contains an invalid key.' unless KEYS.has_key?(tag) +    tagval = Array.new -      tags.push(KEYS[tag]) -      values.push(value) +    scheme.each do |tag, val| +      tagval << [KEYS[tag], val]      end -    status('Generating tags...') - -    tags.zip(values).each do |arr| +    tagval.each do |arr|        @tags.set_frame_text(TAGS[arr.first], arr.last)        view_tag(arr.first)      end + +    @write = true    end -  # Method for removing all tags.    def remove      raise 'No tags are set.' if @tags.empty? -    status('Removing tags...')      @tags.clear +    @write = true    end -  # Method for removing certain tags.    def remove_tag(arg) -    status('Removing tags...') +    tag = arg.upcase +    raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) +    raise tag + ' tag is not set.' if @tags.frame_text(TAGS[tag]).nil? -    arg.each do |arg| -      # Process exceptions within the argument loop, as using the exception in -      # the file loop may end the argument loop prematurely. -      begin -        tag = arg.upcase -        raise tag + ' is not a valid tag.' unless TAGS.has_key?(tag) -        raise tag + ' tag is not set.' if @tags.frame_text(TAGS[tag]).nil? - -        @tags.remove_frame(TAGS[tag]) -      rescue RuntimeError => message -        warn(message) -      end -    end +    @tags.remove_frame(TAGS[tag]) +    @write = true    end -  # Method for renaming files based on tags. -  def rename -    file = $opts['scheme'] +  def rename(scheme) +    file = scheme -    file.scan(/%([a-z])/).flatten.each do |key| -      raise 'Naming scheme contains an invalid key.' unless KEYS.has_key?(key) -      raise 'Missing ' + KEYS[key] + ' tag.' \ -        if @tags.frame_text(TAGS[KEYS[key]]).nil? +    file.scan(/%([#{KEYS.keys.join}])/).flatten.each do |key| +      raise 'Missing ' + KEYS[key] + ' tag.' if +        @tags.frame_text(TAGS[KEYS[key]]).nil?        file = file.gsub('%' + key, @tags.frame_text(TAGS[KEYS[key]]))      end      file = File.dirname(@file) + '/' + file.gsub('/', '_') + -      @file.sub(/.*\./, '.') +      File.extname(@file) -    raise 'Generated filename and current filename are identical.' \ -      if File.basename(@file) == File.basename(file) +    raise 'Generated filename and current filename are identical.' if +      File.basename(@file) == File.basename(file) -    status('Renaming to ' + file.sub(/^\.\//, '') + '...') -    File.rename(@file, file) +    output('FILENAME', file.sub(/^\.\//, '')) +    raise 'File with generated name already exists.' if File.exist?(file) + +    @file = file +  end + +  def finalise +    if @write +      if @tags.empty? or (@tags.length == 1 and @tags.frame_text(:TLEN)) +        @tags.strip! +      else +        @tags.update! +      end +    end + +    File.rename(@origfile, @file) unless @file == @origfile    end  end -# Display program help if help action is set. If no actions are set, default to -# either help or view, depending on whether an argument was passed. -if $opts.has_key?('help') or (!action and ARGV.empty?) -  puts colyel('Usage:') + ' omptagger ' + colgrn('[options]') + ' ' + -    colgrn('[files]') -  puts -  puts colyel('Options:') -  puts '  ' + colgrn('--view') + '        ' + colgrn('-v') + -    '  Display all tags' -  puts '  ' + colgrn('--view-tag') + '    ' + colgrn('-t') + -    '  Display a tag ' + colcyn('[required argument: tag]') -  puts '  ' + colgrn('--set-tag') + '     ' + colgrn('-s') + -    '  Set a tag ' + colcyn('[required argument: tag=string]') -  puts '  ' + colgrn('--generate') + '    ' + colgrn('-g') + -    '  Generate tags based on filename' -  puts '  ' + colgrn('--preview') + '     ' + colgrn('-p') + -    '  Preview tags that will be set with generate option' -  puts '  ' + colgrn('--remove') + '      ' + colgrn('-r') + -    '  Remove all tags' -  puts '  ' + colgrn('--remove-tag') + '  ' + colgrn('-d') + -    '  Remove a tag ' + colcyn('[required argument: tag]') -  puts '  ' + colgrn('--rename') + '      ' + colgrn('-m') + -    '  Rename files based on tags' -  puts '  ' + colgrn('--scheme') + '      ' + colgrn('-n') + -    '  Allow a file naming scheme to be specified' -  puts '  ' + colgrn('--no-colour') + '   ' + colgrn('-c') + -    '  Disable use of colour in program output' -  puts '  ' + colgrn('--help') + '        ' + colgrn('-h') + -    '  Display program help' -  puts -  puts colyel('Notes:') -  puts '  ' + colgrn('*') + ' Default file naming scheme is ' + -    colcyn('Artist - Title') + '.' -  puts '  ' + colgrn('*') + ' Underscores in filenames are converted to ' + -    'spaces in tags.' -  puts '  ' + colgrn('*') + ' Scheme keys: ' + colcyn('%a') + ' - ARTIST, ' + -    colcyn('%b') + ' - ALBUM, ' + colcyn('%d') + ' - DATE/YEAR,' -  puts '  ' + '  ' + colcyn('%n') + ' - TRACKNUMBER/TRACK, ' + -    colcyn('%t') + ' - TITLE.' -  exit 0 -elsif !action -  $opts['view'] = '' +# Parse options. +actions = Array.new +options = Array.new +scheme = '%a - %t' +GetoptLong.new( +  ['--view',       '-v', GetoptLong::NO_ARGUMENT], +  ['--view-tag',   '-t', GetoptLong::REQUIRED_ARGUMENT], +  ['--set-tag',    '-s', GetoptLong::REQUIRED_ARGUMENT], +  ['--remove',     '-r', GetoptLong::NO_ARGUMENT], +  ['--remove-tag', '-d', GetoptLong::REQUIRED_ARGUMENT], +  ['--generate',   '-g', GetoptLong::NO_ARGUMENT], +  ['--rename',     '-m', GetoptLong::NO_ARGUMENT], +  ['--scheme',     '-n', GetoptLong::REQUIRED_ARGUMENT], +  ['--pretend',    '-p', GetoptLong::NO_ARGUMENT], +  ['--no-colour',  '-c', GetoptLong::NO_ARGUMENT], +  ['--list',       '-l', GetoptLong::NO_ARGUMENT], +  ['--help',       '-h', GetoptLong::NO_ARGUMENT] +).each do |opt, arg| +  opt = opt.sub(/^--/, '').gsub('-', '_').intern + +  case opt +  when :no_colour +    def colred(str) return str end +    def colgrn(str) return str end +    def colyel(str) return str end +    def colcyn(str) return str end +  when :scheme +    scheme = arg +  when :pretend, :help, :list +    options << opt +  when :view_tag, :set_tag, :remove_tag +    actions << [opt, arg] +  when :generate, :rename +    actions << [opt, scheme] +  else +    actions << [opt] +  end +end + +# Deal with help and list options. Set a default action if one was not +# specified. Display error if an action was specified, but no arguments are +# present. +if options.include?(:help) +  help +elsif options.include?(:list) +  list +elsif actions.empty? and ARGV.empty? +  help +elsif actions.empty? +  actions = [[:view]] +elsif ARGV.empty? +  puts colred('ERROR:') + ' No files specified.'  end -# Treat all remaining arguments as files. -warn('No files specified.') if ARGV.length.zero? +# Treat each argument as a file.  ARGV.each do |file| -  # Process exceptions in order to produce error messages in a custom format -  # without an excessive number of nested if statements. +  # Continue with next file if an error is raised.    begin -    # Output name of file. +    # Output filename and verify that file exists.      puts colyel(file) - -    # Verify that file exists.      raise 'File does not exist.' unless File.exist?(file)      # Determine file format. -    raise 'File has no extension.' if (file =~ /\./).nil? -    case file.sub(/.*\./, '') -    when 'flac' +    case File.extname(file).downcase +    when '.flac'        track = FLAC.new(file) -    when 'ogg' +    when '.ogg'        track = Vorbis.new(file) -    when 'mp3' +    when '.mp3'        track = ID3.new(file)      else -      raise 'Not a supported file format.'  +      raise 'File extension not recognised.'      end -    # Call methods based on the actions set. -    if $opts.has_key?('set-tag') -      track.set_tag($opts['set-tag']) -      track.write_tags -    end - -    if $opts.has_key?('remove') -      track.remove -      track.write_tags -    elsif $opts.has_key?('remove-tag') -      track.remove_tag($opts['remove-tag']) -      track.write_tags -    end - -    if $opts.has_key?('preview') -      track.generate -    elsif $opts.has_key?('generate') -      track.generate -      track.write_tags -    elsif $opts.has_key?('rename') -      track.rename +    # Run specified actions. +    actions.each do |action| +      # Continue with next action if an error is raised. +      begin +        # Output action name and arguments. +        puts '  ' + colgrn('Action: ' + action.join(' ')) + +        # Run action. +        case action.first +        when :view, :remove +          track.send(action.first) +        when :view_tag, :set_tag, :remove_tag, :generate, :rename +          track.send(action.first, action.last) +        end +      rescue RuntimeError => message +        puts '    ' + colred('ERROR:') + ' ' + message +      end      end -    if $opts.has_key?('view') -      track.view -    elsif $opts.has_key?('view-tag') -      track.view_tag($opts['view-tag']) -    end +    # Finalise changes. +    track.finalise unless options.include?(:pretend)    rescue RuntimeError => message -    warn(message) +    puts '  ' + colred('ERROR:') + ' ' + message    end  end  | 
