Major Plex Companion overhaul, part 4
This commit is contained in:
parent
47779bbbee
commit
4547ec52af
10 changed files with 365 additions and 240 deletions
|
@ -11,6 +11,7 @@ from xbmc import sleep, executebuiltin
|
||||||
|
|
||||||
from utils import settings, thread_methods
|
from utils import settings, thread_methods
|
||||||
from plexbmchelper import listener, plexgdm, subscribers, httppersist
|
from plexbmchelper import listener, plexgdm, subscribers, httppersist
|
||||||
|
from plexbmchelper.subscribers import LOCKER
|
||||||
from PlexFunctions import ParseContainerKey, GetPlexMetadata
|
from PlexFunctions import ParseContainerKey, GetPlexMetadata
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
from playlist_func import get_pms_playqueue, get_plextype_from_xml
|
from playlist_func import get_pms_playqueue, get_plextype_from_xml
|
||||||
|
@ -44,8 +45,127 @@ class PlexCompanion(Thread):
|
||||||
self.player = player.PKC_Player()
|
self.player = player.PKC_Player()
|
||||||
self.httpd = False
|
self.httpd = False
|
||||||
self.queue = None
|
self.queue = None
|
||||||
|
self.subscription_manager = None
|
||||||
Thread.__init__(self)
|
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):
|
def _process_tasks(self, task):
|
||||||
"""
|
"""
|
||||||
Processes tasks picked up e.g. by Companion listener, e.g.
|
Processes tasks picked up e.g. by Companion listener, e.g.
|
||||||
|
@ -63,128 +183,25 @@ class PlexCompanion(Thread):
|
||||||
"""
|
"""
|
||||||
LOG.debug('Processing: %s', task)
|
LOG.debug('Processing: %s', task)
|
||||||
data = task['data']
|
data = task['data']
|
||||||
|
|
||||||
# Get the token of the user flinging media (might be different one)
|
|
||||||
token = data.get('token')
|
|
||||||
if task['action'] == 'alexa':
|
if task['action'] == 'alexa':
|
||||||
# e.g. Alexa
|
self._process_alexa(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 = 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)))
|
|
||||||
|
|
||||||
elif (task['action'] == 'playlist' and
|
elif (task['action'] == 'playlist' and
|
||||||
data.get('address') == 'node.plexapp.com'):
|
data.get('address') == 'node.plexapp.com'):
|
||||||
# E.g. watch later initiated by Companion
|
self._process_node(data)
|
||||||
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)))
|
|
||||||
|
|
||||||
elif task['action'] == 'playlist':
|
elif task['action'] == 'playlist':
|
||||||
# Get the playqueue ID
|
self._process_playlist(data)
|
||||||
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
|
|
||||||
|
|
||||||
elif task['action'] == 'refreshPlayQueue':
|
elif task['action'] == 'refreshPlayQueue':
|
||||||
# example data: {'playQueueID': '8475', 'commandID': '11'}
|
self._process_refresh(data)
|
||||||
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'])
|
|
||||||
|
|
||||||
elif task['action'] == 'setStreams':
|
elif task['action'] == 'setStreams':
|
||||||
# Plex Companion client adjusted audio or subtitle stream
|
self._process_streams(data)
|
||||||
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)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Ensure that
|
Ensure that sockets will be closed no matter what
|
||||||
- STOP sent to PMS
|
|
||||||
- sockets will be closed no matter what
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
finally:
|
finally:
|
||||||
self.subscription_manager.signal_stop()
|
|
||||||
try:
|
try:
|
||||||
self.httpd.socket.shutdown(SHUT_RDWR)
|
self.httpd.socket.shutdown(SHUT_RDWR)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -288,4 +305,5 @@ class PlexCompanion(Thread):
|
||||||
# Don't sleep
|
# Don't sleep
|
||||||
continue
|
continue
|
||||||
sleep(50)
|
sleep(50)
|
||||||
|
self.subscription_manager.signal_stop()
|
||||||
client.stop_all()
|
client.stop_all()
|
||||||
|
|
|
@ -58,7 +58,7 @@ def process_command(request_path, params, queue=None):
|
||||||
if params.get('deviceName') == 'Alexa':
|
if params.get('deviceName') == 'Alexa':
|
||||||
convert_alexa_to_companion(params)
|
convert_alexa_to_companion(params)
|
||||||
LOG.debug('Received request_path: %s, params: %s', request_path, params)
|
LOG.debug('Received request_path: %s, params: %s', request_path, params)
|
||||||
if "/playMedia" in request_path:
|
if request_path == 'player/playback/playMedia':
|
||||||
# We need to tell service.py
|
# We need to tell service.py
|
||||||
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
||||||
queue.put({
|
queue.put({
|
||||||
|
|
|
@ -247,20 +247,23 @@ def input_sendtext(text):
|
||||||
return JsonRPC("Input.SendText").execute({'test': text, 'done': False})
|
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
|
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
|
Returns a list of Kodi playlist items as dicts with the keys specified in
|
||||||
properties. Or an empty list if unsuccessful. Example:
|
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({
|
reply = JsonRPC('Playlist.GetItems').execute({
|
||||||
'playlistid': playlistid,
|
'playlistid': playlistid,
|
||||||
'properties': properties
|
'properties': ['title', 'file']
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
reply = reply['result']['items']
|
reply = reply['result']['items']
|
||||||
|
|
|
@ -10,8 +10,10 @@ import plexdb_functions as plexdb
|
||||||
from utils import window, settings, CatchExceptions, plex_command
|
from utils import window, settings, CatchExceptions, plex_command
|
||||||
from PlexFunctions import scrobble
|
from PlexFunctions import scrobble
|
||||||
from kodidb_functions import kodiid_from_filename
|
from kodidb_functions import kodiid_from_filename
|
||||||
|
from plexbmchelper.subscribers import LOCKER
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
import json_rpc as js
|
import json_rpc as js
|
||||||
|
import playlist_func as PL
|
||||||
import state
|
import state
|
||||||
import variables as v
|
import variables as v
|
||||||
|
|
||||||
|
@ -124,17 +126,20 @@ class KodiMonitor(Monitor):
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
|
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
# Should refresh our video nodes, e.g. on deck
|
# Should refresh our video nodes, e.g. on deck
|
||||||
# xbmc.executebuiltin('ReloadSkin()')
|
# xbmc.executebuiltin('ReloadSkin()')
|
||||||
pass
|
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":
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
# Manually marking as watched/unwatched
|
# Manually marking as watched/unwatched
|
||||||
playcount = data.get('playcount')
|
playcount = data.get('playcount')
|
||||||
item = data.get('item')
|
item = data.get('item')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kodiid = item['id']
|
kodiid = item['id']
|
||||||
item_type = item['type']
|
item_type = item['type']
|
||||||
|
@ -161,30 +166,84 @@ class KodiMonitor(Monitor):
|
||||||
scrobble(itemid, 'watched')
|
scrobble(itemid, 'watched')
|
||||||
else:
|
else:
|
||||||
scrobble(itemid, 'unwatched')
|
scrobble(itemid, 'unwatched')
|
||||||
|
|
||||||
elif method == "VideoLibrary.OnRemove":
|
elif method == "VideoLibrary.OnRemove":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif method == "System.OnSleep":
|
elif method == "System.OnSleep":
|
||||||
# Connection is going to sleep
|
# Connection is going to sleep
|
||||||
LOG.info("Marking the server as offline. SystemOnSleep activated.")
|
LOG.info("Marking the server as offline. SystemOnSleep activated.")
|
||||||
window('plex_online', value="sleep")
|
window('plex_online', value="sleep")
|
||||||
|
|
||||||
elif method == "System.OnWake":
|
elif method == "System.OnWake":
|
||||||
# Allow network to wake up
|
# Allow network to wake up
|
||||||
sleep(10000)
|
sleep(10000)
|
||||||
window('plex_onWake', value="true")
|
window('plex_onWake', value="true")
|
||||||
window('plex_online', value="false")
|
window('plex_online', value="false")
|
||||||
|
|
||||||
elif method == "GUI.OnScreensaverDeactivated":
|
elif method == "GUI.OnScreensaverDeactivated":
|
||||||
if settings('dbSyncScreensaver') == "true":
|
if settings('dbSyncScreensaver') == "true":
|
||||||
sleep(5000)
|
sleep(5000)
|
||||||
plex_command('RUN_LIB_SCAN', 'full')
|
plex_command('RUN_LIB_SCAN', 'full')
|
||||||
|
|
||||||
elif method == "System.OnQuit":
|
elif method == "System.OnQuit":
|
||||||
LOG.info('Kodi OnQuit detected - shutting down')
|
LOG.info('Kodi OnQuit detected - shutting down')
|
||||||
state.STOP_PKC = True
|
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):
|
def PlayBackStart(self, data):
|
||||||
"""
|
"""
|
||||||
Called whenever playback is started. Example 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'item': {u'type': u'movie', u'title': u''},
|
||||||
u'player': {u'playerid': 1, u'speed': 1}
|
u'player': {u'playerid': 1, u'speed': 1}
|
||||||
}
|
}
|
||||||
Unfortunately VERY random inputs!
|
Unfortunately when using Widgets, Kodi doesn't tell us shit
|
||||||
E.g. when using Widgets, Kodi doesn't tell us shit
|
|
||||||
"""
|
"""
|
||||||
# Get the type of media we're playing
|
# Get the type of media we're playing
|
||||||
try:
|
try:
|
||||||
|
@ -237,16 +295,37 @@ class KodiMonitor(Monitor):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# No plex id, hence item not in the library. E.g. clips
|
# No plex id, hence item not in the library. E.g. clips
|
||||||
pass
|
pass
|
||||||
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
info = js.get_player_props(playerid)
|
||||||
state.PLAYER_STATES[playerid]['file'] = json_data['file']
|
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_id'] = kodi_id
|
||||||
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
|
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
|
||||||
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
|
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
|
||||||
state.PLAYER_STATES[playerid]['plex_type'] = plex_type
|
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])
|
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):
|
def StartDirectPath(self, plex_id, type, currentFile):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,22 +25,23 @@ REGEX = re_compile(r'''metadata%2F(\d+)''')
|
||||||
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
|
# {u'type': u'movie', u'id': 3, 'file': path-to-file}
|
||||||
|
|
||||||
|
|
||||||
class Playlist_Object_Baseclase(object):
|
class PlaylistObjectBaseclase(object):
|
||||||
"""
|
"""
|
||||||
Base class
|
Base class
|
||||||
"""
|
"""
|
||||||
playlistid = None
|
def __init__(self):
|
||||||
type = None
|
self.playlistid = None
|
||||||
kodi_pl = None
|
self.type = None
|
||||||
items = []
|
self.kodi_pl = None
|
||||||
old_kodi_pl = []
|
self.items = []
|
||||||
id = None
|
self.old_kodi_pl = []
|
||||||
version = None
|
self.id = None
|
||||||
selectedItemID = None
|
self.version = None
|
||||||
selectedItemOffset = None
|
self.selectedItemID = None
|
||||||
shuffled = 0
|
self.selectedItemOffset = None
|
||||||
repeat = 0
|
self.shuffled = 0
|
||||||
plex_transient_token = None
|
self.repeat = 0
|
||||||
|
self.plex_transient_token = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -76,14 +77,14 @@ class Playlist_Object_Baseclase(object):
|
||||||
LOG.debug('Playlist cleared: %s', self)
|
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
|
To be done for synching Plex playlists to Kodi
|
||||||
"""
|
"""
|
||||||
kind = 'playList'
|
kind = 'playList'
|
||||||
|
|
||||||
|
|
||||||
class Playqueue_Object(Playlist_Object_Baseclase):
|
class Playqueue_Object(PlaylistObjectBaseclase):
|
||||||
"""
|
"""
|
||||||
PKC object to represent PMS playQueues and Kodi playlist for queueing
|
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
|
id = None [str] Plex playlist/playqueue id, e.g. playQueueItemID
|
||||||
plex_id = None [str] Plex unique item id, "ratingKey"
|
plex_id = None [str] Plex unique item id, "ratingKey"
|
||||||
plex_type = None [str] Plex type, e.g. 'movie', 'clip'
|
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_id = None Kodi unique kodi id (unique only within type!)
|
||||||
kodi_type = None [str] Kodi type: 'movie'
|
kodi_type = None [str] Kodi type: 'movie'
|
||||||
file = None [str] Path to the item's file. STRING!!
|
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
|
guid = None [str] Weird Plex guid
|
||||||
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
|
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
|
||||||
"""
|
"""
|
||||||
id = None
|
def __init__(self):
|
||||||
plex_id = None
|
self.id = None
|
||||||
plex_type = None
|
self.plex_id = None
|
||||||
plex_UUID = None
|
self.plex_type = None
|
||||||
kodi_id = None
|
self.plex_uuid = None
|
||||||
kodi_type = None
|
self.kodi_id = None
|
||||||
file = None
|
self.kodi_type = None
|
||||||
uri = None
|
self.file = None
|
||||||
guid = None
|
self.uri = None
|
||||||
xml = None
|
self.guid = None
|
||||||
|
self.xml = None
|
||||||
# Yet to be implemented: handling of a movie with several parts
|
# Yet to be implemented: handling of a movie with several parts
|
||||||
part = 0
|
self.part = 0
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -201,7 +202,7 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
try:
|
try:
|
||||||
item.plex_id = plex_dbitem[0]
|
item.plex_id = plex_dbitem[0]
|
||||||
item.plex_type = plex_dbitem[2]
|
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:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
item.file = kodi_item.get('file')
|
item.file = kodi_item.get('file')
|
||||||
|
@ -214,7 +215,7 @@ def playlist_item_from_kodi(kodi_item):
|
||||||
else:
|
else:
|
||||||
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
# TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER
|
||||||
item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' %
|
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)
|
LOG.debug('Made playlist item from Kodi: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
@ -233,11 +234,11 @@ def playlist_item_from_plex(plex_id):
|
||||||
item.plex_type = plex_dbitem[5]
|
item.plex_type = plex_dbitem[5]
|
||||||
item.kodi_id = plex_dbitem[0]
|
item.kodi_id = plex_dbitem[0]
|
||||||
item.kodi_type = plex_dbitem[4]
|
item.kodi_type = plex_dbitem[4]
|
||||||
except:
|
except (TypeError, IndexError):
|
||||||
raise KeyError('Could not find plex_id %s in database' % plex_id)
|
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.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)
|
LOG.debug('Made playlist item from plex: %s', item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
@ -335,6 +336,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
|
||||||
Returns True if successful, False otherwise
|
Returns True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
LOG.debug('Initializing the playlist %s on the Plex side', playlist)
|
LOG.debug('Initializing the playlist %s on the Plex side', playlist)
|
||||||
|
playlist.clear()
|
||||||
try:
|
try:
|
||||||
if plex_id:
|
if plex_id:
|
||||||
item = playlist_item_from_plex(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",
|
action_type="POST",
|
||||||
parameters=params)
|
parameters=params)
|
||||||
get_playlist_details_from_xml(playlist, xml)
|
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):
|
except (KeyError, IndexError, TypeError):
|
||||||
LOG.error('Could not init Plex playlist with plex_id %s and '
|
LOG.error('Could not init Plex playlist with plex_id %s and '
|
||||||
'kodi_item %s', plex_id, kodi_item)
|
'kodi_item %s', plex_id, kodi_item)
|
||||||
return False
|
return False
|
||||||
playlist.items.append(item)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -206,8 +206,7 @@ class Playqueue(Thread):
|
||||||
LOG.info("----===## Starting PlayQueue client ##===----")
|
LOG.info("----===## Starting PlayQueue client ##===----")
|
||||||
# Initialize the playqueues, if Kodi already got items in them
|
# Initialize the playqueues, if Kodi already got items in them
|
||||||
for playqueue in self.playqueues:
|
for playqueue in self.playqueues:
|
||||||
for i, item in enumerate(js.playlist_get_items(
|
for i, item in enumerate(js.playlist_get_items(playqueue.id)):
|
||||||
playqueue.id, ["title", "file"])):
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
PL.init_Plex_playlist(playqueue, kodi_item=item)
|
PL.init_Plex_playlist(playqueue, kodi_item=item)
|
||||||
else:
|
else:
|
||||||
|
@ -217,17 +216,16 @@ class Playqueue(Thread):
|
||||||
if thread_stopped():
|
if thread_stopped():
|
||||||
break
|
break
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
with LOCK:
|
# with LOCK:
|
||||||
for playqueue in self.playqueues:
|
# for playqueue in self.playqueues:
|
||||||
kodi_playqueue = js.playlist_get_items(playqueue.id,
|
# kodi_playqueue = js.playlist_get_items(playqueue.id)
|
||||||
["title", "file"])
|
# if playqueue.old_kodi_pl != kodi_playqueue:
|
||||||
if playqueue.old_kodi_pl != kodi_playqueue:
|
# # compare old and new playqueue
|
||||||
# compare old and new playqueue
|
# self._compare_playqueues(playqueue, kodi_playqueue)
|
||||||
self._compare_playqueues(playqueue, kodi_playqueue)
|
# playqueue.old_kodi_pl = list(kodi_playqueue)
|
||||||
playqueue.old_kodi_pl = list(kodi_playqueue)
|
# # Still sleep a bit so Kodi does not become
|
||||||
# Still sleep a bit so Kodi does not become
|
# # unresponsive
|
||||||
# unresponsive
|
# sleep(10)
|
||||||
sleep(10)
|
# continue
|
||||||
continue
|
|
||||||
sleep(200)
|
sleep(200)
|
||||||
LOG.info("----===## PlayQueue client stopped ##===----")
|
LOG.info("----===## PlayQueue client stopped ##===----")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import logging
|
from logging import getLogger
|
||||||
import httplib
|
import httplib
|
||||||
import traceback
|
import traceback
|
||||||
import string
|
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 = {}
|
self.conns = {}
|
||||||
|
|
||||||
def getConnection(self, protocol, host, port):
|
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 not conn:
|
||||||
if protocol == "https":
|
if protocol == "https":
|
||||||
conn = httplib.HTTPSConnection(host, port)
|
conn = httplib.HTTPSConnection(host, port)
|
||||||
else:
|
else:
|
||||||
conn = httplib.HTTPConnection(host, port)
|
conn = httplib.HTTPConnection(host, port)
|
||||||
self.conns[protocol+host+str(port)] = conn
|
self.conns[protocol + host + str(port)] = conn
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def closeConnection(self, protocol, host, port):
|
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:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
|
|
||||||
def dumpConnections(self):
|
def dumpConnections(self):
|
||||||
for conn in self.conns.values():
|
for conn in self.conns.values():
|
||||||
|
@ -45,7 +45,7 @@ class RequestMgr:
|
||||||
conn.request("POST", path, body, header)
|
conn.request("POST", path, body, header)
|
||||||
data = conn.getresponse()
|
data = conn.getresponse()
|
||||||
if int(data.status) >= 400:
|
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
|
# this should return false, but I'm hacking it since iOS
|
||||||
# returns 404 no matter what
|
# returns 404 no matter what
|
||||||
return data.read() or True
|
return data.read() or True
|
||||||
|
@ -56,14 +56,14 @@ class RequestMgr:
|
||||||
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.error("Unable to connect to %s\nReason:" % host)
|
LOG.error("Unable to connect to %s\nReason:" % host)
|
||||||
log.error(traceback.print_exc())
|
LOG.error(traceback.print_exc())
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
conn.close()
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Exception encountered: %s" % e)
|
LOG.error("Exception encountered: %s", e)
|
||||||
# Close connection just in case
|
# Close connection just in case
|
||||||
try:
|
try:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
@ -76,7 +76,7 @@ class RequestMgr:
|
||||||
newpath = path + '?'
|
newpath = path + '?'
|
||||||
pairs = []
|
pairs = []
|
||||||
for key in params:
|
for key in params:
|
||||||
pairs.append(str(key)+'='+str(params[key]))
|
pairs.append(str(key) + '=' + str(params[key]))
|
||||||
newpath += string.join(pairs, '&')
|
newpath += string.join(pairs, '&')
|
||||||
return self.get(host, port, newpath, header, protocol)
|
return self.get(host, port, newpath, header, protocol)
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class RequestMgr:
|
||||||
conn.request("GET", path, headers=header)
|
conn.request("GET", path, headers=header)
|
||||||
data = conn.getresponse()
|
data = conn.getresponse()
|
||||||
if int(data.status) >= 400:
|
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
|
return False
|
||||||
else:
|
else:
|
||||||
return data.read() or True
|
return data.read() or True
|
||||||
|
@ -96,8 +96,8 @@ class RequestMgr:
|
||||||
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
if serr.errno in (errno.WSAECONNABORTED, errno.WSAECONNREFUSED):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
log.error("Unable to connect to %s\nReason:" % host)
|
LOG.error("Unable to connect to %s\nReason:", host)
|
||||||
log.error(traceback.print_exc())
|
LOG.error(traceback.print_exc())
|
||||||
self.conns.pop(protocol+host+str(port), None)
|
self.conns.pop(protocol + host + str(port), None)
|
||||||
conn.close()
|
conn.close()
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -4,10 +4,11 @@ subscribed Plex Companion clients.
|
||||||
"""
|
"""
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from re import sub
|
from re import sub
|
||||||
from threading import Thread, RLock
|
from threading import Thread, Lock
|
||||||
|
|
||||||
from downloadutils import DownloadUtils as DU
|
from downloadutils import DownloadUtils as DU
|
||||||
from utils import window, kodi_time_to_millis
|
from utils import window, kodi_time_to_millis, Lock_Function
|
||||||
|
from playlist_func import init_Plex_playlist
|
||||||
import state
|
import state
|
||||||
import variables as v
|
import variables as v
|
||||||
import json_rpc as js
|
import json_rpc as js
|
||||||
|
@ -15,6 +16,9 @@ import json_rpc as js
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
LOG = getLogger("PLEX." + __name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
# Need to lock all methods and functions messing with subscribers or state
|
||||||
|
LOCK = Lock()
|
||||||
|
LOCKER = Lock_Function(LOCK)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -48,6 +52,7 @@ class SubscriptionMgr(object):
|
||||||
self.server = ""
|
self.server = ""
|
||||||
self.protocol = "http"
|
self.protocol = "http"
|
||||||
self.port = ""
|
self.port = ""
|
||||||
|
self.isplaying = False
|
||||||
# In order to be able to signal a stop at the end
|
# In order to be able to signal a stop at the end
|
||||||
self.last_params = {}
|
self.last_params = {}
|
||||||
self.lastplayers = {}
|
self.lastplayers = {}
|
||||||
|
@ -79,6 +84,7 @@ class SubscriptionMgr(object):
|
||||||
return server
|
return server
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
def msg(self, players):
|
def msg(self, players):
|
||||||
"""
|
"""
|
||||||
Returns a timeline xml as str
|
Returns a timeline xml as str
|
||||||
|
@ -94,7 +100,7 @@ class SubscriptionMgr(object):
|
||||||
msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO),
|
msg += self._timeline_xml(players.get(v.KODI_TYPE_VIDEO),
|
||||||
v.PLEX_TYPE_VIDEO)
|
v.PLEX_TYPE_VIDEO)
|
||||||
msg += "</MediaContainer>"
|
msg += "</MediaContainer>"
|
||||||
LOG.debug('msg is: %s', msg)
|
LOG.debug('Our PKC message is: %s', msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def signal_stop(self):
|
def signal_stop(self):
|
||||||
|
@ -125,9 +131,9 @@ class SubscriptionMgr(object):
|
||||||
state.PLAYER_STATES[playerid]['plex_id']
|
state.PLAYER_STATES[playerid]['plex_id']
|
||||||
return key
|
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'
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
"""
|
"""
|
||||||
|
@ -136,18 +142,34 @@ class SubscriptionMgr(object):
|
||||||
return playqueue.items[info['position']].plex_stream_index(
|
return playqueue.items[info['position']].plex_stream_index(
|
||||||
info[STREAM_DETAILS[stream_type]]['index'], stream_type)
|
info[STREAM_DETAILS[stream_type]]['index'], stream_type)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _player_info(playerid):
|
||||||
|
"""
|
||||||
|
Grabs all player info again for playerid [int].
|
||||||
|
Returns the dict state.PLAYER_STATES[playerid]
|
||||||
|
"""
|
||||||
|
# Update our PKC state of how the player actually looks like
|
||||||
|
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
||||||
|
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
||||||
|
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
||||||
|
return state.PLAYER_STATES[playerid]
|
||||||
|
|
||||||
def _timeline_xml(self, player, ptype):
|
def _timeline_xml(self, player, ptype):
|
||||||
if player is None:
|
if player is None:
|
||||||
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
|
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
|
||||||
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
|
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
|
||||||
playerid = player['playerid']
|
playerid = player['playerid']
|
||||||
# Update our PKC state of how the player actually looks like
|
info = self._player_info(playerid)
|
||||||
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
playqueue = self.playqueue.playqueues[playerid]
|
||||||
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
pos = info['position']
|
||||||
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
try:
|
||||||
# Get the message together to send to Plex
|
playqueue.items[pos]
|
||||||
info = state.PLAYER_STATES[playerid]
|
except IndexError:
|
||||||
LOG.debug('timeline player state: %s', info)
|
# 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'
|
status = 'paused' if info['speed'] == '0' else 'playing'
|
||||||
ret = ' <Timeline state="%s"' % status
|
ret = ' <Timeline state="%s"' % status
|
||||||
ret += ' controllable="%s"' % CONTROLLABLE[ptype]
|
ret += ' controllable="%s"' % CONTROLLABLE[ptype]
|
||||||
|
@ -171,8 +193,6 @@ class SubscriptionMgr(object):
|
||||||
ret += ' key="/library/metadata/%s"' % info['plex_id']
|
ret += ' key="/library/metadata/%s"' % info['plex_id']
|
||||||
ret += ' ratingKey="%s"' % info['plex_id']
|
ret += ' ratingKey="%s"' % info['plex_id']
|
||||||
# PlayQueue stuff
|
# PlayQueue stuff
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
|
||||||
pos = info['position']
|
|
||||||
key = self._get_container_key(playerid)
|
key = self._get_container_key(playerid)
|
||||||
if key is not None and key.startswith('/playQueues'):
|
if key is not None and key.startswith('/playQueues'):
|
||||||
self.container_key = key
|
self.container_key = key
|
||||||
|
@ -193,25 +213,26 @@ class SubscriptionMgr(object):
|
||||||
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
|
ret += ' token="%s"' % state.PLEX_TRANSIENT_TOKEN
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
ret += ' token="%s"' % 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:
|
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:
|
if strm_id is not None:
|
||||||
ret += ' audioStreamID="%s"' % strm_id
|
ret += ' audioStreamID="%s"' % strm_id
|
||||||
else:
|
else:
|
||||||
LOG.error('We could not select a Plex audiostream')
|
LOG.error('We could not select a Plex audiostream')
|
||||||
if ptype == v.KODI_TYPE_VIDEO and info['subtitleenabled']:
|
if ptype == v.KODI_TYPE_VIDEO and info['subtitleenabled']:
|
||||||
try:
|
try:
|
||||||
strm_id = self._kodi_stream_index(playerid, 'subtitle')
|
strm_id = self._plex_stream_index(playerid, 'subtitle')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# subtitleenabled can be True while currentsubtitle can be {}
|
# subtitleenabled can be True while currentsubtitle can be {}
|
||||||
strm_id = None
|
strm_id = None
|
||||||
if strm_id is not None:
|
if strm_id is not None:
|
||||||
# If None, then the subtitle is only present on Kodi side
|
# If None, then the subtitle is only present on Kodi side
|
||||||
ret += ' subtitleStreamID="%s"' % strm_id
|
ret += ' subtitleStreamID="%s"' % strm_id
|
||||||
ret += '/>\n'
|
self.isplaying = True
|
||||||
return ret
|
return ret + '/>\n'
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
def update_command_id(self, uuid, command_id):
|
def update_command_id(self, uuid, command_id):
|
||||||
"""
|
"""
|
||||||
Updates the Plex Companien client with the machine identifier uuid with
|
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
|
Causes PKC to tell the PMS and Plex Companion players to receive a
|
||||||
notification what's being played.
|
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?
|
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
|
||||||
players = js.get_players()
|
players = js.get_players()
|
||||||
# fetch the message, subscribers or not, since the server
|
# fetch the message, subscribers or not, since the server will need the
|
||||||
# will need the info anyway
|
# info anyway
|
||||||
|
self.isplaying = False
|
||||||
msg = self.msg(players)
|
msg = self.msg(players)
|
||||||
if self.subscribers:
|
with LOCK:
|
||||||
with RLock():
|
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():
|
for subscriber in self.subscribers.values():
|
||||||
subscriber.send_update(msg, not players)
|
subscriber.send_update(msg, not players)
|
||||||
self._notify_server(players)
|
self._notify_server(players)
|
||||||
self.lastplayers = players
|
self.lastplayers = players
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_server(self, players):
|
def _notify_server(self, players):
|
||||||
|
@ -280,14 +305,16 @@ class SubscriptionMgr(object):
|
||||||
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
xargs['X-Plex-Token'] = 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'),
|
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
||||||
serv.get('server', 'localhost'),
|
serv.get('server', 'localhost'),
|
||||||
serv.get('port', '32400'))
|
serv.get('port', '32400'))
|
||||||
DU().downloadUrl(url, parameters=params, headerOptions=xargs)
|
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",
|
LOG.debug("Sent server notification with parameters: %s to %s",
|
||||||
params, url)
|
params, url)
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
def add_subscriber(self, protocol, host, port, uuid, command_id):
|
def add_subscriber(self, protocol, host, port, uuid, command_id):
|
||||||
"""
|
"""
|
||||||
Adds a new Plex Companion subscriber to PKC.
|
Adds a new Plex Companion subscriber to PKC.
|
||||||
|
@ -299,28 +326,26 @@ class SubscriptionMgr(object):
|
||||||
command_id,
|
command_id,
|
||||||
self,
|
self,
|
||||||
self.request_mgr)
|
self.request_mgr)
|
||||||
with RLock():
|
self.subscribers[subscriber.uuid] = subscriber
|
||||||
self.subscribers[subscriber.uuid] = subscriber
|
|
||||||
return subscriber
|
return subscriber
|
||||||
|
|
||||||
|
@LOCKER.lockthis
|
||||||
def remove_subscriber(self, uuid):
|
def remove_subscriber(self, uuid):
|
||||||
"""
|
"""
|
||||||
Removes a connected Plex Companion subscriber with machine identifier
|
Removes a connected Plex Companion subscriber with machine identifier
|
||||||
uuid from PKC notifications.
|
uuid from PKC notifications.
|
||||||
(Calls the cleanup() method of the subscriber)
|
(Calls the cleanup() method of the subscriber)
|
||||||
"""
|
"""
|
||||||
with RLock():
|
for subscriber in self.subscribers.values():
|
||||||
for subscriber in self.subscribers.values():
|
if subscriber.uuid == uuid or subscriber.host == uuid:
|
||||||
if subscriber.uuid == uuid or subscriber.host == uuid:
|
subscriber.cleanup()
|
||||||
subscriber.cleanup()
|
del self.subscribers[subscriber.uuid]
|
||||||
del self.subscribers[subscriber.uuid]
|
|
||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
with RLock():
|
for subscriber in self.subscribers.values():
|
||||||
for subscriber in self.subscribers.values():
|
if subscriber.age > 30:
|
||||||
if subscriber.age > 30:
|
subscriber.cleanup()
|
||||||
subscriber.cleanup()
|
del self.subscribers[subscriber.uuid]
|
||||||
del self.subscribers[subscriber.uuid]
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriber(object):
|
class Subscriber(object):
|
||||||
|
|
|
@ -227,8 +227,7 @@ class Plex_DB_Functions():
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
self.plexcursor.execute(query, (plex_id,))
|
self.plexcursor.execute(query, (plex_id,))
|
||||||
item = self.plexcursor.fetchone()
|
return self.plexcursor.fetchone()
|
||||||
return item
|
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1079,7 +1079,7 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None):
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
class Lock_Function:
|
class Lock_Function(object):
|
||||||
"""
|
"""
|
||||||
Decorator for class methods and functions to lock them with lock.
|
Decorator for class methods and functions to lock them with lock.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue