From ead989ff91505ed9240cd6911735c98b9171d1e2 Mon Sep 17 00:00:00 2001 From: David Maisonave <47364845+David-Maisonave@users.noreply.github.com> Date: Sat, 17 Aug 2024 02:10:27 -0400 Subject: [PATCH] Update StashPluginHelper.py --- StashPluginHelper/StashPluginHelper.py | 211 ++++++++++++++++++------- 1 file changed, 150 insertions(+), 61 deletions(-) diff --git a/StashPluginHelper/StashPluginHelper.py b/StashPluginHelper/StashPluginHelper.py index eaeae19..7301f0f 100644 --- a/StashPluginHelper/StashPluginHelper.py +++ b/StashPluginHelper/StashPluginHelper.py @@ -1,12 +1,8 @@ -import stashapi.log as stashLog # stashapi.log by default for error and critical logging from stashapi.stashapp import StashInterface from logging.handlers import RotatingFileHandler -import inspect -import sys -import os -import pathlib -import logging -import json +import inspect, sys, os, pathlib, logging, json +import concurrent.futures +from stashapi.stash_types import PhashDistance import __main__ # StashPluginHelper (By David Maisonave aka Axter) @@ -16,7 +12,6 @@ import __main__ # Logging includes source code line number # Sets a maximum plugin log file size # Stash Interface Features: - # Sets STASH_INTERFACE with StashInterface # Gets STASH_URL value from command line argument and/or from STDIN_READ # Sets FRAGMENT_SERVER based on command line arguments or STDIN_READ # Sets PLUGIN_ID based on the main script file name (in lower case) @@ -27,14 +22,15 @@ import __main__ # Gets DEBUG_TRACING value from command line argument and/or from UI and/or from config file # Sets RUNNING_IN_COMMAND_LINE_MODE to True if detects multiple arguments # Sets CALLED_AS_STASH_PLUGIN to True if it's able to read from STDIN_READ -class StashPluginHelper: +class StashPluginHelper(StashInterface): # Primary Members for external reference PLUGIN_TASK_NAME = None PLUGIN_ID = None PLUGIN_CONFIGURATION = None + PLUGINS_PATH = None pluginSettings = None pluginConfig = None - STASH_INTERFACE = None + STASH_INTERFACE_INIT = False STASH_URL = None STASH_CONFIGURATION = None JSON_INPUT = None @@ -42,6 +38,9 @@ class StashPluginHelper: DRY_RUN = False CALLED_AS_STASH_PLUGIN = False RUNNING_IN_COMMAND_LINE_MODE = False + FRAGMENT_SERVER = None + STASHPATHSCONFIG = None + STASH_PATHS = [] # printTo argument LOG_TO_FILE = 1 @@ -59,9 +58,9 @@ class StashPluginHelper: LOG_FILE_DIR = None LOG_FILE_NAME = None STDIN_READ = None - FRAGMENT_SERVER = None - logger = None - traceOncePreviousHits = [] + pluginLog = None + logLinePreviousHits = [] + thredPool = None # Prefix message value LEV_TRACE = "TRACE: " @@ -77,7 +76,8 @@ class StashPluginHelper: # Externally modifiable variables log_to_err_set = LOG_TO_FILE + LOG_TO_STDERR # This can be changed by the calling source in order to customize what targets get error messages log_to_norm = LOG_TO_FILE + LOG_TO_CONSOLE # Can be change so-as to set target output for normal logging - log_to_wrn_set = LOG_TO_FILE + LOG_TO_STASH # This can be changed by the calling source in order to customize what targets get warning messages + # Warn message goes to both plugin log file and stash when sent to Stash log file. + log_to_wrn_set = LOG_TO_STASH # This can be changed by the calling source in order to customize what targets get warning messages def __init__(self, debugTracing = None, # Set debugTracing to True so as to output debug and trace logging @@ -96,7 +96,9 @@ class StashPluginHelper: fragmentServer = None, stash_url = None, # Stash URL (endpoint URL) Example: http://localhost:9999 DebugTraceFieldName = "zzdebugTracing", - DryRunFieldName = "zzdryRun"): + DryRunFieldName = "zzdryRun", + setStashLoggerAsPluginLogger = False): + self.thredPool = concurrent.futures.ThreadPoolExecutor(max_workers=2) if logToWrnSet: self.log_to_wrn_set = logToWrnSet if logToErrSet: self.log_to_err_set = logToErrSet if logToNormSet: self.log_to_norm = logToNormSet @@ -143,7 +145,8 @@ class StashPluginHelper: self.FRAGMENT_SERVER['Scheme'] = endpointUrlArr[0] self.FRAGMENT_SERVER['Host'] = endpointUrlArr[1][2:] self.FRAGMENT_SERVER['Port'] = endpointUrlArr[2] - self.STASH_INTERFACE = self.ExtendStashInterface(self.FRAGMENT_SERVER) + super().__init__(self.FRAGMENT_SERVER) + self.STASH_INTERFACE_INIT = True else: try: self.STDIN_READ = sys.stdin.read() @@ -156,11 +159,17 @@ class StashPluginHelper: self.PLUGIN_TASK_NAME = self.JSON_INPUT["args"]["mode"] self.FRAGMENT_SERVER = self.JSON_INPUT["server_connection"] self.STASH_URL = f"{self.FRAGMENT_SERVER['Scheme']}://{self.FRAGMENT_SERVER['Host']}:{self.FRAGMENT_SERVER['Port']}" - self.STASH_INTERFACE = self.ExtendStashInterface(self.FRAGMENT_SERVER) + super().__init__(self.FRAGMENT_SERVER) + self.STASH_INTERFACE_INIT = True - if self.STASH_INTERFACE: - self.PLUGIN_CONFIGURATION = self.STASH_INTERFACE.get_configuration()["plugins"] - self.STASH_CONFIGURATION = self.STASH_INTERFACE.get_configuration()["general"] + if self.STASH_INTERFACE_INIT: + self.PLUGIN_CONFIGURATION = self.get_configuration()["plugins"] + self.STASH_CONFIGURATION = self.get_configuration()["general"] + self.STASHPATHSCONFIG = self.STASH_CONFIGURATION['stashes'] + if 'pluginsPath' in self.STASH_CONFIGURATION: + self.PLUGINS_PATH = self.STASH_CONFIGURATION['pluginsPath'] + for item in self.STASHPATHSCONFIG: + self.STASH_PATHS.append(item["path"]) if settings: self.pluginSettings = settings if self.PLUGIN_ID in self.PLUGIN_CONFIGURATION: @@ -172,7 +181,12 @@ class StashPluginHelper: if self.DEBUG_TRACING: self.LOG_LEVEL = logging.DEBUG logging.basicConfig(level=self.LOG_LEVEL, format=logFormat, datefmt=dateFmt, handlers=[RFH]) - self.logger = logging.getLogger(pathlib.Path(self.MAIN_SCRIPT_NAME).stem) + self.pluginLog = logging.getLogger(pathlib.Path(self.MAIN_SCRIPT_NAME).stem) + if setStashLoggerAsPluginLogger: + self.log = self.pluginLog + + def __del__(self): + self.thredPool.shutdown(wait=False) def Log(self, logMsg, printTo = 0, logLevel = logging.INFO, lineNo = -1, levelStr = "", logAlways = False): if printTo == 0: @@ -192,32 +206,33 @@ class StashPluginHelper: # print(f"{LN_Str}, {logAlways}, {self.LOG_LEVEL}, {logging.DEBUG}, {levelStr}, {logMsg}") if logLevel == logging.DEBUG and (logAlways == False or self.LOG_LEVEL == logging.DEBUG): if levelStr == "": levelStr = self.LEV_DBG - if printTo & self.LOG_TO_FILE: self.logger.debug(f"{LN_Str} {levelStr}{logMsg}") - if printTo & self.LOG_TO_STASH: stashLog.debug(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_FILE: self.pluginLog.debug(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_STASH: self.log.debug(f"{LN_Str} {levelStr}{logMsg}") elif logLevel == logging.INFO or logLevel == logging.DEBUG: if levelStr == "": levelStr = self.LEV_INF if logLevel == logging.INFO else self.LEV_DBG - if printTo & self.LOG_TO_FILE: self.logger.info(f"{LN_Str} {levelStr}{logMsg}") - if printTo & self.LOG_TO_STASH: stashLog.info(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_FILE: self.pluginLog.info(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_STASH: self.log.info(f"{LN_Str} {levelStr}{logMsg}") elif logLevel == logging.WARN: if levelStr == "": levelStr = self.LEV_WRN - if printTo & self.LOG_TO_FILE: self.logger.warning(f"{LN_Str} {levelStr}{logMsg}") - if printTo & self.LOG_TO_STASH: stashLog.warning(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_FILE: self.pluginLog.warning(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_STASH: self.log.warning(f"{LN_Str} {levelStr}{logMsg}") elif logLevel == logging.ERROR: if levelStr == "": levelStr = self.LEV_ERR - if printTo & self.LOG_TO_FILE: self.logger.error(f"{LN_Str} {levelStr}{logMsg}") - if printTo & self.LOG_TO_STASH: stashLog.error(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_FILE: self.pluginLog.error(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_STASH: self.log.error(f"{LN_Str} {levelStr}{logMsg}") elif logLevel == logging.CRITICAL: if levelStr == "": levelStr = self.LEV_CRITICAL - if printTo & self.LOG_TO_FILE: self.logger.critical(f"{LN_Str} {levelStr}{logMsg}") - if printTo & self.LOG_TO_STASH: stashLog.error(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_FILE: self.pluginLog.critical(f"{LN_Str} {levelStr}{logMsg}") + if printTo & self.LOG_TO_STASH: self.log.error(f"{LN_Str} {levelStr}{logMsg}") if (printTo & self.LOG_TO_CONSOLE) and (logLevel != logging.DEBUG or self.DEBUG_TRACING or logAlways): print(f"{LN_Str} {levelStr}{logMsg}") if (printTo & self.LOG_TO_STDERR) and (logLevel != logging.DEBUG or self.DEBUG_TRACING or logAlways): print(f"StdErr: {LN_Str} {levelStr}{logMsg}", file=sys.stderr) - def Trace(self, logMsg = "", printTo = 0, logAlways = False): + def Trace(self, logMsg = "", printTo = 0, logAlways = False, lineNo = -1): if printTo == 0: printTo = self.LOG_TO_FILE - lineNo = inspect.currentframe().f_back.f_lineno + if lineNo == -1: + lineNo = inspect.currentframe().f_back.f_lineno logLev = logging.INFO if logAlways else logging.DEBUG if self.DEBUG_TRACING or logAlways: if logMsg == "": @@ -226,17 +241,25 @@ class StashPluginHelper: # Log once per session. Only logs the first time called from a particular line number in the code. def TraceOnce(self, logMsg = "", printTo = 0, logAlways = False): - if printTo == 0: printTo = self.LOG_TO_FILE lineNo = inspect.currentframe().f_back.f_lineno - logLev = logging.INFO if logAlways else logging.DEBUG if self.DEBUG_TRACING or logAlways: FuncAndLineNo = f"{inspect.currentframe().f_back.f_code.co_name}:{lineNo}" - if FuncAndLineNo in traceOncePreviousHits: + if FuncAndLineNo in self.logLinePreviousHits: return - traceOncePreviousHits.append(FuncAndLineNo) - if logMsg == "": - logMsg = f"Line number {lineNo}..." - self.Log(logMsg, printTo, logLev, lineNo, self.LEV_TRACE, logAlways) + self.logLinePreviousHits.append(FuncAndLineNo) + self.Trace(logMsg, printTo, logAlways, lineNo) + + # Log INFO on first call, then do Trace on remaining calls. + def LogOnce(self, logMsg = "", printTo = 0, logAlways = False, traceOnRemainingCalls = True): + if printTo == 0: printTo = self.LOG_TO_FILE + lineNo = inspect.currentframe().f_back.f_lineno + FuncAndLineNo = f"{inspect.currentframe().f_back.f_code.co_name}:{lineNo}" + if FuncAndLineNo in self.logLinePreviousHits: + if traceOnRemainingCalls: + self.Trace(logMsg, printTo, logAlways, lineNo) + else: + self.logLinePreviousHits.append(FuncAndLineNo) + self.Log(logMsg, printTo, logging.INFO, lineNo) def Warn(self, logMsg, printTo = 0): if printTo == 0: printTo = self.log_to_wrn_set @@ -255,26 +278,92 @@ class StashPluginHelper: self.Log(f"StashPluginHelper Status: (CALLED_AS_STASH_PLUGIN={self.CALLED_AS_STASH_PLUGIN}), (RUNNING_IN_COMMAND_LINE_MODE={self.RUNNING_IN_COMMAND_LINE_MODE}), (DEBUG_TRACING={self.DEBUG_TRACING}), (DRY_RUN={self.DRY_RUN}), (PLUGIN_ID={self.PLUGIN_ID}), (PLUGIN_TASK_NAME={self.PLUGIN_TASK_NAME}), (STASH_URL={self.STASH_URL}), (MAIN_SCRIPT_NAME={self.MAIN_SCRIPT_NAME})", printTo, logLevel, lineNo) + def ExecuteProcess(self, args, ExecDetach=False): + import platform, subprocess + is_windows = any(platform.win32_ver()) + pid = None + self.Trace(f"is_windows={is_windows} args={args}") + if is_windows: + if ExecDetach: + self.Trace("Executing process using Windows DETACHED_PROCESS") + DETACHED_PROCESS = 0x00000008 + pid = subprocess.Popen(args,creationflags=DETACHED_PROCESS, shell=True).pid + else: + pid = subprocess.Popen(args, shell=True).pid + else: + self.Trace("Executing process using normal Popen") + pid = subprocess.Popen(args).pid + self.Trace(f"pid={pid}") + return pid + + def ExecutePythonScript(self, args, ExecDetach=True): + PythonExe = f"{sys.executable}" + argsWithPython = [f"{PythonExe}"] + args + return self.ExecuteProcess(argsWithPython,ExecDetach=ExecDetach) + + def Submit(*args, **kwargs): + thredPool.submit(*args, **kwargs) + # Extends class StashInterface with functions which are not yet in the class - class ExtendStashInterface(StashInterface): - def metadata_autotag(self, paths:list=[], dry_run=False): - if not paths: - return + def metadata_autotag(self, paths:list=[], performers:list=[], studios:list=[], tags:list=[]): + query = """ + mutation MetadataAutoTag($input:AutoTagMetadataInput!) { + metadataAutoTag(input: $input) + } + """ + metadata_autotag_input = { + "paths":paths, + "performers": performers, + "studios":studios, + "tags":tags, + } + result = self.call_GQL(query, {"input": metadata_autotag_input}) + return result + + def backup_database(self): + return self.call_GQL("mutation { backupDatabase(input: {download: false})}") - query = """ - mutation MetadataAutoTag($input:AutoTagMetadataInput!) { - metadataAutoTag(input: $input) - } - """ - - metadata_autotag_input = { - "paths": paths - } - result = self.call_GQL(query, {"input": metadata_autotag_input}) - return result - - def backup_database(self): - return self.call_GQL("mutation { backupDatabase(input: {download: false})}") - - def optimise_database(self): - return self.call_GQL("mutation OptimiseDatabase { optimiseDatabase }") + def optimise_database(self): + return self.call_GQL("mutation OptimiseDatabase { optimiseDatabase }") + + def metadata_clean_generated(self, blobFiles=True, dryRun=False, imageThumbnails=True, markers=True, screenshots=True, sprites=True, transcodes=True): + query = """ + mutation MetadataCleanGenerated($input: CleanGeneratedInput!) { + metadataCleanGenerated(input: $input) + } + """ + clean_metadata_input = { + "blobFiles": blobFiles, + "dryRun": dryRun, + "imageThumbnails": imageThumbnails, + "markers": markers, + "screenshots": screenshots, + "sprites": sprites, + "transcodes": transcodes, + } + result = self.call_GQL(query, {"input": clean_metadata_input}) + return result + + def rename_generated_files(self): + return self.call_GQL("mutation MigrateHashNaming {migrateHashNaming}") + # def find_duplicate_scenes(self, distance: PhashDistance=PhashDistance.EXACT, fragment=None): + # query = """ + # query FindDuplicateScenes($distance: Int) { + # findDuplicateScenes(distance: $distance) { + # ...SceneSlim + # } + # } + # """ + # if fragment: + # query = re.sub(r'\.\.\.SceneSlim', fragment, query) + # else: + # query = """ + # query FindDuplicateScenes($distance: Int) { + # findDuplicateScenes(distance: $distance) + # } + # """ + # variables = { + # "distance": distance + # } + # result = self.call_GQL(query, variables) + # return result['findDuplicateScenes'] \ No newline at end of file