From 93cc8a23c9e071c2af8eb3fd54d3ec38a76c0ffb Mon Sep 17 00:00:00 2001 From: Maveno Date: Sun, 13 Oct 2024 21:37:28 +0200 Subject: [PATCH] nightly --- bin/ffx.py | 162 +---------------------- bin/ffx/ffx_controller.py | 255 +++++++++++++++++++++++++++++++++++- bin/ffx/media_descriptor.py | 3 +- 3 files changed, 259 insertions(+), 161 deletions(-) diff --git a/bin/ffx.py b/bin/ffx.py index 35ff523..f3af68b 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -9,37 +9,6 @@ from ffx.database import databaseContext VERSION='0.1.0' -DEFAULT_VIDEO_ENCODER = 'vp9' - -DEFAULT_QUALITY = 23 -DEFAULT_AV1_PRESET = 5 - -DEFAULT_FILE_FORMAT = 'webm' -DEFAULT_FILE_EXTENSION = 'webm' - -DEFAULT_STEREO_BANDWIDTH = "128" -DEFAULT_AC3_BANDWIDTH = "256" -DEFAULT_DTS_BANDWIDTH = "320" - -DEFAULT_CROP_START = 60 -DEFAULT_CROP_LENGTH = 180 - -TEMP_FILE_NAME = "ffmpeg2pass-0.log" - - -MKVMERGE_METADATA_KEYS = ['BPS', - 'NUMBER_OF_FRAMES', - 'NUMBER_OF_BYTES', - '_STATISTICS_WRITING_APP', - '_STATISTICS_WRITING_DATE_UTC', - '_STATISTICS_TAGS'] - -FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm'] - - -COMMAND_TOKENS = ['ffmpeg', '-y'] -NULL_TOKENS = ['-f', 'null', '/dev/null'] # -f null /dev/null - STREAM_TYPE_VIDEO = 'video' STREAM_TYPE_AUDIO = 'audio' STREAM_TYPE_SUBTITLE = 'subtitle' @@ -51,6 +20,9 @@ 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""" @@ -62,134 +34,6 @@ def getModifiedStreamOrder(length, last): return seq -# def getReorderedSubstreams(subDescriptor, last): -# numSubStreams = len(subDescriptor) -# modifiedOrder = getModifiedStreamOrder(numSubStreams, last) -# reorderedDescriptor = [] -# for streamIndex in range(numSubStreams): -# reorderedDescriptor.append(subDescriptor[modifiedOrder[streamIndex]]) -# return reorderedDescriptor - - -def generateAV1Tokens(q, p): - - return ['-c:v:0', 'libsvtav1', - '-svtav1-params', f"crf={q}:preset={p}:tune=0:enable-overlays=1:scd=1:scm=0", - '-pix_fmt', 'yuv420p10le'] - - -# -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 1 -speed 4 -frame-parallel 0 -g 9999 -aq-mode 0 -def generateVP9Pass1Tokens(q): - - return ['-c:v:0', 'libvpx-vp9', - '-row-mt', '1', - '-crf', str(q), - '-pass', '1', - '-speed', '4', - '-frame-parallel', '0', - '-g', '9999', - '-aq-mode', '0'] - -# -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 2 -frame-parallel 0 -g 9999 -aq-mode 0 -auto-alt-ref 1 -lag-in-frames 25 -def generateVP9Pass2Tokens(q): - - return ['-c:v:0', 'libvpx-vp9', - '-row-mt', '1', - '-crf', str(q), - '-pass', '2', - '-frame-parallel', '0', - '-g', '9999', - '-aq-mode', '0', - '-auto-alt-ref', '1', - '-lag-in-frames', '25'] - - -def generateCropTokens(start, length): - - return ['-ss', str(start), '-t', str(length)] - - -def generateDenoiseTokens(spatial=5, patch=7, research=7, hw=False): - filterName = 'nlmeans_opencl' if hw else 'nlmeans' - return ['-vf', f"{filterName}=s={spatial}:p={patch}:r={research}"] - - -def generateOutputTokens(filepath, format, ext): - return ['-f', format, f"{filepath}.{ext}"] - - -def generateAudioEncodingTokens(context, index, layout): - """Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate""" - pass -# -# if layout == STREAM_LAYOUT_6_1: -# return [f"-c:a:{index}", -# 'libopus', -# f"-filter:a:{index}", -# 'channelmap=channel_layout=6.1', -# f"-b:a:{index}", -# context['bitrates']['dts']] -# -# elif layout == STREAM_LAYOUT_5_1: -# return [f"-c:a:{index}", -# 'libopus', -# f"-filter:a:{index}", -# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", -# f"-b:a:{index}", -# context['bitrates']['ac3']] -# -# elif layout == STREAM_LAYOUT_STEREO: -# return [f"-c:a:{index}", -# 'libopus', -# f"-b:a:{index}", -# context['bitrates']['stereo']] -# -# elif layout == STREAM_LAYOUT_6CH: -# return [f"-c:a:{index}", -# 'libopus', -# f"-filter:a:{index}", -# "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", -# f"-b:a:{index}", -# context['bitrates']['ac3']] -# else: -# return [] - - -def generateClearTokens(streams): - clearTokens = [] - for s in streams: - for k in MKVMERGE_METADATA_KEYS: - clearTokens += [f"-metadata:s:{s['type'][0]}:{s['sub_index']}", f"{k}="] - return clearTokens - - -def getDispositionFlags(subStreamDescriptor): - return {k for (k,v) in subStreamDescriptor['disposition'].items() if v == 1} if 'disposition' in subStreamDescriptor.keys() else set() - - - -# def generateDispositionTokens(subDescriptor): -def generateDispositionTokens(subDescriptor, modifyOrder = []): - """-disposition:s:X default+forced""" - - dispositionTokens = [] - - for subStreamIndex in range(len(subDescriptor)): - - sourceSubStreamIndex = modifyOrder[subStreamIndex] if modifyOrder else subStreamIndex - - subStream = subDescriptor[sourceSubStreamIndex] - - streamType = subStream['codec_type'][0] # v|a|s - dispositionFlags = getDispositionFlags(subStream) - - if dispositionFlags: - dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '+'.join(dispositionFlags)] - else: - dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '0'] - - return dispositionTokens - # def countStreamDispositions(subStreamDescriptor): # return len([l for (k,v) in subStreamDescriptor['disposition'].items()]) diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index 6892bae..4374737 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -1,2 +1,255 @@ +import click + +from ffx.media_descriptor import MediaDescriptor +from ffx.helper import DIFF_ADDED_KEY, DIFF_REMOVED_KEY, DIFF_CHANGED_KEY +from ffx.track_descriptor import TrackDescriptor +from ffx.model.track import Track + + class FfxController(): - pass + + COMMAND_TOKENS = ['ffmpeg', '-y'] + NULL_TOKENS = ['-f', 'null', '/dev/null'] # -f null /dev/null + + TEMP_FILE_NAME = "ffmpeg2pass-0.log" + + DEFAULT_VIDEO_ENCODER = 'vp9' + + DEFAULT_QUALITY = 23 + DEFAULT_AV1_PRESET = 5 + + DEFAULT_FILE_FORMAT = 'webm' + DEFAULT_FILE_EXTENSION = 'webm' + + DEFAULT_STEREO_BANDWIDTH = "128" + DEFAULT_AC3_BANDWIDTH = "256" + DEFAULT_DTS_BANDWIDTH = "320" + + DEFAULT_CROP_START = 60 + DEFAULT_CROP_LENGTH = 180 + + MKVMERGE_METADATA_KEYS = ['BPS', + 'NUMBER_OF_FRAMES', + 'NUMBER_OF_BYTES', + '_STATISTICS_WRITING_APP', + '_STATISTICS_WRITING_DATE_UTC', + '_STATISTICS_TAGS'] + + + + # def getReorderedSubstreams(subDescriptor, last): + # numSubStreams = len(subDescriptor) + # modifiedOrder = getModifiedStreamOrder(numSubStreams, last) + # reorderedDescriptor = [] + # for streamIndex in range(numSubStreams): + # reorderedDescriptor.append(subDescriptor[modifiedOrder[streamIndex]]) + # return reorderedDescriptor + + + def generateAV1Tokens(self, quality, preset): + + return ['-c:v:0', 'libsvtav1', + '-svtav1-params', f"crf={quality}:preset={preset}:tune=0:enable-overlays=1:scd=1:scm=0", + '-pix_fmt', 'yuv420p10le'] + + + # -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 1 -speed 4 -frame-parallel 0 -g 9999 -aq-mode 0 + def generateVP9Pass1Tokens(self, quality): + + return ['-c:v:0', 'libvpx-vp9', + '-row-mt', '1', + '-crf', str(quality), + '-pass', '1', + '-speed', '4', + '-frame-parallel', '0', + '-g', '9999', + '-aq-mode', '0'] + + # -c:v:0 libvpx-vp9 -row-mt 1 -crf 32 -pass 2 -frame-parallel 0 -g 9999 -aq-mode 0 -auto-alt-ref 1 -lag-in-frames 25 + def generateVP9Pass2Tokens(self, quality): + + return ['-c:v:0', 'libvpx-vp9', + '-row-mt', '1', + '-crf', str(quality), + '-pass', '2', + '-frame-parallel', '0', + '-g', '9999', + '-aq-mode', '0', + '-auto-alt-ref', '1', + '-lag-in-frames', '25'] + + + def generateCropTokens(self, start, length): + + return ['-ss', str(start), '-t', str(length)] + + + def generateDenoiseTokens(self, spatial=5, patch=7, research=7, hw=False): + filterName = 'nlmeans_opencl' if hw else 'nlmeans' + return ['-vf', f"{filterName}=s={spatial}:p={patch}:r={research}"] + + + def generateOutputTokens(self, filepath, format, ext): + return ['-f', format, f"{filepath}.{ext}"] + + + def generateAudioEncodingTokens(self, context, index, layout): + """Generates ffmpeg options for one output audio stream including channel remapping, codec and bitrate""" + pass + # + # if layout == STREAM_LAYOUT_6_1: + # return [f"-c:a:{index}", + # 'libopus', + # f"-filter:a:{index}", + # 'channelmap=channel_layout=6.1', + # f"-b:a:{index}", + # context['bitrates']['dts']] + # + # elif layout == STREAM_LAYOUT_5_1: + # return [f"-c:a:{index}", + # 'libopus', + # f"-filter:a:{index}", + # "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", + # f"-b:a:{index}", + # context['bitrates']['ac3']] + # + # elif layout == STREAM_LAYOUT_STEREO: + # return [f"-c:a:{index}", + # 'libopus', + # f"-b:a:{index}", + # context['bitrates']['stereo']] + # + # elif layout == STREAM_LAYOUT_6CH: + # return [f"-c:a:{index}", + # 'libopus', + # f"-filter:a:{index}", + # "channelmap=FL-FL|FR-FR|FC-FC|LFE-LFE|SL-BL|SR-BR:5.1", + # f"-b:a:{index}", + # context['bitrates']['ac3']] + # else: + # return [] + + + def generateClearTokens(self, streams): + clearTokens = [] + for s in streams: + for k in FfxController.MKVMERGE_METADATA_KEYS: + clearTokens += [f"-metadata:s:{s['type'][0]}:{s['sub_index']}", f"{k}="] + return clearTokens + + + def getDispositionFlags(self, subStreamDescriptor): + return {k for (k,v) in subStreamDescriptor['disposition'].items() if v == 1} if 'disposition' in subStreamDescriptor.keys() else set() + + + + # def generateDispositionTokens(subDescriptor): + def generateDispositionTokens(self, subDescriptor, modifyOrder = []): + """-disposition:s:X default+forced""" + + dispositionTokens = [] + + for subStreamIndex in range(len(subDescriptor)): + + sourceSubStreamIndex = modifyOrder[subStreamIndex] if modifyOrder else subStreamIndex + + subStream = subDescriptor[sourceSubStreamIndex] + + streamType = subStream['codec_type'][0] # v|a|s + dispositionFlags = self.getDispositionFlags(subStream) + + if dispositionFlags: + dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '+'.join(dispositionFlags)] + else: + dispositionTokens += [f"-disposition:{streamType}:{subStreamIndex}", '0'] + + return dispositionTokens + + + + def generateMappingTokensFromDescriptors(self, sourceMediaDescriptor : MediaDescriptor, targetMediaDescriptor : MediaDescriptor): + + mappingTokens = [] + + mediaDifferences = targetMediaDescriptor.compare(sourceMediaDescriptor) + + if MediaDescriptor.TAGS_KEY in mediaDifferences.keys(): + + sourceTags = sourceMediaDescriptor.getTags() + targetTags = targetMediaDescriptor.getTags() + + if DIFF_ADDED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys(): + for addedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]: + # row = (f"added media tag: key='{addedTagKey}' value='{targetTags[addedTagKey]}'",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if DIFF_REMOVED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys(): + for removedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]: + # row = (f"removed media tag: key='{removedTagKey}' value='{sourceTags[removedTagKey]}'",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if DIFF_CHANGED_KEY in mediaDifferences[MediaDescriptor.TAGS_KEY].keys(): + for changedTagKey in mediaDifferences[MediaDescriptor.TAGS_KEY][DIFF_CHANGED_KEY]: + # row = (f"changed media tag: key='{changedTagKey}' value='{sourceTags[changedTagKey]}'->'{targetTags[changedTagKey]}'",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if MediaDescriptor.TRACKS_KEY in mediaDifferences.keys(): + + sourceTracks = sourceMediaDescriptor.getAllTracks() # 0,1,2,3 + targetTracks = targetMediaDescriptor.getAllTracks() # 0 <- from DB + + if DIFF_ADDED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys(): + addedTracksIndices = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY] + raise click.ClickException(f"FfxController.generateMappingTokensFromDescriptors(): Adding tracks is not supported. Track indices {addedTracksIndices}") + + #raise click.ClickException(f"add track {mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]}") + #for addedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY]: + #addedTrack : Track = targetTracks[addedTrackIndex] + # row = (f"added {addedTrack.getType().label()} track: index={addedTrackIndex} lang={addedTrack.getLanguage().threeLetter()}",) + # self.differencesTable.add_row(*map(str, row)) + + if DIFF_REMOVED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys(): + removedTracksIndices = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_ADDED_KEY].keys() + raise click.ClickException(f"FfxController.generateMappingTokensFromDescriptors(): Removing tracks is not supported. Track indices {removedTracksIndices}") + #for removedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_REMOVED_KEY]: + # row = (f"removed track: index={removedTrackIndex}",) + # self.differencesTable.add_row(*map(str, row)) + + if DIFF_CHANGED_KEY in mediaDifferences[MediaDescriptor.TRACKS_KEY].keys(): + for changedTrackIndex in mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY].keys(): + + changedTrack : Track = targetTracks[changedTrackIndex] + changedTrackDiff : dict = mediaDifferences[MediaDescriptor.TRACKS_KEY][DIFF_CHANGED_KEY][changedTrackIndex] + + if MediaDescriptor.TAGS_KEY in changedTrackDiff.keys(): + + if DIFF_ADDED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]: + for addedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_ADDED_KEY]: + addedTagValue = changedTrack.getTags()[addedTagKey] + # row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} added key={addedTagKey} value={addedTagValue}",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if DIFF_REMOVED_KEY in changedTrackDiff[MediaDescriptor.TAGS_KEY]: + for removedTagKey in changedTrackDiff[MediaDescriptor.TAGS_KEY][DIFF_REMOVED_KEY]: + # row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} removed key={removedTagKey}",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if TrackDescriptor.DISPOSITION_SET_KEY in changedTrackDiff.keys(): + + if DIFF_ADDED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]: + for addedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_ADDED_KEY]: + # row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} added disposition={addedDisposition.label()}",) + # self.differencesTable.add_row(*map(str, row)) + pass + + if DIFF_REMOVED_KEY in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY]: + for removedDisposition in changedTrackDiff[TrackDescriptor.DISPOSITION_SET_KEY][DIFF_REMOVED_KEY]: + # row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} removed disposition={removedDisposition.label()}",) + # self.differencesTable.add_row(*map(str, row)) + pass + diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index 38286b0..fcbd41d 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -95,7 +95,8 @@ class MediaDescriptor(): def getSubtitleTracks(self) -> List[TrackDescriptor]: return [d for d in self.__trackDescriptors if d.getType() == TrackType.SUBTITLE] - + def getClearTags(self): + return self.__clearTags def compare(self, vsMediaDescriptor : Self):