diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index d48612ce..9b0a14e6 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -291,12 +291,11 @@ class Movies(Items): path = playurl.replace(filename, "") if doIndirect: # Set plugin path and media flags using real filename - path = "plugin://plugin.video.plexkodiconnect/movies/" + path = "plugin://plugin.video.plexkodiconnect" params = { - 'filename': API.getKey(), - 'id': itemid, - 'dbid': movieid, - 'mode': "play" + 'mode': 'play', + 'plex_id': itemid, + 'plex_type': v.PLEX_TYPE_MOVIE } filename = "%s?%s" % (path, urlencode(params)) playurl = filename diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 17bf28eb..8c18b648 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -12,11 +12,11 @@ from PlexFunctions import scrobble from kodidb_functions import kodiid_from_filename from plexbmchelper.subscribers import LOCKER from PlexAPI import API +import playqueue as PQ import json_rpc as js import playlist_func as PL import state import variables as v -import playqueue as PQ ############################################################################### @@ -254,31 +254,16 @@ class KodiMonitor(Monitor): return playqueue.clear() - @LOCKER.lockthis - def PlayBackStart(self, data): + def _get_ids(self, json_item): """ - 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 - try: - kodi_type = data['item']['type'] - 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') + kodi_id = json_item.get('id') + kodi_type = json_item.get('type') + path = json_item.get('file') if not path and not kodi_id: LOG.info('Aborting playback report - no Kodi id or file for %s', - json_data) - return + json_item) + raise RuntimeError # Plex id will NOT be set with direct paths plex_id = state.PLEX_IDS.get(path) try: @@ -306,28 +291,49 @@ class KodiMonitor(Monitor): except TypeError: # No plex id, hence item not in the library. E.g. clips pass - info = js.get_player_props(playerid) - 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]) - # Check whether we need to init our playqueues (e.g. direct play) - init = False - playqueue = PQ.PLAYQUEUES[playerid] + return kodi_id, kodi_type, plex_id, plex_type + + @LOCKER.lockthis + 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 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: - init = True - if init is False and plex_id is not None: - if plex_id != playqueue.items[info['position']].plex_id: - init = True - elif init is False and path != playqueue.items[info['position']].file: - init = True - if init is True: - LOG.debug('Need to initialize Plex and PKC playqueue') + init_done = False + if init_done is True: + kodi_id = item.kodi_id + kodi_type = item.kodi_type + plex_id = item.plex_id + plex_type = item.plex_type + else: + 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: PL.init_Plex_playlist(playqueue, plex_id=plex_id) else: @@ -335,17 +341,25 @@ class KodiMonitor(Monitor): kodi_item={'id': kodi_id, 'type': kodi_type, 'file': path}) - # Set the Plex container key (e.g. using the Plex playqueue) - container_key = None - if info['playlistid'] != -1: - # -1 is Kodi's answer if there is no playlist - container_key = PQ.PLAYQUEUES[playerid].id - if container_key is not None: - container_key = '/playQueues/%s' % container_key - elif plex_id is not None: - container_key = '/library/metadata/%s' % plex_id - state.PLAYER_STATES[playerid]['container_key'] = container_key - LOG.debug('Set the Plex container_key to: %s', container_key) + # Set the Plex container key (e.g. using the Plex playqueue) + container_key = None + if info['playlistid'] != -1: + # -1 is Kodi's answer if there is no playlist + container_key = PQ.PLAYQUEUES[playerid].id + if container_key is not None: + container_key = '/playQueues/%s' % container_key + elif plex_id is not None: + container_key = '/library/metadata/%s' % plex_id + state.PLAYER_STATES[playerid]['container_key'] = 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): """ diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 42808be9..1a5d7060 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -1,52 +1,213 @@ """ 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 PlexFunctions import GetPlexMetadata, init_plex_playqueue +import plexdb_functions as plexdb +import playlist_func as PL import playqueue as PQ from playutils import PlayUtils -from PKC_listitem import PKC_ListItem, convert_PKC_to_listitem -from pickler import Playback_Successful -from utils import settings, dialog, language as lang +from PKC_listitem import PKC_ListItem +from pickler import pickle_me, Playback_Successful +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 - For the single element, e.g. including trailers and parts - For playQueue (init by Companion or Alexa) - Set up - PKC/Kodi/Plex Playqueue - Trailers - Clips - Several parts - companion playqueue - Alexa music album + Hit this function for addon path playback, Plex trailers, etc. + Will setup playback first, then on second call complete playback. + 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 - if (api.getType() == v.PLEX_TYPE_MOVIE and - not seektime and - sizePlaylist < 2 and + if (plex_type == v.PLEX_TYPE_MOVIE and + not resume and + size_playlist < 2 and settings('enableCinema') == "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 else: trailers = True # Post to the PMS. REUSE THE PLAYQUEUE! xml = init_plex_playqueue(plex_id, - plex_lib_UUID, - mediatype=api.getType(), + xml.attrib.get('librarySectionUUID'), + mediatype=plex_type, 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, - pos, - plex_id=None, - kodi_id=None, - kodi_type=None, - path=None): +def _prep_playlist_stack(xml): + stack = [] + for item in xml: + api = API(item) + with plexdb.Get_Plex_DB() as plex_db: + 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). @@ -63,17 +224,16 @@ def conclude_playback_startup(playqueue_no, """ result = Playback_Successful() listitem = PKC_ListItem() - playqueue = PQ.PLAYQUEUES[playqueue_no] item = playqueue.items[pos] - api = API(item.xml) - api.setPartNumber(item.part) - api.CreateListItemFromPlexItem(listitem) - if plex_id is not None: + if item.xml is not None: + # Got a Plex element + api = API(item.xml) + api.setPartNumber(item.part) + api.CreateListItemFromPlexItem(listitem) playutils = PlayUtils(api, item) playurl = playutils.getPlayUrl() - elif path is not None: - playurl = path - item.playmethod = 'DirectStream' + else: + playurl = item.file listitem.setPath(playurl) if item.playmethod in ("DirectStream", "DirectPlay"): listitem.setSubtitles(api.externalSubs()) @@ -81,4 +241,4 @@ def conclude_playback_startup(playqueue_no, playutils.audio_subtitle_prefs(listitem) listitem.setPath(playurl) result.listitem = listitem - return result + pickle_me(result) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 901e75e6..b1491ee8 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -9,6 +9,7 @@ from xbmc import Player from PKC_listitem import PKC_ListItem from pickler import pickle_me, Playback_Successful from playbackutils import PlaybackUtils +import playback from utils import window from PlexFunctions import GetPlexMetadata from PlexAPI import API @@ -126,30 +127,23 @@ class Playback_Starter(Thread): params = dict(parse_qsl(params)) mode = params.get('mode') LOG.debug('Received mode: %s, params: %s', mode, params) - try: - if mode == 'play': - result = self.process_play(params.get('id'), - params.get('dbid')) - elif mode == 'companion': - result = self.process_companion() - elif mode == 'plex_node': - result = self.process_plex_node( - params.get('key'), - params.get('view_offset'), - directplay=True if params.get('play_directly') else False, - node=False if params.get('node') == 'false' else True) - elif mode == 'context_menu': - ContextMenu() - result = Playback_Successful() - except: - LOG.error('Error encountered for mode %s, params %s', - mode, params) - import traceback - LOG.error(traceback.format_exc()) - # Let default.py know! - pickle_me(None) - else: - pickle_me(result) + if mode == 'play': + result = playback.playback_triage(plex_id=params.get('plex_id'), + plex_type=params.get('plex_type'), + path=params.get('path')) + elif mode == 'companion': + result = self.process_companion() + elif mode == 'plex_node': + result = self.process_plex_node( + params.get('key'), + params.get('view_offset'), + directplay=True if params.get('play_directly') else False, + node=False if params.get('node') == 'false' else True) + elif mode == 'context_menu': + ContextMenu() + result = Playback_Successful() + # Let default.py know! + # pickle_me(result) def run(self): queue = state.COMMAND_PIPELINE_QUEUE diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 3d4e8dc5..b3007d32 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -190,6 +190,8 @@ class Playlist_Item(object): guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below 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): self.id = None @@ -203,8 +205,9 @@ class Playlist_Item(object): self.guid = None self.xml = 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.init_done = False 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, - file=None): + file=None, xml_video_element=None): """ 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! """ @@ -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: LOG.error('Could not add item to playlist. Kodi reply. %s', reply) playlist.is_kodi_onadd() - return False - 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) - try: + return + if xml_video_element is not None: + item = playlist_item_from_xml(playlist, xml_video_element) + item.kodi_id = kodi_id + item.kodi_type = kodi_type + 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] - except (TypeError, IndexError): - LOG.error('Could not get metadata for playlist item %s', item) playlist.items.insert(pos, item) - return True + return item 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 playlist.items.insert(pos, item) LOG.debug('Done inserting for %s', playlist) + return item def remove_from_kodi_playlist(playlist, pos): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 40804803..aa9529dc 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -58,17 +58,18 @@ def init_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', - 'picture') passed in + Returns the playqueue according to the kodi_playlist_type ('video', + 'audio', 'picture') passed in """ with LOCK: for playqueue in PLAYQUEUES: - if playqueue.type == typus: + if playqueue.type == kodi_playlist_type: break else: - raise ValueError('Wrong playlist type passed in: %s' % typus) + raise ValueError('Wrong playlist type passed in: %s', + kodi_playlist_type) return playqueue