88import logging
99import urllib
1010import time
11+ import os
12+ from datetime import datetime , date , timedelta
1113from threading import Timer
1214from watchdog .observers import Observer
1315from watchdog .events import PatternMatchingEventHandler
1719from OneShotQueueTimer import OneShotQueueTimer
1820from plexUsers import plexUsers
1921from lightroomTags import parse_xmp_for_lightroom_tags
22+ from photoElement import PhotoElement
2023
2124from config import ppTagConfig
2225
@@ -124,6 +127,8 @@ def updateTagsAndRating(key, filename):
124127 # get the tags
125128 data = process_file (img_file , stop_tag = stop_tag , details = detailed , strict = strict , debug = debug )
126129
130+ img_file .close ()
131+
127132 if not data :
128133 #print("No EXIF information found\n")
129134 return
@@ -152,8 +157,68 @@ def updateTagsAndRating(key, filename):
152157 # it is a corrupt file (exif/xmp)
153158 return
154159
160+ def parseExifAndTags (filename ):
161+
162+ detailed = True
163+ stop_tag = DEFAULT_STOP_TAG
164+ debug = False
165+ strict = False
166+ color = False
167+
168+ #exif_log.setup_logger(debug, color)
169+
170+ filepath = ppTagConfig .PHOTOS_LIBRARY_PATH + filename
171+
172+ try :
173+ img_file = open (str (filepath ), 'rb' )
174+ except IOError :
175+ # print("'%s' is unreadable" % filename)
176+ return None
177+
178+ try :
179+ # get the tags
180+ data = process_file (img_file , stop_tag = stop_tag , details = detailed , strict = strict , debug = debug )
181+
182+ img_file .close ()
183+
184+ if not data :
185+ #print("No EXIF information found\n")
186+ return None
187+
188+ if 'JPEGThumbnail' in data :
189+ # logger.info('File has JPEG thumbnail')
190+ del data ['JPEGThumbnail' ]
191+ if 'TIFFThumbnail' in data :
192+ # logger.info('File has TIFF thumbnail')
193+ del data ['TIFFThumbnail' ]
155194
156- def triggerLoopThroughAllPhotos ():
195+ parsedXMP = {}
196+ parsedXMP ['rating' ] = 0
197+ parsedXMP ['tags' ] = []
198+ # xmp data
199+ if 'Image ApplicationNotes' in data :
200+ xml = data ['Image ApplicationNotes' ].printable
201+
202+ parsedXMP = parse_xmp_for_lightroom_tags (xml )
203+
204+ # if 'Image Copyright' in data:
205+ # print("Copyright : %s", data['Image Copyright'].printable)
206+
207+ date = datetime .today ().date ()
208+ if 'EXIF DateTimeOriginal' in data :
209+ date = datetime .strptime (data ['EXIF DateTimeOriginal' ].printable , '%Y:%m:%d %H:%M:%S' ).date ()
210+ else :
211+ datetimeModified = datetime .fromtimestamp (os .path .getmtime (filepath ))
212+ date = datetimeModified .date ()
213+
214+ photoElement = PhotoElement (filename , date , parsedXMP ['tags' ], parsedXMP ['rating' ])
215+ return photoElement
216+ except :
217+ # it is a corrupt file (exif/xmp)
218+ return None
219+
220+
221+ def triggerProcess ():
157222 global t
158223 t .start ()
159224
@@ -162,8 +227,91 @@ def uniqify(seq):
162227 keys = {}
163228 for e in seq :
164229 keys [e ] = 1
165- return keys .keys ()
230+ return list ( keys .keys () )
166231
232+ def fetchPhotosAndProcess ():
233+ global firstRun
234+
235+ if firstRun :
236+ # if a complete update on startup is requested loop through all photos
237+ loopThroughAllPhotos ()
238+ else :
239+ # else fetch all photos based on date
240+ if fetchAndProcessByDate ():
241+ # failed so loop through all photos
242+ loopThroughAllPhotos ()
243+
244+ def fetchAndProcessByDate ():
245+ global doUpdate
246+ doUpdateTemp = uniqify (doUpdate )
247+ doUpdate = []
248+
249+ photoGroups = {}
250+ # first group all photos by date
251+ for filepath in doUpdateTemp :
252+ photoElement = parseExifAndTags (filepath )
253+ if photoElement :
254+ # this has exif data
255+ date = photoElement .date ()
256+ if date in photoGroups .keys ():
257+ photoGroups [date ].append (photoElement )
258+ else :
259+ photoGroups [date ] = [photoElement ]
260+
261+ for date in photoGroups .keys ():
262+ #print(date)
263+ fromTimecode = int (datetime .strptime (date .isoformat (), '%Y-%m-%d' ).timestamp ())
264+ toTimecode = int ((datetime .strptime (date .isoformat (), '%Y-%m-%d' ) + timedelta (days = 1 )).timestamp ())- 1
265+
266+ toDo = True
267+ start = 0
268+ size = 1000
269+
270+ plexData = {}
271+ #print('loop through all, started %i' % int(time.time()))
272+ while toDo :
273+ if len (p .photoSections ):
274+ url = "/library/sections/" + p .photoSections [0 ] + "/all?originallyAvailableAt%3E=" + str (fromTimecode ) + "&originallyAvailableAt%3C=" + str (toTimecode ) + "&X-Plex-Container-Start=%i&X-Plex-Container-Size=%i" % (start , size )
275+ metadata = p .fetchPlexApi (url )
276+ container = metadata ["MediaContainer" ]
277+ elements = container ["Metadata" ]
278+ totalSize = container ["totalSize" ]
279+ offset = container ["offset" ]
280+ size = container ["size" ]
281+ start = start + size
282+ if totalSize - offset - size == 0 :
283+ toDo = False
284+ # loop through all elements
285+ for photo in elements :
286+ mediaType = photo ["type" ]
287+ if mediaType != "photo" :
288+ continue
289+ key = photo ["ratingKey" ]
290+ src = photo ["Media" ][0 ]["Part" ][0 ]["file" ].replace (ppTagConfig .PHOTOS_LIBRARY_PATH_PLEX ,"" , 1 )
291+
292+ plexData [src ] = key
293+
294+ for photo in photoGroups [date ]:
295+ path = photo .path ()
296+ # make sure path seperator is equal in plex and ppTag
297+ if "/" in ppTagConfig .PHOTOS_LIBRARY_PATH_PLEX :
298+ path = path .replace ("\\ " ,"/" )
299+ if path in plexData .keys ():
300+ updateMetadata (plexData [path ], photo .tags (), photo .rating ()* 2 )
301+ photoGroups [date ].remove (photo )
302+
303+
304+ for photo in photoGroups [date ]:
305+ # we need to fetch all data as this method failed
306+ # print(photo.path() + " was not processed!")
307+ doUpdate = [* doUpdate , * doUpdateTemp ]
308+ return True
309+
310+ # after the loop we maybe have new or modifed files which was blocked before so trigger again
311+ if len (doUpdate ):
312+ triggerProcess ()
313+
314+ return False
167315
168316def loopThroughAllPhotos ():
169317 global doUpdate
@@ -172,7 +320,7 @@ def loopThroughAllPhotos():
172320 doUpdate = []
173321 toDo = True
174322 start = 0
175- size = 100
323+ size = 1000
176324 #print('loop through all, started %i' % int(time.time()))
177325 while toDo :
178326 if len (p .photoSections ):
@@ -194,17 +342,30 @@ def loopThroughAllPhotos():
194342 key = photo ["ratingKey" ]
195343 src = photo ["Media" ][0 ]["Part" ][0 ]["file" ].replace (ppTagConfig .PHOTOS_LIBRARY_PATH_PLEX ,"" , 1 )
196344
345+ # make sure path seperator is equal in plex and ppTag
346+ if "\\ " in ppTagConfig .PHOTOS_LIBRARY_PATH :
347+ src = src .replace ("/" ,"\\ " )
348+
197349 if src in doUpdateTemp or firstRun :
198350
199351 # update tags and rating
200352 # print(key)
201353 # print(src)
202354 updateTagsAndRating (key , src )
355+ if not firstRun :
356+ doUpdateTemp .remove (src )
357+
358+ if len (doUpdateTemp ) == 0 and not firstRun :
359+ # finished
360+ # after the loop we maybe have new or modifed files which was blocked before so trigger again
361+ if len (doUpdate ):
362+ triggerProcess ()
363+ return
203364
204365
205366 # after the loop we maybe have new or modifed files which was blocked before so trigger again
206367 if len (doUpdate ):
207- triggerLoopThroughAllPhotos ()
368+ triggerProcess ()
208369 #print("change detected while processing, retrigger")
209370 #print('loop through all, done %i' % int(time.time()))
210371 firstRun = False
@@ -227,7 +388,7 @@ def process(self, event):
227388 if not event .is_directory :
228389 # put file into forced update list
229390 doUpdate .append (event .src_path .replace (ppTagConfig .PHOTOS_LIBRARY_PATH ,"" , 1 ))
230- triggerLoopThroughAllPhotos ()
391+ triggerProcess ()
231392
232393 def on_modified (self , event ):
233394 self .process (event )
@@ -244,12 +405,12 @@ def on_created(self, event):
244405
245406 # setup timer
246407 # wait 120 sec after change was detected
247- t = OneShotQueueTimer (120 , loopThroughAllPhotos )
408+ t = OneShotQueueTimer (120 , fetchPhotosAndProcess )
248409
249410 p = plexUsers ()
250411
251412 # run at startup
252- loopThroughAllPhotos ()
413+ fetchPhotosAndProcess ()
253414
254415 # now start the observer
255416 observer .start ()
@@ -261,4 +422,3 @@ def on_created(self, event):
261422 observer .stop ()
262423
263424 observer .join ()
264-
0 commit comments