diff --git a/resources/lib/app/__init__.py b/resources/lib/app/__init__.py
index 8243a64a..634fe004 100644
--- a/resources/lib/app/__init__.py
+++ b/resources/lib/app/__init__.py
@@ -9,12 +9,14 @@ from .application import App
from .connection import Connection
from .libsync import Sync
from .playstate import PlayState
+from .playqueues import Playqueues
ACCOUNT = None
APP = None
CONN = None
SYNC = None
PLAYSTATE = None
+PLAYQUEUES = None
def init(entrypoint=False):
@@ -22,13 +24,15 @@ def init(entrypoint=False):
entrypoint=True initiates only the bare minimum - for other PKC python
instances
"""
- global ACCOUNT, APP, CONN, SYNC, PLAYSTATE
+ global ACCOUNT, APP, CONN, SYNC, PLAYSTATE, PLAYQUEUES
APP = App(entrypoint)
CONN = Connection(entrypoint)
ACCOUNT = Account(entrypoint)
SYNC = Sync(entrypoint)
if not entrypoint:
PLAYSTATE = PlayState()
+ PLAYQUEUES = Playqueues()
+
def reload():
"""
diff --git a/resources/lib/app/playqueues.py b/resources/lib/app/playqueues.py
new file mode 100644
index 00000000..0f4b9c3f
--- /dev/null
+++ b/resources/lib/app/playqueues.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from logging import getLogger
+
+import xbmc
+
+from .. import variables as v
+
+
+LOG = getLogger('PLEX.playqueue')
+
+
+class Playqueue(object):
+ """
+ PKC object to represent PMS playQueues and Kodi playlist for queueing
+
+ playlistid = None [int] Kodi playlist id (0, 1, 2)
+ type = None [str] Kodi type: 'audio', 'video', 'picture'
+ kodi_pl = None Kodi xbmc.PlayList object
+ items = [] [list] of Playlist_Items
+ id = None [str] Plex playQueueID, unique Plex identifier
+ version = None [int] Plex version of the playQueue
+ selectedItemID = None
+ [str] Plex selectedItemID, playing element in queue
+ selectedItemOffset = None
+ [str] Offset of the playing element in queue
+ shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
+ repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
+
+ If Companion playback is initiated by another user:
+ plex_transient_token = None
+ """
+ kind = 'playQueue'
+
+ def __init__(self):
+ self.id = None
+ self.type = None
+ self.playlistid = None
+ self.kodi_pl = None
+ self.items = []
+ self.version = None
+ self.selectedItemID = None
+ self.selectedItemOffset = None
+ self.shuffled = 0
+ self.repeat = 0
+ self.plex_transient_token = None
+ # Need a hack for detecting swaps of elements
+ self.old_kodi_pl = []
+ # Did PKC itself just change the playqueue so the PKC playqueue monitor
+ # should not pick up any changes?
+ self.pkc_edit = False
+ # Workaround to avoid endless loops of detecting PL clears
+ self._clear_list = []
+ # To keep track if Kodi playback was initiated from a Kodi playlist
+ # There are a couple of pitfalls, unfortunately...
+ self.kodi_playlist_playback = False
+
+ def __repr__(self):
+ answ = ("{{"
+ "'playlistid': {self.playlistid}, "
+ "'id': {self.id}, "
+ "'version': {self.version}, "
+ "'type': '{self.type}', "
+ "'selectedItemID': {self.selectedItemID}, "
+ "'selectedItemOffset': {self.selectedItemOffset}, "
+ "'shuffled': {self.shuffled}, "
+ "'repeat': {self.repeat}, "
+ "'kodi_playlist_playback': {self.kodi_playlist_playback}, "
+ "'pkc_edit': {self.pkc_edit}, ".format(self=self))
+ # Since list.__repr__ will return string, not unicode
+ return answ + "'items': {self.items}}}".format(self=self)
+
+ def is_pkc_clear(self):
+ """
+ Returns True if PKC has cleared the Kodi playqueue just recently.
+ Then this clear will be ignored from now on
+ """
+ try:
+ self._clear_list.pop()
+ except IndexError:
+ return False
+ else:
+ return True
+
+ def clear(self, kodi=True):
+ """
+ Resets the playlist object to an empty playlist.
+
+ Pass kodi=False in order to NOT clear the Kodi playqueue
+ """
+ # kodi monitor's on_clear method will only be called if there were some
+ # items to begin with
+ if kodi and self.kodi_pl.size() != 0:
+ self._clear_list.append(None)
+ self.kodi_pl.clear() # Clear Kodi playlist object
+ self.items = []
+ self.id = None
+ self.version = None
+ self.selectedItemID = None
+ self.selectedItemOffset = None
+ self.shuffled = 0
+ self.repeat = 0
+ self.plex_transient_token = None
+ self.old_kodi_pl = []
+ self.kodi_playlist_playback = False
+ LOG.debug('Playlist cleared: %s', self)
+
+ def position_from_plex_id(self, plex_id):
+ """
+ Returns the position [int] for the very first item with plex_id [int]
+ (Plex seems uncapable of adding the same element multiple times to a
+ playqueue or playlist)
+
+ Raises KeyError if not found
+ """
+ for position, item in enumerate(self.items):
+ if item.plex_id == plex_id:
+ break
+ else:
+ raise KeyError('Did not find plex_id %s in %s', plex_id, self)
+ return position
+
+
+class Playqueues(list):
+
+ def __init__(self):
+ super().__init__()
+ for i, typus in enumerate((v.KODI_PLAYLIST_TYPE_AUDIO,
+ v.KODI_PLAYLIST_TYPE_VIDEO,
+ v.KODI_PLAYLIST_TYPE_PHOTO)):
+ playqueue = Playqueue()
+ playqueue.playlistid = i
+ playqueue.type = typus
+ # Initialize each Kodi playlist
+ if typus == v.KODI_PLAYLIST_TYPE_AUDIO:
+ playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
+ elif typus == v.KODI_PLAYLIST_TYPE_VIDEO:
+ playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ else:
+ # Currently, only video or audio playqueues available
+ playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ # Overwrite 'picture' with 'photo'
+ playqueue.type = v.KODI_TYPE_PHOTO
+ self.append(playqueue)
+
+ @property
+ def audio(self):
+ return self[0]
+
+ @property
+ def video(self):
+ return self[1]
+
+ @property
+ def photo(self):
+ return self[2]
+
+ def from_kodi_playlist_type(self, kodi_playlist_type):
+ """
+ Returns the playqueue according to the kodi_playlist_type ('video',
+ 'audio', 'picture') passed in
+ """
+ if kodi_playlist_type == v.KODI_PLAYLIST_TYPE_AUDIO:
+ return self[0]
+ elif kodi_playlist_type == v.KODI_PLAYLIST_TYPE_VIDEO:
+ return self[1]
+ elif kodi_playlist_type == v.KODI_PLAYLIST_TYPE_PHOTO:
+ return self[2]
+ else:
+ raise ValueError('Unknown kodi_playlist_type: %s' % kodi_playlist_type)
+
+ def from_kodi_type(self, kodi_type):
+ """
+ Pass in the kodi_type (e.g. the string 'movie') to get the correct
+ playqueue (either video, audio or picture)
+ """
+ if kodi_type == v.KODI_TYPE_VIDEO:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_MOVIE:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_EPISODE:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_SEASON:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_SHOW:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_CLIP:
+ return self[1]
+ elif kodi_type == v.KODI_TYPE_SONG:
+ return self[0]
+ elif kodi_type == v.KODI_TYPE_ALBUM:
+ return self[0]
+ elif kodi_type == v.KODI_TYPE_ARTIST:
+ return self[0]
+ elif kodi_type == v.KODI_TYPE_AUDIO:
+ return self[0]
+ elif kodi_type == v.KODI_TYPE_PHOTO:
+ return self[2]
+ else:
+ raise ValueError('Unknown kodi_type: %s' % kodi_type)
+
+ def from_plex_type(self, plex_type):
+ """
+ Pass in the plex_type (e.g. the string 'movie') to get the correct
+ playqueue (either video, audio or picture)
+ """
+ if plex_type == v.PLEX_TYPE_VIDEO:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_MOVIE:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_EPISODE:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_SEASON:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_SHOW:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_CLIP:
+ return self[1]
+ elif plex_type == v.PLEX_TYPE_SONG:
+ return self[0]
+ elif plex_type == v.PLEX_TYPE_ALBUM:
+ return self[0]
+ elif plex_type == v.PLEX_TYPE_ARTIST:
+ return self[0]
+ elif plex_type == v.PLEX_TYPE_AUDIO:
+ return self[0]
+ elif plex_type == v.PLEX_TYPE_PHOTO:
+ return self[2]
+ else:
+ raise ValueError('Unknown plex_type: %s' % plex_type)
diff --git a/resources/lib/companion.py b/resources/lib/companion.py
index 077474fd..cb630222 100644
--- a/resources/lib/companion.py
+++ b/resources/lib/companion.py
@@ -6,8 +6,10 @@ Processes Plex companion inputs from the plexbmchelper to Kodi commands
from logging import getLogger
from xbmc import Player
-from . import playqueue as PQ, plex_functions as PF
-from . import json_rpc as js, variables as v, app
+from . import plex_functions as PF
+from . import json_rpc as js
+from . import variables as v
+from . import app
###############################################################################
@@ -28,7 +30,7 @@ def skip_to(params):
playqueue_item_id, plex_id)
found = True
for player in list(js.get_players().values()):
- playqueue = PQ.PLAYQUEUES[player['playerid']]
+ playqueue = app.PLAYQUEUES[player['playerid']]
for i, item in enumerate(playqueue.items):
if item.id == playqueue_item_id:
found = True
diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py
index 8195016b..6a6553ad 100644
--- a/resources/lib/context_entry.py
+++ b/resources/lib/context_entry.py
@@ -6,8 +6,11 @@ import xbmcgui
from .plex_api import API
from .plex_db import PlexDB
-from . import context, plex_functions as PF, playqueue as PQ
-from . import utils, variables as v, app
+from . import context
+from . import plex_functions as PF
+from . import utils
+from . import variables as v
+from . import app
###############################################################################
@@ -137,8 +140,7 @@ class ContextMenu(object):
"""
For using direct paths: Initiates playback using the PMS
"""
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
+ playqueue = app.PLAYQUEUES.from_kodi_type(self.kodi_type)
playqueue.clear()
app.PLAYSTATE.context_menu_play = True
handle = self.api.fullpath(force_addon=True)[0]
diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py
index 29669750..0d3bbe5f 100644
--- a/resources/lib/kodimonitor.py
+++ b/resources/lib/kodimonitor.py
@@ -17,7 +17,7 @@ from .kodi_db import KodiVideoDB
from . import kodi_db
from .downloadutils import DownloadUtils as DU
from . import utils, timing, plex_functions as PF
-from . import json_rpc as js, playqueue as PQ, playlist_func as PL
+from . import json_rpc as js, playlist_func as PL
from . import backgroundthread, app, variables as v
from . import exceptions
@@ -140,7 +140,7 @@ class KodiMonitor(xbmc.Monitor):
u'playlistid': 1,
}
"""
- playqueue = PQ.PLAYQUEUES[data['playlistid']]
+ playqueue = app.PLAYQUEUES[data['playlistid']]
if not playqueue.is_pkc_clear():
playqueue.pkc_edit = True
playqueue.clear(kodi=False)
@@ -256,7 +256,7 @@ class KodiMonitor(xbmc.Monitor):
if not playerid:
LOG.error('Coud not get playerid for data %s', data)
return
- playqueue = PQ.PLAYQUEUES[playerid]
+ playqueue = app.PLAYQUEUES[playerid]
info = js.get_player_props(playerid)
if playqueue.kodi_playlist_playback:
# Kodi will tell us the wrong position - of the playlist, not the
@@ -326,7 +326,7 @@ class KodiMonitor(xbmc.Monitor):
container_key = None
if info['playlistid'] != -1:
# -1 is Kodi's answer if there is no playlist
- container_key = PQ.PLAYQUEUES[playerid].id
+ container_key = app.PLAYQUEUES[playerid].id
if container_key is not None:
container_key = '/playQueues/%s' % container_key
elif plex_id is not None:
@@ -367,6 +367,11 @@ class KodiMonitor(xbmc.Monitor):
# We need to switch to the Plex streams ONCE upon playback start
if playerid == v.KODI_VIDEO_PLAYER_ID:
+ # The Kodi player takes forever to initialize all streams
+ # Especially subtitles, apparently. No way to tell when Kodi
+ # is done :-(
+ if app.APP.monitor.waitForAbort(5):
+ return
item.init_kodi_streams()
item.switch_to_plex_stream('video')
if utils.settings('audioStreamPick') == '0':
diff --git a/resources/lib/playback.py b/resources/lib/playback.py
index a40d4685..3cd493ef 100644
--- a/resources/lib/playback.py
+++ b/resources/lib/playback.py
@@ -12,8 +12,12 @@ import xbmc
from .plex_api import API
from .plex_db import PlexDB
from .kodi_db import KodiVideoDB
-from . import plex_functions as PF, playlist_func as PL, playqueue as PQ
-from . import json_rpc as js, variables as v, utils, transfer
+from . import plex_functions as PF
+from . import playlist_func as PL
+from . import json_rpc as js
+from . import variables as v
+from . import utils
+from . import transfer
from . import playback_decision, app
from . import exceptions
@@ -74,20 +78,19 @@ def _playback_triage(plex_id, plex_type, path, resolve, resume):
_ensure_resolve(abort=True)
return
with app.APP.lock_playqueues:
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
+ playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# add-on paths
LOG.debug('No position returned from player! Assuming playlist')
- playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
+ playqueue = app.PLAYQUEUES.audio
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
LOG.debug('Assuming video instead of audio playlist playback')
- playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_VIDEO)
+ playqueue = app.PLAYQUEUES.video
try:
pos = js.get_position(playqueue.playlistid)
except KeyError:
@@ -159,7 +162,7 @@ def _playlist_playback(plex_id):
return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
# has actually started. Need to tell Kodimonitor
- playqueue = PQ.get_playqueue_from_type(v.KODI_PLAYLIST_TYPE_AUDIO)
+ playqueue = app.PLAYQUEUES.audio
playqueue.clear(kodi=False)
# Set the flag for the potentially WRONG audio playlist so Kodimonitor
# can pick up on it
@@ -499,8 +502,7 @@ def process_indirect(key, offset, resolve=True):
api = API(xml[0])
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
+ playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type)
playqueue.clear()
item = PL.playlist_item_from_xml(xml[0])
item.offset = offset
diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py
index 697823b5..b472bb7b 100644
--- a/resources/lib/playlist_func.py
+++ b/resources/lib/playlist_func.py
@@ -22,117 +22,6 @@ from .subtitles import accessible_plex_subtitles
LOG = getLogger('PLEX.playlist_func')
-class Playqueue_Object(object):
- """
- PKC object to represent PMS playQueues and Kodi playlist for queueing
-
- playlistid = None [int] Kodi playlist id (0, 1, 2)
- type = None [str] Kodi type: 'audio', 'video', 'picture'
- kodi_pl = None Kodi xbmc.PlayList object
- items = [] [list] of Playlist_Items
- id = None [str] Plex playQueueID, unique Plex identifier
- version = None [int] Plex version of the playQueue
- selectedItemID = None
- [str] Plex selectedItemID, playing element in queue
- selectedItemOffset = None
- [str] Offset of the playing element in queue
- shuffled = 0 [int] 0: not shuffled, 1: ??? 2: ???
- repeat = 0 [int] 0: not repeated, 1: ??? 2: ???
-
- If Companion playback is initiated by another user:
- plex_transient_token = None
- """
- kind = 'playQueue'
-
- def __init__(self):
- self.id = None
- self.type = None
- self.playlistid = None
- self.kodi_pl = None
- self.items = []
- self.version = None
- self.selectedItemID = None
- self.selectedItemOffset = None
- self.shuffled = 0
- self.repeat = 0
- self.plex_transient_token = None
- # Need a hack for detecting swaps of elements
- self.old_kodi_pl = []
- # Did PKC itself just change the playqueue so the PKC playqueue monitor
- # should not pick up any changes?
- self.pkc_edit = False
- # Workaround to avoid endless loops of detecting PL clears
- self._clear_list = []
- # To keep track if Kodi playback was initiated from a Kodi playlist
- # There are a couple of pitfalls, unfortunately...
- self.kodi_playlist_playback = False
-
- def __repr__(self):
- answ = ("{{"
- "'playlistid': {self.playlistid}, "
- "'id': {self.id}, "
- "'version': {self.version}, "
- "'type': '{self.type}', "
- "'selectedItemID': {self.selectedItemID}, "
- "'selectedItemOffset': {self.selectedItemOffset}, "
- "'shuffled': {self.shuffled}, "
- "'repeat': {self.repeat}, "
- "'kodi_playlist_playback': {self.kodi_playlist_playback}, "
- "'pkc_edit': {self.pkc_edit}, ".format(self=self))
- # Since list.__repr__ will return string, not unicode
- return answ + "'items': {self.items}}}".format(self=self)
-
- def is_pkc_clear(self):
- """
- Returns True if PKC has cleared the Kodi playqueue just recently.
- Then this clear will be ignored from now on
- """
- try:
- self._clear_list.pop()
- except IndexError:
- return False
- else:
- return True
-
- def clear(self, kodi=True):
- """
- Resets the playlist object to an empty playlist.
-
- Pass kodi=False in order to NOT clear the Kodi playqueue
- """
- # kodi monitor's on_clear method will only be called if there were some
- # items to begin with
- if kodi and self.kodi_pl.size() != 0:
- self._clear_list.append(None)
- self.kodi_pl.clear() # Clear Kodi playlist object
- self.items = []
- self.id = None
- self.version = None
- self.selectedItemID = None
- self.selectedItemOffset = None
- self.shuffled = 0
- self.repeat = 0
- self.plex_transient_token = None
- self.old_kodi_pl = []
- self.kodi_playlist_playback = False
- LOG.debug('Playlist cleared: %s', self)
-
- def position_from_plex_id(self, plex_id):
- """
- Returns the position [int] for the very first item with plex_id [int]
- (Plex seems uncapable of adding the same element multiple times to a
- playqueue or playlist)
-
- Raises KeyError if not found
- """
- for position, item in enumerate(self.items):
- if item.plex_id == plex_id:
- break
- else:
- raise KeyError('Did not find plex_id %s in %s', plex_id, self)
- return position
-
-
class PlaylistItem(object):
"""
Object to fill our playqueues and playlists with.
@@ -281,14 +170,44 @@ class PlaylistItem(object):
elif stream_type == 'video':
return self.video_streams
+ @staticmethod
+ def _current_index(stream_type):
+ """
+ Kodi might tell us the wrong index for any stream after playback start
+ Get the correct one!
+ """
+ function = {
+ 'audio': js.get_current_audio_stream_index,
+ 'video': js.get_current_video_stream_index,
+ 'subtitle': js.get_current_subtitle_stream_index
+ }[stream_type]
+ i = 0
+ while i < 30:
+ # Really annoying: Kodi might return wrong results directly after
+ # playback startup, e.g. a Kodi audio index of 1953718901 (!)
+ try:
+ index = function(v.KODI_VIDEO_PLAYER_ID)
+ except TypeError:
+ # No sensible reply yet
+ pass
+ else:
+ if index != 1953718901:
+ # Correct result!
+ return index
+ i += 1
+ app.APP.monitor.waitForAbort(0.1)
+ else:
+ raise RuntimeError('Kodi did not tell us the correct index for %s'
+ % stream_type)
+
def init_kodi_streams(self):
"""
Initializes all streams after Kodi has started playing this video
"""
- self.current_kodi_video_stream = js.get_current_video_stream_index(v.KODI_VIDEO_PLAYER_ID)
- self.current_kodi_audio_stream = js.get_current_audio_stream_index(v.KODI_VIDEO_PLAYER_ID)
+ self.current_kodi_video_stream = self._current_index('video')
+ self.current_kodi_audio_stream = self._current_index('audio')
self.current_kodi_sub_stream = False if not js.get_subtitle_enabled(v.KODI_VIDEO_PLAYER_ID) \
- else js.get_current_subtitle_stream_index(v.KODI_VIDEO_PLAYER_ID)
+ else self._current_index('subtitle')
def plex_stream_index(self, kodi_stream_index, stream_type):
"""
@@ -312,13 +231,16 @@ class PlaylistItem(object):
Pass in the plex_stream_index [int] in order to receive the Kodi stream
index [int].
stream_type: 'video', 'audio', 'subtitle'
- Returns None if unsuccessful
+ Raises ValueError if unsuccessful
"""
- if plex_stream_index is None:
- return
+ if not isinstance(plex_stream_index, int):
+ raise ValueError('%s plex_stream_index %s of type %s received' %
+ (stream_type, plex_stream_index, type(plex_stream_index)))
for i, stream in enumerate(self._get_iterator(stream_type)):
if cast(int, stream.get('id')) == plex_stream_index:
return i
+ raise ValueError('No %s kodi_stream_index for plex_stream_index %s' %
+ (stream_type, plex_stream_index))
def active_plex_stream_index(self, stream_type):
"""
@@ -408,8 +330,10 @@ class PlaylistItem(object):
return
LOG.debug('The PMS wants to display %s stream with Plex id %s and '
'languageTag %s', typus, plex_index, language_tag)
- kodi_index = self.kodi_stream_index(plex_index, typus)
- if kodi_index is None:
+ try:
+ kodi_index = self.kodi_stream_index(plex_index, typus)
+ except ValueError:
+ kodi_index = None
LOG.debug('Leaving Kodi %s stream settings untouched since we '
'could not parse Plex %s stream with id %s to a Kodi'
' index', typus, typus, plex_index)
@@ -438,10 +362,28 @@ class PlaylistItem(object):
Call this method if Kodi reports an "AV-Change"
(event "Player.OnAVChange")
"""
- kodi_video_stream = js.get_current_video_stream_index(playerid)
- kodi_audio_stream = js.get_current_audio_stream_index(playerid)
+ i = 0
+ while i < 20:
+ # Really annoying: Kodi might return wrong results directly after
+ # playback startup, e.g. a Kodi audio index of 1953718901 (!)
+ kodi_video_stream = js.get_current_video_stream_index(playerid)
+ kodi_audio_stream = js.get_current_audio_stream_index(playerid)
+ if kodi_video_stream < len(self.video_streams) and kodi_audio_stream < len(self.audio_streams):
+ # Correct result!
+ break
+ i += 1
+ if app.APP.monitor.waitForAbort(0.1):
+ # Need to quit PKC
+ return
+ else:
+ LOG.error('Could not get sensible Kodi indices! kodi_video_stream '
+ '%s, kodi_audio_stream %s',
+ kodi_video_stream, kodi_audio_stream)
+ return
+ kodi_video_stream = self._current_index('video')
+ kodi_audio_stream = self._current_index('audio')
sub_enabled = js.get_subtitle_enabled(playerid)
- kodi_sub_stream = js.get_current_subtitle_stream_index(playerid)
+ kodi_sub_stream = self._current_index('subtitle')
# Audio
if kodi_audio_stream != self.current_kodi_audio_stream:
self.on_kodi_audio_stream_change(kodi_audio_stream)
@@ -457,28 +399,43 @@ class PlaylistItem(object):
and kodi_sub_stream != self.current_kodi_sub_stream)):
self.on_kodi_subtitle_stream_change(kodi_sub_stream, sub_enabled)
- def on_plex_stream_change(self, plex_data):
+ def on_plex_stream_change(self, video_stream_id=None, audio_stream_id=None,
+ subtitle_stream_id=None):
"""
- Call this method if Plex Companion wants to change streams
+ Call this method if Plex Companion wants to change streams [ints]
"""
- if 'audioStreamID' in plex_data:
- plex_index = int(plex_data['audioStreamID'])
- kodi_index = self.kodi_stream_index(plex_index, 'audio')
- self._set_kodi_stream_if_different(kodi_index, 'audio')
- self.current_kodi_audio_stream = kodi_index
- if 'videoStreamID' in plex_data:
- plex_index = int(plex_data['videoStreamID'])
- kodi_index = self.kodi_stream_index(plex_index, 'video')
+ if video_stream_id is not None:
+ try:
+ kodi_index = self.kodi_stream_index(video_stream_id, 'video')
+ except ValueError:
+ LOG.error('Unexpected Plex video_stream_id %s, not changing '
+ 'the video stream!', video_stream_id)
+ return
self._set_kodi_stream_if_different(kodi_index, 'video')
self.current_kodi_video_stream = kodi_index
- if 'subtitleStreamID' in plex_data:
- plex_index = int(plex_data['subtitleStreamID'])
- if plex_index == 0:
+ if audio_stream_id is not None:
+ try:
+ kodi_index = self.kodi_stream_index(audio_stream_id, 'audio')
+ except ValueError:
+ LOG.error('Unexpected Plex audio_stream_id %s, not changing '
+ 'the video stream!', audio_stream_id)
+ return
+ self._set_kodi_stream_if_different(kodi_index, 'audio')
+ self.current_kodi_audio_stream = kodi_index
+ if subtitle_stream_id is not None:
+ if subtitle_stream_id == 0:
app.APP.player.showSubtitles(False)
kodi_index = False
else:
- kodi_index = self.kodi_stream_index(plex_index, 'subtitle')
- if kodi_index:
+ try:
+ kodi_index = self.kodi_stream_index(subtitle_stream_id,
+ 'subtitle')
+ except ValueError:
+ kodi_index = None
+ LOG.debug('The PMS wanted to change subs, but we could not'
+ ' match the sub with id %s to a Kodi sub',
+ subtitle_stream_id)
+ else:
app.APP.player.setSubtitleStream(kodi_index)
app.APP.player.showSubtitles(True)
self.current_kodi_sub_stream = kodi_index
diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py
deleted file mode 100644
index 1f06cb32..00000000
--- a/resources/lib/playqueue.py
+++ /dev/null
@@ -1,232 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
-"""
-from logging import getLogger
-import copy
-
-import xbmc
-
-from .plex_api import API
-from . import playlist_func as PL, plex_functions as PF
-from . import backgroundthread, utils, json_rpc as js, app, variables as v
-from . import exceptions
-
-###############################################################################
-LOG = getLogger('PLEX.playqueue')
-
-PLUGIN = 'plugin://%s' % v.ADDON_ID
-
-# Our PKC playqueues (3 instances of Playqueue_Object())
-PLAYQUEUES = []
-###############################################################################
-
-
-def init_playqueues():
- """
- Call this once on startup to initialize the PKC playqueue objects in
- the list PLAYQUEUES
- """
- if PLAYQUEUES:
- LOG.debug('Playqueues have already been initialized')
- return
- # Initialize Kodi playqueues
- with app.APP.lock_playqueues:
- for i in (0, 1, 2):
- # Just in case the Kodi response is not sorted correctly
- for queue in js.get_playlists():
- if queue['playlistid'] != i:
- continue
- playqueue = PL.Playqueue_Object()
- playqueue.playlistid = i
- playqueue.type = queue['type']
- # Initialize each Kodi playlist
- if playqueue.type == v.KODI_TYPE_AUDIO:
- playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC)
- elif playqueue.type == v.KODI_TYPE_VIDEO:
- playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
- else:
- # Currently, only video or audio playqueues available
- playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
- # Overwrite 'picture' with 'photo'
- playqueue.type = v.KODI_TYPE_PHOTO
- PLAYQUEUES.append(playqueue)
- LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
-
-
-def get_playqueue_from_type(kodi_playlist_type):
- """
- Returns the playqueue according to the kodi_playlist_type ('video',
- 'audio', 'picture') passed in
- """
- for playqueue in PLAYQUEUES:
- if playqueue.type == kodi_playlist_type:
- break
- else:
- raise ValueError('Wrong playlist type passed in: %s',
- kodi_playlist_type)
- return playqueue
-
-
-def init_playqueue_from_plex_children(plex_id, transient_token=None):
- """
- Init a new playqueue e.g. from an album. Alexa does this
-
- Returns the playqueue
- """
- xml = PF.GetAllPlexChildren(plex_id)
- try:
- xml[0].attrib
- except (TypeError, IndexError, AttributeError):
- LOG.error('Could not download the PMS xml for %s', plex_id)
- return
- playqueue = get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
- playqueue.clear()
- for i, child in enumerate(xml):
- api = API(child)
- try:
- PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
- except exceptions.PlaylistError:
- LOG.error('Could not add Plex item to our playlist: %s, %s',
- child.tag, child.attrib)
- playqueue.plex_transient_token = transient_token
- LOG.debug('Firing up Kodi player')
- app.APP.player.play(playqueue.kodi_pl, None, False, 0)
- return playqueue
-
-
-class PlayqueueMonitor(backgroundthread.KillableThread):
- """
- Unfortunately, Kodi does not tell if items within a Kodi playqueue
- (playlist) are swapped. This is what this monitor is for. Don't replace
- this mechanism till Kodi's implementation of playlists has improved
- """
- def _compare_playqueues(self, playqueue, new_kodi_playqueue):
- """
- Used to poll the Kodi playqueue and update the Plex playqueue if needed
- """
- old = list(playqueue.items)
- # We might append to new_kodi_playqueue but will need the original
- # still back in the main loop
- new = copy.deepcopy(new_kodi_playqueue)
- index = list(range(0, len(old)))
- LOG.debug('Comparing new Kodi playqueue %s with our play queue %s',
- new, old)
- for i, new_item in enumerate(new):
- if (new_item['file'].startswith('plugin://') and
- not new_item['file'].startswith(PLUGIN)):
- # Ignore new media added by other addons
- continue
- for j, old_item in enumerate(old):
- if self.should_suspend() or self.should_cancel():
- # Chances are that we got an empty Kodi playlist due to
- # Kodi exit
- return
- try:
- if (old_item.file.startswith('plugin://') and
- not old_item.file.startswith(PLUGIN)):
- # Ignore media by other addons
- continue
- except AttributeError:
- # were not passed a filename; ignore
- pass
- if 'id' in new_item:
- identical = (old_item.kodi_id == new_item['id'] and
- old_item.kodi_type == new_item['type'])
- else:
- try:
- plex_id = int(utils.REGEX_PLEX_ID.findall(new_item['file'])[0])
- except IndexError:
- LOG.debug('Comparing paths directly as a fallback')
- identical = old_item.file == new_item['file']
- else:
- identical = plex_id == old_item.plex_id
- if j == 0 and identical:
- del old[j], index[j]
- break
- elif identical:
- LOG.debug('Playqueue item %s moved to position %s',
- i + j, i)
- try:
- PL.move_playlist_item(playqueue, i + j, i)
- except exceptions.PlaylistError:
- LOG.error('Could not modify playqueue positions')
- LOG.error('This is likely caused by mixing audio and '
- 'video tracks in the Kodi playqueue')
- del old[j], index[j]
- break
- else:
- LOG.debug('Detected new Kodi element at position %s: %s ',
- i, new_item)
- try:
- if playqueue.id is None:
- PL.init_plex_playqueue(playqueue, kodi_item=new_item)
- else:
- PL.add_item_to_plex_playqueue(playqueue,
- i,
- kodi_item=new_item)
- except exceptions.PlaylistError:
- # Could not add the element
- pass
- except KeyError:
- # Catches KeyError from PL.verify_kodi_item()
- # Hack: Kodi already started playback of a new item and we
- # started playback already using kodimonitors
- # PlayBackStart(), but the Kodi playlist STILL only shows
- # the old element. Hence ignore playlist difference here
- LOG.debug('Detected an outdated Kodi playlist - ignoring')
- return
- except IndexError:
- # This is really a hack - happens when using Addon Paths
- # and repeatedly starting the same element. Kodi will then
- # not pass kodi id nor file path AND will also not
- # start-up playback. Hence kodimonitor kicks off playback.
- # Also see kodimonitor.py - _playlist_onadd()
- pass
- else:
- for j in range(i, len(index)):
- index[j] += 1
- for i in reversed(index):
- if self.should_suspend() or self.should_cancel():
- # Chances are that we got an empty Kodi playlist due to
- # Kodi exit
- return
- LOG.debug('Detected deletion of playqueue element at pos %s', i)
- try:
- PL.delete_playlist_item_from_PMS(playqueue, i)
- except exceptions.PlaylistError:
- LOG.error('Could not delete PMS element from position %s', i)
- LOG.error('This is likely caused by mixing audio and '
- 'video tracks in the Kodi playqueue')
- LOG.debug('Done comparing playqueues')
-
- def run(self):
- LOG.info("----===## Starting PlayqueueMonitor ##===----")
- app.APP.register_thread(self)
- try:
- self._run()
- finally:
- app.APP.deregister_thread(self)
- LOG.info("----===## PlayqueueMonitor stopped ##===----")
-
- def _run(self):
- while not self.should_cancel():
- if self.should_suspend():
- if self.wait_while_suspended():
- return
- with app.APP.lock_playqueues:
- for playqueue in PLAYQUEUES:
- kodi_pl = js.playlist_get_items(playqueue.playlistid)
- if playqueue.old_kodi_pl != kodi_pl:
- if playqueue.id is None and (not app.SYNC.direct_paths or
- app.PLAYSTATE.context_menu_play):
- # Only initialize if directly fired up using direct
- # paths. Otherwise let default.py do its magic
- LOG.debug('Not yet initiating playback')
- else:
- # compare old and new playqueue
- self._compare_playqueues(playqueue, kodi_pl)
- playqueue.old_kodi_pl = list(kodi_pl)
- self.sleep(0.2)
diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py
deleted file mode 100644
index e6400dd8..00000000
--- a/resources/lib/plex_companion.py
+++ /dev/null
@@ -1,362 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-The Plex Companion master python file
-"""
-from logging import getLogger
-from threading import Thread
-from queue import Empty
-from socket import SHUT_RDWR
-from xbmc import executebuiltin
-
-from .plexbmchelper import listener, plexgdm, subscribers, httppersist
-from .plex_api import API
-from . import utils
-from . import plex_functions as PF
-from . import playlist_func as PL
-from . import playback
-from . import json_rpc as js
-from . import playqueue as PQ
-from . import variables as v
-from . import backgroundthread
-from . import app
-from . import exceptions
-
-###############################################################################
-
-LOG = getLogger('PLEX.plex_companion')
-
-###############################################################################
-
-
-def update_playqueue_from_PMS(playqueue,
- playqueue_id=None,
- repeat=None,
- offset=None,
- transient_token=None,
- start_plex_id=None):
- """
- Completely updates the Kodi playqueue with the new Plex playqueue. Pass
- in playqueue_id if we need to fetch a new playqueue
-
- repeat = 0, 1, 2
- offset = time offset in Plextime (milliseconds)
- """
- LOG.info('New playqueue %s received from Plex companion with offset '
- '%s, repeat %s, start_plex_id %s',
- playqueue_id, offset, repeat, start_plex_id)
- # Safe transient token from being deleted
- if transient_token is None:
- transient_token = playqueue.plex_transient_token
- with app.APP.lock_playqueues:
- try:
- xml = PL.get_PMS_playlist(playqueue, playqueue_id)
- except exceptions.PlaylistError:
- LOG.error('Could now download playqueue %s', playqueue_id)
- return
- if playqueue.id == playqueue_id:
- # This seems to be happening ONLY if a Plex Companion device
- # reconnects and Kodi is already playing something - silly, really
- # For all other cases, a new playqueue is generated by Plex
- LOG.debug('Update for existing playqueue detected')
- return
- playqueue.clear()
- # Get new metadata for the playqueue first
- try:
- PL.get_playlist_details_from_xml(playqueue, xml)
- except exceptions.PlaylistError:
- LOG.error('Could not get playqueue ID %s', playqueue_id)
- return
- playqueue.repeat = 0 if not repeat else int(repeat)
- playqueue.plex_transient_token = transient_token
- playback.play_xml(playqueue,
- xml,
- offset=offset,
- start_plex_id=start_plex_id)
-
-
-class PlexCompanion(backgroundthread.KillableThread):
- """
- Plex Companion monitoring class. Invoke only once
- """
- def __init__(self):
- LOG.info("----===## Starting PlexCompanion ##===----")
- # Init Plex Companion queue
- # Start GDM for server/client discovery
- self.client = plexgdm.plexgdm()
- self.client.clientDetails()
- LOG.debug("Registration string is:\n%s", self.client.getClientDetails())
- self.httpd = False
- self.subscription_manager = None
- super(PlexCompanion, self).__init__()
-
- @staticmethod
- def _process_alexa(data):
- if 'key' not in data or 'containerKey' not in data:
- LOG.error('Received malformed Alexa data: %s', data)
- return
- xml = PF.GetPlexMetadata(data['key'])
- try:
- xml[0].attrib
- except (AttributeError, IndexError, TypeError):
- LOG.error('Could not download Plex metadata for: %s', data)
- return
- api = API(xml[0])
- if api.plex_type == v.PLEX_TYPE_ALBUM:
- LOG.debug('Plex music album detected')
- PQ.init_playqueue_from_plex_children(
- api.plex_id,
- transient_token=data.get('token'))
- elif data['containerKey'].startswith('/playQueues/'):
- _, container_key, _ = PF.ParseContainerKey(data['containerKey'])
- xml = PF.DownloadChunks('{server}/playQueues/%s' % container_key)
- if xml is None:
- # "Play error"
- utils.dialog('notification',
- utils.lang(29999),
- utils.lang(30128),
- icon='{error}')
- return
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
- playqueue.clear()
- PL.get_playlist_details_from_xml(playqueue, xml)
- playqueue.plex_transient_token = data.get('token')
- if data.get('offset') != '0':
- offset = float(data['offset']) / 1000.0
- else:
- offset = None
- playback.play_xml(playqueue, xml, offset)
- else:
- app.CONN.plex_transient_token = data.get('token')
- playback.playback_triage(api.plex_id,
- api.plex_type,
- resolve=False,
- resume=data.get('offset') not in ('0', None))
-
- @staticmethod
- def _process_node(data):
- """
- E.g. watch later initiated by Companion. Basically navigating Plex
- """
- app.CONN.plex_transient_token = data.get('key')
- params = {
- 'mode': 'plex_node',
- 'key': f"{{server}}{data.get('key')}",
- 'offset': data.get('offset')
- }
- handle = f'RunPlugin(plugin://{utils.extend_url(v.ADDON_ID, params)})'
- executebuiltin(handle)
-
- @staticmethod
- def _process_playlist(data):
- if 'containerKey' not in data:
- LOG.error('Received malformed playlist data: %s', data)
- return
- # Get the playqueue ID
- _, container_key, query = PF.ParseContainerKey(data['containerKey'])
- try:
- playqueue = PQ.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 = PF.GetPlexMetadata(data['key'])
- try:
- xml[0].attrib
- except (AttributeError, IndexError, TypeError):
- LOG.error('Could not download Plex metadata')
- return
- api = API(xml[0])
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
- key = data.get('key')
- if key:
- _, key, _ = PF.ParseContainerKey(key)
- update_playqueue_from_PMS(playqueue,
- playqueue_id=container_key,
- repeat=query.get('repeat'),
- offset=utils.cast(int, data.get('offset')),
- transient_token=data.get('token'),
- start_plex_id=key)
-
- @staticmethod
- def _process_streams(data):
- """
- Plex Companion client adjusted audio or subtitle stream
- """
- if 'type' not in data:
- LOG.error('Received malformed stream data: %s', data)
- return
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']])
- pos = js.get_position(playqueue.playlistid)
- playqueue.items[pos].on_plex_stream_change(data)
-
- @staticmethod
- def _process_refresh(data):
- """
- example data: {'playQueueID': '8475', 'commandID': '11'}
- """
- if 'playQueueID' not in data:
- LOG.error('Received malformed refresh data: %s', data)
- return
- xml = PL.get_pms_playqueue(data['playQueueID'])
- if xml is None:
- return
- if len(xml) == 0:
- LOG.debug('Empty playqueue received - clearing playqueue')
- plex_type = PL.get_plextype_from_xml(xml)
- if plex_type is None:
- return
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
- playqueue.clear()
- return
- playqueue = PQ.get_playqueue_from_type(
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
- update_playqueue_from_PMS(playqueue, data['playQueueID'])
-
- def _process_tasks(self, task):
- """
- Processes tasks picked up e.g. by Companion listener, e.g.
- {'action': 'playlist',
- 'data': {'address': 'xyz.plex.direct',
- 'commandID': '7',
- 'containerKey': '/playQueues/6669?own=1&repeat=0&window=200',
- 'key': '/library/metadata/220493',
- 'machineIdentifier': 'xyz',
- 'offset': '0',
- 'port': '32400',
- 'protocol': 'https',
- 'token': 'transient-cd2527d1-0484-48e0-a5f7-f5caa7d591bd',
- 'type': 'video'}}
- """
- LOG.debug('Processing: %s', task)
- data = task['data']
- if task['action'] == 'alexa':
- with app.APP.lock_playqueues:
- self._process_alexa(data)
- elif (task['action'] == 'playlist' and
- data.get('address') == 'node.plexapp.com'):
- self._process_node(data)
- elif task['action'] == 'playlist':
- with app.APP.lock_playqueues:
- self._process_playlist(data)
- elif task['action'] == 'refreshPlayQueue':
- with app.APP.lock_playqueues:
- self._process_refresh(data)
- elif task['action'] == 'setStreams':
- try:
- self._process_streams(data)
- except KeyError:
- pass
-
- def run(self):
- """
- Ensure that sockets will be closed no matter what
- """
- app.APP.register_thread(self)
- try:
- self._run()
- finally:
- try:
- self.httpd.socket.shutdown(SHUT_RDWR)
- except AttributeError:
- pass
- finally:
- try:
- self.httpd.socket.close()
- except AttributeError:
- pass
- app.APP.deregister_thread(self)
- LOG.info("----===## Plex Companion stopped ##===----")
-
- def _run(self):
- httpd = self.httpd
- # Cache for quicker while loops
- client = self.client
-
- # Start up instances
- request_mgr = httppersist.RequestMgr()
- subscription_manager = subscribers.SubscriptionMgr(request_mgr,
- app.APP.player)
- self.subscription_manager = subscription_manager
-
- if utils.settings('plexCompanion') == 'true':
- # Start up httpd
- start_count = 0
- while True:
- try:
- httpd = listener.PKCHTTPServer(
- client,
- subscription_manager,
- ('', v.COMPANION_PORT),
- listener.MyHandler)
- httpd.timeout = 10.0
- break
- except Exception:
- LOG.error("Unable to start PlexCompanion. Traceback:")
- import traceback
- LOG.error(traceback.print_exc())
- app.APP.monitor.waitForAbort(3)
- if start_count == 3:
- LOG.error("Error: Unable to start web helper.")
- httpd = False
- break
- start_count += 1
- else:
- LOG.info('User deactivated Plex Companion')
- client.start_all()
- message_count = 0
- if httpd:
- thread = Thread(target=httpd.handle_request)
-
- while not self.should_cancel():
- # If we are not authorized, sleep
- # Otherwise, we trigger a download which leads to a
- # re-authorizations
- if self.should_suspend():
- if self.wait_while_suspended():
- break
- try:
- message_count += 1
- if httpd:
- if not thread.is_alive():
- # Use threads cause the method will stall
- thread = Thread(target=httpd.handle_request)
- thread.start()
-
- if message_count == 3000:
- message_count = 0
- if client.check_client_registration():
- LOG.debug('Client is still registered')
- else:
- LOG.debug('Client is no longer registered. Plex '
- 'Companion still running on port %s',
- v.COMPANION_PORT)
- client.register_as_client()
- # Get and set servers
- if message_count % 30 == 0:
- subscription_manager.serverlist = client.getServerList()
- subscription_manager.notify()
- if not httpd:
- message_count = 0
- except Exception:
- LOG.warn("Error in loop, continuing anyway. Traceback:")
- import traceback
- LOG.warn(traceback.format_exc())
- # See if there's anything we need to process
- try:
- task = app.APP.companion_queue.get(block=False)
- except Empty:
- pass
- else:
- # Got instructions, process them
- self._process_tasks(task)
- app.APP.companion_queue.task_done()
- # Don't sleep
- continue
- self.sleep(0.05)
- subscription_manager.signal_stop()
- client.stop_all()
diff --git a/resources/lib/plex_companion/__init__.py b/resources/lib/plex_companion/__init__.py
new file mode 100644
index 00000000..ba2478a9
--- /dev/null
+++ b/resources/lib/plex_companion/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from .polling import Listener
+from .playstate import PlaystateMgr
diff --git a/resources/lib/plex_companion/common.py b/resources/lib/plex_companion/common.py
new file mode 100644
index 00000000..14f6e4c1
--- /dev/null
+++ b/resources/lib/plex_companion/common.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from .. import variables as v
+from .. import app
+
+
+def log_error(logger, error_message, response):
+ logger('%s: %s: %s', error_message, response.status_code, response.reason)
+ logger('headers received from the PMS: %s', response.headers)
+ logger('Message received from the PMS: %s', response.text)
+
+
+def proxy_headers():
+ return {
+ 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
+ 'X-Plex-Product': v.ADDON_NAME,
+ 'X-Plex-Version': v.ADDON_VERSION,
+ 'X-Plex-Platform': v.PLATFORM,
+ 'X-Plex-Platform-Version': v.PLATFORM_VERSION,
+ 'X-Plex-Device-Name': v.DEVICENAME,
+ 'Content-Type': 'text/xml;charset=utf-8'
+ }
+
+
+def proxy_params():
+ params = {
+ 'deviceClass': 'pc',
+ 'protocolCapabilities': 'timeline,playback,navigation,playqueues',
+ 'protocolVersion': 3
+ }
+ if app.ACCOUNT.pms_token:
+ params['X-Plex-Token'] = app.ACCOUNT.pms_token
+ return params
diff --git a/resources/lib/plex_companion/playqueue.py b/resources/lib/plex_companion/playqueue.py
new file mode 100644
index 00000000..064b7065
--- /dev/null
+++ b/resources/lib/plex_companion/playqueue.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from logging import getLogger
+import copy
+
+from ..plex_api import API
+from .. import variables as v
+from .. import app
+from .. import utils
+from .. import plex_functions as PF
+from .. import playlist_func as PL
+from .. import exceptions
+
+log = getLogger('PLEX.companion.playqueue')
+
+PLUGIN = 'plugin://%s' % v.ADDON_ID
+
+
+def init_playqueue_from_plex_children(plex_id, transient_token=None):
+ """
+ Init a new playqueue e.g. from an album. Alexa does this
+
+ Returns the playqueue
+ """
+ xml = PF.GetAllPlexChildren(plex_id)
+ try:
+ xml[0].attrib
+ except (TypeError, IndexError, AttributeError):
+ log.error('Could not download the PMS xml for %s', plex_id)
+ return
+ playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type'])
+ playqueue.clear()
+ for i, child in enumerate(xml):
+ api = API(child)
+ try:
+ PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
+ except exceptions.PlaylistError:
+ log.error('Could not add Plex item to our playlist: %s, %s',
+ child.tag, child.attrib)
+ playqueue.plex_transient_token = transient_token
+ log.debug('Firing up Kodi player')
+ app.APP.player.play(playqueue.kodi_pl, None, False, 0)
+ return playqueue
+
+
+def compare_playqueues(playqueue, new_kodi_playqueue):
+ """
+ Used to poll the Kodi playqueue and update the Plex playqueue if needed
+ """
+ old = list(playqueue.items)
+ # We might append to new_kodi_playqueue but will need the original
+ # still back in the main loop
+ new = copy.deepcopy(new_kodi_playqueue)
+ index = list(range(0, len(old)))
+ log.debug('Comparing new Kodi playqueue %s with our play queue %s',
+ new, old)
+ for i, new_item in enumerate(new):
+ if (new_item['file'].startswith('plugin://') and
+ not new_item['file'].startswith(PLUGIN)):
+ # Ignore new media added by other addons
+ continue
+ for j, old_item in enumerate(old):
+
+ if app.APP.stop_pkc:
+ # Chances are that we got an empty Kodi playlist due to
+ # Kodi exit
+ return
+ try:
+ if (old_item.file.startswith('plugin://') and
+ not old_item.file.startswith(PLUGIN)):
+ # Ignore media by other addons
+ continue
+ except AttributeError:
+ # were not passed a filename; ignore
+ pass
+ if 'id' in new_item:
+ identical = (old_item.kodi_id == new_item['id'] and
+ old_item.kodi_type == new_item['type'])
+ else:
+ try:
+ plex_id = int(utils.REGEX_PLEX_ID.findall(new_item['file'])[0])
+ except IndexError:
+ log.debug('Comparing paths directly as a fallback')
+ identical = old_item.file == new_item['file']
+ else:
+ identical = plex_id == old_item.plex_id
+ if j == 0 and identical:
+ del old[j], index[j]
+ break
+ elif identical:
+ log.debug('Playqueue item %s moved to position %s',
+ i + j, i)
+ try:
+ PL.move_playlist_item(playqueue, i + j, i)
+ except exceptions.PlaylistError:
+ log.error('Could not modify playqueue positions')
+ log.error('This is likely caused by mixing audio and '
+ 'video tracks in the Kodi playqueue')
+ del old[j], index[j]
+ break
+ else:
+ log.debug('Detected new Kodi element at position %s: %s ',
+ i, new_item)
+ try:
+ if playqueue.id is None:
+ PL.init_plex_playqueue(playqueue, kodi_item=new_item)
+ else:
+ PL.add_item_to_plex_playqueue(playqueue,
+ i,
+ kodi_item=new_item)
+ except exceptions.PlaylistError:
+ # Could not add the element
+ pass
+ except KeyError:
+ # Catches KeyError from PL.verify_kodi_item()
+ # Hack: Kodi already started playback of a new item and we
+ # started playback already using kodimonitors
+ # PlayBackStart(), but the Kodi playlist STILL only shows
+ # the old element. Hence ignore playlist difference here
+ log.debug('Detected an outdated Kodi playlist - ignoring')
+ return
+ except IndexError:
+ # This is really a hack - happens when using Addon Paths
+ # and repeatedly starting the same element. Kodi will then
+ # not pass kodi id nor file path AND will also not
+ # start-up playback. Hence kodimonitor kicks off playback.
+ # Also see kodimonitor.py - _playlist_onadd()
+ pass
+ else:
+ for j in range(i, len(index)):
+ index[j] += 1
+ for i in reversed(index):
+ if app.APP.stop_pkc:
+ # Chances are that we got an empty Kodi playlist due to
+ # Kodi exit
+ return
+ log.debug('Detected deletion of playqueue element at pos %s', i)
+ try:
+ PL.delete_playlist_item_from_PMS(playqueue, i)
+ except exceptions.PlaylistError:
+ log.error('Could not delete PMS element from position %s', i)
+ log.error('This is likely caused by mixing audio and '
+ 'video tracks in the Kodi playqueue')
+ log.debug('Done comparing playqueues')
diff --git a/resources/lib/plex_companion/playstate.py b/resources/lib/plex_companion/playstate.py
new file mode 100644
index 00000000..16ba8417
--- /dev/null
+++ b/resources/lib/plex_companion/playstate.py
@@ -0,0 +1,411 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from logging import getLogger
+import requests
+import xml.etree.ElementTree as etree
+
+from .common import proxy_headers, proxy_params, log_error
+from .playqueue import compare_playqueues
+
+from .. import json_rpc as js
+from .. import variables as v
+from .. import backgroundthread
+from .. import app
+from .. import timing
+from .. import skip_plex_intro
+
+
+# Disable annoying requests warnings
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+log = getLogger('PLEX.companion.playstate')
+
+TIMEOUT = (5, 5)
+
+# What is Companion controllable?
+CONTROLLABLE = {
+ v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
+ 'subtitleStream,seekTo,skipPrevious,skipNext,'
+ 'stepBack,stepForward',
+ v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
+ 'skipPrevious,skipNext,stepBack,stepForward',
+ v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext'
+}
+
+
+def split_server_uri(server):
+ (protocol, url, port) = server.split(':')
+ url = url.replace('/', '')
+ return (protocol, url, port)
+
+
+def get_correct_position(info, playqueue):
+ """
+ Kodi tells us the PLAYLIST position, not PLAYQUEUE position, if the
+ user initiated playback of a playlist
+ """
+ if playqueue.kodi_playlist_playback:
+ position = 0
+ else:
+ position = info['position'] or 0
+ return position
+
+
+def timeline_dict(playerid, typus):
+ with app.APP.lock_playqueues:
+ info = app.PLAYSTATE.player_states[playerid]
+ playqueue = app.PLAYQUEUES[playerid]
+ position = get_correct_position(info, playqueue)
+ try:
+ item = playqueue.items[position]
+ except IndexError:
+ # E.g. for direct path playback for single item
+ return {
+ 'controllable': CONTROLLABLE[typus],
+ 'type': typus,
+ 'state': 'stopped'
+ }
+ protocol, url, port = split_server_uri(app.CONN.server)
+ status = 'paused' if int(info['speed']) == 0 else 'playing'
+ duration = timing.kodi_time_to_millis(info['totaltime'])
+ shuffle = '1' if info['shuffled'] else '0'
+ mute = '1' if info['muted'] is True else '0'
+ answ = {
+ 'controllable': CONTROLLABLE[typus],
+ 'protocol': protocol,
+ 'address': url,
+ 'port': port,
+ 'machineIdentifier': app.CONN.machine_identifier,
+ 'state': status,
+ 'type': typus,
+ 'itemType': typus,
+ 'time': str(timing.kodi_time_to_millis(info['time'])),
+ 'duration': str(duration),
+ 'seekRange': '0-%s' % duration,
+ 'shuffle': shuffle,
+ 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
+ 'volume': str(info['volume']),
+ 'mute': mute,
+ 'mediaIndex': '0', # Still to implement
+ 'partIndex': '0',
+ 'partCount': '1',
+ 'providerIdentifier': 'com.plexapp.plugins.library',
+ }
+ # Get the plex id from the PKC playqueue not info, as Kodi jumps to
+ # next playqueue element way BEFORE kodi monitor onplayback is
+ # called
+ if item.plex_id:
+ answ['key'] = '/library/metadata/%s' % item.plex_id
+ answ['ratingKey'] = str(item.plex_id)
+ # PlayQueue stuff
+ if info['container_key']:
+ answ['containerKey'] = info['container_key']
+ if (info['container_key'] is not None and
+ info['container_key'].startswith('/playQueues')):
+ answ['playQueueID'] = str(playqueue.id)
+ answ['playQueueVersion'] = str(playqueue.version)
+ answ['playQueueItemID'] = str(item.id)
+ if playqueue.items[position].guid:
+ answ['guid'] = item.guid
+ # Temp. token set?
+ if app.CONN.plex_transient_token:
+ answ['token'] = app.CONN.plex_transient_token
+ elif playqueue.plex_transient_token:
+ answ['token'] = playqueue.plex_transient_token
+ # Process audio and subtitle streams
+ if typus == v.PLEX_PLAYLIST_TYPE_VIDEO:
+ answ['videoStreamID'] = str(item.current_plex_video_stream)
+ answ['audioStreamID'] = str(item.current_plex_audio_stream)
+ # Mind the zero - meaning subs are deactivated
+ answ['subtitleStreamID'] = str(item.current_plex_sub_stream or 0)
+ return answ
+
+
+def timeline(players):
+ """
+ Returns a timeline xml as str
+ (xml containing video, audio, photo player state)
+ """
+ xml = etree.Element('MediaContainer')
+ location = 'navigation'
+ for typus in (v.PLEX_PLAYLIST_TYPE_AUDIO,
+ v.PLEX_PLAYLIST_TYPE_VIDEO,
+ v.PLEX_PLAYLIST_TYPE_PHOTO):
+ player = players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus])
+ if player is None:
+ # Kodi player currently not actively playing, but stopped
+ timeline = {
+ 'controllable': CONTROLLABLE[typus],
+ 'type': typus,
+ 'state': 'stopped'
+ }
+ else:
+ # Active Kodi player, i.e. video, audio or picture player
+ timeline = timeline_dict(player['playerid'], typus)
+ if typus in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO):
+ location = 'fullScreenVideo'
+ etree.SubElement(xml, 'Timeline', attrib=timeline)
+ xml.set('location', location)
+ return xml
+
+
+def stopped_timeline():
+ """
+ Returns an XML stating that all players have stopped playback
+ """
+ xml = etree.Element('MediaContainer', attrib={'location': 'navigation'})
+ for typus in (v.PLEX_PLAYLIST_TYPE_AUDIO,
+ v.PLEX_PLAYLIST_TYPE_VIDEO,
+ v.PLEX_PLAYLIST_TYPE_PHOTO):
+ # Kodi player currently not actively playing, but stopped
+ timeline = {
+ 'controllable': CONTROLLABLE[typus],
+ 'type': typus,
+ 'state': 'stopped'
+ }
+ etree.SubElement(xml, 'Timeline', attrib=timeline)
+ return xml
+
+
+def update_player_info(players):
+ """
+ Update the playstate info for other PKC "consumers"
+ """
+ for player in players.values():
+ playerid = player['playerid']
+ app.PLAYSTATE.player_states[playerid].update(js.get_player_props(playerid))
+ app.PLAYSTATE.player_states[playerid]['volume'] = js.get_volume()
+ app.PLAYSTATE.player_states[playerid]['muted'] = js.get_muted()
+
+
+class PlaystateMgr(backgroundthread.KillableThread):
+ """
+ If Kodi plays something, tell the PMS about it and - if a Companion client
+ is connected - tell the PMS Plex Companion piece of the PMS about it.
+ Also checks whether an intro is currently playing, enabling the user to
+ skip it.
+ """
+ daemon = True
+
+ def __init__(self):
+ self._subscribed = False
+ self._command_id = None
+ self.s = None
+ self.t = None
+ self.stopped_timeline = stopped_timeline()
+ super().__init__()
+
+ def _get_requests_session(self):
+ if self.s is None:
+ log.debug('Creating new requests session')
+ self.s = requests.Session()
+ self.s.headers = proxy_headers()
+ self.s.verify = app.CONN.verify_ssl_cert
+ if app.CONN.ssl_cert_path:
+ self.s.cert = app.CONN.ssl_cert_path
+ self.s.params = proxy_params()
+ return self.s
+
+ def _get_requests_session_companion(self):
+ if self.t is None:
+ log.debug('Creating new companion requests session')
+ self.t = requests.Session()
+ self.t.headers = proxy_headers()
+ self.t.verify = app.CONN.verify_ssl_cert
+ if app.CONN.ssl_cert_path:
+ self.t.cert = app.CONN.ssl_cert_path
+ self.t.params = proxy_params()
+ return self.t
+
+ def close_requests_session(self):
+ for session in (self.s, self.t):
+ if session is not None:
+ try:
+ session.close()
+ except AttributeError:
+ # "thread-safety" - Just in case s was set to None in the
+ # meantime
+ pass
+ session = None
+
+ @staticmethod
+ def communicate(method, url, **kwargs):
+ try:
+ # This will usually block until timeout is reached!
+ req = method(url, **kwargs)
+ except requests.ConnectTimeout:
+ # The request timed out while trying to connect to the PMS
+ log.error('Requests ConnectionTimeout!')
+ raise
+ except requests.ReadTimeout:
+ # The PMS did not send any data in the allotted amount of time
+ log.error('Requests ReadTimeout!')
+ raise
+ except requests.TooManyRedirects:
+ log.error('TooManyRedirects error!')
+ raise
+ except requests.HTTPError as error:
+ log.error('HTTPError: %s', error)
+ raise
+ except requests.ConnectionError as error:
+ log.error('ConnectionError: %s', error)
+ raise
+ req.encoding = 'utf-8'
+ # To make sure that we release the socket, need to access content once
+ req.content
+ return req
+
+ def _subscribe(self, cmd):
+ self._command_id = int(cmd.get('commandID'))
+ self._subscribed = True
+
+ def _unsubscribe(self):
+ self._subscribed = False
+ self._command_id = None
+
+ def send_stop(self):
+ """
+ If we're still connected to a PMS, tells the PMS that playback stopped
+ """
+ if app.CONN.online and app.ACCOUNT.authenticated:
+ # Only try to send something if we're connected
+ self.pms_timeline(dict(), self.stopped_timeline)
+ self.companion_timeline(self.stopped_timeline)
+
+ def check_subscriber(self, cmd):
+ if cmd.get('path') == '/player/timeline/unsubscribe':
+ log.info('Stop Plex Companion subscription')
+ self._unsubscribe()
+ elif not self._subscribed:
+ log.info('Start Plex Companion subscription')
+ self._subscribe(cmd)
+ else:
+ try:
+ self._command_id = int(cmd.get('commandID'))
+ except TypeError:
+ pass
+
+ def companion_timeline(self, message):
+ if not self._subscribed:
+ return
+ url = f'{app.CONN.server}/player/proxy/timeline'
+ self._get_requests_session_companion()
+ self.t.params['commandID'] = self._command_id
+ message.set('commandID', str(self._command_id))
+ # Get the correct playstate
+ state = 'stopped'
+ for timeline in message:
+ if timeline.get('state') != 'stopped':
+ state = timeline.get('state')
+ self.t.params['state'] = state
+ # Send update
+ try:
+ req = self.communicate(self.t.post,
+ url,
+ data=etree.tostring(message,
+ encoding='utf-8'),
+ timeout=TIMEOUT)
+ except (requests.RequestException, SystemExit):
+ return
+ if not req.ok:
+ log_error(log.error, 'Unexpected Companion timeline', req)
+
+ def pms_timeline_per_player(self, playerid, message):
+ """
+ Pass a really low timeout in seconds if shutting down Kodi and we don't
+ need the PMS' response
+ """
+ url = f'{app.CONN.server}/:/timeline'
+ self._get_requests_session()
+ self.s.params.update(message[playerid].attrib)
+ # Tell the PMS about our playstate progress
+ try:
+ req = self.communicate(self.s.get, url, timeout=TIMEOUT)
+ except (requests.RequestException, SystemExit):
+ return
+ if not req.ok:
+ log_error(log.error, 'Failed reporting playback progress', req)
+
+ def pms_timeline(self, players, message):
+ players = players if players else \
+ {0: {'playerid': 0}, 1: {'playerid': 1}, 2: {'playerid': 2}}
+ for player in players.values():
+ self.pms_timeline_per_player(player['playerid'], message)
+
+ def run(self):
+ app.APP.register_thread(self)
+ log.info("----===## Starting PlaystateMgr ##===----")
+ try:
+ self._run()
+ finally:
+ # Make sure we're telling the PMS that playback will stop
+ self.send_stop()
+ # Cleanup
+ self.close_requests_session()
+ app.APP.deregister_thread(self)
+ log.info("----===## PlaystateMgr stopped ##===----")
+
+ def _run(self):
+ signaled_playback_stop = True
+ while not self.should_cancel():
+ if self.should_suspend():
+ self._unsubscribe()
+ self.close_requests_session()
+ if self.wait_while_suspended():
+ break
+ # Check for Kodi playlist changes first
+ with app.APP.lock_playqueues:
+ for playqueue in app.PLAYQUEUES:
+ kodi_pl = js.playlist_get_items(playqueue.playlistid)
+ if playqueue.old_kodi_pl != kodi_pl:
+ if playqueue.id is None and (not app.SYNC.direct_paths or
+ app.PLAYSTATE.context_menu_play):
+ # Only initialize if directly fired up using direct
+ # paths. Otherwise let default.py do its magic
+ log.debug('Not yet initiating playback')
+ else:
+ # compare old and new playqueue
+ compare_playqueues(playqueue, kodi_pl)
+ playqueue.old_kodi_pl = list(kodi_pl)
+ # Then check for Kodi playback
+ players = js.get_players()
+ if not players and signaled_playback_stop:
+ self.sleep(1)
+ continue
+ elif not players:
+ # Playback has just stopped, need to tell Plex
+ signaled_playback_stop = True
+ self.send_stop()
+ self.sleep(1)
+ continue
+ else:
+ # Update the playstate info, such as playback progress
+ update_player_info(players)
+ try:
+ message = timeline(players)
+ except TypeError:
+ # We haven't had a chance to set the kodi_stream_index for
+ # the currently playing item. Just skip for now
+ self.sleep(1)
+ continue
+ else:
+ # Kodi will started with 'stopped' - make sure we're
+ # waiting here until we got something playing or on pause.
+ for entry in message:
+ if entry.get('state') != 'stopped':
+ break
+ else:
+ continue
+ signaled_playback_stop = False
+ try:
+ # Check whether an intro is currently running
+ skip_plex_intro.check()
+ except IndexError:
+ # Playback might have already stopped
+ pass
+ # Send the playback progress info to the PMS
+ self.pms_timeline(players, message)
+ # Send the info to all Companion devices via the PMS
+ self.companion_timeline(message)
+ self.sleep(1)
diff --git a/resources/lib/plex_companion/polling.py b/resources/lib/plex_companion/polling.py
new file mode 100644
index 00000000..b9c29e38
--- /dev/null
+++ b/resources/lib/plex_companion/polling.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from logging import getLogger
+import requests
+
+from .processing import process_proxy_xml
+from .common import proxy_headers, proxy_params, log_error
+
+from .. import utils
+from .. import backgroundthread
+from .. import app
+from .. import variables as v
+
+# Disable annoying requests warnings
+import requests.packages.urllib3
+requests.packages.urllib3.disable_warnings()
+
+# Timeout (connection timeout, read timeout)
+# The later is up to 20 seconds, if the PMS has nothing to tell us
+# THIS WILL PREVENT PKC FROM SHUTTING DOWN CORRECTLY
+TIMEOUT = (5.0, 3.0)
+
+log = getLogger('PLEX.companion.listener')
+
+
+class Listener(backgroundthread.KillableThread):
+ """
+ Opens a GET HTTP connection to the current PMS (that will time-out PMS-wise
+ after ~20 seconds) and listens for any commands by the PMS. Listening
+ will cause this PKC client to be registered as a Plex Companien client.
+ """
+ daemon = True
+
+ def __init__(self, playstate_mgr):
+ self.s = None
+ self.playstate_mgr = playstate_mgr
+ super().__init__()
+
+ def _get_requests_session(self):
+ if self.s is None:
+ log.debug('Creating new requests session')
+ self.s = requests.Session()
+ self.s.headers = proxy_headers()
+ self.s.verify = app.CONN.verify_ssl_cert
+ if app.CONN.ssl_cert_path:
+ self.s.cert = app.CONN.ssl_cert_path
+ self.s.params = proxy_params()
+ return self.s
+
+ def close_requests_session(self):
+ try:
+ self.s.close()
+ except AttributeError:
+ # "thread-safety" - Just in case s was set to None in the
+ # meantime
+ pass
+ self.s = None
+
+ def ok_message(self, command_id):
+ url = f'{app.CONN.server}/player/proxy/response?commandID={command_id}'
+ try:
+ req = self.communicate(self.s.post,
+ url,
+ data=v.COMPANION_OK_MESSAGE.encode('utf-8'))
+ except (requests.RequestException, SystemExit):
+ return
+ if not req.ok:
+ log_error(log.error, 'Error replying OK', req)
+
+ @staticmethod
+ def communicate(method, url, **kwargs):
+ try:
+ req = method(url, **kwargs)
+ except requests.ConnectTimeout:
+ # The request timed out while trying to connect to the PMS
+ log.error('Requests ConnectionTimeout!')
+ raise
+ except requests.ReadTimeout:
+ # The PMS did not send any data in the allotted amount of time
+ log.error('Requests ReadTimeout!')
+ raise
+ except requests.TooManyRedirects:
+ log.error('TooManyRedirects error!')
+ raise
+ except requests.HTTPError as error:
+ log.error('HTTPError: %s', error)
+ raise
+ except requests.ConnectionError:
+ # Caused by PKC terminating the connection prematurely
+ # log.error('ConnectionError: %s', error)
+ raise
+ else:
+ req.encoding = 'utf-8'
+ # Access response content once in order to make sure to release the
+ # underlying sockets
+ req.content
+ return req
+
+ def run(self):
+ """
+ Ensure that sockets will be closed no matter what
+ """
+ app.APP.register_thread(self)
+ log.info("----===## Starting PollCompanion ##===----")
+ try:
+ self._run()
+ finally:
+ self.close_requests_session()
+ app.APP.deregister_thread(self)
+ log.info("----===## PollCompanion stopped ##===----")
+
+ def _run(self):
+ while not self.should_cancel():
+ if self.should_suspend():
+ self.close_requests_session()
+ if self.wait_while_suspended():
+ break
+ # See if there's anything we need to process
+ # timeout=1 will cause the PMS to "hold" the connection for approx
+ # 20 seconds. This will BLOCK requests - not something we can
+ # circumvent.
+ url = app.CONN.server + '/player/proxy/poll?timeout=1'
+ self._get_requests_session()
+ try:
+ req = self.communicate(self.s.get,
+ url,
+ timeout=TIMEOUT)
+ except requests.ConnectionError:
+ # No command received from the PMS - try again immediately
+ continue
+ except requests.RequestException:
+ self.sleep(0.5)
+ continue
+ except SystemExit:
+ # We need to quit PKC entirely
+ break
+
+ # Sanity checks
+ if not req.ok:
+ log_error(log.error, 'Error while contacting the PMS', req)
+ self.sleep(0.5)
+ continue
+ if not req.text:
+ # Means the connection timed-out (usually after 20 seconds),
+ # because there was no command from the PMS or a client to
+ # remote-control anything no the PKC-side
+ # Received an empty body, but still header Content-Type: xml
+ continue
+ if not ('content-type' in req.headers
+ and 'xml' in req.headers['content-type']):
+ log_error(log.error, 'Unexpected answer from the PMS', req)
+ self.sleep(0.5)
+ continue
+
+ # Parsing
+ try:
+ xml = utils.etree.fromstring(req.content)
+ cmd = xml[0]
+ if len(xml) > 1:
+ # We should always just get ONE command per message
+ raise IndexError()
+ except (utils.ParseError, IndexError):
+ log_error(log.error, 'Could not parse the PMS xml:', req)
+ self.sleep(0.5)
+ continue
+
+ # Do the work
+ log.debug('Received a Plex Companion command from the PMS:')
+ utils.log_xml(xml, log.debug)
+ self.playstate_mgr.check_subscriber(cmd)
+ if process_proxy_xml(cmd):
+ self.ok_message(cmd.get('commandID'))
diff --git a/resources/lib/plex_companion/processing.py b/resources/lib/plex_companion/processing.py
new file mode 100644
index 00000000..0f3de1f1
--- /dev/null
+++ b/resources/lib/plex_companion/processing.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+The Plex Companion master python file
+"""
+from logging import getLogger
+
+import xbmc
+
+from ..plex_api import API
+from .. import utils
+from ..utils import cast
+from .. import plex_functions as PF
+from .. import playlist_func as PL
+from .. import playback
+from .. import json_rpc as js
+from .. import variables as v
+from .. import app
+from .. import exceptions
+
+
+log = getLogger('PLEX.companion.processing')
+
+
+def update_playqueue_from_PMS(playqueue,
+ playqueue_id=None,
+ repeat=None,
+ offset=None,
+ transient_token=None,
+ start_plex_id=None):
+ """
+ Completely updates the Kodi playqueue with the new Plex playqueue. Pass
+ in playqueue_id if we need to fetch a new playqueue
+
+ repeat = 0, 1, 2
+ offset = time offset in Plextime (milliseconds)
+ """
+ log.info('New playqueue %s received from Plex companion with offset '
+ '%s, repeat %s, start_plex_id %s',
+ playqueue_id, offset, repeat, start_plex_id)
+ # Safe transient token from being deleted
+ if transient_token is None:
+ transient_token = playqueue.plex_transient_token
+ with app.APP.lock_playqueues:
+ try:
+ xml = PL.get_PMS_playlist(playqueue, playqueue_id)
+ except exceptions.PlaylistError:
+ log.error('Could now download playqueue %s', playqueue_id)
+ return
+ if playqueue.id == playqueue_id:
+ # This seems to be happening ONLY if a Plex Companion device
+ # reconnects and Kodi is already playing something - silly, really
+ # For all other cases, a new playqueue is generated by Plex
+ log.debug('Update for existing playqueue detected')
+ return
+ playqueue.clear()
+ # Get new metadata for the playqueue first
+ try:
+ PL.get_playlist_details_from_xml(playqueue, xml)
+ except exceptions.PlaylistError:
+ log.error('Could not get playqueue ID %s', playqueue_id)
+ return
+ playqueue.repeat = 0 if not repeat else int(repeat)
+ playqueue.plex_transient_token = transient_token
+ playback.play_xml(playqueue,
+ xml,
+ offset=offset,
+ start_plex_id=start_plex_id)
+
+
+def process_node(key, transient_token, offset):
+ """
+ E.g. watch later initiated by Companion. Basically navigating Plex
+ """
+ app.CONN.plex_transient_token = transient_token
+ params = {
+ 'mode': 'plex_node',
+ 'key': f'{{server}}{key}',
+ 'offset': offset
+ }
+ handle = f'RunPlugin(plugin://{utils.extend_url(v.ADDON_ID, params)})'
+ xbmc.executebuiltin(handle)
+
+
+def process_playlist(containerKey, typus, key, offset, token):
+ # Get the playqueue ID
+ _, container_key, query = PF.ParseContainerKey(containerKey)
+ try:
+ playqueue = app.PLAYQUEUES.from_plex_type(typus)
+ except ValueError:
+ # E.g. Plex web does not supply the media type
+ # Still need to figure out the type (video vs. music vs. pix)
+ xml = PF.GetPlexMetadata(key)
+ try:
+ xml[0].attrib
+ except (AttributeError, IndexError, TypeError):
+ log.error('Could not download Plex metadata')
+ return
+ api = API(xml[0])
+ playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type)
+ if key:
+ _, key, _ = PF.ParseContainerKey(key)
+ update_playqueue_from_PMS(playqueue,
+ playqueue_id=container_key,
+ repeat=query.get('repeat'),
+ offset=utils.cast(int, offset),
+ transient_token=token,
+ start_plex_id=key)
+
+
+def process_streams(plex_type, video_stream_id, audio_stream_id,
+ subtitle_stream_id):
+ """
+ Plex Companion client adjusted audio or subtitle stream
+ """
+ playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
+ pos = js.get_position(playqueue.playlistid)
+ playqueue.items[pos].on_plex_stream_change(video_stream_id,
+ audio_stream_id,
+ subtitle_stream_id)
+
+
+def process_refresh(playqueue_id):
+ """
+ example data: {'playQueueID': '8475', 'commandID': '11'}
+ """
+ xml = PL.get_pms_playqueue(playqueue_id)
+ if xml is None:
+ return
+ if len(xml) == 0:
+ log.debug('Empty playqueue received - clearing playqueue')
+ plex_type = PL.get_plextype_from_xml(xml)
+ if plex_type is None:
+ return
+ playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
+ playqueue.clear()
+ return
+ playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type'])
+ update_playqueue_from_PMS(playqueue, playqueue_id)
+
+
+def skip_to(playqueue_item_id, key):
+ """
+ Skip to a specific playlist position.
+
+ Does not seem to be implemented yet by Plex!
+ """
+ _, plex_id = PF.GetPlexKeyNumber(key)
+ log.debug('Skipping to playQueueItemID %s, plex_id %s',
+ playqueue_item_id, plex_id)
+ found = True
+ for player in list(js.get_players().values()):
+ playqueue = app.PLAYQUEUES[player['playerid']]
+ for i, item in enumerate(playqueue.items):
+ if item.id == playqueue_item_id:
+ found = True
+ break
+ else:
+ for i, item in enumerate(playqueue.items):
+ if item.plex_id == plex_id:
+ found = True
+ break
+ if found is True:
+ app.APP.player.play(playqueue.kodi_pl, None, False, i)
+ else:
+ log.error('Item not found to skip to')
+
+
+def process_proxy_xml(cmd):
+ """cmd: a "Command" etree xml"""
+ path = cmd.get('path')
+ if (path == '/player/playback/playMedia'
+ and cmd.get('queryAddress') == 'node.plexapp.com'):
+ process_node(cmd.get('queryKey'),
+ cmd.get('queryToken'),
+ cmd.get('queryOffset') or 0)
+ elif path == '/player/playback/playMedia':
+ with app.APP.lock_playqueues:
+ process_playlist(cmd.get('queryContainerKey'),
+ cmd.get('queryType'),
+ cmd.get('queryKey'),
+ cmd.get('queryOffset'),
+ cmd.get('queryToken'))
+ elif path == '/player/playback/refreshPlayQueue':
+ with app.APP.lock_playqueues:
+ process_refresh(cmd.get('queryPlayQueueID'))
+ elif path == '/player/playback/setParameters':
+ if 'queryVolume' in cmd.attrib:
+ js.set_volume(int(cmd.get('queryVolume')))
+ else:
+ log.error('Unknown command: %s: %s', cmd.tag, cmd.attrib)
+ elif path == '/player/playback/play':
+ js.play()
+ elif path == '/player/playback/pause':
+ js.pause()
+ elif path == '/player/playback/stop':
+ js.stop()
+ elif path == '/player/playback/seekTo':
+ js.seek_to(float(cmd.get('queryOffset', 0.0)) / 1000.0)
+ elif path == '/player/playback/stepForward':
+ js.smallforward()
+ elif path == '/player/playback/stepBack':
+ js.smallbackward()
+ elif path == '/player/playback/skipNext':
+ js.skipnext()
+ elif path == '/player/playback/skipPrevious':
+ js.skipprevious()
+ elif path == '/player/playback/skipTo':
+ skip_to(cmd.get('queryPlayQueueItemID'), cmd.get('queryKey'))
+ elif path == '/player/navigation/moveUp':
+ js.input_up()
+ elif path == '/player/navigation/moveDown':
+ js.input_down()
+ elif path == '/player/navigation/moveLeft':
+ js.input_left()
+ elif path == '/player/navigation/moveRight':
+ js.input_right()
+ elif path == '/player/navigation/select':
+ js.input_select()
+ elif path == '/player/navigation/home':
+ js.input_home()
+ elif path == '/player/navigation/back':
+ js.input_back()
+ elif path == '/player/playback/setStreams':
+ process_streams(cmd.get('queryType'),
+ cast(int, cmd.get('queryVideoStreamID')),
+ cast(int, cmd.get('queryAudioStreamID')),
+ cast(int, cmd.get('querySubtitleStreamID')))
+ elif path == '/player/timeline/subscribe':
+ pass
+ elif path == '/player/timeline/unsubscribe':
+ pass
+ else:
+ log.error('Unknown Plex companion path/command: %s: %s',
+ cmd.tag, cmd.attrib)
+ return True
diff --git a/resources/lib/plex_db/playlists.py b/resources/lib/plex_db/playlists.py
index 57d3b085..5c318cf6 100644
--- a/resources/lib/plex_db/playlists.py
+++ b/resources/lib/plex_db/playlists.py
@@ -18,7 +18,7 @@ class Playlists(object):
def delete_playlist(self, playlist):
"""
- Removes the entry for playlist [Playqueue_Object] from the Plex
+ Removes the entry for playlist [Playqueue()] from the Plex
playlists table.
Be sure to either set playlist.id or playlist.kodi_path
"""
diff --git a/resources/lib/plexbmchelper/__init__.py b/resources/lib/plexbmchelper/__init__.py
deleted file mode 100644
index b93054b3..00000000
--- a/resources/lib/plexbmchelper/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Dummy file to make this directory a package.
diff --git a/resources/lib/plexbmchelper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py
deleted file mode 100644
index 2c4735df..00000000
--- a/resources/lib/plexbmchelper/httppersist.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-from logging import getLogger
-import http.client
-import traceback
-import string
-import errno
-from socket import error as socket_error
-
-###############################################################################
-
-LOG = getLogger('PLEX.httppersist')
-
-###############################################################################
-
-
-class RequestMgr(object):
- def __init__(self):
- self.conns = {}
-
- def getConnection(self, protocol, host, port):
- conn = self.conns.get(protocol + host + str(port), False)
- if not conn:
- if protocol == "https":
- conn = http.client.HTTPSConnection(host, port)
- else:
- conn = http.client.HTTPConnection(host, port)
- self.conns[protocol + host + str(port)] = conn
- return conn
-
- def closeConnection(self, protocol, host, port):
- conn = self.conns.get(protocol + host + str(port), False)
- if conn:
- conn.close()
- self.conns.pop(protocol + host + str(port), None)
-
- def dumpConnections(self):
- for conn in list(self.conns.values()):
- conn.close()
- self.conns = {}
-
- def post(self, host, port, path, body, header={}, protocol="http"):
- conn = None
- try:
- conn = self.getConnection(protocol, host, port)
- header['Connection'] = "keep-alive"
- conn.request("POST", path, body, header)
- data = conn.getresponse()
- if int(data.status) >= 400:
- 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
- else:
- return data.read() or True
- except socket_error as serr:
- # Ignore remote close and connection refused (e.g. shutdown PKC)
- 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)
- if conn:
- conn.close()
- return False
- except Exception as e:
- LOG.error("Exception encountered: %s", e)
- # Close connection just in case
- try:
- conn.close()
- except Exception:
- pass
- return False
-
- def getwithparams(self, host, port, path, params, header={},
- protocol="http"):
- newpath = path + '?'
- pairs = []
- for key in params:
- pairs.append(str(key) + '=' + str(params[key]))
- newpath += string.join(pairs, '&')
- return self.get(host, port, newpath, header, protocol)
-
- def get(self, host, port, path, header={}, protocol="http"):
- try:
- conn = self.getConnection(protocol, host, port)
- header['Connection'] = "keep-alive"
- conn.request("GET", path, headers=header)
- data = conn.getresponse()
- if int(data.status) >= 400:
- LOG.error("HTTP response error: %s", str(data.status))
- return False
- else:
- return data.read() or True
- except socket_error as serr:
- # Ignore remote close and connection refused (e.g. shutdown PKC)
- 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)
- conn.close()
- return False
diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py
deleted file mode 100644
index 6473bfed..00000000
--- a/resources/lib/plexbmchelper/listener.py
+++ /dev/null
@@ -1,238 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Plex Companion listener
-"""
-from logging import getLogger
-from re import sub
-from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
-
-from .. import utils, companion, json_rpc as js, clientinfo, variables as v
-from .. import app
-
-###############################################################################
-
-LOG = getLogger('PLEX.listener')
-
-# Hack we need in order to keep track of the open connections from Plex Web
-CLIENT_DICT = {}
-
-###############################################################################
-
-RESOURCES_XML = ('%s\n'
- ' \n'
- '\n') % (v.XML_HEADER,
- v.ADDON_NAME,
- v.PLATFORM,
- v.PLATFORM_VERSION)
-
-
-class MyHandler(BaseHTTPRequestHandler):
- """
- BaseHTTPRequestHandler implementation of Plex Companion listener
- """
- protocol_version = 'HTTP/1.1'
-
- def __init__(self, *args, **kwargs):
- self.serverlist = []
- super().__init__(*args, **kwargs)
-
- def log_message(self, format, *args):
- '''
- Mute all requests, don't log 'em
- '''
- pass
-
- def do_HEAD(self):
- LOG.debug("Serving HEAD request...")
- self.answer_request(0)
-
- def do_GET(self):
- LOG.debug("Serving GET request...")
- self.answer_request(1)
-
- def do_OPTIONS(self):
- LOG.debug("Serving OPTIONS request...")
- self.send_response(200)
- self.send_header('Content-Length', '0')
- self.send_header('X-Plex-Client-Identifier', v.PKC_MACHINE_IDENTIFIER)
- self.send_header('Content-Type', 'text/plain')
- self.send_header('Connection', 'close')
- self.send_header('Access-Control-Max-Age', '1209600')
- self.send_header('Access-Control-Allow-Origin', '*')
- self.send_header('Access-Control-Allow-Methods',
- 'POST, GET, OPTIONS, DELETE, PUT, HEAD')
- self.send_header(
- 'Access-Control-Allow-Headers',
- 'x-plex-version, x-plex-platform-version, x-plex-username, '
- 'x-plex-client-identifier, x-plex-target-client-identifier, '
- 'x-plex-device-name, x-plex-platform, x-plex-product, accept, '
- 'x-plex-device, x-plex-device-screen-resolution')
- self.end_headers()
-
- def response(self, body, headers=None, code=200):
- headers = {} if headers is None else headers
- self.send_response(code)
- for key in headers:
- self.send_header(key, headers[key])
- self.send_header('Content-Length', len(body))
- self.end_headers()
- if body:
- self.wfile.write(body.encode('utf-8'))
-
- def answer_request(self, send_data):
- self.serverlist = self.server.client.getServerList()
- sub_mgr = self.server.subscription_manager
-
- request_path = self.path[1:]
- request_path = sub(r"\?.*", "", request_path)
- parseresult = utils.urlparse(self.path)
- paramarrays = utils.parse_qs(parseresult.query)
- params = {}
- for key in paramarrays:
- params[key] = paramarrays[key][0]
- LOG.debug("remote request_path: %s, received from %s with headers: %s",
- request_path, self.client_address, self.headers.items())
- LOG.debug("params received from remote: %s", params)
- sub_mgr.update_command_id(self.headers.get(
- 'X-Plex-Client-Identifier', self.client_address[0]),
- params.get('commandID'))
-
- conntype = self.headers.get('Connection', '')
- if conntype.lower() == 'keep-alive':
- headers = {
- 'Connection': 'Keep-Alive',
- 'Keep-Alive': 'timeout=20'
- }
- else:
- headers = {'Connection': 'Close'}
-
- if request_path == "version":
- self.response(
- "PlexKodiConnect Plex Companion: Running\nVersion: %s"
- % v.ADDON_VERSION,
- headers)
- elif request_path == "verify":
- self.response("XBMC JSON connection test:\n" + js.ping(),
- headers)
- elif request_path == 'resources':
- self.response(
- RESOURCES_XML.format(
- title=v.DEVICENAME,
- machineIdentifier=v.PKC_MACHINE_IDENTIFIER),
- clientinfo.getXArgsDeviceInfo(options=headers,
- include_token=False))
- elif request_path == 'player/timeline/poll':
- # Plex web does polling if connected to PKC via Companion
- # Only reply if there is indeed something playing
- # Otherwise, all clients seem to keep connection open
- if params.get('wait') == '1':
- app.APP.monitor.waitForAbort(0.95)
- if self.client_address[0] not in CLIENT_DICT:
- CLIENT_DICT[self.client_address[0]] = []
- tracker = CLIENT_DICT[self.client_address[0]]
- tracker.append(self.client_address[1])
- while (not app.APP.is_playing and
- not app.APP.monitor.abortRequested() and
- sub_mgr.stop_sent_to_web and not
- (len(tracker) >= 4 and
- tracker[0] == self.client_address[1])):
- # Keep at most 3 connections open, then drop the first one
- # Doesn't need to be thread-save
- # Silly stuff really
- app.APP.monitor.waitForAbort(1)
- # Let PKC know that we're releasing this connection
- tracker.pop(0)
- msg = sub_mgr.msg(js.get_players()).format(
- command_id=params.get('commandID', 0))
- if sub_mgr.isplaying:
- self.response(
- msg,
- {
- 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
- 'X-Plex-Protocol': '1.0',
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Max-Age': '1209600',
- 'Access-Control-Expose-Headers':
- 'X-Plex-Client-Identifier',
- 'Content-Type': 'text/xml;charset=utf-8'
- }.update(headers))
- elif not sub_mgr.stop_sent_to_web:
- sub_mgr.stop_sent_to_web = True
- LOG.debug('Signaling STOP to Plex Web')
- self.response(
- msg,
- {
- 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
- 'X-Plex-Protocol': '1.0',
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Max-Age': '1209600',
- 'Access-Control-Expose-Headers':
- 'X-Plex-Client-Identifier',
- 'Content-Type': 'text/xml;charset=utf-8'
- }.update(headers))
- else:
- # We're not playing anything yet, just reply with a 200
- self.response(
- msg,
- {
- 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
- 'X-Plex-Protocol': '1.0',
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Max-Age': '1209600',
- 'Access-Control-Expose-Headers':
- 'X-Plex-Client-Identifier',
- 'Content-Type': 'text/xml;charset=utf-8'
- }.update(headers))
- elif "/subscribe" in request_path:
- headers['Content-Type'] = 'text/xml;charset=utf-8'
- headers = clientinfo.getXArgsDeviceInfo(options=headers,
- include_token=False)
- self.response(v.COMPANION_OK_MESSAGE, headers)
- protocol = params.get('protocol')
- host = self.client_address[0]
- port = params.get('port')
- uuid = self.headers.get('X-Plex-Client-Identifier')
- command_id = params.get('commandID', 0)
- sub_mgr.add_subscriber(protocol,
- host,
- port,
- uuid,
- command_id)
- elif "/unsubscribe" in request_path:
- headers['Content-Type'] = 'text/xml;charset=utf-8'
- headers = clientinfo.getXArgsDeviceInfo(options=headers,
- include_token=False)
- self.response(v.COMPANION_OK_MESSAGE, headers)
- uuid = self.headers.get('X-Plex-Client-Identifier') \
- or self.client_address[0]
- sub_mgr.remove_subscriber(uuid)
- else:
- # Throw it to companion.py
- companion.process_command(request_path, params)
- headers['Content-Type'] = 'text/xml;charset=utf-8'
- headers = clientinfo.getXArgsDeviceInfo(options=headers,
- include_token=False)
- self.response(v.COMPANION_OK_MESSAGE, headers)
-
-
-class PKCHTTPServer(ThreadingHTTPServer):
- def __init__(self, client, subscription_manager, *args, **kwargs):
- """
- client: Class handle to plexgdm.plexgdm. We can thus ask for an up-to-
- date serverlist without instantiating anything
-
- same for SubscriptionMgr
- """
- self.client = client
- self.subscription_manager = subscription_manager
- super().__init__(*args, **kwargs)
diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py
deleted file mode 100644
index 652d1bbc..00000000
--- a/resources/lib/plexbmchelper/plexgdm.py
+++ /dev/null
@@ -1,314 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-PlexGDM.py - Version 0.2
-
-This class implements the Plex GDM (G'Day Mate) protocol to discover
-local Plex Media Servers. Also allow client registration into all local
-media servers.
-
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
-MA 02110-1301, USA.
-"""
-import logging
-import socket
-import threading
-import time
-
-from ..downloadutils import DownloadUtils as DU
-from .. import utils, app, variables as v
-
-###############################################################################
-
-log = logging.getLogger('PLEX.plexgdm')
-
-###############################################################################
-
-
-class plexgdm(object):
-
- def __init__(self):
- self.discover_message = 'M-SEARCH * HTTP/1.0'
- self.client_header = '* HTTP/1.0'
- self.client_data = None
- self.update_sock = None
- self.discover_t = None
- self.register_t = None
-
- self._multicast_address = '239.0.0.250'
- self.discover_group = (self._multicast_address, 32414)
- self.client_register_group = (self._multicast_address, 32413)
- self.client_update_port = int(utils.settings('companionUpdatePort'))
-
- self.server_list = []
- self.discovery_interval = 120
-
- self._discovery_is_running = False
- self._registration_is_running = False
-
- self.client_registered = False
- self.download = DU().downloadUrl
-
- def clientDetails(self):
- self.client_data = (
- "Content-Type: plex/media-player\n"
- "Resource-Identifier: %s\n"
- "Name: %s\n"
- "Port: %s\n"
- "Product: %s\n"
- "Version: %s\n"
- "Protocol: plex\n"
- "Protocol-Version: 1\n"
- "Protocol-Capabilities: timeline,playback,navigation,"
- "playqueues\n"
- "Device-Class: HTPC\n"
- ) % (
- v.PKC_MACHINE_IDENTIFIER,
- v.DEVICENAME,
- v.COMPANION_PORT,
- v.ADDON_NAME,
- v.ADDON_VERSION
- )
-
- def getClientDetails(self):
- return self.client_data
-
- def register_as_client(self):
- """
- Registers PKC's Plex Companion to the PMS
- """
- try:
- log.debug("Sending registration data: HELLO %s\n%s"
- % (self.client_header, self.client_data))
- msg = 'HELLO {}\n{}'.format(self.client_header, self.client_data)
- self.update_sock.sendto(msg.encode('utf-8'),
- self.client_register_group)
- log.debug('(Re-)registering PKC Plex Companion successful')
- except Exception as exc:
- log.error("Unable to send registration message. Error: %s", exc)
-
- def client_update(self):
- self.update_sock = socket.socket(socket.AF_INET,
- socket.SOCK_DGRAM,
- socket.IPPROTO_UDP)
- update_sock = self.update_sock
-
- # Set socket reuse, may not work on all OSs.
- try:
- update_sock.setsockopt(socket.SOL_SOCKET,
- socket.SO_REUSEADDR,
- 1)
- except Exception:
- pass
-
- # Attempt to bind to the socket to recieve and send data. If we cant
- # do this, then we cannot send registration
- try:
- update_sock.bind(('0.0.0.0', self.client_update_port))
- except Exception:
- log.error("Unable to bind to port [%s] - Plex Companion will not "
- "be registered. Change the Plex Companion update port!"
- % self.client_update_port)
- if utils.settings('companion_show_gdm_port_warning') == 'true':
- from ..windows import optionsdialog
- # Plex Companion could not open the GDM port. Please change it
- # in the PKC settings.
- if optionsdialog.show(utils.lang(29999),
- 'Port %s\n%s' % (self.client_update_port,
- utils.lang(39079)),
- utils.lang(30013), # Never show again
- utils.lang(186)) == 0:
- utils.settings('companion_show_gdm_port_warning',
- value='false')
- from xbmc import executebuiltin
- executebuiltin(
- 'Addon.OpenSettings(plugin.video.plexkodiconnect)')
- return
-
- update_sock.setsockopt(socket.IPPROTO_IP,
- socket.IP_MULTICAST_TTL,
- 255)
- update_sock.setsockopt(socket.IPPROTO_IP,
- socket.IP_ADD_MEMBERSHIP,
- socket.inet_aton(
- self._multicast_address) +
- socket.inet_aton('0.0.0.0'))
- update_sock.setblocking(0)
-
- # Send initial client registration
- self.register_as_client()
-
- # Now, listen format client discovery reguests and respond.
- while self._registration_is_running:
- try:
- data, addr = update_sock.recvfrom(1024)
- data = data.decode()
- log.debug("Recieved UDP packet from [%s] containing [%s]"
- % (addr, data.strip()))
- except socket.error:
- pass
- else:
- if "M-SEARCH * HTTP/1." in data:
- log.debug('Detected client discovery request from %s. '
- 'Replying', addr)
- message = f'HTTP/1.0 200 OK\n{self.client_data}'.encode()
- try:
- update_sock.sendto(message, addr)
- except Exception:
- log.error("Unable to send client update message")
- else:
- log.debug("Sent registration data HTTP/1.0 200 OK")
- self.client_registered = True
- app.APP.monitor.waitForAbort(0.5)
- log.info("Client Update loop stopped")
- # When we are finished, then send a final goodbye message to
- # deregister cleanly.
- log.debug("Sending registration data: BYE %s\n%s"
- % (self.client_header, self.client_data))
- try:
- update_sock.sendto("BYE %s\n%s"
- % (self.client_header, self.client_data),
- self.client_register_group)
- except Exception:
- log.error("Unable to send client update message")
- self.client_registered = False
-
- def check_client_registration(self):
- if not self.client_registered:
- log.debug('Client has not been marked as registered')
- return False
- if not self.server_list:
- log.info("Server list is empty. Unable to check")
- return False
- for server in self.server_list:
- if server['uuid'] == app.CONN.machine_identifier:
- media_server = server['server']
- media_port = server['port']
- scheme = server['protocol']
- break
- else:
- log.info("Did not find our server!")
- return False
-
- log.debug("Checking server [%s] on port [%s]"
- % (media_server, media_port))
- xml = self.download(
- '%s://%s:%s/clients' % (scheme, media_server, media_port))
- try:
- xml[0].attrib
- except (TypeError, IndexError, AttributeError):
- log.error('Could not download clients for %s' % media_server)
- return False
- registered = False
- for client in xml:
- if (client.attrib.get('machineIdentifier') ==
- v.PKC_MACHINE_IDENTIFIER):
- registered = True
- if registered:
- return True
- else:
- log.info("Client registration not found. "
- "Client data is: %s" % xml)
- return False
-
- def getServerList(self):
- return self.server_list
-
- def discover(self):
- currServer = app.CONN.server
- if not currServer:
- return
- currServerProt, currServerIP, currServerPort = \
- currServer.split(':')
- currServerIP = currServerIP.replace('/', '')
- # Currently active server was not discovered via GDM; ADD
- self.server_list = [{
- 'port': currServerPort,
- 'protocol': currServerProt,
- 'class': None,
- 'content-type': 'plex/media-server',
- 'discovery': 'auto',
- 'master': 1,
- 'owned': '1',
- 'role': 'master',
- 'server': currServerIP,
- 'serverName': app.CONN.server_name,
- 'updated': int(time.time()),
- 'uuid': app.CONN.machine_identifier,
- 'version': 'irrelevant'
- }]
-
- def setInterval(self, interval):
- self.discovery_interval = interval
-
- def stop_all(self):
- self.stop_discovery()
- self.stop_registration()
-
- def stop_discovery(self):
- if self._discovery_is_running:
- log.info("Discovery shutting down")
- self._discovery_is_running = False
- self.discover_t.join()
- del self.discover_t
- else:
- log.info("Discovery not running")
-
- def stop_registration(self):
- if self._registration_is_running:
- log.info("Registration shutting down")
- self._registration_is_running = False
- self.register_t.join()
- del self.register_t
- else:
- log.info("Registration not running")
-
- def run_discovery_loop(self):
- # Run initial discovery
- self.discover()
-
- discovery_count = 0
- while self._discovery_is_running:
- discovery_count += 1
- if discovery_count > self.discovery_interval:
- self.discover()
- discovery_count = 0
- app.APP.monitor.waitForAbort(0.5)
-
- def start_discovery(self, daemon=False):
- if not self._discovery_is_running:
- log.info("Discovery starting up")
- self._discovery_is_running = True
- self.discover_t = threading.Thread(target=self.run_discovery_loop)
- self.discover_t.setDaemon(daemon)
- self.discover_t.start()
- else:
- log.info("Discovery already running")
-
- def start_registration(self, daemon=False):
- if not self._registration_is_running:
- log.info("Registration starting up")
- self._registration_is_running = True
- self.register_t = threading.Thread(target=self.client_update)
- self.register_t.setDaemon(daemon)
- self.register_t.start()
- else:
- log.info("Registration already running")
-
- def start_all(self, daemon=False):
- self.start_discovery(daemon)
- if utils.settings('plexCompanion') == 'true':
- self.start_registration(daemon)
diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py
deleted file mode 100644
index 4e9767f2..00000000
--- a/resources/lib/plexbmchelper/subscribers.py
+++ /dev/null
@@ -1,470 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Manages getting playstate from Kodi and sending it to the PMS as well as
-subscribed Plex Companion clients.
-"""
-from logging import getLogger
-from threading import Thread
-
-from ..downloadutils import DownloadUtils as DU
-from .. import timing
-from .. import app
-from .. import variables as v
-from .. import json_rpc as js
-from .. import playqueue as PQ
-
-###############################################################################
-LOG = getLogger('PLEX.subscribers')
-###############################################################################
-
-# What is Companion controllable?
-CONTROLLABLE = {
- v.PLEX_PLAYLIST_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
- 'subtitleStream,seekTo,skipPrevious,skipNext,'
- 'stepBack,stepForward',
- v.PLEX_PLAYLIST_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
- 'skipPrevious,skipNext,stepBack,stepForward',
- v.PLEX_PLAYLIST_TYPE_PHOTO: 'playPause,stop,skipPrevious,skipNext'
-}
-
-STREAM_DETAILS = {
- 'video': 'currentvideostream',
- 'audio': 'currentaudiostream',
- 'subtitle': 'currentsubtitle'
-}
-
-XML = ('%s\n'
- ' \n'
- ' \n'
- ' \n'
- '\n') % (v.XML_HEADER,
- v.PLEX_PLAYLIST_TYPE_VIDEO,
- v.PLEX_PLAYLIST_TYPE_AUDIO,
- v.PLEX_PLAYLIST_TYPE_PHOTO)
-
-# Headers are different for Plex Companion - use these for PMS notifications
-HEADERS_PMS = {
- 'Connection': 'keep-alive',
- 'Accept': 'text/plain, */*; q=0.01',
- 'Accept-Language': 'en',
- 'Accept-Encoding': 'gzip, deflate',
- 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.DEVICE)
-}
-
-
-def params_pms():
- """
- Returns the url parameters for communicating with the PMS
- """
- return {
- 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
- 'X-Plex-Device': v.DEVICE,
- 'X-Plex-Device-Name': v.DEVICENAME,
- 'X-Plex-Model': v.MODEL,
- 'X-Plex-Platform': v.PLATFORM,
- 'X-Plex-Platform-Version': v.PLATFORM_VERSION,
- 'X-Plex-Product': v.ADDON_NAME,
- 'X-Plex-Version': v.ADDON_VERSION,
- }
-
-
-def headers_companion_client():
- """
- Headers are different for Plex Companion - use these for a Plex Companion
- client
- """
- return {
- 'Content-Type': 'application/xml',
- 'Connection': 'Keep-Alive',
- 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
- 'X-Plex-Device-Name': v.DEVICENAME,
- 'X-Plex-Platform': v.PLATFORM,
- 'X-Plex-Platform-Version': v.PLATFORM_VERSION,
- 'X-Plex-Product': v.ADDON_NAME,
- 'X-Plex-Version': v.ADDON_VERSION,
- 'Accept-Encoding': 'gzip, deflate',
- 'Accept-Language': 'en,*'
- }
-
-
-def update_player_info(playerid):
- """
- Updates all player info for playerid [int] in state.py.
- """
- app.PLAYSTATE.player_states[playerid].update(js.get_player_props(playerid))
- app.PLAYSTATE.player_states[playerid]['volume'] = js.get_volume()
- app.PLAYSTATE.player_states[playerid]['muted'] = js.get_muted()
-
-
-class SubscriptionMgr(object):
- """
- Manages Plex companion subscriptions
- """
- def __init__(self, request_mgr, player):
- self.serverlist = []
- self.subscribers = {}
- self.info = {}
- self.server = ""
- self.protocol = "http"
- self.port = ""
- self.isplaying = False
- self.location = 'navigation'
- # In order to be able to signal a stop at the end
- self.last_params = {}
- self.lastplayers = {}
- # In order to signal a stop to Plex Web ONCE on playback stop
- self.stop_sent_to_web = True
- self.request_mgr = request_mgr
-
- def _server_by_host(self, host):
- if len(self.serverlist) == 1:
- return self.serverlist[0]
- for server in self.serverlist:
- if (server.get('serverName') in host or
- server.get('server') in host):
- return server
- return {}
-
- @staticmethod
- def _get_correct_position(info, playqueue):
- """
- Kodi tells us the PLAYLIST position, not PLAYQUEUE position, if the
- user initiated playback of a playlist
- """
- if playqueue.kodi_playlist_playback:
- position = 0
- else:
- position = info['position'] or 0
- return position
-
- def msg(self, players):
- """
- Returns a timeline xml as str
- (xml containing video, audio, photo player state)
- """
- self.isplaying = False
- self.location = 'navigation'
- answ = str(XML)
- timelines = {
- v.PLEX_PLAYLIST_TYPE_VIDEO: None,
- v.PLEX_PLAYLIST_TYPE_AUDIO: None,
- v.PLEX_PLAYLIST_TYPE_PHOTO: None
- }
- for typus in timelines:
- if players.get(v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]) is None:
- timeline = {
- 'controllable': CONTROLLABLE[typus],
- 'type': typus,
- 'state': 'stopped'
- }
- else:
- timeline = self._timeline_dict(players[
- v.KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE[typus]],
- typus)
- timelines[typus] = self._dict_to_xml(timeline)
- timelines.update({'command_id': '{command_id}',
- 'location': self.location})
- return answ.format(**timelines)
-
- @staticmethod
- def _dict_to_xml(dictionary):
- """
- Returns the string 'key1="value1" key2="value2" ...' for dictionary
- """
- answ = ''
- for key, value in dictionary.items():
- answ += '%s="%s" ' % (key, value)
- return answ
-
- def _timeline_dict(self, player, ptype):
- with app.APP.lock_playqueues:
- playerid = player['playerid']
- info = app.PLAYSTATE.player_states[playerid]
- playqueue = PQ.PLAYQUEUES[playerid]
- position = self._get_correct_position(info, playqueue)
- try:
- item = playqueue.items[position]
- except IndexError:
- # E.g. for direct path playback for single item
- return {
- 'controllable': CONTROLLABLE[ptype],
- 'type': ptype,
- 'state': 'stopped'
- }
- self.isplaying = True
- self.stop_sent_to_web = False
- if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
- v.PLEX_PLAYLIST_TYPE_PHOTO):
- self.location = 'fullScreenVideo'
- pbmc_server = app.CONN.server
- if pbmc_server:
- (self.protocol, self.server, self.port) = pbmc_server.split(':')
- self.server = self.server.replace('/', '')
- status = 'paused' if int(info['speed']) == 0 else 'playing'
- duration = timing.kodi_time_to_millis(info['totaltime'])
- shuffle = '1' if info['shuffled'] else '0'
- mute = '1' if info['muted'] is True else '0'
- answ = {
- 'controllable': CONTROLLABLE[ptype],
- 'protocol': self.protocol,
- 'address': self.server,
- 'port': self.port,
- 'machineIdentifier': app.CONN.machine_identifier,
- 'state': status,
- 'type': ptype,
- 'itemType': ptype,
- 'time': timing.kodi_time_to_millis(info['time']),
- 'duration': duration,
- 'seekRange': '0-%s' % duration,
- 'shuffle': shuffle,
- 'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']],
- 'volume': info['volume'],
- 'mute': mute,
- 'mediaIndex': 0, # Still to implement from here
- 'partIndex': 0,
- 'partCount': 1,
- 'providerIdentifier': 'com.plexapp.plugins.library',
- }
- # Get the plex id from the PKC playqueue not info, as Kodi jumps to
- # next playqueue element way BEFORE kodi monitor onplayback is
- # called
- if item.plex_id:
- answ['key'] = '/library/metadata/%s' % item.plex_id
- answ['ratingKey'] = item.plex_id
- # PlayQueue stuff
- if info['container_key']:
- answ['containerKey'] = info['container_key']
- if (info['container_key'] is not None and
- info['container_key'].startswith('/playQueues')):
- answ['playQueueID'] = playqueue.id
- answ['playQueueVersion'] = playqueue.version
- answ['playQueueItemID'] = item.id
- if playqueue.items[position].guid:
- answ['guid'] = item.guid
- # Temp. token set?
- if app.CONN.plex_transient_token:
- answ['token'] = app.CONN.plex_transient_token
- elif playqueue.plex_transient_token:
- answ['token'] = playqueue.plex_transient_token
- # Process audio and subtitle streams
- if ptype == v.PLEX_PLAYLIST_TYPE_VIDEO:
- answ['videoStreamID'] = str(item.current_plex_video_stream)
- answ['audioStreamID'] = str(item.current_plex_audio_stream)
- # Mind the zero - meaning subs are deactivated
- answ['subtitleStreamID'] = str(item.current_plex_sub_stream or 0)
- return answ
-
- def signal_stop(self):
- """
- Externally called on PKC shutdown to ensure that PKC signals a stop to
- the PMS. Otherwise, PKC might be stuck at "currently playing"
- """
- LOG.info('Signaling a complete stop to PMS')
- # To avoid RuntimeError, don't use self.lastplayers
- for playerid in (0, 1, 2):
- self.last_params['state'] = 'stopped'
- self._send_pms_notification(playerid,
- self.last_params,
- timeout=0.0001)
-
- def update_command_id(self, uuid, command_id):
- """
- Updates the Plex Companien client with the machine identifier uuid with
- command_id
- """
- with app.APP.lock_subscriber:
- if command_id and self.subscribers.get(uuid):
- self.subscribers[uuid].command_id = int(command_id)
-
- def _playqueue_init_done(self, players):
- """
- update_player_info() can result in values BEFORE kodi monitor is called.
- Hence we'd have a missmatch between the state.PLAYER_STATES and our
- playqueues.
- """
- for player in list(players.values()):
- info = app.PLAYSTATE.player_states[player['playerid']]
- playqueue = PQ.PLAYQUEUES[player['playerid']]
- position = self._get_correct_position(info, playqueue)
- try:
- item = playqueue.items[position]
- except IndexError:
- # E.g. for direct path playback for single item
- return False
- if item.plex_id != info['plex_id']:
- # Kodi playqueue already progressed; need to wait until
- # everything is loaded
- return False
- return True
-
- def notify(self):
- """
- Causes PKC to tell the PMS and Plex Companion players to receive a
- notification what's being played.
- """
- with app.APP.lock_subscriber:
- self._cleanup()
- # Get all the active/playing Kodi players (video, audio, pictures)
- players = js.get_players()
- # Update the PKC info with what's playing on the Kodi side
- for player in list(players.values()):
- update_player_info(player['playerid'])
- # Check whether we can use the CURRENT info or whether PKC is still
- # initializing
- if self._playqueue_init_done(players) is False:
- LOG.debug('PKC playqueue is still initializing - skip update')
- return
- self._notify_server(players)
- if self.subscribers:
- msg = self.msg(players)
- for subscriber in list(self.subscribers.values()):
- subscriber.send_update(msg)
- self.lastplayers = players
-
- def _notify_server(self, players):
- for typus, player in players.items():
- self._send_pms_notification(
- player['playerid'], self._get_pms_params(player['playerid']))
- try:
- del self.lastplayers[typus]
- except KeyError:
- pass
- # Process the players we have left (to signal a stop)
- for player in list(self.lastplayers.values()):
- self.last_params['state'] = 'stopped'
- self._send_pms_notification(player['playerid'], self.last_params)
-
- def _get_pms_params(self, playerid):
- info = app.PLAYSTATE.player_states[playerid]
- playqueue = PQ.PLAYQUEUES[playerid]
- position = self._get_correct_position(info, playqueue)
- try:
- item = playqueue.items[position]
- except IndexError:
- return self.last_params
- status = 'paused' if int(info['speed']) == 0 else 'playing'
- params = {
- 'state': status,
- 'ratingKey': item.plex_id,
- 'key': '/library/metadata/%s' % item.plex_id,
- 'time': timing.kodi_time_to_millis(info['time']),
- 'duration': timing.kodi_time_to_millis(info['totaltime'])
- }
- if info['container_key'] is not None:
- # params['containerKey'] = info['container_key']
- if info['container_key'].startswith('/playQueues/'):
- # params['playQueueVersion'] = playqueue.version
- # params['playQueueID'] = playqueue.id
- params['playQueueItemID'] = item.id
- self.last_params = params
- return params
-
- def _send_pms_notification(self, playerid, params, timeout=None):
- """
- Pass a really low timeout in seconds if shutting down Kodi and we don't
- need the PMS' response
- """
- serv = self._server_by_host(self.server)
- playqueue = PQ.PLAYQUEUES[playerid]
- xargs = params_pms()
- xargs.update(params)
- if app.CONN.plex_transient_token:
- xargs['X-Plex-Token'] = app.CONN.plex_transient_token
- elif playqueue.plex_transient_token:
- xargs['X-Plex-Token'] = playqueue.plex_transient_token
- elif app.ACCOUNT.pms_token:
- xargs['X-Plex-Token'] = app.ACCOUNT.pms_token
- url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
- serv.get('server', 'localhost'),
- serv.get('port', '32400'))
- DU().downloadUrl(url,
- authenticate=False,
- parameters=xargs,
- headerOverride=HEADERS_PMS,
- timeout=timeout)
- LOG.debug("Sent server notification with parameters: %s to %s",
- xargs, url)
-
- def add_subscriber(self, protocol, host, port, uuid, command_id):
- """
- Adds a new Plex Companion subscriber to PKC.
- """
- subscriber = Subscriber(protocol,
- host,
- port,
- uuid,
- command_id,
- self,
- self.request_mgr)
- with app.APP.lock_subscriber:
- self.subscribers[subscriber.uuid] = subscriber
- return subscriber
-
- 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 app.APP.lock_subscriber:
- for subscriber in list(self.subscribers.values()):
- if subscriber.uuid == uuid or subscriber.host == uuid:
- subscriber.cleanup()
- del self.subscribers[subscriber.uuid]
-
- def _cleanup(self):
- for subscriber in list(self.subscribers.values()):
- if subscriber.age > 30:
- subscriber.cleanup()
- del self.subscribers[subscriber.uuid]
-
-
-class Subscriber(object):
- """
- Plex Companion subscribing device
- """
- def __init__(self, protocol, host, port, uuid, command_id, sub_mgr,
- request_mgr):
- self.protocol = protocol or "http"
- self.host = host
- self.port = port or 32400
- self.uuid = uuid or host
- self.command_id = int(command_id) or 0
- self.age = 0
- self.sub_mgr = sub_mgr
- self.request_mgr = request_mgr
-
- def __eq__(self, other):
- return self.uuid == other.uuid
-
- def cleanup(self):
- """
- Closes the connection to the Plex Companion client
- """
- self.request_mgr.closeConnection(self.protocol, self.host, self.port)
-
- def send_update(self, msg):
- """
- Sends msg to the Plex Companion client (via .../:/timeline)
- """
- self.age += 1
- msg = msg.format(command_id=self.command_id)
- LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
- self.uuid, self.command_id, msg)
- url = '%s://%s:%s/:/timeline' % (self.protocol, self.host, self.port)
- thread = Thread(target=self._threaded_send, args=(url, msg))
- thread.start()
-
- def _threaded_send(self, url, msg):
- """
- Threaded POST request, because they stall due to response missing
- the Content-Length header :-(
- """
- response = DU().downloadUrl(url,
- action_type="POST",
- postBody=msg,
- authenticate=False,
- headerOverride=headers_companion_client())
- if response in (False, None, 401):
- self.sub_mgr.remove_subscriber(self.uuid)
diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py
index ce3e9678..033613e5 100644
--- a/resources/lib/service_entry.py
+++ b/resources/lib/service_entry.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import logging
import sys
+
import xbmc
import xbmcvfs
@@ -11,14 +12,12 @@ from . import kodimonitor
from . import sync, library_sync
from . import websocket_client
from . import plex_companion
-from . import plex_functions as PF, playqueue as PQ
+from . import plex_functions as PF
from . import playback_starter
-from . import playqueue
from . import variables as v
from . import app
from . import loghandler
from . import backgroundthread
-from . import skip_plex_intro
from .windows import userselect
###############################################################################
@@ -34,7 +33,6 @@ WINDOW_PROPERTIES = (
class Service(object):
ws = None
sync = None
- plexcompanion = None
def __init__(self):
self._init_done = False
@@ -100,7 +98,6 @@ class Service(object):
self.setup = None
self.pms_ws = None
self.alexa_ws = None
- self.playqueue = None
# Flags for other threads
self.connection_check_running = False
self.auth_running = False
@@ -437,8 +434,6 @@ class Service(object):
app.init()
app.APP.monitor = kodimonitor.KodiMonitor()
app.APP.player = xbmc.Player()
- # Initialize the PKC playqueues
- PQ.init_playqueues()
# Server auto-detect
self.setup = initialsetup.InitialSetup()
@@ -448,8 +443,11 @@ class Service(object):
self.pms_ws = websocket_client.get_pms_websocketapp()
self.alexa_ws = websocket_client.get_alexa_websocketapp()
self.sync = sync.Sync()
- self.plexcompanion = plex_companion.PlexCompanion()
- self.playqueue = playqueue.PlayqueueMonitor()
+ self.companion_playstate_mgr = plex_companion.PlaystateMgr()
+ if utils.settings('plexCompanion') == 'true':
+ self.companion_listener = plex_companion.Listener(self.companion_playstate_mgr)
+ else:
+ self.companion_listener = None
# Main PKC program loop
while not self.should_cancel():
@@ -548,13 +546,11 @@ class Service(object):
self.startup_completed = True
self.pms_ws.start()
self.sync.start()
- self.plexcompanion.start()
- self.playqueue.start()
+ self.companion_playstate_mgr.start()
+ if self.companion_listener is not None:
+ self.companion_listener.start()
self.alexa_ws.start()
- elif app.APP.is_playing:
- skip_plex_intro.check()
-
xbmc.sleep(200)
# EXITING PKC
diff --git a/resources/lib/skip_plex_intro.py b/resources/lib/skip_plex_intro.py
index 3ab4cddc..ec6d76ad 100644
--- a/resources/lib/skip_plex_intro.py
+++ b/resources/lib/skip_plex_intro.py
@@ -32,8 +32,6 @@ def skip_intro(intros):
def check():
with app.APP.lock_playqueues:
- if len(app.PLAYSTATE.active_players) != 1:
- return
playerid = list(app.PLAYSTATE.active_players)[0]
intros = app.PLAYSTATE.player_states[playerid]['intro_markers']
if not intros:
diff --git a/resources/lib/utils.py b/resources/lib/utils.py
index 11db93c0..9355c5b8 100644
--- a/resources/lib/utils.py
+++ b/resources/lib/utils.py
@@ -556,6 +556,15 @@ def reset(ask_user=True):
reboot_kodi()
+def log_xml(xml, logger):
+ """
+ Logs an etree xml
+ """
+ string = undefused_etree.tostring(xml, encoding='utf-8')
+ string = string.decode('utf-8')
+ logger('\n' + string)
+
+
def compare_version(current, minimum):
"""
Returns True if current is >= then minimum. False otherwise. Returns True
diff --git a/resources/lib/variables.py b/resources/lib/variables.py
index 8174c5ea..ada62b23 100644
--- a/resources/lib/variables.py
+++ b/resources/lib/variables.py
@@ -30,8 +30,9 @@ ADDON_FOLDER = xbmcvfs.translatePath('special://home')
ADDON_PROFILE = xbmcvfs.translatePath(_ADDON.getAddonInfo('profile'))
# Used e.g. for json_rpc
-KODI_VIDEO_PLAYER_ID = 1
KODI_AUDIO_PLAYER_ID = 0
+KODI_VIDEO_PLAYER_ID = 1
+KODI_PHOTO_PLAYER_ID = 2
KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1)
KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2])