X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/38d7bbebf8335dfc4735b7aa6a51f32e7543e1c9..fe8996f09b7acb90ff10342f05deeb6c038af045:/fa-flacd diff --git a/fa-flacd b/fa-flacd index 74c35ab..14fb706 100755 --- a/fa-flacd +++ b/fa-flacd @@ -1,36 +1,132 @@ #! /usr/bin/env perl # $Id$ +# $URL$ =head1 NAME -B, B, B - archive CDs to single FLAC files +B - archive CDs to single FLAC files =head1 SYNOPSIS -B [B<-jv>] +B [B<-j> I] [B<-v>] -B +=cut -B ID TRACKCOUNT OFFSET [OFFSET ...] LENGTH +package Jobs; -=cut +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::Std; $Getopt::Std::STANDARD_HELP_VERSION = 1; +use Getopt::Long qw(:config gnu_getopt no_ignore_case); use POSIX ':sys_wait_h'; +use Pod::Usage; -our $VERSION = 1; - +my $debug; my $verbose; -my @jobs; -my @finished; sub verbose { - $verbose and map({ print(STDERR $_) } @_); + $verbose and print(STDERR $_) for @_; } # Return the ARTIST, ALBUM, and DATE followed by a list of all the @@ -41,7 +137,7 @@ sub get_tags { my $value; my $artist; my $album; - my $date; + my $discnum; my @tags; verbose("Opening tags file $fn\n"); @@ -52,162 +148,277 @@ sub get_tags { ($tag, $value) = split(/=/, $_, 2); - if (/^ARTIST=/) { + if (/^ARTIST=/i) { $artist = $value; verbose("ARTIST $artist from $fn\n"); - } elsif (/^ALBUM=/) { + } elsif (/^ALBUM=/i) { $album = $value; - verbose("ALBUM $album from $fn\n"); - } elsif (/^DATE=/) { - $date = $value; - verbose("DATE $date from $fn\n"); + 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, $date, @tags); + return ($artist, $album, $discnum, @tags); } -# Process the fa-rip output in the directory DIR. -sub flac { - my $dir = shift; - my $artist; - my $album; - my $date; - my @tags; - my $status; +sub track_tags { + my $h = shift; + my @result; - verbose("Renaming $dir/tags\n"); - rename("$dir/tags", "$dir/using-tags") - or die("rename($dir/tags, $dir/using-tags): $!"); + while (my ($key, $vall) = each(%$h)) { + for my $val (@$vall) { + push(@result, "$key=$val") + } + } - ($artist, $album, $date, @tags) = get_tags("$dir/using-tags"); + return @result; +} - verbose("mkdir($artist)\n"); - -d $artist or mkdir($artist) or die("mkdir($artist): $!"); +sub run_flac { + my $infile = shift; + my $cue = shift; + my $outfile = shift; - verbose("chdir($dir)\n"); - chdir($dir) or die("chdir($dir): $!"); + my @cue; + if (defined($cue)) { + @cue = ('--cuesheet', $cue); + } verbose("Running flac\n"); - $status = system('flac', '-o', "../$artist/$album.flac-tmp", - '--delete-input-file', '-V', '--cuesheet', - 'cue', '--no-padding', '--best', - map({ ('-T', $_) } @tags), - 'wav'); - if (WIFEXITED($status) and ($status = WEXITSTATUS($status)) != 0) { - die("flac: $status"); + 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): \$? = $? \$! = $!"); } - verbose("Cleaning up $dir\n"); - unlink('using-tags') or die("unlink(using-tags): $!"); - unlink('cue') or die("unlink(cue): $!"); - rename('toc', "../$artist/$album.toc") - or die("rename(toc, ../$artist/$album.toc): $!"); - rename('log', "../$artist/$album.log") - or die("rename(log, ../$artist/$album.log): $!"); - chdir('..') or die("chdir(..): $!"); - rmdir($dir) or die("rmdir($dir): $!"); - - rename("$artist/$album.flac-tmp", "$artist/$album.flac") - or die("rename($artist/$album.flac-tmp, $artist/$album.flac): $!"); - - return 0; + rename("$outfile.flac-tmp", "$outfile.flac") + or die("rename($outfile.flac-tmp, $outfile.flac): $!"); } -sub reaper { - my $pid; - - while (($pid = waitpid(0, WNOHANG)) > 0) { - push(@finished, [$pid, $?]); +# 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; } - $SIG{CHLD} = \&reaper; -} - -sub newjob { - my $dir = shift; - my $pid; + verbose("mkdir($artist)\n"); + mkdir($artist) or $!{EEXIST} or die("mkdir($artist): $!"); - $pid = fork(); - if (not defined($pid)) { - die("fork: $!"); - } elsif ($pid == 0) { - $SIG{CHLD} = 'IGNORE'; - open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!"); - exit(flac($dir)); + if (not $single_file) { + $outdir = "$artist/$album"; + verbose("mkdir($outdir)\n"); + mkdir("$outdir") or $!{EEXIST} or die("mkdir($outdir): $!"); } - verbose("new job $pid for $dir\n"); - return $pid; -} - -sub deljob { - my $i = shift; - my $j; - my $pid; - my $status; + verbose("chdir($dir)\n"); + chdir($dir) or die("chdir($dir): $!"); - $pid = $finished[$i][0]; - $status = $finished[$i][1]; + 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; + } - verbose("$pid finished ("); - if (WIFEXITED($status)) { - verbose('exited ', WEXITSTATUS($status)); - } elsif (WIFSIGNALED($status)) { - verbose('signalled ', WTERMSIG($status)); - } elsif (WIFSTOPPED($status)) { - verbose('stopped ', WSTOPSIG($status)); + 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(")\n"); - for ($j = 0; $j <= $#jobs; $j++) { - $pid == $jobs[$j] and splice(@jobs, $j, 1) and last; + 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): $!"); } - splice(@finished, $i, 1); + # 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 $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: { - my %opts; - - $opts{'j'} = 4; - $opts{'v'} = 0; - if (not getopts('j:v', \%opts)) { - print("usage: flacd [-jN -v]\n"); - exit(2); - } - - $verbose = $opts{'v'}; - - flacloop($opts{'j'}); + 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); } @@ -215,29 +426,23 @@ __END__ =head1 DESCRIPTION -B, B, and B together comprise -B, a system for archiving audio CDs to single FLAC -files. B is the guts of the system. It runs in the -directory where the audio archives are stored, scanning for new CDs to -encode and rename; it never exits. B generates the inputs for -B: the ripped WAV file, Vorbis tags, and a cuesheet. -B is not meant to be run directly; B uses it to -generate the candidate Vorbis tags. - -All three programs expect to be run from the same directory. They use -that directory to manage directories named by artist and by disc ID. -Intermediate files are written to the disc ID directory. B -processes the disc ID directories into per-album files in the artist -directories. - -=head2 FA-FLACD - -B does not exit; it runs until the user kills it. Every 5 -seconds it scans its current directory for directories with a file -called "tags" and creates a processing job for each one. The number -of jobs B attempts to run is controlled by the B<-j> option -and defaults to 4. B will print diagnostic output when the -B<-v> option is given. +B and B together comprise B, a system +for archiving audio CDs to single FLAC files. B is the guts +of the system. It runs in the directory where the audio archives are +stored, scanning for new ripped CDs to encode and rename; it never +exits. B generates the inputs for B: the ripped WAV +file, Vorbis tags, and a cuesheet. + +Both programs expect to be run from the same directory. They use that +directory to manage directories named by artist. Intermediate files +are written to temporary directories here. B processes the +temporary directories into per-album files in the artist directories. + +Every 5 seconds, B scans its current directory for +directories with a file called "tags" and creates a processing job for +each one. The number of jobs B attempts to run is +controlled by the B<-j> option and defaults to 4. B will +print diagnostic output when the B<-v> option is given. A processing job first renames the directory's "tags" file to "using-tags" so that B will not try to start another job for @@ -246,65 +451,41 @@ so a new job will not be started until the user corrects the error condition and renames "using-tags" back to "tags". Next, it encodes the "wav" file to a FLAC file, using the "cue" file for the cuesheet and "using-tags" for Vorbis tags. Any diagnostic output is saved in -the "log" file. Finally, the "cue" and "log" files are moved to the -artist directory (and named by album) and the ID directory is removed. - -=head2 FA-RIP - -B uses C to retrieve the disc ID and track -information. It creates a directory named by ID for storage of its -intermediate files. It passes the C output as -command-line arguments to B in the background. It then uses -C to create the "cue" file in the background. Finally, it -execs C to rip the CD to the "wav" file. - -In order for this CD to be processed by B, the user must -create a "tags" file. This is usually done by renaming one of the -candidate-tags files and deleting the others. - -=head2 FA-TAGS - -B uses C (from the B package) to -populate candidate-tags files. These are numbered in the order of -entries read from CDDB, e.g. candidate-tags-1, candidate-tags-2, etc. -B also creates candidate-tags-0, which has the correct fields -for this CD (including correct number of TITLE= lines), but with all -fields blank. - -B expects the output of C as command-line -arguments. That is, the disc ID, number of tracks, list of track -offsets, and total length of the CD in seconds. - -=head1 ENVIRONMENT +the "log" file. Finally, B moves the "cue" and "log" files +to the artist directory (named by album) and removes the temporary +directory. + +If the temporary directory contains an executable file named +"post-processor", B executes that file with the relative +path to the output FLAC file as an argument. The output files are in +their final location when "post-processor" starts. Possible uses are +running B, moving the output files to a different location, +removing the lock file, or adding to a database. The standard input, +output, and error streams are inherited from B, so they may +be connected to anything from a tty to /dev/null. This means that you +may want to redirect these streams, if you want to save them or do any +logging. + +=head1 OPTIONS =over 4 -=item CDDBURL +=item B<-j> [B<--jobs>] I -B uses this to retrieve candidate Vorbis tags. Defaults to -"http://freedb.freedb.org/~cddb/cddb.cgi". +Run up to I jobs instead of the default 4. -=item CDDEV +=item B<-v> [B<--verbose>] -B uses this to rip audio and save the cuesheet for a CD. It -makes some effort to check some common device names for FreeBSD, -Linux, and NetBSD by default. +Print diagnostic information. =back =head1 AUTHORS -Written by Eric Gillespie . B contains -code from B, which bears the following notice: - -# Copyright (c) 1998-2001 Robert Woodcock -# Copyright (c) 2003-2004 Jesus Climent -# This code is hereby licensed for public consumption under either the -# GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice. +Written by Eric Gillespie . -B is hereby licensed for public consumption under either -the GNU GPL v2 or greater, or Larry Wall's Artistic license - your -choice. +flac-archive is free software; you may redistribute it and/or modify +it under the same terms as Perl itself. =cut