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