-
-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added plugin FileMonitor and updated RenameFile plugin (#375)
- Loading branch information
1 parent
0d22fce
commit 8280454
Showing
9 changed files
with
644 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# FileMonitor: Ver 0.1.0 (By David Maisonave) | ||
FileMonitor is a [Stash](https://github.com/stashapp/stash) plugin which updates Stash if any changes occurs in the Stash library paths. | ||
|
||
### Using FileMonitor as a plugin | ||
- To start monitoring file changes, go to **Stash->Settings->Task->[Plugin Tasks]->FileMonitor**, and click on the [Start Library Monitor] button. | ||
- ![FileMonitor_Task](https://github.com/user-attachments/assets/f275a70f-8e86-42a4-b2c1-98b3f4935334) | ||
- To stop this task, go to **Stash->Settings->Task->[Task Queue]**, and click on the **[x]**. | ||
- ![Kill_FileMonitor_Task](https://github.com/user-attachments/assets/a3f4abca-f3a2-49fa-9db5-e0c733e0aeb1) | ||
|
||
### Using FileMonitor as a script | ||
**FileMonitor** can be called as a standalone script. | ||
- To start monitoring call the script and pass any argument. | ||
- python filemonitor.py **start** | ||
- To stop **FileMonitor**, pass argument **stop**. | ||
- python filemonitor.py **stop** | ||
- After running above command line, **FileMonitor** will stop after the next file change occurs. | ||
- The stop command works to stop the standalone job and the Stash plugin task job. | ||
|
||
### Requirements | ||
`pip install stashapp-tools` | ||
`pip install pyYAML` | ||
`pip install watchdog` | ||
|
||
### Installation | ||
- Follow **Requirements** instructions. | ||
- In the stash plugin directory (C:\Users\MyUserName\.stash\plugins), create a folder named **FileMonitor**. | ||
- Copy all the plugin files to this folder.(**C:\Users\MyUserName\\.stash\plugins\FileMonitor**). | ||
- Restart Stash. | ||
|
||
That's it!!! | ||
|
||
### Options | ||
- All options are accessible in the GUI via Settings->Plugins->Plugins->[FileMonitor]. | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,289 @@ | ||
# Description: This is a Stash plugin which updates Stash if any changes occurs in the Stash library paths. | ||
# By David Maisonave (aka Axter) Jul-2024 (https://www.axter.com/) | ||
# Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/FileMonitor | ||
# Note: To call this script outside of Stash, pass any argument. | ||
# Example: python filemonitor.py foofoo | ||
import os | ||
import sys | ||
import time | ||
import shutil | ||
import fileinput | ||
import hashlib | ||
import json | ||
from pathlib import Path | ||
import requests | ||
import logging | ||
from logging.handlers import RotatingFileHandler | ||
import stashapi.log as log # Importing stashapi.log as log for critical events ONLY | ||
from stashapi.stashapp import StashInterface | ||
from watchdog.observers import Observer # This is also needed for event attributes | ||
import watchdog # pip install watchdog # https://pythonhosted.org/watchdog/ | ||
from threading import Lock, Condition | ||
from multiprocessing import shared_memory | ||
from filemonitor_config import config # Import settings from filemonitor_config.py | ||
|
||
# ********************************************************************** | ||
# Constant global variables -------------------------------------------- | ||
LOG_FILE_PATH = log_file_path = f"{Path(__file__).resolve().parent}\\{Path(__file__).stem}.log" | ||
FORMAT = "[%(asctime)s - LN:%(lineno)s] %(message)s" | ||
PLUGIN_ARGS = False | ||
PLUGIN_ARGS_MODE = False | ||
PLUGIN_ID = Path(__file__).stem.lower() | ||
# GraphQL query to fetch all scenes | ||
QUERY_ALL_SCENES = """ | ||
query AllScenes { | ||
allScenes { | ||
id | ||
updated_at | ||
} | ||
} | ||
""" | ||
RFH = RotatingFileHandler( | ||
filename=LOG_FILE_PATH, | ||
mode='a', | ||
maxBytes=2*1024*1024, # Configure logging for this script with max log file size of 2000K | ||
backupCount=2, | ||
encoding=None, | ||
delay=0 | ||
) | ||
TIMEOUT = 5 | ||
CONTINUE_RUNNING_SIG = 99 | ||
|
||
# ********************************************************************** | ||
# Global variables -------------------------------------------- | ||
exitMsg = "Change success!!" | ||
mutex = Lock() | ||
signal = Condition(mutex) | ||
shouldUpdate = False | ||
TargetPaths = [] | ||
runningInPluginMode = False | ||
|
||
# Configure local log file for plugin within plugin folder having a limited max log file size | ||
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt="%y%m%d %H:%M:%S", handlers=[RFH]) | ||
logger = logging.getLogger(Path(__file__).stem) | ||
|
||
# ********************************************************************** | ||
# ---------------------------------------------------------------------- | ||
# Code section to fetch variables from Plugin UI and from filemonitor_settings.py | ||
# Check if being called as Stash plugin | ||
gettingCalledAsStashPlugin = True | ||
stopLibraryMonitoring = False | ||
StdInRead = None | ||
try: | ||
if len(sys.argv) == 1: | ||
print(f"Attempting to read stdin. (len(sys.argv)={len(sys.argv)})", file=sys.stderr) | ||
StdInRead = sys.stdin.read() | ||
# for line in fileinput.input(): | ||
# StdInRead = line | ||
# break | ||
else: | ||
if len(sys.argv) > 1 and sys.argv[1].lower() == "stop": | ||
stopLibraryMonitoring = True | ||
raise Exception("Not called in plugin mode.") | ||
except: | ||
gettingCalledAsStashPlugin = False | ||
print(f"Either len(sys.argv) not expected value OR sys.stdin.read() failed! (stopLibraryMonitoring={stopLibraryMonitoring}) (StdInRead={StdInRead}) (len(sys.argv)={len(sys.argv)})", file=sys.stderr) | ||
pass | ||
|
||
if gettingCalledAsStashPlugin and StdInRead: | ||
print(f"StdInRead={StdInRead} (len(sys.argv)={len(sys.argv)})", file=sys.stderr) | ||
runningInPluginMode = True | ||
json_input = json.loads(StdInRead) | ||
FRAGMENT_SERVER = json_input["server_connection"] | ||
else: | ||
runningInPluginMode = False | ||
FRAGMENT_SERVER = {'Scheme': config['endpoint_Scheme'], 'Host': config['endpoint_Host'], 'Port': config['endpoint_Port'], 'SessionCookie': {'Name': 'session', 'Value': '', 'Path': '', 'Domain': '', 'Expires': '0001-01-01T00:00:00Z', 'RawExpires': '', 'MaxAge': 0, 'Secure': False, 'HttpOnly': False, 'SameSite': 0, 'Raw': '', 'Unparsed': None}, 'Dir': os.path.dirname(Path(__file__).resolve().parent), 'PluginDir': Path(__file__).resolve().parent} | ||
print("Running in non-plugin mode!", file=sys.stderr) | ||
|
||
stash = StashInterface(FRAGMENT_SERVER) | ||
PLUGINCONFIGURATION = stash.get_configuration()["plugins"] | ||
STASHCONFIGURATION = stash.get_configuration()["general"] | ||
STASHPATHSCONFIG = STASHCONFIGURATION['stashes'] | ||
stashPaths = [] | ||
settings = { | ||
"recursiveDisabled": False, | ||
"runCleanAfterDelete": False, | ||
"scanModified": False, | ||
"zzdebugTracing": False, | ||
"zzdryRun": False, | ||
} | ||
|
||
if PLUGIN_ID in PLUGINCONFIGURATION: | ||
settings.update(PLUGINCONFIGURATION[PLUGIN_ID]) | ||
# ---------------------------------------------------------------------- | ||
debugTracing = settings["zzdebugTracing"] | ||
RECURSIVE = settings["recursiveDisabled"] == False | ||
SCAN_MODIFIED = settings["scanModified"] | ||
RUN_CLEAN_AFTER_DELETE = settings["runCleanAfterDelete"] | ||
RUN_GENERATE_CONTENT = config['runGenerateContent'] | ||
|
||
for item in STASHPATHSCONFIG: | ||
stashPaths.append(item["path"]) | ||
|
||
# Extract dry_run setting from settings | ||
DRY_RUN = settings["zzdryRun"] | ||
dry_run_prefix = '' | ||
try: | ||
PLUGIN_ARGS = json_input['args'] | ||
PLUGIN_ARGS_MODE = json_input['args']["mode"] | ||
except: | ||
pass | ||
logger.info(f"\nStarting (runningInPluginMode={runningInPluginMode}) (debugTracing={debugTracing}) (DRY_RUN={DRY_RUN}) (PLUGIN_ARGS_MODE={PLUGIN_ARGS_MODE}) (PLUGIN_ARGS={PLUGIN_ARGS})************************************************") | ||
if debugTracing: logger.info(f"Debug Tracing (stash.get_configuration()={stash.get_configuration()})................") | ||
if debugTracing: logger.info("settings: %s " % (settings,)) | ||
if debugTracing: logger.info(f"Debug Tracing (STASHCONFIGURATION={STASHCONFIGURATION})................") | ||
if debugTracing: logger.info(f"Debug Tracing (stashPaths={stashPaths})................") | ||
|
||
if DRY_RUN: | ||
logger.info("Dry run mode is enabled.") | ||
dry_run_prefix = "Would've " | ||
if debugTracing: logger.info("Debug Tracing................") | ||
# ---------------------------------------------------------------------- | ||
# ********************************************************************** | ||
if debugTracing: logger.info(f"Debug Tracing (SCAN_MODIFIED={SCAN_MODIFIED}) (RECURSIVE={RECURSIVE})................") | ||
|
||
def start_library_monitor(): | ||
global shouldUpdate | ||
global TargetPaths | ||
try: | ||
# Create shared memory buffer which can be used as singleton logic or to get a signal to quit task from external script | ||
shm_a = shared_memory.SharedMemory(name="DavidMaisonaveAxter_FileMonitor", create=True, size=4) | ||
except: | ||
pass | ||
logger.info("Could not open shared memory map. Change File Monitor must be running. Can not run multiple instance of Change File Monitor.") | ||
return | ||
type(shm_a.buf) | ||
shm_buffer = shm_a.buf | ||
len(shm_buffer) | ||
shm_buffer[0] = CONTINUE_RUNNING_SIG | ||
if debugTracing: logger.info(f"Shared memory map opended, and flag set to {shm_buffer[0]}") | ||
RunCleanMetadata = False | ||
|
||
event_handler = watchdog.events.FileSystemEventHandler() | ||
def on_created(event): | ||
global shouldUpdate | ||
global TargetPaths | ||
TargetPaths.append(event.src_path) | ||
logger.info(f"CREATE *** '{event.src_path}'") | ||
with mutex: | ||
shouldUpdate = True | ||
signal.notify() | ||
|
||
def on_deleted(event): | ||
global shouldUpdate | ||
global TargetPaths | ||
nonlocal RunCleanMetadata | ||
TargetPaths.append(event.src_path) | ||
logger.info(f"DELETE *** '{event.src_path}'") | ||
with mutex: | ||
shouldUpdate = True | ||
RunCleanMetadata = True | ||
signal.notify() | ||
|
||
def on_modified(event): | ||
global shouldUpdate | ||
global TargetPaths | ||
if SCAN_MODIFIED: | ||
TargetPaths.append(event.src_path) | ||
logger.info(f"MODIFIED *** '{event.src_path}'") | ||
with mutex: | ||
shouldUpdate = True | ||
signal.notify() | ||
else: | ||
if debugTracing: logger.info(f"Ignoring modifications due to plugin UI setting. path='{event.src_path}'") | ||
|
||
def on_moved(event): | ||
global shouldUpdate | ||
global TargetPaths | ||
TargetPaths.append(event.src_path) | ||
TargetPaths.append(event.dest_path) | ||
logger.info(f"MOVE *** from '{event.src_path}' to '{event.dest_path}'") | ||
with mutex: | ||
shouldUpdate = True | ||
signal.notify() | ||
|
||
if debugTracing: logger.info("Debug Trace........") | ||
event_handler.on_created = on_created | ||
event_handler.on_deleted = on_deleted | ||
event_handler.on_modified = on_modified | ||
event_handler.on_moved = on_moved | ||
|
||
observer = Observer() | ||
|
||
# Iterate through stashPaths | ||
for path in stashPaths: | ||
observer.schedule(event_handler, path, recursive=RECURSIVE) | ||
if debugTracing: logger.info(f"Observing {path}") | ||
observer.start() | ||
if debugTracing: logger.info("Starting loop................") | ||
try: | ||
while True: | ||
TmpTargetPaths = [] | ||
with mutex: | ||
while not shouldUpdate: | ||
if debugTracing: logger.info("Wait start................") | ||
signal.wait() | ||
if debugTracing: logger.info("Wait end................") | ||
shouldUpdate = False | ||
TmpTargetPaths = [] | ||
for TargetPath in TargetPaths: | ||
TmpTargetPaths.append(os.path.dirname(TargetPath)) | ||
TargetPaths = [] | ||
TmpTargetPaths = list(set(TmpTargetPaths)) | ||
if TmpTargetPaths != []: | ||
logger.info(f"Triggering stash scan for path(s) {TmpTargetPaths}") | ||
if not DRY_RUN: | ||
stash.metadata_scan(paths=TmpTargetPaths) | ||
if RUN_CLEAN_AFTER_DELETE and RunCleanMetadata: | ||
stash.metadata_clean(paths=TmpTargetPaths, dry_run=DRY_RUN) | ||
if RUN_GENERATE_CONTENT: | ||
stash.metadata_generate() | ||
if gettingCalledAsStashPlugin and shm_buffer[0] == CONTINUE_RUNNING_SIG: | ||
stash.run_plugin_task(plugin_id=PLUGIN_ID, task_name="Start Library Monitor") | ||
if debugTracing: logger.info("Exiting plugin so that metadata_scan task can run.") | ||
return | ||
else: | ||
if debugTracing: logger.info("Nothing to scan.") | ||
if shm_buffer[0] != CONTINUE_RUNNING_SIG: | ||
logger.info(f"Exiting Change File Monitor. (shm_buffer[0]={shm_buffer[0]})") | ||
shm_a.close() | ||
shm_a.unlink() # Call unlink only once to release the shared memory | ||
raise KeyboardInterrupt | ||
except KeyboardInterrupt: | ||
observer.stop() | ||
if debugTracing: logger.info("Stopping observer................") | ||
observer.join() | ||
if debugTracing: logger.info("Exiting function................") | ||
|
||
# This function is only useful when called outside of Stash. | ||
# Example: python filemonitor.py stop | ||
# Stops monitoring after triggered by the next file change. | ||
# ToDo: Add logic so it doesn't have to wait until the next file change | ||
def stop_library_monitor(): | ||
if debugTracing: logger.info("Opening shared memory map.") | ||
try: | ||
shm_a = shared_memory.SharedMemory(name="DavidMaisonaveAxter_FileMonitor", create=False, size=4) | ||
except: | ||
pass | ||
logger.info("Could not open shared memory map. Change File Monitor must not be running.") | ||
return | ||
type(shm_a.buf) | ||
shm_buffer = shm_a.buf | ||
len(shm_buffer) | ||
shm_buffer[0] = 123 | ||
if debugTracing: logger.info(f"Shared memory map opended, and flag set to {shm_buffer[0]}") | ||
shm_a.close() | ||
shm_a.unlink() # Call unlink only once to release the shared memory | ||
time.sleep(1) | ||
return | ||
|
||
if stopLibraryMonitoring: | ||
stop_library_monitor() | ||
if debugTracing: logger.info(f"stop_library_monitor EXIT................") | ||
elif PLUGIN_ARGS_MODE == "start_library_monitor" or not gettingCalledAsStashPlugin: | ||
start_library_monitor() | ||
if debugTracing: logger.info(f"start_library_monitor EXIT................") | ||
else: | ||
logger.info(f"Nothing to do!!! (PLUGIN_ARGS_MODE={PLUGIN_ARGS_MODE})") | ||
|
||
if debugTracing: logger.info("\n*********************************\nEXITING ***********************\n*********************************") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: FileMonitor | ||
description: Monitors the Stash library folders, and updates Stash if any changes occurs in the Stash library paths. | ||
version: 0.2.0 | ||
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/FileMonitor | ||
settings: | ||
recursiveDisabled: | ||
displayName: No Recursive | ||
description: Enable stop monitoring paths recursively. | ||
type: BOOLEAN | ||
runCleanAfterDelete: | ||
displayName: Run Clean | ||
description: Enable to run metadata clean task after file deletion. | ||
type: BOOLEAN | ||
scanModified: | ||
displayName: Scan Modifications | ||
description: Enable to monitor changes in file system for modification flag. This option is NOT needed for Windows, because on Windows changes are triggered via CREATE, DELETE, and MOVE flags. Other OS may differ. | ||
type: BOOLEAN | ||
zzdebugTracing: | ||
displayName: Debug Tracing | ||
description: (Default=false) [***For Advanced Users***] Enable debug tracing. When enabled, additional tracing logging is added to Stash\plugins\FileMonitor\filemonitor.log | ||
type: BOOLEAN | ||
zzdryRun: | ||
displayName: Dry Run | ||
description: Enable to run script in [Dry Run] mode. In this mode, Stash does NOT call meta_scan, and only logs the action it would have taken. | ||
type: BOOLEAN | ||
exec: | ||
- python | ||
- "{pluginDir}/filemonitor.py" | ||
interface: raw | ||
tasks: | ||
- name: Start Library Monitor | ||
description: Monitors paths in Stash library for media file changes, and updates Stash. | ||
defaultArgs: | ||
mode: start_library_monitor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# Description: This is a Stash plugin which updates Stash if any changes occurs in the Stash library paths. | ||
# By David Maisonave (aka Axter) Jul-2024 (https://www.axter.com/) | ||
# Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/FileMonitor | ||
config = { | ||
# Enable to run metadata_generate (Generate Content) after metadata scan. | ||
"runGenerateContent": False, | ||
|
||
# The following fields are ONLY used when running FileMonitor in script mode | ||
"endpoint_Scheme" : "http", # Define endpoint to use when contacting the Stash server | ||
"endpoint_Host" : "0.0.0.0", # Define endpoint to use when contacting the Stash server | ||
"endpoint_Port" : 9999, # Define endpoint to use when contacting the Stash server | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
stashapp-tools | ||
pyYAML | ||
watchdog | ||
requests |
Oops, something went wrong.