You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
741 lines
30 KiB
Python
741 lines
30 KiB
Python
#! /usr/bin/python3
|
|
|
|
import os, click, time, logging
|
|
|
|
from ffx.configuration_controller import ConfigurationController
|
|
|
|
from ffx.file_properties import FileProperties
|
|
|
|
from ffx.ffx_app import FfxApp
|
|
from ffx.ffx_controller import FfxController
|
|
from ffx.tmdb_controller import TmdbController
|
|
|
|
from ffx.database import databaseContext
|
|
|
|
from ffx.media_descriptor import MediaDescriptor
|
|
from ffx.track_descriptor import TrackDescriptor
|
|
from ffx.show_descriptor import ShowDescriptor
|
|
|
|
from ffx.track_type import TrackType
|
|
from ffx.video_encoder import VideoEncoder
|
|
from ffx.track_disposition import TrackDisposition
|
|
|
|
from ffx.process import executeProcess
|
|
from ffx.helper import filterFilename
|
|
|
|
from ffx.constants import DEFAULT_QUALITY, DEFAULT_AV1_PRESET
|
|
from ffx.constants import DEFAULT_STEREO_BANDWIDTH, DEFAULT_AC3_BANDWIDTH, DEFAULT_DTS_BANDWIDTH, DEFAULT_7_1_BANDWIDTH
|
|
|
|
from ffx.filter.quality_filter import QualityFilter
|
|
from ffx.filter.preset_filter import PresetFilter
|
|
|
|
from ffx.filter.nlmeans_filter import NlmeansFilter
|
|
|
|
|
|
|
|
VERSION='0.2.2'
|
|
|
|
# 0.1.1
|
|
# Bugfixes, TMBD identify shows
|
|
# 0.1.2
|
|
# Bugfixes
|
|
# 0.1.3
|
|
# Subtitle file imports
|
|
# 0.2.0
|
|
# Tests, Config-File
|
|
# 0.2.1
|
|
# Signature, Tags cleaning, Bugfixes, Refactoring
|
|
# 0.2.2
|
|
# CLI-Overrides
|
|
|
|
|
|
@click.group()
|
|
@click.pass_context
|
|
@click.option('--database-file', type=str, default='', help='Path to database file')
|
|
@click.option('-v', '--verbose', type=int, default=0, help='Set verbosity of output')
|
|
@click.option("--dry-run", is_flag=True, default=False)
|
|
def ffx(ctx, database_file, verbose, dry_run):
|
|
"""FFX"""
|
|
|
|
ctx.obj = {}
|
|
|
|
ctx.obj['config'] = ConfigurationController()
|
|
|
|
ctx.obj['database'] = databaseContext(databasePath=database_file
|
|
if database_file else ctx.obj['config'].getDatabaseFilePath())
|
|
|
|
ctx.obj['dry_run'] = dry_run
|
|
ctx.obj['verbosity'] = verbose
|
|
|
|
# Critical 50
|
|
# Error 40
|
|
# Warning 30
|
|
# Info 20
|
|
# Debug 10
|
|
fileLogVerbosity = max(40 - verbose * 10, 10)
|
|
consoleLogVerbosity = max(20 - verbose * 10, 10)
|
|
|
|
ctx.obj['logger'] = logging.getLogger('FFX')
|
|
ctx.obj['logger'].setLevel(logging.DEBUG)
|
|
|
|
ffxFileHandler = logging.FileHandler(ctx.obj['config'].getLogFilePath())
|
|
ffxFileHandler.setLevel(fileLogVerbosity)
|
|
ffxConsoleHandler = logging.StreamHandler()
|
|
ffxConsoleHandler.setLevel(consoleLogVerbosity)
|
|
|
|
fileFormatter = logging.Formatter(
|
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
ffxFileHandler.setFormatter(fileFormatter)
|
|
consoleFormatter = logging.Formatter(
|
|
'%(message)s')
|
|
ffxConsoleHandler.setFormatter(consoleFormatter)
|
|
|
|
ctx.obj['logger'].addHandler(ffxConsoleHandler)
|
|
ctx.obj['logger'].addHandler(ffxFileHandler)
|
|
|
|
|
|
# Define a subcommand
|
|
@ffx.command()
|
|
def version():
|
|
click.echo(VERSION)
|
|
|
|
|
|
# Another subcommand
|
|
@ffx.command()
|
|
def help():
|
|
click.echo(f"ffx {VERSION}\n")
|
|
click.echo(f"Usage: ffx [input file] [output file] [vp9|av1] [q=[nn[,nn,...]]] [p=nn] [a=nnn[k]] [ac3=nnn[k]] [dts=nnn[k]] [crop]")
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
@click.argument('filename', nargs=1)
|
|
def inspect(ctx, filename):
|
|
|
|
ctx.obj['command'] = 'inspect'
|
|
ctx.obj['arguments'] = {}
|
|
ctx.obj['arguments']['filename'] = filename
|
|
|
|
app = FfxApp(ctx.obj)
|
|
app.run()
|
|
|
|
#TODO: TrackCodec Klasse
|
|
CODEC_LOOKUP_TABLE = {
|
|
'h264': {'format': 'h264', 'extension': 'h264'},
|
|
'aac': { 'extension': 'aac'},
|
|
'ac3': {'format': 'ac3', 'extension': 'ac3'},
|
|
'ass': {'format': 'ass', 'extension': 'ass'},
|
|
'hdmv_pgs_subtitle': {'format': 'sup', 'extension': 'sup'}
|
|
}
|
|
|
|
def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix, targetDirectory = ''):
|
|
|
|
trackCodec = trackDescriptor.getCodec()
|
|
|
|
if not trackCodec in CODEC_LOOKUP_TABLE.keys():
|
|
return []
|
|
|
|
commandTokens = FfxController.COMMAND_TOKENS + ['-i', sourcePath]
|
|
trackType = trackDescriptor.getType()
|
|
|
|
targetPathBase = os.path.join(targetDirectory, targetPrefix) if targetDirectory else targetPrefix
|
|
|
|
commandTokens += ['-map',
|
|
f"0:{trackType.indicator()}:{trackDescriptor.getSubIndex()}",
|
|
'-c',
|
|
'copy']
|
|
|
|
if 'format' in CODEC_LOOKUP_TABLE[trackCodec].keys():
|
|
commandTokens += ['-f', CODEC_LOOKUP_TABLE[trackCodec]['format']]
|
|
|
|
commandTokens += [f"{targetPathBase}.{CODEC_LOOKUP_TABLE[trackCodec]['extension']}"]
|
|
|
|
return commandTokens
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
|
|
@click.argument('paths', nargs=-1)
|
|
@click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix')
|
|
@click.option("-o", "--output-directory", type=str, default='')
|
|
@click.option("-s", "--subtitles-only", is_flag=True, default=False)
|
|
@click.option('--nice', type=int, default=99, help='Niceness of started processes')
|
|
@click.option('--cpu', type=int, default=0, help='Limit CPU for started processes to percent')
|
|
def unmux(ctx,
|
|
paths,
|
|
label,
|
|
output_directory,
|
|
subtitles_only,
|
|
nice,
|
|
cpu):
|
|
|
|
existingSourcePaths = [p for p in paths if os.path.isfile(p)]
|
|
ctx.obj['logger'].debug(f"\nUnmuxing {len(existingSourcePaths)} files")
|
|
|
|
ctx.obj['resource_limits'] = {}
|
|
ctx.obj['resource_limits']['niceness'] = nice
|
|
ctx.obj['resource_limits']['cpu_percent'] = cpu
|
|
|
|
for sourcePath in existingSourcePaths:
|
|
|
|
fp = FileProperties(ctx.obj, sourcePath)
|
|
|
|
|
|
try:
|
|
sourceMediaDescriptor = fp.getMediaDescriptor()
|
|
|
|
season = fp.getSeason()
|
|
episode = fp.getEpisode()
|
|
|
|
#TODO: Recognition für alle Formate anpassen
|
|
targetLabel = label if label else fp.getFileBasename()
|
|
targetIndicator = f"_S{season}E{episode}" if label and season != -1 and episode != -1 else ''
|
|
|
|
if label and not targetIndicator:
|
|
ctx.obj['logger'].warning(f"Skipping file {fp.getFilename()}: Label set but no indicator recognized")
|
|
continue
|
|
else:
|
|
ctx.obj['logger'].debug(f"\nUnmuxing file {fp.getFilename()}\n")
|
|
|
|
for trackDescriptor in sourceMediaDescriptor.getAllTrackDescriptors():
|
|
|
|
if trackDescriptor.getType() == TrackType.SUBTITLE or not subtitles_only:
|
|
|
|
# SEASON_EPISODE_STREAM_LANGUAGE_MATCH = '[sS]([0-9]+)[eE]([0-9]+)_([0-9]+)_([a-z]{3})'
|
|
targetPrefix = f"{targetLabel}{targetIndicator}_{trackDescriptor.getIndex()}_{trackDescriptor.getLanguage().threeLetter()}"
|
|
|
|
unmuxSequence = getUnmuxSequence(trackDescriptor, sourcePath, targetPrefix, targetDirectory = output_directory)
|
|
|
|
if unmuxSequence:
|
|
if not ctx.obj['dry_run']:
|
|
ctx.obj['logger'].debug(f"Executing unmuxing sequence")
|
|
out, err, rc = executeProcess(unmuxSequence, context = ctx.obj)
|
|
if rc:
|
|
ctx.obj['logger'].error(f"Unmuxing of stream {trackDescriptor.getIndex()} failed with error ({rc}) {err}")
|
|
else:
|
|
ctx.obj['logger'].warning(f"Skipping stream with unknown codec {trackDescriptor.getCodec()}")
|
|
except Exception as ex:
|
|
ctx.obj['logger'].warning(f"Skipping File {sourcePath} ({ex})")
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
|
|
def shows(ctx):
|
|
|
|
ctx.obj['command'] = 'shows'
|
|
|
|
app = FfxApp(ctx.obj)
|
|
app.run()
|
|
|
|
|
|
def checkUniqueDispositions(context, mediaDescriptor: MediaDescriptor):
|
|
|
|
# Check for multiple default or forced dispositions if not set by user input or database requirements
|
|
#
|
|
# Query user for the correct sub indices, then configure flags in track descriptors associated with media descriptor accordingly.
|
|
# The correct tokens should then be created by
|
|
if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one default video stream detected and no prompt set')
|
|
defaultVideoTrackSubIndex = click.prompt("More than one default video stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setDefaultSubTrack(TrackType.VIDEO, defaultVideoTrackSubIndex)
|
|
|
|
if len([v for v in mediaDescriptor.getVideoTracks() if v.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one forced video stream detected and no prompt set')
|
|
forcedVideoTrackSubIndex = click.prompt("More than one forced video stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setForcedSubTrack(TrackType.VIDEO, forcedVideoTrackSubIndex)
|
|
|
|
if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one default audio stream detected and no prompt set')
|
|
defaultAudioTrackSubIndex = click.prompt("More than one default audio stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setDefaultSubTrack(TrackType.AUDIO, defaultAudioTrackSubIndex)
|
|
|
|
if len([a for a in mediaDescriptor.getAudioTracks() if a.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one forced audio stream detected and no prompt set')
|
|
forcedAudioTrackSubIndex = click.prompt("More than one forced audio stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setForcedSubTrack(TrackType.AUDIO, forcedAudioTrackSubIndex)
|
|
|
|
if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.DEFAULT)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one default subtitle stream detected and no prompt set')
|
|
defaultSubtitleTrackSubIndex = click.prompt("More than one default subtitle stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setDefaultSubTrack(TrackType.SUBTITLE, defaultSubtitleTrackSubIndex)
|
|
|
|
if len([s for s in mediaDescriptor.getSubtitleTracks() if s.getDispositionFlag(TrackDisposition.FORCED)]) > 1:
|
|
if context['no_prompt']:
|
|
raise click.ClickException('More than one forced subtitle stream detected and no prompt set')
|
|
forcedSubtitleTrackSubIndex = click.prompt("More than one forced subtitle stream detected! Please select stream", type=int)
|
|
mediaDescriptor.setForcedSubTrack(TrackType.SUBTITLE, forcedSubtitleTrackSubIndex)
|
|
|
|
|
|
@ffx.command()
|
|
@click.pass_context
|
|
|
|
@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=FfxController.DEFAULT_VIDEO_ENCODER, help=f"Target video encoder (vp9 or av1)", show_default=True)
|
|
|
|
@click.option('-q', '--quality', type=str, default="", help=f"Quality settings to be used with VP9 encoder")
|
|
@click.option('-p', '--preset', type=str, default="", help=f"Quality preset to be used with AV1 encoder")
|
|
|
|
@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", 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", show_default=True)
|
|
|
|
@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('--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('--default-video', type=int, default=-1, help='Index of default video stream')
|
|
@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('--forced-audio', type=int, default=-1, help='Index of forced audio 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')
|
|
|
|
@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("--output-directory", type=str, default='')
|
|
|
|
@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('--show', type=int, default=-1, help='Set TMDB show identifier')
|
|
@click.option('--season', type=int, default=-1, help='Set season of show')
|
|
@click.option('--episode', type=int, default=-1, help='Set episode of show')
|
|
|
|
@click.option("--no-tmdb", is_flag=True, default=False)
|
|
@click.option("--no-pattern", is_flag=True, default=False)
|
|
|
|
@click.option("--dont-pass-dispositions", is_flag=True, default=False)
|
|
|
|
@click.option("--no-prompt", is_flag=True, default=False)
|
|
@click.option("--no-signature", is_flag=True, default=False)
|
|
@click.option("--keep-mkvmerge-metadata", is_flag=True, default=False)
|
|
|
|
@click.option('--nice', type=int, default=99, help='Niceness of started processes')
|
|
@click.option('--cpu', type=int, default=0, help='Limit CPU for started processes to percent')
|
|
|
|
def convert(ctx,
|
|
paths,
|
|
label,
|
|
video_encoder,
|
|
quality,
|
|
preset,
|
|
stereo_bitrate,
|
|
ac3,
|
|
dts,
|
|
|
|
subtitle_directory,
|
|
subtitle_prefix,
|
|
|
|
language,
|
|
title,
|
|
|
|
default_video,
|
|
forced_video,
|
|
default_audio,
|
|
forced_audio,
|
|
default_subtitle,
|
|
forced_subtitle,
|
|
|
|
rearrange_streams,
|
|
|
|
crop,
|
|
output_directory,
|
|
|
|
denoise,
|
|
denoise_use_hw,
|
|
denoise_strength,
|
|
denoise_patch_size,
|
|
denoise_chroma_patch_size,
|
|
denoise_research_window,
|
|
denoise_chroma_research_window,
|
|
|
|
show,
|
|
season,
|
|
episode,
|
|
|
|
no_tmdb,
|
|
# no_jellyfin,
|
|
no_pattern,
|
|
dont_pass_dispositions,
|
|
no_prompt,
|
|
no_signature,
|
|
keep_mkvmerge_metadata,
|
|
|
|
nice,
|
|
cpu):
|
|
"""Batch conversion of audiovideo files in format suitable for web playback, e.g. jellyfin
|
|
|
|
Files found under PATHS will be converted according to parameters.
|
|
Filename extensions will be changed appropriately.
|
|
Suffices will we appended to filename in case of multiple created files
|
|
or if the filename has not changed."""
|
|
|
|
startTime = time.perf_counter()
|
|
|
|
context = ctx.obj
|
|
|
|
context['video_encoder'] = VideoEncoder.fromLabel(video_encoder)
|
|
|
|
targetFormat = FfxController.DEFAULT_FILE_FORMAT
|
|
targetExtension = FfxController.DEFAULT_FILE_EXTENSION
|
|
|
|
|
|
#TODO: #407 Without effect -> remove
|
|
context['use_jellyfin'] = False
|
|
|
|
context['use_tmdb'] = not no_tmdb
|
|
context['use_pattern'] = not no_pattern
|
|
context['no_prompt'] = no_prompt
|
|
context['no_signature'] = no_signature
|
|
context['keep_mkvmerge_metadata'] = keep_mkvmerge_metadata
|
|
|
|
|
|
context['resource_limits'] = {}
|
|
context['resource_limits']['niceness'] = nice
|
|
context['resource_limits']['cpu_percent'] = cpu
|
|
|
|
|
|
context['import_subtitles'] = (subtitle_directory and subtitle_prefix)
|
|
if context['import_subtitles']:
|
|
context['subtitle_directory'] = subtitle_directory
|
|
context['subtitle_prefix'] = subtitle_prefix
|
|
|
|
|
|
existingSourcePaths = [p for p in paths if os.path.isfile(p) and p.split('.')[-1] in FfxController.INPUT_FILE_EXTENSIONS]
|
|
|
|
|
|
# CLI Overrides
|
|
|
|
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 show != -1 or season != -1 or episode != -1:
|
|
if len(existingSourcePaths) > 1:
|
|
context['logger'].warning(f"Ignoring TMDB show, season, episode overrides, not supported for multiple source files")
|
|
else:
|
|
cliOverrides['tmdb'] = {}
|
|
if show != -1:
|
|
cliOverrides['tmdb']['show'] = show
|
|
if season != -1:
|
|
cliOverrides['tmdb']['season'] = season
|
|
if episode != -1:
|
|
cliOverrides['tmdb']['episode'] = episode
|
|
|
|
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}")
|
|
|
|
qualityTokens = quality.split(',')
|
|
q_list = [q for q in qualityTokens if q.isnumeric()]
|
|
ctx.obj['logger'].debug(f"Qualities: {q_list}")
|
|
|
|
presetTokens = preset.split(',')
|
|
p_list = [p for p in presetTokens if p.isnumeric()]
|
|
ctx.obj['logger'].debug(f"Presets: {p_list}")
|
|
|
|
|
|
context['bitrates'] = {}
|
|
context['bitrates']['stereo'] = str(stereo_bitrate) if str(stereo_bitrate).endswith('k') else f"{stereo_bitrate}k"
|
|
context['bitrates']['ac3'] = str(ac3) if str(ac3).endswith('k') else f"{ac3}k"
|
|
context['bitrates']['dts'] = str(dts) if str(dts).endswith('k') else f"{dts}k"
|
|
|
|
ctx.obj['logger'].debug(f"Stereo bitrate: {context['bitrates']['stereo']}")
|
|
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']}")
|
|
|
|
|
|
tc = TmdbController() if context['use_tmdb'] else None
|
|
|
|
qualityKwargs = {QualityFilter.QUALITY_KEY: quality}
|
|
qf = QualityFilter(**qualityKwargs)
|
|
|
|
if context['video_encoder'] == VideoEncoder.AV1 and preset:
|
|
presetKwargs = {PresetFilter.PRESET_KEY: preset}
|
|
PresetFilter(**presetKwargs)
|
|
|
|
denoiseKwargs = {}
|
|
if denoise_strength:
|
|
denoiseKwargs[NlmeansFilter.STRENGTH_KEY] = denoise_strength
|
|
if denoise_patch_size:
|
|
denoiseKwargs[NlmeansFilter.PATCH_SIZE_KEY] = denoise_patch_size
|
|
if denoise_chroma_patch_size:
|
|
denoiseKwargs[NlmeansFilter.CHROMA_PATCH_SIZE_KEY] = denoise_chroma_patch_size
|
|
if denoise_research_window:
|
|
denoiseKwargs[NlmeansFilter.RESEARCH_WINDOW_KEY] = denoise_research_window
|
|
if denoise_chroma_research_window:
|
|
denoiseKwargs[NlmeansFilter.CHROMA_RESEARCH_WINDOW_KEY] = denoise_chroma_research_window
|
|
if denoise != 'none' or denoiseKwargs:
|
|
NlmeansFilter(**denoiseKwargs)
|
|
|
|
chainYield = list(qf.getChainYield())
|
|
|
|
ctx.obj['logger'].info(f"\nRunning {len(existingSourcePaths) * len(chainYield)} jobs")
|
|
|
|
jobIndex = 0
|
|
|
|
for sourcePath in existingSourcePaths:
|
|
|
|
# Separate basedir, basename and extension for current source file
|
|
sourceDirectory = os.path.dirname(sourcePath)
|
|
sourceFilename = os.path.basename(sourcePath)
|
|
sourcePathTokens = sourceFilename.split('.')
|
|
|
|
sourceFileBasename = '.'.join(sourcePathTokens[:-1])
|
|
sourceFilenameExtension = sourcePathTokens[-1]
|
|
|
|
ctx.obj['logger'].info(f"\nProcessing file {sourcePath}")
|
|
|
|
targetSuffices = {}
|
|
|
|
|
|
mediaFileProperties = FileProperties(context, sourceFilename)
|
|
|
|
|
|
#HINT: -1 if not set
|
|
if 'tmdb' in cliOverrides.keys() and 'season' in cliOverrides['tmdb']:
|
|
showSeason = cliOverrides['tmdb']['season']
|
|
else:
|
|
showSeason = mediaFileProperties.getSeason()
|
|
|
|
if 'tmdb' in cliOverrides.keys() and 'episode' in cliOverrides['tmdb']:
|
|
showEpisode = cliOverrides['tmdb']['episode']
|
|
else:
|
|
showEpisode = mediaFileProperties.getEpisode()
|
|
|
|
|
|
ctx.obj['logger'].debug(f"Season={showSeason} Episode={showEpisode}")
|
|
|
|
|
|
sourceMediaDescriptor = mediaFileProperties.getMediaDescriptor()
|
|
|
|
#HINT: This is None if the filename did not match anything in database
|
|
currentPattern = mediaFileProperties.getPattern() if context['use_pattern'] else None
|
|
|
|
ctx.obj['logger'].debug(f"Pattern matching: {'No' if currentPattern is None else 'Yes'}")
|
|
|
|
# Setup FfxController accordingly depending on pattern matching is enabled and a pattern was matched
|
|
if currentPattern is None:
|
|
|
|
checkUniqueDispositions(context, sourceMediaDescriptor)
|
|
currentShowDescriptor = None
|
|
|
|
if context['import_subtitles']:
|
|
sourceMediaDescriptor.importSubtitles(context['subtitle_directory'],
|
|
context['subtitle_prefix'],
|
|
showSeason,
|
|
showEpisode)
|
|
|
|
if cliOverrides:
|
|
sourceMediaDescriptor.applyOverrides(cliOverrides)
|
|
|
|
fc = FfxController(context, sourceMediaDescriptor)
|
|
|
|
else:
|
|
targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj)
|
|
checkUniqueDispositions(context, targetMediaDescriptor)
|
|
currentShowDescriptor = currentPattern.getShowDescriptor(ctx.obj)
|
|
|
|
if context['import_subtitles']:
|
|
targetMediaDescriptor.importSubtitles(context['subtitle_directory'],
|
|
context['subtitle_prefix'],
|
|
showSeason,
|
|
showEpisode)
|
|
|
|
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 cliOverrides:
|
|
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"Input mapping tokens (2nd pass): {targetMediaDescriptor.getInputMappingTokens()}")
|
|
|
|
fc = FfxController(context, targetMediaDescriptor, sourceMediaDescriptor)
|
|
|
|
|
|
indexSeasonDigits = currentShowDescriptor.getIndexSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_SEASON_DIGITS
|
|
indexEpisodeDigits = currentShowDescriptor.getIndexEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDEX_EPISODE_DIGITS
|
|
indicatorSeasonDigits = currentShowDescriptor.getIndicatorSeasonDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_SEASON_DIGITS
|
|
indicatorEpisodeDigits = currentShowDescriptor.getIndicatorEpisodeDigits() if not currentPattern is None else ShowDescriptor.DEFAULT_INDICATOR_EPISODE_DIGITS
|
|
|
|
|
|
# Assemble target filename accordingly depending on TMDB lookup is enabled
|
|
#HINT: -1 if not set
|
|
showId = cliOverrides['tmdb']['show'] if 'tmdb' in cliOverrides.keys() and 'show' in cliOverrides['tmdb'] else (-1 if currentShowDescriptor is None else currentShowDescriptor.getId())
|
|
|
|
if context['use_tmdb'] and showId != -1 and showSeason != -1 and showEpisode != -1:
|
|
|
|
ctx.obj['logger'].debug(f"Querying TMDB for show_id={showId} season={showSeason} episode{showEpisode}")
|
|
|
|
if currentPattern is None:
|
|
sName, showYear = tc.getShowNameAndYear(showId)
|
|
showName = filterFilename(sName)
|
|
showFilenamePrefix = f"{showName} ({str(showYear)})"
|
|
else:
|
|
showFilenamePrefix = currentShowDescriptor.getFilenamePrefix()
|
|
|
|
tmdbEpisodeResult = tc.queryEpisode(showId, showSeason, showEpisode)
|
|
|
|
ctx.obj['logger'].debug(f"tmdbEpisodeResult={tmdbEpisodeResult}")
|
|
|
|
if tmdbEpisodeResult:
|
|
filteredEpisodeName = filterFilename(tmdbEpisodeResult['name'])
|
|
sourceFileBasename = TmdbController.getEpisodeFileBasename(showFilenamePrefix,
|
|
filteredEpisodeName,
|
|
showSeason,
|
|
showEpisode,
|
|
indexSeasonDigits,
|
|
indexEpisodeDigits,
|
|
indicatorSeasonDigits,
|
|
indicatorEpisodeDigits)
|
|
|
|
|
|
if label:
|
|
if showSeason > -1 and showEpisode > -1:
|
|
targetSuffices['se'] = f"S{showSeason:0{indicatorSeasonDigits}d}E{showEpisode:0{indicatorEpisodeDigits}d}"
|
|
elif showEpisode > -1:
|
|
targetSuffices['se'] = f"E{showEpisode:0{indicatorEpisodeDigits}d}"
|
|
else:
|
|
if 'se' in targetSuffices.keys():
|
|
del targetSuffices['se']
|
|
|
|
ctx.obj['logger'].debug(f"fileBasename={sourceFileBasename}")
|
|
|
|
|
|
for chainIteration in chainYield:
|
|
|
|
|
|
ctx.obj['logger'].debug(f"\nchain iteration: {chainIteration}\n")
|
|
|
|
# if len(q_list) > 1:
|
|
# targetSuffices['q'] = f"q{q}"
|
|
|
|
chainVariant = '-'.join([fy['variant'] for fy in chainIteration])
|
|
|
|
ctx.obj['logger'].debug(f"\nRunning job {jobIndex} file={sourcePath} variant={chainVariant}")
|
|
jobIndex += 1
|
|
|
|
ctx.obj['logger'].debug(f"label={label if label else 'Falsy'}")
|
|
ctx.obj['logger'].debug(f"sourceFileBasename={sourceFileBasename}")
|
|
|
|
# targetFileBasename = mediaFileProperties.assembleTargetFileBasename(label,
|
|
# q if len(q_list) > 1 else -1,
|
|
#
|
|
targetFileBasename = sourceFileBasename if context['use_tmdb'] and not label else label
|
|
|
|
|
|
targetFilenameTokens = [targetFileBasename]
|
|
|
|
if 'se' in targetSuffices.keys():
|
|
targetFilenameTokens += [targetSuffices['se']]
|
|
|
|
# if 'q' in targetSuffices.keys():
|
|
# targetFilenameTokens += [targetSuffices['q']]
|
|
for filterYield in chainIteration:
|
|
|
|
# filterIdentifier = filterYield['identifier']
|
|
# filterParameters = filterYield['parameters']
|
|
# filterSuffices = filterYield['suffices']
|
|
|
|
targetFilenameTokens += filterYield['suffices']
|
|
|
|
#TODO #387
|
|
# targetFilename = ((f"{sourceFileBasename}_q{q}" if len(q_list) > 1 else sourceFileBasename)
|
|
# if context['use_tmdb'] else targetFileBasename)
|
|
|
|
targetFilename = f"{'_'.join(targetFilenameTokens)}.{targetExtension}"
|
|
|
|
targetPath = os.path.join(output_directory if output_directory else sourceDirectory, targetFilename)
|
|
|
|
#TODO: target extension anpassen
|
|
ctx.obj['logger'].info(f"Creating file {targetFilename}")
|
|
|
|
fc.runJob(sourcePath,
|
|
targetPath,
|
|
targetFormat,
|
|
context['video_encoder'],
|
|
chainIteration)
|
|
|
|
#TODO: click.confirm('Warning! This file is not compliant to the defined source schema! Do you want to continue?', abort=True)
|
|
|
|
endTime = time.perf_counter()
|
|
ctx.obj['logger'].info(f"\nDONE\nTime elapsed {endTime - startTime}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ffx()
|