#!/usr/bin/env ruby # # omptagger # http://github.com/omp/omptagger # Modify and display metadata of audio files. # # Copyright 2007-2010 David Vazgenovich Shakaryan # Distributed under the terms of the GNU General Public License v3. # See http://www.gnu.org/licenses/gpl.txt for the full license text. # # Dependencies: # Ruby http://www.ruby-lang.org/ # RubyGems http://rubygems.org/ # rtaglib http://rubygems.org/gems/rtaglib # ruby-filemagic http://rubygems.org/gems/ruby-filemagic # TagLib http://developer.kde.org/~wheeler/taglib.html require 'getoptlong' require 'rubygems' require 'TagLib' require 'filemagic' module Output @colour = true def self.colourise(string, colour) return string unless @colour case colour when :green code = '32' when :yellow code = '33' when :cyan code = '36' else return string end return "\e[#{code}m#{string}\e[0m" end def self.file(file) puts colourise(file + ':', :yellow) end def self.action(action) puts ' ' + colourise(action + ':', :green) end def self.field(field, value, padding = 0) puts ' ' + colourise(field, :cyan) + ' ' * (2 + padding) + value end def self.info(info) puts ' ' + info end def self.help_information puts <<-end Usage: omptagger [actions/options] [files] Actions: --view -v View all tags --view-tag -t View a tag --add-tag -a Add 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 Rename file based on tags --scheme -n Change file naming scheme Options: --no-colour -c Disable colourisation of output --pretend -p Disable finalisation of changes --help -h Display help information --info -i Display additional information end exit end def self.additional_information puts <<-end Actions: * Multiple actions can be chained together. * Actions are executed in the order they are specified. * Example: omptagger --view --set-tag ALBUM='Foobar' --view Schemes: * The default file naming scheme is Track - Artist - Title. * Schemes must be specified prior to actions that use them. * Example: omptagger --scheme '%a - %t' --generate * %a - Artist %* - Wildcard %b - Album %d - Date %n - Track %t - Title Supported Tags: * Vorbis comments: Any field name allowed by specification. * ID3v2 frames: Artist, album, title, track, year. Tag Generation: * Underscores in filenames are converted to spaces in tags. * Backslashes in filenames are converted to forward slashes in tags. end exit end end class Array def longest_element_length self.inject(0) do |longest, key| key.length > longest ? key.length : longest end end end class Hash def longest_key_length self.keys.inject(0) do |longest, key| key.length > longest ? key.length : longest end end end class Metadata def initialize(file) Output.file(file) @filename = File.expand_path(file) @write = false @file = open(file) @metadata = read end def view Output.action('Viewing all fields') next_if_no_fields_set @metadata.sort.each do |field, value| value.each do |value| Output.field(field, value, @metadata.longest_key_length - field.length) end end end def viewtag(field) field.upcase! Output.action('Viewing ' + field + ' field') next_if_field_not_set(field) @metadata[field].each do |value| Output.field(field, value) end end def addtag(field, value) field.upcase! Output.action('Adding ' + field + ' field') next_if_not_valid_field(field) if @metadata.has_key?(field) @metadata[field] << value else @metadata[field] = value end Output.field(field, value) @write = true end def settag(field, value) field.upcase! Output.action('Setting ' + field + ' field') next_if_not_valid_field(field) @metadata[field] = [value] Output.field(field, value) @write = true end def remove Output.action('Removing all fields') next_if_no_fields_set @metadata.clear Output.info('Removed') @write = true end def removetag(field) field.upcase! Output.action('Removing ' + field + ' field') next_if_field_not_set(field) @metadata.delete(field) Output.info('Removed') @write = true end def generate(scheme) Output.action('Generating fields') regexp = Regexp.escape(scheme) regexp = regexp.gsub(/%([#{keys.keys.join}]|\\\*)/, '([^/]*?)') regexp = /#{regexp}\Z/ fields = scheme.scan(regexp).flatten values = @filename.chomp(File.extname(@filename)).scan(regexp).flatten unless fields.length == values.length Output.info('Filename does not match naming scheme.') throw :next end fields.collect! do |field| keys[field[1,1]] end longest = fields.compact.longest_element_length fields.zip(values).each do |field, value| unless field.nil? Output.field(field, value, longest - field.length) end end end private def next_if_no_fields_set if @metadata.empty? Output.info('No fields set.') throw :next end end def next_if_field_not_set(field) unless @metadata.has_key? field Output.info('Field not set.') throw :next end end def next_if_not_valid_field(field) unless valid_field?(field) Output.info('Invalid field name.') throw :next end end end class VorbisComment < Metadata def keys Hash['a' => 'ARTIST', 'b' => 'ALBUM', 'd' => 'DATE', 'n' => 'TRACKNUMBER', 't' => 'TITLE'] end def save return unless @write read.each_key do |field| metadata.removeField(TagLib::String.new(field)) end @metadata.each do |field, value| field = TagLib::String.new(field) value.each do |value| metadata.addField(field, TagLib::String.new(value), false) end end @file.save end private def read metadata.fieldListMap.hash end def valid_field?(field) valid = (32..125).collect do |character| character.chr end valid.delete('=') field.scan(/./).each do |character| return false unless valid.include?(character) end return true end end class FLAC < VorbisComment private def open(file) TagLib::FLAC::File.new(file) end def metadata @file.xiphComment end end class Vorbis < VorbisComment private def open(file) TagLib::Vorbis::File.new(file) end def metadata @file.tag end end class MP3 < Metadata private def open(file) TagLib::MPEG::File.new(file) end def metadata @file.ID3v2Tag end def read Hash[metadata.frameListMap.hash.collect do |field, value| [field, value.collect do |frame| frame.to_s end] end] end end actions = Array.new options = Array.new scheme = '%n - %a - %t' GetoptLong.new( ['--view', '-v', GetoptLong::NO_ARGUMENT], ['--view-tag', '-t', GetoptLong::REQUIRED_ARGUMENT], ['--add-tag', '-a', 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], ['--no-colour', '-c', GetoptLong::NO_ARGUMENT], ['--pretend', '-p', GetoptLong::NO_ARGUMENT], ['--help', '-h', GetoptLong::NO_ARGUMENT], ['--info', '-i', GetoptLong::NO_ARGUMENT] ).each do |option, argument| option = option.delete('-').intern case option when :help Output.help_information when :info Output.additional_information when :scheme scheme = argument when :nocolour, :pretend options << option when :viewtag, :removetag actions << [option, argument.dup] when :addtag, :settag raise 'Incorrect argument' unless argument.include?('=') actions << [option, argument.split('=', 2)].flatten when :generate, :rename actions << [option, scheme] else actions << [option] end end Output.help_information if actions.empty? if ARGV.empty? puts 'No files specified.' exit end if options.include?(:nocolour) module Output @colour = false end end mime = FileMagic.mime mime.simplified = true ARGV.each do |file| begin raise 'File does not exist.' unless File.exist?(file) case mime.file(file) when 'audio/x-flac' track = FLAC.new(file) when 'application/ogg' track = Vorbis.new(file) when 'audio/mpeg' track = MP3.new(file) else raise 'File extension not recognised.' end actions.each do |action| catch :next do track.send(*action) end end track.save unless options.include?(:pretend) rescue RuntimeError => message $stderr.puts $0 + ': ' + file + ': ' + message end end