From: Eric Gillespie Date: Sat, 5 Mar 2022 23:53:00 +0000 (-0600) Subject: Rewrite flac2mp3 back in Perl! (some duplication introducec) X-Git-Url: https://diplodocus.org/git/flac-archive/commitdiff_plain/a0ba03920454c789c3ed241d13bfe64c115f3d19?hp=10d83bbfbcd2461f4fd53f6bc17d1250922214b5 Rewrite flac2mp3 back in Perl! (some duplication introducec) --- diff --git a/fa-encode b/fa-encode index c18b1bc..a4db1b6 100755 --- a/fa-encode +++ b/fa-encode @@ -27,8 +27,14 @@ TODO: implement B<-d> # - TRACKNUMBER - The track number of this piece # - ARTIST - The artist generally considered responsible for the track # - DATE - Date the track was recorded (XXX I use US release date) -# I use one more, though I'm unsure where I got it. Did I make it up? -# - DISCNUMBER - number in multi-disc collection +# https://age.hobba.nl/audio/mirroredpages/ogg-tagging.html (supposedly mirrored from http://reactor-core.org/ogg-tagging.html ) +# specifies more: +# - DISCNUMBER - if part of a multi-disc album, put the disc number here +# - VERSION - e.g. "live", "radio edit" +# - PARTNUMBER - part number if a work is divided across tracks +# - PART - part name e.g. "Oh sole mio" +# https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html +# - ALBUMARTIST - maps to ID3v2 TPE2 # Input is a directory containing: # - trackNN.cdda.wav - WAV format files ripped from CD-DA audio tracks where NN is track number 01 - 99 @@ -133,7 +139,7 @@ sub main { ), ); -e $fn && die("cowardly refusing to clobber $fn"); - my @pictures = ('--picture', quote('3|image/jpeg|||cover.front')); # TODO optional + my @pictures = ('--picture', quote('3|image/jpeg|||cover.front.jpeg')); # TODO optional say(join(' ', 'flac -o', quote($fn), @@ -151,7 +157,7 @@ sub main { "track$tracknum_s.cdda.wav" )); } - say('rm tags cover.front'); + say('rm tags cover.front.jpeg'); return 0; } @@ -161,4 +167,3 @@ if (!caller) { } 1; - diff --git a/flac2mp3 b/flac2mp3 index de2000f..bd39bbe 100755 --- a/flac2mp3 +++ b/flac2mp3 @@ -1,13 +1,12 @@ -#!/usr/bin/python2 +#!/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 @@ -30,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 @@ -52,274 +47,131 @@ 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.tags import Tags - -################################################################################ -# The child processes - -def flac2mp3(fn, title, artist, album_artist, album, discnum, date, - track, skip_until): - (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) - - unquoted_fn = fn - - # XXX all this quoting is an unholy mess and I can see bugs: - # Escaping quotes and then assembling a file name out of that?! - # Moved this part up 2018-11-03 and followed Microsoft file name rules. - outfile_album = album - if discnum != None: - outfile_album = '%s (disc %s)' % (album, discnum) - quoted_outfile = ('%s (%s) %02d %s.mp3' % ( - artist, outfile_album, track, title)) \ - .replace("'", '_') \ - .replace('<', '_') \ - .replace('>', '_') \ - .replace(':', '_') \ - .replace('"', '_') \ - .replace('/', '_') \ - .replace('\\', '_') \ - .replace('|', '_') \ - .replace('?', '_') \ - .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 - - discnum_options = '' - if discnum != None: - discnum_options = "--tv 'TPOS=%d'" % int(discnum) - - # XXX and I have no idea what this was... - # HACK! :( - if check_missing: - return quoted_outfile.replace(r"'\''", "'") - - pic_options = '' - (fd, picfn) = tempfile.mkstemp() - os.close(fd) - p = Popen(['metaflac', '--export-picture-to', picfn, unquoted_fn], - stderr=PIPE) - status = p.wait() - stderr = ''.join(p.stderr) - # Hacky check for flac with no album art - if 'no PICTURE block' in stderr: - # That's fine, just no picture. - pass - else: - if status != 0: - sys.stderr.write('metaflac exited %d: %s\n' % (status, stderr)) - return - pic_options = "--ti '%s'" % picfn - try: - # TODO: Look at TDOR, TDRL, TDRC for date. - 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: - 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) - - 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]) - 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])) - 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_pic_options = (); + # (fd, picfn) = tempfile.mkstemp() + # os.close(fd) + # p = Popen(['metaflac', '--export-picture-to', picfn, unquoted_fn], + # stderr=PIPE) + # status = p.wait() + # stderr = ''.join(p.stderr) + # # Hacky check for flac with no album art + # if 'no PICTURE block' in stderr: + # # That's fine, just no picture. + # pass + # else: + # if status != 0: + # sys.stderr.write('metaflac exited %d: %s\n' % (status, stderr)) + # return + # pic_options = "--ti '%s'" % picfn + + # This is an old TODO; what's wrong with --ty ? + # TODO: Look at TDOR, TDRL, TDRC for date. + say(join(' ', + 'flac', + '-cd', + quote($a{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}), + @quoted_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' + ) + )); +} + +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; diff --git a/tags.pl b/tags.pl index 696a97d..5969ae0 100644 --- a/tags.pl +++ b/tags.pl @@ -6,6 +6,7 @@ use warnings; use Exporter 'import'; our @EXPORT_OK = qw[ + disc_tags read_tags mangle_for_file_name quote @@ -38,6 +39,58 @@ sub read_tags { \%album, \@tracks } +sub one_or_undef { + my $name = shift; + my $tags = shift; + my $tag = $tags->{$name}; + if (defined($tag) && @$tag > 0) { + return $tag->[0] + } + undef +} + +sub one { + my $tag = one_or_undef(@_); + if (!defined($tag)) { + my $name = shift; + die("exactly one $name tag required") + } + $tag +} + +sub disc_tags { + my $album = shift; + + if (!defined($album->{ALBUM}) || @{$album->{ALBUM}} != 1) { + die('exactly one ALBUM tag required') + } + my $album_tag = $album->{ALBUM}->[0]; + + # TODO ALBUMARTIST + if (!defined($album->{ARTIST}) || @{$album->{ARTIST}} != 1) { + die('exactly one ARTIST tag required') + } + my $artist = $album->{ARTIST}->[0]; + + my $date; + if (defined($album->{DATE})) { + if (@{$album->{DATE}} != 1) { + die('one or zero DATE tags required') + } + $date = $album->{DATE}->[0]; + } + + my @discnumber = (); + if (defined($album->{DISCNUMBER})) { + if (@{$album->{DISCNUMBER}} != 1) { + die('one or zero DISCNUMBER tags required') + } + @discnumber = ($album->{DISCNUMBER}->[0]); + } + + $album_tag, $artist, $date, \@discnumber +} + sub mangle_for_file_name { my $fn = shift; $fn =~ s/[!,.?]//g; # discard these punctuation marks