#! /usr/bin/python3 import os, click, time, logging from ffx.configuration_controller import ConfigurationController from ffx.file_properties import FileProperties from ffx.ffx_app import FfxApp from ffx.ffx_controller import FfxController from ffx.tmdb_controller import TmdbController from ffx.database import databaseContext from ffx.media_descriptor import MediaDescriptor from ffx.track_descriptor import TrackDescriptor from ffx.show_descriptor import ShowDescriptor from ffx.track_type import TrackType from ffx.video_encoder import VideoEncoder from ffx.track_disposition import TrackDisposition from ffx.nlmeans_controller import NlmeansController 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.1' # 0.1.1 # Bugfixes, TMBD identify shows # 0.1.2 # Bugfixes # 0.1.3 # Subtitle file imports # 0.2.0 # Tests, Config-File # 0.2.1 # Signature, Tags cleaning, Bugfixes, Refactoring @click.group() @click.pass_context @click.option('--database-file', type=str, default='', help='Path to database file') @click.option('-v', '--verbose', type=int, default=0, help='Set verbosity of output') @click.option("--dry-run", is_flag=True, default=False) def ffx(ctx, database_file, verbose, dry_run): """FFX""" ctx.obj = {} ctx.obj['config'] = ConfigurationController() ctx.obj['database'] = databaseContext(databasePath=database_file if database_file else ctx.obj['config'].getDatabaseFilePath()) ctx.obj['dry_run'] = dry_run ctx.obj['verbosity'] = verbose # Critical 50 # Error 40 # Warning 30 # Info 20 # Debug 10 fileLogVerbosity = max(40 - verbose * 10, 10) consoleLogVerbosity = max(20 - verbose * 10, 10) ctx.obj['logger'] = logging.getLogger('FFX') ctx.obj['logger'].setLevel(logging.DEBUG) ffxFileHandler = logging.FileHandler(ctx.obj['config'].getLogFilePath()) ffxFileHandler.setLevel(fileLogVerbosity) ffxConsoleHandler = logging.StreamHandler() ffxConsoleHandler.setLevel(consoleLogVerbosity) fileFormatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') ffxFileHandler.setFormatter(fileFormatter) consoleFormatter = logging.Formatter( '%(message)s') ffxConsoleHandler.setFormatter(consoleFormatter) ctx.obj['logger'].addHandler(ffxConsoleHandler) ctx.obj['logger'].addHandler(ffxFileHandler) # Define a subcommand @ffx.command() def version(): click.echo(VERSION) # Another subcommand @ffx.command() def help(): click.echo(f"ffx {VERSION}\n") click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]") @ffx.command() @click.pass_context @click.argument('filename', nargs=1) def inspect(ctx, filename): ctx.obj['command'] = 'inspect' ctx.obj['arguments'] = {} ctx.obj['arguments']['filename'] = filename app = FfxApp(ctx.obj) app.run() #TODO: TrackCodec Klasse CODEC_LOOKUP_TABLE = { 'h264': {'format': 'h264', 'extension': 'h264'}, 'aac': { 'extension': 'aac'}, 'ac3': {'format': 'ac3', 'extension': 'ac3'}, 'ass': {'format': 'ass', 'extension': 'ass'}, 'hdmv_pgs_subtitle': {'format': 'sup', 'extension': 'sup'} } def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix, targetDirectory = ''): trackCodec = trackDescriptor.getCodec() if not trackCodec in CODEC_LOOKUP_TABLE.keys(): return [] commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath] trackType = trackDescriptor.getType() targetPathBase = os.path.join(targetDirectory, targetPrefix) if targetDirectory else targetPrefix commandTokens += ['-map', f"0:{trackType.indicator()}:{trackDescriptor.getSubIndex()}", '-c', 'copy'] if 'format' in CODEC_LOOKUP_TABLE[trackCodec].keys(): commandTokens += ['-f', CODEC_LOOKUP_TABLE[trackCodec]['format']] commandTokens += [f"{targetPathBase}.{CODEC_LOOKUP_TABLE[trackCodec]['extension']}"] return commandTokens @ffx.command() @click.pass_context @click.argument('paths', nargs=-1) @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') @click.option("-o", "--output-directory", type=str, default='') @click.option("-s", "--subtitles-only", is_flag=True, default=False) @click.option('--nice', type=int, default=99, help='Niceness of started processes') @click.option('--cpu', type=int, default=0, help='Limit CPU for started processes to percent') def unmux(ctx, paths, label, output_directory, subtitles_only, nice, cpu): existingSourcePaths = [p for p in paths if os.path.isfile(p)] ctx.obj['logger'].debug(f"\nUnmuxing {len(existingSourcePaths)} files") for sourcePath in existingSourcePaths: fp = FileProperties(ctx.obj, sourcePath) try: sourceMediaDescriptor = fp.getMediaDescriptor() season = fp.getSeason() episode = fp.getEpisode() #TODO: Recognition für alle Formate anpassen targetLabel = label if label else fp.getFileBasename() targetIndicator = f"_S{season}E{episode}" if label and season != -1 and episode != -1 else '' if label and not targetIndicator: ctx.obj['logger'].warning(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized") continue else: ctx.obj['logger'].debug(f"\nUnmuxing file {fp.getFilename()}\n") for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors(): if trackDescriptor.getType() == TrackType.SUBTITLE or not subtitles_only: # SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})' targetPrefix = f"{targetLabel}{targetIndicator}_{trackDescriptor.getIndex()}_{trackDescriptor.getLanguage().threeLetter()}" unmuxSequence = getUnmuxSequence(trackDescriptor, sourcePath, targetPrefix, targetDirectory = output_directory) if unmuxSequence: if not ctx.obj['dry_run']: ctx.obj['logger'].debug(f"Executing unmuxing sequence: {' '.join(unmuxSequence)}") out, err, rc = executeProcess(unmuxSequence, niceness=nice, cpu_percent=cpu) if rc: ctx.obj['logger'].error(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}") else: ctx.obj['logger'].warning(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}") except Exception as ex: ctx.obj['logger'].warning(f"Skipping File {sourcePath} ({ex})") @ffx.command() @click.pass_context def shows(ctx): ctx.obj['command'] = 'shows' app = FfxApp(ctx.obj) app.run() def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): # Check for multiple default or forced dispositions if not set by user input or database requirements # # Query user for the correct sub indices, then configure flags in track descriptors associated with media descriptor accordingly. # The correct tokens should then be created by if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: raise click.ClickException('More than one default video stream detected and no prompt set') defaultVideoTrackSubIndex = click.prompt("More than one default video stream detected! Please select stream", type=int) mediaDescriptor.setDefaultSubTrack(TrackType.VIDEO, defaultVideoTrackSubIndex) if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: raise click.ClickException('More than one forced video stream detected and no prompt set') forcedVideoTrackSubIndex = click.prompt("More than one forced video stream detected! Please select stream", type=int) mediaDescriptor.setForcedSubTrack(TrackType.VIDEO, forcedVideoTrackSubIndex) if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: raise click.ClickException('More than one default audio stream detected and no prompt set') defaultAudioTrackSubIndex = click.prompt("More than one default audio stream detected! Please select stream", type=int) mediaDescriptor.setDefaultSubTrack(TrackType.AUDIO, defaultAudioTrackSubIndex) if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: raise click.ClickException('More than one forced audio stream detected and no prompt set') forcedAudioTrackSubIndex = click.prompt("More than one forced audio stream detected! Please select stream", type=int) mediaDescriptor.setForcedSubTrack(TrackType.AUDIO, forcedAudioTrackSubIndex) if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1: if context['no_prompt']: raise click.ClickException('More than one default subtitle stream detected and no prompt set') defaultSubtitleTrackSubIndex = click.prompt("More than one default subtitle stream detected! Please select stream", type=int) mediaDescriptor.setDefaultSubTrack(TrackType.SUBTITLE, defaultSubtitleTrackSubIndex) if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.FORCED)]) > 1: if context['no_prompt']: raise click.ClickException('More than one forced subtitle stream detected and no prompt set') forcedSubtitleTrackSubIndex = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) mediaDescriptor.setForcedSubTrack(TrackType.SUBTITLE, forcedSubtitleTrackSubIndex) @ffx.command() @click.pass_context @click.argument('paths', nargs=-1) @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') @click.option('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1)", show_default=True) @click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder", show_default=True) @click.option('-p', '--preset', type=str, default=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder", show_default=True) @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", show_default=True) @click.option('--ac3', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams", show_default=True) @click.option('--dts', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams", show_default=True) @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('--language', type=str, multiple=True, help='Set stream language. Use format :<3 letter iso code>') @click.option('--title', type=str, multiple=True, help='Set stream title. Use format :') @click.option('--default-video', type=int, default=-1, help='Index of default video stream') @click.option('--forced-video', type=int, default=-1, help='Index of forced video stream') @click.option('--default-audio', type=int, default=-1, help='Index of default audio stream') @click.option('--forced-audio', type=int, default=-1, help='Index of forced audio stream') @click.option('--default-subtitle', type=int, default=-1, help='Index of default subtitle stream') @click.option('--forced-subtitle', type=int, default=-1, help='Index of forced subtitle stream') @click.option('--rearrange-streams', type=str, default="", help='Rearrange output streams order. Use format comma separated integers') @click.option("--crop", is_flag=False, flag_value="default", default="none") @click.option("--output-directory", type=str, default='') @click.option("--denoise", is_flag=False, flag_value="default", default="none") @click.option("--denoise-use-hw", is_flag=True, default=False) @click.option('--denoise-strength', type=str, default='', help='Denoising strength, more blurring vs more details.') @click.option('--denoise-patch-size', type=str, default='', help='Subimage size to apply filtering on luminosity plane. Reduces broader noise patterns but costly.') @click.option('--denoise-chroma-patch-size', type=str, default='', help='Subimage size to apply filtering on chroma planes.') @click.option('--denoise-research-window', type=str, default='', help='Range to search for comparable patches on luminosity plane. Better filtering but costly.') @click.option('--denoise-chroma-research-window', type=str, default='', help='Range to search for comparable patches on chroma planes.') @click.option('--show', type=int, default=-1, help='Set TMDB show identifier') @click.option('--season', type=int, default=-1, help='Set season of show') @click.option('--episode', type=int, default=-1, help='Set episode of show') @click.option("--no-tmdb", 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) @click.option('--nice', type=int, default=99, help='Niceness of started processes') @click.option('--cpu', type=int, default=0, help='Limit CPU for started processes to percent') def convert(ctx, paths, label, video_encoder, quality, preset, stereo_bitrate, ac3, dts, subtitle_directory, subtitle_prefix, language, title, default_video, forced_video, default_audio, forced_audio, default_subtitle, forced_subtitle, rearrange_streams, crop, output_directory, denoise, denoise_use_hw, denoise_strength, denoise_patch_size, denoise_chroma_patch_size, denoise_research_window, denoise_chroma_research_window, show, season, episode, no_tmdb, # no_jellyfin, no_pattern, dont_pass_dispositions, no_prompt, no_signature, keep_mkvmerge_metadata, nice, cpu): """Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin Files found under PATHS will be converted according to parameters. Filename extensions will be changed appropriately. Suffices will we appended to filename in case of multiple created files or if the filename has not changed.""" startTime = time.perf_counter() context = ctx.obj context['video_encoder'] = VideoEncoder.fromLabel(video_encoder) targetFormat = FfxController.DEFAULT_FILE_FORMAT targetExtension = FfxController.DEFAULT_FILE_EXTENSION #TODO: #407 Without effect -> remove context['use_jellyfin'] = False 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['resource_limits'] = {} context['resource_limits']['niceness'] = nice context['resource_limits']['cpu_percent'] = cpu context['denoiser'] = NlmeansController(parameters = denoise, strength = denoise_strength, patchSize = denoise_patch_size, chromaPatchSize = denoise_chroma_patch_size, researchWindow = denoise_research_window, chromaResearchWindow = denoise_chroma_research_window, useHardware = denoise_use_hw) context['import_subtitles'] = (subtitle_directory and subtitle_prefix) if context['import_subtitles']: context['subtitle_directory'] = subtitle_directory context['subtitle_prefix'] = subtitle_prefix existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS] # CLI Overrides cliOverrides = {} if language: cliOverrides['languages'] = {} for overLang in language: olTokens = overLang.split(':') if len(olTokens) == 2: try: cliOverrides['languages'][int(olTokens[0])] = olTokens[1] except ValueError: ctx.obj['logger'].warning(f"Ignoring non-integer language index {olTokens[0]}") continue if title: cliOverrides['titles'] = {} for overTitle in title: otTokens = overTitle.split(':') if len(otTokens) == 2: try: cliOverrides['titles'][int(otTokens[0])] = otTokens[1] except ValueError: ctx.obj['logger'].warning(f"Ignoring non-integer title index {otTokens[0]}") continue if default_video != -1: cliOverrides['default_video'] = default_video if forced_video != -1: cliOverrides['forced_video'] = forced_video if default_audio != -1: cliOverrides['default_audio'] = default_audio if forced_audio != -1: cliOverrides['forced_audio'] = forced_audio if default_subtitle != -1: cliOverrides['default_subtitle'] = default_subtitle if forced_subtitle != -1: cliOverrides['forced_subtitle'] = forced_subtitle if show != -1 or season != -1 or episode != -1: if len(existingSourcePaths) > 1: context['logger'].warning(f"Ignoring TMDB show, season, episode overrides, not supported for multiple source files") else: cliOverrides['tmdb'] = {} if show != -1: cliOverrides['tmdb']['show'] = show if season != -1: cliOverrides['tmdb']['season'] = season if episode != -1: cliOverrides['tmdb']['episode'] = episode if cliOverrides: context['overrides'] = cliOverrides if rearrange_streams: try: cliOverrides['stream_order'] = [int(si) for si in rearrange_streams.split(",")] except ValueError as ve: errorMessage = "Non-integer in rearrange stream parameter" ctx.obj['logger'].error(errorMessage) raise click.Abort() ctx.obj['logger'].debug(f"\nVideo encoder: {video_encoder}") qualityTokens = quality.split(',') q_list = [q for q in qualityTokens if q.isnumeric()] 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" 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 context['perform_crop'] = (crop != 'none') if context['perform_crop']: cTokens = crop.split(',') if cTokens and len(cTokens) == 2: context['crop_start'] = int(cTokens[0]) context['crop_length'] = int(cTokens[1]) ctx.obj['logger'].debug(f"Crop start={context['crop_start']} length={context['crop_length']}") tc = TmdbController() if context['use_tmdb'] else None ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs") jobIndex = 0 for sourcePath in existingSourcePaths: # Separate basedir, basename and extension for current source file sourceDirectory = os.path.dirname(sourcePath) sourceFilename = os.path.basename(sourcePath) sourcePathTokens = sourceFilename.split('.') sourceFileBasename = '.'.join(sourcePathTokens[:-1]) sourceFilenameExtension = sourcePathTokens[-1] ctx.obj['logger'].info(f"\nProcessing file {sourcePath}") targetSuffices = {} mediaFileProperties = FileProperties(context, sourceFilename) #HINT: -1 if not set if 'tmdb' in cliOverrides.keys() and 'season' in cliOverrides['tmdb']: showSeason = cliOverrides['tmdb']['season'] else: showSeason = mediaFileProperties.getSeason() if 'tmdb' in cliOverrides.keys() and 'episode' in cliOverrides['tmdb']: showEpisode = cliOverrides['tmdb']['episode'] else: showEpisode = mediaFileProperties.getEpisode() ctx.obj['logger'].debug(f"Season={showSeason} Episode={showEpisode}") sourceMediaDescriptor = mediaFileProperties.getMediaDescriptor() #HINT: This is None if the filename did not match anything in database currentPattern = mediaFileProperties.getPattern() if context['use_pattern'] else None ctx.obj['logger'].debug(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}") # Setup FfxController accordingly depending on pattern matching is enabled and a pattern was matched if currentPattern is None: checkUniqueDispositions(context, sourceMediaDescriptor) currentShowDescriptor = None if context['import_subtitles']: sourceMediaDescriptor.importSubtitles(context['subtitle_directory'], context['subtitle_prefix'], showSeason, showEpisode) if cliOverrides: sourceMediaDescriptor.applyOverrides(cliOverrides) fc = FfxController(context, sourceMediaDescriptor) else: targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj) checkUniqueDispositions(context, targetMediaDescriptor) currentShowDescriptor = currentPattern.getShowDescriptor(ctx.obj) if context['import_subtitles']: targetMediaDescriptor.importSubtitles(context['subtitle_directory'], context['subtitle_prefix'], showSeason, showEpisode) 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 cliOverrides: targetMediaDescriptor.applyOverrides(cliOverrides) 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()]}") ctx.obj['logger'].debug(f"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}") fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor) indexSeasonDigits = currentShowDescriptor.getIndexSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS indexEpisodeDigits = currentShowDescriptor.getIndexEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS indicatorSeasonDigits = currentShowDescriptor.getIndicatorSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS indicatorEpisodeDigits = currentShowDescriptor.getIndicatorEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS # Assemble target filename accordingly depending on TMDB lookup is enabled #HINT: -1 if not set showId = cliOverrides['tmdb']['show'] if 'tmdb' in cliOverrides.keys() and 'show' in cliOverrides['tmdb'] else (-1 if currentShowDescriptor is None else currentShowDescriptor.getId()) if context['use_tmdb'] and showId != -1 and showSeason != -1 and showEpisode != -1: ctx.obj['logger'].debug(f"Querying TMDB for show_id={showId} season={showSeason} episode{showEpisode}") if currentPattern is None: sName, showYear = tc.getShowNameAndYear(showId) showName = filterFilename(sName) showFilenamePrefix = f"{showName} ({str(showYear)})" else: showFilenamePrefix = currentShowDescriptor.getFilenamePrefix() tmdbEpisodeResult = tc.queryEpisode(showId, showSeason, showEpisode) ctx.obj['logger'].debug(f"tmdbEpisodeResult={tmdbEpisodeResult}") if tmdbEpisodeResult: filteredEpisodeName = filterFilename(tmdbEpisodeResult['name']) sourceFileBasename = TmdbController.getEpisodeFileBasename(showFilenamePrefix, filteredEpisodeName, showSeason, showEpisode, indexSeasonDigits, indexEpisodeDigits, indicatorSeasonDigits, indicatorEpisodeDigits) if label: if showSeason > -1 and showEpisode > -1: targetSuffices['se'] = f"S{showSeason:0{indicatorSeasonDigits}d}E{showEpisode:0{indicatorEpisodeDigits}d}" elif showEpisode > -1: targetSuffices['se'] = f"E{showEpisode:0{indicatorEpisodeDigits}d}" else: if 'se' in targetSuffices.keys(): del targetSuffices['se'] ctx.obj['logger'].debug(f"fileBasename={sourceFileBasename}") for q in q_list: if len(q_list) > 1: targetSuffices['q'] = f"q{q}" ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} q={q}") jobIndex += 1 ctx.obj['logger'].debug(f"label={label if label else 'Falsy'}") ctx.obj['logger'].debug(f"sourceFileBasename={sourceFileBasename}") # targetFileBasename = mediaFileProperties.assembleTargetFileBasename(label, # q if len(q_list) > 1 else -1, # targetFileBasename = sourceFileBasename if context['use_tmdb'] and not label else label targetFilenameTokens = [targetFileBasename] if 'se' in targetSuffices.keys(): targetFilenameTokens += [targetSuffices['se']] if 'q' in targetSuffices.keys(): targetFilenameTokens += [targetSuffices['q']] #TODO #387 # targetFilename = ((f"{sourceFileBasename}_q{q}" if len(q_list) > 1 else sourceFileBasename) # if context['use_tmdb'] else targetFileBasename) targetFilename = f"{'_'.join(targetFilenameTokens)}.{targetExtension}" targetPath = os.path.join(output_directory if output_directory else sourceDirectory, targetFilename) #TODO: target extension anpassen ctx.obj['logger'].info(f"Creating file {targetFilename}") fc.runJob(sourcePath, targetPath, targetFormat, context['video_encoder'], q, preset) #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() ctx.obj['logger'].info(f"\nDONE\nTime elapsed {endTime - startTime}") if __name__ == '__main__': ffx()