X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/956d1d13564d3a830d2ec2e817d95c521857ce3d..e0a7f4f7a7ae12ecbd9e016713bca7962ddf77c6:/fa-flacd diff --git a/fa-flacd b/fa-flacd index 69566ac..bef4954 100755 --- a/fa-flacd +++ b/fa-flacd @@ -1,8 +1,6 @@ -#! /usr/bin/env perl - -# $Id$ -# $URL$ +#! /usr/bin/env python2.4 +''' =head1 NAME B - archive CDs to single FLAC files @@ -11,287 +9,6 @@ B - archive CDs to single FLAC files B [B<-j> I] [B<-v>] -=cut - -use strict; -use warnings; - -use File::Basename; -use Getopt::Long qw(:config gnu_getopt no_ignore_case); -use POSIX ':sys_wait_h'; -use Pod::Usage; - -my $debug; -my $verbose; -my @jobs; -my @finished; - -sub verbose { - $verbose and print(STDERR $_) for @_; -} - -# Return the ARTIST, ALBUM, and DATE followed by a list of all the -# lines in the file FN. -sub get_tags { - my $fn = shift; - my $tag; - my $value; - my $artist; - my $album; - my $discnum; - my @tags; - - verbose("Opening tags file $fn\n"); - open(TAGS, $fn) or die("open($fn): $!"); - while () { - chomp; - push(@tags, $_); - - ($tag, $value) = split(/=/, $_, 2); - - if (/^ARTIST=/i) { - $artist = $value; - verbose("ARTIST $artist from $fn\n"); - } elsif (/^ALBUM=/i) { - $album = $value; - verbose("ALBUM $album from $fn\n"); # cperl-mode sucks " - } elsif (/^DISCNUMBER=/i) { - $discnum = int($value); - verbose("DISCNUMBER $discnum from $fn\n"); - } - } - close(TAGS) or die("close($fn): $!"); - - return ($artist, $album, $discnum, @tags); -} - -# Hash of Hashes of Lists Push -sub hhlp { - my $hash = shift; - my $key1 = shift; - my $key2 = shift; - my $val = shift; - - return push(@{$hash->{$key1}->{$key2}}, $val); -} - -sub bork_tags { - my $h = shift; - my @result; - - while (my ($key, $vall) = each(%$h)) { - for my $val (@$vall) { - push(@result, "$key=$val") - } - } - - return @result; -} - -# Process the fa-rip output in the directory DIR. -sub flac { - my $dir = shift; - my $artist; - my $album; - my $discnum; - my @tags; - my $outfile; - my $status; - - verbose("Renaming $dir/tags\n"); - rename("$dir/tags", "$dir/using-tags") - or die("rename($dir/tags, $dir/using-tags): $!"); - - ($artist, $album, $discnum, @tags) = get_tags("$dir/using-tags"); - for ($artist, $album) { - s|/|_|g; - } - - verbose("mkdir($artist)\n"); - -d $artist or mkdir($artist) or die("mkdir($artist): $!"); - - my $outdir = "$artist/$album"; - verbose("mkdir($outdir)\n"); - -d "$outdir" or mkdir("$outdir") or die("mkdir($outdir): $!"); - - verbose("chdir($dir)\n"); - chdir($dir) or die("chdir($dir): $!"); - - my @artist; - my %things; - my (@bork, @titles); - for my $tag (@tags) { - if ($tag =~ /^([^[]+)\[(\d+)]=(.*)/) { - hhlp(\%things, $2, $1, $3); - } elsif ($tag =~ /^ARTIST=/) { - push(@artist, $tag); - } else { - push(@bork, $tag); - } - } - @tags = @bork; - - my @files; - for my $tracknum (sort(map(int, keys(%things)))) { - my $title = join(' ', map(split, @{$things{$tracknum}->{'TITLE'}})); - $title =~ s|/|_|g; - $outfile = join(' ', - (defined($discnum) - ? sprintf('%02d', $discnum) - : ()), - sprintf('%02d', $tracknum), - $title); - push(@files, "$outdir/$outfile.flac"); - $outfile = "../$outdir/$outfile"; - - my @lartist; - if (exists($things{$tracknum}->{'ARTIST'})) { - @lartist = (); - } else { - @lartist = @artist; - } - - verbose("Running flac\n"); - $status = system('flac', '-o', "$outfile.flac-tmp", - '--delete-input-file', '-V', '--no-padding', '--best', - map({ ('-T', $_) } - @lartist, - grep({ $_ !~ /^ARTIST=/ } @tags), - bork_tags($things{$tracknum})), - sprintf('track%02d.cdda.wav', $tracknum)); - if (WIFEXITED($status)) { - if (($status = WEXITSTATUS($status)) != 0) { - die("flac exited with status $status"); - } - } elsif (WIFSIGNALED($status)) { - die("flac killed with signal ", WTERMSIG($status)); - } elsif (WIFSTOPPED($status)) { - die("flac stopped with signal ", WSTOPSIG($status)); - } else { - die("Major horkage on system(flac): \$? = $? \$! = $!"); - } - - rename("$outfile.flac-tmp", "$outfile.flac") - or die("rename($outfile.flac-tmp, $outfile.flac): $!"); - } - - verbose("Cleaning up $dir\n"); - unlink('using-tags') or die("unlink(using-tags): $!"); - unlink('cue') or die("unlink(cue): $!"); - rename('log', "../$outdir/log") - or die("rename(log, ../$outdir/log): $!"); - chdir('..') or die("chdir(..): $!"); - - if (-x "$dir/post-processor") { - verbose("Running './$dir/post-processor'\n"); - system("./$dir/post-processor", @files); - unlink("$dir/post-processor") or die("unlink($dir/post-processor): $!"); - } - - rmdir($dir) or die("rmdir($dir): $!"); - - return 0; -} - -sub reaper { - my $pid; - - while (($pid = waitpid(-1, WNOHANG)) > 0) { - push(@finished, [$pid, $?]); - } - - $SIG{CHLD} = \&reaper; -} - -sub newjob { - my $dir = shift; - my $pid; - - if (not $debug) { - $pid = fork(); - if (not defined($pid)) { - die("fork: $!"); - } - } - - if ($debug or $pid == 0) { - $SIG{CHLD} = 'DEFAULT'; - open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!"); - exit(flac($dir)); - } - - verbose("new job $pid for $dir\n"); - return $pid; -} - -sub deljob { - my $i = shift; - my $j; - my $pid; - my $status; - - $pid = $finished[$i][0]; - $status = $finished[$i][1]; - - verbose("$pid finished ("); - if (WIFEXITED($status)) { - verbose('exited with status ', WEXITSTATUS($status)); - } elsif (WIFSIGNALED($status)) { - verbose('killed with signal ', WTERMSIG($status)); - } elsif (WIFSTOPPED($status)) { - verbose('stopped with signal ', WSTOPSIG($status)); - } - verbose(")\n"); - - for ($j = 0; $j <= $#jobs; $j++) { - $pid == $jobs[$j] and splice(@jobs, $j, 1) and last; - } - - splice(@finished, $i, 1); -} - -sub flacloop { - my $MAXJOBS = shift; - my $i; - my $j; - - - $SIG{CHLD} = \&reaper; - while (1) { - if (scalar(@jobs) <= $MAXJOBS) { - foreach $i (glob('*/tags')) { - push(@jobs, newjob(dirname($i))) <= $MAXJOBS or last; - } - } - - for ($i = 0; $i <= $#finished; $i++) { - deljob($i); - } - - verbose(scalar(@jobs), " jobs\n"); - sleep(5); - } -} - -MAIN: { - my $jobs; - my $help; - - $jobs = 4; - GetOptions( - 'debug|X' => \$debug, - 'jobs|j=i' => \$jobs, - 'verbose|v' => \$verbose, - 'help|h|?' => \$help, - ) or pod2usage(); - $help and pod2usage(-exitstatus=>0, -verbose=>1); - - flacloop($jobs); -} - - -__END__ - =head1 DESCRIPTION B and B together comprise B, a system @@ -357,10 +74,300 @@ it under the same terms as Perl itself. =cut -# Local variables: -# cperl-indent-level: 4 -# perl-indent-level: 4 -# indent-tabs-mode: nil -# End: - -# vi: set tabstop=4 expandtab: +''' #' # python-mode is sucks + +import os +import re +import sys +import time +import traceback +from errno import EEXIST, ENOENT +from glob import glob +from optparse import OptionParser + +import org.diplodocus.jobs +from org.diplodocus.structures import ListDictDict +from org.diplodocus.util import die, flatten, nothing +from org.diplodocus.util import catch_EnvironmentError as c + +def spew(*args): + if verbose: + for i in args: + sys.stderr.write(i) + +################################################################################ +# The child processes + +def run_flac(infile, cue, outfile, tags): + argv = ['flac', '-o', outfile + '.flac-tmp', + '--delete-input-file', '-V', '--no-padding', '--best'] + if cue != None: + argv.extend(['--cuesheet', cue]) + for i in tags: + argv.extend(['-T', i]) + argv.append(infile) + # flac 1.1.3 PICTURE support + #argv.extend(['--picture', '3|image/jpeg|||cover.front']) + + spew('Running flac\n') + status = os.spawnvp(os.P_WAIT, argv[0], argv) + if status > 0: + die(2, 'flac exited with status ', str(status)) + elif status < 0: + die(2, 'flac killed with signal ', str(abs(status))) + + c(os.rename, outfile + '.flac-tmp', outfile + '.flac') + +def flac(dir, artist, album, discnum, tracknum, track_tags, disc_artist, tags): + '''Encode a single wav file to a single flac file, whether the wav and + flac files represent individual tracks or whole discs.''' + + (artist, album) = [x.replace('/', '_') for x in (artist, album)] + + spew('mkdir(%s)\n' % (artist,)) + try: + c(os.mkdir, artist) + except EnvironmentError, error: + error.errno == EEXIST or die(2, error.msg, traceback.format_exc()) + + if tracknum != None: + outdir = '/'.join([artist, album]) + spew('mkdir(%s)\n' % (outdir,)) + try: + c(os.mkdir, outdir) + except EnvironmentError, error: + error.errno == EEXIST or die(2, error.msg, traceback.format_exc()) + + spew('chdir(%s)\n' % (dir,)) + c(os.chdir, dir) + + if tracknum == None: + outfile = album + if discnum != None: + outfile = ''.join([discnum, ' ', outfile]) + run_flac('wav', 'cue', '/'.join(['..', artist, outfile]), tags) + files = ['%s/%s.flac' % (artist, outfile)] + + c(os.unlink, 'cue') + outlog = '/'.join(['..', artist, outfile + '.log']) + c(os.rename, 'log', outlog) + else: + title = [] + for i in track_tags['TITLE']: + title.extend(i.split()) + title = ' '.join(title).replace('/', '_') + tmp = [] + if discnum != None: + tmp.append('%02d' % (int(discnum),)) + tmp.extend(['%02d' % (tracknum,), title]) + outfile = '/'.join([outdir, ' '.join(tmp)]) + + tags = tags[:] + # If we have ARTIST[n] tags for this track, they'll go in with + # the other [n] tags. Else, prepend disc_artist to tags. + if 'ARTIST' not in track_tags: + for i in disc_artist: + tags.insert(0, i) + + tags.append('TRACKNUMBER=%d' % (tracknum,)) + tags.extend(flatten([['='.join([key, x]) for x in track_tags[key]] + for key in track_tags])) + + run_flac('track%02d.cdda.wav' % (tracknum,), None, + '../' + outfile, tags) + outlog = ''.join(['../', outfile, '.log']) + files = [outfile + '.flac'] + + c(os.rename, str(tracknum) + '.log', outlog) + + c(os.chdir, '..') + + post = dir + '/post-processor' + if os.path.exists(post): + spew('Running ', post); spew(files); spew('\n') + files.insert(0, post) + os.spawnv(os.P_WAIT, post, files) + c(os.unlink, post) + + # Clean up if we're the last job for dir; for multi-file dirs, + # it's possible for more than one job to run cleanup at once, so + # don't fail if things are already clean. + ld = os.listdir(dir) + if ld == ['using-tags'] or sorted(ld) == ['cover.front', 'using-tags']: + try: + os.unlink(dir + '/cover.front') + os.unlink(dir + '/using-tags') + os.rmdir(dir) + except EnvironmentError: + pass + + return 0 + +################################################################################ +# The master process + +def get_tags(fn): + '''Return the ARTIST, ALBUM, and DATE followed by a list of all the + lines in the file FN.''' + + artist = album = discnum = None + tags = [] + + spew('Opening tags file %s\n' % (fn,)) + fp = file(fn) + for line in (x.rstrip() for x in fp): + tags.append(line) + + (tag, value) = line.split('=', 1) + + if re.match(r'ARTIST=', line, re.IGNORECASE): + artist = value + spew('ARTIST %s from %s\n' % (artist, fn)) + elif re.match(r'ALBUM=', line, re.IGNORECASE): + album = value + spew('ALBUM %s from %s\n' % (album, fn)) + elif re.match(r'DISCNUMBER=', line, re.IGNORECASE): + discnum = value + spew('DISCNUMBER %s from %s\n' % (discnum, fn)) + + return (artist, album, discnum, tags) + +def flacloop(maxjobs): + dir = [None] # [str] instead of str for lame python closures + jobs = [] + + # Get a job for jobs.run. On each call, look for new fa-rip + # directories and append an item to the queue @jobs for each wav + # file therein. Then, if we have anything in the queue, return a + # function to call flac for it, otherwise sleep for a bit. This + # looks forever, never returning None, so jobs.run never returns. + def getjob(reap): + # Look for new fa-rip directories. + while True: + for i in glob('*/tags'): + try: + dir[0] = os.path.dirname(i) + + spew("Renaming %s/tags\n" % (dir[0],)) + c(os.rename, dir[0] + '/tags', dir[0] + '/using-tags') + + (artist, album, discnum, tags) = get_tags(dir[0] + '/using-tags') + if os.path.exists(dir[0] + '/wav'): + # single-file + jobs.append([dir[0], artist, album, discnum, + None, None, None, tags]) + else: + # multi-file + # Don't need cue file. + try: + c(os.unlink, dir[0] + '/cue') + except EnvironmentError, error: + if error.errno != ENOENT: + raise error + + # Go over @tags, store all [n] tags in a list keyed by + # n in %tracks_to_tags, store all ARTIST (not + # ARTIST[n]) tags in @disc_artist, and leave the rest + # in @tags. + tracks_to_tags = ListDictDict() + disc_artist = [] + tmp = [] + for tag in tags: + m = re.match(r'([^[]+)\[(\d+)]=(.*)', tag) + if m != None: + tracks_to_tags.append(int(m.group(2)), m.group(1), m.group(3)) + elif re.match(r'ARTIST=', tag, re.IGNORECASE): + disc_artist.append(tag) + else: + tmp.append(tag) + tags = tmp + + jobs.extend([[dir[0], artist, album, discnum, x, + tracks_to_tags[x], disc_artist, tags] + for x in sorted(map(int, + tracks_to_tags.keys()))]) + except Exception, error: + sys.stderr.write(getattr(error, 'msg', '')) + traceback.print_exc() + sys.stderr.write('Continuing...\n') + + # Return a job if we found any work. + try: + job = jobs.pop(0) + except IndexError: + # Didn't find anything; wait a while and check again. + time.sleep(5) + reap() + continue + + def lamb(): + log = '/'.join([job[0], + job[4] == None and 'log' or str(job[4]) + + '.log']) + try: + c(os.dup2, c(os.open, log, os.O_CREAT | os.O_WRONLY), 2) + return flac(*job) + except EnvironmentError, error: + sys.stderr.write(getattr(error, 'msg', '')) + traceback.print_exc() + return 1 + return lamb + + def notify_start(pid, jobs): + # make this print '2 jobs; start 378 for Artist/01 Title.flac' + spew('%d jobs; start %d for %s\n' % (len(jobs), pid, dir[0])) + + def notify_finish(pid, status, jobs): + spew('%d jobs; %d finished (' % (len(jobs), pid)) + if os.WIFEXITED(status): + spew('exited with status ', str(os.WEXITSTATUS(status))) + elif os.WIFSIGNALED(status): + spew('killed with signal ', str(os.WTERMSIG(status))) + elif os.WIFSTOPPED(status): + spew('stopped with signal ', str(os.WSTOPSIG(status))) + spew(')\n') + + # Never returns (see getjob comment). + org.diplodocus.jobs.run(maxjobs=maxjobs, debug=debug, + get_job=getjob, + notify_start=notify_start, + notify_finish=notify_finish) + +def main(argv): + # Control the exit code for any uncaught exceptions. + try: + parser = OptionParser() + parser.disable_interspersed_args() + parser.add_option('-X', '--debug', action='store_true', default=False) + parser.add_option('-j', '--jobs', type='int', default=1) + parser.add_option('-v', '--verbose', action='store_true', default=False) + except: + traceback.print_exc() + return 2 + + try: + # Raises SystemExit on invalid options in argv. + (options, args) = parser.parse_args(argv[1:]) + except Exception, error: + if isinstance(error, SystemExit): + return 1 + traceback.print_exc() + return 2 + + try: + global debug, verbose + debug = options.debug + verbose = options.verbose + + # Never returns... + flacloop(options.jobs) + except Exception, error: + # but might blow up. + if isinstance(error, SystemExit): + raise + sys.stderr.write(getattr(error, 'msg', '')) + traceback.print_exc() + return 2 + +if __name__ == '__main__': + sys.exit(main(sys.argv))