]>
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 if len(tags
.get('ALBUMARTIST')) > 0:
130 artist_tag
= tags
.gets('ALBUMARTIST', separator
=', ')
132 artist_tag
= tags
.gets('ARTIST', separator
=', ')
133 artist
= (artist_tag
or '').replace('/', '_')
134 album
= (tags
.gets('ALBUM', separator
=separator
) or '').replace('/', '_')
135 discnum
= tags
.gets('DISCNUMBER')
137 spew('mkdir(%s)\n' % (artist
,))
140 except EnvironmentError, error
:
141 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
144 outdir
= '/'.join([artist
, album
])
145 spew('mkdir(%s)\n' % (outdir
,))
148 except EnvironmentError, error
:
149 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
151 spew('chdir(%s)\n' % (dir,))
157 outfile
= ''.join([discnum
, ' ', outfile
])
158 run_flac('wav', 'cue', '/'.join(['..', artist
, outfile
]), tags
.all())
159 files
= ['%s/%s.flac' % (artist
, outfile
)]
162 outlog
= '/'.join(['..', artist
, outfile
+ '.log'])
163 c(os
.rename
, 'log', outlog
)
165 title
= tags
.gets('TITLE', tracknum
, separator
).replace('/', '_')
168 tmp
.append('%02d' % (int(discnum
),))
169 tmp
.extend(['%02d' % (tracknum
,), title
])
170 part
= tags
.gets('PART', tracknum
)
172 tmp
.extend(['-', part
])
173 outfile
= '/'.join([outdir
, ' '.join(tmp
)])
175 run_flac('track%02d.cdda.wav' % (tracknum
,), None,
176 '../' + outfile
, tags
.track(tracknum
))
177 outlog
= ''.join(['../', outfile
, '.log'])
178 files
= [outfile
+ '.flac']
180 c(os
.rename
, str(tracknum
) + '.log', outlog
)
184 post
= dir + '/post-processor'
185 if os
.path
.exists(post
):
186 spew('Running ', post
); spew(files
); spew('\n')
187 files
.insert(0, post
)
188 os
.spawnv(os
.P_WAIT
, post
, files
)
191 # Clean up if we're the last job for dir; for multi-file dirs,
192 # it's possible for more than one job to run cleanup at once, so
193 # don't fail if things are already clean.
195 if ld
== ['using-tags'] or sorted(ld
) == ['cover.front', 'using-tags']:
198 os
.unlink(dir + '/cover.front')
201 os
.unlink(dir + '/using-tags')
203 except EnvironmentError:
208 ################################################################################
212 """Return the ARTIST, ALBUM, and DATE followed by a list of all the
213 lines in the file FN."""
216 spew('Opening tags file %s\n' % (fn
,))
219 spew('ARTIST %s from %s\n' % (tags
.gets('ARTIST'), fn
))
220 spew('ALBUM %s from %s\n' % (tags
.gets('ALBUM'), fn
))
221 spew('DISCNUMBER %s from %s\n' % (tags
.gets('DISCNUMBER'), fn
))
225 def flacloop(maxjobs
):
226 dir = [None] # [str] instead of str for lame python closures
229 # Get a job for jobs.run. On each call, look for new fa-rip
230 # directories and append an item to the queue @jobs for each wav
231 # file therein. Then, if we have anything in the queue, return a
232 # function to call flac for it, otherwise sleep for a bit. This
233 # looks forever, never returning None, so jobs.run never returns.
235 # Look for new fa-rip directories.
237 for i
in glob('*/tags'):
239 dir[0] = os
.path
.dirname(i
)
241 spew("Renaming %s/tags\n" % (dir[0],))
242 c(os
.rename
, dir[0] + '/tags', dir[0] + '/using-tags')
244 tags
= get_tags(dir[0] + '/using-tags')
245 if os
.path
.exists(dir[0] + '/wav'):
247 jobs
.append((dir[0], None, tags
))
250 # Don't need cue file.
252 c(os
.unlink
, dir[0] + '/cue')
253 except EnvironmentError, error
:
254 if error
.errno
!= ENOENT
:
257 jobs
.extend([(dir[0], x
, tags
)
258 for x
in xrange(1, len(tags
) + 1)])
259 except Exception, error
:
260 sys
.stderr
.write(getattr(error
, 'msg', ''))
261 traceback
.print_exc()
262 sys
.stderr
.write('Continuing...\n')
264 # Return a job if we found any work.
268 # Didn't find anything; wait a while and check again.
274 log
= '/'.join([job
[0],
275 job
[1] == None and 'log'
276 or str(job
[1]) + '.log'])
278 c(os
.dup2
, c(os
.open, log
, os
.O_CREAT | os
.O_WRONLY
), 2)
280 except EnvironmentError, error
:
281 sys
.stderr
.write(getattr(error
, 'msg', ''))
282 traceback
.print_exc()
286 def notify_start(pid
, jobs
):
287 # make this print '2 jobs; start 378 for Artist/01 Title.flac'
288 spew('%d jobs; start %d for %s\n' % (len(jobs
), pid
, dir[0]))
290 def notify_finish(pid
, status
, jobs
):
291 spew('%d jobs; %d finished (' % (len(jobs
), pid
))
292 if os
.WIFEXITED(status
):
293 spew('exited with status ', str(os
.WEXITSTATUS(status
)))
294 elif os
.WIFSIGNALED(status
):
295 spew('killed with signal ', str(os
.WTERMSIG(status
)))
296 elif os
.WIFSTOPPED(status
):
297 spew('stopped with signal ', str(os
.WSTOPSIG(status
)))
300 # Never returns (see getjob comment).
301 org
.diplodocus
.jobs
.run(maxjobs
=maxjobs
, debug
=debug
,
303 notify_start
=notify_start
,
304 notify_finish
=notify_finish
)
307 # Control the exit code for any uncaught exceptions.
309 parser
= OptionParser()
310 parser
.disable_interspersed_args()
311 parser
.add_option('-X', '--debug', action
='store_true', default
=False)
312 parser
.add_option('-j', '--jobs', type='int', default
=1)
313 parser
.add_option('-v', '--verbose', action
='store_true', default
=False)
315 traceback
.print_exc()
319 # Raises SystemExit on invalid options in argv.
320 (options
, args
) = parser
.parse_args(argv
[1:])
321 except Exception, error
:
322 if isinstance(error
, SystemExit):
324 traceback
.print_exc()
328 global debug
, verbose
329 debug
= options
.debug
330 verbose
= options
.verbose
333 flacloop(options
.jobs
)
334 except Exception, error
:
336 if isinstance(error
, SystemExit):
338 sys
.stderr
.write(getattr(error
, 'msg', ''))
339 traceback
.print_exc()
342 if __name__
== '__main__':
343 sys
.exit(main(sys
.argv
))