mirror of
https://github.com/AnimeThemes/animethemes-batch-encoder.git
synced 2026-07-11 01:24:29 +02:00
140 lines
5.9 KiB
Python
140 lines
5.9 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
|
|
|
|
# Abstraction of the source file from which we are producing our encodes
|
|
# We are prefetching properties of the source file audio/video streams to help determine encoding argument values
|
|
class SourceFile:
|
|
format_args = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', '-show_chapters']
|
|
|
|
def __init__(self, file, file_format, selected_video_stream, selected_audio_stream, video_format, audio_format):
|
|
self.file = file
|
|
self.file_format = file_format
|
|
self.selected_video_stream = selected_video_stream
|
|
self.selected_audio_stream = selected_audio_stream
|
|
self.video_format = video_format
|
|
self.audio_format = audio_format
|
|
|
|
@classmethod
|
|
def from_file(cls, file, encoding_config):
|
|
video_file = None
|
|
audio_file = None
|
|
try:
|
|
file_format = SourceFile.get_file_format(file)
|
|
|
|
selected_video_stream = SourceFile.get_default_stream(file_format, 'video', encoding_config)
|
|
if selected_video_stream is None:
|
|
selected_video_stream = SourceFile.get_selected_stream(file_format, 'video')
|
|
|
|
selected_audio_stream = SourceFile.get_default_stream(file_format, 'audio', encoding_config)
|
|
if selected_audio_stream is None:
|
|
selected_audio_stream = SourceFile.get_selected_stream(file_format, 'audio')
|
|
|
|
logging.info('Retrieving extracted audio/video stream/format data...')
|
|
|
|
video_file = '[Video]' + file
|
|
logging.debug(f'[SourceFile.from_file] video_file: \'{video_file}\'')
|
|
audio_file = '[Audio]' + file
|
|
logging.debug(f'[SourceFile.from_file] audio_file: \'{audio_file}\'')
|
|
|
|
demux_args = ['ffmpeg', '-i', file, '-v', 'quiet', '-y', '-sn', '-dn', '-map',
|
|
f'0:v:{selected_video_stream}', '-vcodec', 'copy', video_file, '-map',
|
|
f'0:a:{selected_audio_stream}', '-acodec', 'copy', audio_file]
|
|
subprocess.call(demux_args)
|
|
|
|
video_args = SourceFile.format_args + [video_file]
|
|
video_format = subprocess.check_output(video_args).decode('utf-8')
|
|
video_format = json.loads(video_format)
|
|
|
|
os.remove(video_file)
|
|
logging.debug(f'[SourceFile.from_file] video_file deleted: {not os.path.isfile(video_file)}')
|
|
|
|
audio_args = SourceFile.format_args + [audio_file]
|
|
audio_format = subprocess.check_output(audio_args).decode('utf-8')
|
|
audio_format = json.loads(audio_format)
|
|
|
|
os.remove(audio_file)
|
|
logging.debug(f'[SourceFile.from_file] audio_file deleted: {not os.path.isfile(audio_file)}')
|
|
|
|
return cls(file, file_format, selected_video_stream, selected_audio_stream, video_format, audio_format)
|
|
except KeyboardInterrupt:
|
|
logging.info('Attempting to delete temp files after keyboard interrupt')
|
|
|
|
if os.path.isfile(video_file):
|
|
os.remove(video_file)
|
|
|
|
if os.path.isfile(audio_file):
|
|
os.remove(audio_file)
|
|
|
|
raise
|
|
|
|
# Source file streams/formats
|
|
@staticmethod
|
|
def get_file_format(file):
|
|
logging.info('Retrieving source file stream/format data...')
|
|
format_args = SourceFile.format_args + [file]
|
|
|
|
file_format = subprocess.check_output(format_args).decode('utf-8')
|
|
|
|
return json.loads(file_format)
|
|
|
|
# Get the number of streams of the codec type (audio/video)
|
|
@staticmethod
|
|
def get_stream_count(file_format, target_codec_type):
|
|
count = 0
|
|
|
|
for stream in file_format['streams']:
|
|
if stream['codec_type'] == target_codec_type:
|
|
count += 1
|
|
|
|
logging.debug(f'[SourceFile.get_stream_count] target_codec_type: \'{target_codec_type}\', count: \'{count}\'')
|
|
|
|
return count
|
|
|
|
# Validate default stream selection before prompting the user to specify which stream to use
|
|
@staticmethod
|
|
def get_default_stream(file_format, stream_type, encoding_config):
|
|
# Exit early if default stream is not set
|
|
default_stream = encoding_config.get_default_stream(stream_type)
|
|
if not default_stream:
|
|
return None
|
|
|
|
stream_count = SourceFile.get_stream_count(file_format, stream_type)
|
|
try:
|
|
default_stream = int(default_stream)
|
|
if default_stream in range(stream_count):
|
|
return default_stream
|
|
logging.error(f'Default stream selection \'{default_stream}\' is invalid')
|
|
except ValueError:
|
|
logging.error(f'Default stream selection \'{default_stream}\' must be an integer')
|
|
|
|
return None
|
|
|
|
# If there exists more than one stream for a codec type (audio/video),
|
|
# we want the user to specify which stream to use
|
|
@staticmethod
|
|
def get_selected_stream(file_format, stream_type):
|
|
stream_count = SourceFile.get_stream_count(file_format, stream_type)
|
|
if stream_count <= 1:
|
|
return 0
|
|
|
|
streams = range(stream_count)
|
|
prompt_text = f'Select {stream_type} stream [0-{stream_count - 1}]: '
|
|
while True:
|
|
try:
|
|
selected_stream = int(input(prompt_text))
|
|
if selected_stream in streams:
|
|
return selected_stream
|
|
logging.error('Stream selection is invalid')
|
|
except ValueError:
|
|
logging.error('Stream selection must be an integer')
|
|
|
|
# If our source file audio stream is not a 2-channel stereo layout, we need to resample it before normalization
|
|
def apply_audio_resampling(self, audio_filters):
|
|
channels = int(self.audio_format['streams'][0].get('channels', 2))
|
|
channel_layout = self.audio_format['streams'][0].get('channel_layout', 'stereo')
|
|
if channels != 2 or channel_layout != 'stereo':
|
|
audio_filters.append('aresample=ochl=stereo')
|