#393 CLI-Overrides

main
Maveno 11 months ago
parent feb5441251
commit b16e76370b

2
.gitignore vendored

@ -1,4 +1,4 @@
__pycache__ __pycache__
junk/ junk/
.vscode/launch.json .vscode
.ipynb_checkpoints/ .ipynb_checkpoints/

@ -17,6 +17,7 @@ from ffx.track_descriptor import TrackDescriptor
from ffx.track_type import TrackType from ffx.track_type import TrackType
from ffx.video_encoder import VideoEncoder from ffx.video_encoder import VideoEncoder
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
from ffx.nlmeans_controller import NlmeansController
from ffx.process import executeProcess from ffx.process import executeProcess
from ffx.helper import filterFilename from ffx.helper import filterFilename
@ -262,38 +263,42 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
@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('-v', '--video-encoder', type=str, default=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1)", show_default=True)
@click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder (default: {DEFAULT_QUALITY})") @click.option('-q', '--quality', type=str, default=DEFAULT_QUALITY, help=f"Quality settings to be used with VP9 encoder", show_default=True)
@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('-p', '--preset', type=str, default=DEFAULT_AV1_PRESET, help=f"Quality preset to be used with AV1 encoder", show_default=True)
@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('-a', '--stereo-bitrate', type=int, default=DEFAULT_STEREO_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode stereo audio streams", show_default=True)
@click.option('--ac3', 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('--ac3', type=int, default=DEFAULT_AC3_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 5.1 audio streams", show_default=True)
@click.option('--dts', 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('--dts', type=int, default=DEFAULT_DTS_BANDWIDTH, help=f"Bitrate in kbit/s to be used to encode 6.1 audio streams", show_default=True)
@click.option('--subtitle-directory', type=str, default='', help='Load subtitles from here') @click.option('--subtitle-directory', type=str, default='', help='Load subtitles from here')
@click.option('--subtitle-prefix', type=str, default='', help='Subtitle filename prefix') @click.option('--subtitle-prefix', type=str, default='', help='Subtitle filename prefix')
@click.option('--language', type=str, multiple=True, help='Set stream language. Use format <stream index>:<3 letter iso code>')
@click.option('--title', type=str, multiple=True, help='Set stream title. Use format <stream index>:<title>')
@click.option('--audio-language', type=str, multiple=True, help='Audio stream language(s)') @click.option('--default-video', type=int, default=-1, help='Index of default video stream')
@click.option('--audio-title', type=str, multiple=True, help='Audio stream title(s)') @click.option('--forced-video', type=int, default=-1, help='Index of forced video stream')
@click.option('--default-audio', type=int, default=-1, help='Index of default audio stream') @click.option('--default-audio', type=int, default=-1, help='Index of default audio stream')
@click.option('--forced-audio', type=int, default=-1, help='Index of forced audio stream') @click.option('--forced-audio', type=int, default=-1, help='Index of forced audio stream')
@click.option('--subtitle-language', type=str, multiple=True, help='Subtitle stream language(s)')
@click.option('--subtitle-title', type=str, multiple=True, help='Subtitle stream title(s)')
@click.option('--default-subtitle', type=int, default=-1, help='Index of default subtitle stream') @click.option('--default-subtitle', type=int, default=-1, help='Index of default subtitle stream')
@click.option('--forced-subtitle', type=int, default=-1, help='Index of forced subtitle stream') # (including default audio stream tag) @click.option('--forced-subtitle', type=int, default=-1, help='Index of forced subtitle stream')
@click.option('--rearrange-streams', type=str, default="", help='Rearrange output streams order. Use format comma separated integers')
@click.option("--crop", is_flag=False, flag_value="default", default="none") @click.option("--crop", is_flag=False, flag_value="default", default="none")
@click.option("--output-directory", type=str, default='') @click.option("--output-directory", type=str, default='')
@click.option("--denoise", is_flag=True, default=False) @click.option("--denoise", is_flag=False, flag_value="default", default="none")
@click.option("--denoise-use-hw", is_flag=True, default=False)
@click.option('--denoise-strength', type=str, default='', help='Denoising strength, more blurring vs more details.')
@click.option('--denoise-patch-size', type=str, default='', help='Subimage size to apply filtering on luminosity plane. Reduces broader noise patterns but costly.')
@click.option('--denoise-chroma-patch-size', type=str, default='', help='Subimage size to apply filtering on chroma planes.')
@click.option('--denoise-research-window', type=str, default='', help='Range to search for comparable patches on luminosity plane. Better filtering but costly.')
@click.option('--denoise-chroma-research-window', type=str, default='', help='Range to search for comparable patches on chroma planes.')
@click.option("--no-tmdb", is_flag=True, default=False) @click.option("--no-tmdb", is_flag=True, default=False)
# @click.option("--no-jellyfin", is_flag=True, default=False) # @click.option("--no-jellyfin", is_flag=True, default=False)
@ -318,19 +323,29 @@ def convert(ctx,
subtitle_directory, subtitle_directory,
subtitle_prefix, subtitle_prefix,
audio_language, language,
audio_title, title,
default_video,
forced_video,
default_audio, default_audio,
forced_audio, forced_audio,
subtitle_language,
subtitle_title,
default_subtitle, default_subtitle,
forced_subtitle, forced_subtitle,
rearrange_streams,
crop, crop,
output_directory, output_directory,
denoise, denoise,
denoise_use_hw,
denoise_strength,
denoise_patch_size,
denoise_chroma_patch_size,
denoise_research_window,
denoise_chroma_research_window,
no_tmdb, no_tmdb,
# no_jellyfin, # no_jellyfin,
no_pattern, no_pattern,
@ -360,12 +375,70 @@ def convert(ctx,
context['no_signature'] = no_signature context['no_signature'] = no_signature
context['keep_mkvmerge_metadata'] = keep_mkvmerge_metadata context['keep_mkvmerge_metadata'] = keep_mkvmerge_metadata
context['denoiser'] = NlmeansController(parameters = denoise,
strength = denoise_strength,
patchSize = denoise_patch_size,
chromaPatchSize = denoise_chroma_patch_size,
researchWindow = denoise_research_window,
chromaResearchWindow = denoise_chroma_research_window,
useHardware = denoise_use_hw)
context['import_subtitles'] = (subtitle_directory and subtitle_prefix) context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
if context['import_subtitles']: if context['import_subtitles']:
context['subtitle_directory'] = subtitle_directory context['subtitle_directory'] = subtitle_directory
context['subtitle_prefix'] = subtitle_prefix context['subtitle_prefix'] = subtitle_prefix
cliOverrides = {}
if language:
cliOverrides['languages'] = {}
for overLang in language:
olTokens = overLang.split(':')
if len(olTokens) == 2:
try:
cliOverrides['languages'][int(olTokens[0])] = olTokens[1]
except ValueError:
ctx.obj['logger'].warning(f"Ignoring non-integer language index {olTokens[0]}")
continue
if title:
cliOverrides['titles'] = {}
for overTitle in title:
otTokens = overTitle.split(':')
if len(otTokens) == 2:
try:
cliOverrides['titles'][int(otTokens[0])] = otTokens[1]
except ValueError:
ctx.obj['logger'].warning(f"Ignoring non-integer title index {otTokens[0]}")
continue
if default_video != -1:
cliOverrides['default_video'] = default_video
if forced_video != -1:
cliOverrides['forced_video'] = forced_video
if default_audio != -1:
cliOverrides['default_audio'] = default_audio
if forced_audio != -1:
cliOverrides['forced_audio'] = forced_audio
if default_subtitle != -1:
cliOverrides['default_subtitle'] = default_subtitle
if forced_subtitle != -1:
cliOverrides['forced_subtitle'] = forced_subtitle
if cliOverrides:
context['overrides'] = cliOverrides
if rearrange_streams:
try:
cliOverrides['stream_order'] = [int(si) for si in rearrange_streams.split(",")]
except ValueError as ve:
errorMessage = "Non-integer in rearrange stream parameter"
ctx.obj['logger'].error(errorMessage)
raise click.Abort()
ctx.obj['logger'].debug(f"\nVideo encoder: {video_encoder}") ctx.obj['logger'].debug(f"\nVideo encoder: {video_encoder}")
qualityTokens = quality.split(',') qualityTokens = quality.split(',')
@ -437,10 +510,10 @@ def convert(ctx,
mediaFileProperties.getSeason(), mediaFileProperties.getSeason(),
mediaFileProperties.getEpisode()) mediaFileProperties.getEpisode())
# if context['use_jellyfin']: if cliOverrides:
# # Reorder subtracks in types with default the last, then make subindices flat again sourceMediaDescriptor.applyOverrides(cliOverrides)
# sourceMediaDescriptor.applyJellyfinOrder()
#YOLO
fc = FfxController(context, sourceMediaDescriptor) fc = FfxController(context, sourceMediaDescriptor)
@ -483,9 +556,10 @@ def convert(ctx,
ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}") ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
# if context['use_jellyfin']:
# # Reorder subtracks in types with default the last, then make subindices flat again if cliOverrides:
# targetMediaDescriptor.applyJellyfinOrder() targetMediaDescriptor.applyOverrides(cliOverrides)
ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}") ctx.obj['logger'].debug(f"tmd subindices: {[t.getIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getSubIndex() for t in targetMediaDescriptor.getAllTrackDescriptors()]} {[t.getDispositionFlag(TrackDisposition.DEFAULT) for t in targetMediaDescriptor.getAllTrackDescriptors()]}")
@ -525,8 +599,7 @@ def convert(ctx,
targetPath, targetPath,
context['video_encoder'], context['video_encoder'],
q, q,
preset, preset)
denoise)
#TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True) #TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)

@ -112,44 +112,6 @@ class FfxController():
return ['-ss', str(cropStart), '-t', str(cropLength)] return ['-ss', str(cropStart), '-t', str(cropLength)]
def generateDenoiseTokens(self,
strength: float = 2.8,
patchSize: int = 12,
chromaPatchSize: int = 8,
researchWindow: int = 22,
chromaResearchWindow: int= 16,
useHardware: bool = False):
"""
s: double
Denoising strength (from 1 to 30) (default 1)
Trade-off between noise removal and detail retention. Comparable to gaussian sigma.
p: int patch size (from 0 to 99) (default 7)
Catches larger areas reducing broader noise patterns, but costly
pc: int patch size for chroma planes (from 0 to 99) (default 0)
r: int research window (from 0 to 99) (default 15)
Range to search for comparable patches.
Better filtering but costly
rc: int research window for chroma planes (from 0 to 99) (default 0)
Good values to denoise film grain that was subobtimally encoded:
strength: float = 2.8
patchSize: int = 12
chromaPatchSize: int = 8
researchWindow: int = 22
chromaResearchWindow: int= 16
"""
filterName = 'nlmeans_opencl' if useHardware else 'nlmeans'
return ['-vf', f"{filterName}=s={strength}:p={patchSize}:pc={chromaPatchSize}:r={researchWindow}:rc={chromaResearchWindow}"]
def generateOutputTokens(self, filepath, format, ext): def generateOutputTokens(self, filepath, format, ext):
outputFilePath = f"{filepath}.{ext}" outputFilePath = f"{filepath}.{ext}"
return ['-f', format, outputFilePath] return ['-f', format, outputFilePath]
@ -300,10 +262,8 @@ class FfxController():
targetPath, targetPath,
videoEncoder: VideoEncoder = VideoEncoder.VP9, videoEncoder: VideoEncoder = VideoEncoder.VP9,
quality: int = DEFAULT_QUALITY, quality: int = DEFAULT_QUALITY,
preset: int = DEFAULT_AV1_PRESET, preset: int = DEFAULT_AV1_PRESET):
denoise: bool = False):
# self.__targetMediaDescriptor order OK
commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath] commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath]
@ -314,11 +274,12 @@ class FfxController():
+ self.__targetMediaDescriptor.getInputMappingTokens() + self.__targetMediaDescriptor.getInputMappingTokens()
+ self.generateDispositionTokens()) + self.generateDispositionTokens())
if not self.__sourceMediaDescriptor is None: if not self.__sourceMediaDescriptor is None or 'overrides' in self.__context.keys():
commandSequence += self.generateMetadataTokens() commandSequence += self.generateMetadataTokens()
if denoise: # if denoise:
commandSequence += self.generateDenoiseTokens() # commandSequence += self.generateDenoiseTokens()
commandSequence1 += self.__context['denoiser'].generateDenoiseTokens()
commandSequence += (self.generateAudioEncodingTokens() commandSequence += (self.generateAudioEncodingTokens()
+ self.generateAV1Tokens(int(quality), int(preset)) + self.generateAV1Tokens(int(quality), int(preset))
@ -361,11 +322,12 @@ class FfxController():
+ self.__targetMediaDescriptor.getInputMappingTokens() + self.__targetMediaDescriptor.getInputMappingTokens()
+ self.generateDispositionTokens()) + self.generateDispositionTokens())
if not self.__sourceMediaDescriptor is None: if not self.__sourceMediaDescriptor is None or 'overrides' in self.__context.keys():
commandSequence2 += self.generateMetadataTokens() commandSequence2 += self.generateMetadataTokens()
if denoise: # if denoise:
commandSequence2 += self.generateDenoiseTokens() # commandSequence2 += self.generateDenoiseTokens()
commandSequence2 += self.__context['denoiser'].generateDenoiseTokens()
commandSequence2 += self.generateVP9Pass2Tokens(int(quality)) + self.generateAudioEncodingTokens() commandSequence2 += self.generateVP9Pass2Tokens(int(quality)) + self.generateAudioEncodingTokens()

@ -3,6 +3,8 @@ import os, re, click, logging
from typing import List, Self from typing import List, Self
from ffx.track_type import TrackType from ffx.track_type import TrackType
from ffx.iso_language import IsoLanguage
from ffx.track_disposition import TrackDisposition from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor from ffx.track_descriptor import TrackDescriptor
@ -70,17 +72,42 @@ class MediaDescriptor:
else: else:
self.__trackDescriptors = [] self.__trackDescriptors = []
# if MediaDescriptor.JELLYFIN_ORDER_FLAG_KEY in kwargs.keys(): #TODO: to be removed
# 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 = self.__context['use_jellyfin'] if 'use_jellyfin' in self.__context.keys() else False
self.__jellyfinOrder = False self.__jellyfinOrder = False
def setTrackLanguage(self, language: str, index: int, trackType: TrackType = None):
trackLanguage = IsoLanguage.findThreeLetter(language)
if trackLanguage == IsoLanguage.UNDEFINED:
self.__logger.warning('MediaDescriptor.setTrackLanguage(): Parameter language does not contain a registered '
+ f"ISO 639 3-letter language code, skipping to set language for"
+ str('' if trackType is None else trackType.label()) + f"track {index}")
trackList = self.getTrackDescriptors(trackType=trackType)
if index < 0 or index > len(trackList) - 1:
self.__logger.warning(f"MediaDescriptor.setTrackLanguage(): Parameter index ({index}) is "
+ f"out of range of {'' if trackType is None else trackType.label()}track list")
td: TrackDescriptor = trackList[index]
td.setLanguage(trackLanguage)
return
def setTrackTitle(self, title: str, index: int, trackType: TrackType = None):
trackList = self.getTrackDescriptors(trackType=trackType)
if index < 0 or index > len(trackList) - 1:
self.__logger.error(f"MediaDescriptor.setTrackTitle(): Parameter index ({index}) is "
+ f"out of range of {'' if trackType is None else trackType.label()}track list")
raise click.Abort()
td: TrackDescriptor = trackList[index]
td.setTitle(title)
def setDefaultSubTrack(self, trackType: TrackType, subIndex: int): def setDefaultSubTrack(self, trackType: TrackType, subIndex: int):
for t in self.getAllTrackDescriptors(): for t in self.getAllTrackDescriptors():
if t.getType() == trackType: if t.getType() == trackType:
@ -123,6 +150,47 @@ class MediaDescriptor:
raise ValueError('Multiple streams originating from the same source stream') raise ValueError('Multiple streams originating from the same source stream')
def applyOverrides(self, overrides: dict):
if 'languages' in overrides.keys():
for trackIndex in overrides['languages'].keys():
self.setTrackLanguage(overrides['languages'][trackIndex], trackIndex)
if 'titles' in overrides.keys():
for trackIndex in overrides['titles'].keys():
self.setTrackTitle(overrides['titles'][trackIndex], trackIndex)
if 'forced_video' in overrides.keys():
sti = int(overrides['forced_video'])
self.setForcedSubTrack(TrackType.VIDEO, sti)
self.setDefaultSubTrack(TrackType.VIDEO, sti)
elif 'default_video' in overrides.keys():
sti = int(overrides['default_video'])
self.setDefaultSubTrack(TrackType.VIDEO, sti)
if 'forced_audio' in overrides.keys():
sti = int(overrides['forced_audio'])
self.setForcedSubTrack(TrackType.AUDIO, sti)
self.setDefaultSubTrack(TrackType.AUDIO, sti)
elif 'default_audio' in overrides.keys():
sti = int(overrides['default_audio'])
self.setDefaultSubTrack(TrackType.AUDIO, sti)
if 'forced_subtitle' in overrides.keys():
sti = int(overrides['forced_subtitle'])
self.setForcedSubTrack(TrackType.SUBTITLE, sti)
self.setDefaultSubTrack(TrackType.SUBTITLE, sti)
elif 'default_subtitle' in overrides.keys():
sti = int(overrides['default_subtitle'])
self.setDefaultSubTrack(TrackType.SUBTITLE, sti)
if 'stream_order' in overrides.keys():
self.rearrangeTrackDescriptors(overrides['stream_order'])
def applySourceIndices(self, sourceMediaDescriptor: Self): def applySourceIndices(self, sourceMediaDescriptor: Self):
sourceTrackDescriptors = sourceMediaDescriptor.getAllTrackDescriptors() sourceTrackDescriptors = sourceMediaDescriptor.getAllTrackDescriptors()
@ -131,51 +199,16 @@ class MediaDescriptor:
raise ValueError('MediaDescriptor.applySourceIndices (): Number of track descriptors does not match') raise ValueError('MediaDescriptor.applySourceIndices (): Number of track descriptors does not match')
for trackIndex in range(numTrackDescriptors): for trackIndex in range(numTrackDescriptors):
# click.echo(f"{trackIndex} -> {sourceTrackDescriptors[trackIndex].getSourceIndex()}")
self.__trackDescriptors[trackIndex].setSourceIndex(sourceTrackDescriptors[trackIndex].getSourceIndex()) self.__trackDescriptors[trackIndex].setSourceIndex(sourceTrackDescriptors[trackIndex].getSourceIndex())
def applyJellyfinOrder(self): def rearrangeTrackDescriptors(self, newOrder: List[int]):
"""Reorder subtracks in types with default the last, then make subindices flat again""" if len(newOrder) != len(self.__trackDescriptors):
raise ValueError('Length of list with reordered indices does not match number of track descriptors')
# videoTracks = self.sortSubIndices(self.getVideoTracks()) reorderedTrackDescriptors = {}
# audioTracks = self.sortSubIndices(self.getAudioTracks()) for oldIndex in newOrder:
# subtitleTracks = self.sortSubIndices(self.getSubtitleTracks()) reorderedTrackDescriptors.append(self.__trackDescriptors[oldIndex])
self.__trackDescriptors = reorderedTrackDescriptors
self.checkConfiguration()
# from self.__trackDescriptors
videoTracks = self.getVideoTracks()
audioTracks = self.getAudioTracks()
subtitleTracks = self.getSubtitleTracks()
defaultVideoTracks = [v for v in videoTracks if v.getDispositionFlag(TrackDisposition.DEFAULT)]
defaultAudioTracks = [a for a in audioTracks if a.getDispositionFlag(TrackDisposition.DEFAULT)]
defaultSubtitleTracks = [s for s in subtitleTracks if s.getDispositionFlag(TrackDisposition.DEFAULT)]
if defaultVideoTracks:
videoTracks.append(videoTracks.pop(videoTracks.index(defaultVideoTracks[0])))
#self.sortSubIndices(videoTracks)
numVideoTracks = len(videoTracks)
for vIndex in range(numVideoTracks):
videoTracks[vIndex].setDispositionFlag(TrackDisposition.DEFAULT,
vIndex == numVideoTracks - 1)
if defaultAudioTracks:
audioTracks.append(audioTracks.pop(audioTracks.index(defaultAudioTracks[0])))
#self.sortSubIndices(audioTracks)
numAudioTracks = len(audioTracks)
for aIndex in range(numAudioTracks):
audioTracks[aIndex].setDispositionFlag(TrackDisposition.DEFAULT,
aIndex == numAudioTracks - 1)
if defaultSubtitleTracks:
subtitleTracks.append(subtitleTracks.pop(subtitleTracks.index(defaultSubtitleTracks[0])))
#self.sortSubIndices(subtitleTracks)
numSubtitleTracks = len(subtitleTracks)
for sIndex in range(numSubtitleTracks):
subtitleTracks[sIndex].setDispositionFlag(TrackDisposition.DEFAULT,
sIndex == numSubtitleTracks - 1)
self.__trackDescriptors = videoTracks + audioTracks + subtitleTracks
#self.sortIndices(self.__trackDescriptors)
self.reindexSubIndices() self.reindexSubIndices()
self.reindexIndices() self.reindexIndices()
@ -254,18 +287,30 @@ class MediaDescriptor:
tdList[trackIndex].setIndex(trackIndex) tdList[trackIndex].setIndex(trackIndex)
def getAllTrackDescriptors(self) -> List[TrackDescriptor]: def getAllTrackDescriptors(self):
"""Returns all track descriptors sorted by type: video, audio then subtitles"""
return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks() return self.getVideoTracks() + self.getAudioTracks() + self.getSubtitleTracks()
def getTrackDescriptors(self,
trackType: TrackType = None) -> List[TrackDescriptor]:
if trackType is None:
return self.__trackDescriptors
descriptorList = []
for td in self.__trackDescriptors:
if td.getType() == trackType:
descriptorList.append(td)
return descriptorList
def getVideoTracks(self) -> List[TrackDescriptor]: def getVideoTracks(self) -> List[TrackDescriptor]:
return [ return [v for v in self.__trackDescriptors if v.getType() == TrackType.VIDEO]
v for v in self.__trackDescriptors if v.getType() == TrackType.VIDEO
]
def getAudioTracks(self) -> List[TrackDescriptor]: def getAudioTracks(self) -> List[TrackDescriptor]:
return [ return [a for a in self.__trackDescriptors if a.getType() == TrackType.AUDIO]
a for a in self.__trackDescriptors if a.getType() == TrackType.AUDIO
]
def getSubtitleTracks(self) -> List[TrackDescriptor]: def getSubtitleTracks(self) -> List[TrackDescriptor]:
return [ return [
@ -278,10 +323,8 @@ class MediaDescriptor:
def compare(self, vsMediaDescriptor: Self): def compare(self, vsMediaDescriptor: Self):
if not isinstance(vsMediaDescriptor, self.__class__): if not isinstance(vsMediaDescriptor, self.__class__):
errorMessage = f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}" self.__logger.error(f"MediaDescriptor.compare(): Argument is required to be of type {self.__class__}")
self.__logger.error(errorMessage) raise click.Abort()
# raise click.ClickException(errorMessage)
click.Abort()
vsTags = vsMediaDescriptor.getTags() vsTags = vsMediaDescriptor.getTags()
tags = self.getTags() tags = self.getTags()
@ -357,10 +400,8 @@ class MediaDescriptor:
def getImportFileTokens(self, use_sub_index: bool = True): def getImportFileTokens(self, use_sub_index: bool = True):
# reorderedTrackDescriptors = self.getReorderedTrackDescriptors()
importFileTokens = [] importFileTokens = []
#for rtd in reorderedTrackDescriptors:
for td in self.__trackDescriptors: for td in self.__trackDescriptors:
importedFilePath = td.getExternalSourceFilePath() importedFilePath = td.getExternalSourceFilePath()
@ -377,14 +418,6 @@ class MediaDescriptor:
def getInputMappingTokens(self, use_sub_index: bool = True, only_video: bool = False): def getInputMappingTokens(self, use_sub_index: bool = True, only_video: bool = False):
"""Tracks must be reordered for source index order""" """Tracks must be reordered for source index order"""
# sourceTrackDescriptorSubIndices = [self.__trackDescriptors[std.getSourceIndex()].getSubIndex()
# for std in self.__trackDescriptors]
# self.reindexSubIndices(trackDescriptors = sourceOrderTrackDescriptors)
# self.reindexIndices(trackDescriptors = sourceOrderTrackDescriptors)
# click.echo(sourceTrackDescriptorIndices)
inputMappingTokens = [] inputMappingTokens = []
filePointer = 1 filePointer = 1
@ -467,17 +500,12 @@ class MediaDescriptor:
availableFileSubtitleDescriptors = self.searchSubtitleFiles(searchDirectory, prefix) availableFileSubtitleDescriptors = self.searchSubtitleFiles(searchDirectory, prefix)
# click.echo(f"availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}")
self.__logger.debug(f"importSubtitles(): availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}") self.__logger.debug(f"importSubtitles(): availableFileSubtitleDescriptors: {availableFileSubtitleDescriptors}")
subtitleTracks = self.getSubtitleTracks() subtitleTracks = self.getSubtitleTracks()
# click.echo(f"subtitleTracks: {[s.getIndex() for s in subtitleTracks]}")
self.__logger.debug(f"importSubtitles(): subtitleTracks: {[s.getIndex() for s in subtitleTracks]}") self.__logger.debug(f"importSubtitles(): subtitleTracks: {[s.getIndex() for s in subtitleTracks]}")
# if len(availableFileSubtitleDescriptors) != len(subtitleTracks):
# raise click.ClickException(f"MediaDescriptor.importSubtitles(): Number if subtitle files not matching number of subtitle tracks")
matchingSubtitleFileDescriptors = ( matchingSubtitleFileDescriptors = (
sorted( sorted(
[ [
@ -491,9 +519,7 @@ class MediaDescriptor:
else [] else []
) )
# click.echo(f"matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}")
self.__logger.debug(f"importSubtitles(): matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}") self.__logger.debug(f"importSubtitles(): matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}")
# click.echo(f"importSubtitles(): matchingSubtitleFileDescriptors: {matchingSubtitleFileDescriptors}")
for msfd in matchingSubtitleFileDescriptors: for msfd in matchingSubtitleFileDescriptors:
matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]] matchingSubtitleTrackDescriptor = [s for s in subtitleTracks if s.getIndex() == msfd["index"]]

@ -0,0 +1,142 @@
class NlmeansController():
"""
s: double
Denoising strength (from 1 to 30) (default 1)
Trade-off between noise removal and detail retention. Comparable to gaussian sigma.
p: int patch size (from 0 to 99) (default 7)
Catches larger areas reducing broader noise patterns, but costly
pc: int patch size for chroma planes (from 0 to 99) (default 0)
r: int research window (from 0 to 99) (default 15)
Range to search for comparable patches.
Better filtering but costly
rc: int research window for chroma planes (from 0 to 99) (default 0)
Good values to denoise film grain that was subobtimally encoded:
strength: float = 2.8
patchSize: int = 12
chromaPatchSize: int = 8
researchWindow: int = 22
chromaResearchWindow: int= 16
"""
DEFAULT_STRENGTH: float = 2.8
DEFAULT_PATCH_SIZE: int = 13
DEFAULT_CHROMA_PATCH_SIZE: int = 9
DEFAULT_RESEARCH_WINDOW: int = 23
DEFAULT_CHROMA_RESEARCH_WINDOW: int= 17
def __init__(self,
parameters: str = "none",
strength: str = "",
patchSize: str = "",
chromaPatchSize: str = "",
researchWindow: str = "",
chromaResearchWindow: str = "",
useHardware: bool = False):
self.__isActive = (parameters != "none"
or strength
or patchSize
or chromaPatchSize
or researchWindow
or chromaResearchWindow)
self.__useHardware = useHardware
parameterTokens = parameters.split(',')
self.__strengthList = []
if strength:
strengthTokens = strength.split(',')
for st in strengthTokens:
try:
strengthValue = float(st)
except:
raise ValueError('NlmeansController: Strength value has to be of type float')
if strengthValue < 1.0 or strengthValue > 30.0:
raise ValueError('NlmeansController: Strength value has to be between 1.0 and 30.0')
self.__strengthList.append(strengthValue)
else:
self.__strengthList = [NlmeansController.DEFAULT_STRENGTH]
self.__patchSizeList = []
if patchSize:
patchSizeTokens = patchSize.split(',')
for pst in patchSizeTokens:
try:
patchSizeValue = int(pst)
except:
raise ValueError('NlmeansController: Patch size value has to be of type int')
if patchSizeValue < 0 or patchSizeValue > 99:
raise ValueError('NlmeansController: Patch size value has to be between 0 and 99')
if patchSizeValue % 2 == 0:
raise ValueError('NlmeansController: Patch size value has to an odd number')
self.__patchSizeList.append(patchSizeValue)
else:
self.__patchSizeList = [NlmeansController.DEFAULT_PATCH_SIZE]
self.__chromaPatchSizeList = []
if chromaPatchSize:
chromaPatchSizeTokens = chromaPatchSize.split(',')
for cpst in chromaPatchSizeTokens:
try:
chromaPatchSizeValue = int(pst)
except:
raise ValueError('NlmeansController: Chroma patch size value has to be of type int')
if chromaPatchSizeValue < 0 or chromaPatchSizeValue > 99:
raise ValueError('NlmeansController: Chroma patch value has to be between 0 and 99')
if chromaPatchSizeValue % 2 == 0:
raise ValueError('NlmeansController: Chroma patch value has to an odd number')
self.__chromaPatchSizeList.append(chromaPatchSizeValue)
else:
self.__chromaPatchSizeList = [NlmeansController.DEFAULT_CHROMA_PATCH_SIZE]
self.__researchWindowList = []
if researchWindow:
researchWindowTokens = researchWindow.split(',')
for rwt in researchWindowTokens:
try:
researchWindowValue = int(rwt)
except:
raise ValueError('NlmeansController: Research window value has to be of type int')
if researchWindowValue < 0 or researchWindowValue > 99:
raise ValueError('NlmeansController: Research window value has to be between 0 and 99')
if researchWindowValue % 2 == 0:
raise ValueError('NlmeansController: Research window value has to an odd number')
self.__researchWindowList.append(researchWindowValue)
else:
self.__researchWindowList = [NlmeansController.DEFAULT_RESEARCH_WINDOW]
self.__chromaResearchWindowList = []
if chromaResearchWindow:
chromaResearchWindowTokens = chromaResearchWindow.split(',')
for crwt in chromaResearchWindowTokens:
try:
chromaResearchWindowValue = int(crwt)
except:
raise ValueError('NlmeansController: Chroma research window value has to be of type int')
if chromaResearchWindowValue < 0 or chromaResearchWindowValue > 99:
raise ValueError('NlmeansController: Chroma research window value has to be between 0 and 99')
if chromaResearchWindowValue % 2 == 0:
raise ValueError('NlmeansController: Chroma research window value has to an odd number')
self.__chromaResearchWindowList.append(chromaResearchWindowValue)
else:
self.__chromaResearchWindowList = [NlmeansController.DEFAULT_CHROMA_RESEARCH_WINDOW]
def isActive(self):
return self.__isActive
def generateDenoiseTokens(self):
filterName = 'nlmeans_opencl' if self.__useHardware else 'nlmeans'
return ['-vf', f"{filterName}=s={self.__strengthList[0]}"
+ f":p={self.__patchSizeList[0]}"
+ f":pc={self.__chromaPatchSizeList[0]}"
+ f":r={self.__researchWindowList[0]}"
+ f":rc={self.__chromaResearchWindowList[0]}"] if self.__isActive else []

@ -136,9 +136,9 @@ class Scenario2(Scenario):
resultFileProperties = FileProperties(testContext, resultFile) resultFileProperties = FileProperties(testContext, resultFile)
resultMediaDescriptor = resultFileProperties.getMediaDescriptor() resultMediaDescriptor = resultFileProperties.getMediaDescriptor()
if testContext['use_jellyfin']: # if testContext['use_jellyfin']:
sourceMediaDescriptor.applyJellyfinOrder() # sourceMediaDescriptor.applyJellyfinOrder()
resultMediaDescriptor.applySourceIndices(sourceMediaDescriptor) # resultMediaDescriptor.applySourceIndices(sourceMediaDescriptor)
resultMediaTracks = resultMediaDescriptor.getAllTrackDescriptors() resultMediaTracks = resultMediaDescriptor.getAllTrackDescriptors()

@ -237,8 +237,8 @@ class Scenario4(Scenario):
for l in rmd.getConfiguration(label = 'resultMediaDescriptor'): for l in rmd.getConfiguration(label = 'resultMediaDescriptor'):
self._logger.debug(l) self._logger.debug(l)
if testContext['use_jellyfin']: # if testContext['use_jellyfin']:
sourceMediaDescriptor.applyJellyfinOrder() # sourceMediaDescriptor.applyJellyfinOrder()
# num tracks differ # num tracks differ
rmd.applySourceIndices(sourceMediaDescriptor) rmd.applySourceIndices(sourceMediaDescriptor)

@ -282,12 +282,21 @@ class TrackDescriptor:
else: else:
return IsoLanguage.UNDEFINED return IsoLanguage.UNDEFINED
def setLanguage(self, language: IsoLanguage):
if not type(language) is IsoLanguage:
raise TypeError('language has to be of type IsoLanguage')
self.__trackTags["language"] = language
def getTitle(self): def getTitle(self):
if "title" in self.__trackTags.keys(): if "title" in self.__trackTags.keys():
return str(self.__trackTags["title"]) return str(self.__trackTags["title"])
else: else:
return "" return ""
def setTitle(self, title: str):
self.__trackTags["title"] = str(title)
def getAudioLayout(self): def getAudioLayout(self):
return self.__audioLayout return self.__audioLayout

Loading…
Cancel
Save