From 1bbe09b6f084001228cc0e96b0453dc90614aa32 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sun, 18 Jun 2023 13:44:54 -0300 Subject: [PATCH 1/2] Initial Commit for Beta --- README.md | 24 +++++-- batch_encoder/__main__.py | 65 ++++++++++------- batch_encoder/_interface.py | 116 +++++++++++++++++++++++++++++++ batch_encoder/_seek_collector.py | 10 ++- setup.py | 4 +- 5 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 batch_encoder/_interface.py diff --git a/README.md b/README.md index c6de88b..e2472d9 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,25 @@ Ideally we are iterating over a combination of filters and settings, picking the **Install:** - pip install animethemes-batch-encoder + pip install animethemes-beta-batch-encoder ### Usage - python -m batch_encoder [-h] --mode [{1,2,3}] [--file [FILE]] [--configfile [CONFIGFILE]] --loglevel [{debug,info,error}] + python -m beta_batch_encoder [-h] [--generate | -g] [--execute | -e] [--file [FILE]] [--configfile [CONFIGFILE]] --loglevel [{debug,info,error}] **Mode** -`--mode 1` generates commands from input files in the current directory. +`--generate` generates commands from input files in the current directory. The user will be prompted for values that are not determined programmatically, such as inclusion/exclusion of a source file candidate, start time, end time, output file name and new audio filters. -`--mode 2` executes commands from file in the current directory line-by-line. +`--execute` executes commands from file in the current directory line-by-line. By default, the program looks for a file named `commands.txt` in the current directory. This file name can be specified by the `--file` argument. -`--mode 3` generates commands from input files in the current directory and executes the commands sequentially. +`--generate` and `--execute` generates commands from input files in the current directory and executes the commands sequentially. + +`None` will give modes options to run. **File** @@ -43,9 +45,17 @@ By default, the program will write to or read from `commands.txt` in the current The configuration file in which our encoding properties are defined. -By default, the program will write to or read from `batch_encoder.ini` in the user config directory of appname `batch_encoder` and author `AnimeThemes`. +By default, the program will write to or read from `beta_batch_encoder.ini` in the user config directory of appname `beta_batch_encoder` and author `AnimeThemes`. -Example: `C:\Users\Kyrch\AppData\Local\AnimeThemes\batch_encoder\batch_encoder.ini` +Example: `C:\Users\Kyrch\AppData\Local\AnimeThemes\beta_batch_encoder\beta_batch_encoder.ini` + +**Audio Filters** + +* `Exit` Saves audio filters if selected and continues script execution. +* `Custom` Apply a custom audio filter string. +* `Fade In` Select an exponential value to apply Fade In. +* `Fade Out` Select a start position and an exponential value to Fade Out. +* `Mute` Select a start and end position to leave the volume at 0. **Encoding Properties** diff --git a/batch_encoder/__main__.py b/batch_encoder/__main__.py index 119b7d4..b001c65 100644 --- a/batch_encoder/__main__.py +++ b/batch_encoder/__main__.py @@ -1,5 +1,6 @@ from ._encode_webm import EncodeWebM from ._encoding_config import EncodingConfig +from ._interface import Interface from ._seek_collector import SeekCollector from ._source_file import SourceFile from ._utils import commandfile_arg_type @@ -17,19 +18,17 @@ import sys def main(): # Load/Validate Arguments - parser = argparse.ArgumentParser(prog='batch_encoder', + parser = argparse.ArgumentParser(prog='beta_batch_encoder', description='Generate/Execute FFmpeg commands for files in acting directory', formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('--mode', nargs='?', type=int, choices=[1, 2, 3], required=True, - help='1: Generate commands and write to file\n' - '2: Execute commands from file\n' - '3: Generate and execute commands') + parser.add_argument('--generate', '-g', action='store_true',help='Generate commands and write to file') + parser.add_argument('--execute', '-e', action='store_true', help='Execute commands from file') parser.add_argument('--file', nargs='?', default='commands.txt', type=commandfile_arg_type, help='1: Name of file commands are written to (default: commands.txt)\n' '2: Name of file commands are executed from (default: commands.txt)\n' '3: Name of file commands are written to (default: commands.txt)') - parser.add_argument('--configfile', nargs='?', default='batch_encoder.ini', type=configfile_arg_type, - help='Name of config file (default: batch_encoder.ini)\n' + parser.add_argument('--configfile', nargs='?', default='beta_batch_encoder.ini', type=configfile_arg_type, + help='Name of config file (default: beta_batch_encoder.ini)\n' 'If the file does not exist, default configuration will be written\n' 'The file is expected to exist in the same directory as this script') parser.add_argument('--loglevel', nargs='?', default='info', choices=['debug', 'info', 'error'], @@ -51,7 +50,7 @@ def main(): # Write default config file if it doesn't exist config = configparser.ConfigParser() - dirs = AppDirs('batch_encoder', 'AnimeThemes') + dirs = AppDirs('beta_batch_encoder', 'AnimeThemes') config_file = os.path.join(dirs.user_config_dir, args.configfile) if not os.path.exists(config_file): config['Encoding'] = {EncodingConfig.config_allowed_filetypes: EncodingConfig.default_allowed_filetypes, @@ -75,32 +74,46 @@ def main(): commands = [] + # Set the mode to integer or prompt to the user + if args.generate and args.execute: + mode = 3 + elif args.generate: + mode = 1 + elif args.execute: + mode = 2 + else: + mode = Interface.choose_mode() + # Generate commands from source file candidates in current directory - if args.mode == 1 or args.mode == 3: + if mode == 1 or mode == 3: source_file_candidates = [f for f in os.listdir('.') if f.endswith(tuple(encoding_config.allowed_filetypes))] if not source_file_candidates: logging.error('No source file candidates in current directory') sys.exit() + source_file_candidates = Interface.choose_source_files(source_file_candidates) + source_file = {} + for source_file_candidate in source_file_candidates: - if SourceFile.yes_or_no(source_file_candidate): - try: - source_file = SourceFile.from_file(source_file_candidate, encoding_config) + source_file[source_file_candidate] = SourceFile.from_file(source_file_candidate, encoding_config) - is_collector_valid = False - seek_collector = None - while not is_collector_valid: - seek_collector = SeekCollector(source_file) - is_collector_valid = seek_collector.is_valid() + for file, file_value in source_file.items(): + try: + is_collector_valid = False + seek_collector = None + while not is_collector_valid: + print(f'\033[92mSource File: {file}\033[0m') + seek_collector = SeekCollector(file_value) + is_collector_valid = seek_collector.is_valid() - for seek in seek_collector.get_seek_list(): - logging.info(f'Generating commands with seek ss: \'{seek.ss}\', to: \'{seek.to}\'') - encode_webm = EncodeWebM(source_file, seek) - load_commands = encode_webm.get_commands(encoding_config) - commands = commands + load_commands - except KeyboardInterrupt: - logging.info(f'Exiting from inclusion of file \'{source_file_candidate}\' after keyboard interrupt') + for seek in seek_collector.get_seek_list(): + logging.info(f'Generating commands with seek ss: \'{seek.ss}\', to: \'{seek.to}\'') + encode_webm = EncodeWebM(file_value, seek) + load_commands = encode_webm.get_commands(encoding_config) + commands = commands + load_commands + except KeyboardInterrupt: + logging.info(f'Exiting from inclusion of file \'{file}\' after keyboard interrupt') # Alternate lines per source files if encoding_config.alternate_source_files == True: @@ -120,14 +133,14 @@ def main(): f.write(command + '\n') # Execute commands in memory and write commands to file if requested - if args.mode == 3: + if mode == 3: logging.info(f'Executing {len(commands)} commands...') for command in commands: subprocess.call(command, shell=True) # Read and execute commands from file - if args.mode == 2: + if mode == 2: if not os.path.isfile(args.file): logging.error(f'File \'{args.file}\' does not exist') sys.exit() diff --git a/batch_encoder/_interface.py b/batch_encoder/_interface.py new file mode 100644 index 0000000..8e4987c --- /dev/null +++ b/batch_encoder/_interface.py @@ -0,0 +1,116 @@ +import inquirer +import logging +import re + +class Interface: + # Time Duration Specification: https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax + time_pattern = re.compile('^([0-5]?\d:){1,2}[0-5]?\d(?=\.\d+$|$)|\d+(?=\.\d+$|$)') + + # Prompt the mode options to run to the user + def choose_mode(): + modes = { + 'Generate commands': 1, + 'Execute commands': 2, + 'Generate and execute commands': 3 + } + + question = [inquirer.List('mode', message='Mode (Enter)', choices=list(modes.keys()))] + answer = inquirer.prompt(question) + + logging.debug(f'[Interface.choose_mode] answer["mode"]: \'{answer["mode"]}\'') + + return modes[answer['mode']] + + # Prompt the source files to choose + def choose_source_files(source_files): + question = [inquirer.Checkbox('source_files', message='Source Files (Space)', choices=source_files)] + answer = inquirer.prompt(question) + + logging.debug(f'[Interface.choose_source_files] answer["source_files"]: \'{answer["source_files"]}\'') + + return answer['source_files'] + + # Prompt the audio filters options + def audio_filters_options(output_name): + audio_filters = { + 'Exit': False, + 'Custom': '', + 'Fade In': 0, + 'Fade Out': { + 'Start Time': 0, + 'Exp': 0 + }, + 'Mute': { + 'Start Time': 0, + 'End Time': 0 + } + } + + while not audio_filters['Exit']: + print(f'\n\033[92mOutput Name: {output_name}\033[0m') + question = [inquirer.List('audio_filters', message='Audio Filters (Enter)', choices=list(audio_filters.keys()))] + answer = inquirer.prompt(question) + + if answer['audio_filters'] == 'Fade In': + while True: + exp_value = input('Exponential Value: ').strip() or '0' + + if exp_value == 0 or Interface.time_pattern.match(exp_value): + audio_filters['Fade In'] = exp_value + break + else: + logging.error('Invalid Time') + + elif answer['audio_filters'] == 'Fade Out': + while True: + start_time = input('Start Time: ').strip() or '0' + exp_value = input('Exponential Value: ').strip() or '0' + + if Interface.time_pattern.match(start_time) and Interface.time_pattern.match(exp_value): + audio_filters['Fade Out']['Start Time'] = start_time + audio_filters['Fade Out']['Exp'] = exp_value + break + else: + logging.error('Invalid Time') + + elif answer['audio_filters'] == 'Mute': + while True: + start_time = input('Start Time: ').strip() or '0' + end_time = input('End Time: ').strip() or '0' + + if Interface.time_pattern.match(start_time) and Interface.time_pattern.match(end_time): + audio_filters['Mute']['Start Time'] = start_time + audio_filters['Mute']['End Time'] = end_time + break + else: + logging.error('Invalid Time') + + elif answer['audio_filters'] == 'Custom': + custom_audio_filter = input('Custom Audio Filter(s): ').strip() + audio_filters['Custom'] = custom_audio_filter + + else: + audio_filters['Exit'] = True + + audio_filters_list = [] + + if float(audio_filters['Fade In']) > 0: + audio_filters_list.append(f"afade=d={audio_filters['Fade In']}:curve=exp") + if float(audio_filters['Fade Out']['Exp']) > 0: + audio_filters_list.append(f"afade=t=out:st={audio_filters['Fade Out']['Start Time']}:d={audio_filters['Fade Out']['Exp']}") + if float(audio_filters['Mute']['Start Time']) > 0 or float(audio_filters['Mute']['End Time']) > 0: + audio_filters_list.append(f"volume=enable='between(t,{audio_filters['Mute']['Start Time']},{audio_filters['Mute']['End Time']})':volume=0") + if len(audio_filters['Custom']) > 0: + audio_filters_list.append(audio_filters['Custom']) + + logging.debug( + f'[Interface.audio_filters_options] ' + f'audio_filters["Fade In"]: \'{audio_filters["Fade In"]}\', ' + f'audio_filters["Fade Out"]["Start Time"]: \'{audio_filters["Fade Out"]["Start Time"]}\', ' + f'audio_filters["Fade Out"]["Exp"]: \'{audio_filters["Fade Out"]["Exp"]}\', ' + f'audio_filters["Mute"]["Start Time"]: \'{audio_filters["Mute"]["Start Time"]}\', ' + f'audio_filters["Mute"]["End Time"]: \'{audio_filters["Mute"]["End Time"]}\', ' + f'audio_filters["Custom"]: \'{audio_filters["Custom"]}\'' + ) + + return ','.join(audio_filters_list) \ No newline at end of file diff --git a/batch_encoder/_seek_collector.py b/batch_encoder/_seek_collector.py index 4debaf0..3e643a5 100644 --- a/batch_encoder/_seek_collector.py +++ b/batch_encoder/_seek_collector.py @@ -1,3 +1,4 @@ +from ._interface import Interface from ._seek import Seek from ._utils import string_to_seconds @@ -23,7 +24,7 @@ class SeekCollector: self.start_positions = SeekCollector.prompt_time('Start time(s): ') self.end_positions = SeekCollector.prompt_time('End time(s): ') self.output_names = SeekCollector.prompt_output_name() - self.new_audio_filters = SeekCollector.prompt_new_audio_filters() + self.new_audio_filters = SeekCollector.prompt_new_audio_filters(self) # Prompt the user for our list of starting/ending positions of our WebMs # For starting positions, a blank input value is the 0 position of the source file @@ -57,8 +58,11 @@ class SeekCollector: # Prompt the user for ours list of audio filters of ours WebMs @staticmethod - def prompt_new_audio_filters(): - new_audio_filters = input('Audio Filter(s): ').split(',,') + def prompt_new_audio_filters(self): + new_audio_filters = [] + for output_name in self.output_names: + new_audio_filters.append(Interface.audio_filters_options(output_name)) + return new_audio_filters # Integrity Test 1: Our lists should be of equal length diff --git a/setup.py b/setup.py index 5c6acb6..bbe3dcd 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ with open('README.md') as f: long_description = f.read() setup( - name='animethemes-batch-encoder', - version='1.3.1', + name='animethemes-beta-batch-encoder', + version='1.0', author='AnimeThemes', author_email='admin@animethemes.moe', url='https://github.com/AnimeThemes/animethemes-batch-encoder', From b5c756a498745702d0779201bf835e288f252594 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sun, 18 Jun 2023 14:02:07 -0300 Subject: [PATCH 2/2] fix: folder name incorrect --- {batch_encoder => beta_batch_encoder}/__init__.py | 0 {batch_encoder => beta_batch_encoder}/__main__.py | 0 {batch_encoder => beta_batch_encoder}/_bitrate_mode.py | 0 {batch_encoder => beta_batch_encoder}/_colorspace.py | 0 {batch_encoder => beta_batch_encoder}/_encode_webm.py | 0 {batch_encoder => beta_batch_encoder}/_encoding_config.py | 0 {batch_encoder => beta_batch_encoder}/_interface.py | 0 {batch_encoder => beta_batch_encoder}/_loudnorm_filter.py | 0 {batch_encoder => beta_batch_encoder}/_seek.py | 0 {batch_encoder => beta_batch_encoder}/_seek_collector.py | 0 {batch_encoder => beta_batch_encoder}/_source_file.py | 0 {batch_encoder => beta_batch_encoder}/_utils.py | 0 setup.py | 2 +- 13 files changed, 1 insertion(+), 1 deletion(-) rename {batch_encoder => beta_batch_encoder}/__init__.py (100%) rename {batch_encoder => beta_batch_encoder}/__main__.py (100%) rename {batch_encoder => beta_batch_encoder}/_bitrate_mode.py (100%) rename {batch_encoder => beta_batch_encoder}/_colorspace.py (100%) rename {batch_encoder => beta_batch_encoder}/_encode_webm.py (100%) rename {batch_encoder => beta_batch_encoder}/_encoding_config.py (100%) rename {batch_encoder => beta_batch_encoder}/_interface.py (100%) rename {batch_encoder => beta_batch_encoder}/_loudnorm_filter.py (100%) rename {batch_encoder => beta_batch_encoder}/_seek.py (100%) rename {batch_encoder => beta_batch_encoder}/_seek_collector.py (100%) rename {batch_encoder => beta_batch_encoder}/_source_file.py (100%) rename {batch_encoder => beta_batch_encoder}/_utils.py (100%) diff --git a/batch_encoder/__init__.py b/beta_batch_encoder/__init__.py similarity index 100% rename from batch_encoder/__init__.py rename to beta_batch_encoder/__init__.py diff --git a/batch_encoder/__main__.py b/beta_batch_encoder/__main__.py similarity index 100% rename from batch_encoder/__main__.py rename to beta_batch_encoder/__main__.py diff --git a/batch_encoder/_bitrate_mode.py b/beta_batch_encoder/_bitrate_mode.py similarity index 100% rename from batch_encoder/_bitrate_mode.py rename to beta_batch_encoder/_bitrate_mode.py diff --git a/batch_encoder/_colorspace.py b/beta_batch_encoder/_colorspace.py similarity index 100% rename from batch_encoder/_colorspace.py rename to beta_batch_encoder/_colorspace.py diff --git a/batch_encoder/_encode_webm.py b/beta_batch_encoder/_encode_webm.py similarity index 100% rename from batch_encoder/_encode_webm.py rename to beta_batch_encoder/_encode_webm.py diff --git a/batch_encoder/_encoding_config.py b/beta_batch_encoder/_encoding_config.py similarity index 100% rename from batch_encoder/_encoding_config.py rename to beta_batch_encoder/_encoding_config.py diff --git a/batch_encoder/_interface.py b/beta_batch_encoder/_interface.py similarity index 100% rename from batch_encoder/_interface.py rename to beta_batch_encoder/_interface.py diff --git a/batch_encoder/_loudnorm_filter.py b/beta_batch_encoder/_loudnorm_filter.py similarity index 100% rename from batch_encoder/_loudnorm_filter.py rename to beta_batch_encoder/_loudnorm_filter.py diff --git a/batch_encoder/_seek.py b/beta_batch_encoder/_seek.py similarity index 100% rename from batch_encoder/_seek.py rename to beta_batch_encoder/_seek.py diff --git a/batch_encoder/_seek_collector.py b/beta_batch_encoder/_seek_collector.py similarity index 100% rename from batch_encoder/_seek_collector.py rename to beta_batch_encoder/_seek_collector.py diff --git a/batch_encoder/_source_file.py b/beta_batch_encoder/_source_file.py similarity index 100% rename from batch_encoder/_source_file.py rename to beta_batch_encoder/_source_file.py diff --git a/batch_encoder/_utils.py b/beta_batch_encoder/_utils.py similarity index 100% rename from batch_encoder/_utils.py rename to beta_batch_encoder/_utils.py diff --git a/setup.py b/setup.py index bbe3dcd..74be7a7 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open('README.md') as f: setup( name='animethemes-beta-batch-encoder', - version='1.0', + version='1.1', author='AnimeThemes', author_email='admin@animethemes.moe', url='https://github.com/AnimeThemes/animethemes-batch-encoder',