#! /usr/bin/env python2.4 ''' =head1 NAME B - transcode FLAC file to MP3 files =head1 SYNOPSIS B [B<--lame-options> I] [B<-j> I] [B<-q>] [B<-v>] I [...] =head1 DESCRIPTION B transcodes the FLAC files I to MP3 files. I may be the kind of FLAC file B generates. That is, it contains a cue sheet, one TITLE tag per track listed therein, and ARTIST, ALBUM, and DATE tags. =head1 OPTIONS =over 4 =item B<--lame-options> I Pass I to B. This ends up being passed to the shell, so feel free to take advantage of that. You'll almost certainly have to put I in single quotes. =item B<-j> [B<--jobs>] I Run up to I jobs instead of the default 1. =item B<-q> [B<--quiet>] Suppress status information. This option is passed along to B and B. =item B<-v> [B<--verbose>] Print diagnostic information. This option is passed along to B and B. =back =head1 AUTHORS Written by Eric Gillespie . =cut ''' #' # python-mode is sucks import re, sys, traceback from optparse import OptionParser from subprocess import Popen, PIPE import org.diplodocus.jobs from org.diplodocus.util import run_or_die ################################################################################ # The child processes def flac2mp3(fn, title, artist, album, date, track, skip_until): (title, artist, album, date) = [(x == None and 'unknown') or x for x in (title, artist, album, date)] track = int(track) # XXX caller should int this try: (skip_arg, until_arg) = skip_until except ValueError: skip_arg = until_arg = '' if quiet: flac_options = '--silent' else: flac_options = '' tmp = [] global lame_options if lame_options != None: tmp.append(lame_options) else: tmp.append('--preset standard'); quiet and tmp.append('--quiet') verbose and tmp.append('--verbose') lame_options = ' '.join(tmp) # Escape any single quotes ' so we can quote this. (fn, title, artist, album, date) = [x.replace("'", r"'\''") for x in (fn, title, artist, album, date)] outfile = ('%s (%s) %02d %s.mp3' % (artist, album, track, title)).replace('/', '_') run_or_die(3, "flac %s -cd %s %s '%s' | lame %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d - '%s'" % (flac_options, skip_arg or '', until_arg or '', fn, lame_options, title, artist, album, date, track, outfile)) return 0 ################################################################################ # The master process def tformat(m, s, c): return '%02d:%02d.%02d' % (m, s, c) def get_decode_args(fn): l = [] p = Popen(['metaflac', '--export-cuesheet-to=-', fn], stdout=PIPE) for line in (x.rstrip() for x in p.stdout): m = re.search(r'INDEX 01 (\d\d):(\d\d):(\d\d)$', line) if m != None: l.append(map(int, m.groups())) status = p.wait() # XXX dataloss! check status args = [] for i in xrange(len(l)): arg = ['--skip=' + tformat(*l[i])] try: next = l[i + 1] except IndexError: next = None if next != None: if next[2] == 0: if next[1] == 0: arg.append('--until=' + tformat(next[0] - 1, 59, 74)) else: arg.append('--until=' + tformat(next[0], next[1] - 1, 74)) else: arg.append('--until=' + tformat(next[0], next[1], next[2] - 1)) args.append(arg) # If no cue sheet, stick a dummy in here. if len(args) == 0: args = [[]] return args def get_tags(fn): '''Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags in the file FN.''' date = discnum = track = None artists = [] titles = [] p = Popen(['metaflac', '--export-vc-to=-', fn], stdout=PIPE) for line in (x.rstrip() for x in p.stdout): (tag, value) = line.split('=', 1) if re.match(r'ARTIST=', line, re.IGNORECASE): artist = value elif re.match(r'ALBUM=', line, re.IGNORECASE): album = value elif re.match(r'DATE=', line, re.IGNORECASE): date = value elif re.match(r'DISCNUMBER=', line, re.IGNORECASE): discnum = value elif re.match(r'TRACKNUMBER=', line, re.IGNORECASE): track = value # XXX Need to support per-track of everything but album and # discnum, not just TITLE... Also unify with fa-flacd get_tags. elif re.match(r'ARTIST\[', line, re.IGNORECASE): artists.append(value) elif re.match(r'TITLE', line, re.IGNORECASE): titles.append(value) # XXX dataloss! check status status = p.wait() # If no TITLEs, stick a dummy in here. if len(titles) == 0: titles.append(None) return (artist, album, date, discnum, track, artists, titles) 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('--lame-options') parser.add_option('-q', '--quiet', action='store_true', default=False) 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, flac_options, lame_options, quiet, verbose debug = options.debug lame_options = options.lame_options quiet = options.quiet verbose = options.verbose jobs = [] for fn in args: try: args = get_decode_args(fn) (artist, album, date, discnum, track, artists, titles) = get_tags(fn) # lame doesn't seem to support disc number. if discnum != None: album = '%s (disc %d)' % (album, discnum) # Stupid hack: only a single-track file should have the # TRACKNUMBER tag, so use it if set for the first pass through # the loop. At the end of the loop, we'll set $track for the # next run, so this continues to work for multi-track files. if track == None: track = 1 for i in range(len(titles)): if len(artists) > 0: artist = artists[i] jobs.append([fn, titles[i], artist, album, date, track, args[i]]) track = i + 2 except Exception, error: sys.stderr.write(getattr(error, 'msg', '')) traceback.print_exc() sys.stderr.write('Continuing...\n') def getjob(reap): try: job = jobs.pop(0) except IndexError: return return lambda: flac2mp3(*job) org.diplodocus.jobs.run(maxjobs=options.jobs, debug=debug, get_job=getjob) except Exception, error: if isinstance(error, SystemExit): raise # check all print_exc and format_exc in fa-flacd.py; i think # for some i don't do this msg print check sys.stderr.write(getattr(error, 'msg', '')) traceback.print_exc() return 2 return 0 if __name__ == '__main__': sys.exit(main(sys.argv))