Major Plex Companion overhaul, part 4

This commit is contained in:
croneter 2017-12-21 09:28:06 +01:00
parent 47779bbbee
commit 4547ec52af
10 changed files with 365 additions and 240 deletions

View File

@ -11,6 +11,7 @@ from xbmc import sleep, executebuiltin
from utils import settings, thread_methods
from plexbmchelper import listener, plexgdm, subscribers, httppersist
from plexbmchelper.subscribers import LOCKER
from PlexFunctions import ParseContainerKey, GetPlexMetadata
from PlexAPI import API
from playlist_func import get_pms_playqueue, get_plextype_from_xml
@ -44,8 +45,127 @@ class PlexCompanion(Thread):
self.player = player.PKC_Player()
self.httpd = False
self.queue = None
self.subscription_manager = None
Thread.__init__(self)
@LOCKER.lockthis
def _process_alexa(self, data):
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
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')
else:
state.PLEX_TRANSIENT_TOKEN = data.get('token')
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true',
'node': 'false'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
@staticmethod
def _process_node(data):
"""
E.g. watch later initiated by Companion. Basically navigating Plex
"""
state.PLEX_TRANSIENT_TOKEN = data.get('key')
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
@LOCKER.lockthis
def _process_playlist(self, data):
# Get the playqueue ID
try:
_, plex_id, query = ParseContainerKey(data['containerKey'])
except:
LOG.error('Exception while processing')
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
return
try:
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
except KeyError:
# E.g. Plex web does not supply the media type
# Still need to figure out the type (video vs. music vs. pix)
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
api = API(xml[0])
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
plex_id,
repeat=query.get('repeat'),
offset=data.get('offset'))
playqueue.plex_transient_token = data.get('key')
@LOCKER.lockthis
def _process_streams(self, data):
"""
Plex Companion client adjusted audio or subtitle stream
"""
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
self.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
self.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
self.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', data)
@LOCKER.lockthis
def _process_refresh(self, data):
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
xml = get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear()
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
data['playQueueID'])
def _process_tasks(self, task):
"""
Processes tasks picked up e.g. by Companion listener, e.g.
@ -63,128 +183,25 @@ class PlexCompanion(Thread):
"""
LOG.debug('Processing: %s', task)
data = task['data']
# Get the token of the user flinging media (might be different one)
token = data.get('token')
if task['action'] == 'alexa':
# e.g. Alexa
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
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 = token
else:
state.PLEX_TRANSIENT_TOKEN = token
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true',
'node': 'false'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
# E.g. watch later initiated by Companion
state.PLEX_TRANSIENT_TOKEN = token
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
'view_offset': data.get('offset'),
'play_directly': 'true'
}
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
self._process_node(data)
elif task['action'] == 'playlist':
# Get the playqueue ID
try:
_, plex_id, query = ParseContainerKey(data['containerKey'])
except:
LOG.error('Exception while processing')
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
return
try:
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
except KeyError:
# E.g. Plex web does not supply the media type
# Still need to figure out the type (video vs. music vs. pix)
xml = GetPlexMetadata(data['key'])
try:
xml[0].attrib
except (AttributeError, IndexError, TypeError):
LOG.error('Could not download Plex metadata')
return
api = API(xml[0])
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.getType()])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
plex_id,
repeat=query.get('repeat'),
offset=data.get('offset'))
playqueue.plex_transient_token = token
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
# example data: {'playQueueID': '8475', 'commandID': '11'}
xml = get_pms_playqueue(data['playQueueID'])
if xml is None:
return
if len(xml) == 0:
LOG.debug('Empty playqueue received - clearing playqueue')
plex_type = get_plextype_from_xml(xml)
if plex_type is None:
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear()
return
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
self.mgr.playqueue.update_playqueue_from_PMS(
playqueue,
data['playQueueID'])
self._process_refresh(data)
elif task['action'] == 'setStreams':
# Plex Companion client adjusted audio or subtitle stream
playqueue = self.mgr.playqueue.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
pos = js.get_position(playqueue.playlistid)
if 'audioStreamID' in data:
index = playqueue.items[pos].kodi_stream_index(
data['audioStreamID'], 'audio')
self.player.setAudioStream(index)
elif 'subtitleStreamID' in data:
if data['subtitleStreamID'] == '0':
self.player.showSubtitles(False)
else:
index = playqueue.items[pos].kodi_stream_index(
data['subtitleStreamID'], 'subtitle')
self.player.setSubtitleStream(index)
else:
LOG.error('Unknown setStreams command: %s', task)
self._process_streams(data)
def run(self):
"""
Ensure that
- STOP sent to PMS
- sockets will be closed no matter what
Ensure that sockets will be closed no matter what
"""
try:
self._run()
finally:
self.subscription_manager.signal_stop()
try:
self.httpd.socket.shutdown(SHUT_RDWR)
except AttributeError:
@ -288,4 +305,5 @@ class PlexCompanion(Thread):
# Don't sleep
continue
sleep(50)
self.subscription_manager.signal_stop()
client.stop_all()

View File

@ -58,7 +58,7 @@ def process_command(request_path, params, queue=None):
if params.get('deviceName') == 'Alexa':
convert_alexa_to_companion(params)
LOG.debug('Received request_path: %s, params: %s', request_path, params)
if "/playMedia" in request_path:
if request_path == 'player/playback/playMedia':
# We need to tell service.py
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
queue.put({

View File

@ -247,20 +247,23 @@ def input_sendtext(text):
return JsonRPC("Input.SendText").execute({'test': text, 'done': False})
def playlist_get_items(playlistid, properties):
def playlist_get_items(playlistid):
"""
playlistid: [int] id of the Kodi playlist
properties: [list] of strings for the properties to return
e.g. 'title', 'file'
Returns a list of Kodi playlist items as dicts with the keys specified in
properties. Or an empty list if unsuccessful. Example:
[{u'title': u'3 Idiots', u'type': u'movie', u'id': 3, u'file':
u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv', u'label': u'3 Idiots'}]
[
{
u'file':u'smb://nas/PlexMovies/3 Idiots 2009 pt1.mkv',
u'title': u'3 Idiots',
u'type': u'movie', # IF possible! Else key missing
u'id': 3, # IF possible! Else key missing
u'label': u'3 Idiots'}]
"""
reply = JsonRPC('Playlist.GetItems').execute({
'playlistid': playlistid,
'properties': properties
'properties': ['title', 'file']
})
try:
reply = reply['result']['items']

View File

@ -10,8 +10,10 @@ import plexdb_functions as plexdb
from utils import window, settings, CatchExceptions, plex_command
from PlexFunctions import scrobble
from kodidb_functions import kodiid_from_filename
from plexbmchelper.subscribers import LOCKER
from PlexAPI import API
import json_rpc as js
import playlist_func as PL
import state
import variables as v
@ -124,17 +126,20 @@ class KodiMonitor(Monitor):
if method == "Player.OnPlay":
self.PlayBackStart(data)
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
# xbmc.executebuiltin('ReloadSkin()')
pass
elif method == 'Playlist.OnAdd':
self._playlist_onadd(data)
elif method == 'Playlist.OnRemove':
self._playlist_onremove(data)
elif method == 'Playlist.OnClear':
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
playcount = data.get('playcount')
item = data.get('item')
try:
kodiid = item['id']
item_type = item['type']
@ -161,30 +166,84 @@ class KodiMonitor(Monitor):
scrobble(itemid, 'watched')
else:
scrobble(itemid, 'unwatched')
elif method == "VideoLibrary.OnRemove":
pass
elif method == "System.OnSleep":
# Connection is going to sleep
LOG.info("Marking the server as offline. SystemOnSleep activated.")
window('plex_online', value="sleep")
elif method == "System.OnWake":
# Allow network to wake up
sleep(10000)
window('plex_onWake', value="true")
window('plex_online', value="false")
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
sleep(5000)
plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down')
state.STOP_PKC = True
@LOCKER.lockthis
def _playlist_onadd(self, data):
"""
Called if an item is added to a Kodi playlist. Example data dict:
{
u'item': {
u'type': u'movie',
u'id': 2},
u'playlistid': 1,
u'position': 0
}
Will NOT be called if playback initiated by Kodi widgets
"""
playqueue = self.playqueue.playqueues[data['playlistid']]
# Check whether we even need to update our known playqueue
kodi_playqueue = js.playlist_get_items(data['playlistid'])
if playqueue.old_kodi_pl == kodi_playqueue:
# We already know the latest playqueue (e.g. because Plex
# initiated playback)
return
# Playlist has been updated; need to tell Plex about it
if playqueue.id is None:
PL.init_Plex_playlist(playqueue, kodi_item=data['item'])
else:
PL.add_item_to_PMS_playlist(playqueue,
data['position'],
kodi_item=data['item'])
# Make sure that we won't re-add this item
playqueue.old_kodi_pl = kodi_playqueue
@LOCKER.lockthis
def _playlist_onremove(self, data):
"""
Called if an item is removed from a Kodi playlist. Example data dict:
{
u'playlistid': 1,
u'position': 0
}
"""
playqueue = self.playqueue.playqueues[data['playlistid']]
# Check whether we even need to update our known playqueue
kodi_playqueue = js.playlist_get_items(data['playlistid'])
if playqueue.old_kodi_pl == kodi_playqueue:
# We already know the latest playqueue - nothing to do
return
PL.delete_playlist_item_from_PMS(playqueue, data['position'])
playqueue.old_kodi_pl = kodi_playqueue
@LOCKER.lockthis
def _playlist_onclear(self, data):
"""
Called if a Kodi playlist is cleared. Example data dict:
{
u'playlistid': 1,
}
"""
self.playqueue.playqueues[data['playlistid']].clear()
@LOCKER.lockthis
def PlayBackStart(self, data):
"""
Called whenever playback is started. Example data:
@ -192,8 +251,7 @@ class KodiMonitor(Monitor):
u'item': {u'type': u'movie', u'title': u''},
u'player': {u'playerid': 1, u'speed': 1}
}
Unfortunately VERY random inputs!
E.g. when using Widgets, Kodi doesn't tell us shit
Unfortunately when using Widgets, Kodi doesn't tell us shit
"""
# Get the type of media we're playing
try:
@ -237,16 +295,37 @@ class KodiMonitor(Monitor):
except TypeError:
# No plex id, hence item not in the library. E.g. clips
pass
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
state.PLAYER_STATES[playerid]['file'] = json_data['file']
info = js.get_player_props(playerid)
state.PLAYER_STATES[playerid].update(info)
state.PLAYER_STATES[playerid]['file'] = path
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
state.PLAYER_STATES[playerid]['plex_type'] = plex_type
# Set other stuff like volume
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid])
# Check whether we need to init our playqueues (e.g. direct play)
init = False
playqueue = self.playqueue.playqueues[playerid]
try:
playqueue.items[info['position']]
except IndexError:
init = True
if init is False and plex_id is not None:
if plex_id != playqueue.items[
state.PLAYER_STATES[playerid]['position']].id:
init = True
elif init is False and path != playqueue.items[
state.PLAYER_STATES[playerid]['position']].file:
init = True
if init is True:
LOG.debug('Need to initialize Plex and PKC playqueue')
if plex_id:
PL.init_Plex_playlist(playqueue, plex_id=plex_id)
else:
PL.init_Plex_playlist(playqueue,
kodi_item={'id': kodi_id,
'type': kodi_type,
'file': path})
def StartDirectPath(self, plex_id, type, currentFile):
"""

View File

@ -25,22 +25,23 @@ REGEX = re_compile(r'''metadata%2F(\d+)''')
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
class Playlist_Object_Baseclase(object):
class PlaylistObjectBaseclase(object):
"""
Base class
"""
playlistid = None
type = None
kodi_pl = None
items = []
old_kodi_pl = []
id = None
version = None
selectedItemID = None
selectedItemOffset = None
shuffled = 0
repeat = 0
plex_transient_token = None
def __init__(self):
self.playlistid = None
self.type = None
self.kodi_pl = None
self.items = []
self.old_kodi_pl = []
self.id = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.plex_transient_token = None
def __repr__(self):
"""
@ -76,14 +77,14 @@ class Playlist_Object_Baseclase(object):
LOG.debug('Playlist cleared: %s', self)
class Playlist_Object(Playlist_Object_Baseclase):
class Playlist_Object(PlaylistObjectBaseclase):
"""
To be done for synching Plex playlists to Kodi
"""
kind = 'playList'
class Playqueue_Object(Playlist_Object_Baseclase):
class Playqueue_Object(PlaylistObjectBaseclase):
"""
PKC object to represent PMS playQueues and Kodi playlist for queueing
@ -114,27 +115,27 @@ class Playlist_Item(object):
id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID
plex_id = None [str] Plex unique item id, "ratingKey"
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
plex_UUID = None [str] Plex librarySectionUUID
plex_uuid = None [str] Plex librarySectionUUID
kodi_id = None Kodi unique kodi id (unique only within type!)
kodi_type = None [str] Kodi type: 'movie'
file = None [str] Path to the item's file. STRING!!
uri = None [str] Weird Plex uri path involving plex_UUID. STRING!
uri = None [str] Weird Plex uri path involving plex_uuid. STRING!
guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
"""
id = None
plex_id = None
plex_type = None
plex_UUID = None
kodi_id = None
kodi_type = None
file = None
uri = None
guid = None
xml = None
# Yet to be implemented: handling of a movie with several parts
part = 0
def __init__(self):
self.id = None
self.plex_id = None
self.plex_type = None
self.plex_uuid = None
self.kodi_id = None
self.kodi_type = None
self.file = None
self.uri = None
self.guid = None
self.xml = None
# Yet to be implemented: handling of a movie with several parts
self.part = 0
def __repr__(self):
"""
@ -201,7 +202,7 @@ def playlist_item_from_kodi(kodi_item):
try:
item.plex_id = plex_dbitem[0]
item.plex_type = plex_dbitem[2]
item.plex_UUID = plex_dbitem[0] # we dont need the uuid yet :-)
item.plex_uuid = plex_dbitem[0] # we dont need the uuid yet :-)
except TypeError:
pass
item.file = kodi_item.get('file')
@ -214,7 +215,7 @@ def playlist_item_from_kodi(kodi_item):
else:
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, item.plex_id))
(item.plex_uuid, item.plex_id))
LOG.debug('Made playlist item from Kodi: %s', item)
return item
@ -233,11 +234,11 @@ def playlist_item_from_plex(plex_id):
item.plex_type = plex_dbitem[5]
item.kodi_id = plex_dbitem[0]
item.kodi_type = plex_dbitem[4]
except:
except (TypeError, IndexError):
raise KeyError('Could not find plex_id %s in database' % plex_id)
item.plex_UUID = plex_id
item.plex_uuid = plex_id
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
(item.plex_UUID, plex_id))
(item.plex_uuid, plex_id))
LOG.debug('Made playlist item from plex: %s', item)
return item
@ -335,6 +336,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
Returns True if successful, False otherwise
"""
LOG.debug('Initializing the playlist %s on the Plex side', playlist)
playlist.clear()
try:
if plex_id:
item = playlist_item_from_plex(plex_id)
@ -349,13 +351,14 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
action_type="POST",
parameters=params)
get_playlist_details_from_xml(playlist, xml)
item.xml = xml[0]
# Need to get the details for the playlist item
item = playlist_item_from_xml(playlist, xml[0])
except (KeyError, IndexError, TypeError):
LOG.error('Could not init Plex playlist with plex_id %s and '
'kodi_item %s', plex_id, kodi_item)
return False
playlist.items.append(item)
LOG.debug('Initialized the playlist on the Plex side: %s' % playlist)
LOG.debug('Initialized the playlist on the Plex side: %s', playlist)
return True

View File

@ -206,8 +206,7 @@ class Playqueue(Thread):
LOG.info("----===## Starting PlayQueue client ##===----")
# Initialize the playqueues, if Kodi already got items in them
for playqueue in self.playqueues:
for i, item in enumerate(js.playlist_get_items(
playqueue.id, ["title", "file"])):
for i, item in enumerate(js.playlist_get_items(playqueue.id)):
if i == 0:
PL.init_Plex_playlist(playqueue, kodi_item=item)
else:
@ -217,17 +216,16 @@ class Playqueue(Thread):
if thread_stopped():
break
sleep(1000)
with LOCK:
for playqueue in self.playqueues:
kodi_playqueue = js.playlist_get_items(playqueue.id,
["title", "file"])
if playqueue.old_kodi_pl != kodi_playqueue:
# compare old and new playqueue
self._compare_playqueues(playqueue, kodi_playqueue)
playqueue.old_kodi_pl = list(kodi_playqueue)
# Still sleep a bit so Kodi does not become
# unresponsive
sleep(10)
continue
# with LOCK:
# for playqueue in self.playqueues:
# kodi_playqueue = js.playlist_get_items(playqueue.id)
# if playqueue.old_kodi_pl != kodi_playqueue:
# # compare old and new playqueue
# self._compare_playqueues(playqueue, kodi_playqueue)
# playqueue.old_kodi_pl = list(kodi_playqueue)
# # Still sleep a bit so Kodi does not become
# # unresponsive
# sleep(10)
# continue
sleep(200)
LOG.info("----===## PlayQueue client stopped ##===----")

View File

@ -1,4 +1,4 @@
import logging
from logging import getLogger
import httplib
import traceback
import string
@ -7,7 +7,7 @@ from socket import error as socket_error
###############################################################################
log = logging.getLogger("PLEX."+__name__)
LOG = getLogger("PLEX." + __name__)
###############################################################################
@ -17,20 +17,20 @@ class RequestMgr:
self.conns = {}
def getConnection(self, protocol, host, port):
conn = self.conns.get(protocol+host+str(port), False)
conn = self.conns.get(protocol + host + str(port), False)
if not conn:
if protocol == "https":
conn = httplib.HTTPSConnection(host, port)
else:
conn = httplib.HTTPConnection(host, port)
self.conns[protocol+host+str(port)] = conn
self.conns[protocol + host + str(port)] = conn
return conn
def closeConnection(self, protocol, host, port):
conn = self.conns.get(protocol+host+str(port), False)
conn = self.conns.get(protocol + host + str(port), False)
if conn:
conn.close()
self.conns.pop(protocol+host+str(port), None)
self.conns.pop(protocol + host + str(port), None)
def dumpConnections(self):
for conn in self.conns.values():
@ -45,7 +45,7 @@ class RequestMgr:
conn.request("POST", path, body, header)
data = conn.getresponse()
if int(data.status) >= 400:
log.error("HTTP response error: %s" % str(data.status))
LOG.error("HTTP response error: %s" % str(data.status))
# this should return false, but I'm hacking it since iOS
# returns 404 no matter what
return data.read() or True
@ -56,14 +56,14 @@ class RequestMgr:
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
pass
else:
log.error("Unable to connect to %s\nReason:" % host)
log.error(traceback.print_exc())
self.conns.pop(protocol+host+str(port), None)
LOG.error("Unable to connect to %s\nReason:" % host)
LOG.error(traceback.print_exc())
self.conns.pop(protocol + host + str(port), None)
if conn:
conn.close()
return False
except Exception as e:
log.error("Exception encountered: %s" % e)
LOG.error("Exception encountered: %s", e)
# Close connection just in case
try:
conn.close()
@ -76,7 +76,7 @@ class RequestMgr:
newpath = path + '?'
pairs = []
for key in params:
pairs.append(str(key)+'='+str(params[key]))
pairs.append(str(key) + '=' + str(params[key]))
newpath += string.join(pairs, '&')
return self.get(host, port, newpath, header, protocol)
@ -87,7 +87,7 @@ class RequestMgr:
conn.request("GET", path, headers=header)
data = conn.getresponse()
if int(data.status) >= 400:
log.error("HTTP response error: %s" % str(data.status))
LOG.error("HTTP response error: %s", str(data.status))
return False
else:
return data.read() or True
@ -96,8 +96,8 @@ class RequestMgr:
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
pass
else:
log.error("Unable to connect to %s\nReason:" % host)
log.error(traceback.print_exc())
self.conns.pop(protocol+host+str(port), None)
LOG.error("Unable to connect to %s\nReason:", host)
LOG.error(traceback.print_exc())
self.conns.pop(protocol + host + str(port), None)
conn.close()
return False

View File

@ -4,10 +4,11 @@ subscribed Plex Companion clients.
"""
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 utils import window, kodi_time_to_millis
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
@ -15,6 +16,9 @@ import json_rpc as js
###############################################################################
LOG = getLogger("PLEX." + __name__)
# Need to lock all methods and functions messing with subscribers or state
LOCK = Lock()
LOCKER = Lock_Function(LOCK)
###############################################################################
@ -48,6 +52,7 @@ class SubscriptionMgr(object):
self.server = ""
self.protocol = "http"
self.port = ""
self.isplaying = False
# In order to be able to signal a stop at the end
self.last_params = {}
self.lastplayers = {}
@ -79,6 +84,7 @@ class SubscriptionMgr(object):
return server
return {}
@LOCKER.lockthis
def msg(self, players):
"""
Returns a timeline xml as str
@ -94,7 +100,7 @@ class SubscriptionMgr(object):
msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO),
v.PLEX_TYPE_VIDEO)
msg += "</MediaContainer>"
LOG.debug('msg is: %s', msg)
LOG.debug('Our PKC message is: %s', msg)
return msg
def signal_stop(self):
@ -125,9 +131,9 @@ class SubscriptionMgr(object):
state.PLAYER_STATES[playerid]['plex_id']
return key
def _kodi_stream_index(self, playerid, stream_type):
def _plex_stream_index(self, playerid, stream_type):
"""
Returns the current Kodi stream index [int] for the player playerid
Returns the current Plex stream index [str] for the player playerid
stream_type: 'video', 'audio', 'subtitle'
"""
@ -136,18 +142,34 @@ 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']
# 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()
# Get the message together to send to Plex
info = state.PLAYER_STATES[playerid]
LOG.debug('timeline player state: %s', info)
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]
@ -171,8 +193,6 @@ class SubscriptionMgr(object):
ret += ' key="/library/metadata/%s"' % info['plex_id']
ret += ' ratingKey="%s"' % info['plex_id']
# PlayQueue stuff
playqueue = self.playqueue.playqueues[playerid]
pos = info['position']
key = self._get_container_key(playerid)
if key is not None and key.startswith('/playQueues'):
self.container_key = key
@ -193,25 +213,26 @@ class SubscriptionMgr(object):
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
ret += ' token="%s"' % playqueue.plex_transient_token
# Might need an update in the future
# Process audio and subtitle streams
if ptype != v.KODI_TYPE_PHOTO:
strm_id = self._kodi_stream_index(playerid, 'audio')
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._kodi_stream_index(playerid, 'subtitle')
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
ret += '/>\n'
return ret
self.isplaying = True
return ret + '/>\n'
@LOCKER.lockthis
def update_command_id(self, uuid, command_id):
"""
Updates the Plex Companien client with the machine identifier uuid with
@ -225,18 +246,22 @@ class SubscriptionMgr(object):
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
self._cleanup()
with LOCK:
self._cleanup()
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
players = js.get_players()
# fetch the message, subscribers or not, since the server
# will need the info anyway
# fetch the message, subscribers or not, since the server will need the
# info anyway
self.isplaying = False
msg = self.msg(players)
if self.subscribers:
with RLock():
with LOCK:
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
self._notify_server(players)
self.lastplayers = players
return True
def _notify_server(self, players):
@ -280,14 +305,16 @@ class SubscriptionMgr(object):
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
elif playqueue.plex_transient_token:
xargs['X-Plex-Token'] = playqueue.plex_transient_token
elif state.PLEX_TOKEN:
xargs['X-Plex-Token'] = state.PLEX_TOKEN
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'),
serv.get('port', '32400'))
DU().downloadUrl(url, parameters=params, headerOptions=xargs)
# Save to be able to signal a stop at the end
LOG.debug("Sent server notification with parameters: %s to %s",
params, url)
@LOCKER.lockthis
def add_subscriber(self, protocol, host, port, uuid, command_id):
"""
Adds a new Plex Companion subscriber to PKC.
@ -299,28 +326,26 @@ class SubscriptionMgr(object):
command_id,
self,
self.request_mgr)
with RLock():
self.subscribers[subscriber.uuid] = subscriber
self.subscribers[subscriber.uuid] = subscriber
return subscriber
@LOCKER.lockthis
def remove_subscriber(self, uuid):
"""
Removes a connected Plex Companion subscriber with machine identifier
uuid from PKC notifications.
(Calls the cleanup() method of the subscriber)
"""
with RLock():
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
def _cleanup(self):
with RLock():
for subscriber in self.subscribers.values():
if subscriber.age > 30:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
for subscriber in self.subscribers.values():
if subscriber.age > 30:
subscriber.cleanup()
del self.subscribers[subscriber.uuid]
class Subscriber(object):

View File

@ -227,8 +227,7 @@ class Plex_DB_Functions():
'''
try:
self.plexcursor.execute(query, (plex_id,))
item = self.plexcursor.fetchone()
return item
return self.plexcursor.fetchone()
except:
return None

View File

@ -1079,7 +1079,7 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None):
return cls
class Lock_Function:
class Lock_Function(object):
"""
Decorator for class methods and functions to lock them with lock.