From f2cd4d68eae73282b21f65d9da0872459efb581c Mon Sep 17 00:00:00 2001 From: croneter Date: Sun, 31 Oct 2021 10:44:27 +0100 Subject: [PATCH] Refactor playqueues --- resources/lib/app/__init__.py | 6 +- resources/lib/app/playqueues.py | 230 ++++++++++++++++++++ resources/lib/companion.py | 8 +- resources/lib/context_entry.py | 10 +- resources/lib/kodimonitor.py | 8 +- resources/lib/playback.py | 20 +- resources/lib/playlist_func.py | 111 ---------- resources/lib/playqueue.py | 232 --------------------- resources/lib/plex_companion/playqueue.py | 144 +++++++++++++ resources/lib/plex_companion/playstate.py | 20 +- resources/lib/plex_companion/processing.py | 23 +- resources/lib/plex_db/playlists.py | 2 +- resources/lib/service_entry.py | 8 +- 13 files changed, 433 insertions(+), 389 deletions(-) create mode 100644 resources/lib/app/playqueues.py delete mode 100644 resources/lib/playqueue.py create mode 100644 resources/lib/plex_companion/playqueue.py 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..3830e8d8 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: 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 515bf91e..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. 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/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 index 1cf70a70..16ba8417 100644 --- a/resources/lib/plex_companion/playstate.py +++ b/resources/lib/plex_companion/playstate.py @@ -5,13 +5,13 @@ 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 playqueue as PQ from .. import skip_plex_intro @@ -55,7 +55,7 @@ def get_correct_position(info, playqueue): def timeline_dict(playerid, typus): with app.APP.lock_playqueues: info = app.PLAYSTATE.player_states[playerid] - playqueue = PQ.PLAYQUEUES[playerid] + playqueue = app.PLAYQUEUES[playerid] position = get_correct_position(info, playqueue) try: item = playqueue.items[position] @@ -354,7 +354,21 @@ class PlaystateMgr(backgroundthread.KillableThread): self.close_requests_session() if self.wait_while_suspended(): break - # We will only become active if there's Kodi playback going on + # 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) diff --git a/resources/lib/plex_companion/processing.py b/resources/lib/plex_companion/processing.py index b984f7f3..0f3de1f1 100644 --- a/resources/lib/plex_companion/processing.py +++ b/resources/lib/plex_companion/processing.py @@ -14,7 +14,6 @@ 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 app from .. import exceptions @@ -87,9 +86,8 @@ def process_playlist(containerKey, typus, key, offset, token): # Get the playqueue ID _, container_key, query = PF.ParseContainerKey(containerKey) try: - playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[typus]) - except KeyError: + 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) @@ -99,8 +97,7 @@ def process_playlist(containerKey, typus, key, offset, token): 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]) + playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type) if key: _, key, _ = PF.ParseContainerKey(key) update_playqueue_from_PMS(playqueue, @@ -111,12 +108,12 @@ def process_playlist(containerKey, typus, key, offset, token): start_plex_id=key) -def process_streams(typus, video_stream_id, audio_stream_id, subtitle_stream_id): +def process_streams(plex_type, video_stream_id, audio_stream_id, + subtitle_stream_id): """ Plex Companion client adjusted audio or subtitle stream """ - playqueue = PQ.get_playqueue_from_type( - v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[typus]) + 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, @@ -135,12 +132,10 @@ def process_refresh(playqueue_id): 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 = app.PLAYQUEUES.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']]) + playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type']) update_playqueue_from_PMS(playqueue, playqueue_id) @@ -155,7 +150,7 @@ def skip_to(playqueue_item_id, key): 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/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/service_entry.py b/resources/lib/service_entry.py index d33a288e..033613e5 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -12,9 +12,8 @@ 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 @@ -99,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 @@ -436,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() @@ -452,7 +448,6 @@ class Service(object): self.companion_listener = plex_companion.Listener(self.companion_playstate_mgr) else: self.companion_listener = None - self.playqueue = playqueue.PlayqueueMonitor() # Main PKC program loop while not self.should_cancel(): @@ -554,7 +549,6 @@ class Service(object): self.companion_playstate_mgr.start() if self.companion_listener is not None: self.companion_listener.start() - self.playqueue.start() self.alexa_ws.start() xbmc.sleep(200)