@ -19,6 +19,7 @@ from ffx.video_encoder import VideoEncoder
from ffx . track_disposition import TrackDisposition
from ffx . process import executeProcess
from ffx . helper import filterFilename
VERSION = ' 0.2.0 '
@ -150,8 +151,7 @@ def unmux(ctx,
subtitles_only ) :
existingSourcePaths = [ p for p in paths if os . path . isfile ( p ) ]
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " \n Unmuxing { len ( existingSourcePaths ) } files " )
ctx . obj [ ' logger ' ] . debug ( f " \n Unmuxing { len ( existingSourcePaths ) } files " )
for sourcePath in existingSourcePaths :
@ -169,12 +169,10 @@ def unmux(ctx,
targetIndicator = f " _S { season } E { episode } " if label and season != - 1 and episode != - 1 else ' '
if label and not targetIndicator :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Skipping file { fp . getFilename ( ) } : Label set but no indicator recognized " )
ctx . obj [ ' logger ' ] . warning ( f " Skipping file { fp . getFilename ( ) } : Label set but no indicator recognized " )
continue
else :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " \n Unmuxing file { fp . getFilename ( ) } \n " )
ctx . obj [ ' logger ' ] . debug ( f " \n Unmuxing file { fp . getFilename ( ) } \n " )
for trackDescriptor in sourceMediaDescriptor . getAllTrackDescriptors ( ) :
@ -187,18 +185,14 @@ def unmux(ctx,
if unmuxSequence :
if not ctx . obj [ ' dry_run ' ] :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Executing unmuxing sequence: { ' ' . join ( unmuxSequence ) } " )
ctx . obj [ ' logger ' ] . debug ( f " Executing unmuxing sequence: { ' ' . join ( unmuxSequence ) } " )
out , err , rc = executeProcess ( unmuxSequence )
if rc :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Unmuxing of stream { trackDescriptor . getIndex ( ) } failed with error ( { rc } ) { err } " )
ctx . obj [ ' logger ' ] . error ( f " Unmuxing of stream { trackDescriptor . getIndex ( ) } failed with error ( { rc } ) { err } " )
else :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Skipping stream with unknown codec { trackDescriptor . getCodec ( ) } " )
ctx . obj [ ' logger ' ] . warning ( f " Skipping stream with unknown codec { trackDescriptor . getCodec ( ) } " )
except Exception as ex :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Skipping File { sourcePath } ( { ex } ) " )
ctx . obj [ ' logger ' ] . warning ( f " Skipping File { sourcePath } ( { ex } ) " )
@ffx.command ( )
@ -267,7 +261,7 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
@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 ( ' - s ' , ' --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 ( ' - 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 ' , 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 ' , 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 } ) " )
@ -298,8 +292,11 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
@click.option ( " --no-tmdb " , is_flag = True , default = False )
@click.option ( " --no-jellyfin " , is_flag = True , default = False )
@click.option ( " --no-pattern " , is_flag = True , default = False )
@click.option ( " --dont-pass-dispositions " , is_flag = True , default = False )
@click.option ( " --no-prompt " , is_flag = True , default = False )
@click.option ( " --no-signature " , is_flag = True , default = False )
def convert ( ctx ,
paths ,
@ -331,7 +328,8 @@ def convert(ctx,
no_jellyfin ,
no_pattern ,
dont_pass_dispositions ,
no_prompt ) :
no_prompt ,
no_signature ) :
""" Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
Files found under PATHS will be converted according to parameters .
@ -349,29 +347,28 @@ def convert(ctx,
context [ ' use_tmdb ' ] = not no_tmdb
context [ ' use_pattern ' ] = not no_pattern
context [ ' no_prompt ' ] = no_prompt
context [ ' no_signature ' ] = no_signature
context [ ' import_subtitles ' ] = ( subtitle_directory and subtitle_prefix )
if context [ ' import_subtitles ' ] :
context [ ' subtitle_directory ' ] = subtitle_directory
context [ ' subtitle_prefix ' ] = subtitle_prefix
# click.echo(f"\nVideo encoder: {video_encoder}" )
ctx . obj [ ' logger ' ] . debug ( f " \n Video encoder: { video_encoder } " )
qualityTokens = quality . split ( ' , ' )
q_list = [ q for q in qualityTokens if q . isnumeric ( ) ]
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Qualities: { q_list } " )
ctx . obj [ ' logger ' ] . debug ( 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 ) if str ( ac3 ) . endswith ( ' k ' ) else f " { ac3 } k "
context [ ' bitrates ' ] [ ' dts ' ] = str ( dts ) if str ( dts ) . endswith ( ' k ' ) else f " { dts } k "
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Stereo bitrate: { context [ ' bitrates ' ] [ ' stereo ' ] } " )
click . echo ( f " AC3 bitrate: { context [ ' bitrates ' ] [ ' ac3 ' ] } " )
click . echo ( f " DTS bitrate: { context [ ' bitrates ' ] [ ' dts ' ] } " )
ctx . obj [ ' logger ' ] . debug ( f " Stereo bitrate: { context [ ' bitrates ' ] [ ' stereo ' ] } " )
ctx . obj [ ' logger ' ] . debug ( f " AC3 bitrate: { context [ ' bitrates ' ] [ ' ac3 ' ] } " )
ctx . obj [ ' logger ' ] . debug ( f " DTS bitrate: { context [ ' bitrates ' ] [ ' dts ' ] } " )
# Process crop parameters
@ -381,15 +378,14 @@ def convert(ctx,
if cTokens and len ( cTokens ) == 2 :
context [ ' crop_start ' ] = int ( cTokens [ 0 ] )
context [ ' crop_length ' ] = int ( cTokens [ 1 ] )
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Crop start= { context [ ' crop_start ' ] } length= { context [ ' crop_length ' ] } " )
ctx . obj [ ' logger ' ] . debug ( f " Crop start= { context [ ' crop_start ' ] } length= { context [ ' crop_length ' ] } " )
tc = TmdbController ( ) if context [ ' use_tmdb ' ] else None
existingSourcePaths = [ p for p in paths if os . path . isfile ( p ) and p . split ( ' . ' ) [ - 1 ] in FfxController . INPUT_FILE_EXTENSIONS ]
if ctx . obj [ ' verbosity' ] > 0 :
click . echo ( f " \n Running { len ( existingSourcePaths ) * len ( q_list ) } jobs " )
ctx . obj [ ' logger' ] . info ( f " \n Running { len ( existingSourcePaths ) * len ( q_list ) } jobs " )
jobIndex = 0
for sourcePath in existingSourcePaths :
@ -402,8 +398,7 @@ def convert(ctx,
sourceFileBasename = ' . ' . join ( sourcePathTokens [ : - 1 ] )
sourceFilenameExtension = sourcePathTokens [ - 1 ]
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " \n Processing file { sourcePath } " )
ctx . obj [ ' logger ' ] . info ( f " \n Processing file { sourcePath } " )
mediaFileProperties = FileProperties ( context , sourceFilename )
@ -412,8 +407,7 @@ def convert(ctx,
#HINT: This is None if the filename did not match anything in database
currentPattern = mediaFileProperties . getPattern ( ) if context [ ' use_pattern ' ] else None
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Pattern matching: { ' No ' if currentPattern is None else ' Yes ' } " )
ctx . obj [ ' logger ' ] . debug ( f " Pattern matching: { ' No ' if currentPattern is None else ' Yes ' } " )
# fileBasename = ''
@ -452,13 +446,14 @@ def convert(ctx,
if context [ ' use_tmdb ' ] :
c lick. echo ( f " Querying TMDB for show_id= { currentShowDescriptor . getId ( ) } season= { mediaFileProperties . getSeason ( ) } episode { mediaFileProperties . getEpisode ( ) } " )
c tx. obj [ ' logger ' ] . debug ( f " Querying TMDB for show_id= { currentShowDescriptor . getId ( ) } season= { mediaFileProperties . getSeason ( ) } episode { mediaFileProperties . getEpisode ( ) } " )
tmdbEpisodeResult = tc . queryEpisode ( currentShowDescriptor . getId ( ) , mediaFileProperties . getSeason ( ) , mediaFileProperties . getEpisode ( ) )
c lick. echo ( f " tmdbEpisodeResult= { tmdbEpisodeResult } " )
c tx. obj [ ' logger ' ] . debug ( f " tmdbEpisodeResult= { tmdbEpisodeResult } " )
if tmdbEpisodeResult :
filteredEpisodeName = filterFilename ( tmdbEpisodeResult [ ' name ' ] )
sourceFileBasename = TmdbController . getEpisodeFileBasename ( currentShowDescriptor . getFilenamePrefix ( ) ,
tmdbEpisodeResult[ ' name ' ] ,
filteredEpisodeName ,
mediaFileProperties . getSeason ( ) ,
mediaFileProperties . getEpisode ( ) ,
currentShowDescriptor . getIndexSeasonDigits ( ) ,
@ -474,50 +469,45 @@ def convert(ctx,
mediaFileProperties . getSeason ( ) ,
mediaFileProperties . getEpisode ( ) )
# raise click.ClickException(f"tmd subindices: {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
# click.echo(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
ctx . obj [ ' logger ' ] . debug ( f " tmd subindices: { [ t . getIndex ( ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } { [ t . getSubIndex ( ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } { [ t . getDispositionFlag ( TrackDisposition . DEFAULT ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } " )
if context [ ' use_jellyfin ' ] :
# Reorder subtracks in types with default the last, then make subindices flat again
targetMediaDescriptor . applyJellyfinOrder ( )
# sourceMediaDescriptor.applyJellyfinOrder()
# click.echo(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
# raise click.Abort
ctx . obj [ ' logger ' ] . debug ( f " tmd subindices: { [ t . getIndex ( ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } { [ t . getSubIndex ( ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } { [ t . getDispositionFlag ( TrackDisposition . DEFAULT ) for t in targetMediaDescriptor . getAllTrackDescriptors ( ) ] } " )
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Input mapping tokens (2nd pass): { targetMediaDescriptor . getInputMappingTokens ( ) } " )
ctx . obj [ ' logger ' ] . debug ( f " Input mapping tokens (2nd pass): { targetMediaDescriptor . getInputMappingTokens ( ) } " )
fc = FfxController ( context , targetMediaDescriptor , sourceMediaDescriptor )
ctx . obj [ ' logger ' ] . debug ( f " Season= { mediaFileProperties . getSeason ( ) } Episode= { mediaFileProperties . getEpisode ( ) } " )
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " Season= { mediaFileProperties . getSeason ( ) } Episode= { mediaFileProperties . getEpisode ( ) } " )
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " fileBasename= { sourceFileBasename } " )
ctx . obj [ ' logger ' ] . debug ( f " fileBasename= { sourceFileBasename } " )
for q in q_list :
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " \n Running job { jobIndex } file= { sourcePath } q= { q } " )
ctx . obj [ ' logger ' ] . debug ( f " \n Running job { jobIndex } file= { sourcePath } q= { q } " )
jobIndex + = 1
extra = [ ' ffx ' ] if sourceFilenameExtension == FfxController . DEFAULT_FILE_EXTENSION else [ ]
c lick. echo ( f " label= { label if label else ' Falsy ' } " )
c lick. echo ( f " sourceFileBasename= { sourceFileBasename } " )
c tx. obj [ ' logger ' ] . debug ( f " label= { label if label else ' Falsy ' } " )
c tx. obj [ ' logger ' ] . debug ( f " sourceFileBasename= { sourceFileBasename } " )
targetFilename = ( sourceFileBasename if context [ ' use_tmdb ' ]
else mediaFileProperties . assembleTargetFileBasename ( label ,
q if len ( q_list ) > 1 else - 1 ,
extraTokens = extra ) )
targetFileBasename = mediaFileProperties . assembleTargetFileBasename ( label ,
q if len ( q_list ) > 1 else - 1 ,
extraTokens = extra )
#TODO #387
targetFilename = ( ( f " { sourceFileBasename } _q { q } " if len ( q_list ) > 1 else sourceFileBasename )
if context [ ' use_tmdb ' ] else targetFileBasename )
targetPath = os . path . join ( output_directory if output_directory else sourceDirectory , targetFilename )
# media_S01E02_S01E02
c lick. echo ( f " targetPath= { targetPath } " )
# TODO: target extension anpassen
c tx. obj [ ' logger ' ] . info ( f " Creating file { targetFilename } .webm " )
fc . runJob ( sourcePath ,
targetPath ,
@ -529,8 +519,7 @@ def convert(ctx,
#TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)
endTime = time . perf_counter ( )
if ctx . obj [ ' verbosity ' ] > 0 :
click . echo ( f " \n DONE \n Time elapsed { endTime - startTime } " )
ctx . obj [ ' logger ' ] . info ( f " \n DONE \n Time elapsed { endTime - startTime } " )
if __name__ == ' __main__ ' :