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