diff --git a/bin/ffx.py b/bin/ffx.py index 8753d9b..4499560 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -228,8 +228,7 @@ def convert(ctx, if cTokens and len(cTokens) == 2: context['crop_start'] = int(cTokens[0]) context['crop_lenght'] = int(cTokens[1]) - - click.echo(f"Crop start={context['crop_start']} length={context['crop_length']}") + click.echo(f"Crop start={context['crop_start']} length={context['crop_length']}") # ## Conversion parameters @@ -304,6 +303,9 @@ def convert(ctx, forcedSubtitleTrackSubIndex = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int) sourceMediaDescriptor.setForcedSubTrack(TrackType.SUBTITLE, forcedSubtitleTrackSubIndex) + if context['import_subtitles']: + sourceMediaDescriptor.importSubtitles(context['subtitle_directory'], context['subtitle_prefix']) + fc = FfxController(context, sourceMediaDescriptor) # mappingTokens = fc.generateMetadataTokens() @@ -315,9 +317,6 @@ def convert(ctx, audioTokens = fc.generateAudioEncodingTokens() click.echo(f"Audio Tokens: {audioTokens}") - audioTokens = fc - click.echo(f"Audio Tokens: {audioTokens}") - else: # Case pattern matching diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index 136f996..d5cf5e0 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -8,7 +8,7 @@ from ffx.audio_layout import AudioLayout from ffx.track_type import TrackType from ffx.video_encoder import VideoEncoder from ffx.process import executeProcess -from ffx.file_properties import FileProperties + class FfxController(): @@ -360,7 +360,7 @@ class FfxController(): if videoEncoder == VideoEncoder.AV1: commandSequence = (commandTokens - #+ subtitleImportFileTokens + + self.__targetMediaDescriptor.getImportFileTokens() + self.__targetMediaDescriptor.getInputMappingTokens() + self.generateDispositionTokens()) @@ -404,7 +404,7 @@ class FfxController(): executeProcess(commandSequence1) commandSequence2 = (commandTokens - #+ subtitleImportFileTokens + + self.__targetMediaDescriptor.getImportFileTokens() + self.__targetMediaDescriptor.getInputMappingTokens() + self.generateDispositionTokens()) diff --git a/bin/ffx/file_properties.py b/bin/ffx/file_properties.py index 725e807..88ab9fa 100644 --- a/bin/ffx/file_properties.py +++ b/bin/ffx/file_properties.py @@ -17,9 +17,6 @@ class FileProperties(): SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)' EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)' - SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})' - SUBTITLE_FILE_EXTENSION = 'vtt' - DEFAULT_INDEX_DIGITS = 3 def __init__(self, context, sourcePath): diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index 377e0c5..f9d1204 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -1,118 +1,174 @@ -import os, re, click +import os +import re +import click from typing import List, Self from ffx.track_type import TrackType from ffx.track_disposition import TrackDisposition -from ffx.file_properties import FileProperties from ffx.track_descriptor import TrackDescriptor from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY -class MediaDescriptor(): +class MediaDescriptor: """This class represents the structural content of a media file including streams and metadata""" - CONTEXT_KEY = 'context' + CONTEXT_KEY = "context" - TAGS_KEY = 'tags' - TRACKS_KEY = 'tracks' + TAGS_KEY = "tags" + TRACKS_KEY = "tracks" - TRACK_DESCRIPTOR_LIST_KEY = 'track_descriptors' - CLEAR_TAGS_FLAG_KEY = 'clear_tags' + TRACK_DESCRIPTOR_LIST_KEY = "track_descriptors" + CLEAR_TAGS_FLAG_KEY = "clear_tags" - FFPROBE_DISPOSITION_KEY = 'disposition' - FFPROBE_TAGS_KEY = 'tags' - FFPROBE_CODEC_TYPE_KEY = 'codec_type' + FFPROBE_DISPOSITION_KEY = "disposition" + FFPROBE_TAGS_KEY = "tags" + FFPROBE_CODEC_TYPE_KEY = "codec_type" - JELLYFIN_ORDER_FLAG_KEY = 'jellyfin_order' - - EXCLUDED_MEDIA_TAGS = [ - 'creation_time' - ] + JELLYFIN_ORDER_FLAG_KEY = "jellyfin_order" + + EXCLUDED_MEDIA_TAGS = ["creation_time"] + + SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})' + SUBTITLE_FILE_EXTENSION = 'vtt' def __init__(self, **kwargs): if MediaDescriptor.TAGS_KEY in kwargs.keys(): - if type(kwargs[MediaDescriptor.TAGS_KEY]) is not dict: - raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.TAGS_KEY} is required to be of type dict") - self.__mediaTags = kwargs[MediaDescriptor.TAGS_KEY] + if type(kwargs[MediaDescriptor.TAGS_KEY]) is not dict: + raise TypeError( + f"MediaDescriptor.__init__(): Argument { + MediaDescriptor.TAGS_KEY} is required to be of type dict" + ) + self.__mediaTags = kwargs[MediaDescriptor.TAGS_KEY] else: - self.__mediaTags = {} + self.__mediaTags = {} if MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY in kwargs.keys(): - if type(kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]) is not list: # Use List typehint for TrackDescriptor as well if it works - raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} is required to be of type list") - for d in kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]: - if type(d) is not TrackDescriptor: - raise TypeError(f"TrackDesciptor.__init__(): All elements of argument list {MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} are required to be of type TrackDescriptor") - self.__trackDescriptors = kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] + if ( + type(kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]) is not list + ): # Use List typehint for TrackDescriptor as well if it works + raise TypeError( + f"MediaDescriptor.__init__(): Argument { + MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} is required to be of type list" + ) + for d in kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY]: + if type(d) is not TrackDescriptor: + raise TypeError( + f"TrackDesciptor.__init__(): All elements of argument list { + MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY} are required to be of type TrackDescriptor" + ) + self.__trackDescriptors = kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] else: - self.__trackDescriptors = [] + self.__trackDescriptors = [] if MediaDescriptor.CLEAR_TAGS_FLAG_KEY in kwargs.keys(): - if type(kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]) is not bool: - raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CLEAR_TAGS_FLAG_KEY} is required to be of type bool") - self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY] + if type(kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY]) is not bool: + raise TypeError( + f"MediaDescriptor.__init__(): Argument { + MediaDescriptor.CLEAR_TAGS_FLAG_KEY} is required to be of type bool" + ) + self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_FLAG_KEY] else: - self.__clearTags = False + self.__clearTags = False if MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY in kwargs.keys(): - if type(kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]) is not bool: - raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY} is required to be of type bool") - self.__jellyfinOrder = kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY] + if type(kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY]) is not bool: + raise TypeError( + f"MediaDescriptor.__init__(): Argument { + MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY} is required to be of type bool" + ) + self.__jellyfinOrder = kwargs[MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY] else: - self.__jellyfinOrder = False - + self.__jellyfinOrder = False def getDefaultVideoTrack(self): - videoDefaultTracks = [v for v in self.getVideoTracks() if TrackDisposition.DEFAULT in v.getDispositionSet()] + videoDefaultTracks = [ + v + for v in self.getVideoTracks() + if TrackDisposition.DEFAULT in v.getDispositionSet() + ] if len(videoDefaultTracks) > 1: - raise ValueError('MediaDescriptor.getDefaultVideoTrack(): 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()] + videoForcedTracks = [ + v + for v in self.getVideoTracks() + if TrackDisposition.FORCED in v.getDispositionSet() + ] if len(videoForcedTracks) > 1: - raise ValueError('MediaDescriptor.getForcedVideoTrack(): 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()] + audioDefaultTracks = [ + a + for a in self.getAudioTracks() + if TrackDisposition.DEFAULT in a.getDispositionSet() + ] if len(audioDefaultTracks) > 1: - raise ValueError('MediaDescriptor.getDefaultAudioTrack(): 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()] + audioForcedTracks = [ + a + for a in self.getAudioTracks() + if TrackDisposition.FORCED in a.getDispositionSet() + ] if len(audioForcedTracks) > 1: - raise ValueError('MediaDescriptor.getForcedAudioTrack(): 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()] + subtitleDefaultTracks = [ + s + for s in self.getSubtitleTracks() + if TrackDisposition.DEFAULT in s.getDispositionSet() + ] if len(subtitleDefaultTracks) > 1: - raise ValueError('MediaDescriptor.getDefaultSubtitleTrack(): 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()] + subtitleForcedTracks = [ + s + for s in self.getSubtitleTracks() + if TrackDisposition.FORCED in s.getDispositionSet() + ] if len(subtitleForcedTracks) > 1: - raise ValueError('MediaDescriptor.getForcedSubtitleTrack(): 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): + def setDefaultSubTrack(self, trackType: TrackType, subIndex: int): for t in self.getAllTrackDescriptors(): if t.getType() == trackType: - t.setDispositionFlag(TrackDisposition.DEFAULT, t.getSubIndex() == int(subIndex)) + t.setDispositionFlag( + TrackDisposition.DEFAULT, t.getSubIndex() == int(subIndex) + ) - def setForcedSubTrack(self, trackType : TrackType, subIndex : int): + def setForcedSubTrack(self, trackType: TrackType, subIndex: int): for t in self.getAllTrackDescriptors(): if t.getType() == trackType: - t.setDispositionFlag(TrackDisposition.FORCED, t.getSubIndex() == int(subIndex)) - + t.setDispositionFlag( + TrackDisposition.FORCED, t.getSubIndex() == int(subIndex) + ) def getReorderedTrackDescriptors(self): @@ -129,76 +185,95 @@ class MediaDescriptor(): if self.__jellyfinOrder: if not videoDefaultTrack is None: - videoTracks.append(videoTracks.pop(videoTracks.index(videoDefaultTrack))) + videoTracks.append( + videoTracks.pop(videoTracks.index(videoDefaultTrack)) + ) if not audioDefaultTrack is None: - audioTracks.append(audioTracks.pop(audioTracks.index(audioDefaultTrack))) + audioTracks.append( + audioTracks.pop(audioTracks.index(audioDefaultTrack)) + ) if not subtitleDefaultTrack is None: - subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTrack))) + subtitleTracks.append( + subtitleTracks.pop(subtitleTracks.index(subtitleDefaultTrack)) + ) reorderedTrackDescriptors = videoTracks + audioTracks + subtitleTracks - orderedSourceTrackSequence = [t.getSourceIndex() for t in reorderedTrackDescriptors] + orderedSourceTrackSequence = [ + t.getSourceIndex() for t in reorderedTrackDescriptors + ] if len(set(orderedSourceTrackSequence)) < len(orderedSourceTrackSequence): - raise ValueError(f"Multiple streams originating from the same source stream not supported") + raise ValueError( + f"Multiple streams originating from the same source stream not supported" + ) return reorderedTrackDescriptors - @classmethod def fromFfprobe(cls, formatData, streamData): kwargs = {} if MediaDescriptor.FFPROBE_TAGS_KEY in formatData.keys(): - kwargs[MediaDescriptor.TAGS_KEY] = formatData[MediaDescriptor.FFPROBE_TAGS_KEY] + kwargs[MediaDescriptor.TAGS_KEY] = formatData[ + MediaDescriptor.FFPROBE_TAGS_KEY + ] kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = [] - #TODO: Evtl obsolet + # TODO: Evtl obsolet subIndexCounters = {} for streamObj in streamData: - + ffprobeCodecType = streamObj[MediaDescriptor.FFPROBE_CODEC_TYPE_KEY] trackType = TrackType.fromLabel(ffprobeCodecType) if trackType != TrackType.UNKNOWN: if trackType not in subIndexCounters.keys(): - subIndexCounters[trackType] = 0 + subIndexCounters[trackType] = 0 - kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(TrackDescriptor.fromFfprobe(streamObj, - subIndex=subIndexCounters[trackType])) + kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append( + TrackDescriptor.fromFfprobe( + streamObj, subIndex=subIndexCounters[trackType] + ) + ) subIndexCounters[trackType] += 1 - return cls(**kwargs) - def getTags(self): return self.__mediaTags - - def sortSubIndices(self, descriptors : List[TrackDescriptor]) -> List[TrackDescriptor]: + def sortSubIndices( + self, descriptors: List[TrackDescriptor] + ) -> List[TrackDescriptor]: subIndex = 0 for t in descriptors: - t.setSubIndex(subIndex) - subIndex += 1 - return 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]: - return [v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO] - + return [ + v for v in self.__trackDescriptors.copy() if v.getType() == TrackType.VIDEO + ] def getAudioTracks(self) -> List[TrackDescriptor]: - return [a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO] + return [ + a for a in self.__trackDescriptors.copy() if a.getType() == TrackType.AUDIO + ] def getSubtitleTracks(self) -> List[TrackDescriptor]: - return [s for s in self.__trackDescriptors.copy() if s.getType() == TrackType.SUBTITLE] + return [ + s + for s in self.__trackDescriptors.copy() + if s.getType() == TrackType.SUBTITLE + ] def getJellyfin(self): return self.__jellyfinOrder @@ -209,33 +284,33 @@ class MediaDescriptor(): def getClearTags(self): return self.__clearTags - - - def compare(self, vsMediaDescriptor : Self): + def compare(self, vsMediaDescriptor: Self): if not isinstance(vsMediaDescriptor, self.__class__): - raise click.ClickException(f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}") + raise click.ClickException( + f"MediaDescriptor.compare(): Argument is required to be of type { + self.__class__}" + ) vsTags = vsMediaDescriptor.getTags() tags = self.getTags() - - #HINT: Some tags differ per file, for example creation_time, so these are removed before diff + + # HINT: Some tags differ per file, for example creation_time, so these are removed before diff for emt in MediaDescriptor.EXCLUDED_MEDIA_TAGS: - if emt in tags.keys(): - del tags[emt] - if emt in vsTags.keys(): - del vsTags[emt] + if emt in tags.keys(): + del tags[emt] + if emt in vsTags.keys(): + del vsTags[emt] tagsDiff = dictDiff(vsTags, tags) compareResult = {} if tagsDiff: - compareResult[MediaDescriptor.TAGS_KEY] = tagsDiff - + compareResult[MediaDescriptor.TAGS_KEY] = tagsDiff # Target track configuration (from DB) - #tracks = self.getAllTrackDescriptors() + # tracks = self.getAllTrackDescriptors() tracks = self.getReorderedTrackDescriptors() numTracks = len(tracks) @@ -245,95 +320,154 @@ class MediaDescriptor(): maxNumOfTracks = max(numVsTracks, numTracks) - trackCompareResult = {} for tp in range(maxNumOfTracks): - - # inspect/update funktionier nur so - if self.__jellyfinOrder: - vsTrackIndex = tracks[tp].getSourceIndex() - else: - vsTrackIndex = tp - # vsTrackIndex = tracks[tp].getSourceIndex() - - # Will trigger if tracks are missing in file - if tp > (numVsTracks - 1): - if DIFF_ADDED_KEY not in trackCompareResult.keys(): - trackCompareResult[DIFF_ADDED_KEY] = set() - trackCompareResult[DIFF_ADDED_KEY].add(tracks[tp].getIndex()) - continue - - # Will trigger if tracks are missing in DB definition - # New tracks will be added per update via this way - if tp > (numTracks - 1): - if DIFF_REMOVED_KEY not in trackCompareResult.keys(): - trackCompareResult[DIFF_REMOVED_KEY] = {} - trackCompareResult[DIFF_REMOVED_KEY][vsTracks[vsTrackIndex].getIndex()] = vsTracks[vsTrackIndex] - continue - - # assumption is made here that the track order will not change for all files of a sequence - trackDiff = tracks[tp].compare(vsTracks[vsTrackIndex]) - - if trackDiff: - if DIFF_CHANGED_KEY not in trackCompareResult.keys(): - trackCompareResult[DIFF_CHANGED_KEY] = {} - trackCompareResult[DIFF_CHANGED_KEY][vsTracks[vsTrackIndex].getIndex()] = trackDiff + + # inspect/update funktionier nur so + if self.__jellyfinOrder: + vsTrackIndex = tracks[tp].getSourceIndex() + else: + vsTrackIndex = tp + # vsTrackIndex = tracks[tp].getSourceIndex() + + # Will trigger if tracks are missing in file + if tp > (numVsTracks - 1): + if DIFF_ADDED_KEY not in trackCompareResult.keys(): + trackCompareResult[DIFF_ADDED_KEY] = set() + trackCompareResult[DIFF_ADDED_KEY].add(tracks[tp].getIndex()) + continue + + # Will trigger if tracks are missing in DB definition + # New tracks will be added per update via this way + if tp > (numTracks - 1): + if DIFF_REMOVED_KEY not in trackCompareResult.keys(): + trackCompareResult[DIFF_REMOVED_KEY] = {} + trackCompareResult[DIFF_REMOVED_KEY][ + vsTracks[vsTrackIndex].getIndex() + ] = vsTracks[vsTrackIndex] + continue + + # assumption is made here that the track order will not change for all files of a sequence + trackDiff = tracks[tp].compare(vsTracks[vsTrackIndex]) + + if trackDiff: + if DIFF_CHANGED_KEY not in trackCompareResult.keys(): + trackCompareResult[DIFF_CHANGED_KEY] = {} + trackCompareResult[DIFF_CHANGED_KEY][ + vsTracks[vsTrackIndex].getIndex() + ] = trackDiff if trackCompareResult: - compareResult[MediaDescriptor.TRACKS_KEY] = trackCompareResult + compareResult[MediaDescriptor.TRACKS_KEY] = trackCompareResult return compareResult + def getImportFileTokens(self, use_sub_index: bool = True): + + reorderedTrackDescriptors = self.getReorderedTrackDescriptors() + importFileTokens = [] + + for rtd in reorderedTrackDescriptors: + + importedFilePath = rtd.getExternalSourceFilePath() + + if not importedFilePath is None: + importFileTokens += [ + "-i", + importedFilePath, + ] + return importFileTokens - def getInputMappingTokens(self, use_sub_index : bool = True): + def getInputMappingTokens(self, use_sub_index: bool = True): reorderedTrackDescriptors = self.getReorderedTrackDescriptors() inputMappingTokens = [] + filePointer = 1 for rtd in reorderedTrackDescriptors: - trackType = rtd.getType() - if use_sub_index: - inputMappingTokens += ['-map', f"0:{trackType.indicator()}:{rtd.getSubIndex()}"] - else: - inputMappingTokens += ['-map', f"0:{rtd.getIndex()}"] - - return inputMappingTokens + importedFilePath = rtd.getExternalSourceFilePath() - # matchingFileSubtitleDescriptors = sorted([d for d in availableFileSubtitleDescriptors if d['season'] == season and d['episode'] == episode], key=lambda d: d['stream']) if availableFileSubtitleDescriptors else [] + trackType = rtd.getType() + if use_sub_index: + if importedFilePath is None: + inputMappingTokens += [ + "-map", + f"0:{trackType.indicator()}:{rtd.getSubIndex()}", + ] + else: + inputMappingTokens += [ + "-map", + f"{filePointer}:{trackType.indicator()}:0", + ] + filePointer += 1 + else: + inputMappingTokens += ["-map", f"0:{rtd.getIndex()}"] + return inputMappingTokens def searchSubtitleFiles(searchDirectory, prefix): - - sesl_match = re.compile(FileProperties.SEASON_EPISODE_STREAM_LANGUAGE_MATCH) + + sesl_match = re.compile(MediaDescriptor.SEASON_EPISODE_STREAM_LANGUAGE_MATCH) availableFileSubtitleDescriptors = [] for subtitleFilename in os.listdir(searchDirectory): - if subtitleFilename.startswith(prefix) and subtitleFilename.endswith('.' + FileProperties.SUBTITLE_FILE_EXTENSION): + if subtitleFilename.startswith(prefix) and subtitleFilename.endswith( + "." + MediaDescriptor.SUBTITLE_FILE_EXTENSION + ): sesl_result = sesl_match.search(subtitleFilename) if sesl_result is not None: - subtitleFilePath = os.path.join(searchDirectory, subtitleFilename) + subtitleFilePath = os.path.join(searchDirectory, subtitleFilename) if os.path.isfile(subtitleFilePath): subtitleFileDescriptor = {} - subtitleFileDescriptor['path'] = subtitleFilePath - subtitleFileDescriptor['season'] = int(sesl_result.group(1)) - subtitleFileDescriptor['episode'] = int(sesl_result.group(2)) - subtitleFileDescriptor['stream'] = int(sesl_result.group(3)) - subtitleFileDescriptor['language'] = sesl_result.group(4) + subtitleFileDescriptor["path"] = subtitleFilePath + subtitleFileDescriptor["season"] = int(sesl_result.group(1)) + subtitleFileDescriptor["episode"] = int(sesl_result.group(2)) + subtitleFileDescriptor["stream"] = int(sesl_result.group(3)) + subtitleFileDescriptor["language"] = sesl_result.group(4) availableFileSubtitleDescriptors.append(subtitleFileDescriptor) - click.echo(f"Found {len(availableFileSubtitleDescriptors)} subtitles in files\n") + click.echo( + f"Found {len(availableFileSubtitleDescriptors)} subtitles in files\n" + ) return availableFileSubtitleDescriptors - - def importSubtitles(self, searchDirectory, prefix): - - availableFileSubtitleDescriptors = self.searchSubtitleFiles(searchDirectory, prefix) - - if len(availableFileSubtitleDescriptors) != len(self.getSubtitleTracks()): - raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks") + def importSubtitles( + self, searchDirectory, prefix, season: int = -1, episode: int = -1 + ): + + availableFileSubtitleDescriptors = self.searchSubtitleFiles( + searchDirectory, prefix + ) + + subtitleTracks = self.getSubtitleTracks() + + # if len(availableFileSubtitleDescriptors) != len(subtitleTracks): + # raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks") + + matchingFileSubtitleDescriptors = ( + sorted( + [ + d + for d in availableFileSubtitleDescriptors + if d["season"] == int(season) and d["episode"] == int(episode) + ], + key=lambda d: d["stream"], + ) + if availableFileSubtitleDescriptors + else [] + ) + + for mfsd in matchingFileSubtitleDescriptors: + matchingSubtitleTrackDescriptor = [ + s for s in subtitleTracks if s.getIndex() == mfsd["stream"] + ] + if matchingSubtitleTrackDescriptor: + matchingSubtitleTrackDescriptor[0].setExternalSourceFilePath( + mfsd["path"] + ) diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index 22cf3b6..84307f3 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -8,227 +8,287 @@ from .track_disposition import TrackDisposition from .helper import dictDiff, setDiff -class TrackDescriptor(): - - ID_KEY = 'id' - INDEX_KEY = 'index' - SOURCE_INDEX_KEY = 'source_index' - SUB_INDEX_KEY = 'sub_index' - PATTERN_ID_KEY = 'pattern_id' - - DISPOSITION_SET_KEY = 'disposition_set' - TAGS_KEY = 'tags' - - TRACK_TYPE_KEY = 'track_type' - AUDIO_LAYOUT_KEY = 'audio_layout' - - FFPROBE_INDEX_KEY = 'index' - FFPROBE_DISPOSITION_KEY = 'disposition' - FFPROBE_TAGS_KEY = 'tags' - FFPROBE_CODEC_TYPE_KEY = 'codec_type' - - def __init__(self, **kwargs): - - if TrackDescriptor.ID_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.ID_KEY]) is not int: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.ID_KEY} is required to be of type int") - self.__trackId = kwargs[TrackDescriptor.ID_KEY] - else: - self.__trackId = -1 - - if TrackDescriptor.PATTERN_ID_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.PATTERN_ID_KEY]) is not int: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.PATTERN_ID_KEY} is required to be of type int") - self.__patternId = kwargs[TrackDescriptor.PATTERN_ID_KEY] - else: - self.__patternId = -1 - - if TrackDescriptor.INDEX_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.INDEX_KEY]) is not int: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.INDEX_KEY} is required to be of type int") - self.__index = kwargs[TrackDescriptor.INDEX_KEY] - else: - self.__index = -1 - - if TrackDescriptor.SOURCE_INDEX_KEY in kwargs.keys() and type(kwargs[TrackDescriptor.SOURCE_INDEX_KEY]) is int: - self.__sourceIndex = kwargs[TrackDescriptor.SOURCE_INDEX_KEY] - else: - self.__sourceIndex = self.__index - - if TrackDescriptor.SUB_INDEX_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.SUB_INDEX_KEY]) is not int: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.SUB_INDEX_KEY} is required to be of type int") - self.__subIndex = kwargs[TrackDescriptor.SUB_INDEX_KEY] - else: - self.__subIndex = -1 - - if TrackDescriptor.TRACK_TYPE_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.TRACK_TYPE_KEY]) is not TrackType: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TRACK_TYPE_KEY} is required to be of type TrackType") - self.__trackType = kwargs[TrackDescriptor.TRACK_TYPE_KEY] - else: - self.__trackType = TrackType.UNKNOWN - - if TrackDescriptor.TAGS_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TAGS_KEY} is required to be of type dict") - self.__trackTags = kwargs[TrackDescriptor.TAGS_KEY] - else: - self.__trackTags = {} - - if TrackDescriptor.DISPOSITION_SET_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.DISPOSITION_SET_KEY]) is not set: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type set") - for d in kwargs[TrackDescriptor.DISPOSITION_SET_KEY]: - if type(d) is not TrackDisposition: - raise TypeError(f"TrackDesciptor.__init__(): All elements of argument set {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type TrackDisposition") - self.__dispositionSet = kwargs[TrackDescriptor.DISPOSITION_SET_KEY] - else: - self.__dispositionSet = set() - - if TrackDescriptor.AUDIO_LAYOUT_KEY in kwargs.keys(): - if type(kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY]) is not AudioLayout: - raise TypeError(f"TrackDesciptor.__init__(): Argument {TrackDescriptor.AUDIO_LAYOUT_KEY} is required to be of type AudioLayout") - self.__audioLayout = kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] - else: - self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED - - - @classmethod - def fromFfprobe(cls, streamObj, subIndex : int = -1): - """Processes ffprobe stream data as array with elements according to the following example +class TrackDescriptor: + + ID_KEY = "id" + INDEX_KEY = "index" + SOURCE_INDEX_KEY = "source_index" + SUB_INDEX_KEY = "sub_index" + PATTERN_ID_KEY = "pattern_id" + EXTERNAL_SOURCE_FILE_PATH_KEY = "external_source_file" + + DISPOSITION_SET_KEY = "disposition_set" + TAGS_KEY = "tags" + + TRACK_TYPE_KEY = "track_type" + AUDIO_LAYOUT_KEY = "audio_layout" + + FFPROBE_INDEX_KEY = "index" + FFPROBE_DISPOSITION_KEY = "disposition" + FFPROBE_TAGS_KEY = "tags" + FFPROBE_CODEC_TYPE_KEY = "codec_type" + + def __init__(self, **kwargs): + + if TrackDescriptor.ID_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.ID_KEY]) is not int: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.ID_KEY} is required to be of type int" + ) + self.__trackId = kwargs[TrackDescriptor.ID_KEY] + else: + self.__trackId = -1 + + if TrackDescriptor.PATTERN_ID_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.PATTERN_ID_KEY]) is not int: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.PATTERN_ID_KEY} is required to be of type int" + ) + self.__patternId = kwargs[TrackDescriptor.PATTERN_ID_KEY] + else: + self.__patternId = -1 + + if TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY]) is not str: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY} is required to be of type str" + ) + self.__externalSourceFilePath = kwargs[TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY] + else: + self.__externalSourceFilePath = None + + if TrackDescriptor.INDEX_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.INDEX_KEY]) is not int: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.INDEX_KEY} is required to be of type int" + ) + self.__index = kwargs[TrackDescriptor.INDEX_KEY] + else: + self.__index = -1 + + if ( + TrackDescriptor.SOURCE_INDEX_KEY in kwargs.keys() + and type(kwargs[TrackDescriptor.SOURCE_INDEX_KEY]) is int + ): + self.__sourceIndex = kwargs[TrackDescriptor.SOURCE_INDEX_KEY] + else: + self.__sourceIndex = self.__index + + if TrackDescriptor.SUB_INDEX_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.SUB_INDEX_KEY]) is not int: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.SUB_INDEX_KEY} is required to be of type int" + ) + self.__subIndex = kwargs[TrackDescriptor.SUB_INDEX_KEY] + else: + self.__subIndex = -1 + + if TrackDescriptor.TRACK_TYPE_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.TRACK_TYPE_KEY]) is not TrackType: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TRACK_TYPE_KEY} is required to be of type TrackType" + ) + self.__trackType = kwargs[TrackDescriptor.TRACK_TYPE_KEY] + else: + self.__trackType = TrackType.UNKNOWN + + if TrackDescriptor.TAGS_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.TAGS_KEY} is required to be of type dict" + ) + self.__trackTags = kwargs[TrackDescriptor.TAGS_KEY] + else: + self.__trackTags = {} + + if TrackDescriptor.DISPOSITION_SET_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.DISPOSITION_SET_KEY]) is not set: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type set" + ) + for d in kwargs[TrackDescriptor.DISPOSITION_SET_KEY]: + if type(d) is not TrackDisposition: + raise TypeError( + f"TrackDesciptor.__init__(): All elements of argument set {TrackDescriptor.DISPOSITION_SET_KEY} is required to be of type TrackDisposition" + ) + self.__dispositionSet = kwargs[TrackDescriptor.DISPOSITION_SET_KEY] + else: + self.__dispositionSet = set() + + if TrackDescriptor.AUDIO_LAYOUT_KEY in kwargs.keys(): + if type(kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY]) is not AudioLayout: + raise TypeError( + f"TrackDesciptor.__init__(): Argument {TrackDescriptor.AUDIO_LAYOUT_KEY} is required to be of type AudioLayout" + ) + self.__audioLayout = kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] + else: + self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED + + @classmethod + def fromFfprobe(cls, streamObj, subIndex: int = -1): + """Processes ffprobe stream data as array with elements according to the following example + { + "index": 4, + "codec_name": "hdmv_pgs_subtitle", + "codec_long_name": "HDMV Presentation Graphic Stream subtitles", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1421035, + "duration": "1421.035000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "non_diegetic": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "ger", + "title": "German Full" + } + } + """ + + trackType = ( + TrackType.fromLabel(streamObj["codec_type"]) + if "codec_type" in streamObj.keys() + else TrackType.UNKNOWN + ) + + if trackType != TrackType.UNKNOWN: + + kwargs = {} + + kwargs[TrackDescriptor.INDEX_KEY] = ( + int(streamObj[TrackDescriptor.FFPROBE_INDEX_KEY]) + if TrackDescriptor.FFPROBE_INDEX_KEY in streamObj.keys() + else -1 + ) + kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = kwargs[TrackDescriptor.INDEX_KEY] + kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex + + kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType + kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = ( { - "index": 4, - "codec_name": "hdmv_pgs_subtitle", - "codec_long_name": "HDMV Presentation Graphic Stream subtitles", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1421035, - "duration": "1421.035000", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "ger", - "title": "German Full" + t + for d in ( + k + for (k, v) in streamObj[ + TrackDescriptor.FFPROBE_DISPOSITION_KEY + ].items() + if v + ) + if (t := TrackDisposition.find(d)) is not None } - } - """ - - trackType = TrackType.fromLabel(streamObj['codec_type']) if 'codec_type' in streamObj.keys() else TrackType.UNKNOWN - - if trackType != TrackType.UNKNOWN: - - kwargs = {} - - kwargs[TrackDescriptor.INDEX_KEY] = int(streamObj[TrackDescriptor.FFPROBE_INDEX_KEY]) if TrackDescriptor.FFPROBE_INDEX_KEY in streamObj.keys() else -1 - kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = kwargs[TrackDescriptor.INDEX_KEY] - kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex - - kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType - kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = {t for d in (k for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items() if v) - if (t := TrackDisposition.find(d)) if t is not None} if TrackDescriptor.FFPROBE_DISPOSITION_KEY in streamObj.keys() else set() - kwargs[TrackDescriptor.TAGS_KEY] = streamObj[TrackDescriptor.FFPROBE_TAGS_KEY] if TrackDescriptor.FFPROBE_TAGS_KEY in streamObj.keys() else {} - kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.identify(streamObj) if trackType == TrackType.AUDIO else AudioLayout.LAYOUT_UNDEFINED - - return cls(**kwargs) - else: - return None - + if TrackDescriptor.FFPROBE_DISPOSITION_KEY in streamObj.keys() + else set() + ) + kwargs[TrackDescriptor.TAGS_KEY] = ( + streamObj[TrackDescriptor.FFPROBE_TAGS_KEY] + if TrackDescriptor.FFPROBE_TAGS_KEY in streamObj.keys() + else {} + ) + kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = ( + AudioLayout.identify(streamObj) + if trackType == TrackType.AUDIO + else AudioLayout.LAYOUT_UNDEFINED + ) - def getId(self): - return self.__trackId + return cls(**kwargs) + else: + return None - def getPatternId(self): - return self.__patternId + def getId(self): + return self.__trackId - def getIndex(self): - return self.__index + def getPatternId(self): + return self.__patternId - def getSourceIndex(self): - return self.__sourceIndex + def getIndex(self): + return self.__index + def getSourceIndex(self): + return self.__sourceIndex - def getSubIndex(self): - return self.__subIndex + def getSubIndex(self): + return self.__subIndex - def setSubIndex(self, subIndex): - self.__subIndex = subIndex + def setSubIndex(self, subIndex): + self.__subIndex = subIndex - def getType(self): - return self.__trackType + def getType(self): + return self.__trackType - def getLanguage(self): - if 'language' in self.__trackTags.keys(): - return IsoLanguage.findThreeLetter(self.__trackTags['language']) - else: - return IsoLanguage.UNDEFINED + def getLanguage(self): + if "language" in self.__trackTags.keys(): + return IsoLanguage.findThreeLetter(self.__trackTags["language"]) + else: + return IsoLanguage.UNDEFINED - def getTitle(self): - if 'title' in self.__trackTags.keys(): - return str(self.__trackTags['title']) - else: - return '' + def getTitle(self): + if "title" in self.__trackTags.keys(): + return str(self.__trackTags["title"]) + else: + return "" - def getAudioLayout(self): - return self.__audioLayout + def getAudioLayout(self): + return self.__audioLayout + def getTags(self): + return self.__trackTags - def getTags(self): - return self.__trackTags - - def getDispositionSet(self): - return self.__dispositionSet + def getDispositionSet(self): + return self.__dispositionSet + def getDispositionFlag(self, disposition: TrackDisposition) -> bool: + return bool(disposition in 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 setDispositionFlag(self, disposition : TrackDisposition, state : bool): - if state: - self.__dispositionSet.add(disposition) - else: - self.__dispositionSet.discard(disposition) + def compare(self, vsTrackDescriptor): + compareResult = {} - def compare(self, vsTrackDescriptor): + tagsDiffResult = dictDiff(vsTrackDescriptor.getTags(), self.getTags()) - compareResult = {} + if tagsDiffResult: + compareResult[TrackDescriptor.TAGS_KEY] = tagsDiffResult - tagsDiffResult = dictDiff(vsTrackDescriptor.getTags(), self.getTags()) + vsDispositions = vsTrackDescriptor.getDispositionSet() + dispositions = self.getDispositionSet() - if tagsDiffResult: - compareResult[TrackDescriptor.TAGS_KEY] = tagsDiffResult + dispositionDiffResult = setDiff(vsDispositions, dispositions) - vsDispositions = vsTrackDescriptor.getDispositionSet() - dispositions = self.getDispositionSet() + if dispositionDiffResult: + compareResult[TrackDescriptor.DISPOSITION_SET_KEY] = dispositionDiffResult - dispositionDiffResult = setDiff(vsDispositions, dispositions) + return compareResult - if dispositionDiffResult: - compareResult[TrackDescriptor.DISPOSITION_SET_KEY] = dispositionDiffResult + def setExternalSourceFilePath(self, filePath: str): + self.__externalSourceFilePath = str(filePath) - return compareResult + def getExternalSourceFilePath(self): + return self.__externalSourceFilePath