]>
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 from flac_archive
.tags
import Tags
93 except EnvironmentError, error
:
94 error
.msg
= '%s.%s(%s): ' % (f
.__module
__, f
.__name
__,
95 ', '.join(map(str, args
)))
99 sys
.stderr
.write(''.join(args
))
107 ################################################################################
108 # The child processes
110 def run_flac(infile
, cue
, outfile
, tags
):
111 argv
= ['flac', '-o', outfile
+ '.flac-tmp',
112 '--delete-input-file', '-V', '--no-padding', '--best']
114 argv
.extend(['--cuesheet', cue
])
116 argv
.extend(['-T', i
])
118 # flac 1.1.3 PICTURE support
119 if os
.path
.exists('cover.front'):
120 argv
.extend(['--picture', '3|image/jpeg|||cover.front'])
122 spew('Running flac\n')
123 status
= os
.spawnvp(os
.P_WAIT
, argv
[0], argv
)
125 die(2, 'flac exited with status ', str(status
))
127 die(2, 'flac killed with signal ', str(abs(status
)))
129 c(os
.rename
, outfile
+ '.flac-tmp', outfile
+ '.flac')
131 def flac(dir, tracknum
, tags
):
132 """Encode a single wav file to a single flac file, whether the wav and
133 flac files represent individual tracks or whole discs."""
136 if len(tags
.get('ALBUMARTIST')) > 0:
137 artist_tag
= tags
.gets('ALBUMARTIST', separator
=', ')
139 artist_tag
= tags
.gets('ARTIST', separator
=', ')
140 artist
= (artist_tag
or '').replace('/', '_')
141 album
= (tags
.gets('ALBUM', separator
=separator
) or '').replace('/', '_')
142 discnum
= tags
.gets('DISCNUMBER')
144 spew('mkdir(%s)\n' % (artist
,))
147 except EnvironmentError, error
:
148 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
151 outdir
= '/'.join([artist
, album
])
152 spew('mkdir(%s)\n' % (outdir
,))
155 except EnvironmentError, error
:
156 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
158 spew('chdir(%s)\n' % (dir,))
164 outfile
= ''.join([discnum
, ' ', outfile
])
165 run_flac('wav', 'cue', '/'.join(['..', artist
, outfile
]), tags
.all())
166 files
= ['%s/%s.flac' % (artist
, outfile
)]
169 outlog
= '/'.join(['..', artist
, outfile
+ '.log'])
170 c(os
.rename
, 'log', outlog
)
172 title
= tags
.gets('TITLE', tracknum
, separator
).replace('/', '_')
175 tmp
.append('%02d' % (int(discnum
),))
176 tmp
.extend(['%02d' % (tracknum
,), title
])
177 part
= tags
.gets('PART', tracknum
)
179 tmp
.extend(['-', part
])
180 outfile
= '/'.join([outdir
, ' '.join(tmp
)])
182 run_flac('track%02d.cdda.wav' % (tracknum
,), None,
183 '../' + outfile
, tags
.track(tracknum
))
184 outlog
= ''.join(['../', outfile
, '.log'])
185 files
= [outfile
+ '.flac']
187 c(os
.rename
, str(tracknum
) + '.log', outlog
)
191 post
= dir + '/post-processor'
192 if os
.path
.exists(post
):
193 spew('Running ', post
); spew(files
); spew('\n')
194 files
.insert(0, post
)
195 os
.spawnv(os
.P_WAIT
, post
, files
)
198 # Clean up if we're the last job for dir; for multi-file dirs,
199 # it's possible for more than one job to run cleanup at once, so
200 # don't fail if things are already clean.
202 if ld
== ['using-tags'] or sorted(ld
) == ['cover.front', 'using-tags']:
205 os
.unlink(dir + '/cover.front')
208 os
.unlink(dir + '/using-tags')
210 except EnvironmentError:
215 ################################################################################
219 """Return the ARTIST, ALBUM, and DATE followed by a list of all the
220 lines in the file FN."""
223 spew('Opening tags file %s\n' % (fn
,))
226 spew('ARTIST %s from %s\n' % (tags
.gets('ARTIST'), fn
))
227 spew('ALBUM %s from %s\n' % (tags
.gets('ALBUM'), fn
))
228 spew('DISCNUMBER %s from %s\n' % (tags
.gets('DISCNUMBER'), fn
))
232 def flacloop(maxjobs
):
233 dir = [None] # [str] instead of str for lame python closures
236 # Get a job for jobs.run. On each call, look for new fa-rip
237 # directories and append an item to the queue @jobs for each wav
238 # file therein. Then, if we have anything in the queue, return a
239 # function to call flac for it, otherwise sleep for a bit. This
240 # looks forever, never returning None, so jobs.run never returns.
242 # Look for new fa-rip directories.
244 for i
in glob('*/tags'):
246 dir[0] = os
.path
.dirname(i
)
248 spew("Renaming %s/tags\n" % (dir[0],))
249 c(os
.rename
, dir[0] + '/tags', dir[0] + '/using-tags')
251 tags
= get_tags(dir[0] + '/using-tags')
252 if os
.path
.exists(dir[0] + '/wav'):
254 jobs
.append((dir[0], None, tags
))
257 # Don't need cue file.
259 c(os
.unlink
, dir[0] + '/cue')
260 except EnvironmentError, error
:
261 if error
.errno
!= ENOENT
:
264 jobs
.extend([(dir[0], x
, tags
)
265 for x
in xrange(1, len(tags
) + 1)])
266 except Exception, error
:
267 sys
.stderr
.write(getattr(error
, 'msg', ''))
268 traceback
.print_exc()
269 sys
.stderr
.write('Continuing...\n')
271 # Return a job if we found any work.
275 # Didn't find anything; wait a while and check again.
281 log
= '/'.join([job
[0],
282 job
[1] == None and 'log'
283 or str(job
[1]) + '.log'])
285 c(os
.dup2
, c(os
.open, log
, os
.O_CREAT | os
.O_WRONLY
), 2)
287 except EnvironmentError, error
:
288 sys
.stderr
.write(getattr(error
, 'msg', ''))
289 traceback
.print_exc()
294 status
= getjob(lambda *a
,**k
: None)()
295 spew('%d jobs; %d finished (' % (len(jobs
), pid
))
296 if os
.WIFEXITED(status
):
297 spew('exited with status ', str(os
.WEXITSTATUS(status
)))
298 elif os
.WIFSIGNALED(status
):
299 spew('killed with signal ', str(os
.WTERMSIG(status
)))
300 elif os
.WIFSTOPPED(status
):
301 spew('stopped with signal ', str(os
.WSTOPSIG(status
)))
305 # Control the exit code for any uncaught exceptions.
307 parser
= OptionParser()
308 parser
.disable_interspersed_args()
309 parser
.add_option('-X', '--debug', action
='store_true', default
=False)
310 parser
.add_option('-j', '--jobs', type='int', default
=1)
311 parser
.add_option('-v', '--verbose', action
='store_true', default
=False)
313 traceback
.print_exc()
317 # Raises SystemExit on invalid options in argv.
318 (options
, args
) = parser
.parse_args(argv
[1:])
319 except Exception, error
:
320 if isinstance(error
, SystemExit):
322 traceback
.print_exc()
326 global debug
, verbose
327 debug
= options
.debug
328 verbose
= options
.verbose
331 flacloop(options
.jobs
)
332 except Exception, error
:
334 if isinstance(error
, SystemExit):
336 sys
.stderr
.write(getattr(error
, 'msg', ''))
337 traceback
.print_exc()
340 if __name__
== '__main__':
341 sys
.exit(main(sys
.argv
))