#! /usr/bin/env perl # $Id$ # $URL$ =head1 NAME B - archive CDs to single FLAC files =head1 SYNOPSIS B [B<-j> I] [B<-v>] =cut use strict; use warnings; use File::Basename; use Getopt::Long qw(:config gnu_getopt no_ignore_case); use POSIX ':sys_wait_h'; use Pod::Usage; my $debug; 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 $discnum; 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=/i) { $artist = $value; verbose("ARTIST $artist from $fn\n"); } elsif (/^ALBUM=/i) { $album = $value; 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, $discnum, @tags); } sub track_tags { my $h = shift; my @result; while (my ($key, $vall) = each(%$h)) { for my $val (@$vall) { push(@result, "$key=$val") } } return @result; } 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): $!"); } # Process the fa-rip output in the directory DIR. sub flac { my $dir = shift; my $artist; my $album; my $discnum; my @tags; my $single_file = -e "$dir/wav"; my $outdir; my $outfile; my $outlog; my @files; verbose("Renaming $dir/tags\n"); rename("$dir/tags", "$dir/using-tags") or die("rename($dir/tags, $dir/using-tags): $!"); ($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): $!"); if (not $single_file) { $outdir = "$artist/$album"; verbose("mkdir($outdir)\n"); -d "$outdir" or mkdir("$outdir") or die("mkdir($outdir): $!"); } verbose("chdir($dir)\n"); chdir($dir) or die("chdir($dir): $!"); 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"); } else { # 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; for my $tracknum (sort(map(int, keys(%tracks_to_tags)))) { my $title = join(' ', map(split, @{$tracks_to_tags{$tracknum}->{'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($tracks_to_tags{$tracknum}->{'ARTIST'})) { @track_artist = (); } else { @track_artist = @disc_artist; } run_flac(sprintf('track%02d.cdda.wav', $tracknum), undef, "../$outfile", @track_artist, @tags, track_tags($tracks_to_tags{$tracknum})); push(@files, "$outfile.flac"); } $outlog = "../$outdir/log"; } verbose("Cleaning up $dir\n"); unlink('using-tags') or die("unlink(using-tags): $!"); unlink('cue') or die("unlink(cue): $!"); rename('log', $outlog) or die("rename(log, $outlog): $!"); 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): $!"); } rmdir($dir) or 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; $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 $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); } __END__ =head1 DESCRIPTION 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 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, 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 B<-j> [B<--jobs>] I Run up to I jobs instead of the default 4. =item B<-v> [B<--verbose>] Print diagnostic information. =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: