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