mirror of
https://github.com/AnimeThemes/animethemes-batch-encoder.git
synced 2026-07-11 01:24:29 +02:00
Merge pull request #25 from Kyrch/CLI
feat: initial commit for new CLI milestone
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user