#! /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 import flac, taglib from org.diplodocus.util import run_or_die ################################################################################ # The child processes def flac2mp3(fn, title, artist, album, date, track, skip_until, pics=None): (title, artist, album, date) = [(x == None and 'unknown') or x for x in (title, artist, album, date)] 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) outfile = ('%s (%s) %02d %s.mp3' % (artist, album, track, title)).replace('/', '_') # 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)] quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, album, track, title)).replace('/', '_') run_or_die(3, "flac %s -cd %s %s '%s' | lame --add-id3v2 %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, quoted_outfile)) if pics != None: taglib.add_apic_frame_to_mp3(outfile, pics) 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 # XXX other things should usue this; flac files, for example, should # get PART as part of the filelname, same as mp3s. class Tags(object): def __init__(self): self._tags = {} def __len__(self): return len(self._tags) def get(self, key, track=None): key = key.upper() try: if track == None: return self._tags[None][key] try: return self._tags[track][key] except KeyError: return self._tags[None][key] except KeyError: return None def gets(self, key, track=None): value = self.get(key, track) if value == None: return None return '\n'.join(value) def set(self, key, value, track=None): if track not in self._tags: self._tags[track] = {} if key not in self._tags[track]: self._tags[track][key] = [] self._tags[track][key].append(value) def get_tags(fn): '''Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags in the file FN.''' tags = Tags() p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE) for line in (x.rstrip() for x in p.stdout): (tag, value) = line.split('=', 1) m = re.search(r'\[([0-9]+)]$', tag) if m != None: tag = tag[:m.start()] track = int(m.group(1)) else: track = None tags.set(tag, value, track) # XXX dataloss! check status status = p.wait() return tags def find_pics(fn, tags): pics = tags.get('__flac2mp3_PICTURE') if not isinstance(pics, list): pics = flac.get_pictures(fn) tags.set('__flac2mp3_PICTURE', pics) return pics 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) tags = get_tags(fn) album = tags.gets('ALBUM') discnum = tags.gets('DISCNUMBER') track = tags.gets('TRACKNUMBER') # lame doesn't seem to support disc number. if discnum != None: album = '%s (disc %s)' % (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 else: track = int(track) for i in range(len(tags)): title = tags.gets('TITLE', track) part = tags.gets('PART', track) if part != None: title = '%s - %s' % (title, part) jobs.append([fn, title, tags.gets('ARTIST', track), album, tags.gets('DATE', track), track, args[i], find_pics(fn, tags)]) 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))