diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py
index 89fd4b68..159ccc2d 100644
--- a/resources/lib/PlexCompanion.py
+++ b/resources/lib/PlexCompanion.py
@@ -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()
diff --git a/resources/lib/companion.py b/resources/lib/companion.py
index 08db67c6..19f78fa9 100644
--- a/resources/lib/companion.py
+++ b/resources/lib/companion.py
@@ -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({
diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py
index a8fa0f94..727ff9c5 100644
--- a/resources/lib/json_rpc.py
+++ b/resources/lib/json_rpc.py
@@ -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']
diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py
index 555740f5..48974327 100644
--- a/resources/lib/kodimonitor.py
+++ b/resources/lib/kodimonitor.py
@@ -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):
"""
diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py
index f5a39e96..a77b4796 100644
--- a/resources/lib/playlist_func.py
+++ b/resources/lib/playlist_func.py
@@ -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
"""
- 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
diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py
index cf6a5250..8afdd3e2 100644
--- a/resources/lib/playqueue.py
+++ b/resources/lib/playqueue.py
@@ -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 ##===----")
diff --git a/resources/lib/plexbmchelper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py
index e765ae9e..2d65bb60 100644
--- a/resources/lib/plexbmchelper/httppersist.py
+++ b/resources/lib/plexbmchelper/httppersist.py
@@ -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
diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py
index 2592f689..3876d73d 100644
--- a/resources/lib/plexbmchelper/subscribers.py
+++ b/resources/lib/plexbmchelper/subscribers.py
@@ -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 += ""
- 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 ' \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 ' \n' % (CONTROLLABLE[ptype], ptype, ptype)
+ LOG.debug('INFO: %s', info)
+ LOG.debug('playqueue: %s', playqueue)
status = 'paused' if info['speed'] == '0' else 'playing'
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):
diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py
index a08e0d40..239d25df 100644
--- a/resources/lib/plexdb_functions.py
+++ b/resources/lib/plexdb_functions.py
@@ -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
diff --git a/resources/lib/utils.py b/resources/lib/utils.py
index 79ca110a..6dfdd941 100644
--- a/resources/lib/utils.py
+++ b/resources/lib/utils.py
@@ -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.