scruffynerf 2784391227
Fix deprecated configuration getting with StashAPI
My bad, when I merged this into the repo on Oct 19th, I missed this deprecated stashapi call, and the stashapi change had taken effect on Oct 11th, so... bad timing.

Luckily it's a one line fix.
2023-11-22 01:18:43 -05:00

241 lines
8.6 KiB
Python

#!/usr/bin/python -w
import argparse
import configparser
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")
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
logger.addHandler(ch)
#This signals that we should
shouldUpdate = False
mutex = Lock()
signal = Condition(mutex)
modifiedFiles = {}
currentWatcherType = None
def log(msg):
logger.info(msg)
def debug(msg):
logger.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 == 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
elif event.event_type == "closed":
if event.src_path in modifiedFiles:
del modifiedFiles[event.src_path]
shouldTrigger = True
#For download managers and the like that write to a temporary file and then move to the destination (real)
#path. Note that this actually triggers if the destination is in the watched location, and not just if it's
#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:
debug("Triggering updates")
with mutex:
shouldUpdate = True
signal.notify()
def main(stash, scanFlags, paths, extensions, timeout, pollInterval):
global shouldUpdate
global currentWatcherType
if len(extensions) == 1 and extensions[0] == "*":
patterns = ["*"]
else:
patterns = list(map(lambda x : "*." + x, extensions))
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()
try:
while True:
with mutex:
while not shouldUpdate:
signal.wait()
shouldUpdate = False
log("Triggering stash scan")
stash.metadata_scan(flags = scanFlags)
log("Sleeping for " + str(timeout) + " seconds")
time.sleep(timeout)
except KeyboardInterrupt:
observer.stop()
observer.join()
def listConverter(item):
debug("listConverter(" + str(item) + ")")
if not item:
return None
listItems = [i.strip() for i in item.split(',')]
if not listItems or (len(listItems) == 1 and not listItems[0]):
return None
return listItems
def makeArgParser():
parser = argparse.ArgumentParser(description='Stash file watcher')
parser.add_argument('config_path', nargs=1, help='Config file path (toml)')
return parser
def parseConfig(path):
config = configparser.ConfigParser(converters={'list': listConverter })
#Load the defaults first
defaults_path = os.path.join(os.path.dirname('__file__'), 'defaults.toml')
config.read(defaults_path)
#Now read the user config
config.read(path)
return config
if __name__ == '__main__':
#Parse the arguments
parser = makeArgParser()
args = parser.parse_args()
configPath = args.config_path
config = parseConfig(configPath)
#Set up Stash
stashArgs = {
"scheme": config["Host"]["Scheme"],
"host": config["Host"]["Host"],
"port": config["Host"]["Port"]
}
if config["Host"]["ApiKey"]:
stashArgs["ApiKey"] = config["Host"]["ApiKey"]
stash = StashInterface(stashArgs)
#And now the flags for the scan
scanFlags = {
"scanGenerateCovers": config["ScanOptions"].getboolean("Covers"),
"scanGeneratePreviews": config["ScanOptions"].getboolean("Previews"),
"scanGenerateImagePreviews": config["ScanOptions"].getboolean("ImagePreviews"),
"scanGenerateSprites": config["ScanOptions"].getboolean("Sprites"),
"scanGeneratePhashes": config["ScanOptions"].getboolean("Phashes"),
"scanGenerateThumbnails": config["ScanOptions"].getboolean("Thumbnails"),
"scanGenerateClipPreviews": config["ScanOptions"].getboolean("ClipPreviews")
}
paths = config.getlist("Config", "Paths")
timeout = config["Config"].getint("Cooldown")
#If the extensions are in the config, use them. Otherwise pull them from stash.
extensions = config.getlist('Config', 'Extensions')
if not extensions:
stashConfig = stash.get_configuration()
extensions = stashConfig['general']['videoExtensions'] + stashConfig['general']['imageExtensions'] + stashConfig['general']['galleryExtensions']
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)