diff --git a/bin/ffx.py b/bin/ffx.py index 8efed55..1ee131f 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -2,6 +2,8 @@ import os, sys, subprocess, json, click, time, re +from ffx.file_properties import FileProperties + from ffx.ffx_app import FfxApp from ffx.ffx_controller import FfxController from ffx.database import databaseContext @@ -20,10 +22,6 @@ SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z] SUBTITLE_FILE_EXTENSION = 'vtt' -FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm'] - - - def getModifiedStreamOrder(length, last): """This is jellyfin specific as the last stream in the order is set as default""" seq = list(range(length)) @@ -152,21 +150,20 @@ def inspect(ctx, filename): def shows(ctx): -# if 'database' not in ctx.obj.keys(): -# ctx.obj['database'] = databaseContext() - ctx.obj['command'] = 'shows' app = FfxApp(ctx.obj) app.run() - - @ffx.command() @click.pass_context @click.argument('paths', nargs=-1) + +@click.option("-t", "--tmdb", is_flag=True, default=False) +@click.option("-j", "--jellyfin", is_flag=True, default=False) + @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) default: {FfxController.DEFAULT_VIDEO_ENCODER}") @@ -202,13 +199,15 @@ def shows(ctx): @click.option("-c", "--clear-metadata", is_flag=True, default=False) @click.option("-d", "--denoise", is_flag=True, default=False) -@click.option("-j", "--jellyfin", is_flag=True, default=False) + @click.option("--dry-run", is_flag=True, default=False) def convert(ctx, paths, + tmdb, + jellyfin, label, video_encoder, quality, @@ -230,7 +229,6 @@ def convert(ctx, output_directory, clear_metadata, denoise, - jellyfin, dry_run): """Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin @@ -240,548 +238,550 @@ def convert(ctx, or if the filename has not changed.""" startTime = time.perf_counter() - context = ctx.obj - if 'database' not in context.keys(): - context['database'] = databaseContext() - - - click.echo(f"\nVideo encoder: {video_encoder}") - +# click.echo(f"\nVideo encoder: {video_encoder}") +# qualityTokens = quality.split(',') q_list = [q for q in qualityTokens if q.isnumeric()] - click.echo(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_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k" - context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k" - - click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") - click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") - click.echo(f"DTS bitrate: {context['bitrates']['dts']}") - - - se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH) - e_match = re.compile(EPISODE_INDICATOR_MATCH) - - - ## Conversion parameters - - # Parse subtitle files - context['import_subtitles'] = (subtitle_directory and subtitle_prefix) - availableFileSubtitleDescriptors = searchSubtitleFiles(subtitle_directory, subtitle_prefix) if context['import_subtitles'] else [] - - - # Overwrite audio tags if set - audioLanguages = audio_language - audioTitles = audio_title - - # Overwrite subtitle tags if set - subtitleLanguages = subtitle_language - subtitleTitles = subtitle_title - - defaultAudio = default_audio - defaultSubtitle = default_subtitle - forcedAudio = forced_audio - forcedSubtitle = forced_subtitle - - - # Process crop parameters - context['perform_crop'] = (crop != 'none') - if context['perform_crop']: - cTokens = crop.split(',') - if cTokens and len(cTokens) == 2: - cropStart, cropLength = crop.split(',') - else: - cropStart = FfxController.DEFAULT_CROP_START - cropLength = FfxController.DEFAULT_CROP_LENGTH - - click.echo(f"crop start={cropStart} length={cropLength}") - - cropTokens = generateCropTokens(int(cropStart), int(cropLength)) - else: - cropTokens = [] - - - job_index = 0 - - existingSourcePaths = [p for p in paths if os.path.isfile(p)] +# click.echo(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_bitrate) if str(ac3_bitrate).endswith('k') else f"{ac3_bitrate}k" +# context['bitrates']['dts'] = str(dts_bitrate) if str(dts_bitrate).endswith('k') else f"{dts_bitrate}k" +# +# click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") +# click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") +# click.echo(f"DTS bitrate: {context['bitrates']['dts']}") +# +# +# se_match = re.compile(SEASON_EPISODE_INDICATOR_MATCH) +# e_match = re.compile(EPISODE_INDICATOR_MATCH) +# +# +# ## Conversion parameters +# +# # Parse subtitle files +# context['import_subtitles'] = (subtitle_directory and subtitle_prefix) +# availableFileSubtitleDescriptors = searchSubtitleFiles(subtitle_directory, subtitle_prefix) if context['import_subtitles'] else [] +# +# +# # Overwrite audio tags if set +# audioLanguages = audio_language +# audioTitles = audio_title +# +# # Overwrite subtitle tags if set +# subtitleLanguages = subtitle_language +# subtitleTitles = subtitle_title +# +# defaultAudio = default_audio +# defaultSubtitle = default_subtitle +# forcedAudio = forced_audio +# forcedSubtitle = forced_subtitle +# +# +# # Process crop parameters +# context['perform_crop'] = (crop != 'none') +# if context['perform_crop']: +# cTokens = crop.split(',') +# if cTokens and len(cTokens) == 2: +# cropStart, cropLength = crop.split(',') +# else: +# cropStart = FfxController.DEFAULT_CROP_START +# cropLength = FfxController.DEFAULT_CROP_LENGTH +# +# click.echo(f"crop start={cropStart} length={cropLength}") +# +# cropTokens = generateCropTokens(int(cropStart), int(cropLength)) +# else: +# cropTokens = [] +# +# +# job_index = 0 +# + existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS] click.echo(f"\nRunning {len(existingSourcePaths) * len(q_list)} jobs") - + 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('.') - if sourcePathTokens[-1] in FILE_EXTENSIONS: - sourceFileBasename = '.'.join(sourcePathTokens[:-1]) - sourceFilenameExtension = sourcePathTokens[-1] - else: - sourceFileBasename = sourceFilename - sourceFilenameExtension = '' - - click.echo(f"\nProcessing file {sourcePath}") - - - # Determine season and episode if present in current filename - season_digits = 2 - episode_digits = 2 - index_digits = 3 - - se_result = se_match.search(sourceFilename) - e_result = e_match.search(sourceFilename) - - season = -1 - episode = -1 - file_index = 0 - - if se_result is not None: - season = int(se_result.group(1)) - episode = int(se_result.group(2)) - elif e_result is not None: - episode = int(e_result.group(1)) - else: - file_index += 1 - - matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else [] - - print(f"season={season} episode={episode} file={file_index}") - - - # Assemble target filename tokens - targetFilenameTokens = [] - targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION - - if label: - targetFilenameTokens = [label] - - if season > -1 and episode > -1: - targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"] - elif episode > -1: - targetFilenameTokens += [f"E{episode:0{episode_digits}d}"] - else: - targetFilenameTokens += [f"{file_index:0{index_digits}d}"] - - else: - targetFilenameTokens = [sourceFileBasename] - - ### - ### + sourceFileBasename = '.'.join(sourcePathTokens[:-1]) + sourceFilenameExtension = sourcePathTokens[-1] - # Load source stream descriptor - try: - ### - sourceStreamDescriptor = getStreamDescriptor(sourcePath) - ### - except Exception: - click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...") - continue - - - ## ## ## - targetStreamDescriptor = sourceStreamDescriptor.copy() - ## ## ## - - - click.echo('\nSource streams:') - for aStream in sourceStreamDescriptor[STREAM_TYPE_AUDIO]: - click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}") - for sStream in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]: - click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}") - - - # Check for multiple default or forced dispositions if not set by user input or database requirements - #NOTE: It is currently expected that all source file have the same substream pattern, e.g. coming from the same encoder - numDefaultAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1]) - if defaultAudio == -1 and numDefaultAudioStreams > 1: - defaultAudio = click.prompt("More than one default audio stream detected! Please select stream", type=int) - - numForcedAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['forced'] == 1]) - if forcedAudio == -1 and numForcedAudioStreams > 1: - forcedAudio = click.prompt("More than one forced audio stream detected! Please select stream", type=int) - - numDefaultSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['default'] == 1]) - if defaultSubtitle == -1 and numDefaultSubtitleStreams > 1: - defaultSubtitle = click.prompt("More than one default subtitle stream detected! Please select stream", type=int) - - numForcedSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['forced'] == 1]) - if forcedSubtitle == -1 and numForcedSubtitleStreams > 1: - forcedSubtitle = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) - - #Define default/forced tags - if defaultAudio != -1: - for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): - targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultAudio else 0 - if forcedAudio != -1: - for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): - targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedAudio else 0 - if defaultSubtitle != -1: - for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): - targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultSubtitle else 0 - if forcedSubtitle != -1: - for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): - targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedSubtitle else 0 - - - # Set language and title in source stream descriptors if given per command line option - for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): - if streamIndex <= len(audioLanguages) - 1: - targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['language'] = audioLanguages[streamIndex] - if streamIndex <= len(audioTitles) - 1: - targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['title'] = audioTitles[streamIndex] - - for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): - if streamIndex <= len(subtitleLanguages) - 1: - targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['language'] = subtitleLanguages[streamIndex] - if streamIndex <= len(subtitleTitles) - 1: - targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['title'] = subtitleTitles[streamIndex] - - - click.echo('\nTarget streams:') - for aStream in targetStreamDescriptor[STREAM_TYPE_AUDIO]: - click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}") - for sStream in targetStreamDescriptor[STREAM_TYPE_SUBTITLE]: - click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}") - - - numSourceAudioSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_AUDIO]) - numSourceSubtitleSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]) - - # Stream order is just a list of integer - audioStreamSourceOrder = list(range(numSourceAudioSubStreams)) - subtitleStreamSourceOrder = list(range(numSourceSubtitleSubStreams)) - - - # In order for the jellyfin media web UI to work properly the default/forced stream has to be the last in the sequence - if jellyfin: - - defaultTargetAudioStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1] - if defaultTargetAudioStreams: - audioStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_AUDIO]), defaultTargetAudioStreams[0]['sub_index']) - - defaultTargetSubtitleStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_SUBTITLE] if a['disposition']['default'] == 1] - if defaultTargetSubtitleStreams: - subtitleStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]), defaultTargetSubtitleStreams[0]['sub_index']) - - -# audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO]) -# subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE]) - - audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO], modifyOrder = audioStreamSourceOrder) - subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE], modifyOrder = subtitleStreamSourceOrder) - - - - mappingVideoTokens = ['-map', '0:v:0'] - mappingTokens = mappingVideoTokens.copy() - - dispositionTokens = [] - - audioEncodingTokens = [] - - - audioMetadataTokens = [] - for audioStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): - - # Modify selected source audio stream for jellyfin if required - sourceAudioStreamIndex = audioStreamSourceOrder[audioStreamIndex] - - # Add audio mapping tokens to list of general mapping tokens - mappingTokens += ['-map', f"0:a:{sourceAudioStreamIndex}"] - - - targetAudioStream = targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex] - - # audioEncodingTokens += generateAudioEncodingTokens(context, sourceAudioStream['src_sub_index'], sourceAudioStream['layout']) - audioEncodingTokens += generateAudioEncodingTokens(context, audioStreamIndex, targetAudioStream['audio_layout']) - - if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['language']: - audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language']}"] - - if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['title']: - audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title']}"] - - # targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 - - - subtitleImportFileTokens = [] - subtitleMetadataTokens = [] - - if context['import_subtitles'] and numSourceSubtitleSubStreams != len(matchingFileSubtitleDescriptors): - click.echo(f"The number of subtitle streams found in file with path {sourcePath} is different from the number of subtitle streams provided by matching imported files, skipping ...") - continue - - # 0: Quelle f1 = forced - # 1: QUelle f2 = full - - for subtitleStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): - - # Modify selected source subtitle stream for jellyfin if required - sourceSubtitleStreamIndex = subtitleStreamSourceOrder[subtitleStreamIndex] - - - if context['import_subtitles']: - - fileSubtitleDescriptor = matchingFileSubtitleDescriptors[subtitleStreamIndex] # original order - - subtitleImportFileTokens += ['-i', fileSubtitleDescriptor['path']] # original order - - # Create mapping for subtitle streams when imported from files - mappingTokens += ['-map', f"{sourceSubtitleStreamIndex+1}:s:0"] # modified order - - - if fileSubtitleDescriptor['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']: - subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"] - - subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"] - - else: - - # Add subtitle mapping tokens to list of general mapping tokens - mappingTokens += ['-map', f"0:s:{sourceSubtitleStreamIndex}"] - - if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']: - subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"] + click.echo(f"\nProcessing file {sourcePath}") - if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']: - subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"] + mediaFileProperties = FileProperties(context, sourceFilename) + currentMediaDescriptor = mediaFileProperties.getMediaDescriptor() + #HINT: This is None if the filename did not match anything in database + currentPattern = mediaFileProperties.getPattern() + targetMediaDescriptor = currentPattern.getMediaDescriptor() if currentPattern is not None else None + click.echo(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}") + if not currentPattern is None: + click.echo(f"Input mapping tokens: {targetMediaDescriptor.getInputMappingTokens()}") -# # Reorder audio stream descriptors and create disposition options if default is given per command line option -# if defaultAudio == -1: -# sourceAudioStreams = audioStreams -# else: -# for streamIndex in range(len(audioStreams)): -# audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 +# # Determine season and episode if present in current filename +# season_digits = 2 +# episode_digits = 2 +# index_digits = 3 # -# sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams -# -# dispositionTokens += generateDispositionTokens(sourceAudioStreams) -# -# # Set forced tag in subtitle descriptor if given per command line option -# if forcedSubtitle != -1: -# for streamIndex in range(len(subtitleStreams)): -# subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0 -# -# # Reorder subtitle stream descriptors and create disposition options if default is given per command line option -# if defaultSubtitle == -1: -# sourceSubtitleStreams = subtitleStreams -# else: -# for streamIndex in range(len(subtitleStreams)): -# subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0 -# -# sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams -# -# dispositionTokens += generateDispositionTokens(sourceSubtitleStreams) -# - - - - - - click.echo(f"Audio stream source order {audioStreamSourceOrder}") - click.echo(f"Subtitle stream source order {subtitleStreamSourceOrder}") - - - commandTokens = COMMAND_TOKENS + ['-i', sourcePath] - - -# matchingSubtitles = [] -# if context['import_subtitles']: +# se_result = se_match.search(sourceFilename) +# e_result = e_match.search(sourceFilename) # - - +# season = -1 +# episode = -1 +# file_index = 0 # -# for streamIndex in range(len(mSubtitles)): -# mSubtitles[streamIndex]['forced'] = 1 if forcedSubtitle != -1 and streamIndex == forcedSubtitle else 0 -# mSubtitles[streamIndex]['default'] = 1 if defaultSubtitle != -1 and streamIndex == defaultSubtitle else 0 +# if se_result is not None: +# season = int(se_result.group(1)) +# episode = int(se_result.group(2)) +# elif e_result is not None: +# episode = int(e_result.group(1)) +# else: +# file_index += 1 # -# if streamIndex <= len(subtitleTitles) -1: -# mSubtitles[streamIndex]['title'] = subtitleTitles[streamIndex] +# matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else [] +# +# print(f"season={season} episode={episode} file={file_index}") +# +# +# # Assemble target filename tokens +# targetFilenameTokens = [] +# targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION +# +# if label: +# targetFilenameTokens = [label] +# +# if season > -1 and episode > -1: +# targetFilenameTokens += [f"S{season:0{season_digits}d}E{episode:0{episode_digits}d}"] +# elif episode > -1: +# targetFilenameTokens += [f"E{episode:0{episode_digits}d}"] +# else: +# targetFilenameTokens += [f"{file_index:0{index_digits}d}"] +# +# else: +# targetFilenameTokens = [sourceFileBasename] +# +# ### +# ### +# +# # Load source stream descriptor +# try: +# ### +# sourceStreamDescriptor = getStreamDescriptor(sourcePath) +# ### +# +# except Exception: +# click.echo(f"File with path {sourcePath} does not contain any audiovisual data, skipping ...") +# continue +# +# +# ## ## ## +# targetStreamDescriptor = sourceStreamDescriptor.copy() +# ## ## ## +# +# +# click.echo('\nSource streams:') +# for aStream in sourceStreamDescriptor[STREAM_TYPE_AUDIO]: +# click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}") +# for sStream in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]: +# click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}") +# +# +# # Check for multiple default or forced dispositions if not set by user input or database requirements +# #NOTE: It is currently expected that all source file have the same substream pattern, e.g. coming from the same encoder +# numDefaultAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1]) +# if defaultAudio == -1 and numDefaultAudioStreams > 1: +# defaultAudio = click.prompt("More than one default audio stream detected! Please select stream", type=int) +# +# numForcedAudioStreams = len([a for a in sourceStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['forced'] == 1]) +# if forcedAudio == -1 and numForcedAudioStreams > 1: +# forcedAudio = click.prompt("More than one forced audio stream detected! Please select stream", type=int) +# +# numDefaultSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['default'] == 1]) +# if defaultSubtitle == -1 and numDefaultSubtitleStreams > 1: +# defaultSubtitle = click.prompt("More than one default subtitle stream detected! Please select stream", type=int) +# +# numForcedSubtitleStreams = len([s for s in sourceStreamDescriptor[STREAM_TYPE_SUBTITLE] if s['disposition']['forced'] == 1]) +# if forcedSubtitle == -1 and numForcedSubtitleStreams > 1: +# forcedSubtitle = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) +# +# #Define default/forced tags +# if defaultAudio != -1: +# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): +# targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultAudio else 0 +# if forcedAudio != -1: +# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): +# targetStreamDescriptor[STREAM_TYPE_AUDIO][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedAudio else 0 +# if defaultSubtitle != -1: +# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): +# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['default'] = 1 if substreamIndex == defaultSubtitle else 0 +# if forcedSubtitle != -1: +# for substreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): +# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][substreamIndex]['disposition']['forced'] = 1 if substreamIndex == forcedSubtitle else 0 +# +# +# # Set language and title in source stream descriptors if given per command line option +# for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): +# if streamIndex <= len(audioLanguages) - 1: +# targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['language'] = audioLanguages[streamIndex] +# if streamIndex <= len(audioTitles) - 1: +# targetStreamDescriptor[STREAM_TYPE_AUDIO][streamIndex]['tags']['title'] = audioTitles[streamIndex] +# +# for streamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): +# if streamIndex <= len(subtitleLanguages) - 1: +# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['language'] = subtitleLanguages[streamIndex] +# if streamIndex <= len(subtitleTitles) - 1: +# targetStreamDescriptor[STREAM_TYPE_SUBTITLE][streamIndex]['tags']['title'] = subtitleTitles[streamIndex] +# +# +# click.echo('\nTarget streams:') +# for aStream in targetStreamDescriptor[STREAM_TYPE_AUDIO]: +# click.echo(f"audio stream {aStream['sub_index']} lang={aStream['tags']['language']} title={aStream['tags']['title']} default={aStream['disposition']['default']} forced={aStream['disposition']['forced']}") +# for sStream in targetStreamDescriptor[STREAM_TYPE_SUBTITLE]: +# click.echo(f"subtitle stream {sStream['sub_index']} lang={sStream['tags']['language']} title={sStream['tags']['title']} default={sStream['disposition']['default']} forced={sStream['disposition']['forced']}") +# +# +# numSourceAudioSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_AUDIO]) +# numSourceSubtitleSubStreams = len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]) +# +# # Stream order is just a list of integer +# audioStreamSourceOrder = list(range(numSourceAudioSubStreams)) +# subtitleStreamSourceOrder = list(range(numSourceSubtitleSubStreams)) +# +# +# # In order for the jellyfin media web UI to work properly the default/forced stream has to be the last in the sequence +# if jellyfin: +# +# defaultTargetAudioStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_AUDIO] if a['disposition']['default'] == 1] +# if defaultTargetAudioStreams: +# audioStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_AUDIO]), defaultTargetAudioStreams[0]['sub_index']) +# +# defaultTargetSubtitleStreams = [a for a in targetStreamDescriptor[STREAM_TYPE_SUBTITLE] if a['disposition']['default'] == 1] +# if defaultTargetSubtitleStreams: +# subtitleStreamSourceOrder = getModifiedStreamOrder(len(sourceStreamDescriptor[STREAM_TYPE_SUBTITLE]), defaultTargetSubtitleStreams[0]['sub_index']) +# +# +# # audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO]) +# # subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE]) +# +# audioDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_AUDIO], modifyOrder = audioStreamSourceOrder) +# subtitleDispositionTokens = generateDispositionTokens(targetStreamDescriptor[STREAM_TYPE_SUBTITLE], modifyOrder = subtitleStreamSourceOrder) +# +# +# +# mappingVideoTokens = ['-map', '0:v:0'] +# mappingTokens = mappingVideoTokens.copy() +# +# dispositionTokens = [] +# +# audioEncodingTokens = [] +# +# +# audioMetadataTokens = [] +# for audioStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_AUDIO])): +# +# # Modify selected source audio stream for jellyfin if required +# sourceAudioStreamIndex = audioStreamSourceOrder[audioStreamIndex] +# +# # Add audio mapping tokens to list of general mapping tokens +# mappingTokens += ['-map', f"0:a:{sourceAudioStreamIndex}"] +# +# +# targetAudioStream = targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex] +# +# # audioEncodingTokens += generateAudioEncodingTokens(context, sourceAudioStream['src_sub_index'], sourceAudioStream['layout']) +# audioEncodingTokens += generateAudioEncodingTokens(context, audioStreamIndex, targetAudioStream['audio_layout']) +# +# if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['language']: +# audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['language']}"] +# +# if sourceStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['tags']['title']: +# audioMetadataTokens += [f"-metadata:s:a:{audioStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_AUDIO][sourceAudioStreamIndex]['tags']['title']}"] +# +# # targetStreamDescriptor[STREAM_TYPE_AUDIO][audioStreamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 +# +# +# subtitleImportFileTokens = [] +# subtitleMetadataTokens = [] +# +# if context['import_subtitles'] and numSourceSubtitleSubStreams != len(matchingFileSubtitleDescriptors): +# click.echo(f"The number of subtitle streams found in file with path {sourcePath} is different from the number of subtitle streams provided by matching imported files, skipping ...") +# continue +# +# # 0: Quelle f1 = forced +# # 1: QUelle f2 = full +# +# for subtitleStreamIndex in range(len(targetStreamDescriptor[STREAM_TYPE_SUBTITLE])): +# +# # Modify selected source subtitle stream for jellyfin if required +# sourceSubtitleStreamIndex = subtitleStreamSourceOrder[subtitleStreamIndex] # -# if defaultSubtitle != -1 and jellyfin: -# matchingSubtitles = getReorderedSubstreams(mSubtitles, defaultSubtitle) -# else: -# matchingSubtitles = mSubtitles - - - - for q in q_list: - - click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}") - job_index += 1 - - -# # Reorder audio stream descriptors and create disposition options if default is given per command line option -# if defaultAudio == -1: -# sourceAudioStreams = audioStreams -# else: -# for streamIndex in range(len(audioStreams)): -# audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 # -# sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams -# -# dispositionTokens += generateDispositionTokens(sourceAudioStreams) -# -# # Set forced tag in subtitle descriptor if given per command line option -# if forcedSubtitle != -1: -# for streamIndex in range(len(subtitleStreams)): -# subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0 -# -# # Reorder subtitle stream descriptors and create disposition options if default is given per command line option -# if defaultSubtitle == -1: -# sourceSubtitleStreams = subtitleStreams -# else: -# for streamIndex in range(len(subtitleStreams)): -# subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0 -# -# sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams -# -# dispositionTokens += generateDispositionTokens(sourceSubtitleStreams) -# - - -# # Create mapping and ffmpeg options for subtitle streams - # if context['import_subtitles']: # -# numMatchingSubtitles = len(matchingSubtitles) +# fileSubtitleDescriptor = matchingFileSubtitleDescriptors[subtitleStreamIndex] # original order # -# if jellyfin and defaultSubtitle != -1: -# subtitleSequence = getModifiedStreamOrder(numMatchingSubtitles, default_subtitle) #! -# else: -# subtitleSequence = range(numMatchingSubtitles) +# subtitleImportFileTokens += ['-i', fileSubtitleDescriptor['path']] # original order +# +# # Create mapping for subtitle streams when imported from files +# mappingTokens += ['-map', f"{sourceSubtitleStreamIndex+1}:s:0"] # modified order # -# for fileIndex in range(numMatchingSubtitles): # -# # Create mapping for subtitle streams when imported from files -# mappingTokens += ['-map', f"{subtitleSequence[fileIndex]+1}:s:0"] +# if fileSubtitleDescriptor['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']: +# subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"] # -# msg = matchingSubtitles[fileIndex] -# subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"language={msg['language']}"] -# if 'title' in matchingSubtitles[fileIndex].keys(): -# subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"title={matchingSubtitles[fileIndex]['title']}"] +# subtitleMetadataTokens += [f"-metadata:s:s:{sourceSubtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"] # # else: # -# for subtitleStreamIndex in range(len(sourceSubtitleStreams)): +# # Add subtitle mapping tokens to list of general mapping tokens +# mappingTokens += ['-map', f"0:s:{sourceSubtitleStreamIndex}"] # -# subtitleStream = sourceSubtitleStreams[subtitleStreamIndex] +# if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['language'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']: +# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['language']}"] # -# # Create mapping for subtitle streams -# mappingTokens += ['-map', f"s:{subtitleStream['src_sub_index']}"] +# if sourceStreamDescriptor[STREAM_TYPE_SUBTITLE][sourceSubtitleStreamIndex]['tags']['title'] != targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']: +# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={targetStreamDescriptor[STREAM_TYPE_SUBTITLE][subtitleStreamIndex]['tags']['title']}"] # -# if 'tags' in subtitleStream.keys(): -# if 'language' in subtitleStream['tags'].keys(): -# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={subtitleStream['tags']['language']}"] -# if 'title' in subtitleStream['tags'].keys(): -# subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={subtitleStream['tags']['title']}"] - - - # Job specific tokens - targetFilenameJobTokens = targetFilenameTokens.copy() - - if len(q_list) > 1: - targetFilenameJobTokens += [f"q{q}"] - - # In case source and target filenames are the same add an extension to distinct output from input - if not label and sourceFilenameExtension == targetFilenameExtension: - targetFilenameJobTokens += ['ffx'] - - targetFilename = '_'.join(targetFilenameJobTokens) # + '.' + targetFilenameExtension - - click.echo(f"target filename: {targetFilename}") - - - if video_encoder == 'av1': - - commandSequence = (commandTokens - + subtitleImportFileTokens - + mappingTokens - + audioMetadataTokens - + subtitleMetadataTokens - + audioDispositionTokens - + subtitleDispositionTokens - + audioEncodingTokens - + generateAV1Tokens(q, preset) + audioEncodingTokens) - - if clear_metadata: - commandSequence += generateClearTokens(sourceStreamDescriptor) - - commandSequence += cropTokens - - commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION) - - click.echo(f"Command: {' '.join(commandSequence)}") - - if not dry_run: - executeProcess(commandSequence) - - - if video_encoder == 'vp9': - - commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q) - - commandSequence1 += cropTokens - - commandSequence1 += NULL_TOKENS - - click.echo(f"Command 1: {' '.join(commandSequence1)}") - - if os.path.exists(TEMP_FILE_NAME): - os.remove(TEMP_FILE_NAME) - - if not dry_run: - executeProcess(commandSequence1) - - - commandSequence2 = (commandTokens - + subtitleImportFileTokens - + mappingTokens - + audioMetadataTokens - + subtitleMetadataTokens - + audioDispositionTokens - + subtitleDispositionTokens - + dispositionTokens) - - if denoise: - commandSequence2 += generateDenoiseTokens() - - commandSequence2 += generateVP9Pass2Tokens(q) + audioEncodingTokens - - if clear_metadata: - commandSequence2 += generateClearTokens(sourceStreamDescriptor) - - commandSequence2 += cropTokens - - commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION) - - click.echo(f"Command 2: {' '.join(commandSequence2)}") - - if not dry_run: - executeProcess(commandSequence2) - - - #app = ModesApp(context) - #app.run() - - #click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True) - - click.echo('\nDONE\n') +# +# +# +# +# +# # # Reorder audio stream descriptors and create disposition options if default is given per command line option +# # if defaultAudio == -1: +# # sourceAudioStreams = audioStreams +# # else: +# # for streamIndex in range(len(audioStreams)): +# # audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 +# # +# # sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams +# # +# # dispositionTokens += generateDispositionTokens(sourceAudioStreams) +# # +# # # Set forced tag in subtitle descriptor if given per command line option +# # if forcedSubtitle != -1: +# # for streamIndex in range(len(subtitleStreams)): +# # subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0 +# # +# # # Reorder subtitle stream descriptors and create disposition options if default is given per command line option +# # if defaultSubtitle == -1: +# # sourceSubtitleStreams = subtitleStreams +# # else: +# # for streamIndex in range(len(subtitleStreams)): +# # subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0 +# # +# # sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams +# # +# # dispositionTokens += generateDispositionTokens(sourceSubtitleStreams) +# # +# +# +# +# +# +# click.echo(f"Audio stream source order {audioStreamSourceOrder}") +# click.echo(f"Subtitle stream source order {subtitleStreamSourceOrder}") +# +# +# commandTokens = COMMAND_TOKENS + ['-i', sourcePath] +# +# +# # matchingSubtitles = [] +# # if context['import_subtitles']: +# # +# +# +# # +# # for streamIndex in range(len(mSubtitles)): +# # mSubtitles[streamIndex]['forced'] = 1 if forcedSubtitle != -1 and streamIndex == forcedSubtitle else 0 +# # mSubtitles[streamIndex]['default'] = 1 if defaultSubtitle != -1 and streamIndex == defaultSubtitle else 0 +# # +# # if streamIndex <= len(subtitleTitles) -1: +# # mSubtitles[streamIndex]['title'] = subtitleTitles[streamIndex] +# # +# # if defaultSubtitle != -1 and jellyfin: +# # matchingSubtitles = getReorderedSubstreams(mSubtitles, defaultSubtitle) +# # else: +# # matchingSubtitles = mSubtitles +# +# +# +# for q in q_list: +# +# click.echo(f"\nRunning job {job_index} file={sourcePath} q={q}") +# job_index += 1 +# +# +# # # Reorder audio stream descriptors and create disposition options if default is given per command line option +# # if defaultAudio == -1: +# # sourceAudioStreams = audioStreams +# # else: +# # for streamIndex in range(len(audioStreams)): +# # audioStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultAudio else 0 +# # +# # sourceAudioStreams = getReorderedSubstreams(audioStreams, defaultAudio) if jellyfin else audioStreams +# # +# # dispositionTokens += generateDispositionTokens(sourceAudioStreams) +# # +# # # Set forced tag in subtitle descriptor if given per command line option +# # if forcedSubtitle != -1: +# # for streamIndex in range(len(subtitleStreams)): +# # subtitleStreams[streamIndex]['disposition']['forced'] = 1 if streamIndex == forcedSubtitle else 0 +# # +# # # Reorder subtitle stream descriptors and create disposition options if default is given per command line option +# # if defaultSubtitle == -1: +# # sourceSubtitleStreams = subtitleStreams +# # else: +# # for streamIndex in range(len(subtitleStreams)): +# # subtitleStreams[streamIndex]['disposition']['default'] = 1 if streamIndex == defaultSubtitle else 0 +# # +# # sourceSubtitleStreams = getReorderedSubstreams(subtitleStreams, defaultSubtitle) if jellyfin else subtitleStreams +# # +# # dispositionTokens += generateDispositionTokens(sourceSubtitleStreams) +# # +# +# +# # # Create mapping and ffmpeg options for subtitle streams +# +# # if context['import_subtitles']: +# # +# # numMatchingSubtitles = len(matchingSubtitles) +# # +# # if jellyfin and defaultSubtitle != -1: +# # subtitleSequence = getModifiedStreamOrder(numMatchingSubtitles, default_subtitle) #! +# # else: +# # subtitleSequence = range(numMatchingSubtitles) +# # +# # for fileIndex in range(numMatchingSubtitles): +# # +# # # Create mapping for subtitle streams when imported from files +# # mappingTokens += ['-map', f"{subtitleSequence[fileIndex]+1}:s:0"] +# # +# # msg = matchingSubtitles[fileIndex] +# # subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"language={msg['language']}"] +# # if 'title' in matchingSubtitles[fileIndex].keys(): +# # subtitleMetadataTokens += [f"-metadata:s:s:{fileIndex}", f"title={matchingSubtitles[fileIndex]['title']}"] +# # +# # else: +# # +# # for subtitleStreamIndex in range(len(sourceSubtitleStreams)): +# # +# # subtitleStream = sourceSubtitleStreams[subtitleStreamIndex] +# # +# # # Create mapping for subtitle streams +# # mappingTokens += ['-map', f"s:{subtitleStream['src_sub_index']}"] +# # +# # if 'tags' in subtitleStream.keys(): +# # if 'language' in subtitleStream['tags'].keys(): +# # subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"language={subtitleStream['tags']['language']}"] +# # if 'title' in subtitleStream['tags'].keys(): +# # subtitleMetadataTokens += [f"-metadata:s:s:{subtitleStreamIndex}", f"title={subtitleStream['tags']['title']}"] +# +# +# # Job specific tokens +# targetFilenameJobTokens = targetFilenameTokens.copy() +# +# if len(q_list) > 1: +# targetFilenameJobTokens += [f"q{q}"] +# +# # In case source and target filenames are the same add an extension to distinct output from input +# if not label and sourceFilenameExtension == targetFilenameExtension: +# targetFilenameJobTokens += ['ffx'] +# +# targetFilename = '_'.join(targetFilenameJobTokens) # + '.' + targetFilenameExtension +# +# click.echo(f"target filename: {targetFilename}") +# +# +# if video_encoder == 'av1': +# +# commandSequence = (commandTokens +# + subtitleImportFileTokens +# + mappingTokens +# + audioMetadataTokens +# + subtitleMetadataTokens +# + audioDispositionTokens +# + subtitleDispositionTokens +# + audioEncodingTokens +# + generateAV1Tokens(q, preset) + audioEncodingTokens) +# +# if clear_metadata: +# commandSequence += generateClearTokens(sourceStreamDescriptor) +# +# commandSequence += cropTokens +# +# commandSequence += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION) +# +# click.echo(f"Command: {' '.join(commandSequence)}") +# +# if not dry_run: +# executeProcess(commandSequence) +# +# +# if video_encoder == 'vp9': +# +# commandSequence1 = commandTokens + mappingVideoTokens + generateVP9Pass1Tokens(q) +# +# commandSequence1 += cropTokens +# +# commandSequence1 += NULL_TOKENS +# +# click.echo(f"Command 1: {' '.join(commandSequence1)}") +# +# if os.path.exists(TEMP_FILE_NAME): +# os.remove(TEMP_FILE_NAME) +# +# if not dry_run: +# executeProcess(commandSequence1) +# +# +# commandSequence2 = (commandTokens +# + subtitleImportFileTokens +# + mappingTokens +# + audioMetadataTokens +# + subtitleMetadataTokens +# + audioDispositionTokens +# + subtitleDispositionTokens +# + dispositionTokens) +# +# if denoise: +# commandSequence2 += generateDenoiseTokens() +# +# commandSequence2 += generateVP9Pass2Tokens(q) + audioEncodingTokens +# +# if clear_metadata: +# commandSequence2 += generateClearTokens(sourceStreamDescriptor) +# +# commandSequence2 += cropTokens +# +# commandSequence2 += generateOutputTokens(targetFilename, DEFAULT_FILE_FORMAT, DEFAULT_FILE_EXTENSION) +# +# click.echo(f"Command 2: {' '.join(commandSequence2)}") +# +# if not dry_run: +# executeProcess(commandSequence2) +# +# +# #app = ModesApp(context) +# #app.run() +# +# #click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True) +# +# click.echo('\nDONE\n') endTime = time.perf_counter() click.echo(f"Time elapsed {endTime - startTime}") diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index 4374737..85b6567 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -35,6 +35,7 @@ class FfxController(): '_STATISTICS_WRITING_DATE_UTC', '_STATISTICS_TAGS'] + INPUT_FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm'] # def getReorderedSubstreams(subDescriptor, last): diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index 21d442e..746420a 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -63,9 +63,9 @@ class MediaDescriptor(): def getReorderedTrackDescriptors(self): - videoTracks = [v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO] - audioTracks = [a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO] - subtitleTracks = [s for s in self.__trackDescriptors.copy() if s.getType() == TrackType.SUBTITLE] + videoTracks = self.getVideoTracks() + audioTracks = self.getAudioTracks() + subtitleTracks = self.getSubtitleTracks() videoDefaultTracks = [v for v in videoTracks if TrackDisposition.DEFAULT in v.getDispositionSet()] videoForcedTracks = [v for v in videoTracks if TrackDisposition.FORCED in v.getDispositionSet()] @@ -139,16 +139,31 @@ class MediaDescriptor(): def getAllTracks(self) -> List[TrackDescriptor]: - return self.__trackDescriptors + return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks() def getVideoTracks(self) -> List[TrackDescriptor]: - return [d for d in self.__trackDescriptors if d.getType() == TrackType.VIDEO] + videoTracks = [v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO] + subIndex = 0 + for v in videoTracks: + v.setSubIndex(subIndex) + subIndex += 1 + return videoTracks def getAudioTracks(self) -> List[TrackDescriptor]: - return [d for d in self.__trackDescriptors if d.getType() == TrackType.AUDIO] + audioTracks = [a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO] + subIndex = 0 + for a in audioTracks: + a.setSubIndex(subIndex) + subIndex += 1 + return audioTracks def getSubtitleTracks(self) -> List[TrackDescriptor]: - return [d for d in self.__trackDescriptors if d.getType() == TrackType.SUBTITLE] + subtitleTracks = [s for s in self.__trackDescriptors.copy() if s.getType() == TrackType.SUBTITLE] + subIndex = 0 + for s in subtitleTracks: + s.setSubIndex(subIndex) + subIndex += 1 + return subtitleTracks def getClearTags(self): return self.__clearTags @@ -209,3 +224,27 @@ class MediaDescriptor(): compareResult[MediaDescriptor.TRACKS_KEY] = trackCompareResult return compareResult + + + def getInputMappingTokens(self): + reorderedTrackDescriptors = self.getReorderedTrackDescriptors() + inputMappingTokens = [] + + #subIndexCounter = {} + for rtd in reorderedTrackDescriptors: + trackType = rtd.getType() + #if not trackType in subIndexCounter.keys(): + # subIndexCounter[trackType] = 0 + #inputMappingTokens += ['-map', f"0:{trackType.indicator()}:{subIndexCounter[trackType]}"] + inputMappingTokens += ['-map', f"0:{trackType.indicator()}:{rtd.getSubIndex()}"] + #subIndexCounter[trackType] += 1 + return inputMappingTokens + +# for rtd in reorderedTrackDescriptors: +# trackType = rtd.getType() +# #if not trackType in subIndexCounter.keys(): +# # subIndexCounter[trackType] = 0 +# #inputMappingTokens += ['-map', f"0:{trackType.indicator()}:{subIndexCounter[trackType]}"] +# inputMappingTokens += ['-map', f"0:{rtd.getIndex()}"] +# #subIndexCounter[trackType] += 1 +# return inputMappingTokens diff --git a/bin/ffx/media_details_screen.py b/bin/ffx/media_details_screen.py index 44743b9..06cda78 100644 --- a/bin/ffx/media_details_screen.py +++ b/bin/ffx/media_details_screen.py @@ -126,7 +126,6 @@ class MediaDetailsScreen(Screen): if not os.path.isfile(self.__mediaFilename): raise click.ClickException(f"MediaDetailsScreen.__init__(): Media file {self.__mediaFilename} does not exist") - self.loadProperties() @@ -160,8 +159,6 @@ class MediaDetailsScreen(Screen): # from file (=current) vs from stored in database (=target) self.__mediaDifferences = self.__targetMediaDescriptor.compare(self.__currentMediaDescriptor) if self.__currentPattern is not None else {} - #rtd = self.__targetMediaDescriptor.getReorderedTrackDescriptors() - #raise click.ClickException(f"getReorderedTrackDescriptors={[r.getIndex() for r in rtd]}") def updateDifferences(self): @@ -239,7 +236,6 @@ class MediaDetailsScreen(Screen): self.differencesTable.add_row(*map(str, row)) - def on_mount(self): if self.__currentPattern is None: @@ -273,12 +269,14 @@ class MediaDetailsScreen(Screen): self.query_one("#pattern_input", Input).value = self.__mediaFilename self.highlightPattern(True) + def highlightPattern(self, state : bool): if state: self.query_one("#pattern_input", Input).styles.background = 'red' else: self.query_one("#pattern_input", Input).styles.background = None + def updateTracks(self): self.tracksTable.clear() @@ -311,7 +309,6 @@ class MediaDetailsScreen(Screen): def compose(self): - # Create the DataTable widget self.showsTable = DataTable() @@ -323,7 +320,6 @@ class MediaDetailsScreen(Screen): self.showsTable.cursor_type = 'row' - self.mediaTagsTable = DataTable() # Define the columns with headers @@ -393,69 +389,6 @@ class MediaDetailsScreen(Screen): yield self.tracksTable yield Static(" ") -# # 8 -# yield Static(" ", classes="three") -# -# # 9 -# yield Static("Subtitle Streams") -# yield self.subtitleStreamsTable -# yield Static(" ") - - - # 1 -# yield Static("Edit filename pattern" if self.__pattern is not None else "New filename pattern", id="toplabel") -# yield Input(type="text", id="pattern_input", classes="four") -# -# # 2 -# yield Static("from show") -# yield Static("", id="showlabel", classes="three") -# yield Button("Substitute pattern", id="patternbutton") -# -# # 3 -# yield Static(" ", classes="five") -# # 4 -# yield Static(" ", classes="five") -# -# # 5 -# yield Static("Audio streams") -# yield Static(" ") -# -# if self.__pattern is not None: -# yield Button("Add", id="button_add_audio_stream") -# yield Button("Edit", id="button_edit_audio_stream") -# yield Button("Delete", id="button_delete_audio_stream") -# else: -# yield Static("") -# yield Static("") -# yield Static("") -# # 6 -# yield self.tracksTable -# -# # 7 -# yield Static(" ", classes="five") -# -# # 8 -# yield Static("Subtitle streams") -# yield Static(" ") -# -# if self.__pattern is not None: -# yield Button("Add", id="button_add_subtitle_stream") -# yield Button("Edit", id="button_edit_subtitle_stream") -# yield Button("Delete", id="button_delete_subtitle_stream") -# else: -# yield Static("") -# yield Static("") -# yield Static("") -# # 9 -# yield self.subtitleStreamsTable -# -# # 10 -# yield Static(" ", classes="five") -# -# # 11 -# yield Button("Save", id="save_button") -# yield Button("Cancel", id="cancel_button") - yield Footer() @@ -463,113 +396,8 @@ class MediaDetailsScreen(Screen): return str(self.query_one("#pattern_input", Input).value) - -# def getSelectedAudioTrackDescriptor(self): -# -# if not self.__pattern: -# return None -# -# try: -# -# # Fetch the currently selected row when 'Enter' is pressed -# #selected_row_index = self.table.cursor_row -# row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate) -# -# if row_key is not None: -# selected_track_data = self.tracksTable.get_row(row_key) -# -# subIndex = int(selected_track_data[0]) -# -# return self.__tc.findTrack(self.__pattern.getId(), TrackType.AUDIO, subIndex).getDescriptor() -# -# else: -# return None -# -# except CellDoesNotExist: -# return None -# - -# def getSelectedSubtitleTrackDescriptor(self) -> TrackDescriptor: -# -# if not self.__pattern is None: -# return None -# -# try: -# -# # Fetch the currently selected row when 'Enter' is pressed -# #selected_row_index = self.table.cursor_row -# row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate) -# -# if row_key is not None: -# -# selected_track_data = self.subtitleStreamsTable.get_row(row_key) -# subIndex = int(selected_track_data[0]) -# -# return self.__tc.findTrack(self.__pattern.getId(), TrackType.SUBTITLE, subIndex).getDescriptor() -# -# else: -# return None -# -# except CellDoesNotExist: -# return None - - - - # Event handler for button press def on_button_pressed(self, event: Button.Pressed) -> None: -# # Check if the button pressed is the one we are interested in -# if event.button.id == "save_button": -# -# patternDescriptor = {} -# patternDescriptor['show_id'] = self.show_obj['id'] -# patternDescriptor['pattern'] = self.getPatternFromInput() -# -# if self.__pattern is not None: -# -# if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor): -# self.dismiss(patternDescriptor) -# else: -# #TODO: Meldung -# self.app.pop_screen() -# -# else: -# if self.__pc.addPattern(patternDescriptor): -# self.dismiss(patternDescriptor) -# else: -# #TODO: Meldung -# self.app.pop_screen() -# -# -# -# if event.button.id == "cancel_button": -# self.app.pop_screen() -# -# -# # Save pattern when just created before adding streams -# if self.__pattern is not None: -# -# if event.button.id == "button_add_audio_stream": -# self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.__pattern.getId(), subIndex = len(self.tracksTable.rows)), self.handle_add_track) -# -# selectedAudioTrack = self.getSelectedAudioTrackDescriptor() -# if selectedAudioTrack is not None: -# if event.button.id == "button_edit_audio_stream": -# -# self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedAudioTrack), self.handle_edit_track) -# if event.button.id == "button_delete_audio_stream": -# self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedAudioTrack), self.handle_delete_track) -# -# if event.button.id == "button_add_subtitle_stream": -# self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.__pattern.getId(), subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track) -# -# selectedSubtitleTrack = self.getSelectedSubtitleTrackDescriptor() -# if selectedSubtitleTrack is not None: -# if event.button.id == "button_edit_subtitle_stream": -# self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedSubtitleTrack), self.handle_edit_track) -# if event.button.id == "button_delete_subtitle_stream": -# self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedSubtitleTrack), self.handle_delete_track) - if event.button.id == "pattern_button": INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)' @@ -582,75 +410,6 @@ class MediaDetailsScreen(Screen): self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN) -# def handle_add_track(self, trackDescriptor): -# -# dispoSet = trackDescriptor.getDispositionSet() -# trackType = trackDescriptor.getType() -# subIndex = trackDescriptor.getSubIndex() -# language = trackDescriptor.getLanguage() -# title = trackDescriptor.getTitle() -# -# if trackType == TrackType.AUDIO: -# -# row = (subIndex, -# " ", -# language.label(), -# title, -# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No', -# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No') -# -# self.tracksTable.add_row(*map(str, row)) -# -# if trackType == TrackType.SUBTITLE: -# -# row = (subIndex, -# " ", -# language.label(), -# title, -# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No', -# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No') -# -# self.subtitleStreamsTable.add_row(*map(str, row)) - - -# def handle_edit_track(self, trackDescriptor : TrackDescriptor): -# -# try: -# if trackDescriptor.getType() == TrackType.AUDIO: -# -# row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate) -# -# self.tracksTable.update_cell(row_key, self.column_key_track_language, trackDescriptor.getLanguage().label()) -# self.tracksTable.update_cell(row_key, self.column_key_track_title, trackDescriptor.getTitle()) -# self.tracksTable.update_cell(row_key, self.column_key_track_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No') -# self.tracksTable.update_cell(row_key, self.column_key_track_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No') -# -# if trackDescriptor.getType() == TrackType.SUBTITLE: -# -# row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate) -# -# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_language, trackDescriptor.getLanguage().label()) -# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_title, trackDescriptor.getTitle()) -# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No') -# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No') -# -# except CellDoesNotExist: -# pass - - -# def handle_delete_track(self, trackDescriptor : TrackDescriptor): -# -# try: -# if trackDescriptor.getType() == TrackType.AUDIO: -# self.updateAudioTracks() -# -# if trackDescriptor.getType() == TrackType.SUBTITLE: -# self.updateSubtitleTracks() -# -# except CellDoesNotExist: -# pass - - def getSelectedShow(self): try: diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index ec7b138..b77d82c 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -172,6 +172,8 @@ class TrackDescriptor(): def getSubIndex(self): return self.__subIndex + def setSubIndex(self, subIndex): + self.__subIndex = subIndex def getType(self): return self.__trackType @@ -197,7 +199,7 @@ class TrackDescriptor(): def getDispositionSet(self): return self.__dispositionSet - + def compare(self, vsTrackDescriptor): diff --git a/bin/ffx/track_type.py b/bin/ffx/track_type.py index 6eca443..9e566da 100644 --- a/bin/ffx/track_type.py +++ b/bin/ffx/track_type.py @@ -13,6 +13,10 @@ class TrackType(Enum): """Returns the stream type as string""" return str(self.value['label']) + def indicator(self): + """Returns the stream type as single letter""" + return self.label()[0] + def index(self): """Returns the stream type index""" return int(self.value['index'])