forked from Github/Axter-Stash
Added logic to allow RenameFile to close open file handles before renaming
This commit is contained in:
@@ -1,29 +1,31 @@
|
|||||||
|
"""
|
||||||
|
StashPluginHelper (By David Maisonave aka Axter)
|
||||||
|
See end of this file for example usage
|
||||||
|
Log Features:
|
||||||
|
Can optionally log out to multiple outputs for each Log or Trace call.
|
||||||
|
Logging includes source code line number
|
||||||
|
Sets a maximum plugin log file size
|
||||||
|
Stash Interface Features:
|
||||||
|
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)
|
||||||
|
Gets PLUGIN_TASK_NAME value
|
||||||
|
Sets pluginSettings (The plugin UI settings)
|
||||||
|
Misc Features:
|
||||||
|
Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
||||||
|
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
|
||||||
|
"""
|
||||||
from stashapi.stashapp import StashInterface
|
from stashapi.stashapp import StashInterface
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import re, inspect, sys, os, pathlib, logging, json
|
import re, inspect, sys, os, pathlib, logging, json, ctypes
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from stashapi.stash_types import PhashDistance
|
from stashapi.stash_types import PhashDistance
|
||||||
import __main__
|
import __main__
|
||||||
|
|
||||||
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
||||||
|
|
||||||
# StashPluginHelper (By David Maisonave aka Axter)
|
|
||||||
# See end of this file for example usage
|
|
||||||
# Log Features:
|
|
||||||
# Can optionally log out to multiple outputs for each Log or Trace call.
|
|
||||||
# Logging includes source code line number
|
|
||||||
# Sets a maximum plugin log file size
|
|
||||||
# Stash Interface Features:
|
|
||||||
# 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)
|
|
||||||
# Gets PLUGIN_TASK_NAME value
|
|
||||||
# Sets pluginSettings (The plugin UI settings)
|
|
||||||
# Misc Features:
|
|
||||||
# Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
|
||||||
# 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(StashInterface):
|
class StashPluginHelper(StashInterface):
|
||||||
# Primary Members for external reference
|
# Primary Members for external reference
|
||||||
PLUGIN_TASK_NAME = None
|
PLUGIN_TASK_NAME = None
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
|
"""
|
||||||
|
StashPluginHelper (By David Maisonave aka Axter)
|
||||||
|
See end of this file for example usage
|
||||||
|
Log Features:
|
||||||
|
Can optionally log out to multiple outputs for each Log or Trace call.
|
||||||
|
Logging includes source code line number
|
||||||
|
Sets a maximum plugin log file size
|
||||||
|
Stash Interface Features:
|
||||||
|
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)
|
||||||
|
Gets PLUGIN_TASK_NAME value
|
||||||
|
Sets pluginSettings (The plugin UI settings)
|
||||||
|
Misc Features:
|
||||||
|
Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
||||||
|
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
|
||||||
|
"""
|
||||||
from stashapi.stashapp import StashInterface
|
from stashapi.stashapp import StashInterface
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import re, inspect, sys, os, pathlib, logging, json
|
import re, inspect, sys, os, pathlib, logging, json, ctypes
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from stashapi.stash_types import PhashDistance
|
from stashapi.stash_types import PhashDistance
|
||||||
import __main__
|
import __main__
|
||||||
|
|
||||||
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
||||||
|
|
||||||
# StashPluginHelper (By David Maisonave aka Axter)
|
|
||||||
# See end of this file for example usage
|
|
||||||
# Log Features:
|
|
||||||
# Can optionally log out to multiple outputs for each Log or Trace call.
|
|
||||||
# Logging includes source code line number
|
|
||||||
# Sets a maximum plugin log file size
|
|
||||||
# Stash Interface Features:
|
|
||||||
# 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)
|
|
||||||
# Gets PLUGIN_TASK_NAME value
|
|
||||||
# Sets pluginSettings (The plugin UI settings)
|
|
||||||
# Misc Features:
|
|
||||||
# Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
|
||||||
# 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(StashInterface):
|
class StashPluginHelper(StashInterface):
|
||||||
# Primary Members for external reference
|
# Primary Members for external reference
|
||||||
PLUGIN_TASK_NAME = None
|
PLUGIN_TASK_NAME = None
|
||||||
|
|||||||
@@ -592,9 +592,9 @@ def start_library_monitor():
|
|||||||
if lastScanJob['timeOutDelayProcess'] > MAX_TIMEOUT_FOR_DELAY_PATH_PROCESS:
|
if lastScanJob['timeOutDelayProcess'] > MAX_TIMEOUT_FOR_DELAY_PATH_PROCESS:
|
||||||
lastScanJob['timeOutDelayProcess'] = MAX_TIMEOUT_FOR_DELAY_PATH_PROCESS
|
lastScanJob['timeOutDelayProcess'] = MAX_TIMEOUT_FOR_DELAY_PATH_PROCESS
|
||||||
timeOutInSeconds = lastScanJob['timeOutDelayProcess']
|
timeOutInSeconds = lastScanJob['timeOutDelayProcess']
|
||||||
stash.LogOnce(f"Awaiting file change-trigger, with a short timeout ({timeOutInSeconds} seconds), because of active delay path processing.")
|
stash.Log(f"Awaiting file change-trigger, with a short timeout ({timeOutInSeconds} seconds), because of active delay path processing.")
|
||||||
else:
|
else:
|
||||||
stash.LogOnce(f"Waiting for a file change-trigger. Timeout = {timeOutInSeconds} seconds.")
|
stash.Log(f"Waiting for a file change-trigger. Timeout = {timeOutInSeconds} seconds.")
|
||||||
signal.wait(timeout=timeOutInSeconds)
|
signal.wait(timeout=timeOutInSeconds)
|
||||||
if lastScanJob['DelayedProcessTargetPaths'] != []:
|
if lastScanJob['DelayedProcessTargetPaths'] != []:
|
||||||
stash.TraceOnce(f"Processing delay scan for path(s) {lastScanJob['DelayedProcessTargetPaths']}")
|
stash.TraceOnce(f"Processing delay scan for path(s) {lastScanJob['DelayedProcessTargetPaths']}")
|
||||||
@@ -727,29 +727,35 @@ def start_library_monitor_service():
|
|||||||
args = args + ["-a", stash.API_KEY]
|
args = args + ["-a", stash.API_KEY]
|
||||||
stash.ExecutePythonScript(args)
|
stash.ExecutePythonScript(args)
|
||||||
|
|
||||||
if parse_args.stop or parse_args.restart or stash.PLUGIN_TASK_NAME == "stop_library_monitor":
|
runTypeID=0
|
||||||
stop_library_monitor()
|
runTypeName=["NothingToDo", "stop_library_monitor", "StartFileMonitorAsAServiceTaskID", "StartFileMonitorAsAPluginTaskID", "CommandLineStartLibMonitor"]
|
||||||
if parse_args.restart:
|
try:
|
||||||
time.sleep(5)
|
if parse_args.stop or parse_args.restart or stash.PLUGIN_TASK_NAME == "stop_library_monitor":
|
||||||
stash.run_plugin_task(plugin_id=stash.PLUGIN_ID, task_name=StartFileMonitorAsAPluginTaskName)
|
runTypeID=1
|
||||||
stash.Trace(f"Restart FileMonitor EXIT")
|
stop_library_monitor()
|
||||||
else:
|
if parse_args.restart:
|
||||||
stash.Trace(f"Stop FileMonitor EXIT")
|
time.sleep(5)
|
||||||
elif stash.PLUGIN_TASK_NAME == StartFileMonitorAsAServiceTaskID:
|
stash.run_plugin_task(plugin_id=stash.PLUGIN_ID, task_name=StartFileMonitorAsAPluginTaskName)
|
||||||
start_library_monitor_service()
|
stash.Trace(f"Restart FileMonitor EXIT")
|
||||||
stash.Trace(f"{StartFileMonitorAsAServiceTaskID} EXIT")
|
else:
|
||||||
elif stash.PLUGIN_TASK_NAME == StartFileMonitorAsAPluginTaskID:
|
stash.Trace(f"Stop FileMonitor EXIT")
|
||||||
start_library_monitor()
|
elif stash.PLUGIN_TASK_NAME == StartFileMonitorAsAServiceTaskID:
|
||||||
stash.Trace(f"{StartFileMonitorAsAPluginTaskID} EXIT")
|
runTypeID=2
|
||||||
elif not stash.CALLED_AS_STASH_PLUGIN:
|
start_library_monitor_service()
|
||||||
try:
|
stash.Trace(f"{StartFileMonitorAsAServiceTaskID} EXIT")
|
||||||
|
elif stash.PLUGIN_TASK_NAME == StartFileMonitorAsAPluginTaskID:
|
||||||
|
runTypeID=3
|
||||||
|
start_library_monitor()
|
||||||
|
stash.Trace(f"{StartFileMonitorAsAPluginTaskID} EXIT")
|
||||||
|
elif not stash.CALLED_AS_STASH_PLUGIN:
|
||||||
|
runTypeID=4
|
||||||
start_library_monitor()
|
start_library_monitor()
|
||||||
stash.Trace("Command line FileMonitor EXIT")
|
stash.Trace("Command line FileMonitor EXIT")
|
||||||
except Exception as e:
|
else:
|
||||||
tb = traceback.format_exc()
|
stash.Log(f"Nothing to do!!! (stash.PLUGIN_TASK_NAME={stash.PLUGIN_TASK_NAME})")
|
||||||
stash.Error(f"Exception while running FileMonitor from the command line. Error: {e}\nTraceBack={tb}")
|
except Exception as e:
|
||||||
stash.log.exception('Got exception on main handler')
|
tb = traceback.format_exc()
|
||||||
else:
|
stash.Error(f"Exception while running FileMonitor. runType='{runTypeName[runTypeID]}'; Error: {e}\nTraceBack={tb}")
|
||||||
stash.Log(f"Nothing to do!!! (stash.PLUGIN_TASK_NAME={stash.PLUGIN_TASK_NAME})")
|
stash.log.exception('Got exception on main handler')
|
||||||
|
|
||||||
stash.Trace("\n*********************************\nEXITING ***********************\n*********************************")
|
stash.Trace("\n*********************************\nEXITING ***********************\n*********************************")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RenameFile: Ver 0.4.6 (By David Maisonave)
|
# RenameFile: Ver 0.5.0 (By David Maisonave)
|
||||||
RenameFile is a [Stash](https://github.com/stashapp/stash) plugin which performs the following tasks.
|
RenameFile is a [Stash](https://github.com/stashapp/stash) plugin which performs the following tasks.
|
||||||
- **Rename Scene File Name** (On-The-Fly)
|
- **Rename Scene File Name** (On-The-Fly)
|
||||||
- **Append tag names** to file name
|
- **Append tag names** to file name
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
|
"""
|
||||||
|
StashPluginHelper (By David Maisonave aka Axter)
|
||||||
|
See end of this file for example usage
|
||||||
|
Log Features:
|
||||||
|
Can optionally log out to multiple outputs for each Log or Trace call.
|
||||||
|
Logging includes source code line number
|
||||||
|
Sets a maximum plugin log file size
|
||||||
|
Stash Interface Features:
|
||||||
|
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)
|
||||||
|
Gets PLUGIN_TASK_NAME value
|
||||||
|
Sets pluginSettings (The plugin UI settings)
|
||||||
|
Misc Features:
|
||||||
|
Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
||||||
|
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
|
||||||
|
"""
|
||||||
from stashapi.stashapp import StashInterface
|
from stashapi.stashapp import StashInterface
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import re, inspect, sys, os, pathlib, logging, json
|
import re, inspect, sys, os, pathlib, logging, json, ctypes
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from stashapi.stash_types import PhashDistance
|
from stashapi.stash_types import PhashDistance
|
||||||
import __main__
|
import __main__
|
||||||
|
|
||||||
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
_ARGUMENT_UNSPECIFIED_ = "_ARGUMENT_UNSPECIFIED_"
|
||||||
|
|
||||||
# StashPluginHelper (By David Maisonave aka Axter)
|
|
||||||
# See end of this file for example usage
|
|
||||||
# Log Features:
|
|
||||||
# Can optionally log out to multiple outputs for each Log or Trace call.
|
|
||||||
# Logging includes source code line number
|
|
||||||
# Sets a maximum plugin log file size
|
|
||||||
# Stash Interface Features:
|
|
||||||
# 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)
|
|
||||||
# Gets PLUGIN_TASK_NAME value
|
|
||||||
# Sets pluginSettings (The plugin UI settings)
|
|
||||||
# Misc Features:
|
|
||||||
# Gets DRY_RUN value from command line argument and/or from UI and/or from config file
|
|
||||||
# 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(StashInterface):
|
class StashPluginHelper(StashInterface):
|
||||||
# Primary Members for external reference
|
# Primary Members for external reference
|
||||||
PLUGIN_TASK_NAME = None
|
PLUGIN_TASK_NAME = None
|
||||||
|
|||||||
187
plugins/RenameFile/openedFile.py
Normal file
187
plugins/RenameFile/openedFile.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"""
|
||||||
|
openedFile (By David Maisonave aka Axter)
|
||||||
|
https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
|
||||||
|
|
||||||
|
Description:
|
||||||
|
Close all (open file) handles on all processes for a given file.
|
||||||
|
Use Case:
|
||||||
|
Can be used when a file needs to be deleted or moved,
|
||||||
|
but the file is locked by one or more other processes.
|
||||||
|
Requirements:
|
||||||
|
This class requires Sysinternals handle.exe, which can be downloaded from following link:
|
||||||
|
https://learn.microsoft.com/en-us/sysinternals/downloads/handle
|
||||||
|
Important: **MUST call this class in admin mode with elevated privileges!!!**
|
||||||
|
Example Usage:
|
||||||
|
handleExe = r"C:\Sysinternals\handle64.exe"
|
||||||
|
of = openedFile(handleExe)
|
||||||
|
of.closeFile(r"B:\V\V\testdup\deleme2.mp4")
|
||||||
|
|
||||||
|
"""
|
||||||
|
import ctypes, os, sys, psutil, argparse, traceback, logging, numbers, string
|
||||||
|
from ctypes import wintypes
|
||||||
|
# from StashPluginHelper import StashPluginHelper
|
||||||
|
# Look at the following links to enhance this code:
|
||||||
|
# https://stackoverflow.com/questions/35106511/how-to-access-the-peb-of-another-process-with-python-ctypes
|
||||||
|
# https://www.codeproject.com/Articles/19685/Get-Process-Info-with-NtQueryInformationProcess
|
||||||
|
|
||||||
|
# Important: MUST call this class in admin mode with elevated privileges!!!
|
||||||
|
# This class has member function runMeAsAdmin, which will elevate privileges.
|
||||||
|
# When member function closeFile is called, it will call runMeAsAdmin as needed.
|
||||||
|
# getPid is the only function which does NOT require elevated admin privileges.
|
||||||
|
class openedFile():
|
||||||
|
# generic strings and constants
|
||||||
|
ntdll = ctypes.WinDLL('ntdll')
|
||||||
|
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||||
|
NTSTATUS = wintypes.LONG
|
||||||
|
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
|
||||||
|
FILE_READ_ATTRIBUTES = 0x80
|
||||||
|
FILE_SHARE_READ = 1
|
||||||
|
OPEN_EXISTING = 3
|
||||||
|
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
||||||
|
FILE_INFORMATION_CLASS = wintypes.ULONG
|
||||||
|
FileProcessIdsUsingFileInformation = 47 # see https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_file_information_class
|
||||||
|
LPSECURITY_ATTRIBUTES = wintypes.LPVOID
|
||||||
|
ULONG_PTR = wintypes.WPARAM
|
||||||
|
lastPath = None
|
||||||
|
handleExe = None
|
||||||
|
stash = None
|
||||||
|
|
||||||
|
def __init__(self, handleExe, stash = None):
|
||||||
|
self.handleExe = handleExe
|
||||||
|
self.stash = stash
|
||||||
|
if handleExe == None or handleExe == "" or not os.path.isfile(handleExe):
|
||||||
|
raise Exception(f"handleExe requires a valid path to Sysinternals 'handle.exe' or 'handle64.exe' executable. Can be downloaded from following link:\nhttps://learn.microsoft.com/en-us/sysinternals/downloads/handle")
|
||||||
|
# create handle on concerned file with dwDesiredAccess == self.FILE_READ_ATTRIBUTES
|
||||||
|
self.kernel32.CreateFileW.restype = wintypes.HANDLE
|
||||||
|
self.kernel32.CreateFileW.argtypes = (
|
||||||
|
wintypes.LPCWSTR, # In lpFileName
|
||||||
|
wintypes.DWORD, # In dwDesiredAccess
|
||||||
|
wintypes.DWORD, # In dwShareMode
|
||||||
|
self.LPSECURITY_ATTRIBUTES, # In_opt lpSecurityAttributes
|
||||||
|
wintypes.DWORD, # In dwCreationDisposition
|
||||||
|
wintypes.DWORD, # In dwFlagsAndAttributes
|
||||||
|
wintypes.HANDLE) # In_opt hTemplateFile
|
||||||
|
|
||||||
|
def getPid(self, path):
|
||||||
|
self.lastPath = path
|
||||||
|
hFile = self.kernel32.CreateFileW(
|
||||||
|
path, self.FILE_READ_ATTRIBUTES, self.FILE_SHARE_READ, None, self.OPEN_EXISTING,
|
||||||
|
self.FILE_FLAG_BACKUP_SEMANTICS, None)
|
||||||
|
if hFile == self.INVALID_HANDLE_VALUE:
|
||||||
|
raise ctypes.WinError(ctypes.get_last_error())
|
||||||
|
# prepare data types for system call
|
||||||
|
class IO_STATUS_BLOCK(ctypes.Structure):
|
||||||
|
class _STATUS(ctypes.Union):
|
||||||
|
_fields_ = (('Status', self.NTSTATUS),
|
||||||
|
('Pointer', wintypes.LPVOID))
|
||||||
|
_anonymous_ = '_Status',
|
||||||
|
_fields_ = (('_Status', _STATUS),
|
||||||
|
('Information', self.ULONG_PTR))
|
||||||
|
iosb = IO_STATUS_BLOCK()
|
||||||
|
class FILE_PROCESS_IDS_USING_FILE_INFORMATION(ctypes.Structure):
|
||||||
|
_fields_ = (('NumberOfProcessIdsInList', wintypes.LARGE_INTEGER),
|
||||||
|
('ProcessIdList', wintypes.LARGE_INTEGER * 64))
|
||||||
|
info = FILE_PROCESS_IDS_USING_FILE_INFORMATION()
|
||||||
|
PIO_STATUS_BLOCK = ctypes.POINTER(IO_STATUS_BLOCK)
|
||||||
|
self.ntdll.NtQueryInformationFile.restype = self.NTSTATUS
|
||||||
|
self.ntdll.NtQueryInformationFile.argtypes = (
|
||||||
|
wintypes.HANDLE, # In FileHandle
|
||||||
|
PIO_STATUS_BLOCK, # Out IoStatusBlock
|
||||||
|
wintypes.LPVOID, # Out FileInformation
|
||||||
|
wintypes.ULONG, # In Length
|
||||||
|
self.FILE_INFORMATION_CLASS) # In FileInformationClass
|
||||||
|
# system call to retrieve list of PIDs currently using the file
|
||||||
|
status = self.ntdll.NtQueryInformationFile(hFile, ctypes.byref(iosb),
|
||||||
|
ctypes.byref(info),
|
||||||
|
ctypes.sizeof(info),
|
||||||
|
self.FileProcessIdsUsingFileInformation)
|
||||||
|
pidList = info.ProcessIdList[0:info.NumberOfProcessIdsInList]
|
||||||
|
if len(pidList) > 0:
|
||||||
|
return pidList
|
||||||
|
return None
|
||||||
|
|
||||||
|
def isAdmin(self):
|
||||||
|
if os.name=='nt':
|
||||||
|
try:
|
||||||
|
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return os.getuid() == 0 # For unix like systems
|
||||||
|
|
||||||
|
def runMeAsAdmin(self):
|
||||||
|
if self.isAdmin() == True:
|
||||||
|
return
|
||||||
|
if os.name=='nt':
|
||||||
|
# Below is a Windows only method which does NOT popup a console.
|
||||||
|
import win32com.shell.shell as shell # Requires: pip install pywin32
|
||||||
|
script = os.path.abspath(sys.argv[0])
|
||||||
|
params = ' '.join([script] + sys.argv[1:])
|
||||||
|
shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
from elevate import elevate # Requires: pip install elevate
|
||||||
|
elevate()
|
||||||
|
|
||||||
|
def getPidExeFileName(self, pid): # Requires running with admin privileges.
|
||||||
|
import win32api, win32con, win32process
|
||||||
|
handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, pid) #get handle for the pid
|
||||||
|
filename = win32process.GetModuleFileNameEx(handle, 0) #get exe path & filename for handle
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def getFilesOpen(self, pid:int): # Requires running with admin privileges.
|
||||||
|
p = psutil.Process(pid1)
|
||||||
|
return p.open_files()
|
||||||
|
|
||||||
|
def getFileHandle(self, pid, path = None): # Requires running with admin privileges.
|
||||||
|
if path == None:
|
||||||
|
path = self.lastPath
|
||||||
|
args = f"{self.handleExe} -p {pid} -nobanner"
|
||||||
|
# if self.stash != None: self.stash.Log(args)
|
||||||
|
results = os.popen(args).read()
|
||||||
|
results = results.splitlines()
|
||||||
|
# if self.stash != None: self.stash.Log(results)
|
||||||
|
hdls = []
|
||||||
|
for line in results:
|
||||||
|
# if self.stash != None: self.stash.Log(line)
|
||||||
|
if line.endswith(path):
|
||||||
|
epos = line.find(":")
|
||||||
|
if epos > 0:
|
||||||
|
hdls += [line[0:epos]]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if len(hdls) == 0:
|
||||||
|
return None
|
||||||
|
return hdls
|
||||||
|
|
||||||
|
def closeHandle(self, pid, fileHandle): # Requires running with admin privileges.
|
||||||
|
args = f"{self.handleExe} -p {pid} -c {fileHandle} -y -nobanner"
|
||||||
|
if self.stash != None: self.stash.Log(args)
|
||||||
|
results = os.popen(args).read()
|
||||||
|
results = results.strip("\n")
|
||||||
|
if results.endswith("Handle closed."):
|
||||||
|
return True
|
||||||
|
if self.stash != None: self.stash.Error(f"Could not close pid {pid} file handle {fileHandle}; results={results}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def closeFile(self, path): # Requires running with admin privileges.
|
||||||
|
pids = self.getPid(path)
|
||||||
|
if pids == None:
|
||||||
|
return None
|
||||||
|
# if self.stash != None: self.stash.Log(f"pids={pids}")
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Need admin privileges starting here.
|
||||||
|
self.runMeAsAdmin()
|
||||||
|
for pid in pids:
|
||||||
|
hdls = self.getFileHandle(pid, path)
|
||||||
|
if hdls == None:
|
||||||
|
# if self.stash != None: self.stash.Log(f"No handle for pid {pid}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for hdl in hdls:
|
||||||
|
# if self.stash != None: self.stash.Log(f"pid {pid} hdl={hdl}")
|
||||||
|
results += [self.closeHandle(pid, hdl)]
|
||||||
|
if len(results) == 0:
|
||||||
|
return None
|
||||||
|
return {"results" : results, "pids" : pids}
|
||||||
@@ -8,6 +8,7 @@ import stashapi.log as log # Importing stashapi.log as log for critical events O
|
|||||||
from stashapi.stashapp import StashInterface
|
from stashapi.stashapp import StashInterface
|
||||||
from StashPluginHelper import StashPluginHelper
|
from StashPluginHelper import StashPluginHelper
|
||||||
from renamefile_settings import config # Import settings from renamefile_settings.py
|
from renamefile_settings import config # Import settings from renamefile_settings.py
|
||||||
|
from openedFile import openedFile
|
||||||
|
|
||||||
# **********************************************************************
|
# **********************************************************************
|
||||||
# Constant global variables --------------------------------------------
|
# Constant global variables --------------------------------------------
|
||||||
@@ -76,6 +77,10 @@ tag_whitelist = config["tagWhitelist"]
|
|||||||
if not tag_whitelist:
|
if not tag_whitelist:
|
||||||
tag_whitelist = ""
|
tag_whitelist = ""
|
||||||
stash.Trace(f"(tag_whitelist={tag_whitelist})")
|
stash.Trace(f"(tag_whitelist={tag_whitelist})")
|
||||||
|
handleExe = stash.pluginConfig['handleExe']
|
||||||
|
openedfile = None
|
||||||
|
if handleExe != None and handleExe != "" and os.path.isfile(handleExe):
|
||||||
|
openedfile = openedFile(handleExe, stash)
|
||||||
|
|
||||||
endpointHost = stash.JSON_INPUT['server_connection']['Host']
|
endpointHost = stash.JSON_INPUT['server_connection']['Host']
|
||||||
if endpointHost == "0.0.0.0":
|
if endpointHost == "0.0.0.0":
|
||||||
@@ -299,6 +304,10 @@ def rename_scene(scene_id):
|
|||||||
return None
|
return None
|
||||||
targetDidExist = True if os.path.isfile(new_file_path) else False
|
targetDidExist = True if os.path.isfile(new_file_path) else False
|
||||||
try:
|
try:
|
||||||
|
if openedfile != None:
|
||||||
|
results = openedfile.closeFile(original_file_path)
|
||||||
|
if results != None:
|
||||||
|
stash.Warn(f"Had to close '{original_file_path}', because it was opened by following pids:{results['pids']}")
|
||||||
if move_files:
|
if move_files:
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
shutil.move(original_file_path, new_file_path)
|
shutil.move(original_file_path, new_file_path)
|
||||||
@@ -340,12 +349,15 @@ def rename_files_task():
|
|||||||
stash.Log("No changes were made.")
|
stash.Log("No changes were made.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if stash.PLUGIN_TASK_NAME == "rename_files_task":
|
try:
|
||||||
rename_files_task()
|
if stash.PLUGIN_TASK_NAME == "rename_files_task":
|
||||||
elif inputToUpdateScenePost:
|
rename_files_task()
|
||||||
rename_files_task()
|
elif inputToUpdateScenePost:
|
||||||
|
rename_files_task()
|
||||||
|
except Exception as e:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
stash.Error(f"Exception while running Plugin. Error: {e}\nTraceBack={tb}")
|
||||||
|
stash.log.exception('Got exception on main handler')
|
||||||
|
|
||||||
stash.Trace("\n*********************************\nEXITING ***********************\n*********************************")
|
stash.Trace("\n*********************************\nEXITING ***********************\n*********************************")
|
||||||
|
|
||||||
# ToDo: Wish List
|
|
||||||
# Add code to get tags from duplicate filenames
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: RenameFile
|
name: RenameFile
|
||||||
description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab.
|
description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab.
|
||||||
version: 0.4.6
|
version: 0.5.0
|
||||||
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
|
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
|
||||||
settings:
|
settings:
|
||||||
performerAppend:
|
performerAppend:
|
||||||
|
|||||||
@@ -47,4 +47,16 @@ config = {
|
|||||||
"if_notitle_use_org_filename": True, # Warning: Do not recommend setting this to False.
|
"if_notitle_use_org_filename": True, # Warning: Do not recommend setting this to False.
|
||||||
# Current Stash DB schema only allows maximum base file name length to be 255
|
# Current Stash DB schema only allows maximum base file name length to be 255
|
||||||
"max_filename_length": 255,
|
"max_filename_length": 255,
|
||||||
|
|
||||||
|
# handleExe is for Windows only.
|
||||||
|
# In Windows, a file can't be renamed if the file is opened by another process.
|
||||||
|
# In other words, if a file is being played by Stash or any other video player, the RenameFile plugin
|
||||||
|
# will get an access denied error when trying to rename the file.
|
||||||
|
# As a workaround, the 'handleExe' field can be populated with a full path to handle.exe or handle64.exe.
|
||||||
|
# This executable can be downloaded from the following link:
|
||||||
|
# https://learn.microsoft.com/en-us/sysinternals/downloads/handle
|
||||||
|
# RenameFile can use the Handle.exe program to close all opened file handles by all processes before renaming the file.
|
||||||
|
#
|
||||||
|
# Warning: This feature can cause the process playing the video to crash.
|
||||||
|
"handleExe": r"C:\Sysinternals\handle64.exe", # https://learn.microsoft.com/en-us/sysinternals/downloads/handle
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user