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