]> diplodocus.org Git - flac-archive/blob - fa-flacd
(flacloop): Oops, remove leftover $SIG{CHLD} setting (still UNTESTED).
[flac-archive] / fa-flacd
1 #! /usr/bin/env perl
2
3 # $Id$
4 # $URL$
5
6 =head1 NAME
7
8 B<fa-flacd> - archive CDs to single FLAC files
9
10 =head1 SYNOPSIS
11
12 B<fa-flacd> [B<-j> I<jobs>] [B<-v>]
13
14 =cut
15
16 package Jobs;
17
18 use strict;
19 use warnings;
20
21 use POSIX ':sys_wait_h';
22
23 our @jobs;
24 our @finished;
25
26 sub reaper {
27 while ((my $pid = waitpid(-1, WNOHANG)) > 0) {
28 push(@finished, [$pid, $?]);
29 }
30
31 # XXX if $pid == -1 handle errors?
32
33 $SIG{CHLD} = \&reaper;
34 }
35
36 sub newjob {
37 my $f = shift;
38 my %o = @_;
39 my $pid;
40
41 $SIG{CHLD} = \&reaper;
42 if (not $o{'debug'}) {
43 $pid = fork();
44 if (not defined($pid)) {
45 die("fork: $!");
46 }
47 }
48
49 if ($o{'debug'} or $pid == 0) {
50 $SIG{CHLD} = 'DEFAULT';
51 exit($f->());
52 }
53
54 push(@jobs, $pid);
55
56 return $pid;
57 }
58
59 sub deljob {
60 my $i = shift;
61 my $j;
62
63 my ($pid, $status) = @{$finished[$i]};
64
65 for ($j = 0; $j <= $#jobs; $j++) {
66 $pid == $jobs[$j] and splice(@jobs, $j, 1) and last;
67 }
68
69 splice(@finished, $i, 1);
70
71 return ($pid, $status);
72 }
73
74 \f
75 ################################################################################
76 package main;
77
78 use strict;
79 use warnings;
80
81 use File::Basename;
82 use Getopt::Long qw(:config gnu_getopt no_ignore_case);
83 use POSIX ':sys_wait_h';
84 use Pod::Usage;
85
86 my $debug;
87 my $verbose;
88
89 sub verbose {
90 $verbose and print(STDERR $_) for @_;
91 }
92
93 # Return the ARTIST, ALBUM, and DATE followed by a list of all the
94 # lines in the file FN.
95 sub get_tags {
96 my $fn = shift;
97 my $tag;
98 my $value;
99 my $artist;
100 my $album;
101 my $discnum;
102 my @tags;
103
104 verbose("Opening tags file $fn\n");
105 open(TAGS, $fn) or die("open($fn): $!");
106 while (<TAGS>) {
107 chomp;
108 push(@tags, $_);
109
110 ($tag, $value) = split(/=/, $_, 2);
111
112 if (/^ARTIST=/i) {
113 $artist = $value;
114 verbose("ARTIST $artist from $fn\n");
115 } elsif (/^ALBUM=/i) {
116 $album = $value;
117 verbose("ALBUM $album from $fn\n"); # cperl-mode sucks "
118 } elsif (/^DISCNUMBER=/i) {
119 $discnum = int($value);
120 verbose("DISCNUMBER $discnum from $fn\n");
121 }
122 }
123 close(TAGS) or die("close($fn): $!");
124
125 return ($artist, $album, $discnum, @tags);
126 }
127
128 sub track_tags {
129 my $h = shift;
130 my @result;
131
132 while (my ($key, $vall) = each(%$h)) {
133 for my $val (@$vall) {
134 push(@result, "$key=$val")
135 }
136 }
137
138 return @result;
139 }
140
141 sub run_flac {
142 my $infile = shift;
143 my $cue = shift;
144 my $outfile = shift;
145
146 my @cue;
147 if (defined($cue)) {
148 @cue = ('--cuesheet', $cue);
149 }
150
151 verbose("Running flac\n");
152 my $status = system('flac', '-o', "$outfile.flac-tmp",
153 '--delete-input-file', '-V', '--no-padding', '--best',
154 @cue,
155 map({ ('-T', $_) } @_),
156 $infile);
157 if (WIFEXITED($status)) {
158 if (($status = WEXITSTATUS($status)) != 0) {
159 die("flac exited with status $status");
160 }
161 } elsif (WIFSIGNALED($status)) {
162 die("flac killed with signal ", WTERMSIG($status));
163 } elsif (WIFSTOPPED($status)) {
164 die("flac stopped with signal ", WSTOPSIG($status));
165 } else {
166 die("Major horkage on system(flac): \$? = $? \$! = $!");
167 }
168
169 rename("$outfile.flac-tmp", "$outfile.flac")
170 or die("rename($outfile.flac-tmp, $outfile.flac): $!");
171 }
172
173 # Process the fa-rip output in the directory DIR.
174 sub flac {
175 my $dir = shift;
176 my $artist;
177 my $album;
178 my $discnum;
179 my @tags;
180 my $single_file = -e "$dir/wav";
181 my $outdir;
182 my $outfile;
183 my $outlog;
184 my @files;
185
186 verbose("Renaming $dir/tags\n");
187 rename("$dir/tags", "$dir/using-tags")
188 or die("rename($dir/tags, $dir/using-tags): $!");
189
190 ($artist, $album, $discnum, @tags) = get_tags("$dir/using-tags");
191 for ($artist, $album) {
192 s|/|_|g;
193 }
194
195 verbose("mkdir($artist)\n");
196 -d $artist or mkdir($artist) or die("mkdir($artist): $!");
197
198 if (not $single_file) {
199 $outdir = "$artist/$album";
200 verbose("mkdir($outdir)\n");
201 -d "$outdir" or mkdir("$outdir") or die("mkdir($outdir): $!");
202 }
203
204 verbose("chdir($dir)\n");
205 chdir($dir) or die("chdir($dir): $!");
206
207 if ($single_file) {
208 $outfile = $album;
209 defined($discnum) and $outfile .= " (disc $discnum)";
210 run_flac('wav', 'cue', "../$artist/$outfile", @tags);
211 $outlog = "../$artist/$outfile.log";
212 @files = ("$artist/$outfile.flac");
213 } else {
214 # Go over @tags, store all [n] tags in a list keyed by n in
215 # %tracks_to_tags, store all ARTIST (not ARTIST[n]) tags in
216 # @disc_artist, and leave the rest in @tags.
217 my %tracks_to_tags;
218 my @disc_artist;
219 my @tmp;
220 for my $tag (@tags) {
221 if ($tag =~ /^([^[]+)\[(\d+)]=(.*)/) {
222 push(@{$tracks_to_tags{$2}->{$1}}, $3);
223 } elsif ($tag =~ /^ARTIST=/) {
224 push(@disc_artist, $tag);
225 } else {
226 push(@tmp, $tag);
227 }
228 }
229 @tags = @tmp;
230
231 for my $tracknum (sort(map(int, keys(%tracks_to_tags)))) {
232 my $title = join(' ', map(split, @{$tracks_to_tags{$tracknum}->{'TITLE'}}));
233 $title =~ s|/|_|g;
234 $outfile = join('/',
235 $outdir,
236 join(' ',
237 (defined($discnum)
238 ? sprintf('%02d', $discnum)
239 : ()),
240 sprintf('%02d', $tracknum),
241 $title));
242
243 # If we have ARTIST[n] tags for this track, set
244 # @track_artist to the empty list; they will go in along
245 # with the other [n] tags.
246 my @track_artist;
247 if (exists($tracks_to_tags{$tracknum}->{'ARTIST'})) {
248 @track_artist = ();
249 } else {
250 @track_artist = @disc_artist;
251 }
252
253 run_flac(sprintf('track%02d.cdda.wav', $tracknum), undef,
254 "../$outfile",
255 @track_artist,
256 @tags,
257 "TRACKNUMBER=$tracknum",
258 track_tags($tracks_to_tags{$tracknum}));
259 push(@files, "$outfile.flac");
260 }
261 $outlog = "../$outdir/log";
262 }
263
264 verbose("Cleaning up $dir\n");
265 unlink('using-tags') or die("unlink(using-tags): $!");
266 unlink('cue') or die("unlink(cue): $!");
267 rename('log', $outlog)
268 or die("rename(log, $outlog): $!");
269 chdir('..') or die("chdir(..): $!");
270
271 if (-x "$dir/post-processor") {
272 verbose(join(' ', "Running ./$dir/post-processor", @files), "\n");
273 system("./$dir/post-processor", @files);
274 unlink("$dir/post-processor") or die("unlink($dir/post-processor): $!");
275 }
276
277 rmdir($dir) or die("rmdir($dir): $!");
278
279 return 0;
280 }
281
282 sub flacloop {
283 my $MAXJOBS = shift;
284 my $i;
285
286
287 while (1) {
288 if (scalar(@jobs) <= $MAXJOBS) {
289 foreach $i (glob('*/tags')) {
290 my $dir = dirname($i);
291 my $pid =
292 Jobs::newjob(sub {
293 open(STDERR, ">$dir/log")
294 or die("open(STDERR, >$dir/log): $!");
295 return flac($dir);
296 }, 'debug'=>$debug);
297 verbose("new job $pid for $dir\n");
298 @Jobs::jobs <= $MAXJOBS or last;
299 }
300 }
301
302 for ($i = 0; $i <= $#finished; $i++) {
303 my ($pid, $status) = Jobs::deljob($i);
304 verbose("$pid finished (");
305 if (WIFEXITED($status)) {
306 verbose('exited with status ', WEXITSTATUS($status));
307 } elsif (WIFSIGNALED($status)) {
308 verbose('killed with signal ', WTERMSIG($status));
309 } elsif (WIFSTOPPED($status)) {
310 verbose('stopped with signal ', WSTOPSIG($status));
311 }
312 verbose(")\n");
313 }
314
315 verbose(scalar(@jobs), " jobs\n");
316 sleep(5);
317 }
318 }
319
320 MAIN: {
321 my $jobs;
322 my $help;
323
324 $jobs = 4;
325 GetOptions(
326 'debug|X' => \$debug,
327 'jobs|j=i' => \$jobs,
328 'verbose|v' => \$verbose,
329 'help|h|?' => \$help,
330 ) or pod2usage();
331 $help and pod2usage(-exitstatus=>0, -verbose=>1);
332
333 flacloop($jobs);
334 }
335
336 \f
337 __END__
338
339 =head1 DESCRIPTION
340
341 B<fa-flacd> and B<fa-rip> together comprise B<flac-archive>, a system
342 for archiving audio CDs to single FLAC files. B<fa-flacd> is the guts
343 of the system. It runs in the directory where the audio archives are
344 stored, scanning for new ripped CDs to encode and rename; it never
345 exits. B<fa-rip> generates the inputs for B<fa-flacd>: the ripped WAV
346 file, Vorbis tags, and a cuesheet.
347
348 Both programs expect to be run from the same directory. They use that
349 directory to manage directories named by artist. Intermediate files
350 are written to temporary directories here. B<fa-flacd> processes the
351 temporary directories into per-album files in the artist directories.
352
353 Every 5 seconds, B<fa-flacd> scans its current directory for
354 directories with a file called "tags" and creates a processing job for
355 each one. The number of jobs B<fa-flacd> attempts to run is
356 controlled by the B<-j> option and defaults to 4. B<fa-flacd> will
357 print diagnostic output when the B<-v> option is given.
358
359 A processing job first renames the directory's "tags" file to
360 "using-tags" so that B<ra-flacd> will not try to start another job for
361 this directory. This file is left as is when an error is encountered,
362 so a new job will not be started until the user corrects the error
363 condition and renames "using-tags" back to "tags". Next, it encodes
364 the "wav" file to a FLAC file, using the "cue" file for the cuesheet
365 and "using-tags" for Vorbis tags. Any diagnostic output is saved in
366 the "log" file. Finally, B<fa-flacd> moves the "cue" and "log" files
367 to the artist directory (named by album) and removes the temporary
368 directory.
369
370 If the temporary directory contains an executable file named
371 "post-processor", B<fa-flacd> executes that file with the relative
372 path to the output FLAC file as an argument. The output files are in
373 their final location when "post-processor" starts. Possible uses are
374 running B<flac2mp3>, moving the output files to a different location,
375 removing the lock file, or adding to a database. The standard input,
376 output, and error streams are inherited from B<fa-flacd>, so they may
377 be connected to anything from a tty to /dev/null. This means that you
378 may want to redirect these streams, if you want to save them or do any
379 logging.
380
381 =head1 OPTIONS
382
383 =over 4
384
385 =item B<-j> [B<--jobs>] I<jobs>
386
387 Run up to I<jobs> jobs instead of the default 4.
388
389 =item B<-v> [B<--verbose>]
390
391 Print diagnostic information.
392
393 =back
394
395 =head1 AUTHORS
396
397 Written by Eric Gillespie <epg@pretzelnet.org>.
398
399 flac-archive is free software; you may redistribute it and/or modify
400 it under the same terms as Perl itself.
401
402 =cut
403
404 # Local variables:
405 # cperl-indent-level: 4
406 # perl-indent-level: 4
407 # indent-tabs-mode: nil
408 # End:
409
410 # vi: set tabstop=4 expandtab: