]> diplodocus.org Git - flac-archive/blob - fa-flacd
UNTESTED: Refactor the job parallelization stuff into a generic Jobs package.
[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 $SIG{CHLD} = \&reaper;
288 while (1) {
289 if (scalar(@jobs) <= $MAXJOBS) {
290 foreach $i (glob('*/tags')) {
291 my $dir = dirname($i);
292 my $pid =
293 Jobs::newjob(sub {
294 open(STDERR, ">$dir/log")
295 or die("open(STDERR, >$dir/log): $!");
296 return flac($dir);
297 }, 'debug'=>$debug);
298 verbose("new job $pid for $dir\n");
299 @Jobs::jobs <= $MAXJOBS or last;
300 }
301 }
302
303 for ($i = 0; $i <= $#finished; $i++) {
304 my ($pid, $status) = Jobs::deljob($i);
305 verbose("$pid finished (");
306 if (WIFEXITED($status)) {
307 verbose('exited with status ', WEXITSTATUS($status));
308 } elsif (WIFSIGNALED($status)) {
309 verbose('killed with signal ', WTERMSIG($status));
310 } elsif (WIFSTOPPED($status)) {
311 verbose('stopped with signal ', WSTOPSIG($status));
312 }
313 verbose(")\n");
314 }
315
316 verbose(scalar(@jobs), " jobs\n");
317 sleep(5);
318 }
319 }
320
321 MAIN: {
322 my $jobs;
323 my $help;
324
325 $jobs = 4;
326 GetOptions(
327 'debug|X' => \$debug,
328 'jobs|j=i' => \$jobs,
329 'verbose|v' => \$verbose,
330 'help|h|?' => \$help,
331 ) or pod2usage();
332 $help and pod2usage(-exitstatus=>0, -verbose=>1);
333
334 flacloop($jobs);
335 }
336
337 \f
338 __END__
339
340 =head1 DESCRIPTION
341
342 B<fa-flacd> and B<fa-rip> together comprise B<flac-archive>, a system
343 for archiving audio CDs to single FLAC files. B<fa-flacd> is the guts
344 of the system. It runs in the directory where the audio archives are
345 stored, scanning for new ripped CDs to encode and rename; it never
346 exits. B<fa-rip> generates the inputs for B<fa-flacd>: the ripped WAV
347 file, Vorbis tags, and a cuesheet.
348
349 Both programs expect to be run from the same directory. They use that
350 directory to manage directories named by artist. Intermediate files
351 are written to temporary directories here. B<fa-flacd> processes the
352 temporary directories into per-album files in the artist directories.
353
354 Every 5 seconds, B<fa-flacd> scans its current directory for
355 directories with a file called "tags" and creates a processing job for
356 each one. The number of jobs B<fa-flacd> attempts to run is
357 controlled by the B<-j> option and defaults to 4. B<fa-flacd> will
358 print diagnostic output when the B<-v> option is given.
359
360 A processing job first renames the directory's "tags" file to
361 "using-tags" so that B<ra-flacd> will not try to start another job for
362 this directory. This file is left as is when an error is encountered,
363 so a new job will not be started until the user corrects the error
364 condition and renames "using-tags" back to "tags". Next, it encodes
365 the "wav" file to a FLAC file, using the "cue" file for the cuesheet
366 and "using-tags" for Vorbis tags. Any diagnostic output is saved in
367 the "log" file. Finally, B<fa-flacd> moves the "cue" and "log" files
368 to the artist directory (named by album) and removes the temporary
369 directory.
370
371 If the temporary directory contains an executable file named
372 "post-processor", B<fa-flacd> executes that file with the relative
373 path to the output FLAC file as an argument. The output files are in
374 their final location when "post-processor" starts. Possible uses are
375 running B<flac2mp3>, moving the output files to a different location,
376 removing the lock file, or adding to a database. The standard input,
377 output, and error streams are inherited from B<fa-flacd>, so they may
378 be connected to anything from a tty to /dev/null. This means that you
379 may want to redirect these streams, if you want to save them or do any
380 logging.
381
382 =head1 OPTIONS
383
384 =over 4
385
386 =item B<-j> [B<--jobs>] I<jobs>
387
388 Run up to I<jobs> jobs instead of the default 4.
389
390 =item B<-v> [B<--verbose>]
391
392 Print diagnostic information.
393
394 =back
395
396 =head1 AUTHORS
397
398 Written by Eric Gillespie <epg@pretzelnet.org>.
399
400 flac-archive is free software; you may redistribute it and/or modify
401 it under the same terms as Perl itself.
402
403 =cut
404
405 # Local variables:
406 # cperl-indent-level: 4
407 # perl-indent-level: 4
408 # indent-tabs-mode: nil
409 # End:
410
411 # vi: set tabstop=4 expandtab: