bba42bb1bb
- Using add-on paths in the Kodi library, we need to make sure that the subsequent call of xbmc.setresolvedUrl fails or is forwarded once again
240 lines
9.4 KiB
Python
240 lines
9.4 KiB
Python
"""
|
|
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
|
"""
|
|
from logging import getLogger
|
|
from threading import Thread
|
|
from re import compile as re_compile
|
|
|
|
from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep
|
|
|
|
from utils import thread_methods
|
|
import playlist_func as PL
|
|
from PlexFunctions import GetAllPlexChildren
|
|
from PlexAPI import API
|
|
from plexbmchelper.subscribers import LOCK
|
|
from playback import play_xml
|
|
import json_rpc as js
|
|
import variables as v
|
|
import kodidb_functions as kodidb
|
|
|
|
###############################################################################
|
|
LOG = getLogger("PLEX." + __name__)
|
|
|
|
PLUGIN = 'plugin://%s' % v.ADDON_ID
|
|
REGEX = re_compile(r'''plex_id=(\d+)''')
|
|
|
|
# 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 LOCK:
|
|
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 = PlayList(PLAYLIST_MUSIC)
|
|
elif playqueue.type == v.KODI_TYPE_VIDEO:
|
|
playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO)
|
|
else:
|
|
# Currently, only video or audio playqueues available
|
|
playqueue.kodi_pl = PlayList(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
|
|
"""
|
|
with LOCK:
|
|
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 Playlist_Object
|
|
"""
|
|
xml = 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)
|
|
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
|
|
playqueue.plex_transient_token = transient_token
|
|
LOG.debug('Firing up Kodi player')
|
|
Player().play(playqueue.kodi_pl, None, False, 0)
|
|
return playqueue
|
|
|
|
|
|
def update_playqueue_from_PMS(playqueue,
|
|
playqueue_id=None,
|
|
repeat=None,
|
|
offset=None,
|
|
transient_token=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', playqueue_id, offset, repeat)
|
|
# Safe transient token from being deleted
|
|
if transient_token is None:
|
|
transient_token = playqueue.plex_transient_token
|
|
with LOCK:
|
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
|
playqueue.clear()
|
|
try:
|
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
|
except PL.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
|
|
play_xml(playqueue, xml, offset)
|
|
|
|
|
|
@thread_methods(add_suspends=['PMS_STATUS'])
|
|
class PlayqueueMonitor(Thread):
|
|
"""
|
|
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):
|
|
"""
|
|
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
|
"""
|
|
old = list(playqueue.items)
|
|
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.stopped():
|
|
# 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 = REGEX.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('Detected playqueue item %s moved to position %s',
|
|
i + j, i)
|
|
PL.move_playlist_item(playqueue, i + j, i)
|
|
del old[j], index[j]
|
|
break
|
|
else:
|
|
LOG.debug('Detected new Kodi element at position %s: %s ',
|
|
i, new_item)
|
|
if playqueue.id is None:
|
|
LOG.debug('Not yet initiating playback')
|
|
return
|
|
try:
|
|
PL.add_item_to_PMS_playlist(playqueue,
|
|
i,
|
|
kodi_item=new_item)
|
|
except PL.PlaylistError:
|
|
# Could not add the element
|
|
pass
|
|
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.stopped():
|
|
# 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)
|
|
PL.delete_playlist_item_from_PMS(playqueue, i)
|
|
LOG.debug('Done comparing playqueues')
|
|
|
|
def run(self):
|
|
stopped = self.stopped
|
|
suspended = self.suspended
|
|
LOG.info("----===## Starting PlayqueueMonitor ##===----")
|
|
tested = False
|
|
while not stopped():
|
|
while suspended():
|
|
if stopped():
|
|
break
|
|
sleep(1000)
|
|
with LOCK:
|
|
for playqueue in PLAYQUEUES:
|
|
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
|
if playqueue.old_kodi_pl != kodi_pl:
|
|
# compare old and new playqueue
|
|
self._compare_playqueues(playqueue, kodi_pl)
|
|
playqueue.old_kodi_pl = list(kodi_pl)
|
|
with kodidb.GetKodiDB('video') as kodi_db:
|
|
# Setup the paths for addon-paths (even when using direct paths)
|
|
if kodi_db.check_path() and not tested:
|
|
tested = True
|
|
LOG.error('NOW!')
|
|
sleep(50)
|
|
LOG.info("----===## PlayqueueMonitor stopped ##===----")
|