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

View File

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

View File

@ -125,7 +125,9 @@ class KodiMonitor(Monitor):
LOG.debug("Method: %s Data: %s", method, data)
if method == "Player.OnPlay":
state.PLAYBACK_INIT_DONE = False
self.PlayBackStart(data)
state.PLAYBACK_INIT_DONE = True
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()')
@ -336,6 +338,17 @@ class KodiMonitor(Monitor):
kodi_item={'id': kodi_id,
'type': kodi_type,
'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):
"""

View File

@ -77,7 +77,7 @@ class Playqueue(Thread):
raise ValueError('Wrong playlist type passed in: %s' % typus)
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
@ -95,6 +95,7 @@ class Playqueue(Thread):
for i, child in enumerate(xml):
api = API(child)
PL.add_item_to_playlist(playqueue, i, plex_id=api.getRatingKey())
playqueue.plex_transient_token = transient_token
LOG.debug('Firing up Kodi player')
Player().play(playqueue.kodi_pl, None, False, 0)
return playqueue
@ -114,6 +115,9 @@ class Playqueue(Thread):
"""
LOG.info('New playqueue %s received from Plex companion with offset '
'%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:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
playqueue.clear()
@ -123,7 +127,7 @@ class Playqueue(Thread):
LOG.error('Could not get playqueue ID %s', playqueue_id)
return
playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.token = transient_token
playqueue.plex_transient_token = transient_token
PlaybackUtils(xml, playqueue).play_all()
window('plex_customplaylist', value="true")
if offset not in (None, "0"):

View File

@ -9,6 +9,7 @@ from urlparse import urlparse, parse_qs
from xbmc import sleep
from companion import process_command
from utils import window
import json_rpc as js
from clientinfo import getXArgsDeviceInfo
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):
"""
@ -78,94 +94,68 @@ class MyHandler(BaseHTTPRequestHandler):
self.serverlist = self.server.client.getServerList()
sub_mgr = self.server.subscription_manager
try:
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
LOG.debug("remote request_path: %s", request_path)
LOG.debug("params received from remote: %s", params)
sub_mgr.update_command_id(self.headers.get(
'X-Plex-Client-Identifier',
self.client_address[0]),
params.get('commandID', False))
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% v.ADDON_VERSION)
elif request_path == "verify":
self.response("XBMC JSON connection test:\n" +
js.ping())
elif request_path == 'resources':
resp = ('%s'
'<MediaContainer>'
'<Player'
' title="%s"'
' protocol="plex"'
' protocolVersion="1"'
' protocolCapabilities="timeline,playback,navigation,playqueues"'
' machineIdentifier="%s"'
' product="PlexKodiConnect"'
' platform="%s"'
' platformVersion="%s"'
' deviceClass="pc"'
'/>'
'</MediaContainer>'
% (v.XML_HEADER,
v.DEVICENAME,
v.PKC_MACHINE_IDENTIFIER,
v.PLATFORM,
v.ADDON_VERSION))
LOG.debug("crafted resources response: %s", resp)
self.response(resp, getXArgsDeviceInfo(include_token=False))
elif "/poll" in request_path:
if params.get('wait', False) == '1':
sleep(950)
command_id = params.get('commandID', 0)
self.response(
sub(r"INSERTCOMMANDID",
str(command_id),
sub_mgr.msg(js.get_players())),
{
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0',
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '1209600',
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
})
elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
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())
request_path = self.path[1:]
request_path = sub(r"\?.*", "", request_path)
url = urlparse(self.path)
paramarrays = parse_qs(url.query)
params = {}
for key in paramarrays:
params[key] = paramarrays[key][0]
LOG.debug("remote request_path: %s", request_path)
LOG.debug("params received from remote: %s", params)
sub_mgr.update_command_id(self.headers.get(
'X-Plex-Client-Identifier', self.client_address[0]),
params.get('commandID'))
if request_path == "version":
self.response(
"PlexKodiConnect Plex Companion: Running\nVersion: %s"
% v.ADDON_VERSION)
elif request_path == "verify":
self.response("XBMC JSON connection test:\n" + js.ping())
elif request_path == 'resources':
self.response(
RESOURCES_XML.format(
title=v.DEVICENAME,
machineIdentifier=window('plex_machineIdentifier')),
getXArgsDeviceInfo(include_token=False))
elif "/poll" in request_path:
if params.get('wait') == '1':
sleep(950)
self.response(
sub_mgr.msg(js.get_players()).format(
command_id=params.get('commandID', 0)),
{
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Protocol': '1.0',
'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': '1209600',
'Access-Control-Expose-Headers':
'X-Plex-Client-Identifier',
'Content-Type': 'text/xml;charset=utf-8'
})
elif "/subscribe" in request_path:
self.response(v.COMPANION_OK_MESSAGE,
getXArgsDeviceInfo(include_token=False))
protocol = params.get('protocol')
host = self.client_address[0]
port = params.get('port')
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') \
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))
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.
"""
from logging import getLogger
from re import sub
from threading import Thread, Lock
from threading import Thread, RLock
from downloadutils import DownloadUtils as DU
from utils import window, kodi_time_to_millis, Lock_Function
from playlist_func import init_Plex_playlist
import state
import variables as v
import json_rpc as js
@ -17,19 +15,19 @@ import json_rpc as js
LOG = getLogger("PLEX." + __name__)
# Need to lock all methods and functions messing with subscribers or state
LOCK = Lock()
LOCK = RLock()
LOCKER = Lock_Function(LOCK)
###############################################################################
# What is Companion controllable?
CONTROLLABLE = {
v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop',
v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
'skipPrevious,skipNext,stepBack,stepForward',
v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
'subtitleStream,seekTo,skipPrevious,skipNext,'
'stepBack,stepForward'
v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
'subtitleStream,seekTo,skipPrevious,skipNext,'
'stepBack,stepForward',
v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
'skipPrevious,skipNext,stepBack,stepForward',
v.PLEX_PLAYLIST_TYPE_PHOTO: 'skipPrevious,skipNext,stop'
}
STREAM_DETAILS = {
@ -38,6 +36,24 @@ STREAM_DETAILS = {
'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):
"""
@ -47,8 +63,6 @@ class SubscriptionMgr(object):
self.serverlist = []
self.subscribers = {}
self.info = {}
self.container_key = None
self.ratingkey = None
self.server = ""
self.protocol = "http"
self.port = ""
@ -90,18 +104,126 @@ class SubscriptionMgr(object):
Returns a timeline xml as str
(xml containing video, audio, photo player state)
"""
msg = v.XML_HEADER
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
msg += ' machineIdentifier="%s">\n' % v.PKC_MACHINE_IDENTIFIER
msg += self._timeline_xml(players.get(v.KODI_TYPE_AUDIO),
v.PLEX_TYPE_AUDIO)
msg += self._timeline_xml(players.get(v.KODI_TYPE_PHOTO),
v.PLEX_TYPE_PHOTO)
msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO),
v.PLEX_TYPE_VIDEO)
msg += "</MediaContainer>"
LOG.debug('Our PKC message is: %s', msg)
return msg
self.isplaying = False
answ = str(XML)
timelines = {
v.PLEX_PLAYLIST_TYPE_VIDEO: None,
v.PLEX_PLAYLIST_TYPE_AUDIO: None,
v.PLEX_PLAYLIST_TYPE_PHOTO: None
}
for typus in timelines:
if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
timeline = {
'controllable': CONTROLLABLE[typus],
'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):
"""
@ -114,23 +236,6 @@ class SubscriptionMgr(object):
self.last_params['state'] = 'stopped'
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):
"""
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(
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
def update_command_id(self, uuid, command_id):
"""
@ -241,28 +256,27 @@ class SubscriptionMgr(object):
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
@LOCKER.lockthis
def notify(self):
"""
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
with LOCK:
self._cleanup()
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
self._cleanup()
# Get all the active/playing Kodi players (video, audio, pictures)
players = js.get_players()
# fetch the message, subscribers or not, since the server will need the
# info anyway
self.isplaying = False
msg = self.msg(players)
with LOCK:
# Update the PKC info with what's playing on the Kodi side
for player in players.values():
update_player_info(player['playerid'])
if self.subscribers and state.PLAYBACK_INIT_DONE is True:
msg = self.msg(players)
if self.isplaying is True:
# If we don't check here, Plex Companion devices will simply
# drop out of the Plex Companion playback screen
for subscriber in self.subscribers.values():
subscriber.send_update(msg, not players)
self._notify_server(players)
self.lastplayers = players
return True
self._notify_server(players)
self.lastplayers = players
def _notify_server(self, players):
for typus, player in players.iteritems():
@ -273,7 +287,7 @@ class SubscriptionMgr(object):
except KeyError:
pass
# 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._send_pms_notification(player['playerid'], self.last_params)
@ -282,18 +296,17 @@ class SubscriptionMgr(object):
status = 'paused' if info['speed'] == '0' else 'playing'
params = {
'state': status,
'ratingKey': self.ratingkey,
'key': '/library/metadata/%s' % self.ratingkey,
'ratingKey': info['plex_id'],
'key': '/library/metadata/%s' % info['plex_id'],
'time': kodi_time_to_millis(info['time']),
'duration': kodi_time_to_millis(info['totaltime'])
}
if self.container_key:
params['containerKey'] = self.container_key
if self.container_key is not None and \
self.container_key.startswith('/playQueues/'):
playqueue = self.playqueue.playqueues[playerid]
params['playQueueVersion'] = playqueue.version
params['playQueueItemID'] = playqueue.id
if info['container_key'] is not None:
params['containerKey'] = info['container_key']
if info['container_key'].startswith('/playQueues/'):
playqueue = self.playqueue.playqueues[playerid]
params['playQueueVersion'] = playqueue.version
params['playQueueItemID'] = playqueue.id
self.last_params = params
return params
@ -384,11 +397,10 @@ class Subscriber(object):
return True
else:
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",
self.uuid, self.command_id, msg)
url = self.protocol + '://' + self.host + ':' + self.port \
+ "/:/timeline"
url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port)
thread = Thread(target=self._threaded_send, args=(url, msg))
thread.start()

View File

@ -106,6 +106,7 @@ PLAYER_STATES = {
'kodi_type': None,
'plex_id': None,
'plex_type': None,
'container_key': None,
'volume': 100,
'muted': False
},
@ -116,6 +117,10 @@ PLAYER_STATES = {
# paths for playback (since we're not receiving a Kodi id)
PLEX_IDS = {}
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
WEBSERVER_PORT = 8080

View File

@ -129,6 +129,20 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo'
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
KODI_TYPE_VIDEO = 'video'