Switch to stream playback, part II
This commit is contained in:
parent
7c6fdad770
commit
059ed7a5f0
13 changed files with 311 additions and 86 deletions
|
@ -72,10 +72,13 @@ class Movie(ItemBase):
|
||||||
scraper='metadata.local')
|
scraper='metadata.local')
|
||||||
if do_indirect:
|
if do_indirect:
|
||||||
# Set plugin path and media flags using real filename
|
# Set plugin path and media flags using real filename
|
||||||
filename = api.file_name(force_first_media=True)
|
path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
|
||||||
path = 'plugin://%s.movies/' % v.ADDON_ID
|
filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}&name={4}'
|
||||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
filename = filename.format(plex_id,
|
||||||
% (path, plex_id, v.PLEX_TYPE_MOVIE, filename))
|
kodi_id,
|
||||||
|
v.KODI_TYPE_MOVIE,
|
||||||
|
v.PLEX_TYPE_MOVIE,
|
||||||
|
api.file_name(force_first_media=True))
|
||||||
playurl = filename
|
playurl = filename
|
||||||
kodi_pathid = self.kodidb.get_path(path)
|
kodi_pathid = self.kodidb.get_path(path)
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ from logging import getLogger
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
from .. import path_ops, timing, variables as v, app
|
from .. import path_ops, timing, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodi_db.video')
|
LOG = getLogger('PLEX.kodi_db.video')
|
||||||
|
|
||||||
MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID
|
MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT
|
||||||
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID
|
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -449,6 +449,23 @@ def _playback_cleanup(ended=False):
|
||||||
app.PLAYSTATE.active_players = set()
|
app.PLAYSTATE.active_players = set()
|
||||||
LOG.info('Finished PKC playback cleanup')
|
LOG.info('Finished PKC playback cleanup')
|
||||||
|
|
||||||
|
def Playlist_OnAdd(self, server, data, *args, **kwargs):
|
||||||
|
'''
|
||||||
|
Detect widget playback. Widget for some reason, use audio playlists.
|
||||||
|
'''
|
||||||
|
LOG.debug('Playlist_OnAdd: %s, %s', server, data)
|
||||||
|
if data['position'] == 0:
|
||||||
|
if data['playlistid'] == 0:
|
||||||
|
utils.window('plex.playlist.audio', value='true')
|
||||||
|
else:
|
||||||
|
utils.window('plex.playlist.audio', clear=True)
|
||||||
|
self.playlistid = data['playlistid']
|
||||||
|
if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1:
|
||||||
|
|
||||||
|
LOG.info("--[ playlist ready ]")
|
||||||
|
utils.window('plex.playlist.ready', value='true')
|
||||||
|
utils.window('plex.playlist.start', clear=True)
|
||||||
|
|
||||||
|
|
||||||
def _record_playstate(status, ended):
|
def _record_playstate(status, ended):
|
||||||
if not status['plex_id']:
|
if not status['plex_id']:
|
||||||
|
|
|
@ -3,11 +3,12 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import plex_function as PF, utils, json_rpc, variables as v, \
|
from .playutils import PlayUtils
|
||||||
widgets
|
from .windows.resume import resume_dialog
|
||||||
|
from . import app, plex_functions as PF, utils, json_rpc, variables as v, \
|
||||||
|
widgets, playlist_func as PL, playqueue as PQ
|
||||||
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.playstrm')
|
LOG = getLogger('PLEX.playstrm')
|
||||||
|
@ -51,11 +52,13 @@ class PlayStrm(object):
|
||||||
self.transcode = params.get('transcode')
|
self.transcode = params.get('transcode')
|
||||||
if self.transcode is None:
|
if self.transcode is None:
|
||||||
self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None
|
self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None
|
||||||
if utils.window('plex.playlist.audio.bool'):
|
if utils.window('plex.playlist.audio'):
|
||||||
LOG.info('Audio playlist detected')
|
LOG.debug('Audio playlist detected')
|
||||||
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
|
||||||
else:
|
else:
|
||||||
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
LOG.debug('Video playlist detected')
|
||||||
|
self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
|
||||||
|
self.kodi_playlist = self.playqueue.kodi_pl
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ("{{"
|
return ("{{"
|
||||||
|
@ -97,6 +100,7 @@ class PlayStrm(object):
|
||||||
else:
|
else:
|
||||||
self.xml[0].set('pkc_db_item', None)
|
self.xml[0].set('pkc_db_item', None)
|
||||||
self.api = API(self.xml[0])
|
self.api = API(self.xml[0])
|
||||||
|
self.playqueue_item = PL.playlist_item_from_xml(self.xml[0])
|
||||||
|
|
||||||
def start_playback(self, index=0):
|
def start_playback(self, index=0):
|
||||||
LOG.debug('Starting playback at %s', index)
|
LOG.debug('Starting playback at %s', index)
|
||||||
|
@ -111,7 +115,7 @@ class PlayStrm(object):
|
||||||
else:
|
else:
|
||||||
self.start_index = max(self.kodi_playlist.getposition(), 0)
|
self.start_index = max(self.kodi_playlist.getposition(), 0)
|
||||||
self.index = self.start_index
|
self.index = self.start_index
|
||||||
listitem = xbmcgui.ListItem()
|
listitem = widgets.get_listitem(self.xml[0])
|
||||||
self._set_playlist(listitem)
|
self._set_playlist(listitem)
|
||||||
LOG.info('Initiating play for %s', self)
|
LOG.info('Initiating play for %s', self)
|
||||||
if not delayed:
|
if not delayed:
|
||||||
|
@ -159,24 +163,41 @@ class PlayStrm(object):
|
||||||
action set in Kodi for accurate resume behavior.
|
action set in Kodi for accurate resume behavior.
|
||||||
'''
|
'''
|
||||||
seektime = self._resume()
|
seektime = self._resume()
|
||||||
|
trailers = False
|
||||||
if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and
|
if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and
|
||||||
utils.settings('enableCinema') == 'true'):
|
utils.settings('enableCinema') == 'true'):
|
||||||
self._set_intros()
|
if utils.settings('askCinema') == "true":
|
||||||
|
# "Play trailers?"
|
||||||
play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server'])
|
trailers = utils.yesno_dialog(utils.lang(29999),
|
||||||
source = play.select_source(play.get_sources())
|
utils.lang(33016)) or False
|
||||||
|
else:
|
||||||
if not source:
|
trailers = True
|
||||||
raise PlayStrmException('Playback selection cancelled')
|
LOG.debug('Playing trailers: %s', trailers)
|
||||||
|
xml = PF.init_plex_playqueue(self.plex_id,
|
||||||
play.set_external_subs(source, listitem)
|
self.xml.get('librarySectionUUID'),
|
||||||
self.set_listitem(self.xml, listitem, self.kodi_id, seektime)
|
mediatype=self.plex_type,
|
||||||
listitem.setPath(self.xml['PlaybackInfo']['Path'])
|
trailers=trailers)
|
||||||
playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id)
|
if xml is None:
|
||||||
|
LOG.error('Could not get playqueue for UUID %s for %s',
|
||||||
self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
|
self.xml.get('librarySectionUUID'), self)
|
||||||
|
# "Play error"
|
||||||
|
utils.dialog('notification',
|
||||||
|
utils.lang(29999),
|
||||||
|
utils.lang(30128),
|
||||||
|
icon='{error}')
|
||||||
|
app.PLAYSTATE.context_menu_play = False
|
||||||
|
app.PLAYSTATE.force_transcode = False
|
||||||
|
app.PLAYSTATE.resume_playback = False
|
||||||
|
return
|
||||||
|
PL.get_playlist_details_from_xml(self.playqueue, xml)
|
||||||
|
# See that we add trailers, if they exist in the xml return
|
||||||
|
self._set_intros(xml)
|
||||||
|
listitem.setSubtitles(self.api.cache_external_subs())
|
||||||
|
play = PlayUtils(self.api, self.playqueue_item)
|
||||||
|
url = play.getPlayUrl().encode('utf-8')
|
||||||
|
listitem.setPath(url)
|
||||||
|
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
|
||||||
if self.xml.get('PartCount'):
|
if self.xml.get('PartCount'):
|
||||||
self._set_additional_parts()
|
self._set_additional_parts()
|
||||||
|
|
||||||
|
@ -203,52 +224,41 @@ class PlayStrm(object):
|
||||||
utils.window('plex.autoplay.bool', value='true')
|
utils.window('plex.autoplay.bool', value='true')
|
||||||
return seektime
|
return seektime
|
||||||
|
|
||||||
def _set_intros(self):
|
def _set_intros(self, xml):
|
||||||
'''
|
'''
|
||||||
if we have any play them when the movie/show is not being resumed.
|
if we have any play them when the movie/show is not being resumed.
|
||||||
'''
|
'''
|
||||||
if self.info['Intros']['Items']:
|
if not len(xml) > 1:
|
||||||
enabled = True
|
LOG.debug('No trailers returned from the PMS')
|
||||||
|
return
|
||||||
if utils.settings('askCinema') == 'true':
|
for intro in xml:
|
||||||
|
if utils.cast(int, xml.get('ratingKey')) == self.plex_id:
|
||||||
resp = dialog('yesno', heading='{emby}', line1=_(33016))
|
# The main item we're looking at - skip!
|
||||||
if not resp:
|
continue
|
||||||
|
api = API(intro)
|
||||||
enabled = False
|
listitem = widgets.get_listitem(intro)
|
||||||
LOG.info('Skip trailers.')
|
listitem.setSubtitles(api.cache_external_subs())
|
||||||
|
playqueue_item = PL.playlist_item_from_xml(intro)
|
||||||
if enabled:
|
play = PlayUtils(api, playqueue_item)
|
||||||
for intro in self.info['Intros']['Items']:
|
url = play.getPlayUrl().encode('utf-8')
|
||||||
|
listitem.setPath(url)
|
||||||
listitem = xbmcgui.ListItem()
|
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
|
||||||
LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name'])
|
|
||||||
|
|
||||||
play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['Server'])
|
|
||||||
source = play.select_source(play.get_sources())
|
|
||||||
self.set_listitem(intro, listitem, intro=True)
|
|
||||||
listitem.setPath(intro['PlaybackInfo']['Path'])
|
|
||||||
playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id)
|
|
||||||
|
|
||||||
self.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
|
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
utils.window('plex.skip.%s' % api.plex_id(), value='true')
|
||||||
utils.window('plex.skip.%s' % intro['plex_id'], value='true')
|
|
||||||
|
|
||||||
def _set_additional_parts(self):
|
def _set_additional_parts(self):
|
||||||
''' Create listitems and add them to the stack of playlist.
|
''' Create listitems and add them to the stack of playlist.
|
||||||
'''
|
'''
|
||||||
for part in self.info['AdditionalParts']['Items']:
|
for part, _ in enumerate(self.xml[0][0]):
|
||||||
|
if part == 0:
|
||||||
listitem = xbmcgui.ListItem()
|
# The first part that we've already added
|
||||||
LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name'])
|
continue
|
||||||
|
self.api.set_part_number(part)
|
||||||
play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server'])
|
listitem = widgets.get_listitem(self.xml[0])
|
||||||
source = play.select_source(play.get_sources())
|
listitem.setSubtitles(self.api.cache_external_subs())
|
||||||
play.set_external_subs(source, listitem)
|
playqueue_item = PL.playlist_item_from_xml(self.xml[0])
|
||||||
self.set_listitem(part, listitem)
|
play = PlayUtils(self.api, playqueue_item)
|
||||||
listitem.setPath(part['PlaybackInfo']['Path'])
|
url = play.getPlayUrl().encode('utf-8')
|
||||||
playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id)
|
listitem.setPath(url)
|
||||||
|
self.kodi_playlist.add(url=url, listitem=listitem, index=self.index)
|
||||||
self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
|
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
|
|
@ -33,7 +33,7 @@ class WebService(backgroundthread.KillableThread):
|
||||||
s.connect(('127.0.0.1', v.WEBSERVICE_PORT))
|
s.connect(('127.0.0.1', v.WEBSERVICE_PORT))
|
||||||
s.sendall('')
|
s.sendall('')
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
LOG.error(error)
|
LOG.error('is_alive error: %s', error)
|
||||||
if 'Errno 61' in str(error):
|
if 'Errno 61' in str(error):
|
||||||
alive = False
|
alive = False
|
||||||
s.close()
|
s.close()
|
||||||
|
@ -47,12 +47,12 @@ class WebService(backgroundthread.KillableThread):
|
||||||
conn.request('QUIT', '/')
|
conn.request('QUIT', '/')
|
||||||
conn.getresponse()
|
conn.getresponse()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
utils.ERROR()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
''' Called to start the webservice.
|
''' Called to start the webservice.
|
||||||
'''
|
'''
|
||||||
LOG.info('----===## Starting Webserver on port %s ##===----',
|
LOG.info('----===## Starting WebService on port %s ##===----',
|
||||||
v.WEBSERVICE_PORT)
|
v.WEBSERVICE_PORT)
|
||||||
app.APP.register_thread(self)
|
app.APP.register_thread(self)
|
||||||
try:
|
try:
|
||||||
|
@ -60,11 +60,12 @@ class WebService(backgroundthread.KillableThread):
|
||||||
RequestHandler)
|
RequestHandler)
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
LOG.error('Error encountered: %s', error)
|
||||||
if '10053' not in error: # ignore host diconnected errors
|
if '10053' not in error: # ignore host diconnected errors
|
||||||
utils.ERROR()
|
utils.ERROR()
|
||||||
finally:
|
finally:
|
||||||
app.APP.deregister_thread(self)
|
app.APP.deregister_thread(self)
|
||||||
LOG.info('##===---- Webserver stopped ----===##')
|
LOG.info('##===---- WebService stopped ----===##')
|
||||||
|
|
||||||
|
|
||||||
class HttpServer(BaseHTTPServer.HTTPServer):
|
class HttpServer(BaseHTTPServer.HTTPServer):
|
||||||
|
@ -75,7 +76,7 @@ class HttpServer(BaseHTTPServer.HTTPServer):
|
||||||
self.pending = []
|
self.pending = []
|
||||||
self.threads = []
|
self.threads = []
|
||||||
self.queue = Queue.Queue()
|
self.queue = Queue.Queue()
|
||||||
super(HttpServer, self).__init__(*args, **kwargs)
|
BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
def serve_forever(self):
|
def serve_forever(self):
|
||||||
|
|
||||||
|
@ -86,8 +87,9 @@ class HttpServer(BaseHTTPServer.HTTPServer):
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
''' Http request handler. Do not use LOG here,
|
'''
|
||||||
it will hang requests in Kodi > show information dialog.
|
Http request handler. Do not use LOG here, it will hang requests in Kodi >
|
||||||
|
show information dialog.
|
||||||
'''
|
'''
|
||||||
timeout = 0.5
|
timeout = 0.5
|
||||||
|
|
||||||
|
@ -101,8 +103,8 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
|
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
pass
|
xbmc.log('Plex.WebService handle error: %s' % error, xbmc.LOGWARNING)
|
||||||
|
|
||||||
def do_QUIT(self):
|
def do_QUIT(self):
|
||||||
''' send 200 OK response, and set server.stop to True
|
''' send 200 OK response, and set server.stop to True
|
||||||
|
@ -142,6 +144,7 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
def handle_request(self, headers_only=False):
|
def handle_request(self, headers_only=False):
|
||||||
'''Send headers and reponse
|
'''Send headers and reponse
|
||||||
'''
|
'''
|
||||||
|
xbmc.log('Plex.WebService handle_request called. path: %s ]' % self.path, xbmc.LOGWARNING)
|
||||||
try:
|
try:
|
||||||
if b'extrafanart' in self.path or b'extrathumbs' in self.path:
|
if b'extrafanart' in self.path or b'extrathumbs' in self.path:
|
||||||
raise Exception('unsupported artwork request')
|
raise Exception('unsupported artwork request')
|
||||||
|
@ -161,7 +164,6 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.send_error(500,
|
self.send_error(500,
|
||||||
b'PLEX.webservice: Exception occurred: %s' % error)
|
b'PLEX.webservice: Exception occurred: %s' % error)
|
||||||
xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING)
|
|
||||||
|
|
||||||
def strm(self):
|
def strm(self):
|
||||||
''' Return a dummy video and and queue real items.
|
''' Return a dummy video and and queue real items.
|
||||||
|
@ -268,7 +270,7 @@ class QueuePlay(backgroundthread.KillableThread):
|
||||||
params = self.server.queue.get(timeout=0.01)
|
params = self.server.queue.get(timeout=0.01)
|
||||||
except Queue.Empty:
|
except Queue.Empty:
|
||||||
count = 20
|
count = 20
|
||||||
while not utils.window('plex.playlist.ready.bool'):
|
while not utils.window('plex.playlist.ready'):
|
||||||
xbmc.sleep(50)
|
xbmc.sleep(50)
|
||||||
if not count:
|
if not count:
|
||||||
LOG.info('Playback aborted')
|
LOG.info('Playback aborted')
|
||||||
|
@ -280,14 +282,14 @@ class QueuePlay(backgroundthread.KillableThread):
|
||||||
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||||
play.start_playback()
|
play.start_playback()
|
||||||
else:
|
else:
|
||||||
utils.window('plex.playlist.play.bool', True)
|
utils.window('plex.playlist.play', value='true')
|
||||||
xbmc.sleep(1000)
|
xbmc.sleep(1000)
|
||||||
play.remove_from_playlist(start_position)
|
play.remove_from_playlist(start_position)
|
||||||
break
|
break
|
||||||
play = PlayStrm(params, params.get('ServerId'))
|
play = PlayStrm(params, params.get('ServerId'))
|
||||||
|
|
||||||
if start_position is None:
|
if start_position is None:
|
||||||
start_position = max(play.info['KodiPlaylist'].getposition(), 0)
|
start_position = max(play.kodi_playlist.getposition(), 0)
|
||||||
position = start_position + 1
|
position = start_position + 1
|
||||||
if play_folder:
|
if play_folder:
|
||||||
position = play.play_folder(position)
|
position = play.play_folder(position)
|
||||||
|
@ -300,13 +302,13 @@ class QueuePlay(backgroundthread.KillableThread):
|
||||||
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
|
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
|
||||||
except Exception:
|
except Exception:
|
||||||
utils.ERROR()
|
utils.ERROR()
|
||||||
play.info['KodiPlaylist'].clear()
|
play.kodi_playlist.clear()
|
||||||
xbmc.Player().stop()
|
xbmc.Player().stop()
|
||||||
self.server.queue.queue.clear()
|
self.server.queue.queue.clear()
|
||||||
if play_folder:
|
if play_folder:
|
||||||
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||||
else:
|
else:
|
||||||
utils.window('plex.playlist.aborted.bool', True)
|
utils.window('plex.playlist.aborted', value='true')
|
||||||
break
|
break
|
||||||
self.server.queue.task_done()
|
self.server.queue.task_done()
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ def get_listitem(xml_element):
|
||||||
"""
|
"""
|
||||||
item = generate_item(xml_element)
|
item = generate_item(xml_element)
|
||||||
prepare_listitem(item)
|
prepare_listitem(item)
|
||||||
return create_listitem(item)
|
return create_listitem(item, as_tuple=False)
|
||||||
|
|
||||||
|
|
||||||
def process_method_on_list(method_to_run, items):
|
def process_method_on_list(method_to_run, items):
|
||||||
|
|
81
resources/lib/windows/resume.py
Normal file
81
resources/lib/windows/resume.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
import xbmcaddon
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.resume')
|
||||||
|
|
||||||
|
XML_PATH = (xbmcaddon.Addon('plugin.video.plexkodiconnect').getAddonInfo('path'),
|
||||||
|
"default",
|
||||||
|
"1080i")
|
||||||
|
|
||||||
|
ACTION_PARENT_DIR = 9
|
||||||
|
ACTION_PREVIOUS_MENU = 10
|
||||||
|
ACTION_BACK = 92
|
||||||
|
RESUME = 3010
|
||||||
|
START_BEGINNING = 3011
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeDialog(xbmcgui.WindowXMLDialog):
|
||||||
|
|
||||||
|
_resume_point = None
|
||||||
|
selected_option = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def set_resume_point(self, time):
|
||||||
|
self._resume_point = time
|
||||||
|
|
||||||
|
def is_selected(self):
|
||||||
|
return True if self.selected_option is not None else False
|
||||||
|
|
||||||
|
def get_selected(self):
|
||||||
|
return self.selected_option
|
||||||
|
|
||||||
|
def onInit(self):
|
||||||
|
|
||||||
|
self.getControl(RESUME).setLabel(self._resume_point)
|
||||||
|
self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021))
|
||||||
|
|
||||||
|
def onAction(self, action):
|
||||||
|
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def onClick(self, controlID):
|
||||||
|
if controlID == RESUME:
|
||||||
|
self.selected_option = 1
|
||||||
|
self.close()
|
||||||
|
if controlID == START_BEGINNING:
|
||||||
|
self.selected_option = 0
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def resume_dialog(seconds):
|
||||||
|
'''
|
||||||
|
Base resume dialog based on Kodi settings
|
||||||
|
Returns True if PKC should resume, False if not, None if user backed out
|
||||||
|
of the dialog
|
||||||
|
'''
|
||||||
|
LOG.info("Resume dialog called")
|
||||||
|
dialog = ResumeDialog("script-plex-resume.xml", *XML_PATH)
|
||||||
|
dialog.set_resume_point("Resume from %s"
|
||||||
|
% unicode(timedelta(seconds=seconds)).split(".")[0])
|
||||||
|
dialog.doModal()
|
||||||
|
|
||||||
|
if dialog.is_selected():
|
||||||
|
if not dialog.get_selected():
|
||||||
|
# Start from beginning selected
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# User backed out
|
||||||
|
LOG.info("User exited without a selection")
|
||||||
|
return
|
||||||
|
return True
|
112
resources/skins/default/1080i/script-plex-resume.xml
Normal file
112
resources/skins/default/1080i/script-plex-resume.xml
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<window id="3301" type="dialog">
|
||||||
|
<defaultcontrol always="true">100</defaultcontrol>
|
||||||
|
<controls>
|
||||||
|
<control type="group">
|
||||||
|
<control type="image">
|
||||||
|
<top>0</top>
|
||||||
|
<bottom>0</bottom>
|
||||||
|
<left>0</left>
|
||||||
|
<right>0</right>
|
||||||
|
<texture colordiffuse="CC000000">white.png</texture>
|
||||||
|
<aspectratio>stretch</aspectratio>
|
||||||
|
<animation effect="fade" end="100" time="200">WindowOpen</animation>
|
||||||
|
<animation effect="fade" start="100" end="0" time="200">WindowClose</animation>
|
||||||
|
</control>
|
||||||
|
<control type="group">
|
||||||
|
<animation effect="slide" time="0" end="0,-15" condition="true">Conditional</animation>
|
||||||
|
<animation type="WindowOpen" reversible="false">
|
||||||
|
<effect type="zoom" start="80" end="100" center="960,540" delay="160" tween="circle" easin="out" time="240" />
|
||||||
|
<effect type="fade" delay="160" end="100" time="240" />
|
||||||
|
</animation>
|
||||||
|
<animation type="WindowClose" reversible="false">
|
||||||
|
<effect type="zoom" start="100" end="80" center="960,540" easing="in" tween="circle" easin="out" time="240" />
|
||||||
|
<effect type="fade" start="100" end="0" time="240" />
|
||||||
|
</animation>
|
||||||
|
<centerleft>50%</centerleft>
|
||||||
|
<centertop>50%</centertop>
|
||||||
|
<width>20%</width>
|
||||||
|
<height>90%</height>
|
||||||
|
<control type="grouplist" id="100">
|
||||||
|
<orientation>vertical</orientation>
|
||||||
|
<left>0</left>
|
||||||
|
<right>0</right>
|
||||||
|
<height>auto</height>
|
||||||
|
<align>center</align>
|
||||||
|
<itemgap>0</itemgap>
|
||||||
|
<onright>close</onright>
|
||||||
|
<onleft>close</onleft>
|
||||||
|
<usecontrolcoords>true</usecontrolcoords>
|
||||||
|
<control type="group">
|
||||||
|
<height>30</height>
|
||||||
|
<control type="image">
|
||||||
|
<left>20</left>
|
||||||
|
<width>100%</width>
|
||||||
|
<height>25</height>
|
||||||
|
<texture>logo-white.png</texture>
|
||||||
|
<aspectratio align="left">keep</aspectratio>
|
||||||
|
</control>
|
||||||
|
<control type="image">
|
||||||
|
<right>20</right>
|
||||||
|
<width>100%</width>
|
||||||
|
<height>25</height>
|
||||||
|
<aspectratio align="right">keep</aspectratio>
|
||||||
|
<texture diffuse="user_image.png">$INFO[Window(Home).Property(EmbyUserImage)]</texture>
|
||||||
|
<visible>!String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
|
||||||
|
</control>
|
||||||
|
<control type="image">
|
||||||
|
<right>20</right>
|
||||||
|
<width>100%</width>
|
||||||
|
<height>25</height>
|
||||||
|
<aspectratio align="right">keep</aspectratio>
|
||||||
|
<texture diffuse="user_image.png">userflyoutdefault.png</texture>
|
||||||
|
<visible>String.IsEmpty(Window(Home).Property(EmbyUserImage))</visible>
|
||||||
|
</control>
|
||||||
|
</control>
|
||||||
|
<control type="image">
|
||||||
|
<width>100%</width>
|
||||||
|
<height>10</height>
|
||||||
|
<texture border="5" colordiffuse="ff222326">dialogs/menu_top.png</texture>
|
||||||
|
</control>
|
||||||
|
<control type="button" id="3010">
|
||||||
|
<width>100%</width>
|
||||||
|
<height>65</height>
|
||||||
|
<align>left</align>
|
||||||
|
<aligny>center</aligny>
|
||||||
|
<textoffsetx>20</textoffsetx>
|
||||||
|
<font>font13</font>
|
||||||
|
<textcolor>ffe1e1e1</textcolor>
|
||||||
|
<focusedcolor>ffe1e1e1</focusedcolor>
|
||||||
|
<shadowcolor>66000000</shadowcolor>
|
||||||
|
<disabledcolor>FF404040</disabledcolor>
|
||||||
|
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
|
||||||
|
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
|
||||||
|
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
|
||||||
|
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
|
||||||
|
</control>
|
||||||
|
<control type="button" id="3011">
|
||||||
|
<width>100%</width>
|
||||||
|
<height>65</height>
|
||||||
|
<align>left</align>
|
||||||
|
<aligny>center</aligny>
|
||||||
|
<textoffsetx>20</textoffsetx>
|
||||||
|
<font>font13</font>
|
||||||
|
<textcolor>ffe1e1e1</textcolor>
|
||||||
|
<focusedcolor>ffe1e1e1</focusedcolor>
|
||||||
|
<shadowcolor>66000000</shadowcolor>
|
||||||
|
<disabledcolor>FF404040</disabledcolor>
|
||||||
|
<texturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</texturefocus>
|
||||||
|
<texturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</texturenofocus>
|
||||||
|
<alttexturefocus border="10" colordiffuse="ff303034">dialogs/menu_back.png</alttexturefocus>
|
||||||
|
<alttexturenofocus border="10" colordiffuse="ff222326">dialogs/menu_back.png</alttexturenofocus>
|
||||||
|
</control>
|
||||||
|
<control type="image">
|
||||||
|
<width>100%</width>
|
||||||
|
<height>10</height>
|
||||||
|
<texture border="5" colordiffuse="ff222326">dialogs/menu_bottom.png</texture>
|
||||||
|
</control>
|
||||||
|
</control>
|
||||||
|
</control>
|
||||||
|
</control>
|
||||||
|
</controls>
|
||||||
|
</window>
|
BIN
resources/skins/default/media/dialogs/dialog_back.png
Normal file
BIN
resources/skins/default/media/dialogs/dialog_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
resources/skins/default/media/dialogs/menu_back.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
resources/skins/default/media/dialogs/menu_bottom.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_bottom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
resources/skins/default/media/dialogs/menu_top.png
Normal file
BIN
resources/skins/default/media/dialogs/menu_top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
resources/skins/default/media/dialogs/white.jpg
Normal file
BIN
resources/skins/default/media/dialogs/white.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Loading…
Reference in a new issue