+#! /usr/bin/env perl
+
+# $Id$
+
+=head1 NAME
+
+B<fa-flacd>, B<fa-rip>, B<fa-tags> - archive CDs to single FLAC files
+
+=head1 SYNOPSIS
+
+B<fa-flacd> [B<-jv>]
+
+B<fa-rip>
+
+B<fa-tags> ID TRACKCOUNT OFFSET [OFFSET ...] LENGTH
+
+=cut
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Getopt::Std; $Getopt::Std::STANDARD_HELP_VERSION = 1;
+use POSIX ':sys_wait_h';
+
+our $VERSION = 1;
+
+my $verbose;
+my @jobs;
+my @finished;
+
+sub verbose {
+ $verbose and map({ print(STDERR $_) } @_);
+}
+
+# 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 $date;
+ my @tags;
+
+ verbose("Opening tags file $fn\n");
+ open(TAGS, $fn) or die("open($fn): $!");
+ while (<TAGS>) {
+ chomp;
+ push(@tags, $_);
+
+ ($tag, $value) = split(/=/, $_, 2);
+
+ if (/^ARTIST=/) {
+ $artist = $value;
+ verbose("ARTIST $artist from $fn\n");
+ } elsif (/^ALBUM=/) {
+ $album = $value;
+ verbose("ALBUM $album from $fn\n");
+ } elsif (/^DATE=/) {
+ $date = $value;
+ verbose("DATE $date from $fn\n");
+ }
+ }
+ close(TAGS);
+
+ return ($artist, $album, $date, @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;
+
+ verbose("Renaming $dir/tags\n");
+ rename("$dir/tags", "$dir/using-tags")
+ or die("rename($dir/tags, $dir/using-tags): $!");
+
+ ($artist, $album, $date, @tags) = get_tags("$dir/using-tags");
+
+ verbose("mkdir($artist)\n");
+ -d $artist or mkdir($artist) or die("mkdir($artist): $!");
+
+ verbose("chdir($dir)\n");
+ chdir($dir) or die("chdir($dir): $!");
+
+ 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");
+ } elsif (WIFSIGNALED($status)) {
+ die("flac killed with signal ", WTERMSIG($status));
+ } elsif (WIFSTOPPED($status)) {
+ die("flac stopped with signal ", WSTOPSIG($status));
+ }
+
+ 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): $!");
+}
+
+sub reaper {
+ my $pid;
+
+ while (($pid = waitpid(0, WNOHANG)) > 0) {
+ push(@finished, [$pid, $?]);
+ }
+
+ $SIG{CHLD} = \&reaper;
+}
+
+sub newjob {
+ my $dir = shift;
+ my $pid;
+
+ $pid = fork();
+ if ($pid == -1) {
+ die("fork: $!");
+ } elsif ($pid == 0) {
+ $SIG{CHLD} = 'IGNORE';
+ open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!");
+ flac($dir);
+ exit(0);
+ }
+
+ 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 = WEXITSTATUS($finished[$i][1]);
+
+ verbose("$pid finished (");
+ if (WIFEXITED($status)) {
+ verbose('exited ', WEXITSTATUS($status));
+ } elsif (WIFSIGNALED($status)) {
+ verbose('signalled ', WTERMSIG($status));
+ } elsif (WIFSTOPPED($status)) {
+ verbose('stopped ', 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 %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'});
+}
+
+\f
+__END__
+
+=head1 DESCRIPTION
+
+B<fa-flacd>, B<fa-rip>, and B<fa-tags> together comprise
+B<flac-archive>, a system for archiving audio CDs to single FLAC
+files. B<fa-flacd> 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<fa-rip> generates the inputs for
+B<fa-flacd>: the ripped WAV file, Vorbis tags, and a cuesheet.
+B<fa-tags> is not meant to be run directly; B<fa-rip> 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<fa-flacd>
+processes the disc ID directories into per-album files in the artist
+directories.
+
+=head2 FA-FLACD
+
+B<fa-flacd> 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<fa-flacd> attempts to run is controlled by the B<-j> option
+and defaults to 4. B<fa-flacd> 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<ra-flacd> will not try to start another job for
+this directory. This file is left as is when an error is encountered,
+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<fa-rip> uses L<cd-discid(1)> to retrieve the disc ID and track
+information. It creates a directory named by ID for storage of its
+intermediate files. It passes the L<cd-discid(1)> output as
+command-line arguments to B<fa-tags> in the background. It then uses
+L<cdrdao(1)> to create the "cue" file in the background. Finally, it
+execs ><cdparanoia(1)> to rip the CD to the "wav" file.
+
+In order for this CD to be processed by B<fa-flacd>, 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<fa-tags> uses L<cddb-tool(1)> (from the B<abcde> 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<fa-tags> 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<fa-tags> expects the output of L<cd-discid(1)> 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
+
+=over 4
+
+=item CDDBURL
+
+B<fa-tags> uses this to retrieve candidate Vorbis tags. Defaults to
+"http://freedb.freedb.org/~cddb/cddb.cgi".
+
+=item CDDEV
+
+B<fa-rip> 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.
+
+=back
+
+=head1 AUTHORS
+
+Written by Eric Gillespie <epg@pretzelnet.org>. B<fa-tags> contains
+code from B<abcde>, which bears the following notice:
+
+# Copyright (c) 1998-2001 Robert Woodcock <rcw@debian.org>
+# Copyright (c) 2003-2004 Jesus Climent <jesus.climent@hispalinux.es>
+# This code is hereby licensed for public consumption under either the
+# GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice.
+
+B<flac-archive> is hereby licensed for public consumption under either
+the GNU GPL v2 or greater, or Larry Wall's Artistic license - your
+choice.
+
+=cut
+
+# Local variables:
+# cperl-indent-level: 4
+# perl-indent-level: 4
+# indent-tabs-mode: nil
+# End:
+
+# vi: set tabstop=4 expandtab: