mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-04-19 01:54:19 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user