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