X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/ed3b7d734a1cbea566f2d5cb9c4a9b3938e5a029..6ca18c015e6e66589c8aa3620c7a11efb7cc836a:/fa-flacd diff --git a/fa-flacd b/fa-flacd index 09bfe4a..7e13bd9 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,419 +9,6 @@ B - archive CDs to single FLAC files B [B<-j> I] [B<-v>] -=cut - -package Jobs; - -use strict; -use warnings; - -use Errno; -use POSIX ':sys_wait_h'; - -sub newjob { - my $f = shift; - my $jobs = shift; - my $debug = shift; - my $pid; - - if (not $debug) { - $pid = fork(); - if (not defined($pid)) { - die("fork: $!"); - } - } - - if ($debug or $pid == 0) { - exit($f->()); - } - - if ($pid == 0) { - exit($f->()); - } - - push(@$jobs, $pid); - - return $pid; -} - -sub deljob { - my $pid = shift; - my $status = shift; - my $jobs = shift; - - for (my $i = 0; $i <= $#$jobs; $i++) { - if ($pid == $jobs->[$i]) { - splice(@$jobs, $i, 1); - last; - } - } - - return ($pid, $status); -} - -sub run { - my %o = @_; - my $maxjobs = $o{'max-jobs'}; - my $get_job = $o{'get-job'}; - my $notify_start = $o{'notify-start'}; - my $notify_finish = $o{'notify-finish'}; - my @jobs; - my $pid; - - # Call notifier function if given. - sub call { - my $f = shift or return; - ref($f) eq 'CODE' or return; - $f->(@_); - } - - while (1) { - if (@jobs <= $maxjobs) { - my $job; - while (defined($job = $get_job->())) { - $pid = newjob($job, \@jobs, $o{'debug'}); - call($notify_start, $pid, @jobs); - @jobs < $maxjobs or last; - } - - # No jobs running and get-job returned undef; we're finished. - if (@jobs == 0 and not defined($job)) { - return; - } - } - - # Now running as many jobs as we can, block waiting for one to die. - do { - $pid = waitpid(-1, 0); - } while ($pid == 0 - or ($pid == -1 and ($!{ECHILD} or $!{EINTR}))); - $pid == -1 and die("waitpid(-1): $!"); - - # Before starting more, see if any others have finished. - do { - call($notify_finish, deljob($pid, $?, \@jobs), @jobs); - } while (($pid = waitpid(-1, WNOHANG)) > 0); - if ($pid == -1) { - $!{ECHILD} or $!{EINTR} or die("waitpid(-1): $!"); - } - } -} - - -################################################################################ -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): $!"); -} - -# Encode a single wav file to a single flac file, whether the wav and -# flac files represent individual tracks or whole discs. -sub flac { - my $dir = shift; - my $artist = shift; - my $album = shift; - my $discnum = shift; - my $tracknum = shift; - my $track_tags = shift; - my $disc_artist = shift; - my $single_file = not defined($tracknum); - my @tags = @_; - my $outdir; - my $outfile; - my $outlog; - my @files; - - for ($artist, $album) { - s|/|_|g; - } - - verbose("mkdir($artist)\n"); - mkdir($artist) or $!{EEXIST} or die("mkdir($artist): $!"); - - if (not $single_file) { - $outdir = "$artist/$album"; - verbose("mkdir($outdir)\n"); - mkdir("$outdir") or $!{EEXIST} 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"); - - unlink('cue') or die("unlink(cue): $!"); - rename('log', $outlog) - or die("rename(log, $outlog): $!"); - } else { - my $title = join(' ', map(split, @{$track_tags->{'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($track_tags->{'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($track_tags)); - $outlog = "../$outfile.log"; - push(@files, "$outfile.flac"); - - rename("$tracknum.log", $outlog) - or die("rename($tracknum.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): $!"); - } - - # 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. -# if (nothing but using-tags in $dir) { -# unlink("$dir/using-tags") or $!{ENOENT} or die("unlink($dir/using-tags): $!"); -# rmdir($dir) or or $!{ENOENT} die("rmdir($dir): $!"); -# } - - return 0; -} - -sub flacloop { - my $MAXJOBS = shift; - my $dir; - my @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 undef, so Jobs::run never returns. - my $getjob = sub { - # Look for new fa-rip directories. - while (1) { - for my $i (glob('*/tags')) { - $dir = dirname($i); - - verbose("Renaming $dir/tags\n"); - rename("$dir/tags", "$dir/using-tags") - or die("rename($dir/tags, $dir/using-tags): $!"); - - my ($artist, $album, - $discnum, @tags) = get_tags("$dir/using-tags"); - if (-e "$dir/wav") { - # single-file - push(@jobs, - [$dir, $artist, $album, $discnum, - undef, undef, undef, @tags]); - } else { - #multi-file - # Don't need cue file. - unlink("$dir/cue") or die("unlink($dir/cue): $!"); - - # 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; - - push(@jobs, - map { - [$dir, $artist, $album, $discnum, $_, - $tracks_to_tags{$_}, \@disc_artist, @tags] - } sort(map(int, keys(%tracks_to_tags)))); - } - } - - # Return a job if we found any work. - if (my $job = shift(@jobs)) { - return sub { - my $log = defined($job->[4]) ? $job->[4] . '.log' : 'log'; - $dir = $job->[0]; - open(STDERR, ">$dir/$log") or die("open(STDERR, >$dir/$log): $!"); - return flac(@$job); - } - } - - # Didn't find anything; wait a while and check again. - sleep(5); - } - }; - - # Never returns (see $getjob comment). - Jobs::run('max-jobs'=>$MAXJOBS, - 'debug'=>$debug, - 'get-job'=>$getjob, - - 'notify-start'=>sub { - my $pid = shift; - verbose("new job $pid for $dir\n"); - verbose(scalar(@_), " jobs\n"); - }, - - 'notify-finish'=>sub { - my $pid = shift; - my $status = shift; - 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"); - }); -} - -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 @@ -489,10 +74,266 @@ it under the same terms as Perl itself. =cut -# Local variables: -# cperl-indent-level: 4 -# perl-indent-level: 4 -# indent-tabs-mode: nil -# End: +""" + +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 -# vi: set tabstop=4 expandtab: +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 = ' ' + artist = (tags.gets('ARTIST', separator=', ') 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))