PlexKodiConnect/resources/lib/playqueue.py

252 lines
10 KiB
Python
Raw Permalink Normal View History

2017-12-09 05:43:06 +11:00
"""
Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
"""
2017-12-10 00:35:08 +11:00
from logging import getLogger
from threading import Thread
2018-02-08 00:57:36 +11:00
from re import compile as re_compile
2016-12-28 03:33:52 +11:00
from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep
2016-12-28 03:33:52 +11:00
from utils import thread_methods
2016-12-28 03:33:52 +11:00
import playlist_func as PL
from PlexFunctions import GetAllPlexChildren
2017-03-06 03:51:58 +11:00
from PlexAPI import API
from plexbmchelper.subscribers import LOCK
from playback import play_xml
2017-12-09 05:43:06 +11:00
import json_rpc as js
2017-03-06 03:51:58 +11:00
import variables as v
import state
2016-12-28 03:33:52 +11:00
###############################################################################
2017-12-10 00:35:08 +11:00
LOG = getLogger("PLEX." + __name__)
2016-12-28 03:33:52 +11:00
PLUGIN = 'plugin://%s' % v.ADDON_ID
2018-02-08 00:57:36 +11:00
REGEX = re_compile(r'''plex_id=(\d+)''')
# Our PKC playqueues (3 instances of Playqueue_Object())
PLAYQUEUES = []
2016-12-28 03:33:52 +11:00
###############################################################################
def init_playqueues():
2016-12-28 03:33:52 +11:00
"""
Call this once on startup to initialize the PKC playqueue objects in
the list PLAYQUEUES
2016-12-28 03:33:52 +11:00
"""
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
2017-12-09 05:43:06 +11:00
for queue in js.get_playlists():
if queue['playlistid'] != i:
continue
2017-01-03 00:07:24 +11:00
playqueue = PL.Playqueue_Object()
playqueue.playlistid = i
2017-01-03 00:07:24 +11:00
playqueue.type = queue['type']
# Initialize each Kodi playlist
if playqueue.type == v.KODI_TYPE_AUDIO:
2017-01-03 00:07:24 +11:00
playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC)
elif playqueue.type == v.KODI_TYPE_VIDEO:
2017-01-03 00:07:24 +11:00
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)
2016-12-28 03:33:52 +11:00
2016-12-30 01:41:14 +11:00
2018-01-11 06:14:05 +11:00
def get_playqueue_from_type(kodi_playlist_type):
"""
2018-01-11 06:14:05 +11:00
Returns the playqueue according to the kodi_playlist_type ('video',
'audio', 'picture') passed in
"""
with LOCK:
for playqueue in PLAYQUEUES:
2018-01-11 06:14:05 +11:00
if playqueue.type == kodi_playlist_type:
break
else:
2018-01-11 06:14:05 +11:00
raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
return playqueue
2017-03-06 03:51:58 +11:00
2016-12-28 23:14:21 +11:00
def init_playqueue_from_plex_children(plex_id, transient_token=None):
"""
Init a new playqueue e.g. from an album. Alexa does this
2016-12-29 00:48:23 +11:00
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)
2018-02-12 00:42:49 +11:00
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
2016-12-29 05:38:43 +11:00
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)
2018-02-04 01:46:41 +11:00
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):
2018-02-12 00:57:39 +11:00
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
2018-02-08 00:57:36 +11:00
if 'id' in new_item:
identical = (old_item.kodi_id == new_item['id'] and
old_item.kodi_type == new_item['type'])
2018-02-08 00:57:36 +11:00
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)
with LOCK:
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)
with LOCK:
try:
if playqueue.id is None:
PL.init_Plex_playlist(playqueue, kodi_item=new_item)
else:
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):
2018-02-12 00:57:39 +11:00
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)
with LOCK:
PL.delete_playlist_item_from_PMS(playqueue, i)
LOG.debug('Done comparing playqueues')
def run(self):
2018-02-12 00:57:39 +11:00
stopped = self.stopped
suspended = self.suspended
LOG.info("----===## Starting PlayqueueMonitor ##===----")
2018-02-12 00:57:39 +11:00
while not stopped():
while suspended():
if stopped():
break
sleep(1000)
work = []
# Detect changed playqueues first, do the work afterwards
with LOCK:
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 state.DIRECT_PATHS or
state.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')
elif playqueue.pkc_edit:
playqueue.pkc_edit = False
LOG.debug('PKC edited the playqueue - skipping')
else:
# We do need to update our playqueues
work.append((playqueue, kodi_pl))
playqueue.old_kodi_pl = kodi_pl
# Now do the work - LOCK individual playqueue edits
for playqueue, kodi_pl in work:
self._compare_playqueues(playqueue, kodi_pl)
sleep(200)
LOG.info("----===## PlayqueueMonitor stopped ##===----")