aboutsummaryrefslogtreecommitdiffstats
path: root/cli/whatmp3.py
diff options
context:
space:
mode:
Diffstat (limited to 'cli/whatmp3.py')
-rwxr-xr-xcli/whatmp3.py370
1 files changed, 370 insertions, 0 deletions
diff --git a/cli/whatmp3.py b/cli/whatmp3.py
new file mode 100755
index 0000000..4269fd0
--- /dev/null
+++ b/cli/whatmp3.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python3
+
+import argparse
+import multiprocessing
+import os
+import re
+import shutil
+import sys
+import threading
+from fnmatch import fnmatch
+
+VERSION = "3.8"
+
+# DEFAULT CONFIGURATION
+
+# Output folder unless specified
+# output = os.path.join(os.environ['HOME'], "Desktop/")
+output = os.getcwd()
+
+# Separate torrent output folder (defaults to output):
+torrent_dir = output
+
+# Do you want to copy additional files (.jpg, .log, etc)?
+copyother = 1
+
+# Do you want to zeropad tracknumbers? (1 => 01, 2 => 02 ...)
+zeropad = 1
+
+# Do you want to dither FLACs to 16/44 before encoding?
+dither = 0
+
+# Specify tracker announce URL
+tracker = None
+
+# Max number of threads (e.g., Dual-core = 2, Hyperthreaded Dual-core = 4)
+max_threads = multiprocessing.cpu_count()
+
+# Tags to copy (note: changing/adding to these requires changing/adding values in/to 'encoders' below)
+copy_tags = ('TITLE', 'ALBUM', 'ARTIST', 'TRACKNUMBER', 'GENRE', 'COMMENT', 'DATE')
+
+# Default encoding options
+enc_opts = {
+ '320': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -b 320 --ignore-tag-errors --noreplaygain'},
+ 'V0': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 0 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'V2': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 2 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'V8': {'enc': 'lame', 'ext': '.mp3', 'opts': '-q 0 -V 8 --vbr-new --ignore-tag-errors --noreplaygain'},
+ 'Q8': {'enc': 'oggenc', 'ext': '.ogg', 'opts': '-q 8 --utf8'},
+ 'AAC': {'enc': 'neroAacEnc', 'ext': '.aac', 'opts': '-br 320000'},
+ 'ALAC': {'enc': 'ffmpeg', 'ext': '.m4a', 'opts': ''},
+ 'FLAC': {'enc': 'flac', 'ext': '.flac', 'opts': '--best'}
+}
+
+encoders = {
+ 'lame': {
+ 'enc': "lame --silent %(opts)s %(tags)s --add-id3v2 - '%(filename)s' 2>&1",
+ 'TITLE': "--tt '%(TITLE)s'",
+ 'ALBUM': "--tl '%(ALBUM)s'",
+ 'ARTIST': "--ta '%(ARTIST)s'",
+ 'TRACKNUMBER': "--tn '%(TRACKNUMBER)s'",
+ 'GENRE': "--tg '%(GENRE)s'",
+ 'DATE': "--ty '%(DATE)s'",
+ 'COMMENT': "--tc '%(COMMENT)s'",
+ 'regain': "mp3gain -q -c -s i '%s'/*.mp3"
+ },
+ 'oggenc': {
+ 'enc': "oggenc -Q %(opts)s %(tags)s -o '%(filename)s' - 2>&1",
+ 'TITLE': "-t '%(TITLE)s'",
+ 'ALBUM': "-l '%(ALBUM)s'",
+ 'ARTIST': "-a '%(ARTIST)s'",
+ 'TRACKNUMBER': "-N '%(TRACKNUMBER)s'",
+ 'GENRE': "-G '%(GENRE)s'",
+ 'DATE': "-d '%(DATE)s'",
+ 'COMMENT': "-c 'comment=%(COMMENT)s'",
+ 'regain': "vorbisgain -qafrs '%s'/*.ogg"
+ },
+ 'neroAacEnc': {
+ 'enc': "neroAacEnc %(opts)s -if - -of '%(filename)s' 2>&1 && neroAacTag %(tags)s",
+ 'TITLE': "-meta:title='%(TITLE)s'",
+ 'ALBUM': "-meta:album='%(ALBUM)s'",
+ 'ARTIST': "-meta:artist='%(ARTIST)s'",
+ 'TRACKNUMBER': "-meta:track='%(TRACKNUMBER)s'",
+ 'GENRE': "-meta:genre='%(GENRE)s'",
+ 'DATE': "-meta:year='%(DATE)s'",
+ 'COMMENT': "-meta:comment='%(COMMENT)s'",
+ 'regain': "aacgain -q -c '%s'/*.aac"
+ },
+ 'ffmpeg': {
+ 'enc': "ffmpeg %(opts)s -i - -acodec alac %(tags)s '%(filename)s' 2>&1",
+ 'TITLE': "-metadata title='%(TITLE)s'",
+ 'ALBUM': "-metadata album='%(ALBUM)s'",
+ 'ARTIST': "-metadata author='%(ARTIST)s'",
+ 'TRACKNUMBER': "-metadata track='%(TRACKNUMBER)s'",
+ 'GENRE': "-metadata genre='%(GENRE)s'",
+ 'DATE': "-metadata date='%(DATE)s'",
+ 'COMMENT': "-metadata comment='%(COMMENT)s'",
+ 'regain': ""
+ },
+ 'flac': {
+ 'enc': "flac %(opts)s -s %(tags)s -o '%(filename)s' - 2>&1",
+ 'TITLE': "-T 'TITLE=%(TITLE)s'",
+ 'ALBUM': "-T 'ALBUM=%(ALBUM)s'",
+ 'ARTIST': "-T 'ARTIST=%(ARTIST)s'",
+ 'TRACKNUMBER': "-T 'TRACKNUMBER=%(TRACKNUMBER)s'",
+ 'GENRE': "-T 'GENRE=%(GENRE)s'",
+ 'DATE': "-T 'DATE=%(DATE)s'",
+ 'COMMENT': "-T 'COMMENT=%(COMMENT)s'",
+ 'regain': "metaflac --add-replay-gain '%s'/*.flac"
+ }
+}
+
+dither_cmd = 'sox -t wav - -b 16 -t wav - rate 44100 dither'
+
+# END CONFIGURATION
+
+codecs = []
+
+def copy_other(opts, flacdir, outdir):
+ if opts.verbose:
+ print('COPYING other files')
+ for dirpath, dirs, files in os.walk(flacdir, topdown=False):
+ for name in files:
+ if opts.nolog and fnmatch(name.lower(), '*.log'):
+ continue
+ if opts.nocue and fnmatch(name.lower(), '*.cue'):
+ continue
+ if opts.nodots and fnmatch(name.lower(), '^.'):
+ continue
+ if (not fnmatch(name.lower(), '*.flac')
+ and not fnmatch(name.lower(), '*.m3u')):
+ d = re.sub(re.escape(flacdir), outdir, dirpath)
+ if (os.path.exists(os.path.join(d, name))
+ and not opts.overwrite):
+ continue
+ if not os.path.exists(d):
+ os.makedirs(d)
+ shutil.copy(os.path.join(dirpath, name), d)
+
+class EncoderArg(argparse.Action):
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
+ super(EncoderArg, self).__init__(option_strings, dest, nargs, **kwargs)
+ def __call__(self, parser, namespace, values, option_string=None):
+ codecs.append(option_string[2:])
+
+def escape_quote(pattern):
+ pattern = re.sub("'", "'\"'\"'", pattern)
+ return pattern
+
+def escape_percent(pattern):
+ pattern = re.sub('%', '%%', pattern)
+ return pattern
+
+def failure(r, msg):
+ print("ERROR: %s: %s" % (r, msg), file=sys.stderr)
+
+def make_torrent(opts, target):
+ if opts.verbose:
+ print('MAKE: %s.torrent' % os.path.relpath(target))
+ torrent_cmd = "mktorrent -p -a '%s' -o '%s.torrent' '%s' 2>&1" % (
+ opts.tracker, escape_quote(os.path.join(opts.torrent_dir,
+ os.path.basename(target))),
+ escape_quote(target)
+ )
+ if opts.additional:
+ torrent_cmd += ' ' + opts.additional
+ if opts.nodate:
+ torrent_cmd += ' -d'
+ if not opts.verbose:
+ torrent_cmd += ' >/dev/null'
+ if opts.verbose:
+ print(torrent_cmd)
+ r = system(torrent_cmd)
+ if r: failure(r, torrent_cmd)
+
+def replaygain(opts, codec, outdir):
+ if opts.verbose:
+ print("APPLYING replaygain")
+ print(encoders[enc_opts[codec]['enc']]['regain'] % outdir)
+ r = system(encoders[enc_opts[codec]['enc']]['regain'] % escape_quote(outdir))
+ if r: failure(r, "replaygain")
+ for dirpath, dirs, files in os.walk(outdir, topdown=False):
+ for name in dirs:
+ r = system(encoders[enc_opts[codec]['enc']]['regain']
+ % os.path.join(dirpath, name))
+ if r: failure(r, "replaygain")
+
+def setup_parser():
+ p = argparse.ArgumentParser(
+ description="whatmp3 transcodes audio files and creates torrents for them",
+ argument_default=False,
+ epilog="""depends on flac, metaflac, mktorrent, and optionally oggenc, lame, neroAacEnc,
+ neroAacTag, mp3gain, aacgain, vorbisgain, and sox""")
+ p.add_argument('--version', action='version', version='%(prog)s ' + VERSION)
+ for a in [
+ [['-v', '--verbose'], False, 'increase verbosity'],
+ [['-n', '--notorrent'], False, 'do not create a torrent after conversion'],
+ [['-r', '--replaygain'], False, 'add ReplayGain to new files'],
+ [['-c', '--original'], False, 'create a torrent for the original FLAC'],
+ [['-i', '--ignore'], False, 'ignore top level directories without flacs'],
+ [['-s', '--silent'], False, 'do not write to stdout'],
+ [['-S', '--skipgenre'], False, 'do not insert a genre tag in MP3 files'],
+ [['-D', '--nodate'], False, 'do not write the creation date to the .torrent file'],
+ [['-L', '--nolog'], False, 'do not copy log files after conversion'],
+ [['-C', '--nocue'], False, 'do not copy cue files after conversion'],
+ [['-H', '--nodots'], False, 'do not copy dot/hidden files after conversion'],
+ [['-w', '--overwrite'], False, 'overwrite files in output dir'],
+ [['-d', '--dither'], dither, 'dither FLACs to 16/44 before encoding'],
+ [['-m', '--copyother'], copyother, 'copy additional files (def: true)'],
+ [['-z', '--zeropad'], zeropad, 'zeropad tracknumbers (def: true)'],
+ ]:
+ p.add_argument(*a[0], **{'default': a[1], 'action': 'store_true', 'help': a[2]})
+ for a in [
+ [['-a', '--additional'], None, 'ARGS', 'additional arguments to mktorrent'],
+ [['-t', '--tracker'], tracker, 'URL', 'tracker URL'],
+ [['-o', '--output'], output, 'DIR', 'set output dir'],
+ [['-O', '--torrent-dir'], torrent_dir, 'DIR', 'set independent torrent output dir'],
+ ]:
+ p.add_argument(*a[0], **{
+ 'default': a[1], 'action': 'store',
+ 'metavar': a[2], 'help': a[3]
+ })
+ p.add_argument('-T', '--threads', default=max_threads, action='store',
+ dest='max_threads', type=int, metavar='THREADS',
+ help='set number of threads THREADS (def: %s)' % max_threads)
+ for enc_opt in enc_opts.keys():
+ p.add_argument("--" + enc_opt, action=EncoderArg, nargs=0,
+ help='convert to %s' % (enc_opt))
+ p.add_argument('flacdirs', nargs='+', metavar='flacdir',
+ help='directories to transcode')
+ return p
+
+def system(cmd):
+ return os.system(cmd)
+
+def transcode(f, flacdir, mp3_dir, codec, opts, lock):
+ tags = {}
+ for tag in copy_tags:
+ tagcmd = "metaflac --show-tag='" + escape_quote(tag) + \
+ "' '" + escape_quote(f) + "'"
+ t = re.sub('\S.+?=', '', os.popen(tagcmd).read().rstrip(), count=1)
+ if t:
+ tags.update({tag: escape_quote(t)})
+ del t
+ if (opts.zeropad and 'TRACKNUMBER' in tags
+ and len(tags['TRACKNUMBER']) == 1):
+ tags['TRACKNUMBER'] = '0' + tags['TRACKNUMBER']
+ if opts.skipgenre and 'GENRE' in tags: del tags['GENRE']
+
+ outname = re.sub(re.escape(flacdir), mp3_dir, f)
+ outname = re.sub(re.compile('\.flac$', re.IGNORECASE), '', outname)
+ with lock:
+ if not os.path.exists(os.path.dirname(outname)):
+ os.makedirs(os.path.dirname(outname))
+ outname += enc_opts[codec]['ext']
+ if os.path.exists(outname) and not opts.overwrite:
+ print("WARN: file %s already exists" % (os.path.relpath(outname)),
+ file=sys.stderr)
+ return 1
+ flac_cmd = encoders[enc_opts[codec]['enc']]['enc']
+ tagline = ''
+ for tag in tags:
+ tagline = tagline + " " + encoders[enc_opts[codec]['enc']][tag]
+ tagline = tagline % tags
+ if opts.dither:
+ flac_cmd = dither_cmd + ' | ' + flac_cmd
+ flac_cmd = "flac -sdc -- '" + escape_percent(escape_quote(f)) + \
+ "' | " + flac_cmd
+ flac_cmd = flac_cmd % {
+ 'opts': enc_opts[codec]['opts'],
+ 'filename': escape_quote(outname),
+ 'tags': tagline
+ }
+ outname = os.path.basename(outname)
+ if not opts.silent:
+ print("encoding %s" % outname)
+ if opts.verbose:
+ print(flac_cmd)
+ r = system(flac_cmd)
+ if r:
+ failure(r, "error encoding %s" % outname)
+ system("touch '%s/FAILURE'" % mp3_dir)
+ return 0
+
+class Transcode(threading.Thread):
+ def __init__(self, file, flacdir, mp3_dir, codec, opts, lock, cv):
+ threading.Thread.__init__(self)
+ self.file = file
+ self.flacdir = flacdir
+ self.mp3_dir = mp3_dir
+ self.codec = codec
+ self.opts = opts
+ self.lock = lock
+ self.cv = cv
+
+ def run(self):
+ r = transcode(self.file, self.flacdir, self.mp3_dir, self.codec,
+ self.opts, self.lock)
+ with self.cv:
+ self.cv.notify_all()
+ return r
+
+def main():
+ parser = setup_parser()
+ opts = parser.parse_args()
+ if not opts.output.endswith('/'):
+ opts.output += '/'
+ if len(codecs) == 0 and not opts.original:
+ parser.error("you must provide at least one format to transcode to")
+ exit()
+ for flacdir in opts.flacdirs:
+ flacdir = os.path.abspath(flacdir)
+ flacfiles = []
+ if not os.path.exists(opts.torrent_dir):
+ os.makedirs(opts.torrent_dir)
+ for dirpath, dirs, files in os.walk(flacdir, topdown=False):
+ for name in files:
+ if fnmatch(name.lower(), '*.flac'):
+ flacfiles.append(os.path.join(dirpath, name))
+ flacfiles.sort()
+ if opts.ignore and not flacfiles:
+ if not opts.silent:
+ print("SKIP (no flacs in): %s" % (os.path.relpath(flacdir)))
+ continue
+ if opts.original:
+ if not opts.silent:
+ print('BEGIN ORIGINAL FLAC')
+ if opts.output and opts.tracker and not opts.notorrent:
+ make_torrent(opts, flacdir)
+ if not opts.silent:
+ print('END ORIGINAL FLAC')
+
+ for codec in codecs:
+ outdir = os.path.basename(flacdir)
+ flacre = re.compile('FLAC', re.IGNORECASE)
+ if flacre.search(outdir):
+ outdir = flacre.sub(codec, outdir)
+ else:
+ outdir = outdir + " (" + codec + ")"
+ outdir = opts.output + outdir
+ if not os.path.exists(outdir):
+ os.makedirs(outdir)
+
+ if not opts.silent:
+ print('BEGIN ' + codec + ': %s' % os.path.relpath(flacdir))
+ threads = []
+ cv = threading.Condition()
+ lock = threading.Lock()
+ for f in flacfiles:
+ with cv:
+ while (threading.active_count() == max(1, opts.max_threads) + 1):
+ cv.wait()
+ t = Transcode(f, flacdir, outdir, codec, opts, lock, cv)
+ t.start()
+ threads.append(t)
+ for t in threads:
+ t.join()
+
+ if opts.copyother:
+ copy_other(opts, flacdir, outdir)
+ if opts.replaygain:
+ replaygain(opts, codec, outdir)
+ if opts.output and opts.tracker and not opts.notorrent:
+ make_torrent(opts, outdir)
+ if not opts.silent:
+ print('END ' + codec + ': %s' % os.path.relpath(flacdir))
+
+ if opts.verbose: print('ALL DONE: ' + os.path.relpath(flacdir))
+ return 0
+
+if __name__ == '__main__':
+ main()