Instantly share code, notes, and snippets.
Last active
August 29, 2015 14:08
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Requires gem taglib-ruby.