diff --git a/lib/_included_packages/plexnet/playqueue.py b/lib/_included_packages/plexnet/playqueue.py index ccf2630c..4f161178 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 7639aa04..26a1d8d1 100644 --- a/lib/_included_packages/plexnet/plexplayer.py +++ b/lib/_included_packages/plexnet/plexplayer.py @@ -598,13 +598,15 @@ 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.url = server.buildUrl(path, True) obj.enableBlur = server.supportsPhotoTranscoding diff --git a/lib/windows/photos.py b/lib/windows/photos.py index ff7a6b64..61e7f210 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,20 @@ def __init__(self, *args, **kwargs): self.showPhotoThread = None self.showPhotoTimeout = 0 self.rotate = 0 + self.tempFolder = None + self.photoStack = [] + self.initialLoad = True 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() @@ -212,40 +231,128 @@ 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()) - def showPhoto(self, **kwargs): + def showPhoto(self, trigger=None, **kwargs): self.slideshowNext = 0 - photo = self.playQueue.current() - self.updatePqueueListSelection(photo) - - self.showPhotoTimeout = time.time() + 0.2 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() - def _showPhoto(self): - while not util.MONITOR.waitForAbort(0.1): - if time.time() >= self.showPhotoTimeout: - break + # wait for the current thread to end, which might still be loading the surrounding images, for 10 seconds + elif 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) + elif xbmc.abortRequested: + self.setBoolProperty('is.updating', False) + return - self._reallyShowPhoto() + util.MONITOR.waitForAbort(0.1) + waitedFor += 0.1 - @busy.dialog() - def _reallyShowPhoto(self): - self.setProperty('photo', 'script.plex/indicators/busy-photo.gif') + # fixme raise error here + + def _showPhoto(self): + """ + load the current photo, preload the previous and the next one + :return: + """ photo = self.playQueue.current() - photo.softReload() + next = self.playQueue.getNext() + loadItems = (photo, next, 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 = [] + 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) + + path, background = self.getCachedPhotoData(url, bgURL) + if not (path and background): + return + + if (path, background) not in self.photoStack: + 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) + 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, url, bgURL): + if not url: + return + + basename = hashlib.sha1(url).hexdigest() + 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): + 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 + + 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'))) @@ -316,16 +423,14 @@ def start(self): self.setFocusId(self.OVERLAY_BUTTON_ID) def prev(self): - if not self.playQueue.prev(): + if not self.playQueue.getPrev(): return - self.updateProperties() - self.showPhoto() + self.showPhoto(trigger=lambda: self.playQueue.prev()) def next(self): - if not self.playQueue.next(): + if not self.playQueue.getNext(): return - self.updateProperties() - self.showPhoto() + self.showPhoto(trigger=lambda: self.playQueue.next()) def play(self): self.setProperty('playing', '1') @@ -344,6 +449,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 cad029b6..1fbc565a 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