#!/usr/bin/python """ =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. Note that lame is retarded, and parses B directly itself! So, in order for it to transcode textual tags, you must specify the encoding in LANG, e.g. LANG=en_US.utf-8 =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 """ import os, re, sys, tempfile, traceback from optparse import OptionParser from subprocess import Popen, PIPE import org.diplodocus.jobs from org.diplodocus.util import run_or_die from flac_archive import flac from flac_archive.tags import Tags ################################################################################ # The child processes def flac2mp3(fn, title, artist, album_artist, album, date, track, skip_until, pics=None): (title, artist, album) = [(x == None and 'unknown') or x for x in (title, artist, album)] if date == None: date = '' 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_artist, album, date) = [(x or '').replace("'", r"'\''") for x in [fn, title, artist, album_artist, album, date]] album_artist_options = '' if album_artist: album_artist_options = "--tv 'TPE2=%s'" % album_artist quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, album, track, title)).replace('/', '_') picfn = None if pics: (fd, picfn) = tempfile.mkstemp() f = os.fdopen(fd, 'wb') f.write(pics[0][7]) f.close() try: run_or_die(3, "flac %s -cd %s '%s' | lame --id3v2-only --id3v2-latin1 --pad-id3v2-size 0 %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d --ti '%s' %s - '%s'" % (flac_options, ' '.join(skip_until), fn, lame_options, title, artist, album, date, track, picfn, album_artist_options, quoted_outfile)) finally: if picfn: try: os.unlink(picfn) except: pass 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.""" tags = Tags() p = Popen(['metaflac', '--export-tags-to=-', fn], stdout=PIPE) tags.load(p.stdout) # XXX dataloss! check status status = p.wait() return tags 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 separator = ' ' 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', separator=separator) 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) pics = flac.get_pictures(fn) for i in range(len(tags)): title = tags.gets('TITLE', track, separator) part = tags.gets('PART', track) if part != None: title = '%s - %s' % (title, part) artist = tags.get('ARTIST', track) artist.extend(tags.get('FEATURING', track)) album_artist = tags.gets('ALBUMARTIST', track) jobs.append([fn, title, ', '.join(artist), album_artist, album, tags.gets('DATE', track), track, args[i], pics]) 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))