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