Major Plex Companion overhaul, part 6

This commit is contained in:
tomkat83 2018-01-01 13:28:39 +01:00
parent cf15799df2
commit 5337ae5715
8 changed files with 302 additions and 271 deletions

View file

@ -34,13 +34,11 @@ class PlexCompanion(Thread):
""" """
def __init__(self, callback=None): def __init__(self, callback=None):
LOG.info("----===## Starting PlexCompanion ##===----") LOG.info("----===## Starting PlexCompanion ##===----")
if callback is not None: self.mgr = callback
self.mgr = callback
# Start GDM for server/client discovery # Start GDM for server/client discovery
self.client = plexgdm.plexgdm() self.client = plexgdm.plexgdm()
self.client.clientDetails() self.client.clientDetails()
LOG.debug("Registration string is:\n%s", LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
self.client.getClientDetails())
# kodi player instance # kodi player instance
self.player = player.PKC_Player() self.player = player.PKC_Player()
self.httpd = False self.httpd = False
@ -54,14 +52,13 @@ class PlexCompanion(Thread):
try: try:
xml[0].attrib xml[0].attrib
except (AttributeError, IndexError, TypeError): except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata') LOG.error('Could not download Plex metadata for: %s', data)
return return
api = API(xml[0]) api = API(xml[0])
if api.getType() == v.PLEX_TYPE_ALBUM: if api.getType() == v.PLEX_TYPE_ALBUM:
LOG.debug('Plex music album detected') LOG.debug('Plex music album detected')
queue = self.mgr.playqueue.init_playqueue_from_plex_children( self.mgr.playqueue.init_playqueue_from_plex_children(
api.getRatingKey()) api.getRatingKey(), transient_token=data.get('token'))
queue.plex_transient_token = data.get('token')
else: else:
state.PLEX_TRANSIENT_TOKEN = data.get('token') state.PLEX_TRANSIENT_TOKEN = data.get('token')
params = { params = {
@ -92,13 +89,7 @@ class PlexCompanion(Thread):
@LOCKER.lockthis @LOCKER.lockthis
def _process_playlist(self, data): def _process_playlist(self, data):
# Get the playqueue ID # Get the playqueue ID
try: _, container_key, query = ParseContainerKey(data['containerKey'])
_, container_key, query = ParseContainerKey(data['containerKey'])
except:
LOG.error('Exception while processing')
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
return
try: try:
playqueue = self.mgr.playqueue.get_playqueue_from_type( playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
@ -114,16 +105,12 @@ class PlexCompanion(Thread):
api = API(xml[0]) api = API(xml[0])
playqueue = self.mgr.playqueue.get_playqueue_from_type( playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
if playqueue.id == container_key: self.mgr.playqueue.update_playqueue_from_PMS(
# OK, really weird, this happens at least with Plex for Android playqueue,
LOG.debug('Already know this Plex playQueue, ignoring this command') playqueue_id=container_key,
else: repeat=query.get('repeat'),
self.mgr.playqueue.update_playqueue_from_PMS( offset=data.get('offset'),
playqueue, transient_token=data.get('token'))
playqueue_id=container_key,
repeat=query.get('repeat'),
offset=data.get('offset'),
transient_token=data.get('token'))
@LOCKER.lockthis @LOCKER.lockthis
def _process_streams(self, data): def _process_streams(self, data):
@ -309,5 +296,5 @@ class PlexCompanion(Thread):
# Don't sleep # Don't sleep
continue continue
sleep(50) sleep(50)
self.subscription_manager.signal_stop() subscription_manager.signal_stop()
client.stop_all() client.stop_all()

View file

@ -9,6 +9,7 @@ from variables import ALEXA_TO_COMPANION
from playqueue import Playqueue from playqueue import Playqueue
from PlexFunctions import GetPlexKeyNumber from PlexFunctions import GetPlexKeyNumber
import json_rpc as js import json_rpc as js
import state
############################################################################### ###############################################################################
@ -64,6 +65,7 @@ def process_command(request_path, params, queue=None):
convert_alexa_to_companion(params) convert_alexa_to_companion(params)
LOG.debug('Received request_path: %s, params: %s', request_path, params) LOG.debug('Received request_path: %s, params: %s', request_path, params)
if request_path == 'player/playback/playMedia': if request_path == 'player/playback/playMedia':
state.PLAYBACK_INIT_DONE = False
# We need to tell service.py # We need to tell service.py
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
queue.put({ queue.put({
@ -71,6 +73,7 @@ def process_command(request_path, params, queue=None):
'data': params 'data': params
}) })
elif request_path == 'player/playback/refreshPlayQueue': elif request_path == 'player/playback/refreshPlayQueue':
state.PLAYBACK_INIT_DONE = False
queue.put({ queue.put({
'action': 'refreshPlayQueue', 'action': 'refreshPlayQueue',
'data': params 'data': params
@ -93,10 +96,13 @@ def process_command(request_path, params, queue=None):
elif request_path == "player/playback/stepBack": elif request_path == "player/playback/stepBack":
js.smallbackward() js.smallbackward()
elif request_path == "player/playback/skipNext": elif request_path == "player/playback/skipNext":
state.PLAYBACK_INIT_DONE = False
js.skipnext() js.skipnext()
elif request_path == "player/playback/skipPrevious": elif request_path == "player/playback/skipPrevious":
state.PLAYBACK_INIT_DONE = False
js.skipprevious() js.skipprevious()
elif request_path == "player/playback/skipTo": elif request_path == "player/playback/skipTo":
state.PLAYBACK_INIT_DONE = False
skip_to(params) skip_to(params)
elif request_path == "player/navigation/moveUp": elif request_path == "player/navigation/moveUp":
js.input_up() js.input_up()

View file

@ -125,7 +125,9 @@ class KodiMonitor(Monitor):
LOG.debug("Method: %s Data: %s", method, data) LOG.debug("Method: %s Data: %s", method, data)
if method == "Player.OnPlay": if method == "Player.OnPlay":
state.PLAYBACK_INIT_DONE = False
self.PlayBackStart(data) self.PlayBackStart(data)
state.PLAYBACK_INIT_DONE = True
elif method == "Player.OnStop": elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck # Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()') # xbmc.executebuiltin('ReloadSkin()')
@ -336,6 +338,17 @@ class KodiMonitor(Monitor):
kodi_item={'id': kodi_id, kodi_item={'id': kodi_id,
'type': kodi_type, 'type': kodi_type,
'file': path}) 'file': path})
# Set the Plex container key (e.g. using the Plex playqueue)
container_key = None
if info['playlistid'] != -1:
# -1 is Kodi's answer if there is no playlist
container_key = self.playqueue.playqueues[playerid].id
if container_key is not None:
container_key = '/playQueues/%s' % container_key
elif plex_id is not None:
container_key = '/library/metadata/%s' % plex_id
state.PLAYER_STATES[playerid]['container_key'] = container_key
LOG.debug('Set the Plex container_key to: %s', container_key)
def StartDirectPath(self, plex_id, type, currentFile): def StartDirectPath(self, plex_id, type, currentFile):
""" """

View file

@ -77,7 +77,7 @@ class Playqueue(Thread):
raise ValueError('Wrong playlist type passed in: %s' % typus) raise ValueError('Wrong playlist type passed in: %s' % typus)
return playqueue return playqueue
def init_playqueue_from_plex_children(self, plex_id): def init_playqueue_from_plex_children(self, plex_id, transient_token=None):
""" """
Init a new playqueue e.g. from an album. Alexa does this Init a new playqueue e.g. from an album. Alexa does this
@ -95,6 +95,7 @@ class Playqueue(Thread):
for i, child in enumerate(xml): for i, child in enumerate(xml):
api = API(child) api = API(child)
PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey()) PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey())
playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player') LOG.debug('Firing up Kodi player')
Player().play(playqueue.kodi_pl, None, False, 0) Player().play(playqueue.kodi_pl, None, False, 0)
return playqueue return playqueue
@ -114,6 +115,9 @@ class Playqueue(Thread):
""" """
LOG.info('New playqueue %s received from Plex companion with offset ' LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s', playqueue_id, offset, repeat) '%s, repeat %s', playqueue_id, offset, repeat)
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
with LOCK: with LOCK:
xml = PL.get_PMS_playlist(playqueue, playqueue_id) xml = PL.get_PMS_playlist(playqueue, playqueue_id)
playqueue.clear() playqueue.clear()
@ -123,7 +127,7 @@ class Playqueue(Thread):
LOG.error('Could not get playqueue ID %s', playqueue_id) LOG.error('Could not get playqueue ID %s', playqueue_id)
return return
playqueue.repeat = 0 if not repeat else int(repeat) playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.token = transient_token playqueue.plex_transient_token = transient_token
PlaybackUtils(xml, playqueue).play_all() PlaybackUtils(xml, playqueue).play_all()
window('plex_customplaylist', value="true") window('plex_customplaylist', value="true")
if offset not in (None, "0"): if offset not in (None, "0"):

View file

@ -9,6 +9,7 @@ from urlparse import urlparse, parse_qs
from xbmc import sleep from xbmc import sleep
from companion import process_command from companion import process_command
from utils import window
import json_rpc as js import json_rpc as js
from clientinfo import getXArgsDeviceInfo from clientinfo import getXArgsDeviceInfo
import variables as v import variables as v
@ -19,6 +20,21 @@ LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
RESOURCES_XML = ('%s<MediaContainer>\n'
' <Player'
' title="{title}"'
' protocol="plex"'
' protocolVersion="1"'
' protocolCapabilities="timeline,playback,navigation,playqueues"'
' machineIdentifier="{machineIdentifier}"'
' product="%s"'
' platform="%s"'
' platformVersion="%s"'
' deviceClass="pc"/>\n'
'</MediaContainer>\n') % (v.XML_HEADER,
v.ADDON_NAME,
v.PLATFORM,
v.ADDON_VERSION)
class MyHandler(BaseHTTPRequestHandler): class MyHandler(BaseHTTPRequestHandler):
""" """
@ -78,94 +94,68 @@ class MyHandler(BaseHTTPRequestHandler):
self.serverlist = self.server.client.getServerList() self.serverlist = self.server.client.getServerList()
sub_mgr = self.server.subscription_manager sub_mgr = self.server.subscription_manager
try: request_path = self.path[1:]
request_path = self.path[1:] request_path = sub(r"\?.*", "", request_path)
request_path = sub(r"\?.*", "", request_path) url = urlparse(self.path)
url = urlparse(self.path) paramarrays = parse_qs(url.query)
paramarrays = parse_qs(url.query) params = {}
params = {} for key in paramarrays:
for key in paramarrays: params[key] = paramarrays[key][0]
params[key] = paramarrays[key][0] LOG.debug("remote request_path: %s", request_path)
LOG.debug("remote request_path: %s", request_path) LOG.debug("params received from remote: %s", params)
LOG.debug("params received from remote: %s", params) sub_mgr.update_command_id(self.headers.get(
sub_mgr.update_command_id(self.headers.get( 'X-Plex-Client-Identifier', self.client_address[0]),
'X-Plex-Client-Identifier', params.get('commandID'))
self.client_address[0]), if request_path == "version":
params.get('commandID', False)) self.response(
if request_path == "version": "PlexKodiConnect Plex Companion: Running\nVersion: %s"
self.response( % v.ADDON_VERSION)
"PlexKodiConnect Plex Companion: Running\nVersion: %s" elif request_path == "verify":
% v.ADDON_VERSION) self.response("XBMC JSON connection test:\n" + js.ping())
elif request_path == "verify": elif request_path == 'resources':
self.response("XBMC JSON connection test:\n" + self.response(
js.ping()) RESOURCES_XML.format(
elif request_path == 'resources': title=v.DEVICENAME,
resp = ('%s' machineIdentifier=window('plex_machineIdentifier')),
'<MediaContainer>' getXArgsDeviceInfo(include_token=False))
'<Player' elif "/poll" in request_path:
' title="%s"' if params.get('wait') == '1':
' protocol="plex"' sleep(950)
' protocolVersion="1"' self.response(
' protocolCapabilities="timeline,playback,navigation,playqueues"' sub_mgr.msg(js.get_players()).format(
' machineIdentifier="%s"' command_id=params.get('commandID', 0)),
' product="PlexKodiConnect"' {
' platform="%s"' 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
' platformVersion="%s"' 'X-Plex-Protocol': '1.0',
' deviceClass="pc"' 'Access-Control-Allow-Origin': '*',
'/>' 'Access-Control-Max-Age': '1209600',
'</MediaContainer>' 'Access-Control-Expose-Headers':
% (v.XML_HEADER, 'X-Plex-Client-Identifier',
v.DEVICENAME, 'Content-Type': 'text/xml;charset=utf-8'
v.PKC_MACHINE_IDENTIFIER, })
v.PLATFORM, elif "/subscribe" in request_path:
v.ADDON_VERSION)) self.response(v.COMPANION_OK_MESSAGE,
LOG.debug("crafted resources response: %s", resp) getXArgsDeviceInfo(include_token=False))
self.response(resp, getXArgsDeviceInfo(include_token=False)) protocol = params.get('protocol')
elif "/poll" in request_path: host = self.client_address[0]
if params.get('wait', False) == '1': port = params.get('port')
sleep(950) uuid = self.headers.get('X-Plex-Client-Identifier')
command_id = params.get('commandID', 0) command_id = params.get('commandID', 0)
self.response( sub_mgr.add_subscriber(protocol,
sub(r"INSERTCOMMANDID", host,
str(command_id), port,
sub_mgr.msg(js.get_players())), uuid,
{ command_id)
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, elif "/unsubscribe" in request_path:
'X-Plex-Protocol': '1.0', self.response(v.COMPANION_OK_MESSAGE,
'Access-Control-Allow-Origin': '*', getXArgsDeviceInfo(include_token=False))
'Access-Control-Max-Age': '1209600', uuid = self.headers.get('X-Plex-Client-Identifier') \
'Access-Control-Expose-Headers': or self.client_address[0]
'X-Plex-Client-Identifier', sub_mgr.remove_subscriber(uuid)
'Content-Type': 'text/xml;charset=utf-8' else:
}) # Throw it to companion.py
elif "/subscribe" in request_path: process_command(request_path, params, self.server.queue)
self.response(v.COMPANION_OK_MESSAGE, self.response('', getXArgsDeviceInfo(include_token=False))
getXArgsDeviceInfo(include_token=False))
protocol = params.get('protocol', False)
host = self.client_address[0]
port = params.get('port', False)
uuid = self.headers.get('X-Plex-Client-Identifier', "")
command_id = params.get('commandID', 0)
sub_mgr.add_subscriber(protocol,
host,
port,
uuid,
command_id)
elif "/unsubscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False))
uuid = self.headers.get('X-Plex-Client-Identifier', False) \
or self.client_address[0]
sub_mgr.remove_subscriber(uuid)
else:
# Throw it to companion.py
process_command(request_path, params, self.server.queue)
self.response('', getXArgsDeviceInfo(include_token=False))
sub_mgr.notify()
except:
LOG.error('Error encountered. Traceback:')
import traceback
LOG.error(traceback.print_exc())
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):

View file

@ -3,12 +3,10 @@ Manages getting playstate from Kodi and sending it to the PMS as well as
subscribed Plex Companion clients. subscribed Plex Companion clients.
""" """
from logging import getLogger from logging import getLogger
from re import sub from threading import Thread, RLock
from threading import Thread, Lock
from downloadutils import DownloadUtils as DU from downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis, Lock_Function from utils import window, kodi_time_to_millis, Lock_Function
from playlist_func import init_Plex_playlist
import state import state
import variables as v import variables as v
import json_rpc as js import json_rpc as js
@ -17,19 +15,19 @@ import json_rpc as js
LOG = getLogger("PLEX." + __name__) LOG = getLogger("PLEX." + __name__)
# Need to lock all methods and functions messing with subscribers or state # Need to lock all methods and functions messing with subscribers or state
LOCK = Lock() LOCK = RLock()
LOCKER = Lock_Function(LOCK) LOCKER = Lock_Function(LOCK)
############################################################################### ###############################################################################
# What is Companion controllable? # What is Companion controllable?
CONTROLLABLE = { CONTROLLABLE = {
v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop', v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' 'subtitleStream,seekTo,skipPrevious,skipNext,'
'skipPrevious,skipNext,stepBack,stepForward', 'stepBack,stepForward',
v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,' v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
'subtitleStream,seekTo,skipPrevious,skipNext,' 'skipPrevious,skipNext,stepBack,stepForward',
'stepBack,stepForward' v.PLEX_PLAYLIST_TYPE_PHOTO: 'skipPrevious,skipNext,stop'
} }
STREAM_DETAILS = { STREAM_DETAILS = {
@ -38,6 +36,24 @@ STREAM_DETAILS = {
'subtitle': 'currentsubtitle' 'subtitle': 'currentsubtitle'
} }
XML = ('%s<MediaContainer commandID="{command_id}" location="{location}">\n'
' <Timeline {%s}/>\n'
' <Timeline {%s}/>\n'
' <Timeline {%s}/>\n'
'</MediaContainer>\n') % (v.XML_HEADER,
v.PLEX_PLAYLIST_TYPE_VIDEO,
v.PLEX_PLAYLIST_TYPE_AUDIO,
v.PLEX_PLAYLIST_TYPE_PHOTO)
def update_player_info(playerid):
"""
Updates all player info for playerid [int] in state.py.
"""
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
class SubscriptionMgr(object): class SubscriptionMgr(object):
""" """
@ -47,8 +63,6 @@ class SubscriptionMgr(object):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
self.container_key = None
self.ratingkey = None
self.server = "" self.server = ""
self.protocol = "http" self.protocol = "http"
self.port = "" self.port = ""
@ -90,18 +104,126 @@ class SubscriptionMgr(object):
Returns a timeline xml as str Returns a timeline xml as str
(xml containing video, audio, photo player state) (xml containing video, audio, photo player state)
""" """
msg = v.XML_HEADER self.isplaying = False
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"' answ = str(XML)
msg += ' machineIdentifier="%s">\n' % v.PKC_MACHINE_IDENTIFIER timelines = {
msg += self._timeline_xml(players.get(v.KODI_TYPE_AUDIO), v.PLEX_PLAYLIST_TYPE_VIDEO: None,
v.PLEX_TYPE_AUDIO) v.PLEX_PLAYLIST_TYPE_AUDIO: None,
msg += self._timeline_xml(players.get(v.KODI_TYPE_PHOTO), v.PLEX_PLAYLIST_TYPE_PHOTO: None
v.PLEX_TYPE_PHOTO) }
msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO), for typus in timelines:
v.PLEX_TYPE_VIDEO) if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
msg += "</MediaContainer>" timeline = {
LOG.debug('Our PKC message is: %s', msg) 'controllable': CONTROLLABLE[typus],
return msg 'type': typus,
'state': 'stopped'
}
else:
timeline = self._timeline_dict(players[
v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]], typus)
timelines[typus] = self._dict_to_xml(timeline)
location = 'fullScreenVideo' if self.isplaying else 'navigation'
timelines.update({'command_id': '{command_id}', 'location': location})
return answ.format(**timelines)
@staticmethod
def _dict_to_xml(dictionary):
"""
Returns the string 'key1="value1" key2="value2" ...' for dictionary
"""
answ = ''
for key, value in dictionary.iteritems():
answ += '%s="%s" ' % (key, value)
return answ
def _timeline_dict(self, player, ptype):
playerid = player['playerid']
info = state.PLAYER_STATES[playerid]
playqueue = self.playqueue.playqueues[playerid]
pos = info['position']
try:
playqueue.items[pos]
except IndexError:
# E.g. for direct path playback for single item
return {
'controllable': CONTROLLABLE[ptype],
'type': ptype,
'state': 'stopped'
}
pbmc_server = window('pms_server')
if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
status = 'paused' if info['speed'] == '0' else 'playing'
duration = kodi_time_to_millis(info['totaltime'])
shuffle = '1' if info['shuffled'] else '0'
mute = '1' if info['muted'] is True else '0'
answ = {
'location': 'fullScreenVideo',
'controllable': CONTROLLABLE[ptype],
'protocol': self.protocol,
'address': self.server,
'port': self.port,
'machineIdentifier': window('plex_machineIdentifier'),
'state': status,
'type': ptype,
'itemType': ptype,
'time': kodi_time_to_millis(info['time']),
'duration': duration,
'seekRange': '0-%s' % duration,
'shuffle': shuffle,
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
'volume': info['volume'],
'mute': mute,
'mediaIndex': pos, # Still to implement from here
'partIndex':pos,
'partCount': len(playqueue.items),
'providerIdentifier': 'com.plexapp.plugins.library',
}
if info['plex_id']:
answ['key'] = '/library/metadata/%s' % info['plex_id']
answ['ratingKey'] = info['plex_id']
# PlayQueue stuff
if info['container_key']:
answ['containerKey'] = info['container_key']
if (info['container_key'] is not None and
info['container_key'].startswith('/playQueues')):
answ['playQueueID'] = playqueue.id
answ['playQueueVersion'] = playqueue.version
answ['playQueueItemID'] = playqueue.items[pos].id
if playqueue.items[pos].guid:
answ['guid'] = playqueue.items[pos].guid
# Temp. token set?
if state.PLEX_TRANSIENT_TOKEN:
answ['token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype != v.PLEX_PLAYLIST_TYPE_PHOTO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id:
answ['audioStreamID'] = strm_id
else:
LOG.error('We could not select a Plex audiostream')
if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
strm_id = self._plex_stream_index(playerid, 'video')
if strm_id:
answ['videoStreamID'] = strm_id
else:
LOG.error('We could not select a Plex videostream')
if info['subtitleenabled']:
try:
strm_id = self._plex_stream_index(playerid, 'subtitle')
except KeyError:
# subtitleenabled can be True while currentsubtitle can
# still be {}
strm_id = None
if strm_id is not None:
# If None, then the subtitle is only present on Kodi side
answ['subtitleStreamID'] = strm_id
self.isplaying = True
return answ
def signal_stop(self): def signal_stop(self):
""" """
@ -114,23 +236,6 @@ class SubscriptionMgr(object):
self.last_params['state'] = 'stopped' self.last_params['state'] = 'stopped'
self._send_pms_notification(playerid, self.last_params) self._send_pms_notification(playerid, self.last_params)
def _get_container_key(self, playerid):
key = None
playlistid = state.PLAYER_STATES[playerid]['playlistid']
if playlistid != -1:
# -1 is Kodi's answer if there is no playlist
try:
key = self.playqueue.playqueues[playlistid].id
except (KeyError, IndexError, TypeError):
pass
if key is not None:
key = '/playQueues/%s' % key
else:
if state.PLAYER_STATES[playerid]['plex_id']:
key = '/library/metadata/%s' % \
state.PLAYER_STATES[playerid]['plex_id']
return key
def _plex_stream_index(self, playerid, stream_type): def _plex_stream_index(self, playerid, stream_type):
""" """
Returns the current Plex stream index [str] for the player playerid Returns the current Plex stream index [str] for the player playerid
@ -142,96 +247,6 @@ class SubscriptionMgr(object):
return playqueue.items[info['position']].plex_stream_index( return playqueue.items[info['position']].plex_stream_index(
info[STREAM_DETAILS[stream_type]]['index'], stream_type) info[STREAM_DETAILS[stream_type]]['index'], stream_type)
@staticmethod
def _player_info(playerid):
"""
Grabs all player info again for playerid [int].
Returns the dict state.PLAYER_STATES[playerid]
"""
# Update our PKC state of how the player actually looks like
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
return state.PLAYER_STATES[playerid]
def _timeline_xml(self, player, ptype):
if player is None:
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
playerid = player['playerid']
info = self._player_info(playerid)
playqueue = self.playqueue.playqueues[playerid]
pos = info['position']
try:
playqueue.items[pos]
except IndexError:
# E.g. for direct path playback for single item
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
LOG.debug('INFO: %s', info)
LOG.debug('playqueue: %s', playqueue)
status = 'paused' if info['speed'] == '0' else 'playing'
ret = ' <Timeline state="%s"' % status
ret += ' controllable="%s"' % CONTROLLABLE[ptype]
ret += ' type="%s" itemType="%s"' % (ptype, ptype)
ret += ' time="%s"' % kodi_time_to_millis(info['time'])
ret += ' duration="%s"' % kodi_time_to_millis(info['totaltime'])
shuffled = '1' if info['shuffled'] else '0'
ret += ' shuffle="%s"' % shuffled
ret += ' repeat="%s"' % v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']]
if ptype != v.KODI_TYPE_PHOTO:
ret += ' volume="%s"' % info['volume']
muted = '1' if info['muted'] is True else '0'
ret += ' mute="%s"' % muted
pbmc_server = window('pms_server')
server = self._server_by_host(self.server)
if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
if info['plex_id']:
self.ratingkey = info['plex_id']
ret += ' key="/library/metadata/%s"' % info['plex_id']
ret += ' ratingKey="%s"' % info['plex_id']
# PlayQueue stuff
key = self._get_container_key(playerid)
if key is not None and key.startswith('/playQueues'):
self.container_key = key
ret += ' containerKey="%s"' % self.container_key
ret += ' playQueueItemID="%s"' % playqueue.items[pos].id or 'null'
ret += ' playQueueID="%s"' % playqueue.id or 'null'
ret += ' playQueueVersion="%s"' % playqueue.version or 'null'
ret += ' guid="%s"' % playqueue.items[pos].guid or 'null'
elif key:
self.container_key = key
ret += ' containerKey="%s"' % self.container_key
ret += ' machineIdentifier="%s"' % server.get('uuid', "")
ret += ' protocol="%s"' % server.get('protocol', 'http')
ret += ' address="%s"' % server.get('server', self.server)
ret += ' port="%s"' % server.get('port', self.port)
# Temp. token set?
if state.PLEX_TRANSIENT_TOKEN:
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
ret += ' token="%s"' % playqueue.plex_transient_token
# Process audio and subtitle streams
if ptype != v.KODI_TYPE_PHOTO:
strm_id = self._plex_stream_index(playerid, 'audio')
if strm_id is not None:
ret += ' audioStreamID="%s"' % strm_id
else:
LOG.error('We could not select a Plex audiostream')
if ptype == v.KODI_TYPE_VIDEO and info['subtitleenabled']:
try:
strm_id = self._plex_stream_index(playerid, 'subtitle')
except KeyError:
# subtitleenabled can be True while currentsubtitle can be {}
strm_id = None
if strm_id is not None:
# If None, then the subtitle is only present on Kodi side
ret += ' subtitleStreamID="%s"' % strm_id
self.isplaying = True
return ret + '/>\n'
@LOCKER.lockthis @LOCKER.lockthis
def update_command_id(self, uuid, command_id): def update_command_id(self, uuid, command_id):
""" """
@ -241,28 +256,27 @@ class SubscriptionMgr(object):
if command_id and self.subscribers.get(uuid): if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id) self.subscribers[uuid].command_id = int(command_id)
@LOCKER.lockthis
def notify(self): def notify(self):
""" """
Causes PKC to tell the PMS and Plex Companion players to receive a Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played. notification what's being played.
""" """
with LOCK: self._cleanup()
self._cleanup() # Get all the active/playing Kodi players (video, audio, pictures)
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
players = js.get_players() players = js.get_players()
# fetch the message, subscribers or not, since the server will need the # Update the PKC info with what's playing on the Kodi side
# info anyway for player in players.values():
self.isplaying = False update_player_info(player['playerid'])
msg = self.msg(players) if self.subscribers and state.PLAYBACK_INIT_DONE is True:
with LOCK: msg = self.msg(players)
if self.isplaying is True: if self.isplaying is True:
# If we don't check here, Plex Companion devices will simply # If we don't check here, Plex Companion devices will simply
# drop out of the Plex Companion playback screen # drop out of the Plex Companion playback screen
for subscriber in self.subscribers.values(): for subscriber in self.subscribers.values():
subscriber.send_update(msg, not players) subscriber.send_update(msg, not players)
self._notify_server(players) self._notify_server(players)
self.lastplayers = players self.lastplayers = players
return True
def _notify_server(self, players): def _notify_server(self, players):
for typus, player in players.iteritems(): for typus, player in players.iteritems():
@ -273,7 +287,7 @@ class SubscriptionMgr(object):
except KeyError: except KeyError:
pass pass
# Process the players we have left (to signal a stop) # Process the players we have left (to signal a stop)
for _, player in self.lastplayers.iteritems(): for player in self.lastplayers.values():
self.last_params['state'] = 'stopped' self.last_params['state'] = 'stopped'
self._send_pms_notification(player['playerid'], self.last_params) self._send_pms_notification(player['playerid'], self.last_params)
@ -282,18 +296,17 @@ class SubscriptionMgr(object):
status = 'paused' if info['speed'] == '0' else 'playing' status = 'paused' if info['speed'] == '0' else 'playing'
params = { params = {
'state': status, 'state': status,
'ratingKey': self.ratingkey, 'ratingKey': info['plex_id'],
'key': '/library/metadata/%s' % self.ratingkey, 'key': '/library/metadata/%s' % info['plex_id'],
'time': kodi_time_to_millis(info['time']), 'time': kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime']) 'duration': kodi_time_to_millis(info['totaltime'])
} }
if self.container_key: if info['container_key'] is not None:
params['containerKey'] = self.container_key params['containerKey'] = info['container_key']
if self.container_key is not None and \ if info['container_key'].startswith('/playQueues/'):
self.container_key.startswith('/playQueues/'): playqueue = self.playqueue.playqueues[playerid]
playqueue = self.playqueue.playqueues[playerid] params['playQueueVersion'] = playqueue.version
params['playQueueVersion'] = playqueue.version params['playQueueItemID'] = playqueue.id
params['playQueueItemID'] = playqueue.id
self.last_params = params self.last_params = params
return params return params
@ -384,11 +397,10 @@ class Subscriber(object):
return True return True
else: else:
self.navlocationsent = True self.navlocationsent = True
msg = sub(r"INSERTCOMMANDID", str(self.command_id), msg) msg = msg.format(command_id=self.command_id)
LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s", LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
self.uuid, self.command_id, msg) self.uuid, self.command_id, msg)
url = self.protocol + '://' + self.host + ':' + self.port \ url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port)
+ "/:/timeline"
thread = Thread(target=self._threaded_send, args=(url, msg)) thread = Thread(target=self._threaded_send, args=(url, msg))
thread.start() thread.start()

View file

@ -106,6 +106,7 @@ PLAYER_STATES = {
'kodi_type': None, 'kodi_type': None,
'plex_id': None, 'plex_id': None,
'plex_type': None, 'plex_type': None,
'container_key': None,
'volume': 100, 'volume': 100,
'muted': False 'muted': False
}, },
@ -116,6 +117,10 @@ PLAYER_STATES = {
# paths for playback (since we're not receiving a Kodi id) # paths for playback (since we're not receiving a Kodi id)
PLEX_IDS = {} PLEX_IDS = {}
PLAYED_INFO = {} PLAYED_INFO = {}
# Set to False after having received a Companion command to play something
# Set to True after Kodi monitor PlayBackStart is done
# This will prohibit "old" Plex Companion messages being sent
PLAYBACK_INIT_DONE = True
# Kodi webserver details # Kodi webserver details
WEBSERVER_PORT = 8080 WEBSERVER_PORT = 8080

View file

@ -129,6 +129,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo'
PLEX_TYPE_PHOTO = 'photo' PLEX_TYPE_PHOTO = 'photo'
# Used for /:/timeline XML messages
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
PLEX_PLAYLIST_TYPE_PHOTO = 'photo'
KODI_PLAYLIST_TYPE_VIDEO = 'video'
KODI_PLAYLIST_TYPE_AUDIO = 'audio'
KODI_PLAYLIST_TYPE_PHOTO = 'picture'
KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = {
PLEX_PLAYLIST_TYPE_VIDEO: KODI_PLAYLIST_TYPE_VIDEO,
PLEX_PLAYLIST_TYPE_AUDIO: KODI_PLAYLIST_TYPE_AUDIO,
PLEX_PLAYLIST_TYPE_PHOTO: KODI_PLAYLIST_TYPE_PHOTO
}
# All the Kodi types as e.g. used in the JSON API # All the Kodi types as e.g. used in the JSON API
KODI_TYPE_VIDEO = 'video' KODI_TYPE_VIDEO = 'video'