]>
diplodocus.org Git - flac-archive/blob - fa-flacd
6 B<fa-flacd> - archive CDs to single FLAC files
10 B<fa-flacd> [B<-j> I<jobs>] [B<-v>]
14 B<fa-flacd> and B<fa-rip> together comprise B<flac-archive>, a system
15 for archiving audio CDs to single FLAC files. B<fa-flacd> is the guts
16 of the system. It runs in the directory where the audio archives are
17 stored, scanning for new ripped CDs to encode and rename; it never
18 exits. B<fa-rip> generates the inputs for B<fa-flacd>: the ripped WAV
19 file, Vorbis tags, and a cuesheet.
21 Both programs expect to be run from the same directory. They use that
22 directory to manage directories named by artist. Intermediate files
23 are written to temporary directories here. B<fa-flacd> processes the
24 temporary directories into per-album files in the artist directories.
26 Every 5 seconds, B<fa-flacd> scans its current directory for
27 directories with a file called "tags" and creates a processing job for
28 each one. The number of jobs B<fa-flacd> attempts to run is
29 controlled by the B<-j> option and defaults to 4. B<fa-flacd> will
30 print diagnostic output when the B<-v> option is given.
32 A processing job first renames the directory's "tags" file to
33 "using-tags" so that B<ra-flacd> will not try to start another job for
34 this directory. This file is left as is when an error is encountered,
35 so a new job will not be started until the user corrects the error
36 condition and renames "using-tags" back to "tags". Next, it encodes
37 the "wav" file to a FLAC file, using the "cue" file for the cuesheet
38 and "using-tags" for Vorbis tags. Any diagnostic output is saved in
39 the "log" file. Finally, B<fa-flacd> moves the "cue" and "log" files
40 to the artist directory (named by album) and removes the temporary
43 If the temporary directory contains an executable file named
44 "post-processor", B<fa-flacd> executes that file with the relative
45 path to the output FLAC file as an argument. The output files are in
46 their final location when "post-processor" starts. Possible uses are
47 running B<flac2mp3>, moving the output files to a different location,
48 removing the lock file, or adding to a database. The standard input,
49 output, and error streams are inherited from B<fa-flacd>, so they may
50 be connected to anything from a tty to /dev/null. This means that you
51 may want to redirect these streams, if you want to save them or do any
58 =item B<-j> [B<--jobs>] I<jobs>
60 Run up to I<jobs> jobs instead of the default 4.
62 =item B<-v> [B<--verbose>]
64 Print diagnostic information.
70 Written by Eric Gillespie <epg@pretzelnet.org>.
72 flac-archive is free software; you may redistribute it and/or modify
73 it under the same terms as Perl itself.
84 from errno
import EEXIST
, ENOENT
86 from optparse
import OptionParser
88 import org
.diplodocus
.jobs
89 from org
.diplodocus
.structures
import ListDictDict
90 from org
.diplodocus
.util
import die
, flatten
, nothing
91 from org
.diplodocus
.util
import catch_EnvironmentError
as c
93 from flac_archive
.tags
import Tags
100 ################################################################################
101 # The child processes
103 def run_flac(infile
, cue
, outfile
, tags
):
104 argv
= ['flac', '-o', outfile
+ '.flac-tmp',
105 '--delete-input-file', '-V', '--no-padding', '--best']
107 argv
.extend(['--cuesheet', cue
])
109 argv
.extend(['-T', i
])
111 # flac 1.1.3 PICTURE support
112 if os
.path
.exists('cover.front'):
113 argv
.extend(['--picture', '3|image/jpeg|||cover.front'])
115 spew('Running flac\n')
116 status
= os
.spawnvp(os
.P_WAIT
, argv
[0], argv
)
118 die(2, 'flac exited with status ', str(status
))
120 die(2, 'flac killed with signal ', str(abs(status
)))
122 c(os
.rename
, outfile
+ '.flac-tmp', outfile
+ '.flac')
124 def flac(dir, tracknum
, tags
):
125 """Encode a single wav file to a single flac file, whether the wav and
126 flac files represent individual tracks or whole discs."""
129 artist
= (tags
.gets('ARTIST', separator
=', ') or '').replace('/', '_')
130 album
= (tags
.gets('ALBUM', separator
=separator
) or '').replace('/', '_')
131 discnum
= tags
.gets('DISCNUMBER')
133 spew('mkdir(%s)\n' % (artist
,))
136 except EnvironmentError, error
:
137 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
140 outdir
= '/'.join([artist
, album
])
141 spew('mkdir(%s)\n' % (outdir
,))
144 except EnvironmentError, error
:
145 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
147 spew('chdir(%s)\n' % (dir,))
153 outfile
= ''.join([discnum
, ' ', outfile
])
154 run_flac('wav', 'cue', '/'.join(['..', artist
, outfile
]), tags
.all())
155 files
= ['%s/%s.flac' % (artist
, outfile
)]
158 outlog
= '/'.join(['..', artist
, outfile
+ '.log'])
159 c(os
.rename
, 'log', outlog
)
161 title
= tags
.gets('TITLE', tracknum
, separator
).replace('/', '_')
164 tmp
.append('%02d' % (int(discnum
),))
165 tmp
.extend(['%02d' % (tracknum
,), title
])
166 part
= tags
.gets('PART', tracknum
)
168 tmp
.extend(['-', part
])
169 outfile
= '/'.join([outdir
, ' '.join(tmp
)])
171 run_flac('track%02d.cdda.wav' % (tracknum
,), None,
172 '../' + outfile
, tags
.track(tracknum
))
173 outlog
= ''.join(['../', outfile
, '.log'])
174 files
= [outfile
+ '.flac']
176 c(os
.rename
, str(tracknum
) + '.log', outlog
)
180 post
= dir + '/post-processor'
181 if os
.path
.exists(post
):
182 spew('Running ', post
); spew(files
); spew('\n')
183 files
.insert(0, post
)
184 os
.spawnv(os
.P_WAIT
, post
, files
)
187 # Clean up if we're the last job for dir; for multi-file dirs,
188 # it's possible for more than one job to run cleanup at once, so
189 # don't fail if things are already clean.
191 if ld
== ['using-tags'] or sorted(ld
) == ['cover.front', 'using-tags']:
194 os
.unlink(dir + '/cover.front')
197 os
.unlink(dir + '/using-tags')
199 except EnvironmentError:
204 ################################################################################
208 """Return the ARTIST, ALBUM, and DATE followed by a list of all the
209 lines in the file FN."""
212 spew('Opening tags file %s\n' % (fn
,))
215 spew('ARTIST %s from %s\n' % (tags
.gets('ARTIST'), fn
))
216 spew('ALBUM %s from %s\n' % (tags
.gets('ALBUM'), fn
))
217 spew('DISCNUMBER %s from %s\n' % (tags
.gets('DISCNUMBER'), fn
))
221 def flacloop(maxjobs
):
222 dir = [None] # [str] instead of str for lame python closures
225 # Get a job for jobs.run. On each call, look for new fa-rip
226 # directories and append an item to the queue @jobs for each wav
227 # file therein. Then, if we have anything in the queue, return a
228 # function to call flac for it, otherwise sleep for a bit. This
229 # looks forever, never returning None, so jobs.run never returns.
231 # Look for new fa-rip directories.
233 for i
in glob('*/tags'):
235 dir[0] = os
.path
.dirname(i
)
237 spew("Renaming %s/tags\n" % (dir[0],))
238 c(os
.rename
, dir[0] + '/tags', dir[0] + '/using-tags')
240 tags
= get_tags(dir[0] + '/using-tags')
241 if os
.path
.exists(dir[0] + '/wav'):
243 jobs
.append((dir[0], None, tags
))
246 # Don't need cue file.
248 c(os
.unlink
, dir[0] + '/cue')
249 except EnvironmentError, error
:
250 if error
.errno
!= ENOENT
:
253 jobs
.extend([(dir[0], x
, tags
)
254 for x
in xrange(1, len(tags
) + 1)])
255 except Exception, error
:
256 sys
.stderr
.write(getattr(error
, 'msg', ''))
257 traceback
.print_exc()
258 sys
.stderr
.write('Continuing...\n')
260 # Return a job if we found any work.
264 # Didn't find anything; wait a while and check again.
270 log
= '/'.join([job
[0],
271 job
[1] == None and 'log'
272 or str(job
[1]) + '.log'])
274 c(os
.dup2
, c(os
.open, log
, os
.O_CREAT | os
.O_WRONLY
), 2)
276 except EnvironmentError, error
:
277 sys
.stderr
.write(getattr(error
, 'msg', ''))
278 traceback
.print_exc()
282 def notify_start(pid
, jobs
):
283 # make this print '2 jobs; start 378 for Artist/01 Title.flac'
284 spew('%d jobs; start %d for %s\n' % (len(jobs
), pid
, dir[0]))
286 def notify_finish(pid
, status
, jobs
):
287 spew('%d jobs; %d finished (' % (len(jobs
), pid
))
288 if os
.WIFEXITED(status
):
289 spew('exited with status ', str(os
.WEXITSTATUS(status
)))
290 elif os
.WIFSIGNALED(status
):
291 spew('killed with signal ', str(os
.WTERMSIG(status
)))
292 elif os
.WIFSTOPPED(status
):
293 spew('stopped with signal ', str(os
.WSTOPSIG(status
)))
296 # Never returns (see getjob comment).
297 org
.diplodocus
.jobs
.run(maxjobs
=maxjobs
, debug
=debug
,
299 notify_start
=notify_start
,
300 notify_finish
=notify_finish
)
303 # Control the exit code for any uncaught exceptions.
305 parser
= OptionParser()
306 parser
.disable_interspersed_args()
307 parser
.add_option('-X', '--debug', action
='store_true', default
=False)
308 parser
.add_option('-j', '--jobs', type='int', default
=1)
309 parser
.add_option('-v', '--verbose', action
='store_true', default
=False)
311 traceback
.print_exc()
315 # Raises SystemExit on invalid options in argv.
316 (options
, args
) = parser
.parse_args(argv
[1:])
317 except Exception, error
:
318 if isinstance(error
, SystemExit):
320 traceback
.print_exc()
324 global debug
, verbose
325 debug
= options
.debug
326 verbose
= options
.verbose
329 flacloop(options
.jobs
)
330 except Exception, error
:
332 if isinstance(error
, SystemExit):
334 sys
.stderr
.write(getattr(error
, 'msg', ''))
335 traceback
.print_exc()
338 if __name__
== '__main__':
339 sys
.exit(main(sys
.argv
))