#! /usr/bin/env perl # $Id$ =head1 NAME B, B, B - archive CDs to single FLAC files =head1 SYNOPSIS B [B<-jv>] B B 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 () { 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'}); } __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 L 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 output as command-line arguments to B in the background. It then uses L to create the "cue" file in the background. Finally, it execs > 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 L (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 L 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 uses this to retrieve candidate Vorbis tags. Defaults to "http://freedb.freedb.org/~cddb/cddb.cgi". =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 . 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. B 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: