-#! /usr/bin/python2
-
-"""
-=head1 NAME
-
-B<fa-flacd> - archive CDs to single FLAC files
-
-=head1 SYNOPSIS
-
-B<fa-flacd> [B<-j> I<jobs>] [B<-v>]
-
-=head1 DESCRIPTION
-
-B<fa-flacd> and B<fa-rip> together comprise B<flac-archive>, a system
-for archiving audio CDs to single FLAC files. B<fa-flacd> is the guts
-of the system. It runs in the directory where the audio archives are
-stored, scanning for new ripped CDs to encode and rename; it never
-exits. B<fa-rip> generates the inputs for B<fa-flacd>: the ripped WAV
-file, Vorbis tags, and a cuesheet.
-
-Both programs expect to be run from the same directory. They use that
-directory to manage directories named by artist. Intermediate files
-are written to temporary directories here. B<fa-flacd> processes the
-temporary directories into per-album files in the artist directories.
-
-Every 5 seconds, B<fa-flacd> scans its current directory for
-directories with a file called "tags" and creates a processing job for
-each one. The number of jobs B<fa-flacd> attempts to run is
-controlled by the B<-j> option and defaults to 4. B<fa-flacd> will
-print diagnostic output when the B<-v> option is given.
-
-A processing job first renames the directory's "tags" file to
-"using-tags" so that B<ra-flacd> will not try to start another job for
-this directory. This file is left as is when an error is encountered,
-so a new job will not be started until the user corrects the error
-condition and renames "using-tags" back to "tags". Next, it encodes
-the "wav" file to a FLAC file, using the "cue" file for the cuesheet
-and "using-tags" for Vorbis tags. Any diagnostic output is saved in
-the "log" file. Finally, B<fa-flacd> moves the "cue" and "log" files
-to the artist directory (named by album) and removes the temporary
-directory.
-
-If the temporary directory contains an executable file named
-"post-processor", B<fa-flacd> executes that file with the relative
-path to the output FLAC file as an argument. The output files are in
-their final location when "post-processor" starts. Possible uses are
-running B<flac2mp3>, moving the output files to a different location,
-removing the lock file, or adding to a database. The standard input,
-output, and error streams are inherited from B<fa-flacd>, so they may
-be connected to anything from a tty to /dev/null. This means that you
-may want to redirect these streams, if you want to save them or do any
-logging.
-
-=head1 OPTIONS
-
-=over 4
-
-=item B<-j> [B<--jobs>] I<jobs>
-
-Run up to I<jobs> jobs instead of the default 4.
-
-=item B<-v> [B<--verbose>]
-
-Print diagnostic information.
-
-=back
-
-=head1 AUTHORS
-
-Written by Eric Gillespie <epg@pretzelnet.org>.
-
-flac-archive is free software; you may redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
-
-"""
-
-import os
-import re
-import sys
-import time
-import traceback
-from errno import EEXIST, ENOENT
-from glob import glob
-from optparse import OptionParser
-
-import org.diplodocus.jobs
-from org.diplodocus.structures import ListDictDict
-from org.diplodocus.util import die, flatten, nothing
-from org.diplodocus.util import catch_EnvironmentError as c
-
-from flac_archive.tags import Tags
-
-def spew(*args):
- if verbose:
- for i in args:
- sys.stderr.write(i)
-
-################################################################################
-# The child processes
-
-def run_flac(infile, cue, outfile, tags):
- argv = ['flac', '-o', outfile + '.flac-tmp',
- '--delete-input-file', '-V', '--no-padding', '--best']
- if cue != None:
- argv.extend(['--cuesheet', cue])
- for i in tags:
- argv.extend(['-T', i])
- argv.append(infile)
- # flac 1.1.3 PICTURE support
- if os.path.exists('cover.front'):
- argv.extend(['--picture', '3|image/jpeg|||cover.front'])
-
- spew('Running flac\n')
- status = os.spawnvp(os.P_WAIT, argv[0], argv)
- if status > 0:
- die(2, 'flac exited with status ', str(status))
- elif status < 0:
- die(2, 'flac killed with signal ', str(abs(status)))
-
- c(os.rename, outfile + '.flac-tmp', outfile + '.flac')
-
-def flac(dir, tracknum, tags):
- """Encode a single wav file to a single flac file, whether the wav and
- flac files represent individual tracks or whole discs."""
-
- separator = ' '
- if len(tags.get('ALBUMARTIST')) > 0:
- artist_tag = tags.gets('ALBUMARTIST', separator=', ')
- else:
- artist_tag = tags.gets('ARTIST', separator=', ')
- artist = (artist_tag or '').replace('/', '_')
- album = (tags.gets('ALBUM', separator=separator) or '').replace('/', '_')
- discnum = tags.gets('DISCNUMBER')
-
- spew('mkdir(%s)\n' % (artist,))
- try:
- c(os.mkdir, artist)
- except EnvironmentError, error:
- error.errno == EEXIST or die(2, error.msg, traceback.format_exc())
-
- if tracknum != None:
- outdir = '/'.join([artist, album])
- spew('mkdir(%s)\n' % (outdir,))
- try:
- c(os.mkdir, outdir)
- except EnvironmentError, error:
- error.errno == EEXIST or die(2, error.msg, traceback.format_exc())
-
- spew('chdir(%s)\n' % (dir,))
- c(os.chdir, dir)
-
- if tracknum == None:
- outfile = album
- if discnum != None:
- outfile = ''.join([discnum, ' ', outfile])
- run_flac('wav', 'cue', '/'.join(['..', artist, outfile]), tags.all())
- files = ['%s/%s.flac' % (artist, outfile)]
-
- try:
- c(os.unlink, 'cue')
- except EnvironmentError, error:
- if error.errno != ENOENT:
- raise error
-
- outlog = '/'.join(['..', artist, outfile + '.log'])
- c(os.rename, 'log', outlog)
- else:
- title = tags.gets('TITLE', tracknum, separator).replace('/', '_')
- tmp = []
- if discnum != None:
- tmp.append('%02d' % (int(discnum),))
- tmp.extend(['%02d' % (tracknum,), title])
- part = tags.gets('PART', tracknum)
- if part != None:
- tmp.extend(['-', part])
- outfile = '/'.join([outdir, ' '.join(tmp)])
-
- run_flac('track%02d.cdda.wav' % (tracknum,), None,
- '../' + outfile, tags.track(tracknum))
- outlog = ''.join(['../', outfile, '.log'])
- files = [outfile + '.flac']
-
- c(os.rename, str(tracknum) + '.log', outlog)
-
- c(os.chdir, '..')
-
- post = dir + '/post-processor'
- if os.path.exists(post):
- spew('Running ', post); spew(files); spew('\n')
- files.insert(0, post)
- os.spawnv(os.P_WAIT, post, files)
- c(os.unlink, post)
-
- # Clean up if we're the last job for dir; for multi-file dirs,
- # it's possible for more than one job to run cleanup at once, so
- # don't fail if things are already clean.
- ld = os.listdir(dir)
- if ld == ['using-tags'] or sorted(ld) == ['cover.front', 'using-tags']:
- try:
- try:
- os.unlink(dir + '/cover.front')
- except OSError:
- pass
- os.unlink(dir + '/using-tags')
- os.rmdir(dir)
- except EnvironmentError:
- pass
-
- return 0
-
-################################################################################
-# The master process
-
-def get_tags(fn):
- """Return the ARTIST, ALBUM, and DATE followed by a list of all the
- lines in the file FN."""
-
- tags = Tags()
- spew('Opening tags file %s\n' % (fn,))
- tags.load(open(fn))
-
- spew('ARTIST %s from %s\n' % (tags.gets('ARTIST'), fn))
- spew('ALBUM %s from %s\n' % (tags.gets('ALBUM'), fn))
- spew('DISCNUMBER %s from %s\n' % (tags.gets('DISCNUMBER'), fn))
-
- return tags
-
-def flacloop(maxjobs):
- dir = [None] # [str] instead of str for lame python closures
- jobs = []
-
- # Get a job for jobs.run. On each call, look for new fa-rip
- # directories and append an item to the queue @jobs for each wav
- # file therein. Then, if we have anything in the queue, return a
- # function to call flac for it, otherwise sleep for a bit. This
- # looks forever, never returning None, so jobs.run never returns.
- def getjob(reap):
- # Look for new fa-rip directories.
- while True:
- for i in glob('*/tags'):
- try:
- dir[0] = os.path.dirname(i)
-
- spew("Renaming %s/tags\n" % (dir[0],))
- c(os.rename, dir[0] + '/tags', dir[0] + '/using-tags')
-
- tags = get_tags(dir[0] + '/using-tags')
- if os.path.exists(dir[0] + '/wav'):
- # single-file
- jobs.append((dir[0], None, tags))
- else:
- # multi-file
- # Don't need cue file.
- try:
- c(os.unlink, dir[0] + '/cue')
- except EnvironmentError, error:
- if error.errno != ENOENT:
- raise error
-
- jobs.extend([(dir[0], x, tags)
- for x in xrange(1, len(tags) + 1)])
- except Exception, error:
- sys.stderr.write(getattr(error, 'msg', ''))
- traceback.print_exc()
- sys.stderr.write('Continuing...\n')
-
- # Return a job if we found any work.
- try:
- job = jobs.pop(0)
- except IndexError:
- # Didn't find anything; wait a while and check again.
- time.sleep(5)
- reap()
- continue
-
- def lamb():
- log = '/'.join([job[0],
- job[1] == None and 'log'
- or str(job[1]) + '.log'])
- try:
- c(os.dup2, c(os.open, log, os.O_CREAT | os.O_WRONLY), 2)
- return flac(*job)
- except EnvironmentError, error:
- sys.stderr.write(getattr(error, 'msg', ''))
- traceback.print_exc()
- return 1
- return lamb
-
- def notify_start(pid, jobs):
- # make this print '2 jobs; start 378 for Artist/01 Title.flac'
- spew('%d jobs; start %d for %s\n' % (len(jobs), pid, dir[0]))
-
- def notify_finish(pid, status, jobs):
- spew('%d jobs; %d finished (' % (len(jobs), pid))
- if os.WIFEXITED(status):
- spew('exited with status ', str(os.WEXITSTATUS(status)))
- elif os.WIFSIGNALED(status):
- spew('killed with signal ', str(os.WTERMSIG(status)))
- elif os.WIFSTOPPED(status):
- spew('stopped with signal ', str(os.WSTOPSIG(status)))
- spew(')\n')
-
- # Never returns (see getjob comment).
- org.diplodocus.jobs.run(maxjobs=maxjobs, debug=debug,
- get_job=getjob,
- notify_start=notify_start,
- notify_finish=notify_finish)
-
-def main(argv):
- # Control the exit code for any uncaught exceptions.
- try:
- parser = OptionParser()
- parser.disable_interspersed_args()
- parser.add_option('-X', '--debug', action='store_true', default=False)
- parser.add_option('-j', '--jobs', type='int', default=1)
- parser.add_option('-v', '--verbose', action='store_true', default=False)
- except:
- traceback.print_exc()
- return 2
-
- try:
- # Raises SystemExit on invalid options in argv.
- (options, args) = parser.parse_args(argv[1:])
- except Exception, error:
- if isinstance(error, SystemExit):
- return 1
- traceback.print_exc()
- return 2
-
- try:
- global debug, verbose
- debug = options.debug
- verbose = options.verbose
-
- # Never returns...
- flacloop(options.jobs)
- except Exception, error:
- # but might blow up.
- if isinstance(error, SystemExit):
- raise
- sys.stderr.write(getattr(error, 'msg', ''))
- traceback.print_exc()
- return 2
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv))