55import os
66from threading import Lock , Condition
77from watchdog .observers import Observer
8+ from watchdog .observers .polling import PollingObserver
89from watchdog .events import PatternMatchingEventHandler
910from stashapi .stashapp import StashInterface
1011import logging
12+ import sys
13+ from enum import Enum
14+
15+ #the type of watcher being used; controls how to interpret the events
16+ WatcherType = Enum ('WatcherType' , ['INOTIFY' , 'WINDOWS' , 'POLLING' , 'KQUEUE' ])
1117
1218#Setup logger
1319logger = logging .getLogger ("stash-watcher" )
2430
2531modifiedFiles = {}
2632
33+
34+ currentWatcherType = None
35+
36+
2737def log (msg ):
2838 logger .info (msg )
2939
@@ -32,12 +42,57 @@ def debug(msg):
3242
3343def handleEvent (event ):
3444 global shouldUpdate
45+ global currentWatcherType
3546 debug ("========EVENT========" )
3647 debug (str (event ))
3748 #log(modifiedFiles)
3849 #Record if the file was modified. When a file is closed, see if it was modified. If so, trigger
3950 shouldTrigger = False
40- if event .is_directory == False :
51+
52+ if event .is_directory == True :
53+ return
54+ #Depending on the watcher type, we have to handle these events differently
55+ if currentWatcherType == WatcherType .WINDOWS :
56+ #On windows here's what happens:
57+ # File moved into a watched directory - Created Event
58+ # File moved out of a watched directory - Deleted Event
59+ # Moved within a watched directory (src and dst in watched directory) - Moved event
60+
61+ # echo blah > foo.mp4 - Created then Modified
62+ # copying a small file - Created then Modified
63+ # 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)
64+
65+ #It looks like you can get an optional Created Event and then
66+ #either one or two Modified events. You can also get Moved events
67+
68+ #For local files on Windows, they can't be opened if they're currently
69+ #being written to. Therefore, every time we get an event, attempt to
70+ #open the file. If we're successful, assume the write is finished and
71+ #trigger the update. Otherwise wait until the next event and try again
72+ if event .event_type == "created" or event .event_type == "modified" :
73+ try :
74+ with open (event .src_path ) as file :
75+ debug ("Successfully opened file; triggering" )
76+ shouldTrigger = True
77+ except :
78+ pass
79+
80+ if event .event_type == "moved" :
81+ shouldTrigger = True
82+ elif currentWatcherType == WatcherType .POLLING :
83+ #Every interval you get 1 event per changed file
84+ # - If the file was not present in the previous poll, then Created
85+ # - If the file was present and has a new size, then Modified
86+ # - If the file was moved within the directory, then Moved
87+ # - If the file is gone, then deleted
88+ #
89+ # For now, just trigger on the created event. In the future, create
90+ # a timer at 2x polling interval. Reschedule the timer on each event
91+ # when it fires, trigger the update.
92+ if event .event_type == "moved" or event .event_type == "created" :
93+ shouldTrigger = True
94+ #Until someone tests this on mac, just do what INOTIFY does
95+ elif currentWatcherType == WatcherType .INOTIFY or currentWatcherType == WatcherType .KQUEUE :
4196 if event .event_type == "modified" :
4297 modifiedFiles [event .src_path ] = 1
4398 #These are for files being copied into the target
@@ -50,6 +105,9 @@ def handleEvent(event):
50105 #moved out of a watched directory
51106 elif event .event_type == "moved" :
52107 shouldTrigger = True
108+ else :
109+ print ("Unknown watcher type " + str (currentWatcherType ))
110+ sys .exit (1 )
53111
54112 #Trigger the update
55113 if shouldTrigger :
@@ -59,8 +117,9 @@ def handleEvent(event):
59117 signal .notify ()
60118
61119
62- def main (stash , scanFlags , paths , extensions , timeout ):
120+ def main (stash , scanFlags , paths , extensions , timeout , pollInterval ):
63121 global shouldUpdate
122+ global currentWatcherType
64123
65124 if len (extensions ) == 1 and extensions [0 ] == "*" :
66125 patterns = ["*" ]
@@ -69,6 +128,21 @@ def main(stash, scanFlags, paths, extensions, timeout):
69128 eventHandler = PatternMatchingEventHandler (patterns , None , False , True )
70129 eventHandler .on_any_event = handleEvent
71130 observer = Observer ()
131+ observerName = type (observer ).__name__
132+ if pollInterval != None and pollInterval > 0 :
133+ currentWatcherType = WatcherType .POLLING
134+ observer = PollingObserver ()
135+ elif observerName == "WindowsApiObserver" :
136+ currentWatcherType = WatcherType .WINDOWS
137+ elif observerName == "KqueueObserver" :
138+ currentWatcherType = WatcherType .KQUEUE
139+ elif observerName == "InotifyObserver" :
140+ currentWatcherType = WatcherType .INOTIFY
141+ else :
142+ print ("Unknown watcher type " + str (observer ))
143+ sys .exit (1 )
144+
145+ debug (str (observer ))
72146 for path in paths :
73147 observer .schedule (eventHandler , path , recursive = True )
74148 observer .start ()
@@ -152,7 +226,18 @@ def parseConfig(path):
152226 stashConfig = stash .graphql_configuration ()
153227 extensions = stashConfig ['general' ]['videoExtensions' ] + stashConfig ['general' ]['imageExtensions' ] + stashConfig ['general' ]['galleryExtensions' ]
154228
155- main (stash , scanFlags , paths , extensions , timeout )
229+ pollIntervalStr = config .get ('Config' , 'PollInterval' )
230+ if pollIntervalStr :
231+ pollInterval = int (pollIntervalStr )
232+ else :
233+ pollInterval = None
234+
235+ if config .get ('Config' , 'Debug' ) == "true" :
236+ logger .setLevel (logging .DEBUG )
237+ ch .setLevel (logging .DEBUG )
238+
239+
240+ main (stash , scanFlags , paths , extensions , timeout , pollInterval )
156241
157242
158243
0 commit comments