inc dispo
This commit is contained in:
178
bin/ffx.py
178
bin/ffx.py
@@ -37,7 +37,7 @@ MKVMERGE_METADATA_KEYS = ['BPS',
|
|||||||
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
|
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
|
||||||
|
|
||||||
|
|
||||||
COMMAND_TOKENS = ['ffmpeg', '-y', '-i']
|
COMMAND_TOKENS = ['ffmpeg', '-y']
|
||||||
NULL_TOKENS = ['-f', 'null', '/dev/null']
|
NULL_TOKENS = ['-f', 'null', '/dev/null']
|
||||||
|
|
||||||
STREAM_TYPE_VIDEO = 'video'
|
STREAM_TYPE_VIDEO = 'video'
|
||||||
@@ -51,6 +51,9 @@ STREAM_LAYOUT_6CH = '6ch'
|
|||||||
|
|
||||||
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
|
||||||
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'
|
||||||
|
SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
|
||||||
|
|
||||||
|
SUBTITLE_FILE_EXTENSION = 'vtt'
|
||||||
|
|
||||||
|
|
||||||
class DashboardScreen(Screen):
|
class DashboardScreen(Screen):
|
||||||
@@ -408,8 +411,17 @@ def streams(filename):
|
|||||||
@click.option('-ac3', '--ac3-bitrate', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {DEFAULT_AC3_BANDWIDTH})")
|
@click.option('-ac3', '--ac3-bitrate', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {DEFAULT_AC3_BANDWIDTH})")
|
||||||
@click.option('-dts', '--dts-bitrate', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})")
|
@click.option('-dts', '--dts-bitrate', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})")
|
||||||
|
|
||||||
|
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
|
||||||
|
@click.option('-sl', '--subtitle-label', type=str, default='', help='Subtitle filename prefix')
|
||||||
|
|
||||||
|
@click.option('-ss', '--subtitle-language', type=str, default='', help='Subtitle stream language(s)')
|
||||||
|
@click.option('-st', '--subtitle-title', type=str, default='', help='Subtitle stream title(s)')
|
||||||
|
|
||||||
@click.option('-ds', '--default-subtitle', type=int, default=-1, help='Index of default subtitle stream')
|
@click.option('-ds', '--default-subtitle', type=int, default=-1, help='Index of default subtitle stream')
|
||||||
@click.option('-fa', '--forced-subtitle', type=int, default=-1, help='Index of forced subtitle stream') # (including default audio stream tag)
|
@click.option('-fs', '--forced-subtitle', type=int, default=-1, help='Index of forced subtitle stream') # (including default audio stream tag)
|
||||||
|
|
||||||
|
@click.option('-as', '--audio-language', type=str, default='', help='Audio stream language(s)')
|
||||||
|
@click.option('-at', '--audio-title', type=str, default='', help='Audio stream title(s)')
|
||||||
|
|
||||||
@click.option('-da', '--default-audio', type=int, default=-1, help='Index of default audio stream')
|
@click.option('-da', '--default-audio', type=int, default=-1, help='Index of default audio stream')
|
||||||
|
|
||||||
@@ -427,7 +439,30 @@ def streams(filename):
|
|||||||
@click.option("--dry-run", is_flag=True, default=False)
|
@click.option("--dry-run", is_flag=True, default=False)
|
||||||
|
|
||||||
|
|
||||||
def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3_bitrate, dts_bitrate, default_subtitle, forced_subtitle, default_audio, crop, output_directory, clear_metadata, denoise, no_jellyfin_tweaks, dry_run):
|
def convert(ctx,
|
||||||
|
paths,
|
||||||
|
label,
|
||||||
|
video_encoder,
|
||||||
|
quality,
|
||||||
|
preset,
|
||||||
|
stereo_bitrate,
|
||||||
|
ac3_bitrate,
|
||||||
|
dts_bitrate,
|
||||||
|
subtitle_directory,
|
||||||
|
subtitle_label,
|
||||||
|
subtitle_language,
|
||||||
|
subtitle_title,
|
||||||
|
default_subtitle,
|
||||||
|
forced_subtitle,
|
||||||
|
audio_language,
|
||||||
|
audio_title,
|
||||||
|
default_audio,
|
||||||
|
crop,
|
||||||
|
output_directory,
|
||||||
|
clear_metadata,
|
||||||
|
denoise,
|
||||||
|
no_jellyfin_tweaks,
|
||||||
|
dry_run):
|
||||||
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
||||||
|
|
||||||
Files found under PATHS will be converted according to parameters.
|
Files found under PATHS will be converted according to parameters.
|
||||||
@@ -457,6 +492,41 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|||||||
click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
||||||
click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
|
click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
|
||||||
|
|
||||||
|
# Parse subtitle files
|
||||||
|
context['import_subtitles'] = (subtitle_directory and subtitle_label)
|
||||||
|
|
||||||
|
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
||||||
|
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
||||||
|
sesl_match = re.compile(SEASON_EPISODE_STREAM_LANGUAGE_MATCH)
|
||||||
|
|
||||||
|
|
||||||
|
availableSubtitles = []
|
||||||
|
if context['import_subtitles']:
|
||||||
|
for subtitleFilename in os.listdir(subtitle_directory):
|
||||||
|
if subtitleFilename.startswith(subtitle_label) 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)
|
||||||
|
availableSubtitles.append(subtitleFileDescriptor)
|
||||||
|
|
||||||
|
click.echo(f"Found {len(availableSubtitles)} subtitles in files")
|
||||||
|
|
||||||
|
|
||||||
|
subtitleLanguages = subtitle_language.split(',') if subtitle_language else []
|
||||||
|
subtitleTitles = subtitle_title.split(',') if subtitle_title else []
|
||||||
|
|
||||||
|
audioLanguages = audio_language.split(',') if audio_language else []
|
||||||
|
audioTitles = audio_title.split(',') if audio_title else []
|
||||||
|
|
||||||
|
|
||||||
|
# Process crop parameters
|
||||||
context['perform_crop'] = (crop != 'none')
|
context['perform_crop'] = (crop != 'none')
|
||||||
|
|
||||||
if context['perform_crop']:
|
if context['perform_crop']:
|
||||||
@@ -481,10 +551,6 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|||||||
|
|
||||||
job_index = 0
|
job_index = 0
|
||||||
|
|
||||||
se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH)
|
|
||||||
e_match = re.compile(EPISODE_INDICATOR_MATCH)
|
|
||||||
|
|
||||||
|
|
||||||
for sourcePath in existingSourcePaths:
|
for sourcePath in existingSourcePaths:
|
||||||
|
|
||||||
sourceDirectory = os.path.dirname(sourcePath)
|
sourceDirectory = os.path.dirname(sourcePath)
|
||||||
@@ -554,52 +620,134 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|||||||
click.echo(f"subtitle stream lang={sStream['language']}")
|
click.echo(f"subtitle stream lang={sStream['language']}")
|
||||||
|
|
||||||
|
|
||||||
|
commandTokens = COMMAND_TOKENS + ['-i', sourcePath]
|
||||||
|
|
||||||
|
subtitleFileTokens = []
|
||||||
|
matchingSubtitles = []
|
||||||
|
if context['import_subtitles']:
|
||||||
|
subtitles = [a for a in availableSubtitles if a['season'] == season and a['episode'] == episode]
|
||||||
|
|
||||||
|
mSubtitles = sorted(subtitles, key=lambda d: d['stream'])
|
||||||
|
|
||||||
|
for sfd in mSubtitles:
|
||||||
|
subtitleFileTokens += ['-i', sfd['path']]
|
||||||
|
|
||||||
|
for streamIndex in range(len(mSubtitles)):
|
||||||
|
mSubtitles[streamIndex]['forced'] = 1 if forced_subtitle != -1 and streamIndex == forced_subtitle else 0
|
||||||
|
mSubtitles[streamIndex]['default'] = 1 if default_subtitle != -1 and streamIndex == default_subtitle else 0
|
||||||
|
|
||||||
|
if default_subtitle == -1:
|
||||||
|
matchingSubtitles = mSubtitles
|
||||||
|
else:
|
||||||
|
matchingSubtitles = getReorderedSubstreams(mSubtitles, default_subtitle)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
commandTokens = COMMAND_TOKENS + [sourcePath]
|
|
||||||
|
|
||||||
for q in q_list:
|
for q in q_list:
|
||||||
|
|
||||||
click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}")
|
click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}")
|
||||||
job_index += 1
|
job_index += 1
|
||||||
|
|
||||||
mappingVideoTokens = ['-map', 'v:0']
|
mappingVideoTokens = ['-map', '0:v:0']
|
||||||
|
|
||||||
mappingTokens = mappingVideoTokens.copy()
|
mappingTokens = mappingVideoTokens.copy()
|
||||||
dispositionTokens = []
|
dispositionTokens = []
|
||||||
|
|
||||||
audioTokens = []
|
audioTokens = []
|
||||||
|
|
||||||
|
# Source stream descriptors
|
||||||
audioStreams = streamDescriptor[STREAM_TYPE_AUDIO]
|
audioStreams = streamDescriptor[STREAM_TYPE_AUDIO]
|
||||||
subtitleStreams = streamDescriptor[STREAM_TYPE_SUBTITLE]
|
subtitleStreams = streamDescriptor[STREAM_TYPE_SUBTITLE]
|
||||||
|
|
||||||
|
# Set language and title in source stream descriptors if given per command line option
|
||||||
|
for streamIndex in range(len(audioStreams)):
|
||||||
|
if 'tags' not in audioStreams[streamIndex].keys():
|
||||||
|
audioStreams[streamIndex]['tags'] = {}
|
||||||
|
|
||||||
|
if streamIndex <= len(audioLanguages) - 1:
|
||||||
|
audioStreams[streamIndex]['tags']['language'] = audioLanguages[streamIndex]
|
||||||
|
if streamIndex <= len(audioTitles) - 1:
|
||||||
|
audioStreams[streamIndex]['tags']['title'] = audioTitles[streamIndex]
|
||||||
|
|
||||||
|
for streamIndex in range(len(subtitleStreams)):
|
||||||
|
if 'tags' not in subtitleStreams[streamIndex].keys():
|
||||||
|
subtitleStreams[streamIndex]['tags'] = {}
|
||||||
|
|
||||||
|
if streamIndex <= len(subtitleLanguages) - 1:
|
||||||
|
subtitleStreams[streamIndex]['tags']['language'] = subtitleLanguages[streamIndex]
|
||||||
|
if streamIndex <= len(subtitleTitles) - 1:
|
||||||
|
subtitleStreams[streamIndex]['tags']['title'] = subtitleTitles[streamIndex]
|
||||||
|
|
||||||
|
# Reorder audio stream descriptors and create disposition options if default is given per command line option
|
||||||
if default_audio == -1:
|
if default_audio == -1:
|
||||||
sourceAudioStreams = audioStreams
|
sourceAudioStreams = audioStreams
|
||||||
else:
|
else:
|
||||||
for streamIndex in range(len(audioStreams)):
|
for streamIndex in range(len(audioStreams)):
|
||||||
audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_audio else 0
|
audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_audio else 0
|
||||||
|
|
||||||
sourceAudioStreams = getReorderedSubstreams(audioStreams, default_audio)
|
sourceAudioStreams = getReorderedSubstreams(audioStreams, default_audio)
|
||||||
dispositionTokens += generateDispositionTokens(sourceAudioStreams)
|
dispositionTokens += generateDispositionTokens(sourceAudioStreams)
|
||||||
|
|
||||||
|
# Set forced tag in subtitle descriptor if given per command line option
|
||||||
if forced_subtitle != -1:
|
if forced_subtitle != -1:
|
||||||
for streamIndex in range(len(subtitleStreams)):
|
for streamIndex in range(len(subtitleStreams)):
|
||||||
subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forced_subtitle else 0
|
subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forced_subtitle else 0
|
||||||
|
|
||||||
|
# Reorder subtitle stream descriptors and create disposition options if default is given per command line option
|
||||||
if default_subtitle == -1:
|
if default_subtitle == -1:
|
||||||
sourceSubtitleStreams = subtitleStreams
|
sourceSubtitleStreams = subtitleStreams
|
||||||
else:
|
else:
|
||||||
for streamIndex in range(len(subtitleStreams)):
|
for streamIndex in range(len(subtitleStreams)):
|
||||||
subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_subtitle else 0
|
subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == default_subtitle else 0
|
||||||
|
|
||||||
sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, default_subtitle)
|
sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, default_subtitle)
|
||||||
dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
||||||
|
|
||||||
|
|
||||||
for audioStream in sourceAudioStreams:
|
audioMetadataTokens = []
|
||||||
mappingTokens += ['-map', f"a:{audioStream['src_sub_index']}"]
|
for audioStreamIndex in range(len(sourceAudioStreams)):
|
||||||
|
|
||||||
|
audioStream = sourceAudioStreams[audioStreamIndex]
|
||||||
|
|
||||||
|
# Create mapping and ffmpeg options for audio streams
|
||||||
|
mappingTokens += ['-map', f"0:a:{audioStream['src_sub_index']}"]
|
||||||
audioTokens += generateAudioTokens(context, audioStream['src_sub_index'], audioStream['layout'])
|
audioTokens += generateAudioTokens(context, audioStream['src_sub_index'], audioStream['layout'])
|
||||||
|
|
||||||
for subtitleStream in sourceSubtitleStreams:
|
if 'tags' in audioStream.keys():
|
||||||
|
if 'language' in audioStream['tags'].keys():
|
||||||
|
audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"language={audioStream['tags']['language']}"]
|
||||||
|
if 'title' in audioStream['tags'].keys():
|
||||||
|
audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"title={audioStream['tags']['title']}"]
|
||||||
|
|
||||||
|
|
||||||
|
# Create mapping and ffmpeg options for subtitle streams
|
||||||
|
subtitleMetadataTokens = []
|
||||||
|
if context['import_subtitles']:
|
||||||
|
|
||||||
|
for fileIndex in range(len(matchingSubtitles)):
|
||||||
|
|
||||||
|
# Create mapping for subtitle streams when imported from files
|
||||||
|
mappingTokens += ['-map', f"{fileIndex+1}:s:0"]
|
||||||
|
|
||||||
|
msg = matchingSubtitles[fileIndex]
|
||||||
|
subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"language={msg['language']}"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
for subtitleStreamIndex in range(len(sourceSubtitleStreams)):
|
||||||
|
|
||||||
|
subtitleStream = sourceSubtitleStreams[subtitleStreamIndex]
|
||||||
|
|
||||||
|
# Create mapping for subtitle streams
|
||||||
mappingTokens += ['-map', f"s:{subtitleStream['src_sub_index']}"]
|
mappingTokens += ['-map', f"s:{subtitleStream['src_sub_index']}"]
|
||||||
|
|
||||||
|
if 'tags' in subtitleStream.keys():
|
||||||
|
if 'language' in subtitleStream['tags'].keys():
|
||||||
|
subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={subtitleStream['tags']['language']}"]
|
||||||
|
if 'title' in subtitleStream['tags'].keys():
|
||||||
|
subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={subtitleStream['tags']['title']}"]
|
||||||
|
|
||||||
|
|
||||||
# Job specific tokens
|
# Job specific tokens
|
||||||
targetFilenameJobTokens = targetFilenameTokens.copy()
|
targetFilenameJobTokens = targetFilenameTokens.copy()
|
||||||
@@ -619,8 +767,11 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|||||||
if video_encoder == 'av1':
|
if video_encoder == 'av1':
|
||||||
|
|
||||||
commandSequence = (commandTokens
|
commandSequence = (commandTokens
|
||||||
|
+ subtitleFileTokens
|
||||||
+ mappingTokens
|
+ mappingTokens
|
||||||
+ dispositionTokens
|
+ dispositionTokens
|
||||||
|
+ audioMetadataTokens
|
||||||
|
+ subtitleMetadataTokens
|
||||||
+ audioTokens
|
+ audioTokens
|
||||||
+ generateAV1Tokens(q, preset) + audioTokens)
|
+ generateAV1Tokens(q, preset) + audioTokens)
|
||||||
|
|
||||||
@@ -657,7 +808,10 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
|
|||||||
|
|
||||||
|
|
||||||
commandSequence2 = (commandTokens
|
commandSequence2 = (commandTokens
|
||||||
|
+ subtitleFileTokens
|
||||||
+ mappingTokens
|
+ mappingTokens
|
||||||
|
+ audioMetadataTokens
|
||||||
|
+ subtitleMetadataTokens
|
||||||
+ dispositionTokens)
|
+ dispositionTokens)
|
||||||
|
|
||||||
if denoise:
|
if denoise:
|
||||||
|
|||||||
Reference in New Issue
Block a user