Compare commits
3 Commits
06f6322d32
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3bb16e850 | ||
|
|
0ed85fce4a | ||
|
|
1a0a5f4482 |
123
bin/ffx.py
123
bin/ffx.py
@@ -19,9 +19,13 @@ from ffx.video_encoder import VideoEncoder
|
||||
from ffx.track_disposition import TrackDisposition
|
||||
|
||||
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
|
||||
# Bugfixes, TMBD identify shows
|
||||
@@ -31,6 +35,9 @@ VERSION='0.2.0'
|
||||
# Subtitle file imports
|
||||
# 0.2.0
|
||||
# Tests, Config-File
|
||||
# 0.2.1
|
||||
# Signature, Tags cleaning, Bugfixes, Refactoring
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
@@ -150,8 +157,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"\nUnmuxing {len(existingSourcePaths)} files")
|
||||
ctx.obj['logger'].debug(f"\nUnmuxing {len(existingSourcePaths)} files")
|
||||
|
||||
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 ''
|
||||
|
||||
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"\nUnmuxing file {fp.getFilename()}\n")
|
||||
ctx.obj['logger'].debug(f"\nUnmuxing file {fp.getFilename()}\n")
|
||||
|
||||
for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors():
|
||||
|
||||
@@ -187,18 +191,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()
|
||||
@@ -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('-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('-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=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('--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('-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=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=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-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-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)
|
||||
@click.option("--keep-mkvmerge-metadata", is_flag=True, default=False)
|
||||
|
||||
def convert(ctx,
|
||||
paths,
|
||||
@@ -331,7 +335,9 @@ def convert(ctx,
|
||||
no_jellyfin,
|
||||
no_pattern,
|
||||
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
|
||||
|
||||
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_pattern'] = not no_pattern
|
||||
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)
|
||||
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"\nVideo 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 +388,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"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
||||
ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs")
|
||||
|
||||
jobIndex = 0
|
||||
|
||||
for sourcePath in existingSourcePaths:
|
||||
@@ -402,8 +408,7 @@ def convert(ctx,
|
||||
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
||||
sourceFilenameExtension = sourcePathTokens[-1]
|
||||
|
||||
if ctx.obj['verbosity'] > 0:
|
||||
click.echo(f"\nProcessing file {sourcePath}")
|
||||
ctx.obj['logger'].info(f"\nProcessing file {sourcePath}")
|
||||
|
||||
|
||||
mediaFileProperties = FileProperties(context, sourceFilename)
|
||||
@@ -412,8 +417,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 +456,14 @@ def convert(ctx,
|
||||
|
||||
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())
|
||||
click.echo(f"tmdbEpisodeResult={tmdbEpisodeResult}")
|
||||
ctx.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 +479,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"\nRunning job {jobIndex} file={sourcePath} q={q}")
|
||||
ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} q={q}")
|
||||
jobIndex += 1
|
||||
|
||||
extra = ['ffx'] if sourceFilenameExtension == FfxController.DEFAULT_FILE_EXTENSION else []
|
||||
|
||||
click.echo(f"label={label if label else 'Falsy'}")
|
||||
click.echo(f"sourceFileBasename={sourceFileBasename}")
|
||||
ctx.obj['logger'].debug(f"label={label if label else 'Falsy'}")
|
||||
ctx.obj['logger'].debug(f"sourceFileBasename={sourceFileBasename}")
|
||||
|
||||
targetFilename = (sourceFileBasename if context['use_tmdb']
|
||||
else mediaFileProperties.assembleTargetFileBasename(label,
|
||||
targetFileBasename = mediaFileProperties.assembleTargetFileBasename(label,
|
||||
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)
|
||||
|
||||
# media_S01E02_S01E02
|
||||
click.echo(f"targetPath={targetPath}")
|
||||
#TODO: target extension anpassen
|
||||
ctx.obj['logger'].info(f"Creating file {targetFilename}.webm")
|
||||
|
||||
fc.runJob(sourcePath,
|
||||
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)
|
||||
|
||||
endTime = time.perf_counter()
|
||||
if ctx.obj['verbosity'] > 0:
|
||||
click.echo(f"\nDONE\nTime elapsed {endTime - startTime}")
|
||||
ctx.obj['logger'].info(f"\nDONE\nTime elapsed {endTime - startTime}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -28,7 +28,6 @@ class AudioLayout(Enum):
|
||||
|
||||
return [a for a in AudioLayout if a.value['label'] == str(label)][0]
|
||||
except:
|
||||
raise click.ClickException('fromLabel failed')
|
||||
return AudioLayout.LAYOUT_UNDEFINED
|
||||
|
||||
@staticmethod
|
||||
@@ -36,7 +35,6 @@ class AudioLayout(Enum):
|
||||
try:
|
||||
return [a for a in AudioLayout if a.value['index'] == int(index)][0]
|
||||
except:
|
||||
raise click.ClickException('fromIndex failed')
|
||||
return AudioLayout.LAYOUT_UNDEFINED
|
||||
|
||||
@staticmethod
|
||||
|
||||
10
bin/ffx/constants.py
Normal file
10
bin/ffx/constants.py
Normal 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
|
||||
@@ -10,6 +10,10 @@ from ffx.video_encoder import VideoEncoder
|
||||
from ffx.process import executeProcess
|
||||
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():
|
||||
|
||||
COMMAND_TOKENS = ['ffmpeg', '-y']
|
||||
@@ -19,19 +23,9 @@ class FfxController():
|
||||
|
||||
DEFAULT_VIDEO_ENCODER = VideoEncoder.VP9.label()
|
||||
|
||||
DEFAULT_QUALITY = 23
|
||||
DEFAULT_AV1_PRESET = 5
|
||||
|
||||
DEFAULT_FILE_FORMAT = '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',
|
||||
'NUMBER_OF_FRAMES',
|
||||
'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'
|
||||
|
||||
SIGNATURE_TAGS = {'RECODED_WITH': 'FFX'}
|
||||
|
||||
def __init__(self,
|
||||
context : dict,
|
||||
@@ -97,8 +92,8 @@ class FfxController():
|
||||
cropStart = int(self.__context['crop_start'])
|
||||
cropLength = int(self.__context['crop_length'])
|
||||
else:
|
||||
cropStart = FfxController.DEFAULT_CROP_START
|
||||
cropLength = FfxController.DEFAULT_CROP_LENGTH
|
||||
cropStart = DEFAULT_CROP_START
|
||||
cropLength = DEFAULT_CROP_LENGTH
|
||||
|
||||
return ['-ss', str(cropStart), '-t', str(cropLength)]
|
||||
|
||||
@@ -214,18 +209,36 @@ class FfxController():
|
||||
|
||||
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",
|
||||
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
|
||||
for td in self.__targetMediaDescriptor.getAllTrackDescriptors():
|
||||
|
||||
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}"]
|
||||
|
||||
if removeMkvmergeMetadata:
|
||||
for mmKey in FfxController.MKVMERGE_METADATA_KEYS:
|
||||
metadataTokens += [f"-metadata:s:{typeIndicator}:{subIndex}",
|
||||
f"{mmKey}="]
|
||||
|
||||
return metadataTokens
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class FileProperties():
|
||||
|
||||
FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
|
||||
|
||||
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]+)'
|
||||
|
||||
@@ -48,14 +49,18 @@ class FileProperties():
|
||||
# Checking if database contains matching pattern
|
||||
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
|
||||
|
||||
if matchResult:
|
||||
databaseMatchedGroups = matchResult['match'].groups()
|
||||
self.__season = databaseMatchedGroups[0]
|
||||
self.__episode = databaseMatchedGroups[1]
|
||||
self.__logger.debug(f"FileProperties.__init__(): Matched groups: {databaseMatchedGroups}")
|
||||
|
||||
seIndicator = databaseMatchedGroups[0]
|
||||
|
||||
se_match = re.search(FileProperties.SEASON_EPISODE_INDICATOR_MATCH, seIndicator)
|
||||
e_match = re.search(FileProperties.EPISODE_INDICATOR_MATCH, seIndicator)
|
||||
|
||||
else:
|
||||
self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}")
|
||||
@@ -224,7 +229,7 @@ class FileProperties():
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -251,7 +256,6 @@ class FileProperties():
|
||||
|
||||
targetFilename = '_'.join(targetFilenameTokens)
|
||||
|
||||
#self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
|
||||
click.echo(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
|
||||
self.__logger.debug(f"assembleTargetFileBasename(): Target filename: {targetFilename}")
|
||||
|
||||
return targetFilename
|
||||
|
||||
@@ -57,5 +57,11 @@ def filterFilename(fileName: str) -> str:
|
||||
"""This filter replaces charactes from TMDB responses with characters
|
||||
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(':', ';')
|
||||
return fileName
|
||||
fileName = str(fileName).replace('*', '')
|
||||
|
||||
return fileName.strip()
|
||||
|
||||
@@ -419,14 +419,12 @@ class MediaDetailsScreen(Screen):
|
||||
|
||||
if event.button.id == "pattern_button":
|
||||
|
||||
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
|
||||
|
||||
pattern = self.query_one("#pattern_input", Input).value
|
||||
|
||||
patternMatch = re.search(INDICATOR_PATTERN, pattern)
|
||||
patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
|
||||
|
||||
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":
|
||||
|
||||
@@ -28,6 +28,8 @@ from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
from textual.widgets._data_table import CellDoesNotExist
|
||||
|
||||
from ffx.file_properties import FileProperties
|
||||
|
||||
|
||||
# Screen[dict[int, str, int]]
|
||||
class PatternDetailsScreen(Screen):
|
||||
@@ -387,15 +389,13 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
if event.button.id == "pattern_button":
|
||||
|
||||
INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
|
||||
|
||||
pattern = self.query_one("#pattern_input", Input).value
|
||||
|
||||
patternMatch = re.search(INDICATOR_PATTERN, pattern)
|
||||
patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
|
||||
|
||||
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):
|
||||
|
||||
|
||||
@@ -65,9 +65,12 @@ class Scenario1(Scenario):
|
||||
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
|
||||
|
||||
if ((self._context['test_passed_counter'] + self._context['test_failed_counter'])
|
||||
>= self._context['test_limit']):
|
||||
return
|
||||
|
||||
self._logger.debug(f"Running Job: {variantLabel}")
|
||||
|
||||
@@ -93,13 +96,14 @@ class Scenario1(Scenario):
|
||||
commandSequence = [sys.executable,
|
||||
self._ffxExecutablePath]
|
||||
|
||||
# if self._context['verbosity']:
|
||||
# commandSequence += ['--verbose',
|
||||
# str(self._context['verbosity'])]
|
||||
if self._context['verbosity']:
|
||||
commandSequence += ['--verbose',
|
||||
str(self._context['verbosity'])]
|
||||
|
||||
commandSequence += ['convert',
|
||||
mediaFilePath,
|
||||
'--no-prompt']
|
||||
'--no-prompt',
|
||||
'--no-signature']
|
||||
|
||||
if variantFilenameLabel:
|
||||
commandSequence += ['--label', variantFilenameLabel]
|
||||
@@ -116,7 +120,7 @@ class Scenario1(Scenario):
|
||||
|
||||
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}")
|
||||
if rc:
|
||||
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")
|
||||
|
||||
@@ -64,10 +64,13 @@ class Scenario2(Scenario):
|
||||
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
|
||||
|
||||
if ((self._context['test_passed_counter'] + self._context['test_failed_counter'])
|
||||
>= self._context['test_limit']):
|
||||
return
|
||||
|
||||
self._logger.debug(f"Running Job: {variantLabel}")
|
||||
|
||||
|
||||
@@ -83,10 +86,16 @@ class Scenario2(Scenario):
|
||||
# Phase 2: Run ffx
|
||||
|
||||
commandSequence = [sys.executable,
|
||||
self._ffxExecutablePath,
|
||||
'convert',
|
||||
self._ffxExecutablePath]
|
||||
|
||||
if self._context['verbosity']:
|
||||
commandSequence += ['--verbose',
|
||||
str(self._context['verbosity'])]
|
||||
|
||||
commandSequence += ['convert',
|
||||
mediaFilePath,
|
||||
'--no-prompt']
|
||||
'--no-prompt',
|
||||
'--no-signature']
|
||||
|
||||
if not testContext['use_jellyfin']:
|
||||
commandSequence += ['--no-jellyfin']
|
||||
@@ -96,7 +105,7 @@ class Scenario2(Scenario):
|
||||
|
||||
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}")
|
||||
if rc:
|
||||
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")
|
||||
|
||||
@@ -33,7 +33,7 @@ class Scenario4(Scenario):
|
||||
TEST_FILE_LABEL = 'rotsh'
|
||||
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'
|
||||
|
||||
@@ -118,17 +118,22 @@ class Scenario4(Scenario):
|
||||
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
|
||||
|
||||
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'):
|
||||
self._logger.debug(l)
|
||||
|
||||
for l in sourceMediaDescriptor.getConfiguration(label = 'sourceMediaDescriptor'):
|
||||
self._logger.debug(l)
|
||||
|
||||
self._logger.debug(f"Running Job: {variantLabel}")
|
||||
|
||||
|
||||
# Phase 1: Setup source files
|
||||
|
||||
@@ -164,13 +169,18 @@ class Scenario4(Scenario):
|
||||
# Phase 3: Run ffx
|
||||
|
||||
commandSequence = [sys.executable,
|
||||
self._ffxExecutablePath,
|
||||
'--database-file',
|
||||
self._ffxExecutablePath]
|
||||
|
||||
if self._context['verbosity']:
|
||||
commandSequence += ['--verbose',
|
||||
str(self._context['verbosity'])]
|
||||
|
||||
commandSequence += ['--database-file',
|
||||
self._testDbFilePath,
|
||||
'convert']
|
||||
commandSequence += [tfo['filename'] for tfo in testFileList]
|
||||
|
||||
commandSequence += ['--no-prompt']
|
||||
commandSequence += ['--no-prompt', '--no-signature']
|
||||
|
||||
if not testContext['use_jellyfin']:
|
||||
commandSequence += ['--no-jellyfin']
|
||||
@@ -179,8 +189,8 @@ class Scenario4(Scenario):
|
||||
|
||||
out, err, rc = executeProcess(commandSequence, directory = self._testDirectory)
|
||||
|
||||
# if out:
|
||||
# self._logger.debug(f"{variantLabel}: Process output: {out}")
|
||||
if out and self._context['verbosity'] >= 9:
|
||||
self._logger.debug(f"{variantLabel}: Process output: {out}")
|
||||
if rc:
|
||||
self._logger.debug(f"{variantLabel}: Process returned ERROR {rc} ({err})")
|
||||
|
||||
@@ -258,14 +268,13 @@ class Scenario4(Scenario):
|
||||
|
||||
|
||||
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:
|
||||
|
||||
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):
|
||||
|
||||
|
||||
@@ -75,8 +75,9 @@ def ffx(ctx, verbose, dry_run):
|
||||
@ffx.command()
|
||||
@click.pass_context
|
||||
@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')
|
||||
def run(ctx, scenario, variant):
|
||||
@click.option('--variant', type=str, default='', help='Only run variants beginning like this')
|
||||
@click.option('--limit', type=int, default=0, help='Only run this number of tests')
|
||||
def run(ctx, scenario, variant, limit):
|
||||
"""Run ffx test sequences"""
|
||||
|
||||
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_variant'] = variant
|
||||
ctx.obj['test_limit'] = limit
|
||||
|
||||
for si in Scenario.list():
|
||||
|
||||
|
||||
Reference in New Issue
Block a user