Major Plex Companion overhaul, part 6
This commit is contained in:
parent
cf15799df2
commit
5337ae5715
8 changed files with 302 additions and 271 deletions
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue