]> diplodocus.org Git - flac-archive/blob - fa-flacd
Support multi- and single- file modes.
[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 $single_file = -e "$dir/wav";
121 my $outdir;
122 my $outfile;
123 my $outlog;
124 my @files;
125
126 verbose("Renaming $dir/tags\n");
127 rename("$dir/tags", "$dir/using-tags")
128 or die("rename($dir/tags, $dir/using-tags): $!");
129
130 ($artist, $album, $discnum, @tags) = get_tags("$dir/using-tags");
131 for ($artist, $album) {
132 s|/|_|g;
133 }
134
135 verbose("mkdir($artist)\n");
136 -d $artist or mkdir($artist) or die("mkdir($artist): $!");
137
138 if (not $single_file) {
139 $outdir = "$artist/$album";
140 verbose("mkdir($outdir)\n");
141 -d "$outdir" or mkdir("$outdir") or die("mkdir($outdir): $!");
142 }
143
144 verbose("chdir($dir)\n");
145 chdir($dir) or die("chdir($dir): $!");
146
147 if ($single_file) {
148 $outfile = $album;
149 defined($discnum) and $outfile .= " (disc $discnum)";
150 run_flac('wav', 'cue', "../$artist/$outfile", @tags);
151 $outlog = "../$artist/$outfile.log";
152 @files = ("$artist/$outfile.flac");
153 } else {
154 my %things;
155 my @artist;
156 my @bork;
157 for my $tag (@tags) {
158 if ($tag =~ /^([^[]+)\[(\d+)]=(.*)/) {
159 push(@{$things{$2}->{$1}}, $3);
160 } elsif ($tag =~ /^ARTIST=/) {
161 push(@artist, $tag);
162 } else {
163 push(@bork, $tag);
164 }
165 }
166 @tags = @bork;
167
168 for my $tracknum (sort(map(int, keys(%things)))) {
169 my $title = join(' ', map(split, @{$things{$tracknum}->{'TITLE'}}));
170 $title =~ s|/|_|g;
171 $outfile = join(' ',
172 (defined($discnum)
173 ? sprintf('%02d', $discnum)
174 : ()),
175 sprintf('%02d', $tracknum),
176 $title);
177 push(@files, "$outdir/$outfile.flac");
178 $outfile = "../$outdir/$outfile";
179
180 my @lartist;
181 if (exists($things{$tracknum}->{'ARTIST'})) {
182 @lartist = ();
183 } else {
184 @lartist = @artist;
185 }
186
187 run_flac(sprintf('track%02d.cdda.wav', $tracknum), undef, $outfile,
188 @lartist,
189 grep({ $_ !~ /^ARTIST=/ } @tags),
190 bork_tags($things{$tracknum}));
191 $outlog = "../$outdir/log";
192 }
193 }
194
195 verbose("Cleaning up $dir\n");
196 unlink('using-tags') or die("unlink(using-tags): $!");
197 unlink('cue') or die("unlink(cue): $!");
198 rename('log', $outlog)
199 or die("rename(log, $outlog): $!");
200 chdir('..') or die("chdir(..): $!");
201
202 if (-x "$dir/post-processor") {
203 verbose(join(' ', 'Running', @files), "\n");
204 system("./$dir/post-processor", @files);
205 unlink("$dir/post-processor") or die("unlink($dir/post-processor): $!");
206 }
207
208 rmdir($dir) or die("rmdir($dir): $!");
209
210 return 0;
211 }
212
213 sub reaper {
214 my $pid;
215
216 while (($pid = waitpid(-1, WNOHANG)) > 0) {
217 push(@finished, [$pid, $?]);
218 }
219
220 $SIG{CHLD} = \&reaper;
221 }
222
223 sub newjob {
224 my $dir = shift;
225 my $pid;
226
227 if (not $debug) {
228 $pid = fork();
229 if (not defined($pid)) {
230 die("fork: $!");
231 }
232 }
233
234 if ($debug or $pid == 0) {
235 $SIG{CHLD} = 'DEFAULT';
236 open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!");
237 exit(flac($dir));
238 }
239
240 verbose("new job $pid for $dir\n");
241 return $pid;
242 }
243
244 sub deljob {
245 my $i = shift;
246 my $j;
247 my $pid;
248 my $status;
249
250 $pid = $finished[$i][0];
251 $status = $finished[$i][1];
252
253 verbose("$pid finished (");
254 if (WIFEXITED($status)) {
255 verbose('exited with status ', WEXITSTATUS($status));
256 } elsif (WIFSIGNALED($status)) {
257 verbose('killed with signal ', WTERMSIG($status));
258 } elsif (WIFSTOPPED($status)) {
259 verbose('stopped with signal ', WSTOPSIG($status));
260 }
261 verbose(")\n");
262
263 for ($j = 0; $j <= $#jobs; $j++) {
264 $pid == $jobs[$j] and splice(@jobs, $j, 1) and last;
265 }
266
267 splice(@finished, $i, 1);
268 }
269
270 sub flacloop {
271 my $MAXJOBS = shift;
272 my $i;
273 my $j;
274
275
276 $SIG{CHLD} = \&reaper;
277 while (1) {
278 if (scalar(@jobs) <= $MAXJOBS) {
279 foreach $i (glob('*/tags')) {
280 push(@jobs, newjob(dirname($i))) <= $MAXJOBS or last;
281 }
282 }
283
284 for ($i = 0; $i <= $#finished; $i++) {
285 deljob($i);
286 }
287
288 verbose(scalar(@jobs), " jobs\n");
289 sleep(5);
290 }
291 }
292
293 MAIN: {
294 my $jobs;
295 my $help;
296
297 $jobs = 4;
298 GetOptions(
299 'debug|X' => \$debug,
300 'jobs|j=i' => \$jobs,
301 'verbose|v' => \$verbose,
302 'help|h|?' => \$help,
303 ) or pod2usage();
304 $help and pod2usage(-exitstatus=>0, -verbose=>1);
305
306 flacloop($jobs);
307 }
308
309 \f
310 __END__
311
312 =head1 DESCRIPTION
313
314 B<fa-flacd> and B<fa-rip> together comprise B<flac-archive>, a system
315 for archiving audio CDs to single FLAC files. B<fa-flacd> is the guts
316 of the system. It runs in the directory where the audio archives are
317 stored, scanning for new ripped CDs to encode and rename; it never
318 exits. B<fa-rip> generates the inputs for B<fa-flacd>: the ripped WAV
319 file, Vorbis tags, and a cuesheet.
320
321 Both programs expect to be run from the same directory. They use that
322 directory to manage directories named by artist. Intermediate files
323 are written to temporary directories here. B<fa-flacd> processes the
324 temporary directories into per-album files in the artist directories.
325
326 Every 5 seconds, B<fa-flacd> scans its current directory for
327 directories with a file called "tags" and creates a processing job for
328 each one. The number of jobs B<fa-flacd> attempts to run is
329 controlled by the B<-j> option and defaults to 4. B<fa-flacd> will
330 print diagnostic output when the B<-v> option is given.
331
332 A processing job first renames the directory's "tags" file to
333 "using-tags" so that B<ra-flacd> will not try to start another job for
334 this directory. This file is left as is when an error is encountered,
335 so a new job will not be started until the user corrects the error
336 condition and renames "using-tags" back to "tags". Next, it encodes
337 the "wav" file to a FLAC file, using the "cue" file for the cuesheet
338 and "using-tags" for Vorbis tags. Any diagnostic output is saved in
339 the "log" file. Finally, B<fa-flacd> moves the "cue" and "log" files
340 to the artist directory (named by album) and removes the temporary
341 directory.
342
343 If the temporary directory contains an executable file named
344 "post-processor", B<fa-flacd> executes that file with the relative
345 path to the output FLAC file as an argument. The output files are in
346 their final location when "post-processor" starts. Possible uses are
347 running B<flac2mp3>, moving the output files to a different location,
348 removing the lock file, or adding to a database. The standard input,
349 output, and error streams are inherited from B<fa-flacd>, so they may
350 be connected to anything from a tty to /dev/null. This means that you
351 may want to redirect these streams, if you want to save them or do any
352 logging.
353
354 =head1 OPTIONS
355
356 =over 4
357
358 =item B<-j> [B<--jobs>] I<jobs>
359
360 Run up to I<jobs> jobs instead of the default 4.
361
362 =item B<-v> [B<--verbose>]
363
364 Print diagnostic information.
365
366 =back
367
368 =head1 AUTHORS
369
370 Written by Eric Gillespie <epg@pretzelnet.org>.
371
372 flac-archive is free software; you may redistribute it and/or modify
373 it under the same terms as Perl itself.
374
375 =cut
376
377 # Local variables:
378 # cperl-indent-level: 4
379 # perl-indent-level: 4
380 # indent-tabs-mode: nil
381 # End:
382
383 # vi: set tabstop=4 expandtab: