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