@ -12,7 +12,6 @@ VERSION='0.1.0'
DEFAULT_VIDEO_ENCODER = ' vp9 '
DEFAULT_QUALITY = 23
DEFAULT_AV1_PRESET = 5
DEFAULT_FILE_FORMAT = ' webm '
@ -230,10 +229,30 @@ def getStreamDescriptor(filename):
s [ ' layout ' ] = ' undefined '
descriptor [ s [ ' codec_type ' ] ] . append ( s )
descriptor [ s [ ' codec_type ' ] ] [ - 1 ] [ ' src_sub_index ' ] = len ( descriptor [ s [ ' codec_type ' ] ] ) - 1
return descriptor
def getModifiedStreamOrder ( length , last ) :
""" This is jellyfin specific as the last stream in the order is set as default """
seq = list ( range ( length ) )
if last < 0 or last > length - 1 :
return seq
seq . pop ( last )
seq . append ( last )
return seq
def getReorderedSubstreams ( subDescriptor , last ) :
numSubStreams = len ( subDescriptor )
modifiedOrder = getModifiedStreamOrder ( numSubStreams , last )
reorderedDescriptor = [ ]
for streamIndex in range ( numSubStreams ) :
reorderedDescriptor . append ( subDescriptor [ modifiedOrder [ streamIndex ] ] )
return reorderedDescriptor
def generateAV1Tokens ( q , p ) :
return [ ' -c:v:0 ' , ' libsvtav1 ' ,
@ -321,6 +340,25 @@ def generateClearTokens(streams):
return clearTokens
def generateDispositionTokens ( subDescriptor ) :
""" -disposition:s:X default+forced """
dispositionTokens = [ ]
for subStreamIndex in range ( len ( subDescriptor ) ) :
subStream = subDescriptor [ subStreamIndex ]
streamType = subStream [ ' codec_type ' ] [ 0 ] # v|a|s
dispositionFlags = { k for ( k , v ) in subStream [ ' disposition ' ] . items ( ) if v == 1 } if ' disposition ' in subStream . keys ( ) else set ( )
if dispositionFlags :
dispositionTokens + = [ f " -disposition: { streamType } : { subStreamIndex } " , ' + ' . join ( dispositionFlags ) ]
else :
dispositionTokens + = [ f " -disposition: { streamType } : { subStreamIndex } " , ' 0 ' ]
return dispositionTokens
@click.group ( )
@click.pass_context
def ffx ( ctx ) :
@ -370,10 +408,10 @@ 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 ( ' -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 ( ' -ds ' , ' --default-subtitle ' , type = int , 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-audio ' , type = int , help= ' Index of forced audio stream (including default audio stream tag) ' )
@click.option ( ' -da ' , ' --default-audio ' , type = int , help= ' Index of default audio stream ' )
@click.option ( ' -fa ' , ' --forced-audio ' , type = int , default= - 1 , help= ' Index of forced audio stream (including default audio stream tag) ' )
@click.option ( ' -da ' , ' --default-audio ' , type = int , default= - 1 , help= ' Index of default audio stream ' )
@click.option ( " --crop " , is_flag = False , flag_value = " default " , default = " none " )
@ -384,10 +422,12 @@ 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 ( " --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_audio , default_audio , crop , output_directory , clear_metadata , denoise , dry_run) :
def convert ( ctx , paths , label , video_encoder , quality , preset , stereo_bitrate , ac3_bitrate , dts_bitrate , default_subtitle , forced_audio , 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
Files found under PATHS will be converted according to parameters .
@ -483,6 +523,7 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
print ( f " season= { season } episode= { episode } file= { file_index } " )
# File specific tokens
targetFilenameTokens = [ ]
targetFilenameExtension = DEFAULT_FILE_EXTENSION
@ -522,22 +563,34 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
job_index + = 1
mappingVideoTokens = [ ' -map ' , ' v:0 ' ]
mappingTokens = mappingVideoTokens . copy ( )
dispositionTokens = [ ]
audioTokens = [ ]
audioIndex = 0
for audioStreamDescriptor in streamDescriptor [ STREAM_TYPE_AUDIO ] :
mappingTokens + = [ ' -map ' , f " a: { audioIndex } " ]
audioTokens + = generateAudioTokens ( context , audioIndex , audioStreamDescriptor [ ' layout ' ] )
audioIndex + = 1
if default_audio == - 1 :
sourceAudioStreams = streamDescriptor [ STREAM_TYPE_AUDIO ]
else :
sourceAudioStreams = getReorderedSubstreams ( streamDescriptor [ STREAM_TYPE_AUDIO ] , default_audio )
dispositionTokens + = generateDispositionTokens ( sourceAudioStreams )
if default_subtitle == - 1 :
sourceSubtitleStreams = streamDescriptor [ STREAM_TYPE_SUBTITLE ]
else :
sourceSubtitleStreams = getReorderedSubstreams ( streamDescriptor [ STREAM_TYPE_SUBTITLE ] , default_subtitle )
dispositionTokens + = generateDispositionTokens ( sourceSubtitleStreams )
subtitleIndex = 0
for subtitleStreamDescriptor in streamDescriptor [ STREAM_TYPE_SUBTITLE ] :
mappingTokens + = [ ' -map ' , f " s: { subtitleIndex } " ]
subtitleIndex + = 1
for audioStream in sourceAudioStreams :
mappingTokens + = [ ' -map ' , f " a: { audioStream [ ' src_sub_index ' ] } " ]
audioTokens + = generateAudioTokens ( context , audioStream [ ' src_sub_index ' ] , audioStream [ ' layout ' ] )
for subtitleStream in sourceSubtitleStreams :
mappingTokens + = [ ' -map ' , f " s: { subtitleStream [ ' src_sub_index ' ] } " ]
# Job specific tokens
targetFilenameJobTokens = targetFilenameTokens . copy ( )
if len ( q_list ) > 1 :
@ -552,10 +605,13 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
click . echo ( f " target filename: { targetFilename } " )
if video_encoder == ' av1 ' :
commandSequence = commandTokens + mappingTokens + audioTokens + generateAV1Tokens ( q , preset ) + audioTokens
commandSequence = ( commandTokens
+ mappingTokens
+ dispositionTokens
+ audioTokens
+ generateAV1Tokens ( q , preset ) + audioTokens )
if clear_metadata :
commandSequence + = generateClearTokens ( streamDescriptor )
@ -589,7 +645,9 @@ def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, a
executeProcess ( commandSequence1 )
commandSequence2 = commandTokens + mappingTokens
commandSequence2 = ( commandTokens
+ mappingTokens
+ dispositionTokens )
if denoise :
commandSequence2 + = generateDenoiseTokens ( )