Skip to content

Commit b0d6062

Browse files
authored
Merge pull request #6 from arehbein-git/#5-implement-date-based-fetching-and-processing
#5: add date based process, fix paths (win/linux)
2 parents b0064e3 + 61e1d5e commit b0d6062

2 files changed

Lines changed: 200 additions & 8 deletions

File tree

photoElement.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from datetime import datetime, date
2+
3+
class PhotoElement():
4+
"""A Object holding the information of the photo"""
5+
6+
_path = ""
7+
_date = datetime.today().date()
8+
_tags = []
9+
_rating = 0
10+
11+
def __init__(self, path, date, tags, rating):
12+
"""path: path of photo including filename, date: date photo was taken [datetime.date], tags: xmp tags [list], rating: 0-5"""
13+
self._path = path
14+
self._date = date
15+
self._tags = tags
16+
self._rating = rating
17+
18+
def path(self):
19+
"""returns the path of the photo"""
20+
return self._path
21+
22+
def date(self):
23+
"""returns the date of the photo"""
24+
return self._date
25+
26+
def tags(self):
27+
"""returns the tags of the photo"""
28+
return self._tags
29+
30+
def rating(self):
31+
"""returns the rating of the photo"""
32+
return self._rating

pptag.py

Lines changed: 168 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import logging
99
import urllib
1010
import time
11+
import os
12+
from datetime import datetime, date, timedelta
1113
from threading import Timer
1214
from watchdog.observers import Observer
1315
from watchdog.events import PatternMatchingEventHandler
@@ -17,6 +19,7 @@
1719
from OneShotQueueTimer import OneShotQueueTimer
1820
from plexUsers import plexUsers
1921
from lightroomTags import parse_xmp_for_lightroom_tags
22+
from photoElement import PhotoElement
2023

2124
from 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

168316
def 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

Comments
 (0)