PlexKodiConnect/resources/lib/webservice.py

465 lines
18 KiB
Python
Raw Normal View History

2019-03-26 03:15:18 +11:00
# -*- 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 socket
import Queue
import xbmc
import xbmcvfs
2019-05-12 22:38:31 +10:00
from .plex_api import API
2019-05-04 21:14:34 +10:00
from .plex_db import PlexDB
2019-04-29 02:03:20 +10:00
from . import backgroundthread, utils, variables as v, app, playqueue as PQ
2019-05-12 22:38:31 +10:00
from . import playlist_func as PL, json_rpc as js, plex_functions as PF
2019-03-26 03:15:18 +11:00
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:
2019-04-06 19:26:01 +11:00
LOG.error('is_alive error: %s', error)
2019-03-26 03:15:18 +11:00
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()
2019-04-06 21:31:37 +11:00
except Exception as error:
2019-05-12 22:38:31 +10:00
xbmc.log('PLEX.webservice abort error: %s' % error, xbmc.LOGWARNING)
2019-03-26 03:15:18 +11:00
2019-04-06 21:09:53 +11:00
def suspend(self):
"""
Called when thread needs to suspend - let's not do anything and keep
webservice up
"""
self.suspend_reached = True
def resume(self):
"""
Called when thread needs to resume - let's not do anything and keep
webservice up
"""
self.suspend_reached = False
2019-03-26 03:15:18 +11:00
def run(self):
''' Called to start the webservice.
'''
2019-04-06 19:26:01 +11:00
LOG.info('----===## Starting WebService on port %s ##===----',
2019-03-26 03:15:18 +11:00
v.WEBSERVICE_PORT)
app.APP.register_thread(self)
try:
server = HttpServer(('127.0.0.1', v.WEBSERVICE_PORT),
RequestHandler)
2019-04-18 00:32:11 +10:00
LOG.info('Serving http on %s', server.socket.getsockname())
2019-03-26 03:15:18 +11:00
server.serve_forever()
except Exception as error:
2019-04-06 19:26:01 +11:00
LOG.error('Error encountered: %s', error)
2019-03-26 03:15:18 +11:00
if '10053' not in error: # ignore host diconnected errors
utils.ERROR()
finally:
app.APP.deregister_thread(self)
2019-04-06 19:26:01 +11:00
LOG.info('##===---- WebService stopped ----===##')
2019-03-26 03:15:18 +11:00
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()
2019-04-06 19:26:01 +11:00
BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
2019-03-26 03:15:18 +11:00
def serve_forever(self):
''' Handle one request at a time until stopped.
'''
while not self.stop:
self.handle_request()
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2019-04-06 19:26:01 +11:00
'''
Http request handler. Do not use LOG here, it will hang requests in Kodi >
show information dialog.
2019-03-26 03:15:18 +11:00
'''
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)
2019-04-06 19:26:01 +11:00
except Exception as error:
if '10054' in error:
# Silence "[Errno 10054] An existing connection was forcibly
# closed by the remote host"
return
2019-05-12 22:38:31 +10:00
xbmc.log('PLEX.webservice handle error: %s' % error, xbmc.LOGWARNING)
2019-03-26 03:15:18 +11:00
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:
2019-05-04 21:14:34 +10:00
path = ''
2019-03-26 03:15:18 +11:00
params = {}
if '?' in path:
path = path.split('?', 1)[1]
2019-03-31 04:05:34 +11:00
params = dict(utils.parse_qsl(path))
2019-05-12 22:38:31 +10:00
if 'plex_id' not in params:
LOG.error('No plex_id received for path %s', path)
return
2019-03-26 03:15:18 +11:00
2019-05-12 22:38:31 +10:00
if 'plex_type' in params and params['plex_type'].lower() == 'none':
del params['plex_type']
2019-05-04 21:14:34 +10:00
if 'plex_type' not in params:
LOG.debug('Need to look-up plex_type')
with PlexDB(lock=False) as plexdb:
db_item = plexdb.item_by_id(params['plex_id'])
if db_item:
params['plex_type'] = db_item['plex_type']
else:
LOG.debug('No plex_type found, using Kodi player id')
players = js.get_players()
2019-05-12 22:38:31 +10:00
if players:
params['plex_type'] = v.PLEX_TYPE_CLIP if 'video' in players \
else v.PLEX_TYPE_SONG
LOG.debug('Using the following plex_type: %s',
params['plex_type'])
else:
xml = PF.GetPlexMetadata(params['plex_id'])
if xml in (None, 401):
LOG.error('Could not get metadata for %s', params)
return
api = API(xml[0])
params['plex_type'] = api.plex_type()
LOG.debug('Got metadata, using plex_type %s',
params['plex_type'])
2019-03-26 03:15:18 +11:00
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
'''
2019-05-12 22:38:31 +10:00
xbmc.log('PLEX.webservice handle_request called. headers %s, path: %s'
2019-04-13 22:22:38 +10:00
% (headers_only, self.path), xbmc.LOGDEBUG)
2019-03-26 03:15:18 +11:00
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()
else:
2019-04-13 22:18:13 +10:00
self.strm()
2019-03-26 03:15:18 +11:00
except Exception as error:
self.send_error(500,
b'PLEX.webservice: Exception occurred: %s' % error)
def strm(self):
''' Return a dummy video and and queue real items.
'''
2019-04-13 22:22:38 +10:00
xbmc.log('PLEX.webservice: starting strm', xbmc.LOGDEBUG)
2019-03-26 03:15:18 +11:00
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('kodi_type'):
path += '&kodi_type=%s' % params['kodi_type']
2019-03-26 03:15:18 +11:00
self.wfile.write(bytes(path))
return
path = 'plugin://plugin.video.plexkodiconnect?mode=playstrm&plex_id=%s' % params['plex_id']
2019-04-06 21:31:37 +11:00
self.wfile.write(bytes(path.encode('utf-8')))
2019-03-26 03:15:18 +11:00
if params['plex_id'] not in self.server.pending:
self.server.pending.append(params['plex_id'])
self.server.queue.put(params)
if not len(self.server.threads):
2019-05-04 21:14:34 +10:00
queue = QueuePlay(self.server, params['plex_type'])
2019-03-26 03:15:18 +11:00
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.
'''
2019-05-04 21:14:34 +10:00
def __init__(self, server, plex_type):
2019-03-26 03:15:18 +11:00
self.server = server
2019-05-04 21:14:34 +10:00
self.plex_type = plex_type
2019-04-29 02:03:20 +10:00
self.plex_id = None
self.kodi_id = None
self.kodi_type = None
2019-05-05 18:42:44 +10:00
self.synched = True
self.force_transcode = False
2019-03-26 03:15:18 +11:00
super(QueuePlay, self).__init__()
2019-05-04 21:14:34 +10:00
def __unicode__(self):
return ("{{"
"'plex_id': {self.plex_id}, "
"'plex_type': '{self.plex_type}', "
"'kodi_id': {self.kodi_id}, "
"'kodi_type': '{self.kodi_type}', "
"'synched: '{self.synched}', "
"'force_transcode: '{self.force_transcode}', "
"}}").format(self=self)
def __str__(self):
return unicode(self).encode('utf-8')
__repr__ = __str__
2019-04-29 02:03:20 +10:00
def load_params(self, params):
self.plex_id = utils.cast(int, params['plex_id'])
self.plex_type = params.get('plex_type')
self.kodi_id = utils.cast(int, params.get('kodi_id'))
self.kodi_type = params.get('kodi_type')
2019-05-04 21:14:34 +10:00
# Some cleanup
if params.get('transcode'):
self.force_transcode = params['transcode'].lower() == 'true'
if params.get('server') and params['server'].lower() == 'none':
self.server = None
2019-05-05 18:42:44 +10:00
if params.get('synched'):
self.synched = not params['synched'].lower() == 'false'
2019-04-29 02:03:20 +10:00
2019-05-05 18:42:44 +10:00
def _get_playqueue(self):
2019-05-12 22:38:31 +10:00
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
if ((self.plex_type in v.PLEX_VIDEOTYPES and
not app.PLAYSTATE.initiated_by_plex and
xbmc.getCondVisibility('Window.IsVisible(Home.xml)'))):
2019-05-04 21:14:34 +10:00
# Video launched from a widget - which starts a Kodi AUDIO playlist
# We will empty everything and start with a fresh VIDEO playlist
2019-05-12 22:38:31 +10:00
LOG.debug('Widget video playback detected')
2019-05-04 21:14:34 +10:00
video_widget_playback = True
2019-05-05 18:29:04 +10:00
# Release default.py
utils.window('plex.playlist.ready', value='true')
2019-05-04 21:14:34 +10:00
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
playqueue.clear()
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
playqueue.clear()
2019-05-05 18:29:04 +10:00
# Wait for Kodi to catch up - xbmcplugin.setResolvedUrl() needs to
# have run its course and thus the original item needs to have
# failed before we start playback anew
xbmc.sleep(200)
2019-05-04 21:14:34 +10:00
else:
video_widget_playback = False
if self.plex_type in v.PLEX_VIDEOTYPES:
LOG.debug('Video playback detected')
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO)
else:
LOG.debug('Audio playback detected')
playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO)
2019-05-05 18:42:44 +10:00
return playqueue, video_widget_playback
2019-05-04 21:14:34 +10:00
2019-05-05 18:42:44 +10:00
def run(self):
LOG.debug('##===---- Starting QueuePlay ----===##')
abort = False
play_folder = False
playqueue, video_widget_playback = self._get_playqueue()
2019-04-29 02:03:20 +10:00
# Position to start playback from (!!)
# Do NOT use kodi_pl.getposition() as that appears to be buggy
2019-05-04 21:14:34 +10:00
try:
start_position = max(js.get_position(playqueue.playlistid), 0)
except KeyError:
# Widgets: Since we've emptied the entire playlist, we won't get a
# position
start_position = 0
2019-04-29 02:03:20 +10:00
# Position to add next element to queue - we're doing this at the end
2019-05-04 21:14:34 +10:00
# of our current playqueue
2019-04-29 02:03:20 +10:00
position = playqueue.kodi_pl.size()
2019-05-12 22:38:31 +10:00
# Set to start_position + 1 because first item will fail
utils.window('plex.playlist.start', str(start_position + 1))
2019-05-04 21:14:34 +10:00
LOG.debug('start_position %s, position %s for current playqueue: %s',
2019-04-29 02:03:20 +10:00
start_position, position, playqueue)
2019-03-26 03:15:18 +11:00
while True:
try:
try:
2019-05-05 19:14:27 +10:00
# We cannot know when Kodi will send the last item, e.g.
# when playing an entire folder
params = self.server.queue.get(timeout=0.01)
2019-03-26 03:15:18 +11:00
except Queue.Empty:
2019-05-04 21:14:34 +10:00
LOG.debug('Wrapping up')
if xbmc.getCondVisibility('VideoPlayer.Content(livetv)'):
# avoid issues with ongoing Live TV playback
2019-05-12 22:38:31 +10:00
app.APP.player.stop()
2019-04-29 02:03:20 +10:00
count = 50
2019-04-06 19:26:01 +11:00
while not utils.window('plex.playlist.ready'):
2019-03-26 03:15:18 +11:00
xbmc.sleep(50)
if not count:
LOG.info('Playback aborted')
2019-04-29 02:03:20 +10:00
raise Exception('Playback aborted')
2019-03-26 03:15:18 +11:00
count -= 1
if play_folder:
LOG.info('Start playing folder')
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
2019-04-29 02:03:20 +10:00
playqueue.start_playback(start_position)
2019-05-04 21:14:34 +10:00
elif video_widget_playback:
LOG.info('Start widget video playback')
utils.window('plex.playlist.play', value='true')
LOG.info('Current PKC queue: %s', playqueue)
LOG.info('current Kodi queue: %s', js.playlist_get_items(playqueue.playlistid))
playqueue.start_playback()
2019-03-26 03:15:18 +11:00
else:
2019-05-04 21:14:34 +10:00
LOG.info('Start normal playback')
# Release default.py
2019-04-06 19:26:01 +11:00
utils.window('plex.playlist.play', value='true')
2019-05-12 22:38:31 +10:00
# Remove the playlist element we just added with the
# right path
xbmc.sleep(1000)
playqueue.kodi_remove_item(start_position)
del playqueue.items[start_position]
2019-05-04 21:14:34 +10:00
LOG.debug('Done wrapping up')
2019-03-26 03:15:18 +11:00
break
2019-04-29 02:03:20 +10:00
self.load_params(params)
2019-03-26 03:15:18 +11:00
if play_folder:
2019-05-12 22:38:31 +10:00
playlistitem = PL.PlaylistItem(plex_id=self.plex_id,
plex_type=self.plex_type,
kodi_id=self.kodi_id,
kodi_type=self.kodi_type)
playlistitem.force_transcode = self.force_transcode
playqueue.add_item(playlistitem, position)
2019-04-29 02:03:20 +10:00
position += 1
2019-03-26 03:15:18 +11:00
else:
if self.server.pending.count(params['plex_id']) != len(self.server.pending):
2019-05-12 22:38:31 +10:00
# E.g. when selecting "play" for an entire video genre
2019-04-29 02:03:20 +10:00
LOG.debug('Folder playback detected')
2019-03-26 03:15:18 +11:00
play_folder = True
2019-05-12 22:38:31 +10:00
xbmc.executebuiltin('Activateutils.window(busydialognocancel)')
playqueue.play(self.plex_id,
2019-04-29 02:03:20 +10:00
plex_type=self.plex_type,
2019-05-12 22:38:31 +10:00
startpos=start_position,
2019-04-29 02:03:20 +10:00
position=position,
synched=self.synched,
force_transcode=self.force_transcode)
# Do NOT start playback here - because Kodi already started
# it!
position = playqueue.index
2019-03-26 03:15:18 +11:00
except Exception:
2019-04-29 02:03:20 +10:00
abort = True
2019-05-12 22:38:31 +10:00
utils.ERROR(notify=True)
2019-04-29 02:03:20 +10:00
try:
self.server.queue.task_done()
except ValueError:
2019-05-12 22:38:31 +10:00
# "task_done() called too many times" when aborting
2019-04-29 02:03:20 +10:00
pass
if abort:
2019-05-12 22:38:31 +10:00
app.APP.player.stop()
2019-05-05 19:14:27 +10:00
playqueue.clear()
2019-03-26 03:15:18 +11:00
self.server.queue.queue.clear()
if play_folder:
xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
else:
2019-04-06 19:26:01 +11:00
utils.window('plex.playlist.aborted', value='true')
2019-03-26 03:15:18 +11:00
break
utils.window('plex.playlist.ready', clear=True)
utils.window('plex.playlist.start', clear=True)
2019-05-12 22:38:31 +10:00
app.PLAYSTATE.initiated_by_plex = False
2019-03-26 03:15:18 +11:00
self.server.threads.remove(self)
self.server.pending = []
2019-04-29 02:03:20 +10:00
LOG.debug('##===---- QueuePlay Stopped ----===##')