|
#! /usr/bin/ruby1.9.3 -w |
|
|
|
require 'optparse' |
|
require 'taglib' |
|
|
|
FIELDS = %w( ALBUM ARTIST CDDB DATE GENRE DISCNUMBER TITLE TRACKNUMBER ) |
|
|
|
$dry_run = false |
|
$tag = true |
|
$rename = false |
|
$quiet = false |
|
$verbose = false |
|
$help = false |
|
|
|
def main |
|
opts = OptionParser.new |
|
opts.banner = "Usage: #{opts.program_name} [options] from_pattern to_pattern" |
|
|
|
opts.on('-n', '--dry-run', "Don't run any commands, just print them") { |
|
$dry_run = true |
|
} |
|
opts.on('-t', '--tag', "copy tags from matching files (default)") { |
|
$tag = true |
|
} |
|
opts.on('-T', '--no-tag', "do not copy tags from matching files") { |
|
$tag = false |
|
} |
|
opts.on('-r', '--rename', "rename from matching files") { |
|
$rename = true |
|
} |
|
opts.on('-R', '--no-rename', "do not rename from matching files (default)") { |
|
$rename = false |
|
} |
|
opts.on('-q', '--quiet', "Display less information") { |
|
$quiet = true |
|
} |
|
opts.on('-v', '--verbose', "Display extra information") { |
|
$verbose = true |
|
} |
|
opts.on('-h', '--help', "Show this message") { |
|
$help = true |
|
} |
|
|
|
args = opts.parse(ARGV) |
|
from_pattern, to_pattern = args |
|
|
|
if $help || from_pattern.nil? || to_pattern.nil? |
|
puts opts.help |
|
puts |
|
puts "Examples" |
|
puts " # Copy tags from 1.Track.ogg to 1.Track.flac" |
|
puts " #{opts.program_name} '%.1d.*.ogg' '%.1d.*.flac'" |
|
puts |
|
puts " # Copy tags from 01.Track.ogg to 01.Track.flac" |
|
puts " #{opts.program_name} '%.2d.*.ogg' '%.2d.*.flac'" |
|
puts |
|
puts " # Assuming 1.Song.ogg, rename 1.Track.flac to 1.Song.flac" |
|
puts " #{opts.program_name} -rT '%.1d.*.ogg' '%.1d.*.flac'" |
|
|
|
exit 0 |
|
end |
|
|
|
(1..100).each do |track| |
|
from_file = match_pattern(from_pattern, track) or next |
|
to_file = match_pattern(to_pattern, track) or next |
|
|
|
if $tag |
|
copy_tags(from_file, to_file) |
|
end |
|
if $rename |
|
rename_file(to_file, from_file) |
|
end |
|
end |
|
end |
|
|
|
def match_pattern(pat, track) |
|
candidate = pat.gsub(/([\[\]])/, '\\\\\1') % track |
|
matches = Dir[candidate] |
|
raise "Multiple matches for \"#{candidate}\"" if matches.size > 1 |
|
matches.first |
|
end |
|
|
|
|
|
def copy_tags(from_file, to_file) |
|
puts "Copying tags from #{from_file} to #{to_file}" unless $quiet |
|
from_fields = read_fields(from_file) |
|
puts " Tags: #{from_fields.inspect}" if $verbose |
|
write_fields(to_file, from_fields) unless $dry_run |
|
end |
|
|
|
def read_fields(file) |
|
case File.extname(file) |
|
when '.ogg', '.OGG' |
|
TagLib::Ogg::Vorbis::File.open(file, false) do |f| |
|
field_map = f.tag.field_list_map |
|
FIELDS.each_with_object({}) do |field, fields| |
|
value = field_map[field] |
|
fields[field] = value.first if value && FIELDS.include?(field) |
|
end |
|
end |
|
when '.mp3', '.MP3' |
|
TagLib::MPEG::File.open(file, false) do |f| |
|
tag = f.id3v2_tag |
|
FIELDS.each_with_object({}) do |fld, flds| |
|
m = fld.downcase |
|
flds[fld] = tag.public_send(m) if tag.respond_to?(m) |
|
end |
|
end |
|
else |
|
raise ArgumentError, "Cannot extract tags from #{file}" |
|
end |
|
end |
|
|
|
def write_fields(file, fields) |
|
TagLib::FLAC::File.open(file, false) do |f| |
|
tag = f.xiph_comment |
|
fields.each do |key, value| |
|
tag.add_field(key, value) |
|
end |
|
f.save |
|
end |
|
end |
|
|
|
def rename_file(new_path, old_path) |
|
ext = File.extname(new_path) |
|
new_dir = File.dirname(new_path) |
|
old_name = File.basename(old_path, '.*') |
|
path = File.join(new_dir, "#{old_name}#{ext}").sub(/^\.\//, '') |
|
unless path == new_path |
|
puts "Renaming #{File.basename(new_path)} -> #{File.basename(path)}" unless $quiet |
|
File.rename(new_path, path) unless $dry_run |
|
end |
|
end |
|
|
|
main |