]>
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.
77 ''' #' # python-mode is sucks
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
98 ################################################################################
101 def run_flac(infile
, cue
, outfile
, tags
):
102 argv
= ['flac', '-o', outfile
+ '.flac-tmp',
103 '--delete-input-file', '-V', '--no-padding', '--best']
105 argv
.extend(['--cuesheet', cue
])
107 argv
.extend(['-T', i
])
109 # flac 1.1.3 PICTURE support
110 #argv.extend(['--picture', '3|image/jpeg|||cover.front'])
112 spew('Running flac\n')
113 status
= os
.spawnvp(os
.P_WAIT
, argv
[0], argv
)
115 die(2, 'flac exited with status ', str(status
))
117 die(2, 'flac killed with signal ', str(abs(status
)))
119 c(os
.rename
, outfile
+ '.flac-tmp', outfile
+ '.flac')
121 def flac(dir, artist
, album
, discnum
, tracknum
, track_tags
, disc_artist
, tags
):
122 '''Encode a single wav file to a single flac file, whether the wav and
123 flac files represent individual tracks or whole discs.'''
125 (artist
, album
) = [x
.replace('/', '_') for x
in (artist
, album
)]
127 spew('mkdir(%s)\n' % (artist
,))
130 except EnvironmentError, error
:
131 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
134 outdir
= '/'.join([artist
, album
])
135 spew('mkdir(%s)\n' % (outdir
,))
138 except EnvironmentError, error
:
139 error
.errno
== EEXIST
or die(2, error
.msg
, traceback
.format_exc())
141 spew('chdir(%s)\n' % (dir,))
147 outfile
= ''.join([discnum
, ' ', outfile
])
148 run_flac('wav', 'cue', '/'.join(['..', artist
, outfile
]), tags
)
149 files
= ['%s/%s.flac' % (artist
, outfile
)]
152 outlog
= '/'.join(['..', artist
, outfile
+ '.log'])
153 c(os
.rename
, 'log', outlog
)
156 for i
in track_tags
['TITLE']:
157 title
.extend(i
.split())
158 title
= ' '.join(title
).replace('/', '_')
161 tmp
.append('%02d' % (int(discnum
),))
162 tmp
.extend(['%02d' % (tracknum
,), title
])
163 outfile
= '/'.join([outdir
, ' '.join(tmp
)])
166 # If we have ARTIST[n] tags for this track, they'll go in with
167 # the other [n] tags. Else, prepend disc_artist to tags.
168 if 'ARTIST' not in track_tags
:
169 for i
in disc_artist
:
172 tags
.append('TRACKNUMBER=%d' % (tracknum
,))
173 tags
.extend(flatten([['='.join([key
, x
]) for x
in track_tags
[key
]]
174 for key
in track_tags
]))
176 run_flac('track%02d.cdda.wav' % (tracknum
,), None,
177 '../' + outfile
, tags
)
178 outlog
= ''.join(['../', outfile
, '.log'])
179 files
= [outfile
+ '.flac']
181 c(os
.rename
, str(tracknum
) + '.log', outlog
)
185 post
= dir + '/post-processor'
186 if os
.path
.exists(post
):
187 spew('Running ', post
); spew(files
); spew('\n')
188 files
.insert(0, post
)
189 os
.spawnv(os
.P_WAIT
, post
, files
)
192 # Clean up if we're the last job for dir; for multi-file dirs,
193 # it's possible for more than one job to run cleanup at once, so
194 # don't fail if things are already clean.
196 if ld
== ['using-tags'] or sorted(ld
) == ['cover.front', 'using-tags']:
198 os
.unlink(dir + '/cover.front')
199 os
.unlink(dir + '/using-tags')
201 except EnvironmentError:
206 ################################################################################
210 '''Return the ARTIST, ALBUM, and DATE followed by a list of all the
211 lines in the file FN.'''
213 artist
= album
= discnum
= None
216 spew('Opening tags file %s\n' % (fn
,))
218 for line
in (x
.rstrip() for x
in fp
):
221 (tag
, value
) = line
.split('=', 1)
223 if re
.match(r
'ARTIST=', line
, re
.IGNORECASE
):
225 spew('ARTIST %s from %s\n' % (artist
, fn
))
226 elif re
.match(r
'ALBUM=', line
, re
.IGNORECASE
):
228 spew('ALBUM %s from %s\n' % (album
, fn
))
229 elif re
.match(r
'DISCNUMBER=', line
, re
.IGNORECASE
):
231 spew('DISCNUMBER %s from %s\n' % (discnum
, fn
))
233 return (artist
, album
, discnum
, tags
)
235 def flacloop(maxjobs
):
236 dir = [None] # [str] instead of str for lame python closures
239 # Get a job for jobs.run. On each call, look for new fa-rip
240 # directories and append an item to the queue @jobs for each wav
241 # file therein. Then, if we have anything in the queue, return a
242 # function to call flac for it, otherwise sleep for a bit. This
243 # looks forever, never returning None, so jobs.run never returns.
245 # Look for new fa-rip directories.
247 for i
in glob('*/tags'):
249 dir[0] = os
.path
.dirname(i
)
251 spew("Renaming %s/tags\n" % (dir[0],))
252 c(os
.rename
, dir[0] + '/tags', dir[0] + '/using-tags')
254 (artist
, album
, discnum
, tags
) = get_tags(dir[0] + '/using-tags')
255 if os
.path
.exists(dir[0] + '/wav'):
257 jobs
.append([dir[0], artist
, album
, discnum
,
258 None, None, None, tags
])
261 # Don't need cue file.
263 c(os
.unlink
, dir[0] + '/cue')
264 except EnvironmentError, error
:
265 if error
.errno
!= ENOENT
:
268 # Go over @tags, store all [n] tags in a list keyed by
269 # n in %tracks_to_tags, store all ARTIST (not
270 # ARTIST[n]) tags in @disc_artist, and leave the rest
272 tracks_to_tags
= ListDictDict()
276 m
= re
.match(r
'([^[]+)\[(\d+)]=(.*)', tag
)
278 tracks_to_tags
.append(int(m
.group(2)), m
.group(1), m
.group(3))
279 elif re
.match(r
'ARTIST=', tag
, re
.IGNORECASE
):
280 disc_artist
.append(tag
)
285 jobs
.extend([[dir[0], artist
, album
, discnum
, x
,
286 tracks_to_tags
[x
], disc_artist
, tags
]
287 for x
in sorted(map(int,
288 tracks_to_tags
.keys()))])
289 except Exception, error
:
290 sys
.stderr
.write(getattr(error
, 'msg', ''))
291 traceback
.print_exc()
292 sys
.stderr
.write('Continuing...\n')
294 # Return a job if we found any work.
298 # Didn't find anything; wait a while and check again.
304 log
= '/'.join([job
[0],
305 job
[4] == None and 'log' or str(job
[4])
308 c(os
.dup2
, c(os
.open, log
, os
.O_CREAT | os
.O_WRONLY
), 2)
310 except EnvironmentError, error
:
311 sys
.stderr
.write(getattr(error
, 'msg', ''))
312 traceback
.print_exc()
316 def notify_start(pid
, jobs
):
317 # make this print '2 jobs; start 378 for Artist/01 Title.flac'
318 spew('%d jobs; start %d for %s\n' % (len(jobs
), pid
, dir[0]))
320 def notify_finish(pid
, status
, jobs
):
321 spew('%d jobs; %d finished (' % (len(jobs
), pid
))
322 if os
.WIFEXITED(status
):
323 spew('exited with status ', str(os
.WEXITSTATUS(status
)))
324 elif os
.WIFSIGNALED(status
):
325 spew('killed with signal ', str(os
.WTERMSIG(status
)))
326 elif os
.WIFSTOPPED(status
):
327 spew('stopped with signal ', str(os
.WSTOPSIG(status
)))
330 # Never returns (see getjob comment).
331 org
.diplodocus
.jobs
.run(maxjobs
=maxjobs
, debug
=debug
,
333 notify_start
=notify_start
,
334 notify_finish
=notify_finish
)
337 # Control the exit code for any uncaught exceptions.
339 parser
= OptionParser()
340 parser
.disable_interspersed_args()
341 parser
.add_option('-X', '--debug', action
='store_true', default
=False)
342 parser
.add_option('-j', '--jobs', type='int', default
=1)
343 parser
.add_option('-v', '--verbose', action
='store_true', default
=False)
345 traceback
.print_exc()
349 # Raises SystemExit on invalid options in argv.
350 (options
, args
) = parser
.parse_args(argv
[1:])
351 except Exception, error
:
352 if isinstance(error
, SystemExit):
354 traceback
.print_exc()
358 global debug
, verbose
359 debug
= options
.debug
360 verbose
= options
.verbose
363 flacloop(options
.jobs
)
364 except Exception, error
:
366 if isinstance(error
, SystemExit):
368 sys
.stderr
.write(getattr(error
, 'msg', ''))
369 traceback
.print_exc()
372 if __name__
== '__main__':
373 sys
.exit(main(sys
.argv
))