diff --git a/src/ffx/constants.py b/src/ffx/constants.py index 4556316..b4f9d87 100644 --- a/src/ffx/constants.py +++ b/src/ffx/constants.py @@ -9,7 +9,7 @@ DEFAULT_AC3_BANDWIDTH = "256" DEFAULT_DTS_BANDWIDTH = "320" DEFAULT_7_1_BANDWIDTH = "384" -DEFAULT_CROP_START = 60 -DEFAULT_CROP_LENGTH = 180 +DEFAULT_cut_start = 60 +DEFAULT_cut_length = 180 DEFAULT_OUTPUT_FILENAME_TEMPLATE = '{{ ffx_show_name }} - {{ ffx_index }}{{ ffx_index_separator }}{{ ffx_episode_name }}{{ ffx_indicator_separator }}{{ ffx_indicator }}' diff --git a/src/ffx/ffx.py b/src/ffx/ffx.py index 1f5997e..5a5891e 100755 --- a/src/ffx/ffx.py +++ b/src/ffx/ffx.py @@ -30,6 +30,7 @@ from ffx.constants import DEFAULT_STEREO_BANDWIDTH, DEFAULT_AC3_BANDWIDTH, DEFAU from ffx.filter.quality_filter import QualityFilter from ffx.filter.preset_filter import PresetFilter +from ffx.filter.crop_filter import CropFilter from ffx.filter.nlmeans_filter import NlmeansFilter from ffx.constants import VERSION @@ -329,7 +330,8 @@ def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor): @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="auto", default="none") +@click.option("--cut", is_flag=False, flag_value="default", default="none") @click.option("--output-directory", type=str, default='') @@ -385,6 +387,8 @@ def convert(ctx, rearrange_streams, crop, + cut, + output_directory, denoise, @@ -531,15 +535,15 @@ def convert(ctx, ctx.obj['logger'].debug(f"AC3 bitrate: {context['bitrates']['ac3']}") ctx.obj['logger'].debug(f"DTS bitrate: {context['bitrates']['dts']}") - - # Process crop parameters - context['perform_crop'] = (crop != 'none') - if context['perform_crop']: - cTokens = crop.split(',') - if cTokens and len(cTokens) == 2: - context['crop_start'] = int(cTokens[0]) - context['crop_length'] = int(cTokens[1]) - ctx.obj['logger'].debug(f"Crop start={context['crop_start']} length={context['crop_length']}") + #-> + # Process cut parameters + context['perform_cut'] = (cut != 'none') + if context['perform_cut']: + cutTokens = cut.split(',') + if cutTokens and len(cutTokens) == 2: + context['cut_start'] = int(cutTokens[0]) + context['cut_length'] = int(cutTokens[1]) + ctx.obj['logger'].debug(f"Cut start={context['cut_start']} length={context['cut_length']}") tc = TmdbController() if context['use_tmdb'] else None @@ -551,6 +555,12 @@ def convert(ctx, presetKwargs = {PresetFilter.PRESET_KEY: preset} PresetFilter(**presetKwargs) + cf = None + # if crop != 'none': + if crop == 'auto': + cropKwargs = {} + cf = CropFilter(**cropKwargs) + denoiseKwargs = {} if denoise_strength: denoiseKwargs[NlmeansFilter.STRENGTH_KEY] = denoise_strength @@ -587,6 +597,11 @@ def convert(ctx, mediaFileProperties = FileProperties(context, sourcePath) + + if not cf is None: + cf.setArguments(**mediaFileProperties.findCropArguments()) + + ssc = ShiftedSeasonController(context) showId = mediaFileProperties.getShowId() diff --git a/src/ffx/ffx_controller.py b/src/ffx/ffx_controller.py index 28b52b5..8fa1710 100644 --- a/src/ffx/ffx_controller.py +++ b/src/ffx/ffx_controller.py @@ -9,7 +9,7 @@ from ffx.track_codec import TrackCodec from ffx.video_encoder import VideoEncoder from ffx.process import executeProcess -from ffx.constants import DEFAULT_CROP_START, DEFAULT_CROP_LENGTH +from ffx.constants import DEFAULT_cut_start, DEFAULT_cut_length from ffx.filter.quality_filter import QualityFilter from ffx.filter.preset_filter import PresetFilter @@ -97,12 +97,12 @@ class FfxController(): def generateCropTokens(self): - if 'crop_start' in self.__context.keys() and 'crop_length' in self.__context.keys(): - cropStart = int(self.__context['crop_start']) - cropLength = int(self.__context['crop_length']) + if 'cut_start' in self.__context.keys() and 'cut_length' in self.__context.keys(): + cropStart = int(self.__context['cut_start']) + cropLength = int(self.__context['cut_length']) else: - cropStart = DEFAULT_CROP_START - cropLength = DEFAULT_CROP_LENGTH + cropStart = DEFAULT_cut_start + cropLength = DEFAULT_cut_length return ['-ss', str(cropStart), '-t', str(cropLength)] @@ -211,7 +211,7 @@ class FfxController(): commandSequence += self.generateAudioEncodingTokens() - if self.__context['perform_crop']: + if self.__context['perform_cut']: commandSequence += self.generateCropTokens() commandSequence += self.generateOutputTokens(targetPath, @@ -241,7 +241,7 @@ class FfxController(): commandSequence += self.generateAudioEncodingTokens() - if self.__context['perform_crop']: + if self.__context['perform_cut']: commandSequence += self.generateCropTokens() commandSequence += self.generateOutputTokens(targetPath, @@ -271,7 +271,7 @@ class FfxController(): if td.getCodec != TrackCodec.PNG: commandSequence1 += self.generateVP9Pass1Tokens(int(quality)) - if self.__context['perform_crop']: + if self.__context['perform_cut']: commandSequence1 += self.generateCropTokens() commandSequence1 += FfxController.NULL_TOKENS @@ -300,7 +300,7 @@ class FfxController(): commandSequence2 += self.generateAudioEncodingTokens() - if self.__context['perform_crop']: + if self.__context['perform_cut']: commandSequence2 += self.generateCropTokens() commandSequence2 += self.generateOutputTokens(targetPath, diff --git a/src/ffx/file_properties.py b/src/ffx/file_properties.py index f209c10..cc010bf 100644 --- a/src/ffx/file_properties.py +++ b/src/ffx/file_properties.py @@ -3,6 +3,8 @@ import os, re, json from .media_descriptor import MediaDescriptor from .pattern_controller import PatternController +from ffx.filter.crop_filter import CropFilter + from .process import executeProcess from ffx.model.pattern import Pattern @@ -177,48 +179,8 @@ class FileProperties(): - def findCropParams(self): - """Returns 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" - } - } - """ + def findCropArguments(self): + """""" # ffmpeg -i -vf cropdetect -f null - ffprobeOutput, ffprobeError, returnCode = executeProcess(["ffmpeg", "-i", @@ -243,9 +205,19 @@ class FileProperties(): if crops: cropHistogram = sorted(crops, reverse=True) - return cropHistogram[0] + cropString = cropHistogram[0] + + cropTokens = cropString.split('=') + cropValueTokens = cropTokens[1] + cropValues = cropValueTokens.split(':') + return { + CropFilter.OUTPUT_WIDTH_KEY: cropValues[0], + CropFilter.OUTPUT_HEIGHT_KEY: cropValues[1], + CropFilter.OFFSET_X_KEY: cropValues[2], + CropFilter.OFFSET_Y_KEY: cropValues[3] + } else: - return '' + return {} def getMediaDescriptor(self): diff --git a/src/ffx/filter/crop_filter.py b/src/ffx/filter/crop_filter.py new file mode 100644 index 0000000..468c419 --- /dev/null +++ b/src/ffx/filter/crop_filter.py @@ -0,0 +1,67 @@ +import itertools + +from .filter import Filter + + +class CropFilter(Filter): + + IDENTIFIER = 'crop' + + OUTPUT_WIDTH_KEY = 'output_width' + OUTPUT_HEIGHT_KEY = 'output_height' + OFFSET_X_KEY = 'x_offset' + OFFSET_Y_KEY = 'y_offset' + +# ffmpeg -i in.mp4 -vf "crop=out_w:out_h:x:y" out.mp4 +# +# Where the options are as follows: +# +# use "-vf" or -"filter:v" - depending on your version of ffmpeg/avconv +# out_w is the width of the output rectangle +# out_h is the height of the output rectangle +# x and y specify the top left corner of the output rectangle (coordinates start at (0,0) in the top left corner of the input) + + + def __init__(self, **kwargs): + + self.__outputWidth = int(kwargs.get(CropFilter.OUTPUT_WIDTH_KEY)) + self.__outputHeight = int(kwargs.get(CropFilter.OUTPUT_HEIGHT_KEY)) + self.__offsetX = int(kwargs.get(CropFilter.OFFSET_X_KEY)) + self.__offsetY = int(kwargs.get(CropFilter.OFFSET_Y_KEY)) + + super().__init__(self) + + def setArguments(self, + outputWidth: int, + outputHeight: int, + offsetX: int, + offsetY: int): + self.__outputWidth = int(outputWidth) + self.__outputHeight = int(outputHeight) + self.__offsetX = int(offsetX) + self.__offsetY = int(offsetY) + + def getPayload(self): + + suffices = [] + + payload = {'identifier': CropFilter.IDENTIFIER, + 'parameters': { + CropFilter.OUTPUT_WIDTH_KEY: self.__outputWidth, + CropFilter.OUTPUT_HEIGHT_KEY: self.__outputHeight, + CropFilter.OFFSET_X_KEY: self.__offsetX, + CropFilter.OFFSET_Y_KEY: self.__offsetY + }, + 'suffices': [], + 'variant': f"C{self.__outputWidth}-{self.__outputHeight}-{self.__offsetX}-{self.__offsetY}", + 'tokens': ['crop=' + + f"{self.__outputWidth}" + + f":{self.__outputHeight}" + + f":{self.__offsetX}" + + f":{self.__offsetY}"]} + + return payload + + + def getYield(self): + yield self.getPayload() diff --git a/src/ffx/filter/nlmeans_filter.py b/src/ffx/filter/nlmeans_filter.py index 4bd442d..dda10f6 100644 --- a/src/ffx/filter/nlmeans_filter.py +++ b/src/ffx/filter/nlmeans_filter.py @@ -144,11 +144,11 @@ class NlmeansFilter(Filter): 'suffices': suffices, 'variant': f"DS{strength}-DP{patchSize}-DPC{chromaPatchSize}" + f"-DR{researchWindow}-DRC{chromaResearchWindow}", - 'tokens': ['-vf', f"{filterName}=s={strength}" - + f":p={patchSize}" - + f":pc={chromaPatchSize}" - + f":r={researchWindow}" - + f":rc={chromaResearchWindow}"]} + 'tokens': [f"{filterName}=s={strength}" + + f":p={patchSize}" + + f":pc={chromaPatchSize}" + + f":r={researchWindow}" + + f":rc={chromaResearchWindow}"]} return payload