X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/956d1d13564d3a830d2ec2e817d95c521857ce3d..ed3b7d734a1cbea566f2d5cb9c4a9b3938e5a029:/fa-flacd diff --git a/fa-flacd b/fa-flacd index 69566ac..09bfe4a 100755 --- a/fa-flacd +++ b/fa-flacd @@ -13,6 +13,107 @@ 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; @@ -23,8 +124,6 @@ use Pod::Usage; my $debug; my $verbose; -my @jobs; -my @finished; sub verbose { $verbose and print(STDERR $_) for @_; @@ -65,17 +164,7 @@ sub get_tags { 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 { +sub track_tags { my $h = shift; my @result; @@ -88,189 +177,232 @@ sub bork_tags { return @result; } -# Process the fa-rip output in the directory DIR. +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; - my $album; - my $discnum; - my @tags; + 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 $status; - - verbose("Renaming $dir/tags\n"); - rename("$dir/tags", "$dir/using-tags") - or die("rename($dir/tags, $dir/using-tags): $!"); + my $outlog; + my @files; - ($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): $!"); + mkdir($artist) or $!{EEXIST} or die("mkdir($artist): $!"); - my $outdir = "$artist/$album"; - verbose("mkdir($outdir)\n"); - -d "$outdir" or mkdir("$outdir") or die("mkdir($outdir): $!"); + 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): $!"); - 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'}})); + 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(' ', - (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)); + $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 { - die("Major horkage on system(flac): \$? = $? \$! = $!"); + @track_artist = @$disc_artist; } - rename("$outfile.flac-tmp", "$outfile.flac") - or die("rename($outfile.flac-tmp, $outfile.flac): $!"); + 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): $!"); } - 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"); + 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): $!"); + # 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 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; - + 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)))); + } + } - $SIG{CHLD} = \&reaper; - while (1) { - if (scalar(@jobs) <= $MAXJOBS) { - foreach $i (glob('*/tags')) { - push(@jobs, newjob(dirname($i))) <= $MAXJOBS or last; + # 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); + } } - } - for ($i = 0; $i <= $#finished; $i++) { - deljob($i); + # Didn't find anything; wait a while and check again. + sleep(5); } - - verbose(scalar(@jobs), " jobs\n"); - 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: {