You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
764 lines
33 KiB
Python
764 lines
33 KiB
Python
#! /usr/bin/python3
|
|
|
|
import os, sys, subprocess, json, click, time, re
|
|
|
|
from ffx.file_properties import FileProperties
|
|
|
|
from ffx.ffx_app import FfxApp
|
|
from ffx.ffx_controller import FfxController
|
|
from ffx.database import databaseContext
|
|
|
|
|
|
VERSION='0.1.0'
|
|
|
|
|
|
@click.group()
|
|
@click.pass_context
|
|
def ffx(ctx):
|
|
"""FFX"""
|
|
|
|
ctx.obj = {}
|
|
ctx.obj['database'] = databaseContext()
|
|
|
|
|
|
# Define a subcommand
|
|
@ffx.command()
|
|
def version():
|
|
click.echo(VERSION)
|
|
|
|
|
|
# Another subcommand
|
|
@ffx.command()
|
|
def help():
|
|
click.echo(f"ffx {VERSION}\n")
|
|
click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]")
|
|
|
|
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
@click.argument('filename', nargs=1)
|
|
def inspect(ctx, filename):
|
|
|
|
ctx.obj['command'] = 'inspect'
|
|
ctx.obj['arguments'] = {}
|
|
ctx.obj['arguments']['filename'] = filename
|
|
|
|
app = FfxApp(ctx.obj)
|
|
app.run()
|
|
|
|
|
|
|
|
# @ffx.command()
|
|
# @click.pass_context
|
|
#
|
|
# @click.argument('paths', nargs=-1)
|
|
# @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
|
#
|
|
# @click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
|
|
# @click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
|
|
#
|
|
# @click.option("-o", "--output-directory", type=str, default='')
|
|
#
|
|
# @click.option("--dry-run", is_flag=True, default=False)
|
|
#
|
|
#
|
|
# def unmux(ctx,
|
|
# label,
|
|
# paths,
|
|
# subtitle_directory,
|
|
# subtitle_prefix,
|
|
# output_directory,
|
|
# dry_run):
|
|
#
|
|
# existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
|
# click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
|
|
#
|
|
# for sourcePath in existingSourcePaths:
|
|
#
|
|
# sd = getStreamDescriptor(sourcePath)
|
|
#
|
|
# print(f"\nFile {sourcePath}\n")
|
|
#
|
|
# for v in sd['video']:
|
|
#
|
|
# if v['codec_name'] == 'h264':
|
|
#
|
|
# commandSequence = ['ffmpeg', '-i', sourcePath, '-map', '0:v:0', '-c', 'copy', '-f', 'h264']
|
|
# executeProcess()
|
|
#
|
|
# for a in sd['audio']:
|
|
# print(f"A: {a}\n")
|
|
# for s in sd['subtitle']:
|
|
# print(f"S: {s}\n")
|
|
|
|
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
|
|
def shows(ctx):
|
|
|
|
ctx.obj['command'] = 'shows'
|
|
|
|
app = FfxApp(ctx.obj)
|
|
app.run()
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
|
|
@click.argument('paths', nargs=-1)
|
|
|
|
@click.option("-t", "--tmdb", is_flag=True, default=False)
|
|
@click.option("-j", "--jellyfin", is_flag=True, default=False)
|
|
|
|
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
|
|
|
@click.option('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1) default: {FfxController.DEFAULT_VIDEO_ENCODER}")
|
|
|
|
@click.option('-q', '--quality', type=str, default=FfxController.DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {FfxController.DEFAULT_QUALITY})")
|
|
@click.option('-p', '--preset', type=str, default=FfxController.DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {FfxController.DEFAULT_AV1_PRESET})")
|
|
|
|
@click.option('-a', '--stereo-bitrate', type=int, default=FfxController.DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {FfxController.DEFAULT_STEREO_BANDWIDTH})")
|
|
@click.option('-ac3', '--ac3-bitrate', type=int, default=FfxController.DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {FfxController.DEFAULT_AC3_BANDWIDTH})")
|
|
@click.option('-dts', '--dts-bitrate', type=int, default=FfxController.DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {FfxController.DEFAULT_DTS_BANDWIDTH})")
|
|
|
|
@click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here')
|
|
@click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
|
|
|
|
@click.option('-ss', '--subtitle-language', type=str, multiple=True, help='Subtitle stream language(s)')
|
|
@click.option('-st', '--subtitle-title', type=str, multiple=True, help='Subtitle stream title(s)')
|
|
|
|
@click.option('-ds', '--default-subtitle', type=int, default=-1, help='Index of default subtitle stream')
|
|
@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, multiple=True, help='Audio stream language(s)')
|
|
@click.option('-at', '--audio-title', type=str, multiple=True, help='Audio stream title(s)')
|
|
|
|
@click.option('-da', '--default-audio', type=int, default=-1, help='Index of default audio stream')
|
|
@click.option('-da', '--forced-audio', type=int, default=-1, help='Index of forced audio stream')
|
|
|
|
|
|
@click.option("--crop", is_flag=False, flag_value="default", default="none")
|
|
|
|
@click.option("-o", "--output-directory", type=str, default='')
|
|
|
|
|
|
@click.option("-c", "--clear-metadata", is_flag=True, default=False)
|
|
@click.option("-d", "--denoise", is_flag=True, default=False)
|
|
|
|
|
|
|
|
@click.option("--dry-run", is_flag=True, default=False)
|
|
|
|
|
|
def convert(ctx,
|
|
paths,
|
|
tmdb,
|
|
jellyfin,
|
|
label,
|
|
video_encoder,
|
|
quality,
|
|
preset,
|
|
stereo_bitrate,
|
|
ac3_bitrate,
|
|
dts_bitrate,
|
|
subtitle_directory,
|
|
subtitle_prefix,
|
|
subtitle_language,
|
|
subtitle_title,
|
|
default_subtitle,
|
|
forced_subtitle,
|
|
audio_language,
|
|
audio_title,
|
|
default_audio,
|
|
forced_audio,
|
|
crop,
|
|
output_directory,
|
|
clear_metadata,
|
|
denoise,
|
|
dry_run):
|
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
|
|
|
Files found under PATHS will be converted according to parameters.
|
|
Filename extensions will be changed appropriately.
|
|
Suffices will we appended to filename in case of multiple created files
|
|
or if the filename has not changed."""
|
|
|
|
startTime = time.perf_counter()
|
|
|
|
context = ctx.obj
|
|
|
|
context['jellyfin'] = jellyfin
|
|
context['tmdb'] = tmdb
|
|
|
|
# click.echo(f"\nVideo encoder: {video_encoder}")
|
|
|
|
|
|
qualityTokens = quality.split(',')
|
|
q_list = [q for q in qualityTokens if q.isnumeric()]
|
|
|
|
# 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"
|
|
context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k"
|
|
#
|
|
# click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}")
|
|
# click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}")
|
|
# click.echo(f"DTS bitrate: {context['bitrates']['dts']}")
|
|
#
|
|
#
|
|
#
|
|
# ## Conversion parameters
|
|
#
|
|
# # Parse subtitle files
|
|
# context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
|
|
# availableFileSubtitleDescriptors = searchSubtitleFiles(subtitle_directory, subtitle_prefix) if context['import_subtitles'] else []
|
|
#
|
|
#
|
|
# # Overwrite audio tags if set
|
|
# audioLanguages = audio_language
|
|
# audioTitles = audio_title
|
|
#
|
|
# # Overwrite subtitle tags if set
|
|
# subtitleLanguages = subtitle_language
|
|
# subtitleTitles = subtitle_title
|
|
#
|
|
# defaultAudio = default_audio
|
|
# defaultSubtitle = default_subtitle
|
|
# forcedAudio = forced_audio
|
|
# forcedSubtitle = forced_subtitle
|
|
#
|
|
#
|
|
# # Process crop parameters
|
|
# context['perform_crop'] = (crop != 'none')
|
|
# if context['perform_crop']:
|
|
# cTokens = crop.split(',')
|
|
# if cTokens and len(cTokens) == 2:
|
|
# cropStart, cropLength = crop.split(',')
|
|
# else:
|
|
# cropStart = FfxController.DEFAULT_CROP_START
|
|
# cropLength = FfxController.DEFAULT_CROP_LENGTH
|
|
#
|
|
# click.echo(f"crop start={cropStart} length={cropLength}")
|
|
#
|
|
# cropTokens = generateCropTokens(int(cropStart), int(cropLength))
|
|
# else:
|
|
# cropTokens = []
|
|
#
|
|
#
|
|
# job_index = 0
|
|
#
|
|
existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS]
|
|
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('.')
|
|
|
|
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
sourceFilenameExtension = sourcePathTokens[-1]
|
|
|
|
|
|
click.echo(f"\nProcessing file {sourcePath}")
|
|
|
|
|
|
mediaFileProperties = FileProperties(context, sourceFilename)
|
|
currentMediaDescriptor = mediaFileProperties.getMediaDescriptor()
|
|
|
|
#HINT: This is None if the filename did not match anything in database
|
|
currentPattern = mediaFileProperties.getPattern()
|
|
|
|
targetMediaDescriptor = currentPattern.getMediaDescriptor() if currentPattern is not None else None
|
|
|
|
click.echo(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}")
|
|
|
|
if not currentPattern is None:
|
|
|
|
targetMediaDescriptor.setJellyfinOrder(context['jellyfin'])
|
|
|
|
click.echo(f"Input mapping tokens: {targetMediaDescriptor.getInputMappingTokens()}")
|
|
|
|
fc = FfxController(context, currentMediaDescriptor, targetMediaDescriptor)
|
|
|
|
mappingTokens = fc.generateMetadataTokens()
|
|
click.echo(f"Metadata Tokens: {mappingTokens}")
|
|
|
|
dispositionTokens = fc.generateDispositionTokens()
|
|
click.echo(f"Disposition Tokens: {dispositionTokens}")
|
|
|
|
audioTokens = fc.generateAudioEncodingTokens()
|
|
click.echo(f"Audio Tokens: {audioTokens}")
|
|
|
|
click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}")
|
|
|
|
# # Determine season and episode if present in current filename
|
|
# season_digits = 2
|
|
# episode_digits = 2
|
|
# index_digits = 3
|
|
#
|
|
# se_result = se_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
|
|
#
|
|
# matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else []
|
|
#
|
|
# print(f"season={season} episode={episode} file={file_index}")
|
|
#
|
|
#
|
|
# # Assemble target filename tokens
|
|
# targetFilenameTokens = []
|
|
# targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION
|
|
#
|
|
# if label:
|
|
# targetFilenameTokens = [label]
|
|
#
|
|
# 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]
|
|
#
|
|
# ###
|
|
# ###
|
|
#
|
|
# # Load source stream descriptor
|
|
# try:
|
|
# ###
|
|
# sourceStreamDescriptor = getStreamDescriptor(sourcePath)
|
|
# ###
|
|
#
|
|
# except Exception:
|
|
# click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...")
|
|
# continue
|
|
#
|
|
#
|
|
# ## ## ##
|
|
# targetStreamDescriptor = sourceStreamDescriptor.copy()
|
|
# ## ## ##
|
|
#
|
|
#
|
|
# click.echo('\nSource streams:')
|
|
# for aStream in sourceStreamDescriptor[STREAM_TYPE_AUDIO]:
|
|
# click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}")
|
|
# for sStream in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]:
|
|
# click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}")
|
|
#
|
|
#
|
|
# # Check for multiple default or forced dispositions if not set by user input or database requirements
|
|
# #NOTE: It is currently expected that all source file have the same substream pattern, e.g. coming from the same encoder
|
|
# numDefaultAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1])
|
|
# if defaultAudio == -1 and numDefaultAudioStreams > 1:
|
|
# defaultAudio = click.prompt("More than one default audio stream detected! Please select stream", type=int)
|
|
#
|
|
# numForcedAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['forced'] == 1])
|
|
# if forcedAudio == -1 and numForcedAudioStreams > 1:
|
|
# forcedAudio = click.prompt("More than one forced audio stream detected! Please select stream", type=int)
|
|
#
|
|
# numDefaultSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['default'] == 1])
|
|
# if defaultSubtitle == -1 and numDefaultSubtitleStreams > 1:
|
|
# defaultSubtitle = click.prompt("More than one default subtitle stream detected! Please select stream", type=int)
|
|
#
|
|
# numForcedSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['forced'] == 1])
|
|
# if forcedSubtitle == -1 and numForcedSubtitleStreams > 1:
|
|
# forcedSubtitle = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int)
|
|
#
|
|
# #Define default/forced tags
|
|
# if defaultAudio != -1:
|
|
# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])):
|
|
# targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultAudio else 0
|
|
# if forcedAudio != -1:
|
|
# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])):
|
|
# targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedAudio else 0
|
|
# if defaultSubtitle != -1:
|
|
# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])):
|
|
# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultSubtitle else 0
|
|
# if forcedSubtitle != -1:
|
|
# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])):
|
|
# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedSubtitle else 0
|
|
#
|
|
#
|
|
# # Set language and title in source stream descriptors if given per command line option
|
|
# for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])):
|
|
# if streamIndex <= len(audioLanguages) - 1:
|
|
# targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['language'] = audioLanguages[streamIndex]
|
|
# if streamIndex <= len(audioTitles) - 1:
|
|
# targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['title'] = audioTitles[streamIndex]
|
|
#
|
|
# for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])):
|
|
# if streamIndex <= len(subtitleLanguages) - 1:
|
|
# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['language'] = subtitleLanguages[streamIndex]
|
|
# if streamIndex <= len(subtitleTitles) - 1:
|
|
# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['title'] = subtitleTitles[streamIndex]
|
|
#
|
|
#
|
|
# click.echo('\nTarget streams:')
|
|
# for aStream in targetStreamDescriptor[STREAM_TYPE_AUDIO]:
|
|
# click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}")
|
|
# for sStream in targetStreamDescriptor[STREAM_TYPE_SUBTITLE]:
|
|
# click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}")
|
|
#
|
|
#
|
|
# numSourceAudioSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_AUDIO])
|
|
# numSourceSubtitleSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE])
|
|
#
|
|
# # Stream order is just a list of integer
|
|
# audioStreamSourceOrder = list(range(numSourceAudioSubStreams))
|
|
# subtitleStreamSourceOrder = list(range(numSourceSubtitleSubStreams))
|
|
#
|
|
#
|
|
# # In order for the jellyfin media web UI to work properly the default/forced stream has to be the last in the sequence
|
|
# if jellyfin:
|
|
#
|
|
# defaultTargetAudioStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1]
|
|
# if defaultTargetAudioStreams:
|
|
# audioStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_AUDIO]), defaultTargetAudioStreams[0]['sub_index'])
|
|
#
|
|
# defaultTargetSubtitleStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_SUBTITLE] if a['disposition']['default'] == 1]
|
|
# if defaultTargetSubtitleStreams:
|
|
# subtitleStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]), defaultTargetSubtitleStreams[0]['sub_index'])
|
|
#
|
|
#
|
|
# # audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO])
|
|
# # subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])
|
|
#
|
|
# audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO], modifyOrder = audioStreamSourceOrder)
|
|
# subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE], modifyOrder = subtitleStreamSourceOrder)
|
|
#
|
|
#
|
|
#
|
|
# mappingVideoTokens = ['-map', '0:v:0']
|
|
# mappingTokens = mappingVideoTokens.copy()
|
|
#
|
|
# dispositionTokens = []
|
|
#
|
|
# audioEncodingTokens = []
|
|
#
|
|
#
|
|
# audioMetadataTokens = []
|
|
# for audioStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])):
|
|
#
|
|
# # Modify selected source audio stream for jellyfin if required
|
|
# sourceAudioStreamIndex = audioStreamSourceOrder[audioStreamIndex]
|
|
#
|
|
# # Add audio mapping tokens to list of general mapping tokens
|
|
# mappingTokens += ['-map', f"0:a:{sourceAudioStreamIndex}"]
|
|
#
|
|
#
|
|
# targetAudioStream = targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]
|
|
#
|
|
# # audioEncodingTokens += generateAudioEncodingTokens(context, sourceAudioStream['src_sub_index'], sourceAudioStream['layout'])
|
|
# audioEncodingTokens += generateAudioEncodingTokens(context, audioStreamIndex, targetAudioStream['audio_layout'])
|
|
#
|
|
# if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['language']:
|
|
# audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language']}"]
|
|
#
|
|
# if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['title']:
|
|
# audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title']}"]
|
|
#
|
|
# # targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0
|
|
#
|
|
#
|
|
# subtitleImportFileTokens = []
|
|
# subtitleMetadataTokens = []
|
|
#
|
|
# if context['import_subtitles'] and numSourceSubtitleSubStreams != len(matchingFileSubtitleDescriptors):
|
|
# click.echo(f"The number of subtitle streams found in file with path {sourcePath} is different from the number of subtitle streams provided by matching imported files, skipping ...")
|
|
# continue
|
|
#
|
|
# # 0: Quelle f1 = forced
|
|
# # 1: QUelle f2 = full
|
|
#
|
|
# for subtitleStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])):
|
|
#
|
|
# # Modify selected source subtitle stream for jellyfin if required
|
|
# sourceSubtitleStreamIndex = subtitleStreamSourceOrder[subtitleStreamIndex]
|
|
#
|
|
#
|
|
# if context['import_subtitles']:
|
|
#
|
|
# fileSubtitleDescriptor = matchingFileSubtitleDescriptors[subtitleStreamIndex] # original order
|
|
#
|
|
# subtitleImportFileTokens += ['-i', fileSubtitleDescriptor['path']] # original order
|
|
#
|
|
# # Create mapping for subtitle streams when imported from files
|
|
# mappingTokens += ['-map', f"{sourceSubtitleStreamIndex+1}:s:0"] # modified order
|
|
#
|
|
#
|
|
# if fileSubtitleDescriptor['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']:
|
|
# subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"]
|
|
#
|
|
# subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"]
|
|
#
|
|
# else:
|
|
#
|
|
# # Add subtitle mapping tokens to list of general mapping tokens
|
|
# mappingTokens += ['-map', f"0:s:{sourceSubtitleStreamIndex}"]
|
|
#
|
|
# if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']:
|
|
# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"]
|
|
#
|
|
# if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']:
|
|
# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"]
|
|
#
|
|
#
|
|
#
|
|
#
|
|
#
|
|
#
|
|
# # # Reorder audio stream descriptors and create disposition options if default is given per command line option
|
|
# # if defaultAudio == -1:
|
|
# # sourceAudioStreams = audioStreams
|
|
# # else:
|
|
# # for streamIndex in range(len(audioStreams)):
|
|
# # audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0
|
|
# #
|
|
# # sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams
|
|
# #
|
|
# # dispositionTokens += generateDispositionTokens(sourceAudioStreams)
|
|
# #
|
|
# # # Set forced tag in subtitle descriptor if given per command line option
|
|
# # if forcedSubtitle != -1:
|
|
# # for streamIndex in range(len(subtitleStreams)):
|
|
# # subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0
|
|
# #
|
|
# # # Reorder subtitle stream descriptors and create disposition options if default is given per command line option
|
|
# # if defaultSubtitle == -1:
|
|
# # sourceSubtitleStreams = subtitleStreams
|
|
# # else:
|
|
# # for streamIndex in range(len(subtitleStreams)):
|
|
# # subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0
|
|
# #
|
|
# # sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams
|
|
# #
|
|
# # dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
|
# #
|
|
#
|
|
#
|
|
#
|
|
#
|
|
#
|
|
# click.echo(f"Audio stream source order {audioStreamSourceOrder}")
|
|
# click.echo(f"Subtitle stream source order {subtitleStreamSourceOrder}")
|
|
#
|
|
#
|
|
# commandTokens = COMMAND_TOKENS + ['-i', sourcePath]
|
|
#
|
|
#
|
|
# # matchingSubtitles = []
|
|
# # if context['import_subtitles']:
|
|
# #
|
|
#
|
|
#
|
|
# #
|
|
# # for streamIndex in range(len(mSubtitles)):
|
|
# # mSubtitles[streamIndex]['forced'] = 1 if forcedSubtitle != -1 and streamIndex == forcedSubtitle else 0
|
|
# # mSubtitles[streamIndex]['default'] = 1 if defaultSubtitle != -1 and streamIndex == defaultSubtitle else 0
|
|
# #
|
|
# # if streamIndex <= len(subtitleTitles) -1:
|
|
# # mSubtitles[streamIndex]['title'] = subtitleTitles[streamIndex]
|
|
# #
|
|
# # if defaultSubtitle != -1 and jellyfin:
|
|
# # matchingSubtitles = getReorderedSubstreams(mSubtitles, defaultSubtitle)
|
|
# # else:
|
|
# # matchingSubtitles = mSubtitles
|
|
#
|
|
#
|
|
#
|
|
# for q in q_list:
|
|
#
|
|
# click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}")
|
|
# job_index += 1
|
|
#
|
|
#
|
|
# # # Reorder audio stream descriptors and create disposition options if default is given per command line option
|
|
# # if defaultAudio == -1:
|
|
# # sourceAudioStreams = audioStreams
|
|
# # else:
|
|
# # for streamIndex in range(len(audioStreams)):
|
|
# # audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0
|
|
# #
|
|
# # sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams
|
|
# #
|
|
# # dispositionTokens += generateDispositionTokens(sourceAudioStreams)
|
|
# #
|
|
# # # Set forced tag in subtitle descriptor if given per command line option
|
|
# # if forcedSubtitle != -1:
|
|
# # for streamIndex in range(len(subtitleStreams)):
|
|
# # subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0
|
|
# #
|
|
# # # Reorder subtitle stream descriptors and create disposition options if default is given per command line option
|
|
# # if defaultSubtitle == -1:
|
|
# # sourceSubtitleStreams = subtitleStreams
|
|
# # else:
|
|
# # for streamIndex in range(len(subtitleStreams)):
|
|
# # subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0
|
|
# #
|
|
# # sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams
|
|
# #
|
|
# # dispositionTokens += generateDispositionTokens(sourceSubtitleStreams)
|
|
# #
|
|
#
|
|
#
|
|
# # # Create mapping and ffmpeg options for subtitle streams
|
|
#
|
|
# # if context['import_subtitles']:
|
|
# #
|
|
# # numMatchingSubtitles = len(matchingSubtitles)
|
|
# #
|
|
# # if jellyfin and defaultSubtitle != -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"{subtitleSequence[fileIndex]+1}:s:0"]
|
|
# #
|
|
# # msg = matchingSubtitles[fileIndex]
|
|
# # subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"language={msg['language']}"]
|
|
# # if 'title' in matchingSubtitles[fileIndex].keys():
|
|
# # subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"title={matchingSubtitles[fileIndex]['title']}"]
|
|
# #
|
|
# # else:
|
|
# #
|
|
# # for subtitleStreamIndex in range(len(sourceSubtitleStreams)):
|
|
# #
|
|
# # subtitleStream = sourceSubtitleStreams[subtitleStreamIndex]
|
|
# #
|
|
# # # Create mapping for subtitle streams
|
|
# # 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
|
|
# targetFilenameJobTokens = targetFilenameTokens.copy()
|
|
#
|
|
# if len(q_list) > 1:
|
|
# targetFilenameJobTokens += [f"q{q}"]
|
|
#
|
|
# # In case source and target filenames are the same add an extension to distinct output from input
|
|
# if not label and sourceFilenameExtension == targetFilenameExtension:
|
|
# targetFilenameJobTokens += ['ffx']
|
|
#
|
|
# targetFilename = '_'.join(targetFilenameJobTokens) # + '.' + targetFilenameExtension
|
|
#
|
|
# click.echo(f"target filename: {targetFilename}")
|
|
#
|
|
#
|
|
# if video_encoder == 'av1':
|
|
#
|
|
# commandSequence = (commandTokens
|
|
# + subtitleImportFileTokens
|
|
# + mappingTokens
|
|
# + audioMetadataTokens
|
|
# + subtitleMetadataTokens
|
|
# + audioDispositionTokens
|
|
# + subtitleDispositionTokens
|
|
# + audioEncodingTokens
|
|
# + generateAV1Tokens(q, preset) + audioEncodingTokens)
|
|
#
|
|
# if clear_metadata:
|
|
# commandSequence += generateClearTokens(sourceStreamDescriptor)
|
|
#
|
|
# commandSequence += cropTokens
|
|
#
|
|
# commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION)
|
|
#
|
|
# click.echo(f"Command: {' '.join(commandSequence)}")
|
|
#
|
|
# if not dry_run:
|
|
# executeProcess(commandSequence)
|
|
#
|
|
#
|
|
# if video_encoder == 'vp9':
|
|
#
|
|
# commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q)
|
|
#
|
|
# commandSequence1 += cropTokens
|
|
#
|
|
# commandSequence1 += NULL_TOKENS
|
|
#
|
|
# click.echo(f"Command 1: {' '.join(commandSequence1)}")
|
|
#
|
|
# if os.path.exists(TEMP_FILE_NAME):
|
|
# os.remove(TEMP_FILE_NAME)
|
|
#
|
|
# if not dry_run:
|
|
# executeProcess(commandSequence1)
|
|
#
|
|
#
|
|
# commandSequence2 = (commandTokens
|
|
# + subtitleImportFileTokens
|
|
# + mappingTokens
|
|
# + audioMetadataTokens
|
|
# + subtitleMetadataTokens
|
|
# + audioDispositionTokens
|
|
# + subtitleDispositionTokens
|
|
# + dispositionTokens)
|
|
#
|
|
# if denoise:
|
|
# commandSequence2 += generateDenoiseTokens()
|
|
#
|
|
# commandSequence2 += generateVP9Pass2Tokens(q) + audioEncodingTokens
|
|
#
|
|
# if clear_metadata:
|
|
# commandSequence2 += generateClearTokens(sourceStreamDescriptor)
|
|
#
|
|
# commandSequence2 += cropTokens
|
|
#
|
|
# commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION)
|
|
#
|
|
# click.echo(f"Command 2: {' '.join(commandSequence2)}")
|
|
#
|
|
# if not dry_run:
|
|
# executeProcess(commandSequence2)
|
|
#
|
|
#
|
|
# #app = ModesApp(context)
|
|
# #app.run()
|
|
#
|
|
# #click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)
|
|
#
|
|
# click.echo('\nDONE\n')
|
|
|
|
endTime = time.perf_counter()
|
|
click.echo(f"Time elapsed {endTime - startTime}")
|
|
|
|
|
|
# click.echo(f"app result: {app.getContext()}")
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ffx()
|