#!/usr/bin/python -w import argparse import configparser import time import os from threading import Lock, Condition from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler from stashapi.stashapp import StashInterface import logging #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 = {} def log(msg): logger.info(msg) def debug(msg): logger.debug(msg) def handleEvent(event): global shouldUpdate 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.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 #Trigger the update if shouldTrigger: debug("Triggering updates") with mutex: shouldUpdate = True signal.notify() def main(stash, scanFlags, paths, extensions, timeout): global shouldUpdate 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() 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.graphql_configuration() extensions = stashConfig['general']['videoExtensions'] + stashConfig['general']['imageExtensions'] + stashConfig['general']['galleryExtensions'] main(stash, scanFlags, paths, extensions, timeout)