diff --git a/bin/ffx.py b/bin/ffx.py index f3af68b..8efed55 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -3,7 +3,7 @@ import os, sys, subprocess, json, click, time, re from ffx.ffx_app import FfxApp - +from ffx.ffx_controller import FfxController from ffx.database import databaseContext @@ -169,14 +169,14 @@ def shows(ctx): @click.argument('paths', nargs=-1) @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') -@click.option('-v', '--video-encoder', type=str, default=DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1) default: {DEFAULT_VIDEO_ENCODER}") +@click.option('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1) default: {FfxController.DEFAULT_VIDEO_ENCODER}") -@click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {DEFAULT_QUALITY})") -@click.option('-p', '--preset', type=str, default=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {DEFAULT_AV1_PRESET})") +@click.option('-q', '--quality', type=str, default=FfxController.DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {FfxController.DEFAULT_QUALITY})") +@click.option('-p', '--preset', type=str, default=FfxController.DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder (default: {FfxController.DEFAULT_AV1_PRESET})") -@click.option('-a', '--stereo-bitrate', type=int, default=DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {DEFAULT_STEREO_BANDWIDTH})") -@click.option('-ac3', '--ac3-bitrate', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {DEFAULT_AC3_BANDWIDTH})") -@click.option('-dts', '--dts-bitrate', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {DEFAULT_DTS_BANDWIDTH})") +@click.option('-a', '--stereo-bitrate', type=int, default=FfxController.DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams (default: {FfxController.DEFAULT_STEREO_BANDWIDTH})") +@click.option('-ac3', '--ac3-bitrate', type=int, default=FfxController.DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams (default: {FfxController.DEFAULT_AC3_BANDWIDTH})") +@click.option('-dts', '--dts-bitrate', type=int, default=FfxController.DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams (default: {FfxController.DEFAULT_DTS_BANDWIDTH})") @click.option('-sd', '--subtitle-directory', type=str, default='', help='Load subtitles from here') @click.option('-sp', '--subtitle-prefix', type=str, default='', help='Subtitle filename prefix') @@ -297,8 +297,8 @@ def convert(ctx, if cTokens and len(cTokens) == 2: cropStart, cropLength = crop.split(',') else: - cropStart = DEFAULT_CROP_START - cropLength = DEFAULT_CROP_LENGTH + cropStart = FfxController.DEFAULT_CROP_START + cropLength = FfxController.DEFAULT_CROP_LENGTH click.echo(f"crop start={cropStart} length={cropLength}") @@ -359,7 +359,7 @@ def convert(ctx, # Assemble target filename tokens targetFilenameTokens = [] - targetFilenameExtension = DEFAULT_FILE_EXTENSION + targetFilenameExtension = FfxController.DEFAULT_FILE_EXTENSION if label: targetFilenameTokens = [label] diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index fcbd41d..8bbefb0 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -3,10 +3,13 @@ import click from typing import List, Self from ffx.track_type import TrackType +from ffx.track_disposition import TrackDisposition + from ffx.track_descriptor import TrackDescriptor from ffx.helper import dictDiff, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY + class MediaDescriptor(): """This class represents the structural content of a media file including streams and metadata""" @@ -14,11 +17,12 @@ class MediaDescriptor(): TRACKS_KEY = 'tracks' TRACK_DESCRIPTOR_LIST_KEY = 'track_descriptors' - CLEAR_TAGS_KEY = 'clear_tags' + CLEAR_TAGS_FLAG_KEY = 'clear_tags' FFPROBE_DISPOSITION_KEY = 'disposition' FFPROBE_TAGS_KEY = 'tags' FFPROBE_CODEC_TYPE_KEY = 'codec_type' + JELLYFIN_ORDER_FLAG_KEY = 'jellyfin_order' def __init__(self, **kwargs): @@ -42,14 +46,64 @@ class MediaDescriptor(): else: self.__trackDescriptors = [] - if MediaDescriptor.CLEAR_TAGS_KEY in kwargs.keys(): - if type(kwargs[MediaDescriptor.CLEAR_TAGS_KEY]) is not bool: - raise TypeError(f"MediaDescriptor.__init__(): Argument {MediaDescriptor.CLEAR_TAGS_KEY} is required to be of type bool") - self.__clearTags = kwargs[MediaDescriptor.CLEAR_TAGS_KEY] + 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] else: 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] + else: + self.__jellyfinOrder = False + + + 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] + + 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()] + + if len(videoDefaultTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default video track is not supported') + if len(videoForcedTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced video track is not supported') + if len(audioDefaultTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default audio track is not supported') + if len(audioForcedTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced audio track is not supported') + if len(subtitleDefaultTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one default subtitle track is not supported') + if len(subtitleForcedTracks) > 1: + raise ValueError('MediaDescriptor.__getSourceIndexOrder(): More than one forced subtitle track is not supported') + + 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]))) + + reorderedTrackDescriptors = videoTracks + audioTracks + subtitleTracks + 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") + + return reorderedTrackDescriptors + + @classmethod def fromFfprobe(cls, formatData, streamData): @@ -72,7 +126,8 @@ class MediaDescriptor(): if trackType not in subIndexCounters.keys(): 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 @@ -112,7 +167,8 @@ class MediaDescriptor(): # Target track configuration (from DB) - tracks = self.getAllTracks() + #tracks = self.getAllTracks() + tracks = self.__getReorderedTrackDescriptors() numTracks = len(tracks) # Current track configuration (of file) @@ -134,6 +190,7 @@ class MediaDescriptor(): 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] = {} diff --git a/bin/ffx/media_details_screen.py b/bin/ffx/media_details_screen.py index 0791c70..72fa1e0 100644 --- a/bin/ffx/media_details_screen.py +++ b/bin/ffx/media_details_screen.py @@ -158,6 +158,8 @@ 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): @@ -242,6 +244,7 @@ class MediaDetailsScreen(Screen): row = (' ', '', ' ') # Convert each element to a string before adding self.showsTable.add_row(*map(str, row)) + #TODO: Stürzt ab wenn keine Shows vorhanden sind. Onthefly Show add impl for show in self.__sc.getAllShows(): row = (int(show.id), show.name, show.year) # Convert each element to a string before adding self.showsTable.add_row(*map(str, row)) diff --git a/bin/ffx/model/track.py b/bin/ffx/model/track.py index 6e1f5e4..6e70176 100644 --- a/bin/ffx/model/track.py +++ b/bin/ffx/model/track.py @@ -32,7 +32,7 @@ class Track(Base): track_type = Column(Integer) # TrackType index = Column(Integer) - # sub_index = Column(Integer) + source_index = Column(Integer) # v1.x pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE")) @@ -149,6 +149,9 @@ class Track(Base): def getIndex(self): return int(self.index) if self.index is not None else -1 + def getSourceIndex(self): + return int(self.source_index) if self.source_index is not None else -1 + def getLanguage(self): tags = {t.key:t.value for t in self.track_tags} return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED @@ -182,6 +185,7 @@ class Track(Base): kwargs[TrackDescriptor.PATTERN_ID_KEY] = self.getPatternId() kwargs[TrackDescriptor.INDEX_KEY] = self.getIndex() + kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = self.getSourceIndex() if subIndex > -1: kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex diff --git a/bin/ffx/track_controller.py b/bin/ffx/track_controller.py index e118712..1df585e 100644 --- a/bin/ffx/track_controller.py +++ b/bin/ffx/track_controller.py @@ -31,6 +31,7 @@ class TrackController(): track = Track(pattern_id = patId, track_type = int(trackDescriptor.getType().index()), index = int(trackDescriptor.getIndex()), + source_index = int(trackDescriptor.getSourceIndex()), disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))) s.add(track) diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index f57609d..ec7b138 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -10,6 +10,7 @@ class TrackDescriptor(): ID_KEY = 'id' INDEX_KEY = 'index' + SOURCE_INDEX_KEY = 'source_index' SUB_INDEX_KEY = 'sub_index' PATTERN_ID_KEY = 'pattern_id' @@ -46,6 +47,11 @@ class TrackDescriptor(): 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") @@ -136,6 +142,7 @@ class TrackDescriptor(): 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 @@ -158,6 +165,10 @@ class TrackDescriptor(): def getIndex(self): return self.__index + def getSourceIndex(self): + return self.__sourceIndex + + def getSubIndex(self): return self.__subIndex