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