Playqueues overhaul continued

This commit is contained in:
tomkat83 2016-12-28 13:14:21 +01:00
parent 0c2d4984ab
commit 9d2902baa5
8 changed files with 216 additions and 139 deletions

View file

@ -1646,7 +1646,7 @@ class API():
If not found, empty str is returned If not found, empty str is returned
""" """
return self.item.attrib.get('playQueueItemID', '') return self.item.attrib.get('playQueueItemID')
def getDataFromPartOrMedia(self, key): def getDataFromPartOrMedia(self, key):
""" """

View file

@ -9,8 +9,7 @@ from xbmc import sleep
from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods
from plexbmchelper import listener, plexgdm, subscribers, functions, \ from plexbmchelper import listener, plexgdm, subscribers, functions, \
httppersist, plexsettings httppersist, plexsettings
from PlexFunctions import ParseContainerKey, GetPlayQueue, \ from PlexFunctions import ParseContainerKey
ConvertPlexToKodiTime
import player import player
############################################################################### ###############################################################################
@ -29,7 +28,6 @@ class PlexCompanion(Thread):
log.info("----===## Starting PlexCompanion ##===----") log.info("----===## Starting PlexCompanion ##===----")
if callback is not None: if callback is not None:
self.mgr = callback self.mgr = callback
self.playqueue = self.mgr.playqueue
self.settings = plexsettings.getSettings() self.settings = plexsettings.getSettings()
# Start GDM for server/client discovery # Start GDM for server/client discovery
self.client = plexgdm.plexgdm() self.client = plexgdm.plexgdm()
@ -60,12 +58,18 @@ class PlexCompanion(Thread):
def processTasks(self, task): def processTasks(self, task):
""" """
Processes tasks picked up e.g. by Companion listener Processes tasks picked up e.g. by Companion listener, e.g.
{'action': 'playlist',
task = { 'data': {'address': 'xyz.plex.direct',
'action': 'playlist' 'commandID': '7',
'data': as received from Plex companion '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) log.debug('Processing: %s' % task)
data = task['data'] data = task['data']
@ -79,36 +83,11 @@ class PlexCompanion(Thread):
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) log.error("Traceback:\n%s" % traceback.format_exc())
return return
self.mgr.playqueue.update_playqueue_with_companion(data) playqueue = self.mgr.playqueue.get_playqueue_from_type(
data['type'])
self.playqueue = self.mgr.playqueue.get_playqueue_from_plextype( if ID != playqueue.ID:
data.get('type')) self.mgr.playqueue.update_playqueue_from_PMS(
if queueId != self.playqueue.ID: playqueue, ID, int(query['repeat']))
log.info('New playlist received, updating!')
xml = GetPlayQueue(queueId)
if xml in (None, 401):
log.error('Could not download Plex playlist.')
return
# Clear existing playlist on the Kodi side
self.playqueue.clear()
# Set new values
self.playqueue.QueueId(queueId)
self.playqueue.PlayQueueVersion(int(
xml.attrib.get('playQueueVersion')))
self.playqueue.Guid(xml.attrib.get('guid'))
items = []
for item in xml:
items.append({
'playQueueItemID': item.get('playQueueItemID'),
'plexId': item.get('ratingKey'),
'kodiId': None})
self.playqueue.playAll(
items,
startitem=self._getStartItem(data.get('key', '')),
offset=ConvertPlexToKodiTime(data.get('offset', 0)))
log.info('Initiated playlist no %s with version %s'
% (self.playqueue.QueueId(),
self.playqueue.PlayQueueVersion()))
else: else:
log.error('This has never happened before!') log.error('This has never happened before!')
@ -123,7 +102,7 @@ class PlexCompanion(Thread):
requestMgr = httppersist.RequestMgr() requestMgr = httppersist.RequestMgr()
jsonClass = functions.jsonClass(requestMgr, self.settings) jsonClass = functions.jsonClass(requestMgr, self.settings)
subscriptionManager = subscribers.SubscriptionManager( subscriptionManager = subscribers.SubscriptionManager(
jsonClass, requestMgr, self.player, self.playqueue) jsonClass, requestMgr, self.player, self.mgr)
queue = Queue.Queue(maxsize=100) queue = Queue.Queue(maxsize=100)

View file

@ -171,22 +171,6 @@ def SelectStreams(url, args):
url + '?' + urlencode(args), action_type='PUT') url + '?' + urlencode(args), action_type='PUT')
def GetPlayQueue(playQueueID):
"""
Fetches the PMS playqueue with the playQueueID as an XML
Returns None if something went wrong
"""
url = "{server}/playQueues/%s" % playQueueID
args = {'Accept': 'application/xml'}
xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args)
try:
xml.attrib['playQueueID']
except (AttributeError, KeyError):
return None
return xml
def GetPlexMetadata(key): def GetPlexMetadata(key):
""" """
Returns raw API metadata for key as an etree XML. Returns raw API metadata for key as an etree XML.

View file

@ -168,10 +168,10 @@ class KodiMonitor(xbmc.Monitor):
pass pass
elif method == "Playlist.OnAdd": elif method == "Playlist.OnAdd":
# User manipulated Kodi playlist # User (or PKC) manipulated Kodi playlist
# Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1, # Data : {u'item': {u'type': u'movie', u'id': 3}, u'playlistid': 1,
# u'position': 0} # u'position': 0}
self.playlist.kodi_onadd(data) self.playqueue.kodi_onadd(data)
def PlayBackStart(self, data): def PlayBackStart(self, data):
""" """

View file

@ -37,7 +37,8 @@ class PlaybackUtils():
self.userid = window('currUserId') self.userid = window('currUserId')
self.server = window('pms_server') self.server = window('pms_server')
self.pl = Playqueue().get_playqueue_from_plextype(self.API.getType()) self.pl = Playqueue().get_playqueue_from_type(
PF.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[self.API.getType()])
def play(self, itemid, dbid=None): def play(self, itemid, dbid=None):

View file

@ -4,6 +4,7 @@ from urllib import quote
import embydb_functions as embydb import embydb_functions as embydb
from downloadutils import DownloadUtils as DU from downloadutils import DownloadUtils as DU
from utils import JSONRPC, tryEncode from utils import JSONRPC, tryEncode
from PlexAPI import API
############################################################################### ###############################################################################
@ -24,13 +25,35 @@ class Playlist_Object_Baseclase(object):
selectedItemOffset = None selectedItemOffset = None
shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ??? shuffled = 0 # [int], 0: not shuffled, 1: ??? 2: ???
repeat = 0 # [int], 0: not repeated, 1: ??? 2: ??? repeat = 0 # [int], 0: not repeated, 1: ??? 2: ???
# Hack to later ignore all Kodi playlist adds that PKC did (Kodimonitor)
PKC_playlist_edits = []
def __repr__(self): def __repr__(self):
answ = "<%s object: " % (self.__class__.__name__) answ = "<%s: " % (self.__class__.__name__)
for key in self.__dict__: for key in self.__dict__:
answ += '%s: %s, ' % (key, getattr(self, key)) answ += '%s: %s, ' % (key, getattr(self, key))
return answ[:-2] + ">" return answ[:-2] + ">"
def clear(self):
"""
Resets the playlist object to an empty playlist
"""
# Clear Kodi playlist object
self.kodi_pl.clear()
self.items = []
self.old_kodi_pl = []
self.ID = None
self.version = None
self.selectedItemID = None
self.selectedItemOffset = None
self.shuffled = 0
self.repeat = 0
self.PKC_playlist_edits = []
log.debug('Playlist cleared: %s' % self)
def log_Kodi_playlist(self):
log.debug('Current Kodi playlist: %s' % get_kodi_playlist_items(self))
class Playlist_Object(Playlist_Object_Baseclase): class Playlist_Object(Playlist_Object_Baseclase):
kind = 'playList' kind = 'playList'
@ -48,6 +71,13 @@ class Playlist_Item(object):
kodi_type = None # Kodi type: 'movie' kodi_type = None # Kodi type: 'movie'
file = None # Path to the item's file file = None # Path to the item's file
uri = None # Weird Plex uri path involving plex_UUID uri = None # Weird Plex uri path involving plex_UUID
guid = None # Weird Plex guid
def __repr__(self):
answ = "<%s: " % (self.__class__.__name__)
for key in self.__dict__:
answ += '%s: %s, ' % (key, getattr(self, key))
return answ[:-2] + ">"
def playlist_item_from_kodi_item(kodi_item): def playlist_item_from_kodi_item(kodi_item):
@ -127,8 +157,10 @@ def _get_playlist_details_from_xml(playlist, xml):
try: try:
playlist.ID = xml.attrib['%sID' % playlist.kind] playlist.ID = xml.attrib['%sID' % playlist.kind]
playlist.version = xml.attrib['%sVersion' % playlist.kind] playlist.version = xml.attrib['%sVersion' % playlist.kind]
playlist.selectedItemID = xml.attrib['%sSelectedItemID' % playlist.kind] playlist.selectedItemID = xml.attrib['%sSelectedItemID'
playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset' % playlist.kind] % playlist.kind]
playlist.selectedItemOffset = xml.attrib['%sSelectedItemOffset'
% playlist.kind]
playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind]
except: except:
log.error('Could not parse xml answer from PMS for playlist %s' log.error('Could not parse xml answer from PMS for playlist %s'
@ -141,9 +173,9 @@ def _get_playlist_details_from_xml(playlist, xml):
def init_Plex_playlist(playlist, plex_id=None, kodi_item=None): def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
""" """
Supply either plex_id or the data supplied by Kodi JSON-RPC Supply either with a plex_id OR the data supplied by Kodi JSON-RPC
""" """
if plex_id is not None: if plex_id:
item = playlist_item_from_plex(plex_id) item = playlist_item_from_plex(plex_id)
else: else:
item = playlist_item_from_kodi_item(kodi_item) item = playlist_item_from_kodi_item(kodi_item)
@ -155,7 +187,7 @@ def init_Plex_playlist(playlist, plex_id=None, kodi_item=None):
xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind,
action_type="POST", action_type="POST",
parameters=params) parameters=params)
_get_playlist_details_from_xml(xml) _get_playlist_details_from_xml(playlist, xml)
playlist.items.append(item) playlist.items.append(item)
log.debug('Initialized the playlist: %s' % playlist) log.debug('Initialized the playlist: %s' % playlist)
@ -176,6 +208,10 @@ def add_playlist_item(playlist, kodi_item, after_pos):
% (kodi_item, playlist)) % (kodi_item, playlist))
_log_xml(xml) _log_xml(xml)
return return
# Get the guid for this item
for plex_item in xml:
if plex_item.attrib['%sItemID' % playlist.kind] == item.ID:
item.guid = plex_item.attrib['guid']
playlist.items.append(item) playlist.items.append(item)
if after_pos == len(playlist.items) - 1: if after_pos == len(playlist.items) - 1:
# Item was added at the end # Item was added at the end
@ -259,6 +295,39 @@ def get_kodi_playqueues():
# Functions operating on the Kodi playlist objects ########## # Functions operating on the Kodi playlist objects ##########
def add_to_Kodi_playlist(playlist, xml_video_element):
"""
Adds a new item to the Kodi playlist via JSON (at the end of the playlist).
Pass in the PMS xml's video element (one level underneath MediaContainer).
Will return a Playlist_Item
"""
item = Playlist_Item()
api = API(xml_video_element)
params = {
'playlistid': playlist.playlistid
}
item.plex_id = api.getRatingKey()
item.ID = xml_video_element.attrib['%sItemID' % playlist.kind]
item.guid = xml_video_element.attrib.get('guid')
if item.plex_id:
with embydb.GetEmbyDB() as emby_db:
db_element = emby_db.getItem_byId(item.plex_id)
try:
item.kodi_id, item.kodi_type = int(db_element[0]), db_element[4]
except TypeError:
pass
if item.kodi_id:
params['item'] = {'%sid' % item.kodi_type: item.kodi_id}
else:
item.file = api.getFilePath()
params['item'] = {'file': tryEncode(item.file)}
log.debug(JSONRPC('Playlist.Add').execute(params))
playlist.PKC_playlist_edits.append(
item.kodi_id if item.kodi_id else item.file)
return item
def insertintoPlaylist(self, def insertintoPlaylist(self,
position, position,
dbid=None, dbid=None,
@ -275,57 +344,51 @@ def insertintoPlaylist(self,
JSONRPC('Playlist.Insert').execute(params) JSONRPC('Playlist.Insert').execute(params)
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
params = {
'playlistid': self.playlistId
}
if dbid is not None:
params['item'] = {'%sid' % tryEncode(mediatype): int(dbid)}
else:
params['item'] = {'file': url}
JSONRPC('Playlist.Add').execute(params)
def removefromPlaylist(self, position): def removefromPlaylist(self, position):
params = { params = {
'playlistid': self.playlistId, 'playlistid': self.playlistId,
'position': position 'position': position
} }
JSONRPC('Playlist.Remove').execute(params) log.debug(JSONRPC('Playlist.Remove').execute(params))
def playAll(self, items, startitem, offset): def get_PMS_playlist(playlist, playlist_id=None):
""" """
items: list of dicts of the form Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we
{ need to fetch a new playlist
'playQueueItemID': Plex playQueueItemID, e.g. '29175'
'plexId': Plex ratingKey, e.g. '125'
'kodiId': Kodi's db id of the same item
}
startitem: tuple (typus, id), where typus is either Returns None if something went wrong
'playQueueItemID' or 'plexId' and id is the corresponding
id as a string
offset: First item's time offset to play in Kodi time (an int)
""" """
log.info("---*** PLAY ALL ***---") playlist_id = playlist_id if playlist_id else playlist.ID
log.debug('Startitem: %s, offset: %s, items: %s' xml = DU().downloadUrl(
% (startitem, offset, items)) "{server}/%ss/%s" % (playlist.kind, playlist_id),
self.items = items headerOptions={'Accept': 'application/xml'})
if self.playlist is None: try:
self._initiatePlaylist() xml.attrib['%sID' % playlist.kind]
if self.playlist is None: except (AttributeError, KeyError):
log.error('Could not create playlist, abort') xml = None
return xml
def update_playlist_from_PMS(playlist, playlist_id=None, repeat=None):
"""
Updates Kodi playlist using a new PMS playlist. Pass in playlist_id if we
need to fetch a new playqueue
"""
xml = get_PMS_playlist(playlist, playlist_id)
try:
xml.attrib['%sVersion' % playlist.kind]
except:
log.error('Could not download Plex playlist.')
return return
# Clear our existing playlist and the associated Kodi playlist
window('plex_customplaylist', value="true") playlist.clear()
if offset != 0: # Set new values
# Seek to the starting position _get_playlist_details_from_xml(playlist, xml)
window('plex_customplaylist.seektime', str(offset)) if repeat:
self._processItems(startitem, startPlayer=True) playlist.repeat = repeat
# Log playlist for plex_item in xml:
self._verifyPlaylist() playlist.items.append(add_to_Kodi_playlist(playlist, plex_item))
log.debug('Internal playlist: %s' % self.items)
def _processItems(self, startitem, startPlayer=False): def _processItems(self, startitem, startPlayer=False):
@ -380,15 +443,3 @@ def _addtoPlaylist_xbmc(self, item):
playbackutils.PlaybackUtils(item).setArtwork(listitem) playbackutils.PlaybackUtils(item).setArtwork(listitem)
self.playlist.add(playurl, listitem) self.playlist.add(playurl, listitem)
def clear(self):
"""
Empties current Kodi playlist and associated variables
"""
self.playlist.clear()
self.items = []
self.queueId = None
self.playQueueVersion = None
self.guid = None
log.info('Playlist cleared')

View file

@ -5,10 +5,10 @@ from threading import Lock, Thread
import xbmc import xbmc
from utils import ThreadMethods, ThreadMethodsAdditionalSuspend, Lock_Function from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend, \
Lock_Function
import playlist_func as PL import playlist_func as PL
from PlexFunctions import KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE, GetPlayQueue, \ from PlexFunctions import ConvertPlexToKodiTime
ParseContainerKey
############################################################################### ###############################################################################
log = logging.getLogger("PLEX."+__name__) log = logging.getLogger("PLEX."+__name__)
@ -36,6 +36,7 @@ class Playqueue(Thread):
if self.playqueues is not None: if self.playqueues is not None:
return return
self.mgr = callback self.mgr = callback
self.player = xbmc.Player()
# Initialize Kodi playqueues # Initialize Kodi playqueues
self.playqueues = [] self.playqueues = []
@ -52,8 +53,64 @@ class Playqueue(Thread):
# Currently, only video or audio playqueues available # Currently, only video or audio playqueues available
playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
self.playqueues.append(playqueue) self.playqueues.append(playqueue)
# sort the list by their playlistid, just in case
self.playqueues = sorted(
self.playqueues, key=lambda i: i.playlistid)
log.debug('Initialized the Kodi play queues: %s' % self.playqueues) log.debug('Initialized the Kodi play queues: %s' % self.playqueues)
def get_playqueue_from_type(self, typus):
"""
Returns the playqueue according to the typus ('video', 'audio',
'picture') passed in
"""
for playqueue in self.playqueues:
if playqueue.type == typus:
break
else:
raise ValueError('Wrong type was passed in: %s' % typus)
return playqueue
def get_playqueue_from_playerid(self, kodi_player_id):
for playqueue in self.playqueues:
if playqueue.playlistid == kodi_player_id:
break
else:
raise ValueError('Wrong kodi_player_id passed was passed in: %s'
% kodi_player_id)
return playqueue
@lockmethod.lockthis
def update_playqueue_from_PMS(self,
playqueue,
playqueue_id=None,
repeat=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
"""
log.info('New playqueue received, updating!')
PL.update_playlist_from_PMS(playqueue, playqueue_id, repeat)
log.debug('Updated playqueue: %s' % playqueue)
window('plex_customplaylist', value="true")
if playqueue.selectedItemOffset not in (None, "0"):
window('plex_customplaylist.seektime',
str(ConvertPlexToKodiTime(playqueue.selectedItemOffset)))
for startpos, item in enumerate(playqueue.items):
if item.ID == playqueue.selectedItemID:
break
else:
startpos = None
# Start playback
if startpos:
self.player.play(playqueue.kodi_pl, startpos=startpos)
else:
self.player.play(playqueue.kodi_pl)
log.debug('Playqueue at the end: %s' % playqueue)
playqueue.log_Kodi_playlist()
@lockmethod.lockthis @lockmethod.lockthis
def update_playqueue_with_companion(self, data): def update_playqueue_with_companion(self, data):
""" """
@ -61,10 +118,6 @@ class Playqueue(Thread):
""" """
# Get the correct queue # Get the correct queue
for playqueue in self.playqueues:
if playqueue.type == KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[
data['type']]:
break
@lockmethod.lockthis @lockmethod.lockthis
def kodi_onadd(self, data): def kodi_onadd(self, data):
@ -80,11 +133,20 @@ class Playqueue(Thread):
for playqueue in self.playqueues: for playqueue in self.playqueues:
if playqueue.playlistid == data['playlistid']: if playqueue.playlistid == data['playlistid']:
break break
if playqueue.PKC_playlist_edits:
old = (data['item'].get('id') if data['item'].get('id')
else data['item'].get('file'))
for i, item in enumerate(playqueue.PKC_playlist_edits):
if old == item:
log.debug('kodimonitor told us of a PKC edit - ignore.')
del playqueue.PKC_playlist_edits[i]
return
if playqueue.ID is None: if playqueue.ID is None:
# Need to initialize the queue for the first time # Need to initialize the queue for the first time
PL.init_Plex_playlist(playqueue, kodi_item=data['item']) PL.init_Plex_playlist(playqueue, kodi_item=data['item'])
else: else:
PL.add_playlist_item(playqueue, data['item'], data['position']) PL.add_playlist_item(playqueue, data['item'], data['position'])
log.debug('Added a new item to the playqueue: %s' % playqueue)
@lockmethod.lockthis @lockmethod.lockthis
def _compare_playqueues(self, playqueue, new): def _compare_playqueues(self, playqueue, new):

View file

@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__)
class SubscriptionManager: class SubscriptionManager:
def __init__(self, jsonClass, RequestMgr, player, playlist): def __init__(self, jsonClass, RequestMgr, player, mgr):
self.serverlist = [] self.serverlist = []
self.subscribers = {} self.subscribers = {}
self.info = {} self.info = {}
@ -36,7 +36,7 @@ class SubscriptionManager:
self.playerprops = {} self.playerprops = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.xbmcplayer = player self.xbmcplayer = player
self.playlist = playlist self.playqueue = mgr.playqueue
self.js = jsonClass self.js = jsonClass
self.RequestMgr = RequestMgr self.RequestMgr = RequestMgr
@ -231,6 +231,8 @@ class SubscriptionManager:
def getPlayerProperties(self, playerid): def getPlayerProperties(self, playerid):
try: try:
# Get the playqueue
playqueue = self.playqueue.playqueues[playerid]
# get info from the player # get info from the player
props = self.js.jsonrpc( props = self.js.jsonrpc(
"Player.GetProperties", "Player.GetProperties",
@ -248,18 +250,16 @@ class SubscriptionManager:
'shuffle': ("0", "1")[props.get('shuffled', False)], 'shuffle': ("0", "1")[props.get('shuffled', False)],
'repeat': pf.getPlexRepeat(props.get('repeat')), 'repeat': pf.getPlexRepeat(props.get('repeat')),
} }
if self.playlist is not None: if playqueue.ID is not None:
if self.playlist.QueueId() is not None: info['playQueueID'] = playqueue.ID
info['playQueueID'] = self.playlist.QueueId() info['playQueueVersion'] = playqueue.version
info['playQueueVersion'] = self.playlist.PlayQueueVersion()
info['guid'] = self.playlist.Guid()
# Get the playlist position # Get the playlist position
pos = self.js.jsonrpc( pos = self.js.jsonrpc(
"Player.GetProperties", "Player.GetProperties",
{"playerid": playerid, {"playerid": playerid,
"properties": ["position"]}) "properties": ["position"]})['position']
info['playQueueItemID'] = \ info['playQueueItemID'] = playqueue.items[pos].ID
self.playlist.getQueueIdFromPosition(pos['position']) info['guid'] = playqueue.items[pos].guid
except: except:
import traceback import traceback
log.error("Traceback:\n%s" % traceback.format_exc()) log.error("Traceback:\n%s" % traceback.format_exc())