X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/3e8794c0d39a363cd6da9f6a29e0b07fb8ede9ea..e53e2434445e78d18c0bdf3596b1444ddb399bde:/flac2mp3 diff --git a/flac2mp3 b/flac2mp3 index 1e29ad0..c1e0944 100755 --- a/flac2mp3 +++ b/flac2mp3 @@ -1,13 +1,12 @@ -#! /usr/bin/env python2.4 +#!/usr/local/bin/perl -""" =head1 NAME B - transcode FLAC file to MP3 files =head1 SYNOPSIS -B [B<--lame-options> I] [B<-j> I] [B<-q>] [B<-v>] I [...] +B [B<--lame-options> I] [B<-q>] [B<-v>] I [...] =head1 DESCRIPTION @@ -16,6 +15,10 @@ 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 @@ -26,10 +29,6 @@ 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 @@ -48,251 +47,121 @@ Written by Eric Gillespie . =cut -""" - -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) = [(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, 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' | lame --add-id3v2 %s --tt '%s' --ta '%s' --tl '%s' --ty '%s' --tn %d - '%s'" - % (flac_options, ' '.join(skip_until), 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._global = {} - self._tags = {} - def __len__(self): - # All files have at least one track. - return max(1, len(self._tags)) - def get(self, key, track=None): - key = key.upper() - try: - try: - return self._tags[track][key] - except KeyError: - return self._global[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): - key = key.upper() - if track == None: - tags = self._global - else: - try: - tags = self._tags[track] - except KeyError: - tags = self._tags[track] = {} - if key not in tags: - tags[key] = [] - tags[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 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) - - pics = flac.get_pictures(fn) - - 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], 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)) +package epg::flac::archive::mp3; + +use v5.12; +use warnings; + +use File::Temp; +use FindBin; + +require "$FindBin::Bin/tags.pl"; +epg::flac::archive::tags->import( + qw[ + disc_tags + read_tags + mangle_for_file_name + quote + ]); + +sub flac2mp3 { + my %a = @_; + my $quoted_flac = quote($a{flac}); + + say('if metaflac --export-picture-to=flac2mp3.cover.$$ ', $quoted_flac, '; then'); + say(' pic_options="--ti flac2mp3.cover.$$"'); + say('fi'); + + # This is an old TODO; what's wrong with --ty ? + # TODO: Look at TDOR, TDRL, TDRC for date. + say(join(' ', + 'flac', + '-cd', + $quoted_flac, + '|', + 'lame', + '--id3v2-only', + '--id3v2-latin1', + '--pad-id3v2-size', 0, + '--preset standard', + '--ta', + quote($a{artist}), + '--tl', + quote($a{album}), + '--tn', + quote($a{track}), + '--tt', + quote($a{title}), + '--ty', + quote($a{date}), + '$pic_options', + (map { ('--tv', quote("TPE2=$_")) } @{$a{albumartist}}), + (map { ('--tv', quote("TPOS=$_")) } @{$a{discnumber}}), + '-', + quote( + mangle_for_file_name( + join(' ', + $a{artist}, + $a{album}, + (map { sprintf('%02d', $_) } @{$a{discnumber}}), + $a{track}, + $a{title}, + )) + . '.mp3' + ) + )); + say('unset pic_options'); + say('rm -f flac2mp3.cover.$$'); +} + +sub read_tags_metaflac { + my $fn = shift; + open(my $fh, '-|', 'metaflac', '--no-utf8-convert', '--export-tags-to=-', $fn) || die("metalfac: $!"); + my @result = read_tags($fh); + if (!close($fh)) { + if ($! == 0) { + die("metaflac exited $?") + } + die("close(metaflac): $!") + } + @result +} + +sub main { + for my $fn (@_) { + my ($tags) = read_tags_metaflac($fn); + my ($album, $artist, $date, $discnumber) = disc_tags($tags); + + # TODO resurrect whole-disc FLAC? + # 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) + { + my $tracknumber = epg::flac::archive::tags::one(TRACKNUMBER => $tags); + my $title = epg::flac::archive::tags::one(TITLE => $tags); + # TODO restore PARTNUMBER and VERSION net time i need them + flac2mp3( + artist => $artist, + album => $album, + date => $date, + discnumber => $discnumber, + track => $tracknumber, + title => $title, + flac => $fn, + ) + } + } + + return 0; +} + +if (!caller) { + exit(main(@ARGV)) +} + +1;