Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved photo playback/slideshow #252

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
41 changes: 28 additions & 13 deletions lib/_included_packages/plexnet/playqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions lib/_included_packages/plexnet/plexplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
159 changes: 133 additions & 26 deletions lib/windows/photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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()
Expand Down Expand Up @@ -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')))
Expand Down Expand Up @@ -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')
Expand All @@ -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):
Expand Down
21 changes: 20 additions & 1 deletion resources/skins/Main/1080i/script-plex-photo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,29 @@
<posy>0</posy>
<width>1920</width>
<height>1080</height>
<fadetime>200</fadetime>
<fadetime>1000</fadetime>
<texture fallback="script.plex/thumb_fallbacks/broken-photo.png" background="true">$INFO[Window.Property(photo)]</texture>
<aspectratio>keep</aspectratio>
</control>
<control type="group">
<visible>!String.IsEmpty(Window.Property(is.updating))</visible>
<animation effect="fade" time="500">VisibleChange</animation>
<control type="image">
<posx>840</posx>
<posy>465</posy>
<width>240</width>
<height>150</height>
<texture>script.plex/busy-back.png</texture>
<colordiffuse>A0FFFFFF</colordiffuse>
</control>
<control type="image">
<posx>915</posx>
<posy>521</posy>
<width>90</width>
<height>38</height>
<texture diffuse="script.plex/busy-diffuse.png">script.plex/busy.gif</texture>
</control>
</control>
</control>
<control type="togglebutton" id="250">
<posx>0</posx>
Expand Down