diff --git a/bin/ffx.py b/bin/ffx.py index 596eba4..4b2480f 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -8,6 +8,8 @@ from ffx.ffx_app import FfxApp from ffx.ffx_controller import FfxController from ffx.database import databaseContext +from ffx.track_type import TrackType + VERSION='0.1.0' @@ -207,53 +209,35 @@ def convert(ctx, 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']}") -# -# -# + + click.echo(f"Stereo bitrate: {context['bitrates']['stereo']}") + click.echo(f"AC3 bitrate: {context['bitrates']['ac3']}") + click.echo(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: + cropStart = int(cTokens[0]) + cropLength = int(cTokens[1]) + cropTokens = FfxController.generateCropTokens(cropStart, cropLength) + else: + cropTokens = FfxController.generateCropTokens() + else: + cropTokens = [] + + click.echo(f"Crop tokens={cropTokens}") + + # ## 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") @@ -267,27 +251,74 @@ def convert(ctx, sourceFileBasename = '.'.join(sourcePathTokens[:-1]) sourceFilenameExtension = sourcePathTokens[-1] - click.echo(f"\nProcessing file {sourcePath}") mediaFileProperties = FileProperties(context, sourceFilename) - currentMediaDescriptor = mediaFileProperties.getMediaDescriptor() + sourceMediaDescriptor = 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: + + if currentPattern is None: + + # Case no pattern matching + + # 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 + try: + sourceMediaDescriptor.getDefaultVideoTrack() + except ValueError: + defaultVideoTrackSubIndex = click.prompt("More than one default video stream detected! Please select stream", type=int) + sourceMediaDescriptor.setDefaultSubTrack(TrackType.VIDEO, defaultVideoTrackSubIndex) + try: + sourceMediaDescriptor.getForcedVideoTrack() + except ValueError: + forcedVideoTrackSubIndex = click.prompt("More than one forced video stream detected! Please select stream", type=int) + sourceMediaDescriptor.setForcedSubTrack(TrackType.VIDEO, forcedVideoTrackSubIndex) + try: + sourceMediaDescriptor.getDefaultAudioTrack() + except ValueError: + defaultAudioTrackSubIndex = click.prompt("More than one default audio stream detected! Please select stream", type=int) + sourceMediaDescriptor.setDefaultSubTrack(TrackType.AUDIO, defaultAudioTrackSubIndex) + try: + sourceMediaDescriptor.getForcedAudioTrack() + except ValueError: + forcedAudioTrackSubIndex = click.prompt("More than one forced audio stream detected! Please select stream", type=int) + sourceMediaDescriptor.setForcedSubTrack(TrackType.AUDIO, forcedAudioTrackSubIndex) + try: + sourceMediaDescriptor.getDefaultSubtitleTrack() + except ValueError: + defaultSubtitleTrackSubIndex = click.prompt("More than one default subtitle stream detected! Please select stream", type=int) + sourceMediaDescriptor.setDefaultSubTrack(TrackType.SUBTITLE, defaultSubtitleTrackSubIndex) + try: + sourceMediaDescriptor.getForcedSubtitleTrack() + except ValueError: + forcedSubtitleTrackSubIndex = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) + sourceMediaDescriptor.setForcedSubTrack(TrackType.SUBTITLE, forcedSubtitleTrackSubIndex) + + fc = FfxController(context, sourceMediaDescriptor) + + dispositionTokens = fc.generateDispositionTokens() + click.echo(f"Disposition Tokens: {dispositionTokens}") + + + else: + + # Case pattern matching + + targetMediaDescriptor = currentPattern.getMediaDescriptor() if currentPattern is not None else None targetMediaDescriptor.setJellyfinOrder(context['jellyfin']) click.echo(f"Input mapping tokens: {targetMediaDescriptor.getInputMappingTokens()}") - fc = FfxController(context, currentMediaDescriptor, targetMediaDescriptor) + fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor) mappingTokens = fc.generateMetadataTokens() click.echo(f"Metadata Tokens: {mappingTokens}") @@ -300,26 +331,7 @@ def convert(ctx, click.echo(f"Season={mediaFileProperties.getSeason()} Episode={mediaFileProperties.getEpisode()}") -# # 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}") @@ -359,212 +371,9 @@ def convert(ctx, # ## ## ## # 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']}"] -# -# 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']}"] -# -# -# -# -# -# -# # # 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] # # @@ -755,9 +564,5 @@ def convert(ctx, click.echo(f"Time elapsed {endTime - startTime}") - # click.echo(f"app result: {app.getContext()}") - - - if __name__ == '__main__': ffx() diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index a86424c..96dab3a 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -40,8 +40,8 @@ class FfxController(): def __init__(self, context : dict, - sourceMediaDescriptor : MediaDescriptor, - targetMediaDescriptor : MediaDescriptor): + targetMediaDescriptor : MediaDescriptor, + sourceMediaDescriptor : MediaDescriptor = None): self.__context = context self.__sourceMediaDescriptor = sourceMediaDescriptor @@ -90,7 +90,12 @@ class FfxController(): '-lag-in-frames', '25'] - def generateCropTokens(self, start, length): + + @staticmethod + def generateCropTokens(cropStart : int = -1, cropLength : int = -1): + + start = int(cropStart if cropStart > -1 else FfxController.DEFAULT_CROP_START) + length = int(cropLength if cropLength > -1 else FfxController.DEFAULT_CROP_LENGTH) return ['-ss', str(start), '-t', str(length)] @@ -171,27 +176,29 @@ class FfxController(): def generateDispositionTokens(self): """-disposition:s:X default+forced""" - sourceTrackDescriptors = self.__sourceMediaDescriptor.getAllTrackDescriptors() + sourceTrackDescriptors = [] if self.__sourceMediaDescriptor is None else self.__sourceMediaDescriptor.getAllTrackDescriptors() targetTrackDescriptors = self.__targetMediaDescriptor.getReorderedTrackDescriptors() - dispositionTokens = [] # for subStreamIndex in range(len(subDescriptor)): for trackDescriptor in targetTrackDescriptors: - # Calculate source sub index - changedTargetTrackDescriptor : TrackDescriptor = targetTrackDescriptors[trackDescriptor.getIndex()] - changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex() - sourceSubIndex = sourceTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex() + # Calculate source sub index. This applies only if a source media descriptor is defined. + if sourceTrackDescriptors: + changedTargetTrackDescriptor : TrackDescriptor = targetTrackDescriptors[trackDescriptor.getIndex()] + changedTargetTrackSourceIndex = changedTargetTrackDescriptor.getSourceIndex() + subIndex = sourceTrackDescriptors[changedTargetTrackSourceIndex].getSubIndex() + else: + subIndex = trackDescriptor.getSubIndex() streamIndicator = trackDescriptor.getType().indicator() dispositionSet = trackDescriptor.getDispositionSet() if dispositionSet: - dispositionTokens += [f"-disposition:{streamIndicator}:{sourceSubIndex}", '+'.join([d.label() for d in dispositionSet])] + dispositionTokens += [f"-disposition:{streamIndicator}:{subIndex}", '+'.join([d.label() for d in dispositionSet])] else: - dispositionTokens += [f"-disposition:{streamIndicator}:{sourceSubIndex}", '0'] + dispositionTokens += [f"-disposition:{streamIndicator}:{subIndex}", '0'] return dispositionTokens diff --git a/bin/ffx/file_properties.py b/bin/ffx/file_properties.py index 07e9bda..9845472 100644 --- a/bin/ffx/file_properties.py +++ b/bin/ffx/file_properties.py @@ -67,24 +67,25 @@ class FileProperties(): # # print(f"season={season} episode={episode} file={file_index}") # -# -# # Assemble target filename tokens -# targetFilenameTokens = [] + + def assembleTargetFilename(self): + + targetFilenameTokens = [] # targetFilenameExtension = 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}"] +# if self.__season > -1 and self.__episode > -1: +# targetFilenameTokens += [f"S{self.__season:0{season_digits}d}E{self.__episode:0{episode_digits}d}"] +# elif self.__episode > -1: +# targetFilenameTokens += [f"E{self.__episode:0{episode_digits}d}"] # else: # targetFilenameTokens += [f"{file_index:0{index_digits}d}"] # # else: -# targetFilenameTokens = [sourceFileBasename] -# +# targetFilenameTokens = [self.__sourceFileBasename] + def searchSubtitleFiles(dir, prefix): diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index dfd3d58..9bd9e99 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -65,39 +65,74 @@ class MediaDescriptor(): self.__jellyfinOrder = False - def getReorderedTrackDescriptors(self): - - 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()] - audioDefaultTracks = [a for a in audioTracks if TrackDisposition.DEFAULT in a.getDispositionSet()] - audioForcedTracks = [a for a in audioTracks if TrackDisposition.FORCED in a.getDispositionSet()] - subtitleDefaultTracks = [s for s in subtitleTracks if TrackDisposition.DEFAULT in s.getDispositionSet()] - subtitleForcedTracks = [s for s in subtitleTracks if TrackDisposition.FORCED in s.getDispositionSet()] - + def getDefaultVideoTrack(self): + videoDefaultTracks = [v for v in self.getVideoTracks() if TrackDisposition.DEFAULT in v.getDispositionSet()] if len(videoDefaultTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default video track is not supported') + raise ValueError('MediaDescriptor.getDefaultVideoTrack(): More than one default video track is not supported') + return videoDefaultTracks[0] if videoDefaultTracks else None + + def getForcedVideoTrack(self): + videoForcedTracks = [v for v in self.getVideoTracks() if TrackDisposition.FORCED in v.getDispositionSet()] if len(videoForcedTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced video track is not supported') + raise ValueError('MediaDescriptor.getForcedVideoTrack(): More than one forced video track is not supported') + return videoForcedTracks[0] if videoForcedTracks else None + + def getDefaultAudioTrack(self): + audioDefaultTracks = [a for a in self.getAudioTracks() if TrackDisposition.DEFAULT in a.getDispositionSet()] if len(audioDefaultTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default audio track is not supported') + raise ValueError('MediaDescriptor.getDefaultAudioTrack(): More than one default audio track is not supported') + return audioDefaultTracks[0] if audioDefaultTracks else None + + def getForcedAudioTrack(self): + audioForcedTracks = [a for a in self.getAudioTracks() if TrackDisposition.FORCED in a.getDispositionSet()] if len(audioForcedTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced audio track is not supported') + raise ValueError('MediaDescriptor.getForcedAudioTrack(): More than one forced audio track is not supported') + return audioForcedTracks[0] if audioForcedTracks else None + + def getDefaultSubtitleTrack(self): + subtitleDefaultTracks = [s for s in self.getSubtitleTracks() if TrackDisposition.DEFAULT in s.getDispositionSet()] if len(subtitleDefaultTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default subtitle track is not supported') + raise ValueError('MediaDescriptor.getDefaultSubtitleTrack(): More than one default subtitle track is not supported') + return subtitleDefaultTracks[0] if subtitleDefaultTracks else None + + def getForcedSubtitleTrack(self): + subtitleForcedTracks = [s for s in self.getSubtitleTracks() if TrackDisposition.FORCED in s.getDispositionSet()] if len(subtitleForcedTracks) > 1: - raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced subtitle track is not supported') - + raise ValueError('MediaDescriptor.getForcedSubtitleTrack(): More than one forced subtitle track is not supported') + return subtitleForcedTracks[0] if subtitleForcedTracks else None + + + def setDefaultSubTrack(self, trackType : TrackType, subIndex : int): + for t in self.getAllTrackDescriptors(): + if t.getType() == trackType: + t.setDispositionFlag(TrackDisposition.DEFAULT, t.getSubIndex() == int(subIndex)) + + def setForcedSubTrack(self, trackType : TrackType, subIndex : int): + for t in self.getAllTrackDescriptors(): + if t.getType() == trackType: + t.setDispositionFlag(TrackDisposition.FORCED, t.getSubIndex() == int(subIndex)) + + + def getReorderedTrackDescriptors(self): + + videoTracks = self.sortSubIndices(self.getVideoTracks()) + audioTracks = self.sortSubIndices(self.getAudioTracks()) + subtitleTracks = self.sortSubIndices(self.getSubtitleTracks()) + + videoDefaultTrack = self.getDefaultVideoTrack() + self.getForcedVideoTrack() + audioDefaultTrack = self.getDefaultAudioTrack() + self.getForcedAudioTrack() + subtitleDefaultTrack = self.getDefaultSubtitleTrack() + self.getForcedSubtitleTrack() + if self.__jellyfinOrder: - if videoDefaultTracks: - videoTracks.append(videoTracks.pop(videoTracks.index(videoDefaultTracks[0]))) - if audioDefaultTracks: - audioTracks.append(audioTracks.pop(audioTracks.index(audioDefaultTracks[0]))) - if subtitleDefaultTracks: - subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTracks[0]))) + if not videoDefaultTrack is None: + videoTracks.append(videoTracks.pop(videoTracks.index(videoDefaultTrack))) + if not audioDefaultTrack is None: + audioTracks.append(audioTracks.pop(audioTracks.index(audioDefaultTrack))) + if not subtitleDefaultTrack is None: + subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTrack))) reorderedTrackDescriptors = videoTracks + audioTracks + subtitleTracks orderedSourceTrackSequence = [t.getSourceIndex() for t in reorderedTrackDescriptors] @@ -143,32 +178,26 @@ class MediaDescriptor(): return self.__mediaTags + def sortSubIndices(self, descriptors : List[TrackDescriptor]) -> List[TrackDescriptor]: + subIndex = 0 + for t in descriptors: + t.setSubIndex(subIndex) + subIndex += 1 + return descriptors + + def getAllTrackDescriptors(self) -> List[TrackDescriptor]: return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks() def getVideoTracks(self) -> List[TrackDescriptor]: - 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 + return [v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO] + def getAudioTracks(self) -> List[TrackDescriptor]: - 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 + return [a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO] def getSubtitleTracks(self) -> List[TrackDescriptor]: - 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 + return [s for s in self.__trackDescriptors.copy() if s.getType() == TrackType.SUBTITLE] def getJellyfin(self): return self.__jellyfinOrder diff --git a/bin/ffx/track_controller.py b/bin/ffx/track_controller.py index 7bae042..f4558e3 100644 --- a/bin/ffx/track_controller.py +++ b/bin/ffx/track_controller.py @@ -229,3 +229,10 @@ class TrackController(): raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}") finally: s.close() + + + def setDefaultSubTrack(self, trackType, subIndex): + pass + + def setForcedSubTrack(self, trackType, subIndex): + pass diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index ef375b9..22cf3b6 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -204,6 +204,16 @@ class TrackDescriptor(): return self.__dispositionSet + def getDispositionFlag(self, disposition : TrackDisposition) -> bool: + return bool(disposition in self.__dispositionSet) + + def setDispositionFlag(self, disposition : TrackDisposition, state : bool): + if state: + self.__dispositionSet.add(disposition) + else: + self.__dispositionSet.discard(disposition) + + def compare(self, vsTrackDescriptor): compareResult = {}