X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/771827160b4deb3a842551b4363d2ff6a8c90617..3e5ac1b35b5e96d2e2e0570f163493fca75a00e3:/flac2mp3 diff --git a/flac2mp3 b/flac2mp3 index 65e3f8f..24e4a48 100755 --- a/flac2mp3 +++ b/flac2mp3 @@ -1,14 +1,13 @@ -#! /usr/bin/env perl - -# $Id$ +#!/usr/bin/python +""" =head1 NAME B - transcode FLAC file to MP3 files =head1 SYNOPSIS -B [B<--lame-options> I] [B<-q>] [B<-v>] I [...] +B [B<--lame-options> I] [B<-j> I] [B<-q>] [B<-v>] I [...] =head1 DESCRIPTION @@ -17,212 +16,9 @@ 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. -=cut - -use strict; -use warnings; - -use POSIX ':sys_wait_h'; -use Pod::Usage; -use Getopt::Long qw(:config gnu_getopt no_ignore_case); - -my $flac_options; -my $lame_options; -my $quiet; -my $verbose; - -sub run_or_die { - my $command = shift; - my $status; - - $verbose and print(STDERR "$command\n"); - $status = system($command); - - if (WIFEXITED($status)) { - if (($status = WEXITSTATUS($status)) != 0) { - die("$command exited with status $status"); - } - } elsif (WIFSIGNALED($status)) { - die("$command killed with signal ", WTERMSIG($status)); - } elsif (WIFSTOPPED($status)) { - die("$command stopped with signal ", WSTOPSIG($status)); - } else { - die("Major horkage on system($command): \$? = $? \$! = $!"); - } -} - -sub tformat { - return sprintf('%02d:%02d.%02d', @_); -} - -sub get_decode_args { - my $fn = shift; - my @l; - - open(F, '-|', 'metaflac', '--export-cuesheet-to=-', $fn); - while () { - /INDEX 01 (\d\d):(\d\d):(\d\d)$/ or next; - push(@l, [$1, $2, $3]); - } - - my @args; - for my $i (0..$#l) { - my $arg = ["--skip=" . tformat(@{$l[$i]})]; - my $next = $l[$i+1]; - if (defined($next)) { - if ($next->[2] == 0) { - if ($next->[1] == 0) { - push(@$arg, '--until=' . tformat($next->[0] - 1, 59, 74)); - } else { - push(@$arg, '--until=' . tformat($next->[0], $next->[1] - 1, - 74)); - } - } else { - push(@$arg, '--until=' . tformat($next->[0], $next->[1], - $next->[2] - 1)); - } - } - push(@args, $arg); - } - - return @args; -} - -# Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags -# in the file FN. -sub get_tags { - my $fn = shift; - my $artists = shift; - my $titles = shift; - my $tag; - my $value; - my $artist; - my $album; - my $date; - my $track; - - open(TAGS, '-|', 'metaflac', '--export-vc-to=-', $fn) - or die("open(metaflac --export-vc-to=- $fn): $!"); - while () { - chomp; - - ($tag, $value) = split(/=/, $_, 2); - - if (/^ARTIST=/i) { - $artist = $value; - } elsif (/^ALBUM=/i) { - $album = $value; - } elsif (/^DATE=/i) { - $date = $value; - } elsif (/^ARTIST\[/i) { - push(@$artists, $value); - } elsif (/^TRACKNUMBER=/i) { - $track = $value; - - # Intentionally don't match the = on this one, to support the - # TITLE[1] .. TITLE[n] tag style. - } elsif (/^TITLE/i) { - push(@$titles, $value); - } - } - close(TAGS) or die("close(metaflac --export-vc-to=- $fn): $?"); - - return ($artist, $album, $date, $track); -} - -sub arg { - my $arg = shift; - my $var = shift; - - if (defined($$var)) { - $$var = "$arg '$$var'"; - } else { - $$var = '' - } -} - -sub flac2mp3 { - my $fn = shift; - my $title = shift; - my $artist = shift; - my $album = shift; - my $date = shift; - my $track = int(shift); - my $skip_arg = shift; - my $until_arg = shift; - my @tmp; - my $outfile; - - if ($quiet) { - $flac_options = '--silent'; - } else { - $flac_options = ''; - } - - if ($lame_options) { - push(@tmp, $lame_options); - } else { - push(@tmp, '--preset standard'); - } - $quiet and push(@tmp, '--quiet'); - $verbose and push(@tmp, '--verbose'); - $lame_options = join(' ', @tmp); - - # We'll be putting these in single quotes, so we need to escape - # any single quotes in the filename by closing the quote ('), - # putting an escaped quote (\'), and then reopening the quote ('). - for ($fn, $title, $artist, $album, $date) { - defined and s/'/'\\''/g; - } - - $outfile = sprintf("$artist ($album) \%02s $title.mp3", $track); - $outfile =~ s/\//_/g; - - arg('--tt', \$title); - arg('--ta', \$artist); - arg('--tl', \$album); - arg('--ty', \$date); - arg('--tn', \$track); - - $until_arg ||= ''; - run_or_die(join(' ', "flac $flac_options -cd $skip_arg $until_arg '$fn'", - " | lame $lame_options $title $artist $album $date $track", - " - '$outfile'")); -} - -MAIN: { - my $help; - GetOptions( - 'lame-options=s', \$lame_options, - 'quiet|q' => \$quiet, - 'verbose|v' => \$verbose, - 'help|h|?' => \$help, - ) or pod2usage(); - $help and pod2usage(-exitstatus=>0, -verbose=>1); - - @ARGV or pod2usage(); - for my $fn (@ARGV) { - my @args = get_decode_args($fn); - my (@artists, @titles); - my ($artist, $album, $date, $track) = get_tags($fn, \@artists, - \@titles); - - # 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. - $track ||= 1; - - for my $i (0..$#titles) { - flac2mp3($fn, $titles[$i], ($artists[$i] or $artist), $album, $date, - $track, (defined($args[$i]) and @{$args[$i]} or '')); - $track = $i + 2; - } - } -} - - -__END__ +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 @@ -234,6 +30,10 @@ 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 @@ -252,10 +52,250 @@ Written by Eric Gillespie . =cut -# Local variables: -# cperl-indent-level: 4 -# perl-indent-level: 4 -# indent-tabs-mode: nil -# End: - -# vi: set tabstop=4 expandtab: +""" + +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, discnum, 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) + + # 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 + + outfile_album = album + discnum_options = '' + if discnum != None: + outfile_album = '%s (disc %s)' % (album, discnum) + discnum_options = "--tv 'TPOS=%d'" % int(discnum) + + quoted_outfile = ('%s (%s) %02d %s.mp3' % (artist, outfile_album, + track, title)).replace('/', '_') + # HACK! :( + if check_missing: + return quoted_outfile.replace(r"'\''", "'") + + pic_options = '' + if pics: + (fd, picfn) = tempfile.mkstemp() + f = os.fdopen(fd, 'wb') + f.write(pics[0][7]) + f.close() + pic_options = "--ti '%s'" % picfn + 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 %s %s %s - '%s'" + % (flac_options, ' '.join(skip_until), fn, + lame_options, title, artist, album, date, track, + pic_options, album_artist_options, + discnum_options, quoted_outfile)) + finally: + if pic_options: + 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', '--no-utf8-convert', '--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', '--no-utf8-convert', '--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) + parser.add_option('--check-missing-files', 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 + global check_missing + check_missing = options.check_missing_files + 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') + + # 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) + version = tags.gets('VERSION', track) + if version != None: + title = '%s (%s)' % (title, version) + artist = tags.get('ARTIST', track) + artist.extend(tags.get('FEATURING', track)) + album_artist = tags.gets('ALBUMARTIST', track) + if check_missing: + mp3 = flac2mp3(fn, title, + ', '.join(artist), + album_artist, album, discnum, + tags.gets('DATE', track), + track, args[i], pics) + if not os.path.exists(mp3): + print fn + break + continue + jobs.append((fn, title, + ', '.join(artist), + album_artist, album, discnum, + 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))