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 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()

View file

@ -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({

View file

@ -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']

View file

@ -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):
""" """

View file

@ -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

View file

@ -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 ##===----")

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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.