4 Commits

Author SHA1 Message Date
Maveno
a3bb16e850 Signature, Tags cleaning, Bugfixes, Refactoring 2024-11-04 16:44:08 +01:00
Maveno
0ed85fce4a Add asterisk to filename filter, Signature 2024-11-04 14:28:11 +01:00
Maveno
1a0a5f4482 Fix test pattern, Test-Limit 2024-11-04 12:54:22 +01:00
Maveno
06f6322d32 ff 2024-11-04 11:45:09 +01:00
13 changed files with 184 additions and 132 deletions

View File

@@ -19,9 +19,13 @@ 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
from ffx.constants import DEFAULT_QUALITY, DEFAULT_AV1_PRESET
from ffx.constants import DEFAULT_STEREO_BANDWIDTH, DEFAULT_AC3_BANDWIDTH, DEFAULT_DTS_BANDWIDTH, DEFAULT_7_1_BANDWIDTH
VERSION='0.2.0' VERSION='0.2.1'
# 0.1.1 # 0.1.1
# Bugfixes, TMBD identify shows # Bugfixes, TMBD identify shows
@@ -31,6 +35,9 @@ VERSION='0.2.0'
# Subtitle file imports # Subtitle file imports
# 0.2.0 # 0.2.0
# Tests, Config-File # Tests, Config-File
# 0.2.1
# Signature, Tags cleaning, Bugfixes, Refactoring
@click.group() @click.group()
@click.pass_context @click.pass_context
@@ -150,8 +157,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"\nUnmuxing {len(existingSourcePaths)} files")
click.echo(f"\nUnmuxing {len(existingSourcePaths)} files")
for sourcePath in existingSourcePaths: for sourcePath in existingSourcePaths:
@@ -169,12 +175,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"\nUnmuxing file {fp.getFilename()}\n")
click.echo(f"\nUnmuxing file {fp.getFilename()}\n")
for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors(): for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors():
@@ -187,18 +191,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()
@@ -264,12 +264,12 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
@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('-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('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {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=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {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=DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {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=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', 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=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})")
@click.option('--subtitle-directory', type=str, default='', help='Load subtitles from here') @click.option('--subtitle-directory', type=str, default='', help='Load subtitles from here')
@click.option('--subtitle-prefix', type=str, default='', help='Subtitle filename prefix') @click.option('--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
@@ -298,8 +298,12 @@ 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)
@click.option("--keep-mkvmerge-metadata", is_flag=True, default=False)
def convert(ctx, def convert(ctx,
paths, paths,
@@ -331,7 +335,9 @@ def convert(ctx,
no_jellyfin, no_jellyfin,
no_pattern, no_pattern,
dont_pass_dispositions, dont_pass_dispositions,
no_prompt): no_prompt,
no_signature,
keep_mkvmerge_metadata):
"""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 +355,30 @@ 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['keep_mkvmerge_metadata'] = keep_mkvmerge_metadata
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"\nVideo 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 +388,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"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
jobIndex = 0 jobIndex = 0
for sourcePath in existingSourcePaths: for sourcePath in existingSourcePaths:
@@ -402,8 +408,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"\nProcessing file {sourcePath}")
click.echo(f"\nProcessing file {sourcePath}")
mediaFileProperties = FileProperties(context, sourceFilename) mediaFileProperties = FileProperties(context, sourceFilename)
@@ -412,8 +417,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 +456,14 @@ def convert(ctx,
if context['use_tmdb']: if context['use_tmdb']:
click.echo(f"Querying TMDB for show_id={currentShowDescriptor.getId()} season={mediaFileProperties.getSeason()} episode{mediaFileProperties.getEpisode()}") ctx.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())
click.echo(f"tmdbEpisodeResult={tmdbEpisodeResult}") ctx.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 +479,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"\nRunning job {jobIndex} file={sourcePath} q={q}")
click.echo(f"\nRunning 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}")
targetFilename = (sourceFileBasename if context['use_tmdb'] targetFileBasename = mediaFileProperties.assembleTargetFileBasename(label,
else mediaFileProperties.assembleTargetFileBasename(label, q if len(q_list) > 1 else -1,
q if len(q_list) > 1 else -1, extraTokens = extra)
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) targetPath = os.path.join(output_directory if output_directory else sourceDirectory, targetFilename)
# media_S01E02_S01E02 #TODO: target extension anpassen
click.echo(f"targetPath={targetPath}") ctx.obj['logger'].info(f"Creating file {targetFilename}.webm")
fc.runJob(sourcePath, fc.runJob(sourcePath,
targetPath, targetPath,
@@ -529,8 +529,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"\nDONE\nTime elapsed {endTime - startTime}")
click.echo(f"\nDONE\nTime elapsed {endTime - startTime}")
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -28,7 +28,6 @@ class AudioLayout(Enum):
return [a for a in AudioLayout if a.value['label'] == str(label)][0] return [a for a in AudioLayout if a.value['label'] == str(label)][0]
except: except:
raise click.ClickException('fromLabel failed')
return AudioLayout.LAYOUT_UNDEFINED return AudioLayout.LAYOUT_UNDEFINED
@staticmethod @staticmethod
@@ -36,7 +35,6 @@ class AudioLayout(Enum):
try: try:
return [a for a in AudioLayout if a.value['index'] == int(index)][0] return [a for a in AudioLayout if a.value['index'] == int(index)][0]
except: except:
raise click.ClickException('fromIndex failed')
return AudioLayout.LAYOUT_UNDEFINED return AudioLayout.LAYOUT_UNDEFINED
@staticmethod @staticmethod

10
bin/ffx/constants.py Normal file
View File

@@ -0,0 +1,10 @@
DEFAULT_QUALITY = 32
DEFAULT_AV1_PRESET = 5
DEFAULT_STEREO_BANDWIDTH = "112"
DEFAULT_AC3_BANDWIDTH = "256"
DEFAULT_DTS_BANDWIDTH = "320"
DEFAULT_7_1_BANDWIDTH = "384"
DEFAULT_CROP_START = 60
DEFAULT_CROP_LENGTH = 180

View File

@@ -10,6 +10,10 @@ from ffx.video_encoder import VideoEncoder
from ffx.process import executeProcess from ffx.process import executeProcess
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
from ffx.constants import DEFAULT_QUALITY, DEFAULT_AV1_PRESET
from ffx.constants import DEFAULT_CROP_START, DEFAULT_CROP_LENGTH
class FfxController(): class FfxController():
COMMAND_TOKENS = ['ffmpeg', '-y'] COMMAND_TOKENS = ['ffmpeg', '-y']
@@ -19,19 +23,9 @@ class FfxController():
DEFAULT_VIDEO_ENCODER = VideoEncoder.VP9.label() DEFAULT_VIDEO_ENCODER = VideoEncoder.VP9.label()
DEFAULT_QUALITY = 23
DEFAULT_AV1_PRESET = 5
DEFAULT_FILE_FORMAT = 'webm' DEFAULT_FILE_FORMAT = 'webm'
DEFAULT_FILE_EXTENSION = 'webm' DEFAULT_FILE_EXTENSION = 'webm'
DEFAULT_STEREO_BANDWIDTH = "128"
DEFAULT_AC3_BANDWIDTH = "256"
DEFAULT_DTS_BANDWIDTH = "320"
DEFAULT_CROP_START = 60
DEFAULT_CROP_LENGTH = 180
MKVMERGE_METADATA_KEYS = ['BPS', MKVMERGE_METADATA_KEYS = ['BPS',
'NUMBER_OF_FRAMES', 'NUMBER_OF_FRAMES',
'NUMBER_OF_BYTES', 'NUMBER_OF_BYTES',
@@ -43,6 +37,7 @@ class FfxController():
CHANNEL_MAP_5_1 = 'FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1' CHANNEL_MAP_5_1 = 'FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1'
SIGNATURE_TAGS = {'RECODED_WITH': 'FFX'}
def __init__(self, def __init__(self,
context : dict, context : dict,
@@ -97,8 +92,8 @@ class FfxController():
cropStart = int(self.__context['crop_start']) cropStart = int(self.__context['crop_start'])
cropLength = int(self.__context['crop_length']) cropLength = int(self.__context['crop_length'])
else: else:
cropStart = FfxController.DEFAULT_CROP_START cropStart = DEFAULT_CROP_START
cropLength = FfxController.DEFAULT_CROP_LENGTH cropLength = DEFAULT_CROP_LENGTH
return ['-ss', str(cropStart), '-t', str(cropLength)] return ['-ss', str(cropStart), '-t', str(cropLength)]
@@ -214,18 +209,36 @@ class FfxController():
metadataTokens = [] metadataTokens = []
for tagKey, tagValue in self.__targetMediaDescriptor.getTags().items(): mediaTags = self.__targetMediaDescriptor.getTags()
if (not 'no_signature' in self.__context.keys()
or not self.__context['no_signature']):
outputMediaTags = mediaTags | FfxController.SIGNATURE_TAGS
else:
outputMediaTags = mediaTags
for tagKey, tagValue in outputMediaTags.items():
metadataTokens += [f"-metadata:g", metadataTokens += [f"-metadata:g",
f"{tagKey}={tagValue}"] f"{tagKey}={tagValue}"]
removeMkvmergeMetadata = (not 'keep_mkvmerge_metadata' in self.__context.keys()
or not self.__context['keep_mkvmerge_metadata'])
#HINT: With current ffmpeg version track metadata tags are not passed to the outfile #HINT: With current ffmpeg version track metadata tags are not passed to the outfile
for td in self.__targetMediaDescriptor.getAllTrackDescriptors(): for td in self.__targetMediaDescriptor.getAllTrackDescriptors():
for tagKey, tagValue in td.getTags().items(): for tagKey, tagValue in td.getTags().items():
metadataTokens += [f"-metadata:s:{td.getType().indicator()}:{td.getSubIndex()}", typeIndicator = td.getType().indicator()
subIndex = td.getSubIndex()
metadataTokens += [f"-metadata:s:{typeIndicator}:{subIndex}",
f"{tagKey}={tagValue}"] f"{tagKey}={tagValue}"]
if removeMkvmergeMetadata:
for mmKey in FfxController.MKVMERGE_METADATA_KEYS:
metadataTokens += [f"-metadata:s:{typeIndicator}:{subIndex}",
f"{mmKey}="]
return metadataTokens return metadataTokens

View File

@@ -14,7 +14,8 @@ class FileProperties():
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm'] FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)' SE_INDICATOR_PATTERN = '([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]+)'
DEFAULT_INDEX_DIGITS = 3 DEFAULT_INDEX_DIGITS = 3
@@ -48,14 +49,18 @@ class FileProperties():
# Checking if database contains matching pattern # Checking if database contains matching pattern
matchResult = self.__pc.matchFilename(self.__sourceFilename) matchResult = self.__pc.matchFilename(self.__sourceFilename)
self.__logger.debug(f"FileProperties.__init__(): Match result {matchResult}") self.__logger.debug(f"FileProperties.__init__(): Match result: {matchResult}")
self.__pattern: Pattern = matchResult['pattern'] if matchResult else None self.__pattern: Pattern = matchResult['pattern'] if matchResult else None
if matchResult: if matchResult:
databaseMatchedGroups = matchResult['match'].groups() databaseMatchedGroups = matchResult['match'].groups()
self.__season = databaseMatchedGroups[0] self.__logger.debug(f"FileProperties.__init__(): Matched groups: {databaseMatchedGroups}")
self.__episode = databaseMatchedGroups[1]
seIndicator = databaseMatchedGroups[0]
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
else: else:
self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}") self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}")
@@ -63,15 +68,15 @@ class FileProperties():
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, self.__sourceFilename) se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, self.__sourceFilename)
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, self.__sourceFilename) e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, self.__sourceFilename)
if se_match is not None: if se_match is not None:
self.__season = int(se_match.group(1)) self.__season = int(se_match.group(1))
self.__episode = int(se_match.group(2)) self.__episode = int(se_match.group(2))
elif e_match is not None: elif e_match is not None:
self.__season = -1 self.__season = -1
self.__episode = int(e_match.group(1)) self.__episode = int(e_match.group(1))
else: else:
self.__season = -1 self.__season = -1
self.__episode = -1 self.__episode = -1
def getFormatData(self): def getFormatData(self):
@@ -224,7 +229,7 @@ class FileProperties():
# targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION if extension is None else str(extension) # targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION if extension is None else str(extension)
click.echo(f"assembleTargetFileBasename(): label={label} is {'truthy' if label else 'falsy'}") self.__logger.debug(f"assembleTargetFileBasename(): label={label} is {'truthy' if label else 'falsy'}")
if label: if label:
@@ -251,7 +256,6 @@ class FileProperties():
targetFilename = '_'.join(targetFilenameTokens) targetFilename = '_'.join(targetFilenameTokens)
#self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}") self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
click.echo(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
return targetFilename return targetFilename

View File

@@ -57,5 +57,11 @@ def filterFilename(fileName: str) -> str:
"""This filter replaces charactes from TMDB responses with characters """This filter replaces charactes from TMDB responses with characters
less problemating when using in filenames or removes them""" less problemating when using in filenames or removes them"""
# This appears in TMDB episode names
fileName = str(fileName).replace(' (*)', '')
fileName = str(fileName).replace('(*)', '')
fileName = str(fileName).replace(':', ';') fileName = str(fileName).replace(':', ';')
return fileName fileName = str(fileName).replace('*', '')
return fileName.strip()

View File

@@ -499,7 +499,7 @@ class MediaDescriptor:
matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]] matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]]
if matchingSubtitleTrackDescriptor: if matchingSubtitleTrackDescriptor:
# click.echo(f"Found matching subtitle file {msfd["path"]}\n") # click.echo(f"Found matching subtitle file {msfd["path"]}\n")
self.__logger.debug(f"importSubtitles(): Found matching subtitle file {msfd["path"]}") self.__logger.debug(f"importSubtitles(): Found matching subtitle file {msfd['path']}")
matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(msfd["path"]) matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath(msfd["path"])

View File

@@ -419,14 +419,12 @@ class MediaDetailsScreen(Screen):
if event.button.id == "pattern_button": if event.button.id == "pattern_button":
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
pattern = self.query_one("#pattern_input", Input).value pattern = self.query_one("#pattern_input", Input).value
patternMatch = re.search(INDICATOR_PATTERN, pattern) patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
if patternMatch: if patternMatch:
self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN) self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), FileProperties.SE_INDICATOR_PATTERN)
if event.button.id == "select_default_button": if event.button.id == "select_default_button":

View File

@@ -28,6 +28,8 @@ from ffx.track_descriptor import TrackDescriptor
from textual.widgets._data_table import CellDoesNotExist from textual.widgets._data_table import CellDoesNotExist
from ffx.file_properties import FileProperties
# Screen[dict[int, str, int]] # Screen[dict[int, str, int]]
class PatternDetailsScreen(Screen): class PatternDetailsScreen(Screen):
@@ -387,15 +389,13 @@ class PatternDetailsScreen(Screen):
if event.button.id == "pattern_button": if event.button.id == "pattern_button":
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
pattern = self.query_one("#pattern_input", Input).value pattern = self.query_one("#pattern_input", Input).value
patternMatch = re.search(INDICATOR_PATTERN, pattern) patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
if patternMatch: if patternMatch:
self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN) self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1),
FileProperties.SE_INDICATOR_PATTERN)
def handle_add_track(self, trackDescriptor : TrackDescriptor): def handle_add_track(self, trackDescriptor : TrackDescriptor):

View File

@@ -65,9 +65,12 @@ class Scenario1(Scenario):
expectedFilename = f"{expectedBasename}.{Scenario1.EXPECTED_FILE_EXTENSION}" expectedFilename = f"{expectedBasename}.{Scenario1.EXPECTED_FILE_EXTENSION}"
if self._context['test_variant'] and variantIdentifier != self._context['test_variant']: if self._context['test_variant'] and not variantIdentifier.startswith(self._context['test_variant']):
return return
if ((self._context['test_passed_counter'] + self._context['test_failed_counter'])
>= self._context['test_limit']):
return
self._logger.debug(f"Running Job: {variantLabel}") self._logger.debug(f"Running Job: {variantLabel}")
@@ -93,13 +96,14 @@ class Scenario1(Scenario):
commandSequence = [sys.executable, commandSequence = [sys.executable,
self._ffxExecutablePath] self._ffxExecutablePath]
# if self._context['verbosity']: if self._context['verbosity']:
# commandSequence += ['--verbose', commandSequence += ['--verbose',
# str(self._context['verbosity'])] str(self._context['verbosity'])]
commandSequence += ['convert', commandSequence += ['convert',
mediaFilePath, mediaFilePath,
'--no-prompt'] '--no-prompt',
'--no-signature']
if variantFilenameLabel: if variantFilenameLabel:
commandSequence += ['--label', variantFilenameLabel] commandSequence += ['--label', variantFilenameLabel]
@@ -116,7 +120,7 @@ class Scenario1(Scenario):
out, err, rc = executeProcess(commandSequence, directory = self._testDirectory) out, err, rc = executeProcess(commandSequence, directory = self._testDirectory)
if out: if out and self._context['verbosity'] >= 9:
self._logger.debug(f"{variantLabel}: Process output: {out}") self._logger.debug(f"{variantLabel}: Process output: {out}")
if rc: if rc:
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})") self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")

View File

@@ -64,10 +64,13 @@ class Scenario2(Scenario):
jellyfinSelectorIndex = -1 jellyfinSelectorIndex = -1
#if self._context['test_variant'] and variantIdentifier != self._context['test_variant']:
if self._context['test_variant'] and not variantIdentifier.startswith(self._context['test_variant']): if self._context['test_variant'] and not variantIdentifier.startswith(self._context['test_variant']):
return return
if ((self._context['test_passed_counter'] + self._context['test_failed_counter'])
>= self._context['test_limit']):
return
self._logger.debug(f"Running Job: {variantLabel}") self._logger.debug(f"Running Job: {variantLabel}")
@@ -83,10 +86,16 @@ class Scenario2(Scenario):
# Phase 2: Run ffx # Phase 2: Run ffx
commandSequence = [sys.executable, commandSequence = [sys.executable,
self._ffxExecutablePath, self._ffxExecutablePath]
'convert',
if self._context['verbosity']:
commandSequence += ['--verbose',
str(self._context['verbosity'])]
commandSequence += ['convert',
mediaFilePath, mediaFilePath,
'--no-prompt'] '--no-prompt',
'--no-signature']
if not testContext['use_jellyfin']: if not testContext['use_jellyfin']:
commandSequence += ['--no-jellyfin'] commandSequence += ['--no-jellyfin']
@@ -96,7 +105,7 @@ class Scenario2(Scenario):
out, err, rc = executeProcess(commandSequence, directory = self._testDirectory) out, err, rc = executeProcess(commandSequence, directory = self._testDirectory)
if out: if out and self._context['verbosity'] >= 9:
self._logger.debug(f"{variantLabel}: Process output: {out}") self._logger.debug(f"{variantLabel}: Process output: {out}")
if rc: if rc:
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})") self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")

View File

@@ -33,7 +33,7 @@ class Scenario4(Scenario):
TEST_FILE_LABEL = 'rotsh' TEST_FILE_LABEL = 'rotsh'
TEST_FILE_EXTENSION = 'mkv' TEST_FILE_EXTENSION = 'mkv'
TEST_PATTERN = f"{TEST_FILE_LABEL}_{FileProperties.SEASON_EPISODE_INDICATOR_MATCH}.{TEST_FILE_EXTENSION}" TEST_PATTERN = f"{TEST_FILE_LABEL}_{FileProperties.SE_INDICATOR_PATTERN}.{TEST_FILE_EXTENSION}"
EXPECTED_FILE_EXTENSION = 'webm' EXPECTED_FILE_EXTENSION = 'webm'
@@ -118,17 +118,22 @@ class Scenario4(Scenario):
jellyfinSelectorIndex = -1 jellyfinSelectorIndex = -1
if self._context['test_variant'] and variantIdentifier != self._context['test_variant']: if self._context['test_variant'] and not variantIdentifier.startswith(self._context['test_variant']):
return return
if ((self._context['test_passed_counter'] + self._context['test_failed_counter'])
>= self._context['test_limit']):
return
self._logger.debug(f"Running Job: {variantLabel}")
for l in presetMediaDescriptor.getConfiguration(label = 'presetMediaDescriptor'): for l in presetMediaDescriptor.getConfiguration(label = 'presetMediaDescriptor'):
self._logger.debug(l) self._logger.debug(l)
for l in sourceMediaDescriptor.getConfiguration(label = 'sourceMediaDescriptor'): for l in sourceMediaDescriptor.getConfiguration(label = 'sourceMediaDescriptor'):
self._logger.debug(l) self._logger.debug(l)
self._logger.debug(f"Running Job: {variantLabel}")
# Phase 1: Setup source files # Phase 1: Setup source files
@@ -164,13 +169,18 @@ class Scenario4(Scenario):
# Phase 3: Run ffx # Phase 3: Run ffx
commandSequence = [sys.executable, commandSequence = [sys.executable,
self._ffxExecutablePath, self._ffxExecutablePath]
'--database-file',
if self._context['verbosity']:
commandSequence += ['--verbose',
str(self._context['verbosity'])]
commandSequence += ['--database-file',
self._testDbFilePath, self._testDbFilePath,
'convert'] 'convert']
commandSequence += [tfo['filename'] for tfo in testFileList] commandSequence += [tfo['filename'] for tfo in testFileList]
commandSequence += ['--no-prompt'] commandSequence += ['--no-prompt', '--no-signature']
if not testContext['use_jellyfin']: if not testContext['use_jellyfin']:
commandSequence += ['--no-jellyfin'] commandSequence += ['--no-jellyfin']
@@ -179,8 +189,8 @@ class Scenario4(Scenario):
out, err, rc = executeProcess(commandSequence, directory = self._testDirectory) out, err, rc = executeProcess(commandSequence, directory = self._testDirectory)
# if out: if out and self._context['verbosity'] >= 9:
# self._logger.debug(f"{variantLabel}: Process output: {out}") self._logger.debug(f"{variantLabel}: Process output: {out}")
if rc: if rc:
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})") self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")
@@ -258,14 +268,13 @@ class Scenario4(Scenario):
self._context['test_passed_counter'] += 1 self._context['test_passed_counter'] += 1
self._reportLogger.info(f"{variantLabel}: Test passed") self._reportLogger.info(f"\n{variantLabel}: Test passed\n")
except AssertionError as ae: except AssertionError as ae:
self._context['test_failed_counter'] += 1 self._context['test_failed_counter'] += 1
self._reportLogger.error(f"{variantLabel}: Test FAILED ({ae})") self._reportLogger.error(f"\n{variantLabel}: Test FAILED ({ae})\n")
# exit()
def run(self): def run(self):

View File

@@ -75,8 +75,9 @@ def ffx(ctx, verbose, dry_run):
@ffx.command() @ffx.command()
@click.pass_context @click.pass_context
@click.option('--scenario', type=str, default='', help='Only run tests from this scenario') @click.option('--scenario', type=str, default='', help='Only run tests from this scenario')
@click.option('--variant', type=str, default='', help='Only run this test variant') @click.option('--variant', type=str, default='', help='Only run variants beginning like this')
def run(ctx, scenario, variant): @click.option('--limit', type=int, default=0, help='Only run this number of tests')
def run(ctx, scenario, variant, limit):
"""Run ffx test sequences""" """Run ffx test sequences"""
ctx.obj['logger'].info('Starting FFX test runs') ctx.obj['logger'].info('Starting FFX test runs')
@@ -84,6 +85,7 @@ def run(ctx, scenario, variant):
ctx.obj['test_failed_counter'] = 0 ctx.obj['test_failed_counter'] = 0
ctx.obj['test_variant'] = variant ctx.obj['test_variant'] = variant
ctx.obj['test_limit'] = limit
for si in Scenario.list(): for si in Scenario.list():