|
|
@ -15,8 +15,7 @@ DEFAULT_QUALITY = 23
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_AV1_PRESET = 5
|
|
|
|
DEFAULT_AV1_PRESET = 5
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_LABEL='output'
|
|
|
|
DEFAULT_FILE_EXTENSION = 'webm'
|
|
|
|
DEFAULT_FILE_SUFFIX = 'webm'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_STEREO_BANDWIDTH = "128"
|
|
|
|
DEFAULT_STEREO_BANDWIDTH = "128"
|
|
|
|
DEFAULT_AC3_BANDWIDTH = "256"
|
|
|
|
DEFAULT_AC3_BANDWIDTH = "256"
|
|
|
@ -50,9 +49,8 @@ STREAM_LAYOUT_5_1 = '5.1(side)'
|
|
|
|
STREAM_LAYOUT_STEREO = 'stereo'
|
|
|
|
STREAM_LAYOUT_STEREO = 'stereo'
|
|
|
|
STREAM_LAYOUT_6CH = '6ch'
|
|
|
|
STREAM_LAYOUT_6CH = '6ch'
|
|
|
|
|
|
|
|
|
|
|
|
SEASON_EPISODE_INDICATOR_MATCH = '([sS][0-9]+)([eE][0-9]+)'
|
|
|
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
|
|
|
SEASON_INDICATOR_MATCH = '([sS][0-9]+)'
|
|
|
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
|
|
|
EPISODE_INDICATOR_MATCH = '([eE][0-9]+)'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardScreen(Screen):
|
|
|
|
class DashboardScreen(Screen):
|
|
|
@ -129,7 +127,7 @@ def executeProcess(commandSequence):
|
|
|
|
|
|
|
|
|
|
|
|
output, error = process.communicate()
|
|
|
|
output, error = process.communicate()
|
|
|
|
|
|
|
|
|
|
|
|
return output
|
|
|
|
return output.decode('utf-8'), error.decode('utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -142,11 +140,14 @@ def executeProcess(commandSequence):
|
|
|
|
|
|
|
|
|
|
|
|
def getStreamDescriptor(filename):
|
|
|
|
def getStreamDescriptor(filename):
|
|
|
|
|
|
|
|
|
|
|
|
ffprobeOutput = executeProcess(["ffprobe",
|
|
|
|
ffprobeOutput, ffprobeError = executeProcess(["ffprobe",
|
|
|
|
"-show_streams",
|
|
|
|
"-show_streams",
|
|
|
|
"-of", "json",
|
|
|
|
"-of", "json",
|
|
|
|
filename])
|
|
|
|
filename])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if 'Invalid data found when processing input' in ffprobeError:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
streamData = json.loads(ffprobeOutput)['streams']
|
|
|
|
streamData = json.loads(ffprobeOutput)['streams']
|
|
|
|
|
|
|
|
|
|
|
|
descriptor = []
|
|
|
|
descriptor = []
|
|
|
@ -249,16 +250,16 @@ def generateOutputTokens(f, suffix, q=None):
|
|
|
|
# if presetTokens:
|
|
|
|
# if presetTokens:
|
|
|
|
# preset = int(presetTokens[0].split('=')[1])
|
|
|
|
# preset = int(presetTokens[0].split('=')[1])
|
|
|
|
|
|
|
|
|
|
|
|
# cropStart = ''
|
|
|
|
# ctx.obj['crop_start'] = ''
|
|
|
|
# cropLength = ''
|
|
|
|
# ctx.obj['crop_length'] = ''
|
|
|
|
# cropTokens = [c for c in sys.argv if c.startswith('crop')]
|
|
|
|
# cropTokens = [c for c in sys.argv if c.startswith('crop')]
|
|
|
|
# if cropTokens:
|
|
|
|
# if cropTokens:
|
|
|
|
# if '=' in cropTokens[0]:
|
|
|
|
# if '=' in cropTokens[0]:
|
|
|
|
# cropString = cropTokens[0].split('=')[1]
|
|
|
|
# cropString = cropTokens[0].split('=')[1]
|
|
|
|
# cropStart, cropLength = cropString.split(',')
|
|
|
|
# ctx.obj['crop_start'], ctx.obj['crop_length'] = cropString.split(',')
|
|
|
|
# else:
|
|
|
|
# else:
|
|
|
|
# cropStart = 60
|
|
|
|
# ctx.obj['crop_start'] = 60
|
|
|
|
# cropLength = 180
|
|
|
|
# ctx.obj['crop_length'] = 180
|
|
|
|
#
|
|
|
|
#
|
|
|
|
# denoiseTokens = [d for d in sys.argv if d.startswith('denoise')]
|
|
|
|
# denoiseTokens = [d for d in sys.argv if d.startswith('denoise')]
|
|
|
|
#
|
|
|
|
#
|
|
|
@ -336,7 +337,10 @@ def help():
|
|
|
|
@click.argument('filename', nargs=1)
|
|
|
|
@click.argument('filename', nargs=1)
|
|
|
|
@ffx.command()
|
|
|
|
@ffx.command()
|
|
|
|
def streams(filename):
|
|
|
|
def streams(filename):
|
|
|
|
for d in getStreamDescriptor(filename):
|
|
|
|
sd = getStreamDescriptor(filename)
|
|
|
|
|
|
|
|
if sd is None:
|
|
|
|
|
|
|
|
raise click.ClickException('This file does not contain any audiovisual data')
|
|
|
|
|
|
|
|
for d in sd:
|
|
|
|
click.echo(f"{d['codec']}{' (' + str(d['channels']) + ')' if d['type'] == 'audio' else ''}")
|
|
|
|
click.echo(f"{d['codec']}{' (' + str(d['channels']) + ')' if d['type'] == 'audio' else ''}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -345,7 +349,7 @@ def streams(filename):
|
|
|
|
@click.pass_context
|
|
|
|
@click.pass_context
|
|
|
|
|
|
|
|
|
|
|
|
@click.argument('paths', nargs=-1)
|
|
|
|
@click.argument('paths', nargs=-1)
|
|
|
|
@click.option('-l', '--label', type=str, default=DEFAULT_LABEL, help='Label to be used as filename prefix')
|
|
|
|
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
|
|
|
|
|
|
|
|
|
|
|
@click.option('-v', '--video-encoder', type=str, default=DEFAULT_VIDEO_ENCODER, help='Target video encoder (vp9 or av1) default: vp9')
|
|
|
|
@click.option('-v', '--video-encoder', type=str, default=DEFAULT_VIDEO_ENCODER, help='Target video encoder (vp9 or av1) default: vp9')
|
|
|
|
|
|
|
|
|
|
|
@ -418,9 +422,9 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"\nRunning {len(paths) * len(q_list)} jobs")
|
|
|
|
click.echo(f"\nRunning {len(paths) * len(q_list)} jobs")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
job_index = 0
|
|
|
|
|
|
|
|
|
|
|
|
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
|
|
|
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
|
|
|
s_match = re.compile(SEASON_INDICATOR_MATCH)
|
|
|
|
|
|
|
|
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
|
|
|
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -442,97 +446,146 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|
|
|
sourceFileBasename = sourceFilename
|
|
|
|
sourceFileBasename = sourceFilename
|
|
|
|
sourceFilenameExtension = ''
|
|
|
|
sourceFilenameExtension = ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"dir={sourceDirectory} base={sourceFileBasename} ext={sourceFilenameExtension}")
|
|
|
|
#click.echo(f"dir={sourceDirectory} base={sourceFileBasename} ext={sourceFilenameExtension}")
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"\nProcessing file {sourcePath}")
|
|
|
|
click.echo(f"\nProcessing file {sourcePath}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
season_digits = 2
|
|
|
|
|
|
|
|
episode_digits = 2
|
|
|
|
|
|
|
|
index_digits = 3
|
|
|
|
|
|
|
|
|
|
|
|
se_result = se_match.search(sourceFilename)
|
|
|
|
se_result = se_match.search(sourceFilename)
|
|
|
|
s_result = s_match.search(sourceFilename)
|
|
|
|
|
|
|
|
e_result = e_match.search(sourceFilename)
|
|
|
|
e_result = e_match.search(sourceFilename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
season = -1
|
|
|
|
|
|
|
|
episode = -1
|
|
|
|
|
|
|
|
file_index = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if se_result is not None:
|
|
|
|
|
|
|
|
season = int(se_result.group(1))
|
|
|
|
|
|
|
|
episode = int(se_result.group(2))
|
|
|
|
|
|
|
|
elif e_result is not None:
|
|
|
|
|
|
|
|
episode = int(e_result.group(1))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
file_index += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(f"season={season} episode={episode} file={file_index}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
targetFilenameTokens = []
|
|
|
|
|
|
|
|
|
|
|
|
#streamDescriptor = getStreamDescriptor(sourcePath)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#commandTokens = COMMAND_TOKENS + [sourcePath]
|
|
|
|
targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#for q in q_list:
|
|
|
|
if label:
|
|
|
|
|
|
|
|
targetFilenameTokens = [label]
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"\nRunning job q={q}")
|
|
|
|
if season > -1 and episode > -1:
|
|
|
|
|
|
|
|
targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"]
|
|
|
|
|
|
|
|
elif episode > -1:
|
|
|
|
|
|
|
|
targetFilenameTokens += [f"E{episode:0{episode_digits}d}"]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
targetFilenameTokens += [f"{file_index:0{index_digits}d}"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
targetFilenameTokens = [sourceFileBasename]
|
|
|
|
|
|
|
|
|
|
|
|
#mappingVideoTokens = ['-map', 'v:0']
|
|
|
|
# In case source and target filenames are the same add an extension to distinct output from input
|
|
|
|
#mappingTokens = mappingVideoTokens.copy()
|
|
|
|
if sourceFilenameExtension == targetFilenameExtension:
|
|
|
|
#audioTokens = []
|
|
|
|
targetFilenameTokens += ['ffx']
|
|
|
|
|
|
|
|
|
|
|
|
#audioIndex = 0
|
|
|
|
|
|
|
|
#for audioStreamDescriptor in streamDescriptor:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if audioStreamDescriptor['type'] == STREAM_TYPE_AUDIO:
|
|
|
|
targetFilename = '_'.join(targetFilenameTokens) + '.' + targetFilenameExtension
|
|
|
|
|
|
|
|
|
|
|
|
#mappingTokens += ['-map', f"a:{audioIndex}"]
|
|
|
|
click.echo(f"target filename: {targetFilename}")
|
|
|
|
#audioTokens += generateAudioTokens(ctx.obj, audioIndex, audioStreamDescriptor['layout'])
|
|
|
|
|
|
|
|
#audioIndex += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
streamDescriptor = getStreamDescriptor(sourcePath)
|
|
|
|
|
|
|
|
|
|
|
|
#for s in range(len([d for d in streamDescriptor if d['type'] == STREAM_TYPE_SUBTITLE])):
|
|
|
|
if streamDescriptor is None:
|
|
|
|
#mappingTokens += ['-map', f"s:{s}"]
|
|
|
|
click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandTokens = COMMAND_TOKENS + [sourcePath]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if video_encoder == 'av1':
|
|
|
|
for q in q_list:
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens(q, preset) + audioTokens
|
|
|
|
click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}")
|
|
|
|
|
|
|
|
job_index += 1
|
|
|
|
|
|
|
|
|
|
|
|
#if clear_metadata:
|
|
|
|
mappingVideoTokens = ['-map', 'v:0']
|
|
|
|
#commandSequence += generateClearTokens(streamDescriptor)
|
|
|
|
mappingTokens = mappingVideoTokens.copy()
|
|
|
|
|
|
|
|
audioTokens = []
|
|
|
|
|
|
|
|
|
|
|
|
#if performCrop:
|
|
|
|
audioIndex = 0
|
|
|
|
#commandSequence += generateCropTokens(cropStart, cropLength)
|
|
|
|
for audioStreamDescriptor in streamDescriptor:
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_SUFFIX, q)
|
|
|
|
if audioStreamDescriptor['type'] == STREAM_TYPE_AUDIO:
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"Command: {' '.join(commandSequence)}")
|
|
|
|
mappingTokens += ['-map', f"a:{audioIndex}"]
|
|
|
|
|
|
|
|
audioTokens += generateAudioTokens(ctx.obj, audioIndex, audioStreamDescriptor['layout'])
|
|
|
|
|
|
|
|
audioIndex += 1
|
|
|
|
|
|
|
|
|
|
|
|
#executeProcess(commandSequence)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for s in range(len([d for d in streamDescriptor if d['type'] == STREAM_TYPE_SUBTITLE])):
|
|
|
|
|
|
|
|
mappingTokens += ['-map', f"s:{s}"]
|
|
|
|
|
|
|
|
|
|
|
|
#if video_encoder == 'vp9':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if performCrop:
|
|
|
|
if video_encoder == 'av1':
|
|
|
|
# commandSequence1 += generateCropTokens(cropStart, cropLength)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence1 += NULL_TOKENS
|
|
|
|
commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens(q, preset) + audioTokens
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"Command 1: {' '.join(commandSequence1)}")
|
|
|
|
if clear_metadata:
|
|
|
|
|
|
|
|
commandSequence += generateClearTokens(streamDescriptor)
|
|
|
|
|
|
|
|
|
|
|
|
#if os.path.exists(TEMP_FILE_NAME):
|
|
|
|
if ctx.obj['perform_crop']:
|
|
|
|
# os.remove(TEMP_FILE_NAME)
|
|
|
|
commandSequence += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length'])
|
|
|
|
|
|
|
|
|
|
|
|
#executeProcess(commandSequence1)
|
|
|
|
commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_EXTENSION, q)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"Command: {' '.join(commandSequence)}")
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence2 = commandTokens + mappingTokens
|
|
|
|
# executeProcess(commandSequence)
|
|
|
|
|
|
|
|
|
|
|
|
#if denoise:
|
|
|
|
|
|
|
|
# commandSequence2 += generateDenoiseTokens()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence2 += generateVP9Pass2Tokens(q) + audioTokens
|
|
|
|
if video_encoder == 'vp9':
|
|
|
|
|
|
|
|
|
|
|
|
#if clear_metadata:
|
|
|
|
commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q)
|
|
|
|
# commandSequence2 += generateClearTokens(streamDescriptor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if performCrop:
|
|
|
|
if ctx.obj['perform_crop']:
|
|
|
|
# commandSequence2 += generateCropTokens(cropStart, cropLength)
|
|
|
|
commandSequence1 += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandSequence1 += NULL_TOKENS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
click.echo(f"Command 1: {' '.join(commandSequence1)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(TEMP_FILE_NAME):
|
|
|
|
|
|
|
|
os.remove(TEMP_FILE_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# executeProcess(commandSequence1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandSequence2 = commandTokens + mappingTokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if denoise:
|
|
|
|
|
|
|
|
commandSequence2 += generateDenoiseTokens()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandSequence2 += generateVP9Pass2Tokens(q) + audioTokens
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if clear_metadata:
|
|
|
|
|
|
|
|
commandSequence2 += generateClearTokens(streamDescriptor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ctx.obj['perform_crop']:
|
|
|
|
|
|
|
|
commandSequence2 += generateCropTokens(ctx.obj['crop_start'], ctx.obj['crop_length'])
|
|
|
|
|
|
|
|
|
|
|
|
#commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_SUFFIX, q)
|
|
|
|
commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_EXTENSION, q)
|
|
|
|
|
|
|
|
|
|
|
|
#click.echo(f"Command 2: {' '.join(commandSequence2)}")
|
|
|
|
click.echo(f"Command 2: {' '.join(commandSequence2)}")
|
|
|
|
|
|
|
|
|
|
|
|
#executeProcess(commandSequence2)
|
|
|
|
# executeProcess(commandSequence2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#app = ModesApp(ctx.obj)
|
|
|
|
#app = ModesApp(ctx.obj)
|
|
|
|