Refactor playqueues

This commit is contained in:
croneter 2021-10-31 10:44:27 +01:00
parent edf0cd9a54
commit f2cd4d68ea
13 changed files with 433 additions and 389 deletions

View file

@ -9,12 +9,14 @@ from .application import App
from .connection import Connection from .connection import Connection
from .libsync import Sync from .libsync import Sync
from .playstate import PlayState from .playstate import PlayState
from .playqueues import Playqueues
ACCOUNT = None ACCOUNT = None
APP = None APP = None
CONN = None CONN = None
SYNC = None SYNC = None
PLAYSTATE = None PLAYSTATE = None
PLAYQUEUES = None
def init(entrypoint=False): def init(entrypoint=False):
@ -22,13 +24,15 @@ def init(entrypoint=False):
entrypoint=True initiates only the bare minimum - for other PKC python entrypoint=True initiates only the bare minimum - for other PKC python
instances instances
""" """
global ACCOUNT, APP, CONN, SYNC, PLAYSTATE global ACCOUNT, APP, CONN, SYNC, PLAYSTATE, PLAYQUEUES
APP = App(entrypoint) APP = App(entrypoint)
CONN = Connection(entrypoint) CONN = Connection(entrypoint)
ACCOUNT = Account(entrypoint) ACCOUNT = Account(entrypoint)
SYNC = Sync(entrypoint) SYNC = Sync(entrypoint)
if not entrypoint: if not entrypoint:
PLAYSTATE = PlayState() PLAYSTATE = PlayState()
PLAYQUEUES = Playqueues()
def reload(): def reload():
""" """

View file

@ -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)

View file

@ -6,8 +6,10 @@ Processes Plex companion inputs from the plexbmchelper to Kodi commands
from logging import getLogger from logging import getLogger
from xbmc import Player from xbmc import Player
from . import playqueue as PQ, plex_functions as PF from . import plex_functions as PF
from . import json_rpc as js, variables as v, app 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) playqueue_item_id, plex_id)
found = True found = True
for player in list(js.get_players().values()): 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): for i, item in enumerate(playqueue.items):
if item.id == playqueue_item_id: if item.id == playqueue_item_id:
found = True found = True

View file

@ -6,8 +6,11 @@ import xbmcgui
from .plex_api import API from .plex_api import API
from .plex_db import PlexDB from .plex_db import PlexDB
from . import context, plex_functions as PF, playqueue as PQ from . import context
from . import utils, variables as v, app 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 For using direct paths: Initiates playback using the PMS
""" """
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_kodi_type(self.kodi_type)
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
playqueue.clear() playqueue.clear()
app.PLAYSTATE.context_menu_play = True app.PLAYSTATE.context_menu_play = True
handle = self.api.fullpath(force_addon=True)[0] handle = self.api.fullpath(force_addon=True)[0]

View file

@ -17,7 +17,7 @@ from .kodi_db import KodiVideoDB
from . import kodi_db from . import kodi_db
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
from . import utils, timing, plex_functions as PF 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 backgroundthread, app, variables as v
from . import exceptions from . import exceptions
@ -140,7 +140,7 @@ class KodiMonitor(xbmc.Monitor):
u'playlistid': 1, u'playlistid': 1,
} }
""" """
playqueue = PQ.PLAYQUEUES[data['playlistid']] playqueue = app.PLAYQUEUES[data['playlistid']]
if not playqueue.is_pkc_clear(): if not playqueue.is_pkc_clear():
playqueue.pkc_edit = True playqueue.pkc_edit = True
playqueue.clear(kodi=False) playqueue.clear(kodi=False)
@ -256,7 +256,7 @@ class KodiMonitor(xbmc.Monitor):
if not playerid: if not playerid:
LOG.error('Coud not get playerid for data %s', data) LOG.error('Coud not get playerid for data %s', data)
return return
playqueue = PQ.PLAYQUEUES[playerid] playqueue = app.PLAYQUEUES[playerid]
info = js.get_player_props(playerid) info = js.get_player_props(playerid)
if playqueue.kodi_playlist_playback: if playqueue.kodi_playlist_playback:
# Kodi will tell us the wrong position - of the playlist, not the # Kodi will tell us the wrong position - of the playlist, not the
@ -326,7 +326,7 @@ class KodiMonitor(xbmc.Monitor):
container_key = None container_key = None
if info['playlistid'] != -1: if info['playlistid'] != -1:
# -1 is Kodi's answer if there is no playlist # -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: if container_key is not None:
container_key = '/playQueues/%s' % container_key container_key = '/playQueues/%s' % container_key
elif plex_id is not None: elif plex_id is not None:

View file

@ -12,8 +12,12 @@ import xbmc
from .plex_api import API from .plex_api import API
from .plex_db import PlexDB from .plex_db import PlexDB
from .kodi_db import KodiVideoDB from .kodi_db import KodiVideoDB
from . import plex_functions as PF, playlist_func as PL, playqueue as PQ from . import plex_functions as PF
from . import json_rpc as js, variables as v, utils, transfer 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 playback_decision, app
from . import exceptions from . import exceptions
@ -74,20 +78,19 @@ def _playback_triage(plex_id, plex_type, path, resolve, resume):
_ensure_resolve(abort=True) _ensure_resolve(abort=True)
return return
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
try: try:
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
except KeyError: except KeyError:
# Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for # Kodi bug - Playlist plays (not Playqueue) will ALWAYS be audio for
# add-on paths # add-on paths
LOG.debug('No position returned from player! Assuming playlist') 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: try:
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
except KeyError: except KeyError:
LOG.debug('Assuming video instead of audio playlist playback') 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: try:
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
except KeyError: except KeyError:
@ -159,7 +162,7 @@ def _playlist_playback(plex_id):
return return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback # Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
# has actually started. Need to tell Kodimonitor # 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) playqueue.clear(kodi=False)
# Set the flag for the potentially WRONG audio playlist so Kodimonitor # Set the flag for the potentially WRONG audio playlist so Kodimonitor
# can pick up on it # can pick up on it
@ -499,8 +502,7 @@ def process_indirect(key, offset, resolve=True):
api = API(xml[0]) api = API(xml[0])
listitem = api.listitem(listitem=transfer.PKCListItem, resume=False) listitem = api.listitem(listitem=transfer.PKCListItem, resume=False)
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
playqueue.clear() playqueue.clear()
item = PL.playlist_item_from_xml(xml[0]) item = PL.playlist_item_from_xml(xml[0])
item.offset = offset item.offset = offset

View file

@ -22,117 +22,6 @@ from .subtitles import accessible_plex_subtitles
LOG = getLogger('PLEX.playlist_func') 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): class PlaylistItem(object):
""" """
Object to fill our playqueues and playlists with. Object to fill our playqueues and playlists with.

View file

@ -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)

View file

@ -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')

View file

@ -5,13 +5,13 @@ import requests
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from .common import proxy_headers, proxy_params, log_error from .common import proxy_headers, proxy_params, log_error
from .playqueue import compare_playqueues
from .. import json_rpc as js from .. import json_rpc as js
from .. import variables as v from .. import variables as v
from .. import backgroundthread from .. import backgroundthread
from .. import app from .. import app
from .. import timing from .. import timing
from .. import playqueue as PQ
from .. import skip_plex_intro from .. import skip_plex_intro
@ -55,7 +55,7 @@ def get_correct_position(info, playqueue):
def timeline_dict(playerid, typus): def timeline_dict(playerid, typus):
with app.APP.lock_playqueues: with app.APP.lock_playqueues:
info = app.PLAYSTATE.player_states[playerid] info = app.PLAYSTATE.player_states[playerid]
playqueue = PQ.PLAYQUEUES[playerid] playqueue = app.PLAYQUEUES[playerid]
position = get_correct_position(info, playqueue) position = get_correct_position(info, playqueue)
try: try:
item = playqueue.items[position] item = playqueue.items[position]
@ -354,7 +354,21 @@ class PlaystateMgr(backgroundthread.KillableThread):
self.close_requests_session() self.close_requests_session()
if self.wait_while_suspended(): if self.wait_while_suspended():
break 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() players = js.get_players()
if not players and signaled_playback_stop: if not players and signaled_playback_stop:
self.sleep(1) self.sleep(1)

View file

@ -14,7 +14,6 @@ from .. import plex_functions as PF
from .. import playlist_func as PL from .. import playlist_func as PL
from .. import playback from .. import playback
from .. import json_rpc as js from .. import json_rpc as js
from .. import playqueue as PQ
from .. import variables as v from .. import variables as v
from .. import app from .. import app
from .. import exceptions from .. import exceptions
@ -87,9 +86,8 @@ def process_playlist(containerKey, typus, key, offset, token):
# Get the playqueue ID # Get the playqueue ID
_, container_key, query = PF.ParseContainerKey(containerKey) _, container_key, query = PF.ParseContainerKey(containerKey)
try: try:
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(typus)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[typus]) except ValueError:
except KeyError:
# E.g. Plex web does not supply the media type # E.g. Plex web does not supply the media type
# Still need to figure out the type (video vs. music vs. pix) # Still need to figure out the type (video vs. music vs. pix)
xml = PF.GetPlexMetadata(key) xml = PF.GetPlexMetadata(key)
@ -99,8 +97,7 @@ def process_playlist(containerKey, typus, key, offset, token):
log.error('Could not download Plex metadata') log.error('Could not download Plex metadata')
return return
api = API(xml[0]) api = API(xml[0])
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
if key: if key:
_, key, _ = PF.ParseContainerKey(key) _, key, _ = PF.ParseContainerKey(key)
update_playqueue_from_PMS(playqueue, update_playqueue_from_PMS(playqueue,
@ -111,12 +108,12 @@ def process_playlist(containerKey, typus, key, offset, token):
start_plex_id=key) 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 Plex Companion client adjusted audio or subtitle stream
""" """
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[typus])
pos = js.get_position(playqueue.playlistid) pos = js.get_position(playqueue.playlistid)
playqueue.items[pos].on_plex_stream_change(video_stream_id, playqueue.items[pos].on_plex_stream_change(video_stream_id,
audio_stream_id, audio_stream_id,
@ -135,12 +132,10 @@ def process_refresh(playqueue_id):
plex_type = PL.get_plextype_from_xml(xml) plex_type = PL.get_plextype_from_xml(xml)
if plex_type is None: if plex_type is None:
return return
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
playqueue.clear() playqueue.clear()
return return
playqueue = PQ.get_playqueue_from_type( playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type'])
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']])
update_playqueue_from_PMS(playqueue, playqueue_id) update_playqueue_from_PMS(playqueue, playqueue_id)
@ -155,7 +150,7 @@ def skip_to(playqueue_item_id, key):
playqueue_item_id, plex_id) playqueue_item_id, plex_id)
found = True found = True
for player in list(js.get_players().values()): 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): for i, item in enumerate(playqueue.items):
if item.id == playqueue_item_id: if item.id == playqueue_item_id:
found = True found = True

View file

@ -18,7 +18,7 @@ class Playlists(object):
def delete_playlist(self, playlist): 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. playlists table.
Be sure to either set playlist.id or playlist.kodi_path Be sure to either set playlist.id or playlist.kodi_path
""" """

View file

@ -12,9 +12,8 @@ from . import kodimonitor
from . import sync, library_sync from . import sync, library_sync
from . import websocket_client from . import websocket_client
from . import plex_companion 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 playback_starter
from . import playqueue
from . import variables as v from . import variables as v
from . import app from . import app
from . import loghandler from . import loghandler
@ -99,7 +98,6 @@ class Service(object):
self.setup = None self.setup = None
self.pms_ws = None self.pms_ws = None
self.alexa_ws = None self.alexa_ws = None
self.playqueue = None
# Flags for other threads # Flags for other threads
self.connection_check_running = False self.connection_check_running = False
self.auth_running = False self.auth_running = False
@ -436,8 +434,6 @@ class Service(object):
app.init() app.init()
app.APP.monitor = kodimonitor.KodiMonitor() app.APP.monitor = kodimonitor.KodiMonitor()
app.APP.player = xbmc.Player() app.APP.player = xbmc.Player()
# Initialize the PKC playqueues
PQ.init_playqueues()
# Server auto-detect # Server auto-detect
self.setup = initialsetup.InitialSetup() self.setup = initialsetup.InitialSetup()
@ -452,7 +448,6 @@ class Service(object):
self.companion_listener = plex_companion.Listener(self.companion_playstate_mgr) self.companion_listener = plex_companion.Listener(self.companion_playstate_mgr)
else: else:
self.companion_listener = None self.companion_listener = None
self.playqueue = playqueue.PlayqueueMonitor()
# Main PKC program loop # Main PKC program loop
while not self.should_cancel(): while not self.should_cancel():
@ -554,7 +549,6 @@ class Service(object):
self.companion_playstate_mgr.start() self.companion_playstate_mgr.start()
if self.companion_listener is not None: if self.companion_listener is not None:
self.companion_listener.start() self.companion_listener.start()
self.playqueue.start()
self.alexa_ws.start() self.alexa_ws.start()
xbmc.sleep(200) xbmc.sleep(200)