#! /usr/bin/env perl # $Id$ =head1 NAME B, B, B - archive CDs to single FLAC files =head1 SYNOPSIS B [B<-jv>] B [B<-d> I] [B<-t> I] B I =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 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 @tags; verbose("Opening tags file $fn\n"); open(TAGS, $fn) or die("open($fn): $!"); while () { 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"); } } close(TAGS) or die("close($fn): $!"); return ($artist, $album, @tags); } # Process the fa-rip output in the directory DIR. sub flac { my $dir = shift; my $artist; my $album; my @tags; my $outfile; my $status; verbose("Renaming $dir/tags\n"); rename("$dir/tags", "$dir/using-tags") or die("rename($dir/tags, $dir/using-tags): $!"); ($artist, $album, @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): $!"); $outfile = "$album"; $outfile =~ s/\//_/g; verbose("Running flac\n"); $status = system('flac', '-o', "../$artist/$outfile.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('log', "../$artist/$outfile.log") or die("rename(log, ../$artist/$outfile.log): $!"); chdir('..') or die("chdir(..): $!"); rmdir($dir) or die("rmdir($dir): $!"); rename("$artist/$outfile.flac-tmp", "$artist/$outfile.flac") or die("rename($artist/$outfile.flac-tmp, $artist/$outfile.flac): $!"); 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; $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)); } 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 ', 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(STDERR "usage: flacd [-jN -v]\n"); exit(2); } $verbose = $opts{'v'}; flacloop($opts{'j'}); } __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. A processing job first renames the directory's "tags" file to "using-tags" so that B 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 uses C to create a directory for storage of its intermediate files. It uses C to create the "cue" file and then passes the number of tracks (from the "cue" file) as command-line arguments to B. If B<-t> I is specified, that number is used instead of counting tracks in the "cue" file, and is also passed to C to ensure that only that number of tracks is listed in the "cue" file. This is handy if the CD has stupid data tracks. 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 to populate candidate-tags files. These are numbered in the order of entries read from MusicBrainz, 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 requires the number of tracks as its sole argument. =head1 ENVIRONMENT =over 4 =item CDDEV 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. =back =head1 AUTHORS Written by Eric Gillespie . flac-archive is free software; you may redistribute it and/or modify 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: