Skip to content

Commit

Permalink
fix: fixed regex search for loudnorm stats (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyrch authored Jan 18, 2025
1 parent 3ff73fd commit e47ec61
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 152 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Ideally we are iterating over a combination of filters and settings, picking the

### Usage

python -m batch_encoder [-h] [--generate | -g] [--execute | -e] [--custom | -c] [--file [FILE]] [--configfile [CONFIGFILE]] [--inputfile [INPUTFILES]] --loglevel [{debug,info,error}]
python -m batch_encoder [-h] [--generate | -g] [--execute | -e] [--custom | -c] [--file [FILE]] [--configfile [CONFIGFILE]] [--inputfile [INPUTFILES]] --loglevel [{debug,info,error}]

**Mode**

Expand Down
86 changes: 47 additions & 39 deletions batch_encoder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from ._cli import CLI
from ._seek_collector import SeekCollector
from ._source_file import SourceFile
from ._typing import Args, EncodingConfigType
from ._utils import commandfile_arg_type
from ._utils import configfile_arg_type
from appdirs import AppDirs
from typing import Literal

import argparse
import configparser
Expand All @@ -19,28 +21,39 @@

def main():
# Load/Validate Arguments
parser = argparse.ArgumentParser(prog='batch_encoder',
description='Generate/Execute FFmpeg commands for files in acting directory',
formatter_class=argparse.RawTextHelpFormatter)
parser = argparse.ArgumentParser(
prog='batch_encoder',
description='Generate/Execute FFmpeg commands for files in acting directory',
formatter_class=argparse.RawTextHelpFormatter
)

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('--custom', '-c', action='store_true', help='Customize some options for each seek')
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'
'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(
'--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'
'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('--inputfile', nargs='?', help='Set the input files separated by two commas')
parser.add_argument('--loglevel', nargs='?', default='info', choices=['debug', 'info', 'error'],
help='Set logging level')
args = parser.parse_args()
parser.add_argument('--loglevel', nargs='?', default='info', choices=['debug', 'info', 'error'], help='Set logging level')
args: Args = parser.parse_args()

# Logging Config
logging.basicConfig(stream=sys.stdout, level=logging.getLevelName(args.loglevel.upper()),
format='%(levelname)s: %(message)s')
logging.basicConfig(stream=sys.stdout, level=logging.getLevelName(args.loglevel.upper()), format='%(levelname)s: %(message)s')

# Env Check: Check that dependencies are installed
if shutil.which('ffmpeg') is None:
Expand All @@ -56,18 +69,20 @@ def main():
dirs = AppDirs('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,
EncodingConfig.config_encoding_modes: EncodingConfig.default_encoding_modes,
EncodingConfig.config_crfs: EncodingConfig.default_crfs,
EncodingConfig.config_cbr_bitrates: EncodingConfig.default_cbr_bitrates,
EncodingConfig.config_cbr_max_bitrates: EncodingConfig.default_cbr_max_bitrates,
EncodingConfig.config_threads: EncodingConfig.default_threads,
EncodingConfig.config_limit_size_enable: EncodingConfig.default_limit_size_enable,
EncodingConfig.config_alternate_source_files: EncodingConfig.default_alternate_source_files,
EncodingConfig.config_create_preview: EncodingConfig.default_create_preview,
EncodingConfig.config_include_unfiltered: EncodingConfig.default_include_unfiltered,
EncodingConfig.config_default_video_stream: '',
EncodingConfig.config_default_audio_stream: ''}
config['Encoding'] = {
EncodingConfig.config_allowed_filetypes: EncodingConfig.default_allowed_filetypes,
EncodingConfig.config_encoding_modes: EncodingConfig.default_encoding_modes,
EncodingConfig.config_crfs: EncodingConfig.default_crfs,
EncodingConfig.config_cbr_bitrates: EncodingConfig.default_cbr_bitrates,
EncodingConfig.config_cbr_max_bitrates: EncodingConfig.default_cbr_max_bitrates,
EncodingConfig.config_threads: EncodingConfig.default_threads,
EncodingConfig.config_limit_size_enable: EncodingConfig.default_limit_size_enable,
EncodingConfig.config_alternate_source_files: EncodingConfig.default_alternate_source_files,
EncodingConfig.config_create_preview: EncodingConfig.default_create_preview,
EncodingConfig.config_include_unfiltered: EncodingConfig.default_include_unfiltered,
EncodingConfig.config_default_video_stream: '',
EncodingConfig.config_default_audio_stream: '',
}
config['VideoFilters'] = EncodingConfig.default_video_filters

os.makedirs(os.path.dirname(config_file), exist_ok=True)
Expand All @@ -76,19 +91,12 @@ def main():

# Load config file
config.read(config_file)
encoding_config = EncodingConfig.from_config(config)
encoding_config: EncodingConfigType = EncodingConfig.from_config(config)

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 = CLI.choose_mode()
mode = CLI.choose_mode(args)

# Generate commands from source file candidates in current directory
if mode == 1 or mode == 3:
Expand Down Expand Up @@ -126,15 +134,15 @@ def main():
if args.custom:
print(f'\033[92mOutput Name: {seek.output_name}\033[0m')
new_encoding_config = CLI.custom_options(new_encoding_config)

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(new_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:
output_list = []
Expand Down
44 changes: 26 additions & 18 deletions batch_encoder/_bitrate_mode.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import enum
from enum import Enum


# The Bitrate Mode Enumerated List
# Bitrate Mode determines the rate control argument values for our commands
# Further Reading: https://developers.google.com/media/vp9/bitrate-modes
class BitrateMode(enum.Enum):
def __new__(cls, value, first_pass_rate_control, second_pass_rate_control):
obj = object.__new__(cls)
obj._value_ = value
obj.first_pass_rate_control = first_pass_rate_control
obj.second_pass_rate_control = second_pass_rate_control
return obj
class BitrateMode(Enum):
def __new__(cls, value, first_pass_rate_control, second_pass_rate_control):
obj = object.__new__(cls)
obj._value_ = value
obj.first_pass_rate_control = first_pass_rate_control
obj.second_pass_rate_control = second_pass_rate_control
return obj

# Constant Bitrate Mode
CBR = (0, lambda cbr_bitrate, cbr_max_bitrate, crf: f'-b:v {cbr_bitrate} -maxrate {cbr_max_bitrate} -qcomp 0.3',
lambda cbr_bitrate, cbr_max_bitrate,
crf: f'-b:v {cbr_bitrate} -maxrate {cbr_max_bitrate} -bufsize 6000k -qcomp 0.3')
# Variable Bitrate Mode / Constant Quality Mode
VBR = (1, lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v 0 -qcomp 0.7',
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v 0 -qcomp 0.7')
# Constrained Quality Mode
CQ = (2, lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v {cbr_bitrate} -qcomp 0.7',
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v {cbr_bitrate} -qcomp 0.7')
# Constant Bitrate Mode
CBR = (
0,
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-b:v {cbr_bitrate} -maxrate {cbr_max_bitrate} -qcomp 0.3',
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-b:v {cbr_bitrate} -maxrate {cbr_max_bitrate} -bufsize 6000k -qcomp 0.3'
)
# Variable Bitrate Mode / Constant Quality Mode
VBR = (
1,
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v 0 -qcomp 0.7',
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v 0 -qcomp 0.7'
)
# Constrained Quality Mode
CQ = (
2,
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v {cbr_bitrate} -qcomp 0.7',
lambda cbr_bitrate, cbr_max_bitrate, crf: f'-crf {crf} -b:v {cbr_bitrate} -qcomp 0.7'
)
54 changes: 34 additions & 20 deletions batch_encoder/_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from ._audio_filter import AudioFilter
from ._bitrate_mode import BitrateMode
from ._typing import Args, EncodingConfigType
from ._video_filter import VideoFilter
from typing import Literal

import inquirer
import logging
Expand All @@ -19,7 +21,7 @@ class CLI:
validate_digits = lambda _, x: all(y.strip().isdigit() for y in x.split(',')) or len(x.strip()) == 0

# Prompt the user for text questions
def prompt_text(message, validate=lambda _, x: x):
def prompt_text(message, validate=lambda _, x: x) -> str:
answer = inquirer.prompt([inquirer.Text('text', message=message, validate=validate)])

if answer is None:
Expand All @@ -28,9 +30,9 @@ def prompt_text(message, validate=lambda _, x: x):
logging.debug(f'[CLI.prompt_text] answer["text"]: \'{answer["text"]}\'')

return answer['text']

# Prompt the user for time questions
def prompt_time(message, validate=validate_time):
def prompt_time(message, validate=validate_time) -> str:
answer = inquirer.prompt([inquirer.Text('time', message=message, validate=validate)])

if answer is None:
Expand All @@ -41,19 +43,31 @@ def prompt_time(message, validate=validate_time):
return answer['time']

# Prompt the user for our mode options to run to the user
def choose_mode():
modes = [('Generate commands', 1), ('Execute commands', 2), ('Generate and execute commands', 3)]
answer = inquirer.prompt([inquirer.List('mode', message='Mode (Enter)', choices=modes)])
def choose_mode(args: Args) -> Literal[1, 2, 3]:
modes = [
('Generate commands', 1),
('Execute commands', 2),
('Generate and execute commands', 3)
]

if args.generate and args.execute:
return 3
elif args.generate:
return 1
elif args.execute:
return 2
else:
answer = inquirer.prompt([inquirer.List('mode', message='Mode (Enter)', choices=modes)])

if answer is None:
sys.exit()
if answer is None:
sys.exit()

logging.debug(f'[CLI.choose_mode] answer["mode"]: \'{answer["mode"]}\'')

logging.debug(f'[CLI.choose_mode] answer["mode"]: \'{answer["mode"]}\'')
return answer['mode']

return answer['mode']

# Prompt the user for source files to choose
def choose_source_files(source_files):
def choose_source_files(source_files) -> list[str]:
answer = inquirer.prompt([inquirer.Checkbox('source_files', message='Source Files (Space to select)', choices=source_files)])

if answer is None:
Expand All @@ -66,9 +80,9 @@ def choose_source_files(source_files):
logging.debug(f'[CLI.choose_source_files] answer["source_files"]: \'{answer["source_files"]}\'')

return answer['source_files']

# Prompt the user for audio filters options
def audio_filters_options(output_name):
def audio_filters_options(output_name) -> str:
af = AudioFilter.get_obj()
fadein, fadeout, mute, custom = (
AudioFilter.FADE_IN._value_[0],
Expand All @@ -90,7 +104,7 @@ def audio_filters_options(output_name):
elif answer['af'] == fadeout:
af[fadeout]['Start Time'] = CLI.prompt_time('Start Time').strip() or '0'
af[fadeout]['Exp'] = CLI.prompt_time('Exponential Value').strip() or '0'

elif answer['af'] == mute:
af[mute]['Start Time'] = CLI.prompt_time('Start Time').strip() or '0'
af[mute]['End Time'] = CLI.prompt_time('End Time').strip() or '0'
Expand Down Expand Up @@ -124,11 +138,11 @@ def audio_filters_options(output_name):
)

return ','.join(af_list)

# Prompt the user for our list of video filters
def video_filters(encoding_config):
def video_filters(encoding_config: EncodingConfigType) -> EncodingConfigType:
video_filters_options = VideoFilter.get_obj()

if encoding_config.include_unfiltered:
encoding_config.video_filters.append((None, 'No Filters'))

Expand All @@ -155,7 +169,7 @@ def video_filters(encoding_config):
return encoding_config

# Prompt the user for custom options if requested
def custom_options(encoding_config):
def custom_options(encoding_config: EncodingConfigType) -> EncodingConfigType:
create_preview = encoding_config.create_preview
limit_size_enable = encoding_config.limit_size_enable
encoding_modes = encoding_config.encoding_modes
Expand All @@ -171,7 +185,7 @@ def custom_options(encoding_config):

if answer is None:
return encoding_config

encoding_mode_questions = []
for encoding_mode in answer['encoding_modes'].split(','):
if encoding_mode == BitrateMode.VBR.name or encoding_mode == BitrateMode.CQ.name:
Expand Down
14 changes: 8 additions & 6 deletions batch_encoder/_colorspace.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import enum
from enum import Enum
import logging


# The Colorspace Enumerated List
# Parse color data from file to provide arguments for the encoded file
class Colorspace(enum.Enum):
class Colorspace(Enum):
def __new__(cls, colorspace, color_primaries, color_trc):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
Expand All @@ -28,16 +28,18 @@ def value_of(source_file):
source_colorspace = source_file.video_format['streams'][0].get('color_space', '')
source_color_primaries = source_file.video_format['streams'][0].get('color_primaries', '')
source_color_trc = source_file.video_format['streams'][0].get('color_transfer', '')

logging.debug(
f'[Colorspace.value_of] source_colorspace: {source_colorspace}, '
f'source_color_primaries: {source_color_primaries}, '
f'source_color_trc: {source_color_trc}')
f'source_color_trc: {source_color_trc}'
)

for colorspace_candidate in Colorspace:
if (
source_colorspace == colorspace_candidate.colorspace
or source_color_primaries == colorspace_candidate.color_primaries
or source_color_trc == colorspace_candidate.color_trc
source_colorspace == colorspace_candidate.colorspace
or source_color_primaries == colorspace_candidate.color_primaries
or source_color_trc == colorspace_candidate.color_trc
):
logging.debug(f'[Colorspace.value_of] carryover colorspace \'{colorspace_candidate.name}\' from source')
return colorspace_candidate
Expand Down
Loading

0 comments on commit e47ec61

Please sign in to comment.