Merge pull request #25 from Kyrch/CLI

feat: initial commit for new CLI milestone
This commit is contained in:
Kyrch
2023-06-18 14:05:15 -03:00
committed by GitHub
14 changed files with 181 additions and 38 deletions
+17 -7
View File
@@ -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**
@@ -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()
+116
View File
@@ -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)
@@ -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
+2 -2
View File
@@ -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.1',
author='AnimeThemes',
author_email='admin@animethemes.moe',
url='https://github.com/AnimeThemes/animethemes-batch-encoder',