inc
This commit is contained in:
128
bin/ffx.py
128
bin/ffx.py
@@ -362,6 +362,32 @@ def generateDispositionTokens(subDescriptor):
|
||||
return dispositionTokens
|
||||
|
||||
|
||||
def searchSubtitleFiles(dir, prefix):
|
||||
|
||||
sesl_match = re.compile(SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
|
||||
|
||||
availableFileSubtitles = []
|
||||
for subtitleFilename in os.listdir(dir):
|
||||
if subtitleFilename.startswith(prefix) and subtitleFilename.endswith('.' + SUBTITLE_FILE_EXTENSION):
|
||||
sesl_result = sesl_match.search(subtitleFilename)
|
||||
if sesl_result is not None:
|
||||
subtitleFilePath = os.path.join(dir, subtitleFilename)
|
||||
if os.path.isfile(subtitleFilePath):
|
||||
|
||||
subtitleFileDescriptor = {}
|
||||
subtitleFileDescriptor['path'] = subtitleFilePath
|
||||
subtitleFileDescriptor['season'] = int(sesl_result.group(1))
|
||||
subtitleFileDescriptor['episode'] = int(sesl_result.group(2))
|
||||
subtitleFileDescriptor['stream'] = int(sesl_result.group(3))
|
||||
subtitleFileDescriptor['language'] = sesl_result.group(4)
|
||||
|
||||
availableFileSubtitles.append(subtitleFileDescriptor)
|
||||
|
||||
click.echo(f"Found {len(availableFileSubtitles)} subtitles in files\n")
|
||||
|
||||
return availableFileSubtitles
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def ffx(ctx):
|
||||
@@ -434,7 +460,7 @@ def streams(filename):
|
||||
@click.option("-c", "--clear-metadata", is_flag=True, default=False)
|
||||
@click.option("-d", "--denoise", is_flag=True, default=False)
|
||||
|
||||
@click.option("-j", "--no-jellyfin-tweaks", is_flag=True, default=False)
|
||||
@click.option("-j", "--jellyfin", is_flag=True, default=False)
|
||||
|
||||
@click.option("--dry-run", is_flag=True, default=False)
|
||||
|
||||
@@ -461,7 +487,7 @@ def convert(ctx,
|
||||
output_directory,
|
||||
clear_metadata,
|
||||
denoise,
|
||||
no_jellyfin_tweaks,
|
||||
jellyfin,
|
||||
dry_run):
|
||||
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
||||
|
||||
@@ -472,8 +498,8 @@ def convert(ctx,
|
||||
|
||||
startTime = time.perf_counter()
|
||||
|
||||
context = ctx.obj
|
||||
|
||||
context = ctx.obj
|
||||
|
||||
click.echo(f"\nVideo encoder: {video_encoder}")
|
||||
|
||||
@@ -482,7 +508,6 @@ def convert(ctx,
|
||||
|
||||
click.echo(f"Qualities: {q_list}")
|
||||
|
||||
|
||||
context['bitrates'] = {}
|
||||
context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
|
||||
context['bitrates']['ac3'] = str(ac3_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k"
|
||||
@@ -492,67 +517,45 @@ def convert(ctx,
|
||||
click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
||||
click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
|
||||
|
||||
# Parse subtitle files
|
||||
context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
|
||||
|
||||
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
||||
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
||||
sesl_match = re.compile(SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
|
||||
|
||||
|
||||
availableFileSubtitles = []
|
||||
if context['import_subtitles']:
|
||||
for subtitleFilename in os.listdir(subtitle_directory):
|
||||
if subtitleFilename.startswith(subtitle_prefix) and subtitleFilename.endswith('.' + SUBTITLE_FILE_EXTENSION):
|
||||
sesl_result = sesl_match.search(subtitleFilename)
|
||||
if sesl_result is not None:
|
||||
subtitleFilePath = os.path.join(subtitle_directory, subtitleFilename)
|
||||
if os.path.isfile(subtitleFilePath):
|
||||
subtitleFileDescriptor = {}
|
||||
subtitleFileDescriptor['path'] = subtitleFilePath
|
||||
subtitleFileDescriptor['season'] = int(sesl_result.group(1))
|
||||
subtitleFileDescriptor['episode'] = int(sesl_result.group(2))
|
||||
subtitleFileDescriptor['stream'] = int(sesl_result.group(3))
|
||||
subtitleFileDescriptor['language'] = sesl_result.group(4)
|
||||
availableFileSubtitles.append(subtitleFileDescriptor)
|
||||
|
||||
click.echo(f"Found {len(availableFileSubtitles)} subtitles in files")
|
||||
# Parse subtitle files
|
||||
context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
|
||||
availableFileSubtitles = searchSubtitleFiles(subtitle_directory, subtitle_prefix) if context['import_subtitles'] else []
|
||||
|
||||
|
||||
subtitleLanguages = subtitle_language
|
||||
subtitleTitles = subtitle_title
|
||||
|
||||
# Overwrite audio tags if set
|
||||
audioLanguages = audio_language
|
||||
audioTitles = audio_title
|
||||
|
||||
# Overwrite subtitle tags if set
|
||||
subtitleLanguages = subtitle_language
|
||||
subtitleTitles = subtitle_title
|
||||
|
||||
|
||||
# Process crop parameters
|
||||
context['perform_crop'] = (crop != 'none')
|
||||
|
||||
if context['perform_crop']:
|
||||
|
||||
cropTokens = crop.split(',')
|
||||
|
||||
if cropTokens and len(cropTokens) == 2:
|
||||
|
||||
context['crop_start'], context['crop_length'] = crop.split(',')
|
||||
else:
|
||||
context['crop_start'] = DEFAULT_CROP_START
|
||||
context['crop_length'] = DEFAULT_CROP_LENGTH
|
||||
|
||||
click.echo(f"crop start={context['crop_start']} length={context['crop_length']}")
|
||||
|
||||
|
||||
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
||||
|
||||
|
||||
click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
||||
|
||||
|
||||
job_index = 0
|
||||
|
||||
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
||||
click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
||||
|
||||
for sourcePath in existingSourcePaths:
|
||||
|
||||
# Separate basedir, basename and extension for current source file
|
||||
sourceDirectory = os.path.dirname(sourcePath)
|
||||
sourceFilename = os.path.basename(sourcePath)
|
||||
sourcePathTokens = sourceFilename.split('.')
|
||||
@@ -564,9 +567,10 @@ def convert(ctx,
|
||||
sourceFileBasename = sourceFilename
|
||||
sourceFilenameExtension = ''
|
||||
|
||||
|
||||
click.echo(f"\nProcessing file {sourcePath}")
|
||||
|
||||
|
||||
# Determine season and episode if present in current filename
|
||||
season_digits = 2
|
||||
episode_digits = 2
|
||||
index_digits = 3
|
||||
@@ -589,7 +593,7 @@ def convert(ctx,
|
||||
print(f"season={season} episode={episode} file={file_index}")
|
||||
|
||||
|
||||
# File specific tokens
|
||||
# Assemble target filename tokens
|
||||
targetFilenameTokens = []
|
||||
targetFilenameExtension = DEFAULT_FILE_EXTENSION
|
||||
|
||||
@@ -608,15 +612,17 @@ def convert(ctx,
|
||||
|
||||
|
||||
try:
|
||||
streamDescriptor = getStreamDescriptor(sourcePath)
|
||||
sourceStreamDescriptor = getStreamDescriptor(sourcePath)
|
||||
targetStreamDescriptor = sourceStreamDescriptor.copy()
|
||||
|
||||
except Exception:
|
||||
click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...")
|
||||
continue
|
||||
|
||||
for aStream in streamDescriptor[STREAM_TYPE_AUDIO]:
|
||||
for aStream in sourceStreamDescriptor[STREAM_TYPE_AUDIO]:
|
||||
click.echo(f"audio stream lang={aStream['language']}")
|
||||
|
||||
for sStream in streamDescriptor[STREAM_TYPE_SUBTITLE]:
|
||||
for sStream in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]:
|
||||
click.echo(f"subtitle stream lang={sStream['language']}")
|
||||
|
||||
|
||||
@@ -639,11 +645,10 @@ def convert(ctx,
|
||||
if streamIndex <= len(subtitleTitles) -1:
|
||||
mSubtitles[streamIndex]['title'] = subtitleTitles[streamIndex]
|
||||
|
||||
if default_subtitle == -1 or no_jellyfin_tweaks:
|
||||
matchingSubtitles = mSubtitles
|
||||
else:
|
||||
if default_subtitle != -1 and jellyfin:
|
||||
matchingSubtitles = getReorderedSubstreams(mSubtitles, default_subtitle)
|
||||
|
||||
else:
|
||||
matchingSubtitles = mSubtitles
|
||||
|
||||
|
||||
|
||||
@@ -660,8 +665,8 @@ def convert(ctx,
|
||||
audioTokens = []
|
||||
|
||||
# Source stream descriptors
|
||||
audioStreams = streamDescriptor[STREAM_TYPE_AUDIO]
|
||||
subtitleStreams = streamDescriptor[STREAM_TYPE_SUBTITLE]
|
||||
audioStreams = sourceStreamDescriptor[STREAM_TYPE_AUDIO]
|
||||
subtitleStreams = sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]
|
||||
|
||||
# Set language and title in source stream descriptors if given per command line option
|
||||
for streamIndex in range(len(audioStreams)):
|
||||
@@ -689,10 +694,7 @@ def convert(ctx,
|
||||
for streamIndex in range(len(audioStreams)):
|
||||
audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_audio else 0
|
||||
|
||||
if no_jellyfin_tweaks:
|
||||
sourceAudioStreams = audioStreams
|
||||
else:
|
||||
sourceAudioStreams = getReorderedSubstreams(audioStreams, default_audio)
|
||||
sourceAudioStreams = getReorderedSubstreams(audioStreams, default_audio) if jellyfin else audioStreams
|
||||
|
||||
dispositionTokens += generateDispositionTokens(sourceAudioStreams)
|
||||
|
||||
@@ -708,10 +710,7 @@ def convert(ctx,
|
||||
for streamIndex in range(len(subtitleStreams)):
|
||||
subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_subtitle else 0
|
||||
|
||||
if no_jellyfin_tweaks:
|
||||
sourceSubtitleStreams = subtitleStreams
|
||||
else:
|
||||
sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, default_subtitle)
|
||||
sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, default_subtitle) if jellyfin else subtitleStreams
|
||||
|
||||
dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
||||
|
||||
@@ -736,10 +735,17 @@ def convert(ctx,
|
||||
subtitleMetadataTokens = []
|
||||
if context['import_subtitles']:
|
||||
|
||||
for fileIndex in range(len(matchingSubtitles)):
|
||||
numMatchingSubtitles = len(matchingSubtitles)
|
||||
|
||||
if jellyfin and default_subtitle != -1:
|
||||
subtitleSequence = getModifiedStreamOrder(numMatchingSubtitles, default_subtitle)
|
||||
else:
|
||||
subtitleSequence = range(numMatchingSubtitles)
|
||||
|
||||
for fileIndex in range(numMatchingSubtitles):
|
||||
|
||||
# Create mapping for subtitle streams when imported from files
|
||||
mappingTokens += ['-map', f"{fileIndex+1}:s:0"]
|
||||
mappingTokens += ['-map', f"{subtitleSequence[fileIndex]+1}:s:0"]
|
||||
|
||||
msg = matchingSubtitles[fileIndex]
|
||||
subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"language={msg['language']}"]
|
||||
@@ -789,7 +795,7 @@ def convert(ctx,
|
||||
+ generateAV1Tokens(q, preset) + audioTokens)
|
||||
|
||||
if clear_metadata:
|
||||
commandSequence += generateClearTokens(streamDescriptor)
|
||||
commandSequence += generateClearTokens(sourceStreamDescriptor)
|
||||
|
||||
if context['perform_crop']:
|
||||
commandSequence += generateCropTokens(context['crop_start'], context['crop_length'])
|
||||
@@ -833,7 +839,7 @@ def convert(ctx,
|
||||
commandSequence2 += generateVP9Pass2Tokens(q) + audioTokens
|
||||
|
||||
if clear_metadata:
|
||||
commandSequence2 += generateClearTokens(streamDescriptor)
|
||||
commandSequence2 += generateClearTokens(sourceStreamDescriptor)
|
||||
|
||||
if context['perform_crop']:
|
||||
commandSequence2 += generateCropTokens(context['crop_start'], context['crop_length'])
|
||||
|
||||
Reference in New Issue
Block a user