Skip to content

Instantly share code, notes, and snippets.

@mschuerig
Last active August 29, 2015 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mschuerig/c1f93169ca168e2f57e6 to your computer and use it in GitHub Desktop.
Save mschuerig/c1f93169ca168e2f57e6 to your computer and use it in GitHub Desktop.
List and apply FLAC file tags. For editing music metadata in a text editor.
#! /usr/bin/ruby2.1 -w
require 'digest/md5'
require 'find'
require 'taglib'
def main
case ARGV[0]
when 'list'
ARGV.shift
each_file(ARGV) do |file|
list_tags(file, $stdout)
$stdout.puts
end
when 'apply'
ARGV.shift
tagsfile = ARGV.first
if File.file?(tagsfile)
tags = Tags.from_tagsfile(tagsfile)
tags.each do |t|
t.apply_to_flac
end
else
usage
exit 2
end
when '-h', '--help'
usage
exit 0
else
usage
exit 2
end
end
def usage
$stderr.puts "#{File.basename($PROGRAM_NAME)} [list file...] [apply tagfile...]"
end
def each_file(args)
args.each do |file|
if File.file?(file)
yield file
elsif File.directory?(file)
Find.find(file) do |f|
Find.prune if File.basename(f)[0] == '.'
next unless File.extname(f) == '.flac'
next unless File.file?(f)
yield f
end
else
$stderr.puts "Don't know what to do with #{file}"
end
end
end
def list_tags(file, out)
out.puts "file: #{file}"
tags = Tags.from_flac(file)
out.puts "hash: #{tags.secure_hash}"
out.puts tags
end
class Tags
include Enumerable
attr_reader :flac
def self.from_flac(flac)
tags = flac_tags(flac)
tags['file'] = [flac]
new(tags)
end
def self.from_tagsfile(tagsfile)
lines = File.readlines(tagsfile).reject { |l| l[0] == '#' }
stanzas = lines.slice_before(/^\s*$/)
all_tags = stanzas.map { |stanza|
stanza.each_with_object({}) { |line, flac_tags|
key, value = line.chomp.split(':', 2)
if key && value
(flac_tags[key] ||= []) << value.strip
end
}
}.reject(&:empty?).
map { |tags|
new(tags)
}
common = new
all_tags.each do |tags|
if tags.has_flac?
tags.fill_in(common)
else
common.update(tags)
end
end
all_tags.select(&:has_flac?)
end
def initialize(tags = {})
@flac = (tags.delete('file') || []).first
@original_secure_hash = (tags.delete('hash') || []).first
@tags = tags
end
def apply_to_flac
if modified?
puts "Updating #{@flac}..."
if can_update_flac?
with_flac_tags(update: true) do |t|
@tags.keys.each do |key|
t.remove_field(key)
end
each do |key, value|
t.add_field(key, value, false)
end
end
else
puts "File does not exist: #{@flac}"
end
else
puts "Not modified #{@flac}"
end
end
def modified?
@original_secure_hash != secure_hash
end
def has_flac?
!!@flac
end
def can_update_flac?
@flac && File.file?(@flac)
end
def update(others)
others.each_tag do |key, values|
@tags[key] = values
end
end
def fill_in(others)
others.each_tag do |key, values|
@tags[key] ||= values
end
end
def each
@tags.sort.each do |(key, values)|
values.sort.each do |v|
yield key, v
end
end
end
def each_tag(&block)
@tags.sort.each(&block)
end
def secure_hash
h = Digest::MD5.new
each do |key, value|
h << "#{key}: #{value}"
end
h.hexdigest
end
def to_s
map { |key, value|
"#{key}: #{value}"
}.join("\n")
end
private
def with_flac_tags(options = {}, &block)
self.class.with_flac_tags(@flac, options, &block)
end
def self.with_flac_tags(flac_file, options = {})
TagLib::FLAC::File.open(flac_file) do |f|
value = yield f.xiph_comment
f.save if options[:update]
value
end
end
def self.flac_tags(flac_file)
with_flac_tags(flac_file) { |t| t.field_list_map }
end
end
main
@mschuerig
Copy link
Author

Requires gem taglib-ruby.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment