X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/21c85cd243a4f9577c55dcc323b08fb6cff05972..1ae4fb3e6c8b5fe43ae5b5f50b2a8e1368a37f42:/fa-flacd diff --git a/fa-flacd b/fa-flacd index 83be598..5e93e1f 100755 --- a/fa-flacd +++ b/fa-flacd @@ -1,8 +1,6 @@ -#! /usr/bin/env perl - -# $Id$ -# $URL$ +#! /usr/bin/python +""" =head1 NAME B - archive CDs to single FLAC files @@ -11,331 +9,6 @@ B - archive CDs to single FLAC files B [B<-j> I] [B<-v>] -=cut - -package Jobs; - -use strict; -use warnings; - -use POSIX ':sys_wait_h'; - -our @jobs; -our @finished; - -sub reaper { - while ((my $pid = waitpid(-1, WNOHANG)) > 0) { - push(@finished, [$pid, $?]); - } - - # XXX if $pid == -1 handle errors? - - $SIG{CHLD} = \&reaper; -} - -sub newjob { - my $f = shift; - my %o = @_; - my $pid; - - $SIG{CHLD} = \&reaper; - if (not $o{'debug'}) { - $pid = fork(); - if (not defined($pid)) { - die("fork: $!"); - } - } - - if ($o{'debug'} or $pid == 0) { - $SIG{CHLD} = 'DEFAULT'; - exit($f->()); - } - - push(@jobs, $pid); - - return $pid; -} - -sub deljob { - my $i = shift; - my $j; - - my ($pid, $status) = @{$finished[$i]}; - - for ($j = 0; $j <= $#jobs; $j++) { - $pid == $jobs[$j] and splice(@jobs, $j, 1) and last; - } - - splice(@finished, $i, 1); - - return ($pid, $status); -} - - -################################################################################ -package main; - -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; - -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); -} - -sub track_tags { - my $h = shift; - my @result; - - while (my ($key, $vall) = each(%$h)) { - for my $val (@$vall) { - push(@result, "$key=$val") - } - } - - return @result; -} - -sub run_flac { - my $infile = shift; - my $cue = shift; - my $outfile = shift; - - my @cue; - if (defined($cue)) { - @cue = ('--cuesheet', $cue); - } - - verbose("Running flac\n"); - my $status = system('flac', '-o', "$outfile.flac-tmp", - '--delete-input-file', '-V', '--no-padding', '--best', - @cue, - map({ ('-T', $_) } @_), - $infile); - 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): $!"); -} - -# Process the fa-rip output in the directory DIR. -sub flac { - my $dir = shift; - my $artist; - my $album; - my $discnum; - my @tags; - my $single_file = -e "$dir/wav"; - my $outdir; - my $outfile; - my $outlog; - my @files; - - 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): $!"); - - if (not $single_file) { - $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): $!"); - - if ($single_file) { - $outfile = $album; - defined($discnum) and $outfile .= " (disc $discnum)"; - run_flac('wav', 'cue', "../$artist/$outfile", @tags); - $outlog = "../$artist/$outfile.log"; - @files = ("$artist/$outfile.flac"); - } else { - # 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. - my %tracks_to_tags; - my @disc_artist; - my @tmp; - for my $tag (@tags) { - if ($tag =~ /^([^[]+)\[(\d+)]=(.*)/) { - push(@{$tracks_to_tags{$2}->{$1}}, $3); - } elsif ($tag =~ /^ARTIST=/) { - push(@disc_artist, $tag); - } else { - push(@tmp, $tag); - } - } - @tags = @tmp; - - for my $tracknum (sort(map(int, keys(%tracks_to_tags)))) { - my $title = join(' ', map(split, @{$tracks_to_tags{$tracknum}->{'TITLE'}})); - $title =~ s|/|_|g; - $outfile = join('/', - $outdir, - join(' ', - (defined($discnum) - ? sprintf('%02d', $discnum) - : ()), - sprintf('%02d', $tracknum), - $title)); - - # If we have ARTIST[n] tags for this track, set - # @track_artist to the empty list; they will go in along - # with the other [n] tags. - my @track_artist; - if (exists($tracks_to_tags{$tracknum}->{'ARTIST'})) { - @track_artist = (); - } else { - @track_artist = @disc_artist; - } - - run_flac(sprintf('track%02d.cdda.wav', $tracknum), undef, - "../$outfile", - @track_artist, - @tags, - "TRACKNUMBER=$tracknum", - track_tags($tracks_to_tags{$tracknum})); - push(@files, "$outfile.flac"); - } - $outlog = "../$outdir/log"; - } - - verbose("Cleaning up $dir\n"); - unlink('using-tags') or die("unlink(using-tags): $!"); - unlink('cue') or die("unlink(cue): $!"); - rename('log', $outlog) - or die("rename(log, $outlog): $!"); - chdir('..') or die("chdir(..): $!"); - - if (-x "$dir/post-processor") { - verbose(join(' ', "Running ./$dir/post-processor", @files), "\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 flacloop { - my $MAXJOBS = shift; - my $i; - - - while (1) { - if (scalar(@jobs) <= $MAXJOBS) { - foreach $i (glob('*/tags')) { - my $dir = dirname($i); - my $pid = - Jobs::newjob(sub { - open(STDERR, ">$dir/log") - or die("open(STDERR, >$dir/log): $!"); - return flac($dir); - }, 'debug'=>$debug); - verbose("new job $pid for $dir\n"); - @Jobs::jobs <= $MAXJOBS or last; - } - } - - for ($i = 0; $i <= $#finished; $i++) { - my ($pid, $status) = Jobs::deljob($i); - 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"); - } - - 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 @@ -401,10 +74,270 @@ 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: +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 + +from flac_archive.tags import Tags + +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 + if os.path.exists('cover.front'): + 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, tracknum, tags): + """Encode a single wav file to a single flac file, whether the wav and + flac files represent individual tracks or whole discs.""" + + separator = ' ' + if len(tags.get('ALBUMARTIST')) > 0: + artist_tag = tags.gets('ALBUMARTIST', separator=', ') + else: + artist_tag = tags.gets('ARTIST', separator=', ') + artist = (artist_tag or '').replace('/', '_') + album = (tags.gets('ALBUM', separator=separator) or '').replace('/', '_') + discnum = tags.gets('DISCNUMBER') + + 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.all()) + files = ['%s/%s.flac' % (artist, outfile)] + + c(os.unlink, 'cue') + outlog = '/'.join(['..', artist, outfile + '.log']) + c(os.rename, 'log', outlog) + else: + title = tags.gets('TITLE', tracknum, separator).replace('/', '_') + tmp = [] + if discnum != None: + tmp.append('%02d' % (int(discnum),)) + tmp.extend(['%02d' % (tracknum,), title]) + part = tags.gets('PART', tracknum) + if part != None: + tmp.extend(['-', part]) + outfile = '/'.join([outdir, ' '.join(tmp)]) + + run_flac('track%02d.cdda.wav' % (tracknum,), None, + '../' + outfile, tags.track(tracknum)) + 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: + try: + os.unlink(dir + '/cover.front') + except OSError: + pass + 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.""" + + tags = Tags() + spew('Opening tags file %s\n' % (fn,)) + tags.load(open(fn)) + + spew('ARTIST %s from %s\n' % (tags.gets('ARTIST'), fn)) + spew('ALBUM %s from %s\n' % (tags.gets('ALBUM'), fn)) + spew('DISCNUMBER %s from %s\n' % (tags.gets('DISCNUMBER'), fn)) + + return 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') + + tags = get_tags(dir[0] + '/using-tags') + if os.path.exists(dir[0] + '/wav'): + # single-file + jobs.append((dir[0], 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 + + jobs.extend([(dir[0], x, tags) + for x in xrange(1, len(tags) + 1)]) + 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[1] == None and 'log' + or str(job[1]) + '.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))