Revamp playback start, part 2

This commit is contained in:
croneter 2018-01-10 20:14:05 +01:00
parent 24f2f60209
commit fb7eafb27a
6 changed files with 310 additions and 135 deletions

View file

@ -291,12 +291,11 @@ class Movies(Items):
path = playurl.replace(filename, "") path = playurl.replace(filename, "")
if doIndirect: if doIndirect:
# Set plugin path and media flags using real filename # Set plugin path and media flags using real filename
path = "plugin://plugin.video.plexkodiconnect/movies/" path = "plugin://plugin.video.plexkodiconnect"
params = { params = {
'filename': API.getKey(), 'mode': 'play',
'id': itemid, 'plex_id': itemid,
'dbid': movieid, 'plex_type': v.PLEX_TYPE_MOVIE
'mode': "play"
} }
filename = "%s?%s" % (path, urlencode(params)) filename = "%s?%s" % (path, urlencode(params))
playurl = filename playurl = filename

View file

@ -12,11 +12,11 @@ from PlexFunctions import scrobble
from kodidb_functions import kodiid_from_filename from kodidb_functions import kodiid_from_filename
from plexbmchelper.subscribers import LOCKER from plexbmchelper.subscribers import LOCKER
from PlexAPI import API from PlexAPI import API
import playqueue as PQ
import json_rpc as js import json_rpc as js
import playlist_func as PL import playlist_func as PL
import state import state
import variables as v import variables as v
import playqueue as PQ
############################################################################### ###############################################################################
@ -254,31 +254,16 @@ class KodiMonitor(Monitor):
return return
playqueue.clear() playqueue.clear()
@LOCKER.lockthis def _get_ids(self, json_item):
def PlayBackStart(self, data):
""" """
Called whenever playback is started. Example data:
{
u'item': {u'type': u'movie', u'title': u''},
u'player': {u'playerid': 1, u'speed': 1}
}
Unfortunately when using Widgets, Kodi doesn't tell us shit
""" """
# Get the type of media we're playing kodi_id = json_item.get('id')
try: kodi_type = json_item.get('type')
kodi_type = data['item']['type'] path = json_item.get('file')
playerid = data['player']['playerid']
except (TypeError, KeyError):
LOG.info('Aborting playback report - item invalid for updates %s',
data)
return
json_data = js.get_item(playerid)
path = json_data.get('file')
kodi_id = json_data.get('id')
if not path and not kodi_id: if not path and not kodi_id:
LOG.info('Aborting playback report - no Kodi id or file for %s', LOG.info('Aborting playback report - no Kodi id or file for %s',
json_data) json_item)
return raise RuntimeError
# Plex id will NOT be set with direct paths # Plex id will NOT be set with direct paths
plex_id = state.PLEX_IDS.get(path) plex_id = state.PLEX_IDS.get(path)
try: try:
@ -306,28 +291,49 @@ class KodiMonitor(Monitor):
except TypeError: except TypeError:
# No plex id, hence item not in the library. E.g. clips # No plex id, hence item not in the library. E.g. clips
pass pass
info = js.get_player_props(playerid) return kodi_id, kodi_type, plex_id, plex_type
state.PLAYER_STATES[playerid].update(info)
state.PLAYER_STATES[playerid]['file'] = path @LOCKER.lockthis
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id def PlayBackStart(self, data):
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type """
state.PLAYER_STATES[playerid]['plex_id'] = plex_id Called whenever playback is started. Example data:
state.PLAYER_STATES[playerid]['plex_type'] = plex_type {
LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid]) u'item': {u'type': u'movie', u'title': u''},
# Check whether we need to init our playqueues (e.g. direct play) u'player': {u'playerid': 1, u'speed': 1}
init = False }
playqueue = PQ.PLAYQUEUES[playerid] Unfortunately when using Widgets, Kodi doesn't tell us shit
"""
# Get the type of media we're playing
try: try:
playqueue.items[info['position']] kodi_type = data['item']['type']
playerid = data['player']['playerid']
except (TypeError, KeyError):
LOG.info('Aborting playback report - item invalid for updates %s',
data)
return
playqueue = PQ.PLAYQUEUES[playerid]
info = js.get_player_props(playerid)
json_item = js.get_item(playerid)
path = json_item.get('file')
pos = info['position'] if info['position'] != -1 else 0
LOG.info('Detected position %s for %s', pos, playqueue)
try:
item = playqueue.items[pos]
# See if playback.py already initiated playback
init_done = item.init_done
except IndexError: except IndexError:
init = True init_done = False
if init is False and plex_id is not None: if init_done is True:
if plex_id != playqueue.items[info['position']].plex_id: kodi_id = item.kodi_id
init = True kodi_type = item.kodi_type
elif init is False and path != playqueue.items[info['position']].file: plex_id = item.plex_id
init = True plex_type = item.plex_type
if init is True: else:
LOG.debug('Need to initialize Plex and PKC playqueue') try:
kodi_id, kodi_type, plex_id, plex_type = self._get_ids(json_item)
except RuntimeError:
return
LOG.info('Need to initialize Plex and PKC playqueue')
if plex_id: if plex_id:
PL.init_Plex_playlist(playqueue, plex_id=plex_id) PL.init_Plex_playlist(playqueue, plex_id=plex_id)
else: else:
@ -335,17 +341,25 @@ class KodiMonitor(Monitor):
kodi_item={'id': kodi_id, kodi_item={'id': kodi_id,
'type': kodi_type, 'type': kodi_type,
'file': path}) 'file': path})
# Set the Plex container key (e.g. using the Plex playqueue) # Set the Plex container key (e.g. using the Plex playqueue)
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 = PQ.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:
container_key = '/library/metadata/%s' % plex_id container_key = '/library/metadata/%s' % plex_id
state.PLAYER_STATES[playerid]['container_key'] = container_key state.PLAYER_STATES[playerid]['container_key'] = container_key
LOG.debug('Set the Plex container_key to: %s', container_key) LOG.debug('Set the Plex container_key to: %s', container_key)
state.PLAYER_STATES[playerid].update(info)
state.PLAYER_STATES[playerid]['file'] = path
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
state.PLAYER_STATES[playerid]['plex_type'] = plex_type
LOG.debug('Set the player state: %s', state.PLAYER_STATES[playerid])
def StartDirectPath(self, plex_id, type, currentFile): def StartDirectPath(self, plex_id, type, currentFile):
""" """

View file

@ -1,52 +1,213 @@
""" """
Used to kick off Kodi playback Used to kick off Kodi playback
""" """
from logging import getLogger
from threading import Thread, Lock
from urllib import urlencode
from xbmc import Player, getCondVisibility, sleep
from PlexAPI import API from PlexAPI import API
from PlexFunctions import GetPlexMetadata, init_plex_playqueue
import plexdb_functions as plexdb
import playlist_func as PL
import playqueue as PQ import playqueue as PQ
from playutils import PlayUtils from playutils import PlayUtils
from PKC_listitem import PKC_ListItem, convert_PKC_to_listitem from PKC_listitem import PKC_ListItem
from pickler import Playback_Successful from pickler import pickle_me, Playback_Successful
from utils import settings, dialog, language as lang import json_rpc as js
from utils import window, settings, dialog, language as lang, Lock_Function
import variables as v
import state
###############################################################################
LOG = getLogger("PLEX." + __name__)
LOCKER = Lock_Function(Lock())
###############################################################################
def playback_setup(plex_id, kodi_id, kodi_type, path): @LOCKER.lockthis
def playback_triage(plex_id=None, plex_type=None, path=None):
""" """
Get XML Hit this function for addon path playback, Plex trailers, etc.
For the single element, e.g. including trailers and parts Will setup playback first, then on second call complete playback.
For playQueue (init by Companion or Alexa)
Set up
PKC/Kodi/Plex Playqueue
Trailers
Clips
Several parts
companion playqueue
Alexa music album
Returns Playback_Successful() with potentially a PKC_ListItem() attached
(to be consumed by setResolvedURL)
""" """
LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s',
plex_id, plex_type, path)
if not state.AUTHENTICATED:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
dialog('notification', lang(29999), lang(30017))
# Don't cause second notification to appear
return Playback_Successful()
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
pos = js.get_position(playqueue.playlistid)
pos = pos if pos != -1 else 0
LOG.info('playQueue position: %s for %s', pos, playqueue)
# Have we already initiated playback?
init_done = True
try:
item = playqueue.items[pos]
except IndexError:
init_done = False
else:
init_done = item.init_done
# Either init the playback now, or - on 2nd pass - kick off playback
if init_done is False:
playback_init(plex_id, path, playqueue)
else:
conclude_playback(playqueue, pos)
def playback_init(plex_id, path, playqueue):
"""
Playback setup. Path is the original path PKC default.py has been called
with
"""
contextmenu_play = window('plex_contextplay') == 'true'
window('plex_contextplay', clear=True)
xml = GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
dialog('notification', lang(29999), lang(30128))
return
result = Playback_Successful()
listitem = PKC_ListItem()
# Set the original path again so Kodi will return a 2nd time to PKC
listitem.setPath(path)
api = API(xml[0])
plex_type = api.getType()
size_playlist = playqueue.kodi_pl.size()
# Can return -1
start_pos = max(playqueue.kodi_pl.getposition(), 0)
LOG.info("Playlist size %s", size_playlist)
LOG.info("Playlist starting position %s", start_pos)
resume, _ = api.getRuntime()
trailers = False trailers = False
if (api.getType() == v.PLEX_TYPE_MOVIE and if (plex_type == v.PLEX_TYPE_MOVIE and
not seektime and not resume and
sizePlaylist < 2 and size_playlist < 2 and
settings('enableCinema') == "true"): settings('enableCinema') == "true"):
if settings('askCinema') == "true": if settings('askCinema') == "true":
trailers = dialog('yesno', lang(29999), "Play trailers?") # "Play trailers?"
trailers = dialog('yesno', lang(29999), lang(33016))
trailers = True if trailers else False trailers = True if trailers else False
else: else:
trailers = True trailers = True
# Post to the PMS. REUSE THE PLAYQUEUE! # Post to the PMS. REUSE THE PLAYQUEUE!
xml = init_plex_playqueue(plex_id, xml = init_plex_playqueue(plex_id,
plex_lib_UUID, xml.attrib.get('librarySectionUUID'),
mediatype=api.getType(), mediatype=plex_type,
trailers=trailers) trailers=trailers)
pass if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
plex_id, xml.attrib.get('librarySectionUUID'))
# "Play error"
dialog('notification', lang(29999), lang(30128))
return
playqueue.clear()
PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml)
force_playback = False
if (not getCondVisibility('Window.IsVisible(MyVideoNav.xml)') and
not getCondVisibility('Window.IsVisible(VideoFullScreen.xml)')):
LOG.info("Detected playback from widget")
force_playback = True
if force_playback is False:
# Return the listelement for setResolvedURL
result.listitem = listitem
pickle_me(result)
# Wait for the setResolvedUrl to have taken its course - ugly
sleep(50)
_process_stack(playqueue, stack)
else:
# Need to kickoff playback, not using setResolvedURL
pickle_me(result)
_process_stack(playqueue, stack)
# Need a separate thread because Player won't return in time
listitem.setProperty('StartOffset', str(resume))
thread = Thread(target=Player().play,
args=(playqueue.kodi_pl, ))
thread.setDaemon(True)
thread.start()
def conclude_playback_startup(playqueue_no, def _prep_playlist_stack(xml):
pos, stack = []
plex_id=None, for item in xml:
kodi_id=None, api = API(item)
kodi_type=None, with plexdb.Get_Plex_DB() as plex_db:
path=None): plex_dbitem = plex_db.getItem_byId(api.getRatingKey())
try:
kodi_id = plex_dbitem[0]
kodi_type = plex_dbitem[4]
except TypeError:
kodi_id = None
kodi_type = None
for part_no, _ in enumerate(item[0]):
api.setPartNumber(part_no)
if kodi_id is not None:
# We don't need the URL, item is in the Kodi library
path = None
listitem = None
else:
# Need to redirect again to PKC to conclude playback
params = {
'mode': 'play',
'plex_id': api.getRatingKey(),
'plex_type': api.getType()
}
path = ('plugin://plugin.video.plexkodiconnect?%s'
% (urlencode(params)))
listitem = api.CreateListItemFromPlexItem()
api.set_listitem_artwork(listitem)
listitem.setPath(path)
stack.append({
'kodi_id': kodi_id,
'kodi_type': kodi_type,
'file': path,
'xml_video_element': item,
'listitem': listitem,
'part_no': part_no
})
return stack
def _process_stack(playqueue, stack):
"""
Takes our stack and adds the items to the PKC and Kodi playqueues.
This needs to be done AFTER setResolvedURL
"""
for i, item in enumerate(stack):
if item['kodi_id'] is not None:
# Use Kodi id & JSON so we get full artwork
playlist_item = PL.add_item_to_kodi_playlist(
playqueue,
i,
kodi_id=item['kodi_id'],
kodi_type=item['kodi_type'],
xml_video_element=item['xml_video_element'])
else:
playlist_item = PL.add_listitem_to_Kodi_playlist(
playqueue,
i,
item['listitem'],
file=item['file'],
xml_video_element=item['xml_video_element'])
playlist_item.part = item['part_no']
playlist_item.init_done = True
def conclude_playback(playqueue, pos):
""" """
ONLY if actually being played (e.g. at 5th position of a playqueue). ONLY if actually being played (e.g. at 5th position of a playqueue).
@ -63,17 +224,16 @@ def conclude_playback_startup(playqueue_no,
""" """
result = Playback_Successful() result = Playback_Successful()
listitem = PKC_ListItem() listitem = PKC_ListItem()
playqueue = PQ.PLAYQUEUES[playqueue_no]
item = playqueue.items[pos] item = playqueue.items[pos]
api = API(item.xml) if item.xml is not None:
api.setPartNumber(item.part) # Got a Plex element
api.CreateListItemFromPlexItem(listitem) api = API(item.xml)
if plex_id is not None: api.setPartNumber(item.part)
api.CreateListItemFromPlexItem(listitem)
playutils = PlayUtils(api, item) playutils = PlayUtils(api, item)
playurl = playutils.getPlayUrl() playurl = playutils.getPlayUrl()
elif path is not None: else:
playurl = path playurl = item.file
item.playmethod = 'DirectStream'
listitem.setPath(playurl) listitem.setPath(playurl)
if item.playmethod in ("DirectStream", "DirectPlay"): if item.playmethod in ("DirectStream", "DirectPlay"):
listitem.setSubtitles(api.externalSubs()) listitem.setSubtitles(api.externalSubs())
@ -81,4 +241,4 @@ def conclude_playback_startup(playqueue_no,
playutils.audio_subtitle_prefs(listitem) playutils.audio_subtitle_prefs(listitem)
listitem.setPath(playurl) listitem.setPath(playurl)
result.listitem = listitem result.listitem = listitem
return result pickle_me(result)

View file

@ -9,6 +9,7 @@ from xbmc import Player
from PKC_listitem import PKC_ListItem from PKC_listitem import PKC_ListItem
from pickler import pickle_me, Playback_Successful from pickler import pickle_me, Playback_Successful
from playbackutils import PlaybackUtils from playbackutils import PlaybackUtils
import playback
from utils import window from utils import window
from PlexFunctions import GetPlexMetadata from PlexFunctions import GetPlexMetadata
from PlexAPI import API from PlexAPI import API
@ -126,30 +127,23 @@ class Playback_Starter(Thread):
params = dict(parse_qsl(params)) params = dict(parse_qsl(params))
mode = params.get('mode') mode = params.get('mode')
LOG.debug('Received mode: %s, params: %s', mode, params) LOG.debug('Received mode: %s, params: %s', mode, params)
try: if mode == 'play':
if mode == 'play': result = playback.playback_triage(plex_id=params.get('plex_id'),
result = self.process_play(params.get('id'), plex_type=params.get('plex_type'),
params.get('dbid')) path=params.get('path'))
elif mode == 'companion': elif mode == 'companion':
result = self.process_companion() result = self.process_companion()
elif mode == 'plex_node': elif mode == 'plex_node':
result = self.process_plex_node( result = self.process_plex_node(
params.get('key'), params.get('key'),
params.get('view_offset'), params.get('view_offset'),
directplay=True if params.get('play_directly') else False, directplay=True if params.get('play_directly') else False,
node=False if params.get('node') == 'false' else True) node=False if params.get('node') == 'false' else True)
elif mode == 'context_menu': elif mode == 'context_menu':
ContextMenu() ContextMenu()
result = Playback_Successful() result = Playback_Successful()
except: # Let default.py know!
LOG.error('Error encountered for mode %s, params %s', # pickle_me(result)
mode, params)
import traceback
LOG.error(traceback.format_exc())
# Let default.py know!
pickle_me(None)
else:
pickle_me(result)
def run(self): def run(self):
queue = state.COMMAND_PIPELINE_QUEUE queue = state.COMMAND_PIPELINE_QUEUE

View file

@ -190,6 +190,8 @@ class Playlist_Item(object):
guid = None [str] Weird Plex guid guid = None [str] Weird Plex guid
xml = None [etree] XML from PMS, 1 lvl below <MediaContainer> xml = None [etree] XML from PMS, 1 lvl below <MediaContainer>
playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode'
part = 0 [int] part number if Plex video consists of mult. parts
init_done = False Set to True only if run through playback init
""" """
def __init__(self): def __init__(self):
self.id = None self.id = None
@ -203,8 +205,9 @@ class Playlist_Item(object):
self.guid = None self.guid = None
self.xml = None self.xml = None
self.playmethod = None self.playmethod = None
# Yet to be implemented: handling of a movie with several parts # If Plex video consists of several parts; part number
self.part = 0 self.part = 0
self.init_done = False
def __repr__(self): def __repr__(self):
""" """
@ -550,11 +553,11 @@ def add_item_to_PMS_playlist(playlist, pos, plex_id=None, kodi_item=None):
def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
file=None): file=None, xml_video_element=None):
""" """
Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS Adds an item to the KODI playlist only. WILL ALSO UPDATE OUR PLAYLISTS
Returns False if unsuccessful Returns the playlist item that was just added or None
file: str! file: str!
""" """
@ -574,17 +577,20 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None,
if reply.get('error') is not None: if reply.get('error') is not None:
LOG.error('Could not add item to playlist. Kodi reply. %s', reply) LOG.error('Could not add item to playlist. Kodi reply. %s', reply)
playlist.is_kodi_onadd() playlist.is_kodi_onadd()
return False return
item = playlist_item_from_kodi( if xml_video_element is not None:
{'id': kodi_id, 'type': kodi_type, 'file': file}) item = playlist_item_from_xml(playlist, xml_video_element)
if item.plex_id is not None: item.kodi_id = kodi_id
xml = GetPlexMetadata(item.plex_id) item.kodi_type = kodi_type
try: item.file = file
elif kodi_id is not None:
item = playlist_item_from_kodi(
{'id': kodi_id, 'type': kodi_type, 'file': file})
if item.plex_id is not None:
xml = GetPlexMetadata(item.plex_id)
item.xml = xml[-1] item.xml = xml[-1]
except (TypeError, IndexError):
LOG.error('Could not get metadata for playlist item %s', item)
playlist.items.insert(pos, item) playlist.items.insert(pos, item)
return True return item
def move_playlist_item(playlist, before_pos, after_pos): def move_playlist_item(playlist, before_pos, after_pos):
@ -706,6 +712,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file,
item.file = file item.file = file
playlist.items.insert(pos, item) playlist.items.insert(pos, item)
LOG.debug('Done inserting for %s', playlist) LOG.debug('Done inserting for %s', playlist)
return item
def remove_from_kodi_playlist(playlist, pos): def remove_from_kodi_playlist(playlist, pos):

View file

@ -58,17 +58,18 @@ def init_playqueues():
LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES) LOG.debug('Initialized the Kodi playqueues: %s', PLAYQUEUES)
def get_playqueue_from_type(typus): def get_playqueue_from_type(kodi_playlist_type):
""" """
Returns the playqueue according to the typus ('video', 'audio', Returns the playqueue according to the kodi_playlist_type ('video',
'picture') passed in 'audio', 'picture') passed in
""" """
with LOCK: with LOCK:
for playqueue in PLAYQUEUES: for playqueue in PLAYQUEUES:
if playqueue.type == typus: if playqueue.type == kodi_playlist_type:
break break
else: else:
raise ValueError('Wrong playlist type passed in: %s' % typus) raise ValueError('Wrong playlist type passed in: %s',
kodi_playlist_type)
return playqueue return playqueue