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