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