Watcher is now aware of which platform you're on. Adds heuristics for Windows, Linux, and a generic Poller.

Mac OS just mirrors Linux (hopefully that works).
Windows tests to see if the file is openable (works on local files to see if they're still in use before triggering an update)
This commit is contained in:
DuctTape42
2023-09-28 08:06:04 -07:00
parent c722cc65ce
commit 161b4fd706
2 changed files with 97 additions and 3 deletions

View File

@@ -19,6 +19,15 @@ Cooldown = 300
#A list of file extensions to watch. If this is omitted, it uses the extensions that are defined
#in your Stash library (for videos, images, and galleries)
Extensions =
#If this is set to a non-zero numeric value, this forces the use of polling to
#determine file system changes. If it is left blank, then the OS appropriate
#mechanism is used. This is much less efficient than the OS mechanism, so it
#should be used with care. The docs claim that this is required to watch SMB
#shares, though in my testing I could watch them on Windows with the regular
#WindowsApiObserver
PollInterval=
#This enables debug logging
Debug=
#Options for the Stash Scan. Stash defaults to everything disabled, so this is the default
#Generate options that match up with what we can do in Scan

View File

@@ -5,9 +5,15 @@ import time
import os
from threading import Lock, Condition
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
from watchdog.events import PatternMatchingEventHandler
from stashapi.stashapp import StashInterface
import logging
import sys
from enum import Enum
#the type of watcher being used; controls how to interpret the events
WatcherType = Enum('WatcherType', ['INOTIFY', 'WINDOWS', 'POLLING', 'KQUEUE'])
#Setup logger
logger = logging.getLogger("stash-watcher")
@@ -24,6 +30,10 @@ signal = Condition(mutex)
modifiedFiles = {}
currentWatcherType = None
def log(msg):
logger.info(msg)
@@ -32,12 +42,57 @@ def debug(msg):
def handleEvent(event):
global shouldUpdate
global currentWatcherType
debug("========EVENT========")
debug(str(event))
#log(modifiedFiles)
#Record if the file was modified. When a file is closed, see if it was modified. If so, trigger
shouldTrigger = False
if event.is_directory == False:
if event.is_directory == True:
return
#Depending on the watcher type, we have to handle these events differently
if currentWatcherType == WatcherType.WINDOWS:
#On windows here's what happens:
# File moved into a watched directory - Created Event
# File moved out of a watched directory - Deleted Event
# Moved within a watched directory (src and dst in watched directory) - Moved event
# echo blah > foo.mp4 - Created then Modified
# copying a small file - Created then Modified
# copying a large file - Created then two (or more) Modified events (appears to be one when the file is created and another when it's finished)
#It looks like you can get an optional Created Event and then
#either one or two Modified events. You can also get Moved events
#For local files on Windows, they can't be opened if they're currently
#being written to. Therefore, every time we get an event, attempt to
#open the file. If we're successful, assume the write is finished and
#trigger the update. Otherwise wait until the next event and try again
if event.event_type == "created" or event.event_type == "modified":
try:
with open(event.src_path) as file:
debug("Successfully opened file; triggering")
shouldTrigger = True
except:
pass
if event.event_type == "moved":
shouldTrigger = True
elif currentWatcherType == WatcherType.POLLING:
#Every interval you get 1 event per changed file
# - If the file was not present in the previous poll, then Created
# - If the file was present and has a new size, then Modified
# - If the file was moved within the directory, then Moved
# - If the file is gone, then deleted
#
# For now, just trigger on the created event. In the future, create
# a timer at 2x polling interval. Reschedule the timer on each event
# when it fires, trigger the update.
if event.event_type == "moved" or event.event_type == "created":
shouldTrigger = True
#Until someone tests this on mac, just do what INOTIFY does
elif currentWatcherType == WatcherType.INOTIFY or currentWatcherType == WatcherType.KQUEUE:
if event.event_type == "modified":
modifiedFiles[event.src_path] = 1
#These are for files being copied into the target
@@ -50,6 +105,9 @@ def handleEvent(event):
#moved out of a watched directory
elif event.event_type == "moved":
shouldTrigger = True
else:
print("Unknown watcher type " + str(currentWatcherType))
sys.exit(1)
#Trigger the update
if shouldTrigger:
@@ -59,8 +117,9 @@ def handleEvent(event):
signal.notify()
def main(stash, scanFlags, paths, extensions, timeout):
def main(stash, scanFlags, paths, extensions, timeout, pollInterval):
global shouldUpdate
global currentWatcherType
if len(extensions) == 1 and extensions[0] == "*":
patterns = ["*"]
@@ -69,6 +128,21 @@ def main(stash, scanFlags, paths, extensions, timeout):
eventHandler = PatternMatchingEventHandler(patterns, None, False, True)
eventHandler.on_any_event = handleEvent
observer = Observer()
observerName = type(observer).__name__
if pollInterval != None and pollInterval > 0:
currentWatcherType = WatcherType.POLLING
observer = PollingObserver()
elif observerName == "WindowsApiObserver":
currentWatcherType = WatcherType.WINDOWS
elif observerName == "KqueueObserver":
currentWatcherType = WatcherType.KQUEUE
elif observerName == "InotifyObserver":
currentWatcherType = WatcherType.INOTIFY
else:
print("Unknown watcher type " + str(observer))
sys.exit(1)
debug(str(observer))
for path in paths:
observer.schedule(eventHandler, path, recursive=True)
observer.start()
@@ -152,7 +226,18 @@ if __name__ == '__main__':
stashConfig = stash.graphql_configuration()
extensions = stashConfig['general']['videoExtensions'] + stashConfig['general']['imageExtensions'] + stashConfig['general']['galleryExtensions']
main(stash, scanFlags, paths, extensions, timeout)
pollIntervalStr = config.get('Config', 'PollInterval')
if pollIntervalStr:
pollInterval = int(pollIntervalStr)
else:
pollInterval = None
if config.get('Config', 'Debug') == "true":
logger.setLevel(logging.DEBUG)
ch.setLevel(logging.DEBUG)
main(stash, scanFlags, paths, extensions, timeout, pollInterval)