diff --git a/aux.py b/aux.py index 9cfcc26..9a23de2 100644 --- a/aux.py +++ b/aux.py @@ -4,7 +4,6 @@ import os import pathlib import over -prefix = over.core.text.prefix # -------------------------------------------------- @@ -32,15 +31,11 @@ def to_Path(raw_path): # -------------------------------------------------- -def _serialize(d): - """ - Transforms d into a string compatible with over config files. - """ - - if type(d) is str: - return '"' + d.replace('"', '\\"') + '"' - else: - return str(d) +context_file_header = """# Context file for %s-%s +# This file stores configuration valid for this directory. +# It is updated automatically with options you use. + +""" def update_cfg_context(main, ignore=[]): """ @@ -51,53 +46,16 @@ def update_cfg_context(main, ignore=[]): name=value """ - options = {o.name: o for o in main.options} - overrides = {} + options_to_consider = [option for option in main.options.values() if option.name not in ignore and option.in_cfg_file] + options_to_write = {option.name: option for option in options_to_consider if option.source == over.app.Option_sources.command_line} + options_to_read = {option.name: option for option in options_to_consider if option.source != over.app.Option_sources.command_line} - try: - with open(".over-video") as f: - for i, line in enumerate(f): - line = line.strip() - - if not line: - continue - - try: - name, value = line.split("=", 1) - except ValueError: - main.print(".over-video §rsyntax error§/ on line §B{:d}§/: §y{:s}§/".format(i, line), prefix.fail) - main.print("fix the error or set §B--no-§gcontext§/") - main.exit(1) - - try: - option = options[name] - - try: - overrides[name] = over.core.app._parse(value, option.dtype) - except: - main.print(".over-video §rdata syntax error§/ on line §B{:d}§/: §y{:s}§/".format(i, line), prefix.fail) - main.print("fix the error or set §B--no-§gcontext§/") - main.exit(1) - except KeyError: - main.print(".over-video option §y{:s}§/ §rdoes not exist§/".format(name), prefix.warn) - except FileNotFoundError: - pass + context_file_read = over.app.ConfigFile(options_to_read, ".over-video") + restored = context_file_read.read_config() + if restored: + main.print("restored from .over-video<.>: %s" %(", ".join("%s<.>" %(o.name) for o in restored)), main.print.tl.note) - main.print("syncing context (from .over-video)", prefix.start) - - for option in options.values(): - if option.name not in ignore: - if option.name in overrides and option.source is not "cmdline": - override = overrides[option.name] - main.print("using §B--§g{:s}§/ = §m{:s}§/".format(option.name, str(override))) - option.value = override - elif option.source is "cmdline": - overrides[option.name] = option.value - main.print("storing §B--§g{:s}§/ = §m{:s}§/".format(option.name, str(option.value))) - - with open(".over-video", "w") as f: - for name, value in overrides.items(): - line = "{:s}={:s}\n".format(name, _serialize(value)) - f.write(line) - - main.print("syncing context", prefix.done) + context_file_write = over.app.ConfigFile(options_to_write, ".over-video") + added = context_file_write.update_config(context_file_header, (main.name, main.version)) + if added: + main.print("added to .over-video<.>: %s" %(", ".join("%s<.>" %(o.name) for o in added))) diff --git a/over-video.py b/over-video.py index f145dfd..574d035 100755 --- a/over-video.py +++ b/over-video.py @@ -12,8 +12,7 @@ import version from aux import parse_fps, to_Path, update_cfg_context -Command = over.core.cmd.Command -prefix = over.core.text.prefix +Command = over.cmd.Command # -------------------------------------------------- X264_BANNED_PIXFMTS = {'bgr24', 'yuv422p'} @@ -21,7 +20,7 @@ X264_BANNED_PIXFMTS = {'bgr24', 'yuv422p'} # -------------------------------------------------- # see doc/command_assembler.png -command = over.core.types.ndict() +command = over.types.ndict() command.identify = Command('ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', 'INFILE') command.normalize_prepass = Command('ffmpeg', '-i', 'INFILE', '-filter:a', 'volumedetect', '-f', 'null', '/dev/null') command.encode_generic = Command('ffmpeg', '-i', 'INFILE', 'MAP', 'VIDEO', 'AUDIO', '-sn', 'OUTFILE') @@ -39,63 +38,64 @@ command.sub_copy_video = Command('-codec:v', 'copy') # -------------------------------------------------- if __name__ == '__main__': - main = over.core.app.Main('over-video', 'Overwatch Video', version.str, 'AO-JSL', use_cfg_file=True) - main.add_option('audio', 'str', 'vorbis', 'Audio codec to use, either §mvorbis§/, §mpcm§/, §mcopy§/ or §mdrop§/.', short_name='a') - main.add_option('audio-quality', 'float', 4, 'Audio encoding quality with §m-1§/ being the worst and §m10§/ being the best.', short_name='q') - main.add_option('video', 'str', 'x264', 'Video codec to use, either §mx265§/, §mx264§/, §mtheora§/, §mcopy§/ or §mdrop§/.', short_name='v') - main.add_option('video-preset', 'str', 'slow', 'Video encoding preset, if supported by the selected encoder.', short_name='P') - main.add_option('video-quality', 'float', 22, 'Video encoding quality (CRF). Use §m0§/-§m10§/ for Theora (§m0§/ being the lowest, §m5§/-§m7§/ is generally watchable) and §m0§/-§m51§/ for x264/5 (§m0§/ being lossless, §m18§/-§m28§/ is reasonable).', short_name='Q') - main.add_option('context', 'bool', True, 'Use .over-video file in CWD, if available, to remember encoding parameters per-directory.', short_name='C') - main.add_option('normalize', 'bool', True, 'Normalize the audio track.', short_name='n') - main.add_option('normalize-target', 'float', -20.0, 'Target mean volume to target.') - main.add_option('normalize-override', 'float', 0.0, 'Volume correction to use instead of computing the required value in a (lengthy) pre-pass.', short_name='N', use_cfg_file=False) - main.add_option('ffmpeg-vfilter', 'str', '', 'Raw ffmpeg -filter:v options, e.g. "§mscale=1280:trunc(ow/a/2)*2,transpose=dir=1§/"', short_name='F') - main.add_option('ffmpeg-map', 'str', [], 'Raw ffmpeg -map options, e.g. "§m0:1 0:2§/". This is a drop-in fix until we get proper stream selection.', short_name='M', plural=True) - main.add_option('move-source', 'str', 'processed', 'Move source file to this directory after conversion. Use an empty string to disable.', short_name='m') - main.add_option('dump-commands', 'bool', False, 'Print ffmpeg commands that would be executed. If §B--§gnormalize§/ is in effect, the normalization pre-pass will still be performed so that the proper volume correction can be computed.', short_name='D') - main.add_option('probe', 'bool', False, 'Print the raw JSON output of ffprobe and exit.', short_name='p') - main.add_option('armed', 'bool', False, 'Perform the suggested action.', short_name='A', use_cfg_file=False) - main.enable_help('h') - main.add_help('Description', ['Over-Video is a simple video converter.']) - main.add_help('Good encoder settings', ['§Bx264§/: §B--§gvideo§/ §mx264§/ §B--§gvideo-preset§/ §mslow§/ §B--§gvideo-quality§/ §m22§/', '§Bx265§/: §B--§gvideo§/ §mx265§/ §B--§gvideo-preset§/ §mmedium§/ §B--§gvideo-quality§/ §m20§/']) - main.parse() + main = over.app.Main('over-video', version.str, 'AO-JSL', features={'config_file': True}) + main.add_option('audio', 'Audio codec to use, either vorbis<.>, pcm<.>, copy<.> or drop<.>.', str, ['vorbis'], abbr='a', count=1) + main.add_option('audio-quality', 'Audio encoding quality with -1<.> being the worst and 10<.> being the best.', float, [4], abbr='q', count=1) + main.add_option('video', 'Video codec to use, either x265<.>, x264<.>, theora<.>, copy<.> or drop<.>.', str, ['x264'], abbr='v', count=1) + main.add_option('video-preset', 'Video encoding preset, if supported by the selected encoder.', str, ['slow'], abbr='P', count=1) + main.add_option('video-quality', 'Video encoding quality (CRF). Use 0<.>-10<.> for Theora (0<.> being the lowest, 5<.>-7<.> is generally watchable) and 0<.>-51<.> for x264/5 (0<.> being lossless, 18<.>-28<.> is reasonable).', float, [22], abbr='Q', count=1) + main.add_option('context', 'Use .over-video file in CWD, if available, to remember encoding parameters per-directory.', bool, [True], abbr='C') + main.add_option('normalize', 'Normalize the audio track.', bool, [True], abbr='n') + main.add_option('normalize-target', 'Target mean volume to target.', float, [-20.0], count=1) + main.add_option('normalize-override', 'Volume correction to use instead of computing the required value in a (lengthy) pre-pass.', float, [0.0], abbr='N', count=1, in_cfg_file=False) + main.add_option('ffmpeg-vfilter', 'Raw ffmpeg -filter:v options, e.g. "scale=1280:trunc(ow/a/2)*2,transpose=dir=1<.>"', str, [''], abbr='F', count=1) + main.add_option('ffmpeg-map', 'Raw ffmpeg -map<.> options, e.g. --map<.> 0:1<.> --map<.> 0:2<.>. This is a drop-in fix until we get proper stream selection.', str, abbr='M', overwrite=False, count=1) + main.add_option('move-source', 'Move source file to this directory after conversion. Use an empty string to disable.', str, ['processed'], count=1) + main.add_option('dump-commands', 'Print ffmpeg commands that would be executed. If --normalize<.> is in effect, the normalization pre-pass will still be performed so that the proper volume correction can be computed.', bool, [False], abbr='D', in_cfg_file=False) + main.add_option('probe', 'Print the raw JSON output of ffprobe and exit.', bool, [False], abbr='p', in_cfg_file=False) + main.add_option('armed', 'Perform the suggested action.', bool, [False], abbr='A', in_cfg_file=False) + + main.add_doc('Description', ['Over-Video is a simple video converter.']) + main.add_doc('Good encoder settings', ['x264<.>: --video<.> x264<.> --video-preset<.> slow<.> --video-quality<.> 22<.>', 'x265<.>: --video<.> x265<.> --video-preset<.> medium<.> --video-quality<.> 20<.>']) + + main.setup() # -------------------------------------------------- # cfg checks - files = over.core.types.ndict() + files = over.types.ndict() audio_words = [] video_words = [] files.container = 'mkv' if main.cfg.context: - update_cfg_context(main, ["armed", "probe", "dump-commands", "ffmpeg-map", "normalize-override"]) + update_cfg_context(main, ["context", "armed", "probe", "dump-commands", "ffmpeg-map", "normalize-override"]) if main.cfg.audio in ('copy', 'drop'): - audio_words.append('§c%s§/' %(main.cfg.audio)) + audio_words.append('%s<.>' %(main.cfg.audio)) else: - audio_words.append('§gcodec§/=§m%s§/' %(main.cfg.audio)) + audio_words.append('codec<.>=%s<.>' %(main.cfg.audio)) if main.cfg.audio == 'vorbis': - audio_words.append('§gquality§/=§m%.1f§/' %(main.cfg.audio_quality)) + audio_words.append('quality<.>=%.1f<.>' %(main.cfg.audio_quality)) if main.cfg.normalize_override != 0: - audio_words.append('§gadjust_volume§/=§m%.1f dB§/' %(main.cfg.normalize_override)) + audio_words.append('adjust_volume<.>=%.1f dB<.>' %(main.cfg.normalize_override)) elif main.cfg.normalize: - audio_words.append('§gnormalize§/=§m%.1f dB§/' %(main.cfg.normalize_target)) + audio_words.append('normalize<.>=%.1f dB<.>' %(main.cfg.normalize_target)) if main.cfg.video in ('copy', 'drop'): - video_words.append('§c%s§/' %(main.cfg.video)) + video_words.append('%s<.>' %(main.cfg.video)) else: - video_words.append('§gcodec§/=§m%s§/' %(main.cfg.video)) - video_words.append('§gquality§/=§m%.1f§/' %(main.cfg.video_quality)) + video_words.append('codec<.>=%s<.>' %(main.cfg.video)) + video_words.append('quality<.>=%.1f<.>' %(main.cfg.video_quality)) if main.cfg.video_preset and main.cfg.video in ('x264', 'x265'): - video_words.append('§gpreset§/=§m%s§/' %(main.cfg.video_preset)) + video_words.append('preset<.>=%s<.>' %(main.cfg.video_preset)) if main.cfg.ffmpeg_vfilter: - video_words.append('§gvfilter§/=§m%s§/' %(main.cfg.ffmpeg_vfilter)) + video_words.append('vfilter<.>=%s<.>' %(main.cfg.ffmpeg_vfilter)) if main.cfg.video == 'drop': if main.cfg.audio == 'pcm': @@ -103,24 +103,22 @@ if __name__ == '__main__': elif main.cfg.audio == 'vorbis': files.container = 'ogg' - main.print('settings', prefix.start, suffix=':\n') + main.print('settings', main.print.tl.start, end=':\n') main.print('audio: %s' %(', '.join(audio_words))) main.print('video: %s' %(', '.join(video_words))) - main.print('container: §gtype§/=§m%s§/' %(files.container)) + main.print('container: type<.>=%s<.>' %(files.container)) if main.cfg.move_source: - main.print('move source files to §B%s§//' %(main.cfg.move_source)) + main.print('move source files to %s<.>/' %(main.cfg.move_source)) if main.cfg.audio not in ('drop', 'copy', 'pcm', 'vorbis'): - main.print('unknown audio codec', prefix.fail) - raise RuntimeError + raise ValueError('unknown audio codec: %s' %(main.cfg.audio)) if main.cfg.video not in ('drop', 'copy', 'theora', 'x264', 'x265'): - main.print('unknown video codec', prefix.fail) - raise RuntimeError + raise ValueError('unknown video codec: %s' %(main.cfg.video)) if not main.targets: - main.print('no files specified', prefix.warn) + main.print('no files specified', main.print.tl.warn) for tgt in main.targets: print() @@ -131,11 +129,11 @@ if __name__ == '__main__': files.move_infile_to = to_Path(main.cfg.move_source) / files.infile.name if main.cfg.move_source else None if not os.path.exists(tgt) or os.path.isdir(tgt): - main.print('target §y%s§/ §ris not a readable file§/, skipping' %(tgt), prefix.fail) + main.print('target %s<.> is not a readable file<.>, skipping' %(tgt), main.print.tl.fail) continue - original_filesize = over.core.text.Unit(files.infile.stat().st_size, 'o') - main.print('processing §B%s§/ (%s)' %(tgt, original_filesize), prefix.start) + original_filesize = over.text.Unit(files.infile.stat().st_size, 'o') + main.print('processing %s<.> (%s)' %(tgt, original_filesize), main.print.tl.start) # -------------------------------------------------- # identify the input file @@ -150,7 +148,7 @@ if __name__ == '__main__': print(identify_raw) continue - info = over.core.types.ndict() + info = over.types.ndict() try: info.duration = float(identify_dict['format']['duration']) @@ -162,10 +160,10 @@ if __name__ == '__main__': amount_as = len(audio_streams) if amount_vs > 1: - main.print('detected §r%d§/ video streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_vs), prefix.warn) + main.print('detected %d<.> video streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_vs), main.print.tl.warn) if amount_as > 1: - main.print('detected §y%d§/ audio streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_as), prefix.warn) + main.print('detected %d<.> audio streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_as), main.print.tl.warn) if video_streams: # ffmpeg picks the stream with the highest pixel count and then the lowest index @@ -174,14 +172,14 @@ if __name__ == '__main__': info.video_codec = video['codec_name'] info.video_size_x = video['width'] info.video_size_y = video['height'] - info.video_fps = over.core.text.Unit(parse_fps(video['r_frame_rate']), 'Hz') + info.video_fps = over.text.Unit(parse_fps(video['r_frame_rate']), 'Hz') if 'bit_rate' in video: - info.video_bitrate = over.core.text.Unit(video['bit_rate'], 'b/s') + info.video_bitrate = over.text.Unit(video['bit_rate'], 'b/s') elif 'tags' in video and 'BPS' in video['tags']: - info.video_bitrate = over.core.text.Unit(int(video['tags']['BPS']), 'b/s') + info.video_bitrate = over.text.Unit(int(video['tags']['BPS']), 'b/s') else: - info.video_bitrate = '§r??§/' + info.video_bitrate = '??<.>' info.pixel_fmt = video['pix_fmt'] else: info.video_fps = 30 # faked for progress bars @@ -192,24 +190,24 @@ if __name__ == '__main__': audio = audio_streams[0] info.audio_codec = audio['codec_name'] info.audio_channels = audio['channels'] - info.audio_samplerate = over.core.text.Unit(audio['sample_rate'], 'Hz') + info.audio_samplerate = over.text.Unit(audio['sample_rate'], 'Hz') info.audio_language = audio['tags']['language'] if 'tags' in audio and 'language' in audio['tags'] else 'und' - info.audio_bitrate = over.core.text.Unit(audio['bit_rate'], 'b/s') if 'bit_rate' in audio else '§r??§/' + info.audio_bitrate = over.text.Unit(audio['bit_rate'], 'b/s') if 'bit_rate' in audio else '??<.>' except: - main.print('exception while reading identify_dict, dump follows', prefix.fail) + main.print('exception while reading identify_dict, dump follows', main.print.tl.fail) print(identify_dict) raise if video_streams: - main.print('§mvideo§/: size=§b%d§/x§b%d§/ px, framerate=%s, codec=%s, bitrate=%s' %(info.video_size_x, info.video_size_y, info.video_fps, info.video_codec, info.video_bitrate)) + main.print('video<.>: size=%d<.>x%d<.> px, framerate=%s, codec=%s, bitrate=%s' %(info.video_size_x, info.video_size_y, info.video_fps, info.video_codec, info.video_bitrate)) else: - main.print('§mvideo§/: §yNone§/', prefix.warn) + main.print('video<.>: None<.>', main.print.tl.warn) if audio_streams: - main.print('§caudio§/: channels=§b%d§/, samplerate=%s, codec=%s, bitrate=%s, language=%s' %(info.audio_channels, info.audio_samplerate, info.audio_codec, info.audio_bitrate, info.audio_language)) + main.print('audio<.>: channels=%d<.>, samplerate=%s, codec=%s, bitrate=%s, language=%s' %(info.audio_channels, info.audio_samplerate, info.audio_codec, info.audio_bitrate, info.audio_language)) else: - main.print('§caudio§/: §yNone§/', prefix.warn) + main.print('audio<.>: None<.>', main.print.tl.warn) # -------------------------------------------------- # normalization pre-pass @@ -222,7 +220,7 @@ if __name__ == '__main__': command.normalize_prepass.INFILE = 'file:' + str(files.infile) command.normalize_prepass.run(stderr=True) - pb = over.core.text.ProgressBar2( + pb = over.text.ProgressBar( "§%a [§=a>§ A] §sa (Trem=§TA)", { "a": { @@ -264,7 +262,7 @@ if __name__ == '__main__': info.max_correction = -float(re.findall(b'max_volume: (-?\d+\.\d+) dB', output)[0]) info.volume_correction = main.cfg.normalize_target - info.mean_volume else: - main.print('§runexpected ffmpeg output§/, dump follows', prefix.fail, suffix=':\n') + main.print('unexpected ffmpeg output<.>, dump follows', main.print.tl.fail, suffix=':\n') print(output.decode('utf-8')) raise RuntimeError @@ -272,11 +270,11 @@ if __name__ == '__main__': if info.volume_correction > info.max_correction: d = info.volume_correction - info.max_correction - main.print("suggested correction is %.1f dB above the stream's maximum and will cause clipping" %(d), prefix.warn) + main.print("suggested correction is %.1f dB above the stream's maximum and will cause clipping" %(d), main.print.tl.warn) else: info.volume_correction = main.cfg.normalize_override - main.print('using user-supplied volume correction §m%.1f dB§/' %(info.volume_correction)) + main.print('using user-supplied volume correction %.1f dB<.>' %(info.volume_correction)) info.normalize_command = command.sub_normalize info.normalize_command.reset() @@ -345,7 +343,7 @@ if __name__ == '__main__': command.sub_x264.VFILTER = info.vfilter_command if info.pixel_fmt in X264_BANNED_PIXFMTS: - main.print('source pixel format §r%s§/ is incompatible with x264, forcing §yyuv420p§/' %(info.pixel_fmt), prefix.warn) + main.print('source pixel format %s<.> is incompatible with x264, forcing yuv420p<.>' %(info.pixel_fmt), main.print.tl.warn) command.sub_x264.PIXFMT = command.force_yuv420p else: command.sub_x264.PIXFMT = None @@ -367,14 +365,14 @@ if __name__ == '__main__': cmd = ' '.join(encode_cmd.dump(pretty=True)) if main.cfg.armed: - main.print('executing §B%s§/' %(cmd), prefix.start) + main.print('executing %s<.>' %(cmd), main.print.tl.start) else: - main.print('will execute §B%s§/' %(cmd)) + main.print('will execute %s<.>' %(cmd)) else: - main.print('will encode into §B%s§/' %(files.tmpfile)) + main.print('will encode into %s<.>' %(files.tmpfile)) if main.cfg.armed: - pb = over.core.text.ProgressBar2( + pb = over.text.ProgressBar( "§%f §rs [§=f>§ F] §sf (§ss) (Sest=§zs, Trem=§TF)", { "f": { @@ -415,14 +413,14 @@ if __name__ == '__main__': pb.render() - new_filesize = over.core.text.Unit(files.tmpfile.stat().st_size, 'o') + new_filesize = over.text.Unit(files.tmpfile.stat().st_size, 'o') pb.end() if encode_cmd.returncode == 0: - main.print('encoding finished: %s -> %s' %(original_filesize, new_filesize), prefix.done) + main.print('encoding finished: %s -> %s' %(original_filesize, new_filesize), main.print.tl.done) else: - main.print('§rencoding failed§/, ffmpeg returned §y%d§/' %(encode_cmd.returncode), prefix.fail) + main.print('encoding failed<.>, ffmpeg returned %d<.>' %(encode_cmd.returncode), main.print.tl.fail) raise RuntimeError # -------------------------------------------------- @@ -433,20 +431,20 @@ if __name__ == '__main__': if not move_to_dir.is_dir(): if main.cfg.armed: - main.print('creating directory §B%s§/' %(move_to_dir), prefix.start) + main.print('creating directory %s<.>' %(move_to_dir), main.print.tl.start) move_to_dir.mkdir() else: - main.print('will create directory §B%s§/' %(move_to_dir)) + main.print('will create directory %s<.>' %(move_to_dir)) if files.move_infile_to: if main.cfg.armed: - main.print('moving §B%s§/ -> §B%s§/' %(files.infile, files.move_infile_to), prefix.start) + main.print('moving %s<.> -> %s<.>' %(files.infile, files.move_infile_to), main.print.tl.start) files.infile.rename(files.move_infile_to) else: - main.print('will move §B%s§/ -> §B%s§/' %(files.infile, files.move_infile_to)) + main.print('will move %s<.> -> %s<.>' %(files.infile, files.move_infile_to)) if main.cfg.armed: - main.print('moving §B%s§/ -> §B%s§/' %(files.tmpfile, files.outfile), prefix.start) + main.print('moving %s<.> -> %s<.>' %(files.tmpfile, files.outfile), main.print.tl.start) files.tmpfile.rename(files.outfile) else: - main.print('will move §B%s§/ -> §B%s§/' %(files.tmpfile, files.outfile)) + main.print('will move %s<.> -> %s<.>' %(files.tmpfile, files.outfile)) diff --git a/version.py b/version.py index 519a9af..428c4f1 100644 --- a/version.py +++ b/version.py @@ -1,8 +1,8 @@ #! /usr/bin/env python3 # encoding: utf-8 -_version = (0, '00000000') # OVER_VERSION_IDENTIFIER - -rev = _version[0] -commit = _version[1] -str = '%d (%s)' %_version +major = 1 # VERSION_MAJOR_IDENTIFIER +minor = 99 # VERSION_MINOR_IDENTIFIER +# VERSION_LAST_MM 1.99 +patch = 0 # VERSION_PATCH_IDENTIFIER +str = ".".join(str(v) for v in (major, minor, patch))