click-textual
Maveno 12 months ago
parent bcb4e4d3d6
commit 93cc8a23c9

@ -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()])

@ -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():
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

@ -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):

Loading…
Cancel
Save