Switch to stream playback, part I
This commit is contained in:
parent
edb9d6e2b0
commit
9b4584e7df
7 changed files with 612 additions and 4 deletions
|
@ -217,7 +217,8 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
|
||||||
# Need to chain keys for navigation
|
# Need to chain keys for navigation
|
||||||
widgets.KEY = key
|
widgets.KEY = key
|
||||||
# Process all items to show
|
# Process all items to show
|
||||||
widgets.attach_kodi_ids(xml)
|
if synched:
|
||||||
|
widgets.attach_kodi_ids(xml)
|
||||||
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
|
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
|
||||||
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
|
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
|
||||||
all_items)
|
all_items)
|
||||||
|
|
255
resources/lib/playstrm.py
Normal file
255
resources/lib/playstrm.py
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
from .plex_api import API
|
||||||
|
from . import plex_function as PF, utils, json_rpc, variables as v, \
|
||||||
|
widgets
|
||||||
|
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.playstrm')
|
||||||
|
|
||||||
|
|
||||||
|
class PlayStrmException(Exception):
|
||||||
|
"""
|
||||||
|
Any Exception associated with playstrm
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PlayStrm(object):
|
||||||
|
'''
|
||||||
|
Workflow: Strm that calls our webservice in database. When played, the
|
||||||
|
webserivce returns a dummy file to play. Meanwhile, PlayStrm adds the real
|
||||||
|
listitems for items to play to the playlist.
|
||||||
|
'''
|
||||||
|
def __init__(self, params, server_id=None):
|
||||||
|
LOG.debug('Starting PlayStrm with server_id %s, params: %s',
|
||||||
|
server_id, params)
|
||||||
|
self.xml = None
|
||||||
|
self.api = None
|
||||||
|
self.start_index = None
|
||||||
|
self.index = None
|
||||||
|
self.server_id = server_id
|
||||||
|
self.plex_id = utils.cast(int, params['plex_id'])
|
||||||
|
self.plex_type = params.get('plex_type')
|
||||||
|
if params.get('synched') and params['synched'].lower() == 'false':
|
||||||
|
self.synched = False
|
||||||
|
else:
|
||||||
|
self.synched = True
|
||||||
|
self._get_xml()
|
||||||
|
self.name = self.api.title()
|
||||||
|
self.kodi_id = utils.cast(int, params.get('kodi_id'))
|
||||||
|
self.kodi_type = params.get('kodi_type')
|
||||||
|
if ((self.kodi_id is None or self.kodi_type is None) and
|
||||||
|
self.xml[0].get('pkc_db_item')):
|
||||||
|
self.kodi_id = self.xml[0].get('pkc_db_item')['kodi_id']
|
||||||
|
self.kodi_type = self.xml[0].get('pkc_db_item')['kodi_type']
|
||||||
|
self.transcode = params.get('transcode')
|
||||||
|
if self.transcode is None:
|
||||||
|
self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None
|
||||||
|
if utils.window('plex.playlist.audio.bool'):
|
||||||
|
LOG.info('Audio playlist detected')
|
||||||
|
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
|
||||||
|
else:
|
||||||
|
self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ("{{"
|
||||||
|
"'name': '{self.name}', "
|
||||||
|
"'plex_id': {self.plex_id}, "
|
||||||
|
"'plex_type': '{self.plex_type}', "
|
||||||
|
"'kodi_id': {self.kodi_id}, "
|
||||||
|
"'kodi_type': '{self.kodi_type}', "
|
||||||
|
"'server_id': '{self.server_id}', "
|
||||||
|
"'transcode': {self.transcode}, "
|
||||||
|
"'start_index': {self.start_index}, "
|
||||||
|
"'index': {self.index}"
|
||||||
|
"}}").format(self=self).encode('utf-8')
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
def add_to_playlist(self, kodi_id, kodi_type, index=None, playlistid=None):
|
||||||
|
playlistid = playlistid or self.kodi_playlist.getPlayListId()
|
||||||
|
LOG.debug('Adding kodi_id %s, kodi_type %s to playlist %s at index %s',
|
||||||
|
kodi_id, kodi_type, playlistid, index)
|
||||||
|
if index is None:
|
||||||
|
json_rpc.playlist_add(playlistid, {'%sid' % kodi_type: kodi_id})
|
||||||
|
else:
|
||||||
|
json_rpc.playlist_insert({'playlistid': playlistid,
|
||||||
|
'position': index,
|
||||||
|
'item': {'%sid' % kodi_type: kodi_id}})
|
||||||
|
|
||||||
|
def remove_from_playlist(self, index):
|
||||||
|
LOG.debug('Removing playlist item number %s from %s', index, self)
|
||||||
|
json_rpc.playlist_remove(self.kodi_playlist.getPlayListId(),
|
||||||
|
index)
|
||||||
|
|
||||||
|
def _get_xml(self):
|
||||||
|
self.xml = PF.GetPlexMetadata(self.plex_id)
|
||||||
|
if self.xml in (None, 401):
|
||||||
|
raise PlayStrmException('No xml received from the PMS')
|
||||||
|
if self.synched:
|
||||||
|
# Adds a new key 'pkc_db_item' to self.xml[0].attrib
|
||||||
|
widgets.attach_kodi_ids(self.xml)
|
||||||
|
else:
|
||||||
|
self.xml[0].set('pkc_db_item', None)
|
||||||
|
self.api = API(self.xml[0])
|
||||||
|
|
||||||
|
def start_playback(self, index=0):
|
||||||
|
LOG.debug('Starting playback at %s', index)
|
||||||
|
xbmc.Player().play(self.kodi_playlist, startpos=index, windowed=False)
|
||||||
|
|
||||||
|
def play(self, start_position=None, delayed=True):
|
||||||
|
'''
|
||||||
|
Create and add listitems to the Kodi playlist.
|
||||||
|
'''
|
||||||
|
if start_position is not None:
|
||||||
|
self.start_index = start_position
|
||||||
|
else:
|
||||||
|
self.start_index = max(self.kodi_playlist.getposition(), 0)
|
||||||
|
self.index = self.start_index
|
||||||
|
listitem = xbmcgui.ListItem()
|
||||||
|
self._set_playlist(listitem)
|
||||||
|
LOG.info('Initiating play for %s', self)
|
||||||
|
if not delayed:
|
||||||
|
self.start_playback(self.start_index)
|
||||||
|
return self.start_index
|
||||||
|
|
||||||
|
def play_folder(self, position=None):
|
||||||
|
'''
|
||||||
|
When an entire queue is requested, If requested from Kodi, kodi_type is
|
||||||
|
provided, add as Kodi would, otherwise queue playlist items using strm
|
||||||
|
links to setup playback later.
|
||||||
|
'''
|
||||||
|
self.start_index = position or max(self.kodi_playlist.size(), 0)
|
||||||
|
self.index = self.start_index + 1
|
||||||
|
LOG.info('Play folder plex_id %s, index: %s', self.plex_id, self.index)
|
||||||
|
if self.kodi_id and self.kodi_type:
|
||||||
|
self.add_to_playlist(self.kodi_id, self.kodi_type, self.index)
|
||||||
|
else:
|
||||||
|
listitem = widgets.get_listitem(self.xml[0])
|
||||||
|
url = 'http://127.0.0.1:%s/plex/play/file.strm' % v.WEBSERVICE_PORT
|
||||||
|
args = {
|
||||||
|
'mode': 'play',
|
||||||
|
'plex_id': self.plex_id,
|
||||||
|
'plex_type': self.api.plex_type()
|
||||||
|
}
|
||||||
|
if self.kodi_id:
|
||||||
|
args['kodi_id'] = self.kodi_id
|
||||||
|
if self.kodi_type:
|
||||||
|
args['kodi_type'] = self.kodi_type
|
||||||
|
if self.server_id:
|
||||||
|
args['server_id'] = self.server_id
|
||||||
|
if self.transcode:
|
||||||
|
args['transcode'] = True
|
||||||
|
url = '%s?%s' % (url, urllib.urlencode(args))
|
||||||
|
listitem.setPath(url)
|
||||||
|
self.kodi_playlist.add(url=url,
|
||||||
|
listitem=listitem,
|
||||||
|
index=self.index)
|
||||||
|
return self.index
|
||||||
|
|
||||||
|
def _set_playlist(self, listitem):
|
||||||
|
'''
|
||||||
|
Verify seektime, set intros, set main item and set additional parts.
|
||||||
|
Detect the seektime for video type content. Verify the default video
|
||||||
|
action set in Kodi for accurate resume behavior.
|
||||||
|
'''
|
||||||
|
seektime = self._resume()
|
||||||
|
if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and
|
||||||
|
utils.settings('enableCinema') == 'true'):
|
||||||
|
self._set_intros()
|
||||||
|
|
||||||
|
play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server'])
|
||||||
|
source = play.select_source(play.get_sources())
|
||||||
|
|
||||||
|
if not source:
|
||||||
|
raise PlayStrmException('Playback selection cancelled')
|
||||||
|
|
||||||
|
play.set_external_subs(source, listitem)
|
||||||
|
self.set_listitem(self.xml, listitem, self.kodi_id, seektime)
|
||||||
|
listitem.setPath(self.xml['PlaybackInfo']['Path'])
|
||||||
|
playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id)
|
||||||
|
|
||||||
|
self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
|
||||||
|
self.index += 1
|
||||||
|
|
||||||
|
if self.xml.get('PartCount'):
|
||||||
|
self._set_additional_parts()
|
||||||
|
|
||||||
|
def _resume(self):
|
||||||
|
'''
|
||||||
|
Resume item if available. Returns bool or raise an PlayStrmException if
|
||||||
|
resume was cancelled by user.
|
||||||
|
'''
|
||||||
|
seektime = utils.window('plex.resume')
|
||||||
|
utils.window('plex.resume', clear=True)
|
||||||
|
seektime = seektime == 'true' if seektime else None
|
||||||
|
auto_play = utils.window('plex.autoplay.bool')
|
||||||
|
if auto_play:
|
||||||
|
seektime = False
|
||||||
|
LOG.info('Skip resume for autoplay')
|
||||||
|
elif seektime is None:
|
||||||
|
resume = self.api.resume_point()
|
||||||
|
if resume:
|
||||||
|
seektime = resume_dialog(resume)
|
||||||
|
LOG.info('Resume: %s', seektime)
|
||||||
|
if seektime is None:
|
||||||
|
raise PlayStrmException('User backed out of resume dialog.')
|
||||||
|
# Todo: Probably need to have a look here
|
||||||
|
utils.window('plex.autoplay.bool', value='true')
|
||||||
|
return seektime
|
||||||
|
|
||||||
|
def _set_intros(self):
|
||||||
|
'''
|
||||||
|
if we have any play them when the movie/show is not being resumed.
|
||||||
|
'''
|
||||||
|
if self.info['Intros']['Items']:
|
||||||
|
enabled = True
|
||||||
|
|
||||||
|
if utils.settings('askCinema') == 'true':
|
||||||
|
|
||||||
|
resp = dialog('yesno', heading='{emby}', line1=_(33016))
|
||||||
|
if not resp:
|
||||||
|
|
||||||
|
enabled = False
|
||||||
|
LOG.info('Skip trailers.')
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
for intro in self.info['Intros']['Items']:
|
||||||
|
|
||||||
|
listitem = xbmcgui.ListItem()
|
||||||
|
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
|
||||||
|
|
||||||
|
utils.window('plex.skip.%s' % intro['plex_id'], value='true')
|
||||||
|
|
||||||
|
def _set_additional_parts(self):
|
||||||
|
''' Create listitems and add them to the stack of playlist.
|
||||||
|
'''
|
||||||
|
for part in self.info['AdditionalParts']['Items']:
|
||||||
|
|
||||||
|
listitem = xbmcgui.ListItem()
|
||||||
|
LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name'])
|
||||||
|
|
||||||
|
play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server'])
|
||||||
|
source = play.select_source(play.get_sources())
|
||||||
|
play.set_external_subs(source, listitem)
|
||||||
|
self.set_listitem(part, listitem)
|
||||||
|
listitem.setPath(part['PlaybackInfo']['Path'])
|
||||||
|
playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id)
|
||||||
|
|
||||||
|
self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index)
|
||||||
|
self.index += 1
|
|
@ -12,3 +12,18 @@ from .sections import Sections
|
||||||
|
|
||||||
class PlexDB(PlexDBBase, TVShows, Movies, Music, Playlists, Sections):
|
class PlexDB(PlexDBBase, TVShows, Movies, Music, Playlists, Sections):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_from_plex(plex_id, plex_type=None):
|
||||||
|
"""
|
||||||
|
Returns the tuple (kodi_id, kodi_type) for plex_id. Faster, if plex_type
|
||||||
|
is provided
|
||||||
|
|
||||||
|
Returns (None, None) if unsuccessful
|
||||||
|
"""
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||||
|
if db_item:
|
||||||
|
return (db_item['kodi_id'], db_item['kodi_type'])
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
|
@ -10,6 +10,7 @@ from . import initialsetup
|
||||||
from . import kodimonitor
|
from . import kodimonitor
|
||||||
from . import sync, library_sync
|
from . import sync, library_sync
|
||||||
from . import websocket_client
|
from . import websocket_client
|
||||||
|
from . import webservice
|
||||||
from . import plex_companion
|
from . import plex_companion
|
||||||
from . import plex_functions as PF, playqueue as PQ
|
from . import plex_functions as PF, playqueue as PQ
|
||||||
from . import playback_starter
|
from . import playback_starter
|
||||||
|
@ -433,6 +434,7 @@ class Service(object):
|
||||||
self.setup.setup()
|
self.setup.setup()
|
||||||
|
|
||||||
# Initialize important threads
|
# Initialize important threads
|
||||||
|
self.webservice = webservice.WebService()
|
||||||
self.ws = websocket_client.PMS_Websocket()
|
self.ws = websocket_client.PMS_Websocket()
|
||||||
self.alexa = websocket_client.Alexa_Websocket()
|
self.alexa = websocket_client.Alexa_Websocket()
|
||||||
self.sync = sync.Sync()
|
self.sync = sync.Sync()
|
||||||
|
@ -494,6 +496,12 @@ class Service(object):
|
||||||
xbmc.sleep(100)
|
xbmc.sleep(100)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if self.webservice is not None and not self.webservice.is_alive():
|
||||||
|
LOG.info('Restarting webservice')
|
||||||
|
self.webservice.abort()
|
||||||
|
self.webservice = webservice.WebService()
|
||||||
|
self.webservice.start()
|
||||||
|
|
||||||
# Before proceeding, need to make sure:
|
# Before proceeding, need to make sure:
|
||||||
# 1. Server is online
|
# 1. Server is online
|
||||||
# 2. User is set
|
# 2. User is set
|
||||||
|
@ -523,6 +531,7 @@ class Service(object):
|
||||||
continue
|
continue
|
||||||
elif not self.startup_completed:
|
elif not self.startup_completed:
|
||||||
self.startup_completed = True
|
self.startup_completed = True
|
||||||
|
self.webservice.start()
|
||||||
self.ws.start()
|
self.ws.start()
|
||||||
self.sync.start()
|
self.sync.start()
|
||||||
self.plexcompanion.start()
|
self.plexcompanion.start()
|
||||||
|
|
|
@ -92,6 +92,9 @@ DEVICENAME = DEVICENAME.replace(' ', "")
|
||||||
|
|
||||||
COMPANION_PORT = int(_ADDON.getSetting('companionPort'))
|
COMPANION_PORT = int(_ADDON.getSetting('companionPort'))
|
||||||
|
|
||||||
|
# Port for the PKC webservice
|
||||||
|
WEBSERVICE_PORT = 57578
|
||||||
|
|
||||||
# Unique ID for this Plex client; also see clientinfo.py
|
# Unique ID for this Plex client; also see clientinfo.py
|
||||||
PKC_MACHINE_IDENTIFIER = None
|
PKC_MACHINE_IDENTIFIER = None
|
||||||
|
|
||||||
|
|
319
resources/lib/webservice.py
Normal file
319
resources/lib/webservice.py
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
PKC-dedicated webserver. Listens to Kodi starting playback; will then hand-over
|
||||||
|
playback to plugin://video.plexkodiconnect
|
||||||
|
'''
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
import BaseHTTPServer
|
||||||
|
import httplib
|
||||||
|
import urlparse
|
||||||
|
import socket
|
||||||
|
import Queue
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
from . import backgroundthread, utils, variables as v, app
|
||||||
|
from .playstrm import PlayStrm
|
||||||
|
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.webservice')
|
||||||
|
|
||||||
|
|
||||||
|
class WebService(backgroundthread.KillableThread):
|
||||||
|
|
||||||
|
''' Run a webservice to trigger playback.
|
||||||
|
'''
|
||||||
|
def is_alive(self):
|
||||||
|
''' Called to see if the webservice is still responding.
|
||||||
|
'''
|
||||||
|
alive = True
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.connect(('127.0.0.1', v.WEBSERVICE_PORT))
|
||||||
|
s.sendall('')
|
||||||
|
except Exception as error:
|
||||||
|
LOG.error(error)
|
||||||
|
if 'Errno 61' in str(error):
|
||||||
|
alive = False
|
||||||
|
s.close()
|
||||||
|
return alive
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
''' Called when the thread needs to stop
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
conn = httplib.HTTPConnection('127.0.0.1:%d' % v.WEBSERVICE_PORT)
|
||||||
|
conn.request('QUIT', '/')
|
||||||
|
conn.getresponse()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
''' Called to start the webservice.
|
||||||
|
'''
|
||||||
|
LOG.info('----===## Starting Webserver on port %s ##===----',
|
||||||
|
v.WEBSERVICE_PORT)
|
||||||
|
app.APP.register_thread(self)
|
||||||
|
try:
|
||||||
|
server = HttpServer(('127.0.0.1', v.WEBSERVICE_PORT),
|
||||||
|
RequestHandler)
|
||||||
|
server.serve_forever()
|
||||||
|
except Exception as error:
|
||||||
|
if '10053' not in error: # ignore host diconnected errors
|
||||||
|
utils.ERROR()
|
||||||
|
finally:
|
||||||
|
app.APP.deregister_thread(self)
|
||||||
|
LOG.info('##===---- Webserver stopped ----===##')
|
||||||
|
|
||||||
|
|
||||||
|
class HttpServer(BaseHTTPServer.HTTPServer):
|
||||||
|
''' Http server that reacts to self.stop flag.
|
||||||
|
'''
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.stop = False
|
||||||
|
self.pending = []
|
||||||
|
self.threads = []
|
||||||
|
self.queue = Queue.Queue()
|
||||||
|
super(HttpServer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def serve_forever(self):
|
||||||
|
|
||||||
|
''' Handle one request at a time until stopped.
|
||||||
|
'''
|
||||||
|
while not self.stop:
|
||||||
|
self.handle_request()
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
''' Http request handler. Do not use LOG here,
|
||||||
|
it will hang requests in Kodi > show information dialog.
|
||||||
|
'''
|
||||||
|
timeout = 0.5
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
''' Mute the webservice requests.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
''' To quiet socket errors with 404.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_QUIT(self):
|
||||||
|
''' send 200 OK response, and set server.stop to True
|
||||||
|
'''
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.server.stop = True
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
''' Get the params as a dict
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
path = self.path[1:].decode('utf-8')
|
||||||
|
except IndexError:
|
||||||
|
params = {}
|
||||||
|
if '?' in path:
|
||||||
|
path = path.split('?', 1)[1]
|
||||||
|
params = dict(urlparse.parse_qsl(path))
|
||||||
|
|
||||||
|
if params.get('transcode'):
|
||||||
|
params['transcode'] = params['transcode'].lower() == 'true'
|
||||||
|
if params.get('server') and params['server'].lower() == 'none':
|
||||||
|
params['server'] = None
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
''' Called on HEAD requests
|
||||||
|
'''
|
||||||
|
self.handle_request(True)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
''' Called on GET requests
|
||||||
|
'''
|
||||||
|
self.handle_request()
|
||||||
|
|
||||||
|
def handle_request(self, headers_only=False):
|
||||||
|
'''Send headers and reponse
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if b'extrafanart' in self.path or b'extrathumbs' in self.path:
|
||||||
|
raise Exception('unsupported artwork request')
|
||||||
|
|
||||||
|
if headers_only:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header(b'Content-type', b'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
elif b'file.strm' not in self.path:
|
||||||
|
self.images()
|
||||||
|
elif b'file.strm' in self.path:
|
||||||
|
self.strm()
|
||||||
|
else:
|
||||||
|
xbmc.log(str(self.path), xbmc.LOGWARNING)
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
self.send_error(500,
|
||||||
|
b'PLEX.webservice: Exception occurred: %s' % error)
|
||||||
|
xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING)
|
||||||
|
|
||||||
|
def strm(self):
|
||||||
|
''' Return a dummy video and and queue real items.
|
||||||
|
'''
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header(b'Content-type', b'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
params = self.get_params()
|
||||||
|
|
||||||
|
if b'kodi/movies' in self.path:
|
||||||
|
params['kodi_type'] = v.KODI_TYPE_MOVIE
|
||||||
|
elif b'kodi/tvshows' in self.path:
|
||||||
|
params['kodi_type'] = v.KODI_TYPE_EPISODE
|
||||||
|
# elif 'kodi/musicvideos' in self.path:
|
||||||
|
# params['MediaType'] = 'musicvideo'
|
||||||
|
|
||||||
|
if utils.settings('pluginSingle.bool'):
|
||||||
|
path = 'plugin://plugin.video.plexkodiconnect?mode=playsingle&plex_id=%s' % params['plex_id']
|
||||||
|
if params.get('server'):
|
||||||
|
path += '&server=%s' % params['server']
|
||||||
|
if params.get('transcode'):
|
||||||
|
path += '&transcode=true'
|
||||||
|
if params.get('kodi_id'):
|
||||||
|
path += '&kodi_id=%s' % params['kodi_id']
|
||||||
|
if params.get('Name'):
|
||||||
|
path += '&filename=%s' % params['Name']
|
||||||
|
self.wfile.write(bytes(path))
|
||||||
|
return
|
||||||
|
|
||||||
|
path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id']
|
||||||
|
self.wfile.write(bytes(path))
|
||||||
|
if params['plex_id'] not in self.server.pending:
|
||||||
|
xbmc.log('PLEX.webserver: %s: path: %s params: %s'
|
||||||
|
% (str(id(self)), str(self.path), str(params)),
|
||||||
|
xbmc.LOGWARNING)
|
||||||
|
|
||||||
|
self.server.pending.append(params['plex_id'])
|
||||||
|
self.server.queue.put(params)
|
||||||
|
if not len(self.server.threads):
|
||||||
|
queue = QueuePlay(self.server)
|
||||||
|
queue.start()
|
||||||
|
self.server.threads.append(queue)
|
||||||
|
|
||||||
|
def images(self):
|
||||||
|
''' Return a dummy image for unwanted images requests over the webservice.
|
||||||
|
Required to prevent freezing of widget playback if the file url has no
|
||||||
|
local textures cached yet.
|
||||||
|
'''
|
||||||
|
image = xbmc.translatePath(
|
||||||
|
'special://home/addons/plugin.video.plexkodiconnect/icon.png').decode('utf-8')
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header(b'Content-type', b'image/png')
|
||||||
|
modified = xbmcvfs.Stat(image).st_mtime()
|
||||||
|
self.send_header(b'Last-Modified', b'%s' % modified)
|
||||||
|
image = xbmcvfs.File(image)
|
||||||
|
size = image.size()
|
||||||
|
self.send_header(b'Content-Length', str(size))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(image.readBytes())
|
||||||
|
image.close()
|
||||||
|
|
||||||
|
|
||||||
|
class QueuePlay(backgroundthread.KillableThread):
|
||||||
|
''' Workflow for new playback:
|
||||||
|
|
||||||
|
Queue up strm playback that was called in the webservice. Called
|
||||||
|
playstrm in default.py which will wait for our signal here. Downloads
|
||||||
|
plex information. Add content to the playlist after the strm file that
|
||||||
|
initiated playback from db. Start playback by telling playstrm waiting.
|
||||||
|
It will fail playback of the current strm and move to the next entry for
|
||||||
|
us. If play folder, playback starts here.
|
||||||
|
|
||||||
|
Required delay for widgets, custom skin containers and non library
|
||||||
|
windows. Otherwise Kodi will freeze if no artwork textures are cached
|
||||||
|
yet in Textures13.db Will be skipped if the player already has media and
|
||||||
|
is playing.
|
||||||
|
|
||||||
|
Why do all this instead of using plugin? Strms behaves better than
|
||||||
|
plugin in database. Allows to load chapter images with direct play.
|
||||||
|
Allows to have proper artwork for intros. Faster than resolving using
|
||||||
|
plugin, especially on low powered devices. Cons: Can't use external
|
||||||
|
players with this method.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, server):
|
||||||
|
self.server = server
|
||||||
|
super(QueuePlay, self).__init__()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
LOG.info('##===---- Starting QueuePlay ----===##')
|
||||||
|
play_folder = False
|
||||||
|
play = None
|
||||||
|
start_position = None
|
||||||
|
position = None
|
||||||
|
|
||||||
|
# Let Kodi catch up
|
||||||
|
xbmc.sleep(200)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
params = self.server.queue.get(timeout=0.01)
|
||||||
|
except Queue.Empty:
|
||||||
|
count = 20
|
||||||
|
while not utils.window('plex.playlist.ready.bool'):
|
||||||
|
xbmc.sleep(50)
|
||||||
|
if not count:
|
||||||
|
LOG.info('Playback aborted')
|
||||||
|
raise Exception('PlaybackAborted')
|
||||||
|
count -= 1
|
||||||
|
LOG.info('Starting playback at position: %s', start_position)
|
||||||
|
if play_folder:
|
||||||
|
LOG.info('Start playing folder')
|
||||||
|
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||||
|
play.start_playback()
|
||||||
|
else:
|
||||||
|
utils.window('plex.playlist.play.bool', True)
|
||||||
|
xbmc.sleep(1000)
|
||||||
|
play.remove_from_playlist(start_position)
|
||||||
|
break
|
||||||
|
play = PlayStrm(params, params.get('ServerId'))
|
||||||
|
|
||||||
|
if start_position is None:
|
||||||
|
start_position = max(play.info['KodiPlaylist'].getposition(), 0)
|
||||||
|
position = start_position + 1
|
||||||
|
if play_folder:
|
||||||
|
position = play.play_folder(position)
|
||||||
|
else:
|
||||||
|
if self.server.pending.count(params['plex_id']) != len(self.server.pending):
|
||||||
|
play_folder = True
|
||||||
|
utils.window('plex.playlist.start', str(start_position))
|
||||||
|
position = play.play(position)
|
||||||
|
if play_folder:
|
||||||
|
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
|
||||||
|
except Exception:
|
||||||
|
utils.ERROR()
|
||||||
|
play.info['KodiPlaylist'].clear()
|
||||||
|
xbmc.Player().stop()
|
||||||
|
self.server.queue.queue.clear()
|
||||||
|
if play_folder:
|
||||||
|
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
|
||||||
|
else:
|
||||||
|
utils.window('plex.playlist.aborted.bool', True)
|
||||||
|
break
|
||||||
|
self.server.queue.task_done()
|
||||||
|
|
||||||
|
utils.window('plex.playlist.ready', clear=True)
|
||||||
|
utils.window('plex.playlist.start', clear=True)
|
||||||
|
utils.window('plex.playlist.audio', clear=True)
|
||||||
|
self.server.threads.remove(self)
|
||||||
|
self.server.pending = []
|
||||||
|
LOG.info('##===---- QueuePlay Stopped ----===##')
|
|
@ -29,11 +29,19 @@ PLEX_TYPE = None
|
||||||
SECTION_ID = None
|
SECTION_ID = None
|
||||||
APPEND_SHOW_TITLE = None
|
APPEND_SHOW_TITLE = None
|
||||||
APPEND_SXXEXX = None
|
APPEND_SXXEXX = None
|
||||||
SYNCHED = True
|
|
||||||
# Need to chain the PMS keys
|
# Need to chain the PMS keys
|
||||||
KEY = None
|
KEY = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_listitem(xml_element):
|
||||||
|
"""
|
||||||
|
Returns a valid xbmcgui.ListItem() for xml_element
|
||||||
|
"""
|
||||||
|
item = generate_item(xml_element)
|
||||||
|
prepare_listitem(item)
|
||||||
|
return create_listitem(item)
|
||||||
|
|
||||||
|
|
||||||
def process_method_on_list(method_to_run, items):
|
def process_method_on_list(method_to_run, items):
|
||||||
"""
|
"""
|
||||||
helper method that processes a method on each listitem with pooling if the
|
helper method that processes a method on each listitem with pooling if the
|
||||||
|
@ -246,8 +254,6 @@ def attach_kodi_ids(xml):
|
||||||
"""
|
"""
|
||||||
Attaches the kodi db_item to the xml's children, attribute 'pkc_db_item'
|
Attaches the kodi db_item to the xml's children, attribute 'pkc_db_item'
|
||||||
"""
|
"""
|
||||||
if not SYNCHED:
|
|
||||||
return
|
|
||||||
with PlexDB(lock=False) as plexdb:
|
with PlexDB(lock=False) as plexdb:
|
||||||
for child in xml:
|
for child in xml:
|
||||||
api = API(child)
|
api = API(child)
|
||||||
|
|
Loading…
Reference in a new issue