]> diplodocus.org Git - flac-archive/blob - fa-flacd
Print error message to stderr.
[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> ID TRACKCOUNT OFFSET [OFFSET ...] LENGTH
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 $status;
74
75 verbose("Renaming $dir/tags\n");
76 rename("$dir/tags", "$dir/using-tags")
77 or die("rename($dir/tags, $dir/using-tags): $!");
78
79 ($artist, $album, @tags) = get_tags("$dir/using-tags");
80
81 verbose("mkdir($artist)\n");
82 -d $artist or mkdir($artist) or die("mkdir($artist): $!");
83
84 verbose("chdir($dir)\n");
85 chdir($dir) or die("chdir($dir): $!");
86
87 verbose("Running flac\n");
88 $status = system('flac', '-o', "../$artist/$album.flac-tmp",
89 '--delete-input-file', '-V', '--cuesheet',
90 'cue', '--no-padding', '--best',
91 map({ ('-T', $_) } @tags),
92 'wav');
93 if (WIFEXITED($status) and ($status = WEXITSTATUS($status)) != 0) {
94 die("flac: $status");
95 } elsif (WIFSIGNALED($status)) {
96 die("flac killed with signal ", WTERMSIG($status));
97 } elsif (WIFSTOPPED($status)) {
98 die("flac stopped with signal ", WSTOPSIG($status));
99 }
100
101 verbose("Cleaning up $dir\n");
102 unlink('using-tags') or die("unlink(using-tags): $!");
103 unlink('cue') or die("unlink(cue): $!");
104 rename('toc', "../$artist/$album.toc")
105 or die("rename(toc, ../$artist/$album.toc): $!");
106 rename('log', "../$artist/$album.log")
107 or die("rename(log, ../$artist/$album.log): $!");
108 chdir('..') or die("chdir(..): $!");
109 rmdir($dir) or die("rmdir($dir): $!");
110
111 rename("$artist/$album.flac-tmp", "$artist/$album.flac")
112 or die("rename($artist/$album.flac-tmp, $artist/$album.flac): $!");
113
114 return 0;
115 }
116
117 sub reaper {
118 my $pid;
119
120 while (($pid = waitpid(0, WNOHANG)) > 0) {
121 push(@finished, [$pid, $?]);
122 }
123
124 $SIG{CHLD} = \&reaper;
125 }
126
127 sub newjob {
128 my $dir = shift;
129 my $pid;
130
131 $pid = fork();
132 if (not defined($pid)) {
133 die("fork: $!");
134 } elsif ($pid == 0) {
135 $SIG{CHLD} = 'IGNORE';
136 open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!");
137 exit(flac($dir));
138 }
139
140 verbose("new job $pid for $dir\n");
141 return $pid;
142 }
143
144 sub deljob {
145 my $i = shift;
146 my $j;
147 my $pid;
148 my $status;
149
150 $pid = $finished[$i][0];
151 $status = $finished[$i][1];
152
153 verbose("$pid finished (");
154 if (WIFEXITED($status)) {
155 verbose('exited ', WEXITSTATUS($status));
156 } elsif (WIFSIGNALED($status)) {
157 verbose('signalled ', WTERMSIG($status));
158 } elsif (WIFSTOPPED($status)) {
159 verbose('stopped ', WSTOPSIG($status));
160 }
161 verbose(")\n");
162
163 for ($j = 0; $j <= $#jobs; $j++) {
164 $pid == $jobs[$j] and splice(@jobs, $j, 1) and last;
165 }
166
167 splice(@finished, $i, 1);
168 }
169
170 sub flacloop {
171 my $MAXJOBS = shift;
172 my $i;
173 my $j;
174
175
176 $SIG{CHLD} = \&reaper;
177 while (1) {
178 if (scalar(@jobs) <= $MAXJOBS) {
179 foreach $i (glob('*/tags')) {
180 push(@jobs, newjob(dirname($i))) <= $MAXJOBS or last;
181 }
182 }
183
184 for ($i = 0; $i <= $#finished; $i++) {
185 deljob($i);
186 }
187
188 verbose(scalar(@jobs), " jobs\n");
189 sleep(5);
190 }
191 }
192
193 MAIN: {
194 my %opts;
195
196 $opts{'j'} = 4;
197 $opts{'v'} = 0;
198 if (not getopts('j:v', \%opts)) {
199 print("usage: flacd [-jN -v]\n");
200 exit(2);
201 }
202
203 $verbose = $opts{'v'};
204
205 flacloop($opts{'j'});
206 }
207
208 \f
209 __END__
210
211 =head1 DESCRIPTION
212
213 B<fa-flacd>, B<fa-rip>, and B<fa-tags> together comprise
214 B<flac-archive>, a system for archiving audio CDs to single FLAC
215 files. B<fa-flacd> is the guts of the system. It runs in the
216 directory where the audio archives are stored, scanning for new CDs to
217 encode and rename; it never exits. B<fa-rip> generates the inputs for
218 B<fa-flacd>: the ripped WAV file, Vorbis tags, and a cuesheet.
219 B<fa-tags> is not meant to be run directly; B<fa-rip> uses it to
220 generate the candidate Vorbis tags.
221
222 All three programs expect to be run from the same directory. They use
223 that directory to manage directories named by artist and by disc ID.
224 Intermediate files are written to the disc ID directory. B<fa-flacd>
225 processes the disc ID directories into per-album files in the artist
226 directories.
227
228 =head2 FA-FLACD
229
230 B<fa-flacd> does not exit; it runs until the user kills it. Every 5
231 seconds it scans its current directory for directories with a file
232 called "tags" and creates a processing job for each one. The number
233 of jobs B<fa-flacd> attempts to run is controlled by the B<-j> option
234 and defaults to 4. B<fa-flacd> will print diagnostic output when the
235 B<-v> option is given.
236
237 A processing job first renames the directory's "tags" file to
238 "using-tags" so that B<ra-flacd> will not try to start another job for
239 this directory. This file is left as is when an error is encountered,
240 so a new job will not be started until the user corrects the error
241 condition and renames "using-tags" back to "tags". Next, it encodes
242 the "wav" file to a FLAC file, using the "cue" file for the cuesheet
243 and "using-tags" for Vorbis tags. Any diagnostic output is saved in
244 the "log" file. Finally, the "cue" and "log" files are moved to the
245 artist directory (and named by album) and the ID directory is removed.
246
247 =head2 FA-RIP
248
249 B<fa-rip> uses C<mktemp(1)> to create a directory for storage of its
250 intermediate files. It uses C<cdrdao(1)> to create the "cue" file and
251 then passes the number of tracks (from the "cue" file) as command-line
252 arguments to B<fa-tags>. Finally, it execs C<cdparanoia(1)> to rip
253 the CD to the "wav" file.
254
255 In order for this CD to be processed by B<fa-flacd>, the user must
256 create a "tags" file. This is usually done by renaming one of the
257 candidate-tags files and deleting the others.
258
259 =head2 FA-TAGS
260
261 B<fa-tags> uses C<MusicBrainz::Client> to populate candidate-tags
262 files. These are numbered in the order of entries read from
263 MusicBrainz, e.g. candidate-tags-1, candidate-tags-2, etc. B<fa-tags>
264 also creates candidate-tags-0, which has the correct fields for this
265 CD (including correct number of TITLE= lines), but with all fields
266 blank.
267
268 B<fa-tags> requires the number of tracks as its sole argument.
269
270 =head1 ENVIRONMENT
271
272 =over 4
273
274 =item CDDBURL
275
276 B<fa-tags> uses this to retrieve candidate Vorbis tags. Defaults to
277 "http://freedb.freedb.org/~cddb/cddb.cgi".
278
279 =item CDDEV
280
281 B<fa-rip> uses this to rip audio and save the cuesheet for a CD. It
282 makes some effort to check some common device names for FreeBSD,
283 Linux, and NetBSD by default.
284
285 =back
286
287 =head1 AUTHORS
288
289 Written by Eric Gillespie <epg@pretzelnet.org>.
290
291 flac-archive is free software; you may redistribute it and/or modify
292 it under the same terms as Perl itself.
293
294 =cut
295
296 # Local variables:
297 # cperl-indent-level: 4
298 # perl-indent-level: 4
299 # indent-tabs-mode: nil
300 # End:
301
302 # vi: set tabstop=4 expandtab: