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