]>
diplodocus.org Git - flac-archive/blob - fa-flacd
1 #! /usr/bin/env python2.4
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 argv
.extend(['--picture', '3|image/jpeg|||cover.front'])
114 spew('Running flac\n')
115 status
= os
.spawnvp(os
.P_WAIT
, argv
[0], argv
)
117 die(2, 'flac exited with status ', str(status
))
119 die(2, 'flac killed with signal ', str(abs(status
)))
121 c(os
.rename
, outfile
+ '.flac-tmp', outfile
+ '.flac')
123 def flac(dir, tracknum
, tags
):
124 """Encode a single wav file to a single flac file, whether the wav and
125 flac files represent individual tracks or whole discs."""
128 artist
= (tags
.gets('ARTIST', separator
=', ') or '').replace('/', '_')
129 album
= (tags
.gets('ALBUM', separator
=separator
) or '').replace('/', '_')
130 discnum
= tags
.gets('DISCNUMBER')
132 spew('mkdir(%s)\n' % (artist
,))
135 except EnvironmentError, error
:
136 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
139 outdir
= '/'.join([artist
, album
])
140 spew('mkdir(%s)\n' % (outdir
,))
143 except EnvironmentError, error
:
144 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
146 spew('chdir(%s)\n' % (dir,))
152 outfile
= ''.join([discnum
, ' ', outfile
])
153 run_flac('wav', 'cue', '/'.join(['..', artist
, outfile
]), tags
.all())
154 files
= ['%s/%s.flac' % (artist
, outfile
)]
157 outlog
= '/'.join(['..', artist
, outfile
+ '.log'])
158 c(os
.rename
, 'log', outlog
)
160 title
= tags
.gets('TITLE', tracknum
, separator
).replace('/', '_')
163 tmp
.append('%02d' % (int(discnum
),))
164 tmp
.extend(['%02d' % (tracknum
,), title
])
165 part
= tags
.gets('PART', tracknum
)
167 tmp
.extend(['-', part
])
168 outfile
= '/'.join([outdir
, ' '.join(tmp
)])
170 run_flac('track%02d.cdda.wav' % (tracknum
,), None,
171 '../' + outfile
, tags
.track(tracknum
))
172 outlog
= ''.join(['../', outfile
, '.log'])
173 files
= [outfile
+ '.flac']
175 c(os
.rename
, str(tracknum
) + '.log', outlog
)
179 post
= dir + '/post-processor'
180 if os
.path
.exists(post
):
181 spew('Running ', post
); spew(files
); spew('\n')
182 files
.insert(0, post
)
183 os
.spawnv(os
.P_WAIT
, post
, files
)
186 # Clean up if we're the last job for dir; for multi-file dirs,
187 # it's possible for more than one job to run cleanup at once, so
188 # don't fail if things are already clean.
190 if ld
== ['using-tags'] or sorted(ld
) == ['cover.front', 'using-tags']:
192 os
.unlink(dir + '/cover.front')
193 os
.unlink(dir + '/using-tags')
195 except EnvironmentError:
200 ################################################################################
204 """Return the ARTIST, ALBUM, and DATE followed by a list of all the
205 lines in the file FN."""
208 spew('Opening tags file %s\n' % (fn
,))
211 spew('ARTIST %s from %s\n' % (tags
.gets('ARTIST'), fn
))
212 spew('ALBUM %s from %s\n' % (tags
.gets('ALBUM'), fn
))
213 spew('DISCNUMBER %s from %s\n' % (tags
.gets('DISCNUMBER'), fn
))
217 def flacloop(maxjobs
):
218 dir = [None] # [str] instead of str for lame python closures
221 # Get a job for jobs.run. On each call, look for new fa-rip
222 # directories and append an item to the queue @jobs for each wav
223 # file therein. Then, if we have anything in the queue, return a
224 # function to call flac for it, otherwise sleep for a bit. This
225 # looks forever, never returning None, so jobs.run never returns.
227 # Look for new fa-rip directories.
229 for i
in glob('*/tags'):
231 dir[0] = os
.path
.dirname(i
)
233 spew("Renaming %s/tags\n" % (dir[0],))
234 c(os
.rename
, dir[0] + '/tags', dir[0] + '/using-tags')
236 tags
= get_tags(dir[0] + '/using-tags')
237 if os
.path
.exists(dir[0] + '/wav'):
239 jobs
.append((dir[0], None, tags
))
242 # Don't need cue file.
244 c(os
.unlink
, dir[0] + '/cue')
245 except EnvironmentError, error
:
246 if error
.errno
!= ENOENT
:
249 jobs
.extend([(dir[0], x
, tags
)
250 for x
in xrange(1, len(tags
) + 1)])
251 except Exception, error
:
252 sys
.stderr
.write(getattr(error
, 'msg', ''))
253 traceback
.print_exc()
254 sys
.stderr
.write('Continuing...\n')
256 # Return a job if we found any work.
260 # Didn't find anything; wait a while and check again.
266 log
= '/'.join([job
[0],
267 job
[1] == None and 'log'
268 or str(job
[1]) + '.log'])
270 c(os
.dup2
, c(os
.open, log
, os
.O_CREAT | os
.O_WRONLY
), 2)
272 except EnvironmentError, error
:
273 sys
.stderr
.write(getattr(error
, 'msg', ''))
274 traceback
.print_exc()
278 def notify_start(pid
, jobs
):
279 # make this print '2 jobs; start 378 for Artist/01 Title.flac'
280 spew('%d jobs; start %d for %s\n' % (len(jobs
), pid
, dir[0]))
282 def notify_finish(pid
, status
, jobs
):
283 spew('%d jobs; %d finished (' % (len(jobs
), pid
))
284 if os
.WIFEXITED(status
):
285 spew('exited with status ', str(os
.WEXITSTATUS(status
)))
286 elif os
.WIFSIGNALED(status
):
287 spew('killed with signal ', str(os
.WTERMSIG(status
)))
288 elif os
.WIFSTOPPED(status
):
289 spew('stopped with signal ', str(os
.WSTOPSIG(status
)))
292 # Never returns (see getjob comment).
293 org
.diplodocus
.jobs
.run(maxjobs
=maxjobs
, debug
=debug
,
295 notify_start
=notify_start
,
296 notify_finish
=notify_finish
)
299 # Control the exit code for any uncaught exceptions.
301 parser
= OptionParser()
302 parser
.disable_interspersed_args()
303 parser
.add_option('-X', '--debug', action
='store_true', default
=False)
304 parser
.add_option('-j', '--jobs', type='int', default
=1)
305 parser
.add_option('-v', '--verbose', action
='store_true', default
=False)
307 traceback
.print_exc()
311 # Raises SystemExit on invalid options in argv.
312 (options
, args
) = parser
.parse_args(argv
[1:])
313 except Exception, error
:
314 if isinstance(error
, SystemExit):
316 traceback
.print_exc()
320 global debug
, verbose
321 debug
= options
.debug
322 verbose
= options
.verbose
325 flacloop(options
.jobs
)
326 except Exception, error
:
328 if isinstance(error
, SystemExit):
330 sys
.stderr
.write(getattr(error
, 'msg', ''))
331 traceback
.print_exc()
334 if __name__
== '__main__':
335 sys
.exit(main(sys
.argv
))