]> diplodocus.org Git - flac-archive/blob - fa-flacd
(flac): Whoops, need to use sanitized version in some more places.
[flac-archive] / fa-flacd
1 #! /usr/bin/env perl
2
3 # $Id$
4
5 =head1 NAME
6
7 B<fa-flacd>, B<fa-rip>, B<fa-tags> - archive CDs to single FLAC files
8
9 =head1 SYNOPSIS
10
11 B<fa-flacd> [B<-jv>]
12
13 B<fa-rip>
14
15 B<fa-tags> I<track-count>
16
17 =cut
18
19 use strict;
20 use warnings;
21
22 use File::Basename;
23 use Getopt::Std; $Getopt::Std::STANDARD_HELP_VERSION = 1;
24 use POSIX ':sys_wait_h';
25
26 our $VERSION = 1;
27
28 my $verbose;
29 my @jobs;
30 my @finished;
31
32 sub verbose {
33 $verbose and print(STDERR $_) for @_;
34 }
35
36 # Return the ARTIST, ALBUM, and DATE followed by a list of all the
37 # lines in the file FN.
38 sub get_tags {
39 my $fn = shift;
40 my $tag;
41 my $value;
42 my $artist;
43 my $album;
44 my @tags;
45
46 verbose("Opening tags file $fn\n");
47 open(TAGS, $fn) or die("open($fn): $!");
48 while (<TAGS>) {
49 chomp;
50 push(@tags, $_);
51
52 ($tag, $value) = split(/=/, $_, 2);
53
54 if (/^ARTIST=/) {
55 $artist = $value;
56 verbose("ARTIST $artist from $fn\n");
57 } elsif (/^ALBUM=/) {
58 $album = $value;
59 verbose("ALBUM $album from $fn\n");
60 }
61 }
62 close(TAGS) or die("close($fn): $!");
63
64 return ($artist, $album, @tags);
65 }
66
67 # Process the fa-rip output in the directory DIR.
68 sub flac {
69 my $dir = shift;
70 my $artist;
71 my $album;
72 my @tags;
73 my $outfile;
74 my $status;
75
76 verbose("Renaming $dir/tags\n");
77 rename("$dir/tags", "$dir/using-tags")
78 or die("rename($dir/tags, $dir/using-tags): $!");
79
80 ($artist, $album, @tags) = get_tags("$dir/using-tags");
81
82 verbose("mkdir($artist)\n");
83 -d $artist or mkdir($artist) or die("mkdir($artist): $!");
84
85 verbose("chdir($dir)\n");
86 chdir($dir) or die("chdir($dir): $!");
87
88 $outfile = "$album";
89 for ($outfile) {
90 s/\//_/g;
91 s/:/_/g;
92 s/'/_/g;
93 s/"/_/g;
94 }
95
96 verbose("Running flac\n");
97 $status = system('flac', '-o', "../$artist/$outfile.flac-tmp",
98 '--delete-input-file', '-V', '--cuesheet',
99 'cue', '--no-padding', '--best',
100 map({ ('-T', $_) } @tags),
101 'wav');
102 if (WIFEXITED($status) and ($status = WEXITSTATUS($status)) != 0) {
103 die("flac: $status");
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 }
109
110 verbose("Cleaning up $dir\n");
111 unlink('using-tags') or die("unlink(using-tags): $!");
112 unlink('cue') or die("unlink(cue): $!");
113 rename('toc', "../$artist/$outfile.toc")
114 or die("rename(toc, ../$artist/$outfile.toc): $!");
115 rename('log', "../$artist/$outfile.log")
116 or die("rename(log, ../$artist/$outfile.log): $!");
117 chdir('..') or die("chdir(..): $!");
118 rmdir($dir) or die("rmdir($dir): $!");
119
120 rename("$artist/$outfile.flac-tmp", "$artist/$outfile.flac")
121 or die("rename($artist/$outfile.flac-tmp, $artist/$outfile.flac): $!");
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 ', WEXITSTATUS($status));
165 } elsif (WIFSIGNALED($status)) {
166 verbose('signalled ', WTERMSIG($status));
167 } elsif (WIFSTOPPED($status)) {
168 verbose('stopped ', 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 %opts;
204
205 $opts{'j'} = 4;
206 $opts{'v'} = 0;
207 if (not getopts('j:v', \%opts)) {
208 print(STDERR "usage: flacd [-jN -v]\n");
209 exit(2);
210 }
211
212 $verbose = $opts{'v'};
213
214 flacloop($opts{'j'});
215 }
216
217 \f
218 __END__
219
220 =head1 DESCRIPTION
221
222 B<fa-flacd>, B<fa-rip>, and B<fa-tags> together comprise
223 B<flac-archive>, a system for archiving audio CDs to single FLAC
224 files. B<fa-flacd> is the guts of the system. It runs in the
225 directory where the audio archives are stored, scanning for new CDs to
226 encode and rename; it never exits. B<fa-rip> generates the inputs for
227 B<fa-flacd>: the ripped WAV file, Vorbis tags, and a cuesheet.
228 B<fa-tags> is not meant to be run directly; B<fa-rip> uses it to
229 generate the candidate Vorbis tags.
230
231 All three programs expect to be run from the same directory. They use
232 that directory to manage directories named by artist and by disc ID.
233 Intermediate files are written to the disc ID directory. B<fa-flacd>
234 processes the disc ID directories into per-album files in the artist
235 directories.
236
237 =head2 FA-FLACD
238
239 B<fa-flacd> does not exit; it runs until the user kills it. Every 5
240 seconds it scans its current directory for directories with a file
241 called "tags" and creates a processing job for each one. The number
242 of jobs B<fa-flacd> attempts to run is controlled by the B<-j> option
243 and defaults to 4. B<fa-flacd> will print diagnostic output when the
244 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, the "cue" and "log" files are moved to the
254 artist directory (and named by album) and the ID directory is removed.
255
256 =head2 FA-RIP
257
258 B<fa-rip> uses C<mktemp(1)> to create a directory for storage of its
259 intermediate files. It uses C<cdrdao(1)> to create the "cue" file and
260 then passes the number of tracks (from the "cue" file) as command-line
261 arguments to B<fa-tags>. Finally, it execs C<cdparanoia(1)> to rip
262 the CD to the "wav" file.
263
264 In order for this CD to be processed by B<fa-flacd>, the user must
265 create a "tags" file. This is usually done by renaming one of the
266 candidate-tags files and deleting the others.
267
268 =head2 FA-TAGS
269
270 B<fa-tags> uses C<MusicBrainz::Client> to populate candidate-tags
271 files. These are numbered in the order of entries read from
272 MusicBrainz, e.g. candidate-tags-1, candidate-tags-2, etc. B<fa-tags>
273 also creates candidate-tags-0, which has the correct fields for this
274 CD (including correct number of TITLE= lines), but with all fields
275 blank.
276
277 B<fa-tags> requires the number of tracks as its sole argument.
278
279 =head1 ENVIRONMENT
280
281 =over 4
282
283 =item CDDEV
284
285 B<fa-rip> uses this to rip audio and save the cuesheet for a CD. It
286 makes some effort to check some common device names for FreeBSD,
287 Linux, and NetBSD by default.
288
289 =back
290
291 =head1 AUTHORS
292
293 Written by Eric Gillespie <epg@pretzelnet.org>.
294
295 flac-archive is free software; you may redistribute it and/or modify
296 it under the same terms as Perl itself.
297
298 =cut
299
300 # Local variables:
301 # cperl-indent-level: 4
302 # perl-indent-level: 4
303 # indent-tabs-mode: nil
304 # End:
305
306 # vi: set tabstop=4 expandtab: