Initial commit
This commit is contained in:
commit
6371d65baf
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# This drawer contains no socks
|
||||||
|
|
||||||
|
Just a dumping ground for random scripts and trinkets that I use.
|
276
flacconv.pl
Executable file
276
flacconv.pl
Executable file
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use utf8;
|
||||||
|
# use open qw( :std :encoding(UTF-8) );
|
||||||
|
|
||||||
|
use Audio::FLAC::Header;
|
||||||
|
use Carp;
|
||||||
|
use Data::Dumper;
|
||||||
|
use Encode;
|
||||||
|
use File::Copy;
|
||||||
|
use File::Find::Rule;
|
||||||
|
use File::Path qw(make_path);
|
||||||
|
use File::Spec qw(catfile catdir);
|
||||||
|
use File::Which qw(which);
|
||||||
|
use List::MoreUtils qw(uniq);
|
||||||
|
use Path::Tiny qw(path);
|
||||||
|
use Readonly;
|
||||||
|
use Scalar::Util qw(looks_like_number reftype);
|
||||||
|
|
||||||
|
my $prgmname = $0;
|
||||||
|
my ($path, $destdir, $filetype) = @ARGV;
|
||||||
|
my $debug = $ENV{DEBUG} or 0;
|
||||||
|
|
||||||
|
my $ffmpeg;
|
||||||
|
|
||||||
|
# For metadata arguments to ffmpeg
|
||||||
|
Readonly my $METATAG_FMT => '%s=%s';
|
||||||
|
|
||||||
|
# Database to map "normalized" FLAC tags to lossy tags.
|
||||||
|
# Arguably there might be better "algorithmic" ways to do this, but
|
||||||
|
# I like having a flat quasi-database to look up how tags are mapped.
|
||||||
|
my $tagmap = {
|
||||||
|
mp3 => {
|
||||||
|
"ALBUMARTIST" => "album_artist",
|
||||||
|
"DISCNUMBER" => "disc",
|
||||||
|
"TRACKNUMBER" => "track",
|
||||||
|
"TRACKTOTAL" => "TRACKTOTAL",
|
||||||
|
"ARTIST" => "artist",
|
||||||
|
"ALBUM" => "album",
|
||||||
|
"DATE" => "date",
|
||||||
|
"GENRE" => "genre",
|
||||||
|
"TITLE" => "title",
|
||||||
|
},
|
||||||
|
m4a => {
|
||||||
|
"ALBUMARTIST" => "album_artist",
|
||||||
|
"DISCNUMBER" => "disc",
|
||||||
|
"TRACKNUMBER" => "track",
|
||||||
|
"TRACKTOTAL" => "TRACKTOTAL",
|
||||||
|
"ARTIST" => "artist",
|
||||||
|
"ALBUM" => "album",
|
||||||
|
"DATE" => "date",
|
||||||
|
"GENRE" => "genre",
|
||||||
|
"TITLE" => "title",
|
||||||
|
},
|
||||||
|
opus => {
|
||||||
|
"ALBUMARTIST" => ["ALBUMARTIST", "album_artist"],
|
||||||
|
"DISCNUMBER" => "DISCNUMBER",
|
||||||
|
"TRACKNUMBER" => "TRACKNUMBER",
|
||||||
|
"TRACKTOTAL" => "TRACKTOTAL",
|
||||||
|
"ARTIST" => "ARTIST",
|
||||||
|
"ALBUM" => "ALBUM",
|
||||||
|
"DATE" => "DATE",
|
||||||
|
"GENRE" => "GENRE",
|
||||||
|
"TITLE" => "TITLE",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
my $codecmap = {
|
||||||
|
mp3 => ['-c:a', 'libmp3lame', '-q:a', '0'],
|
||||||
|
m4a => ['-c:a', 'libfdk_aac', '-vbr', '5', '-c:v', 'copy', '-disposition:v:1', 'attached_pic'],
|
||||||
|
# opus uses -vn because some clients (android) get confused and
|
||||||
|
# think it's video if there's a "video frame" there
|
||||||
|
opus => ['-vn', '-c:a', 'libopus', '-b:a', '160K', '-f', 'ogg']
|
||||||
|
};
|
||||||
|
|
||||||
|
sub usage {
|
||||||
|
my ($exterr) = shift;
|
||||||
|
if (defined $exterr) {
|
||||||
|
print STDERR "err: $exterr\n";
|
||||||
|
}
|
||||||
|
print STDERR "usage: $0 path destdir [filetype]\n";
|
||||||
|
print STDERR " filetype can be 'm4a', 'mp3', or 'opus' (default: opus)\n";
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# get "normalized" FLAC tags
|
||||||
|
sub get_tags {
|
||||||
|
my $flacname = shift;
|
||||||
|
my %flactags = do {
|
||||||
|
my $flacobj = Audio::FLAC::Header->new($flacname);
|
||||||
|
my $fobjtag = $flacobj->tags();
|
||||||
|
map {
|
||||||
|
uc($_) => Encode::decode('UTF-8', $fobjtag->{$_}, Encode::FB_CROAK)
|
||||||
|
} %$fobjtag;
|
||||||
|
};
|
||||||
|
|
||||||
|
# vorbis comments don't really have a "standard" tag set, so the
|
||||||
|
# album artist might be stored a couple different ways. this is
|
||||||
|
# necessary for my weird tastes in folder structure.
|
||||||
|
if (not exists $flactags{"ALBUMARTIST"}) {
|
||||||
|
if (exists $flactags{"ALBUM_ARTIST"}) {
|
||||||
|
$flactags{"ALBUMARTIST"} = $flactags{"ALBUM_ARTIST"};
|
||||||
|
} else {
|
||||||
|
carp("Couldn't get album artist for " . $flactags{"ALBUM"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists $flactags{"TRACKNUMBER"}) {
|
||||||
|
# split tracknumber into TRACKNUMBER and TRACKTOTAL
|
||||||
|
if ($flactags{"TRACKNUMBER"} =~ m{^(\d+)/(\d+)$}x) {
|
||||||
|
$flactags{"TRACKNUMBER"} = $1;
|
||||||
|
$flactags{"TRACKTOTAL"} = $2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists $flactags{"DISCNUMBER"}) {
|
||||||
|
# DISCNUMBER can be X/Y, so we'll leave it be and use DISCIND
|
||||||
|
# for later filename formatting
|
||||||
|
if ($flactags{"DISCNUMBER"} =~ m{^(\d+)/(\d+)$}x) {
|
||||||
|
$flactags{"DISCIND"} = $1;
|
||||||
|
$flactags{"DISCTOTAL"} = $2;
|
||||||
|
} else {
|
||||||
|
$flactags{"DISCIND"} = $flactags{"DISCNUMBER"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return \%flactags;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Actual ffmpeg conversion
|
||||||
|
sub do_convert {
|
||||||
|
my $flacname = shift;
|
||||||
|
my $flactags = &get_tags($flacname);
|
||||||
|
|
||||||
|
if (not defined $flactags) {
|
||||||
|
carp("Couldn't get FLAC metadata for $flacname!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my @pathdirs = ($destdir,
|
||||||
|
$flactags->{"ALBUMARTIST"},
|
||||||
|
$flactags->{"ALBUM"});
|
||||||
|
|
||||||
|
my $outpath = File::Spec->catdir(@pathdirs);
|
||||||
|
|
||||||
|
make_path($outpath) if (! -d $outpath);
|
||||||
|
|
||||||
|
$flactags->{"TITLE"} =~ s,/,-,g;
|
||||||
|
|
||||||
|
# Create the destination path
|
||||||
|
my $fname = sprintf("%02d %s.%s", $flactags->{"TRACKNUMBER"},
|
||||||
|
$flactags->{"TITLE"},
|
||||||
|
$filetype);
|
||||||
|
# Prepend the disc number to the track number
|
||||||
|
# e.g., 2-05 My Song.mp3
|
||||||
|
if (exists $flactags->{"DISCIND"}) {
|
||||||
|
# If DISCTOTAL exists, we can confirm if there are more than
|
||||||
|
# one disc in the collection.
|
||||||
|
if (!exists $flactags->{"DISCTOTAL"}
|
||||||
|
or $flactags->{"DISCTOTAL"} > 1) {
|
||||||
|
$fname = $flactags->{"DISCIND"} . "-$fname";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
my $outfile = File::Spec->catfile(@pathdirs, $fname);
|
||||||
|
|
||||||
|
if (-e $outfile) {
|
||||||
|
print "$outfile already exists, ignoring...\n";
|
||||||
|
return $outfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ffmpeg -i <flacname> <codec_args> -map_metadata -1 ...
|
||||||
|
my @ffargs = ($ffmpeg, "-i", $flacname,
|
||||||
|
@{$codecmap->{$filetype}},
|
||||||
|
'-map_metadata', '-1'); # don't copy metadata
|
||||||
|
|
||||||
|
# Generate the args to map metadata
|
||||||
|
my $tm = $tagmap->{$filetype};
|
||||||
|
for my $tag (keys %$flactags) {
|
||||||
|
if (exists $tm->{$tag}) {
|
||||||
|
my $rt = (reftype($tm->{$tag}) or 'none');
|
||||||
|
if ($rt eq 'ARRAY') {
|
||||||
|
# one FLAC tag applied to multiple $filetype tags
|
||||||
|
push (@ffargs, '-metadata',
|
||||||
|
sprintf($METATAG_FMT, $_, $flactags->{$tag}))
|
||||||
|
for @{ $tm->{$tag} };
|
||||||
|
} else {
|
||||||
|
# 1:1 tagging
|
||||||
|
push(@ffargs, '-metadata',
|
||||||
|
sprintf($METATAG_FMT, $tm->{$tag}, $flactags->{$tag}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ... <outfile>
|
||||||
|
push(@ffargs, $outfile);
|
||||||
|
print "CMD: @ffargs\n" if (exists $ENV{"FLACCONV_DEBUG"}
|
||||||
|
and $ENV{"FLACCONV_DEBUG"} == 1);
|
||||||
|
if (system(@ffargs) == 0) {
|
||||||
|
return $outfile;
|
||||||
|
} else {
|
||||||
|
carp("Failed to process $flacname!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined $path || !defined $destdir) {
|
||||||
|
&usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filetype = "opus" if not defined $filetype;
|
||||||
|
&usage("Filetype $filetype not supported")
|
||||||
|
if not exists $tagmap->{$filetype};
|
||||||
|
|
||||||
|
$ffmpeg = which("ffmpeg") or croak("Could not find ffmpeg in PATH!");
|
||||||
|
|
||||||
|
if (-e $destdir) {
|
||||||
|
croak("$destdir exists but isn't a directory!") if not -d $destdir;
|
||||||
|
} else {
|
||||||
|
make_path($destdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
# map of output dirs to their source dirs
|
||||||
|
my %outdirs;
|
||||||
|
|
||||||
|
if (-f $path) {
|
||||||
|
# single file? no problem..........
|
||||||
|
my $outfile = &do_convert($path);
|
||||||
|
croak("Failed to process $path in single-file mode, bailing out!")
|
||||||
|
if not defined $outfile;
|
||||||
|
my $outdir = path($outfile)->parent->stringify;
|
||||||
|
my $indir = path($path)->parent->stringify;
|
||||||
|
$outdirs{$outdir} = [$indir];
|
||||||
|
} elsif (-d $path) {
|
||||||
|
my @flacfiles = File::Find::Rule->file->name('*.flac')->in($path);
|
||||||
|
my @outfiles;
|
||||||
|
for (@flacfiles) {
|
||||||
|
my $decfile = Encode::decode('UTF-8', $_, Encode::FB_CROAK);
|
||||||
|
print "Reading $decfile\n";
|
||||||
|
my $outfile = &do_convert($decfile);
|
||||||
|
last if not defined $outfile;
|
||||||
|
|
||||||
|
# add outfile dir to outdir
|
||||||
|
my $outdir = path($outfile)->parent->stringify;
|
||||||
|
my $indir = path($decfile)->parent->stringify;
|
||||||
|
$outdirs{$outdir} = [] unless exists $outdirs{$outdir};
|
||||||
|
print "dbg: pushing $outdir to $indir\n" if $debug;
|
||||||
|
push(@{ $outdirs{$outdir} }, $indir);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
croak("$path is neither a directory nor a file... does it exist?");
|
||||||
|
}
|
||||||
|
|
||||||
|
# copy cover.{png,jpg} to the destination dir
|
||||||
|
for my $dir (keys %outdirs) {
|
||||||
|
print "dbg: $dir = $outdirs{$dir}\n" if $debug;
|
||||||
|
for my $id (@{ $outdirs{$dir} }) {
|
||||||
|
print "dbg: opening $id\n" if $debug;
|
||||||
|
opendir IND, $id or last;
|
||||||
|
my ($cfile) = grep(/(cover|folder)\.(png|jpe?g)/xi, readdir IND);
|
||||||
|
closedir IND;
|
||||||
|
if (defined $cfile) {
|
||||||
|
# get file extension
|
||||||
|
if ($cfile =~ /.+\.(.+)?$/) {
|
||||||
|
my $copath = path($dir)->child("cover.$1");
|
||||||
|
print "$cfile -> $copath\n";
|
||||||
|
copy(File::Spec->catfile($id, $cfile), $copath)
|
||||||
|
or carp("Unable to copy $cfile: $!");
|
||||||
|
last; # @{ $outdirs{$dir} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue