From 83f7c8a0c050d2ef56367925c68dbe97486250c6 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 05:59:28 +0200 Subject: [PATCH 01/10] poor man's PoC for a fast pre-loading photo slideshow --- lib/_included_packages/plexnet/playqueue.py | 41 +++++--- lib/_included_packages/plexnet/plexplayer.py | 11 ++- lib/windows/photos.py | 99 ++++++++++++++++--- .../skins/Main/1080i/script-plex-photo.xml | 21 +++- 4 files changed, 138 insertions(+), 34 deletions(-) diff --git a/lib/_included_packages/plexnet/playqueue.py b/lib/_included_packages/plexnet/playqueue.py index ccf2630cf..4f161178f 100644 --- a/lib/_included_packages/plexnet/playqueue.py +++ b/lib/_included_packages/plexnet/playqueue.py @@ -481,32 +481,47 @@ def hasPrev(self): return self.items().index(self.current()) > 0 def next(self): - if not self.hasNext(): - return None - if self.isRepeatOne: return self.current() - pos = self.items().index(self.current()) + 1 - if pos >= len(self.items()): - if not self.isRepeat or self.isWindowed(): - return None - pos = 0 + item = self.getNext() + if not item: + return None - item = self.items()[pos] self.selectedId = item.playQueueItemID.asInt() return item def prev(self): - if not self.hasPrev(): - return None if self.isRepeatOne: return self.current() - pos = self.items().index(self.current()) - 1 - item = self.items()[pos] + + item = self.getPrev() + if not item: + return None + self.selectedId = item.playQueueItemID.asInt() return item + def getPrev(self): + if not self.hasPrev(): + return None + + pos = self.items().index(self.current()) - 1 + return self.items()[pos] + + def getNext(self): + if not self.hasNext(): + return None + + pos = self.items().index(self.current()) + 1 + if pos >= len(self.items()): + if not self.isRepeat or self.isWindowed(): + return None + pos = 0 + + return self.items()[pos] + + def setCurrent(self, pos): if pos < 0 or pos >= len(self.items()): return False diff --git a/lib/_included_packages/plexnet/plexplayer.py b/lib/_included_packages/plexnet/plexplayer.py index 7639aa047..72f6825c7 100644 --- a/lib/_included_packages/plexnet/plexplayer.py +++ b/lib/_included_packages/plexnet/plexplayer.py @@ -598,14 +598,17 @@ def __init__(self, item): self.media = item.media()[0] self.metadata = None - def build(self): - if self.media.parts and self.media.parts[0]: + def build(self, item=None): + item = item or self.item + media = item.media()[0] + if media.parts and media.parts[0]: obj = util.AttributeDict() - part = self.media.parts[0] + part = media.parts[0] path = part.key or part.thumb - server = self.item.getServer() + server = item.getServer() + obj.path = path obj.url = server.buildUrl(path, True) obj.enableBlur = server.supportsPhotoTranscoding diff --git a/lib/windows/photos.py b/lib/windows/photos.py index ff7a6b647..3fd5a648f 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -2,10 +2,14 @@ import time import os +import xbmc import xbmcgui - import kodigui import busy +import tempfile +import shutil +import hashlib +import requests from lib import util, colors from plexnet import plexapp, plexplayer, playqueue @@ -37,6 +41,9 @@ class PhotoWindow(kodigui.BaseWindow): SLIDESHOW_INTERVAL = 3 + PHOTO_STACK_SIZE = 10 + tempSubFolder = ("p4k", "photos") + def __init__(self, *args, **kwargs): kodigui.BaseWindow.__init__(self, *args, **kwargs) self.photo = kwargs.get('photo') @@ -54,8 +61,19 @@ def __init__(self, *args, **kwargs): self.showPhotoThread = None self.showPhotoTimeout = 0 self.rotate = 0 + self.tempFolder = None + self.photoStack = [] def onFirstInit(self): + self.tempFolder = os.path.join(tempfile.gettempdir(), *self.tempSubFolder) + #self.tempFolder = os.path.join(xbmc.translatePath("special://temp/"), *self.tempSubFolder) + if not os.path.exists(self.tempFolder): + try: + os.makedirs(self.tempFolder) + except OSError: + if not os.path.isdir(self.tempFolder): + util.ERROR() + self.pqueueList = kodigui.ManagedControlList(self, self.PQUEUE_LIST_ID, 14) self.setProperty('photo', 'script.plex/indicators/busy-photo.gif') self.getPlayQueue() @@ -223,29 +241,76 @@ def showPhoto(self, **kwargs): photo = self.playQueue.current() self.updatePqueueListSelection(photo) - self.showPhotoTimeout = time.time() + 0.2 if not self.showPhotoThread or not self.showPhotoThread.isAlive(): self.showPhotoThread = threading.Thread(target=self._showPhoto, name="showphoto") self.showPhotoThread.start() def _showPhoto(self): - while not util.MONITOR.waitForAbort(0.1): - if time.time() >= self.showPhotoTimeout: - break - - self._reallyShowPhoto() - - @busy.dialog() - def _reallyShowPhoto(self): - self.setProperty('photo', 'script.plex/indicators/busy-photo.gif') + """ + load the current photo, preload the previous and the next one + :return: + """ photo = self.playQueue.current() - photo.softReload() + loadItems = (photo, self.playQueue.getNext(), self.playQueue.getPrev()) + for item in loadItems: + item.softReload() + self.playerObject = plexplayer.PlexPhotoPlayer(photo) - meta = self.playerObject.build() - url = photo.server.getImageTranscodeURL(meta.get('url', ''), self.width, self.height) + + addToStack = [] + for item in loadItems: + if not item: + continue + + meta = self.playerObject.build(item=item) + bgURL = item.thumb.asTranscodedImageURL(self.width, self.height, blur=128, opacity=60, + background=colors.noAlpha.Background) + + isCurrent = item == photo + if isCurrent: + self.setBoolProperty('is.updating', True) + + path, background = self.getCachedPhotoData(meta.path, meta.url, bgURL) + if (path, background) not in self.photoStack: + addToStack.append((path, background)) + + if isCurrent: + self._reallyShowPhoto(item, path, background) + self.setBoolProperty('is.updating', False) + + # maintain cache folder + self.photoStack = addToStack + self.photoStack + if len(self.photoStack) > self.PHOTO_STACK_SIZE: + clean = self.photoStack[self.PHOTO_STACK_SIZE:] + self.photoStack = self.photoStack[:self.PHOTO_STACK_SIZE] + for remList in clean: + for rem in remList: + try: + os.remove(rem) + except: + pass + + def getCachedPhotoData(self, path, url, bgURL): + if not url: + return + + ext = os.path.splitext(path)[1] + basename = hashlib.sha1(url).hexdigest() + tmpPath = os.path.join(self.tempFolder, basename + ext) + tmpBgPath = os.path.join(self.tempFolder, "%s_bg%s" % (basename, ext)) + + for p, url in ((tmpPath, url), (tmpBgPath, bgURL)): + if not os.path.exists(p):# and not xbmc.getCacheThumbName(tmpFn): + r = requests.get(url, allow_redirects=True) + with open(p, 'wb') as f: + f.write(r.content) + + return tmpPath, tmpBgPath + + def _reallyShowPhoto(self, photo, path, background): self.setRotation(0) - self.setProperty('photo', url) - self.setProperty('background', photo.thumb.asTranscodedImageURL(self.width, self.height, blur=128, opacity=60, background=colors.noAlpha.Background)) + self.setProperty('photo', path) + self.setProperty('background', background) self.setProperty('photo.title', photo.title) self.setProperty('photo.date', util.cleanLeadingZeros(photo.originallyAvailableAt.asDatetime('%d %B %Y'))) @@ -344,6 +409,8 @@ def stop(self): def doClose(self): self.pause() + shutil.rmtree(self.tempFolder, ignore_errors=True) + kodigui.BaseWindow.doClose(self) def getCurrentItem(self): diff --git a/resources/skins/Main/1080i/script-plex-photo.xml b/resources/skins/Main/1080i/script-plex-photo.xml index cad029b61..1fbc565ab 100644 --- a/resources/skins/Main/1080i/script-plex-photo.xml +++ b/resources/skins/Main/1080i/script-plex-photo.xml @@ -37,10 +37,29 @@ 0 1920 1080 - 200 + 1000 $INFO[Window.Property(photo)] keep + + !String.IsEmpty(Window.Property(is.updating)) + VisibleChange + + 840 + 465 + 240 + 150 + script.plex/busy-back.png + A0FFFFFF + + + 915 + 521 + 90 + 38 + script.plex/busy.gif + + 0 From c637e932a177512ee33a11ff05696fdfa3376579 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 16:43:00 +0200 Subject: [PATCH 02/10] don't show intermediate loading state on initial load; add timeouts for photo requests and loading; use photo size of the old method; --- lib/windows/photos.py | 99 ++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 3fd5a648f..54c4c849d 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -63,6 +63,7 @@ def __init__(self, *args, **kwargs): self.rotate = 0 self.tempFolder = None self.photoStack = [] + self.initialLoad = True def onFirstInit(self): self.tempFolder = os.path.join(tempfile.gettempdir(), *self.tempSubFolder) @@ -245,6 +246,18 @@ def showPhoto(self, **kwargs): self.showPhotoThread = threading.Thread(target=self._showPhoto, name="showphoto") self.showPhotoThread.start() + # wait for the current thread to end, which might still be loading the surrounding images, for 10 seconds + elif self.showPhotoThread and self.showPhotoThread.isAlive(): + waitedFor = 0 + self.setBoolProperty('is.updating', True) + while waitedFor < 10: + if not self.showPhotoThread.isAlive() and not xbmc.abortRequested: + return self.showPhoto(**kwargs) + util.MONITOR.waitForAbort(0.1) + waitedFor += 0.1 + + # fixme raise error here + def _showPhoto(self): """ load the current photo, preload the previous and the next one @@ -258,37 +271,45 @@ def _showPhoto(self): self.playerObject = plexplayer.PlexPhotoPlayer(photo) addToStack = [] - for item in loadItems: - if not item: - continue + try: + for item in loadItems: + if not item: + continue + + meta = self.playerObject.build(item=item) + url = photo.server.getImageTranscodeURL(meta.get('url', ''), self.width, self.height) + bgURL = item.thumb.asTranscodedImageURL(self.width, self.height, blur=128, opacity=60, + background=colors.noAlpha.Background) + + isCurrent = item == photo + if isCurrent and not self.initialLoad: + self.setBoolProperty('is.updating', True) - meta = self.playerObject.build(item=item) - bgURL = item.thumb.asTranscodedImageURL(self.width, self.height, blur=128, opacity=60, - background=colors.noAlpha.Background) - - isCurrent = item == photo - if isCurrent: - self.setBoolProperty('is.updating', True) - - path, background = self.getCachedPhotoData(meta.path, meta.url, bgURL) - if (path, background) not in self.photoStack: - addToStack.append((path, background)) - - if isCurrent: - self._reallyShowPhoto(item, path, background) - self.setBoolProperty('is.updating', False) - - # maintain cache folder - self.photoStack = addToStack + self.photoStack - if len(self.photoStack) > self.PHOTO_STACK_SIZE: - clean = self.photoStack[self.PHOTO_STACK_SIZE:] - self.photoStack = self.photoStack[:self.PHOTO_STACK_SIZE] - for remList in clean: - for rem in remList: - try: - os.remove(rem) - except: - pass + path, background = self.getCachedPhotoData(meta.path, url, bgURL) + if not path and background: + return + + if (path, background) not in self.photoStack: + addToStack.append((path, background)) + + if isCurrent: + self._reallyShowPhoto(item, path, background) + self.setBoolProperty('is.updating', False) + self.initialLoad = False + + # maintain cache folder + self.photoStack = addToStack + self.photoStack + if len(self.photoStack) > self.PHOTO_STACK_SIZE: + clean = self.photoStack[self.PHOTO_STACK_SIZE:] + self.photoStack = self.photoStack[:self.PHOTO_STACK_SIZE] + for remList in clean: + for rem in remList: + try: + os.remove(rem) + except: + pass + finally: + self.setBoolProperty('is.updating', False) def getCachedPhotoData(self, path, url, bgURL): if not url: @@ -301,9 +322,15 @@ def getCachedPhotoData(self, path, url, bgURL): for p, url in ((tmpPath, url), (tmpBgPath, bgURL)): if not os.path.exists(p):# and not xbmc.getCacheThumbName(tmpFn): - r = requests.get(url, allow_redirects=True) - with open(p, 'wb') as f: - f.write(r.content) + try: + r = requests.get(url, allow_redirects=True, timeout=10.0) + r.raise_for_status() + except Exception, e: + util.ERROR("Couldn't load image: %s" % e, notify=True) + return None, None + else: + with open(p, 'wb') as f: + f.write(r.content) return tmpPath, tmpBgPath @@ -381,12 +408,18 @@ def start(self): self.setFocusId(self.OVERLAY_BUTTON_ID) def prev(self): + if self.showPhotoThread and self.showPhotoThread.isAlive(): + return + if not self.playQueue.prev(): return self.updateProperties() self.showPhoto() def next(self): + if self.showPhotoThread and self.showPhotoThread.isAlive(): + return + if not self.playQueue.next(): return self.updateProperties() From f99e27183b3ac4c48238b52f988037d106b2a32c Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 16:51:39 +0200 Subject: [PATCH 03/10] no selected pos: bail out --- lib/windows/photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 54c4c849d..10f12c98d 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -231,7 +231,7 @@ def fillPqueueList(self, **kwargs): def updatePqueueListSelection(self, current=None): selected = self.pqueueList.getListItemByDataSource(current or self.playQueue.current()) - if not selected: + if not selected or not selected.pos(): return self.pqueueList.selectItem(selected.pos()) From c057eaac53a42c4929196853f9fd7f471a2cb292 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 16:58:06 +0200 Subject: [PATCH 04/10] prevent erratic prev/next behaviour upon action spam --- lib/windows/photos.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 10f12c98d..4f561c8d5 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -236,13 +236,20 @@ def updatePqueueListSelection(self, current=None): self.pqueueList.selectItem(selected.pos()) - def showPhoto(self, **kwargs): + def showPhoto(self, trigger=None, **kwargs): self.slideshowNext = 0 - photo = self.playQueue.current() - self.updatePqueueListSelection(photo) - if not self.showPhotoThread or not self.showPhotoThread.isAlive(): + # if trigger is given, trigger it. trigger loads the next or prev item, depending on what was requested + # doing this here, this late prevents erratic behaviour when multiple next/prev calls were made but we were + # still loading images + if trigger: + trigger() + self.updateProperties() + + photo = self.playQueue.current() + self.updatePqueueListSelection(photo) + self.showPhotoThread = threading.Thread(target=self._showPhoto, name="showphoto") self.showPhotoThread.start() @@ -408,22 +415,14 @@ def start(self): self.setFocusId(self.OVERLAY_BUTTON_ID) def prev(self): - if self.showPhotoThread and self.showPhotoThread.isAlive(): - return - - if not self.playQueue.prev(): + if not self.playQueue.getPrev(): return - self.updateProperties() - self.showPhoto() + self.showPhoto(trigger=lambda: self.playQueue.next()) def next(self): - if self.showPhotoThread and self.showPhotoThread.isAlive(): + if not self.playQueue.getNext(): return - - if not self.playQueue.next(): - return - self.updateProperties() - self.showPhoto() + self.showPhoto(trigger=lambda: self.playQueue.prev()) def play(self): self.setProperty('playing', '1') From 1e22a7a51ce0bad63770283ec5960016d5ebfa83 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 17:01:37 +0200 Subject: [PATCH 05/10] remove redundant condition --- lib/windows/photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 4f561c8d5..2265ea01e 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -254,7 +254,7 @@ def showPhoto(self, trigger=None, **kwargs): self.showPhotoThread.start() # wait for the current thread to end, which might still be loading the surrounding images, for 10 seconds - elif self.showPhotoThread and self.showPhotoThread.isAlive(): + elif self.showPhotoThread.isAlive(): waitedFor = 0 self.setBoolProperty('is.updating', True) while waitedFor < 10: From 8f4d48b54471fe36f4c92fcc48591d90dd0d9844 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 17:06:49 +0200 Subject: [PATCH 06/10] fix condition --- lib/windows/photos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 2265ea01e..75ebfcbc8 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -293,7 +293,7 @@ def _showPhoto(self): self.setBoolProperty('is.updating', True) path, background = self.getCachedPhotoData(meta.path, url, bgURL) - if not path and background: + if not (path and background): return if (path, background) not in self.photoStack: From 9c30b5b83d3d5efaafe53573b16e8cf6ec4e33ce Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 17:20:13 +0200 Subject: [PATCH 07/10] properly add (next, current, prev) to the top of the photo stack instead of (current, next, prev) --- lib/windows/photos.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 75ebfcbc8..cc38095eb 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -271,7 +271,8 @@ def _showPhoto(self): :return: """ photo = self.playQueue.current() - loadItems = (photo, self.playQueue.getNext(), self.playQueue.getPrev()) + next = self.playQueue.getNext() + loadItems = (photo, next, self.playQueue.getPrev()) for item in loadItems: item.softReload() @@ -297,7 +298,11 @@ def _showPhoto(self): return if (path, background) not in self.photoStack: - addToStack.append((path, background)) + if item == next: + # move the next image to the top of the stack + addToStack.insert(0, (path, background)) + else: + addToStack.append((path, background)) if isCurrent: self._reallyShowPhoto(item, path, background) From dbe137813c220694418b2bc8092bf17ece2c895a Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 17:25:35 +0200 Subject: [PATCH 08/10] fix swapped next/prev --- lib/windows/photos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index cc38095eb..43a87b1fd 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -422,12 +422,12 @@ def start(self): def prev(self): if not self.playQueue.getPrev(): return - self.showPhoto(trigger=lambda: self.playQueue.next()) + self.showPhoto(trigger=lambda: self.playQueue.prev()) def next(self): if not self.playQueue.getNext(): return - self.showPhoto(trigger=lambda: self.playQueue.prev()) + self.showPhoto(trigger=lambda: self.playQueue.next()) def play(self): self.setProperty('playing', '1') From 6c653fc10d4cda4f5345edcc829657476dc64bc4 Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 17:28:42 +0200 Subject: [PATCH 09/10] handle xbmc.abortRequested --- lib/windows/photos.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/windows/photos.py b/lib/windows/photos.py index 43a87b1fd..bcfad5eb9 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -260,6 +260,10 @@ def showPhoto(self, trigger=None, **kwargs): while waitedFor < 10: if not self.showPhotoThread.isAlive() and not xbmc.abortRequested: return self.showPhoto(**kwargs) + elif xbmc.abortRequested: + self.setBoolProperty('is.updating', False) + return + util.MONITOR.waitForAbort(0.1) waitedFor += 0.1 From f5007cfd15cef3a1b0e4fbda4fbbed4599b43b9f Mon Sep 17 00:00:00 2001 From: panni Date: Sun, 9 Sep 2018 19:14:32 +0200 Subject: [PATCH 10/10] store temp files extensionless --- lib/_included_packages/plexnet/plexplayer.py | 1 - lib/windows/photos.py | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/_included_packages/plexnet/plexplayer.py b/lib/_included_packages/plexnet/plexplayer.py index 72f6825c7..26a1d8d15 100644 --- a/lib/_included_packages/plexnet/plexplayer.py +++ b/lib/_included_packages/plexnet/plexplayer.py @@ -608,7 +608,6 @@ def build(self, item=None): path = part.key or part.thumb server = item.getServer() - obj.path = path obj.url = server.buildUrl(path, True) obj.enableBlur = server.supportsPhotoTranscoding diff --git a/lib/windows/photos.py b/lib/windows/photos.py index bcfad5eb9..61e7f2100 100644 --- a/lib/windows/photos.py +++ b/lib/windows/photos.py @@ -297,7 +297,7 @@ def _showPhoto(self): if isCurrent and not self.initialLoad: self.setBoolProperty('is.updating', True) - path, background = self.getCachedPhotoData(meta.path, url, bgURL) + path, background = self.getCachedPhotoData(url, bgURL) if not (path and background): return @@ -327,14 +327,13 @@ def _showPhoto(self): finally: self.setBoolProperty('is.updating', False) - def getCachedPhotoData(self, path, url, bgURL): + def getCachedPhotoData(self, url, bgURL): if not url: return - ext = os.path.splitext(path)[1] basename = hashlib.sha1(url).hexdigest() - tmpPath = os.path.join(self.tempFolder, basename + ext) - tmpBgPath = os.path.join(self.tempFolder, "%s_bg%s" % (basename, ext)) + tmpPath = os.path.join(self.tempFolder, basename) + tmpBgPath = os.path.join(self.tempFolder, "%s_bg" % basename) for p, url in ((tmpPath, url), (tmpBgPath, bgURL)): if not os.path.exists(p):# and not xbmc.getCacheThumbName(tmpFn):