diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 8c18b648..574816ff 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -198,6 +198,10 @@ class KodiMonitor(Monitor): Will NOT be called if playback initiated by Kodi widgets """ playqueue = PQ.PLAYQUEUES[data['playlistid']] + # Have we initiated the playqueue already? If not, ignore this + if not playqueue.items: + LOG.debug('Playqueue not initiated - ignoring') + return # Did PKC cause this add? Then lets not do anything if playqueue.is_kodi_onadd() is False: LOG.debug('PKC added this item to the playqueue - ignoring') diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 1a5d7060..99615fb1 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -2,10 +2,10 @@ Used to kick off Kodi playback """ from logging import getLogger -from threading import Thread, Lock +from threading import Thread from urllib import urlencode -from xbmc import Player, getCondVisibility, sleep +from xbmc import Player, sleep from PlexAPI import API from PlexFunctions import GetPlexMetadata, init_plex_playqueue @@ -16,14 +16,14 @@ from playutils import PlayUtils 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 +from utils import window, settings, dialog, language as lang +from plexbmchelper.subscribers import LOCKER import variables as v import state ############################################################################### LOG = getLogger("PLEX." + __name__) -LOCKER = Lock_Function(Lock()) ############################################################################### @@ -34,42 +34,43 @@ def playback_triage(plex_id=None, plex_type=None, path=None): 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) + Will set Playback_Successful() with potentially a PKC_ListItem() attached + (to be consumed by setResolvedURL in default.py) + + If trailers or additional (movie-)parts are added, default.py is released + and a completely new player instance is called with a new playlist. This + circumvents most issues with Kodi & playqueues """ 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') + # Release default.py + pickle_me(Playback_Successful()) # "Unauthorized for PMS" dialog('notification', lang(29999), lang(30017)) - # Don't cause second notification to appear - return Playback_Successful() + return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type]) pos = js.get_position(playqueue.playlistid) + # Can return -1 (as in "no playlist") 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] + 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) + playback_init(plex_id, plex_type, playqueue) else: + # kick off playback on second pass conclude_playback(playqueue, pos) -def playback_init(plex_id, path, playqueue): +def playback_init(plex_id, plex_type, playqueue): """ - Playback setup. Path is the original path PKC default.py has been called - with + Playback setup if Kodi starts playing an item for the first time. """ + LOG.info('Initializing PKC playback') contextmenu_play = window('plex_contextplay') == 'true' window('plex_contextplay', clear=True) xml = GetPlexMetadata(plex_id) @@ -77,25 +78,15 @@ def playback_init(plex_id, path, playqueue): xml[0].attrib except (IndexError, TypeError, AttributeError): LOG.error('Could not get a PMS xml for plex id %s', plex_id) + # Release default.py + pickle_me(Playback_Successful()) # "Play error" - dialog('notification', lang(29999), lang(30128)) + dialog('notification', lang(29999), lang(30128), icon='{error}') 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 (plex_type == v.PLEX_TYPE_MOVIE and - not resume and - size_playlist < 2 and + if (plex_type == v.PLEX_TYPE_MOVIE and not resume and settings('enableCinema') == "true"): if settings('askCinema') == "true": # "Play trailers?" @@ -103,7 +94,8 @@ def playback_init(plex_id, path, playqueue): trailers = True if trailers else False else: trailers = True - # Post to the PMS. REUSE THE PLAYQUEUE! + LOG.info('Playing trailers: %s', trailers) + # Post to the PMS to create a playqueue - in any case due to Plex Companion xml = init_plex_playqueue(plex_id, xml.attrib.get('librarySectionUUID'), mediatype=plex_type, @@ -111,55 +103,44 @@ def playback_init(plex_id, path, playqueue): if xml is None: LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', plex_id, xml.attrib.get('librarySectionUUID')) + # Release default.py + pickle_me(Playback_Successful()) # "Play error" - dialog('notification', lang(29999), lang(30128)) + dialog('notification', lang(29999), lang(30128), icon='{error}') return + # Should already be empty, but just in case 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() + # Release our default.py before starting our own Kodi player instance + pickle_me(Playback_Successful()) + # Sleep a bit to let setResolvedUrl do its thing - bit ugly + sleep(200) + _process_stack(playqueue, stack) + # New thread to release this one sooner (e.g. harddisk spinning up) + thread = Thread(target=Player().play, + args=(playqueue.kodi_pl, )) + thread.setDaemon(True) + LOG.info('Done initializing PKC playback, starting Kodi player') + thread.start() 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: + if api.getType() != v.PLEX_TYPE_CLIP: + with plexdb.Get_Plex_DB() as plex_db: + plex_dbitem = plex_db.getItem_byId(api.getRatingKey()) + kodi_id = plex_dbitem[0] if plex_dbitem else None + kodi_type = plex_dbitem[4] if plex_dbitem else None + else: + # We will never store clips (trailers) in the Kodi DB 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: + for part, _ in enumerate(item[0]): + api.setPartNumber(part) + if kodi_id is None: # Need to redirect again to PKC to conclude playback params = { 'mode': 'play', @@ -167,17 +148,20 @@ def _prep_playlist_stack(xml): 'plex_type': api.getType() } path = ('plugin://plugin.video.plexkodiconnect?%s' - % (urlencode(params))) + % urlencode(params)) listitem = api.CreateListItemFromPlexItem() - api.set_listitem_artwork(listitem) listitem.setPath(path) + else: + # Will add directly via the Kodi DB + path = None + listitem = None stack.append({ 'kodi_id': kodi_id, 'kodi_type': kodi_type, 'file': path, 'xml_video_element': item, 'listitem': listitem, - 'part_no': part_no + 'part': part }) return stack @@ -185,26 +169,28 @@ def _prep_playlist_stack(xml): 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: + # getposition() can return -1 + pos = max(playqueue.kodi_pl.getposition(), 0) + 1 + for item in stack: + if item['kodi_id'] is None: playlist_item = PL.add_listitem_to_Kodi_playlist( playqueue, - i, + pos, item['listitem'], file=item['file'], xml_video_element=item['xml_video_element']) - playlist_item.part = item['part_no'] + else: + # Directly add element so we have full metadata + playlist_item = PL.add_item_to_kodi_playlist( + playqueue, + pos, + kodi_id=item['kodi_id'], + kodi_type=item['kodi_type'], + xml_video_element=item['xml_video_element']) + playlist_item.part = item['part'] playlist_item.init_done = True + pos += 1 def conclude_playback(playqueue, pos): diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index b3007d32..1d03237f 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -324,7 +324,8 @@ def playlist_item_from_plex(plex_id): return item -def playlist_item_from_xml(playlist, xml_video_element): +def playlist_item_from_xml(playlist, xml_video_element, kodi_id=None, + kodi_type=None): """ Returns a playlist element for the playqueue using the Plex xml @@ -338,7 +339,10 @@ def playlist_item_from_xml(playlist, xml_video_element): item.guid = xml_video_element.attrib.get('guid') if item.guid is not None: item.guid = escape_html(item.guid) - if item.plex_id: + if kodi_id is not None: + item.kodi_id = kodi_id + item.kodi_type = kodi_type + elif item.plex_id is not None: with plexdb.Get_Plex_DB() as plex_db: db_element = plex_db.getItem_byId(item.plex_id) try: @@ -369,20 +373,13 @@ def get_playlist_details_from_xml(playlist, xml): Takes a PMS xml as input and overwrites all the playlist's details, e.g. playlist.id with the XML's playQueueID """ - try: - playlist.id = xml.attrib['%sID' % playlist.kind] - playlist.version = xml.attrib['%sVersion' % playlist.kind] - playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] - playlist.selectedItemID = xml.attrib.get( - '%sSelectedItemID' % playlist.kind) - playlist.selectedItemOffset = xml.attrib.get( - '%sSelectedItemOffset' % playlist.kind) - except: - LOG.error('Could not parse xml answer from PMS for playlist %s', - playlist) - import traceback - LOG.error(traceback.format_exc()) - raise KeyError + playlist.id = xml.attrib['%sID' % playlist.kind] + playlist.version = xml.attrib['%sVersion' % playlist.kind] + playlist.shuffled = xml.attrib['%sShuffled' % playlist.kind] + playlist.selectedItemID = xml.attrib.get( + '%sSelectedItemID' % playlist.kind) + playlist.selectedItemOffset = xml.attrib.get( + '%sSelectedItemOffset' % playlist.kind) LOG.debug('Updated playlist from xml: %s', playlist) @@ -702,7 +699,7 @@ def add_listitem_to_Kodi_playlist(playlist, pos, listitem, file, pos, playlist) # Add the item into Kodi playlist playlist.kodi_onadd() - playlist.kodi_pl.add(file, listitem, index=pos) + playlist.kodi_pl.add(url=file, listitem=listitem, index=pos) # We need to add this to our internal queue as well if xml_video_element is not None: item = playlist_item_from_xml(playlist, xml_video_element)