diff --git a/README.md b/README.md index dc7eeeb7..52430a85 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-2.9.3-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) -[![beta version](https://img.shields.io/badge/beta_version-2.9.3-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) +[![stable version](https://img.shields.io/badge/stable_version-2.9.5-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) +[![beta version](https://img.shields.io/badge/beta_version-2.9.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq) diff --git a/addon.xml b/addon.xml index 9757c468..879dc254 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -83,7 +83,18 @@ Natūralioji „Plex“ integracija į „Kodi“ Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika! Naudokite savo pačių rizika - version 2.9.3: + version 2.9.5: +- Version 2.9.4 for everyone + +version 2.9.4 (beta only): +- Fix extras not playing when path substitution is enabled +- Fix Plex Companion device restarting playback when reconnecting to PKC +- Fix playback report not working after having played a non-Plex video file +- Change how items are added to Plex playqueues by using PMS machine identifier +- Optimize code for playqueue items +- Fix rare AttributeError when shutting down Kodi + +version 2.9.3: - version 2.9.2 for everyone version 2.9.2 (beta only): diff --git a/changelog.txt b/changelog.txt index b57875d9..215356de 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +version 2.9.5: +- Version 2.9.4 for everyone + +version 2.9.4 (beta only): +- Fix extras not playing when path substitution is enabled +- Fix Plex Companion device restarting playback when reconnecting to PKC +- Fix playback report not working after having played a non-Plex video file +- Change how items are added to Plex playqueues by using PMS machine identifier +- Optimize code for playqueue items +- Fix rare AttributeError when shutting down Kodi + version 2.9.3: - version 2.9.2 for everyone diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 8df104de..a7b615ab 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -398,7 +398,11 @@ class KodiMonitor(xbmc.Monitor): LOG.debug('No Plex id obtained - aborting playback report') app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template) return - item = PL.init_plex_playqueue(playqueue, plex_id=plex_id) + try: + item = PL.init_plex_playqueue(playqueue, plex_id=plex_id) + except PL.PlaylistError: + LOG.info('Could not initialize the Plex playlist') + return item.file = path # Set the Plex container key (e.g. using the Plex playqueue) container_key = None diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 441d71b6..cb228793 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -234,14 +234,9 @@ def _playback_init(plex_id, plex_type, playqueue, pos): playqueue.clear() if plex_type != v.PLEX_TYPE_CLIP: # Post to the PMS to create a playqueue - in any case due to Companion - section_uuid = xml.attrib.get('librarySectionUUID') - xml = PF.init_plex_playqueue(plex_id, - section_uuid, - mediatype=plex_type, - trailers=trailers) + xml = PF.init_plex_playqueue(plex_id, plex_type, trailers=trailers) if xml is None: - LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', - plex_id, section_uuid) + LOG.error('Could not get a playqueue xml for plex id %s', plex_id) # "Play error" utils.dialog('notification', utils.lang(29999), @@ -519,10 +514,8 @@ def process_indirect(key, offset, resolve=True): playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type]) playqueue.clear() - item = PL.Playlist_Item() - item.xml = xml[0] + item = PL.playlist_item_from_xml(xml[0]) item.offset = offset - item.plex_type = v.PLEX_TYPE_CLIP item.playmethod = 'DirectStream' # Need to get yet another xml to get the final playback url diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index d32912f8..6c611adc 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -129,19 +129,33 @@ class Playqueue_Object(object): 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) -class Playlist_Item(object): + 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): """ Object to fill our playqueues and playlists with. id = None [int] Plex playlist/playqueue id, e.g. playQueueItemID plex_id = None [int] Plex unique item id, "ratingKey" plex_type = None [str] Plex type, e.g. 'movie', 'clip' - plex_uuid = None [str] Plex librarySectionUUID kodi_id = None [int] Kodi unique kodi id (unique only within type!) kodi_type = None [str] Kodi type: 'movie' file = None [str] Path to the item's file. STRING!! - uri = None [str] Weird Plex uri path involving plex_uuid. STRING! + uri = None [str] PMS path to item; will be auto-set with plex_id guid = None [str] Weird Plex guid xml = None [etree] XML from PMS, 1 lvl below playmethod = None [str] either 'DirectPlay', 'DirectStream', 'Transcode' @@ -151,21 +165,20 @@ class Playlist_Item(object): force_transcode [bool] defaults to False """ def __init__(self): - self._id = None + self.id = None self._plex_id = None self.plex_type = None - self.plex_uuid = None - self._kodi_id = None + self.kodi_id = None self.kodi_type = None self.file = None - self.uri = None + self._uri = None self.guid = None self.xml = None self.playmethod = None - self._playcount = None - self._offset = None + self.playcount = None + self.offset = None # If Plex video consists of several parts; part number - self._part = 0 + self.part = 0 self.force_transcode = False # Shall we ask user to resume this item? # None: ask user to resume @@ -179,82 +192,31 @@ class Playlist_Item(object): @plex_id.setter def plex_id(self, value): - if not isinstance(value, int) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) self._plex_id = value + self._uri = ('server://%s/com.plexapp.plugins.library/library/metadata/%s' % + (app.CONN.machine_identifier, value)) @property - def id(self): - return self._id + def uri(self): + return self._uri - @id.setter - def id(self, value): - if not isinstance(value, int) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) - self._id = value - - @property - def kodi_id(self): - return self._kodi_id - - @kodi_id.setter - def kodi_id(self, value): - if not isinstance(value, int) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) - self._kodi_id = value - - @property - def playcount(self): - return self._playcount - - @playcount.setter - def playcount(self, value): - if not isinstance(value, int) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) - self._playcount = value - - @property - def offset(self): - return self._offset - - @offset.setter - def offset(self, value): - if not isinstance(value, (int, float)) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) - self._offset = value - - @property - def part(self): - return self._part - - @part.setter - def part(self, value): - if not isinstance(value, int) and value is not None: - raise TypeError('Passed %s instead of int!' % type(value)) - self._part = value - - def __repr__(self): - answ = ("{{" + def __unicode__(self): + return ("{{" "'id': {self.id}, " "'plex_id': {self.plex_id}, " "'plex_type': '{self.plex_type}', " - "'plex_uuid': '{self.plex_uuid}', " "'kodi_id': {self.kodi_id}, " "'kodi_type': '{self.kodi_type}', " "'file': '{self.file}', " - "'uri': '{self.uri}', " "'guid': '{self.guid}', " "'playmethod': '{self.playmethod}', " "'playcount': {self.playcount}, " "'offset': {self.offset}, " "'force_transcode': {self.force_transcode}, " - "'part': {self.part}, ".format(self=self)) - answ = answ.encode('utf-8') - # etree xml.__repr__() could return string, not unicode - return answ + b"'xml': \"{self.xml}\"}}".format(self=self) + "'part': {self.part}".format(self=self)) - def __str__(self): - return self.__repr__() + def __repr__(self): + return self.__unicode__().encode('utf-8') def plex_stream_index(self, kodi_stream_index, stream_type): """ @@ -319,7 +281,7 @@ def playlist_item_from_kodi(kodi_item): Supply with data['item'] as returned from Kodi JSON-RPC interface. kodi_item dict contains keys 'id', 'type', 'file' (if applicable) """ - item = Playlist_Item() + item = PlaylistItem() item.kodi_id = kodi_item.get('id') item.kodi_type = kodi_item.get('type') if item.kodi_id: @@ -328,7 +290,6 @@ def playlist_item_from_kodi(kodi_item): if db_item: item.plex_id = db_item['plex_id'] item.plex_type = db_item['plex_type'] - item.plex_uuid = db_item['plex_id'] # we dont need the uuid yet :-) item.file = kodi_item.get('file') if item.plex_id is None and item.file is not None: try: @@ -338,13 +299,6 @@ def playlist_item_from_kodi(kodi_item): query = dict(utils.parse_qsl(query)) item.plex_id = utils.cast(int, query.get('plex_id')) item.plex_type = query.get('itemType') - if item.plex_id is None and item.file is not None: - item.uri = ('library://whatever/item/%s' - % utils.quote(item.file, safe='')) - else: - # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER - item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (item.plex_uuid, item.plex_id)) LOG.debug('Made playlist item from Kodi: %s', item) return item @@ -358,7 +312,8 @@ def verify_kodi_item(plex_id, kodi_item): set to None if unsuccessful. Will raise a PlaylistError if plex_id is None and kodi_item['file'] starts - with either 'plugin' or 'http' + with either 'plugin' or 'http'. + Will raise KeyError if neither plex_id nor kodi_id are found """ if plex_id is not None or kodi_item.get('id') is not None: # Got all the info we need @@ -375,8 +330,8 @@ def verify_kodi_item(plex_id, kodi_item): if ((kodi_item['file'].startswith('plugin') and not kodi_item['file'].startswith('plugin://%s' % v.ADDON_ID)) or kodi_item['file'].startswith('http')): - LOG.info('kodi_item %s cannot be used for Plex playback', kodi_item) - raise PlaylistError + LOG.debug('kodi_item cannot be used for Plex playback: %s', kodi_item) + raise PlaylistError('kodi_item cannot be used for Plex playback') LOG.debug('Starting research for Kodi id since we didnt get one: %s', kodi_item) # Try the VIDEO DB first - will find both movies and episodes @@ -388,6 +343,8 @@ def verify_kodi_item(plex_id, kodi_item): db_type='music') kodi_item['id'] = kodi_id kodi_item['type'] = None if kodi_id is None else kodi_type + if plex_id is None and kodi_id is None: + raise KeyError('Neither Plex nor Kodi id found for %s' % kodi_item) LOG.debug('Research results for kodi_item: %s', kodi_item) return kodi_item @@ -398,7 +355,7 @@ def playlist_item_from_plex(plex_id): Returns a Playlist_Item """ - item = Playlist_Item() + item = PlaylistItem() item.plex_id = plex_id with PlexDB(lock=False) as plexdb: db_item = plexdb.item_by_id(plex_id) @@ -408,9 +365,6 @@ def playlist_item_from_plex(plex_id): item.kodi_type = db_item['kodi_type'] else: raise KeyError('Could not find plex_id %s in database' % plex_id) - item.plex_uuid = plex_id - item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % - (item.plex_uuid, plex_id)) LOG.debug('Made playlist item from plex: %s', item) return item @@ -421,7 +375,7 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None): xml_video_element: etree xml piece 1 level underneath """ - item = Playlist_Item() + item = PlaylistItem() api = API(xml_video_element) item.plex_id = api.plex_id item.plex_type = api.plex_type @@ -431,9 +385,10 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None): if kodi_id is not None: item.kodi_id = kodi_id item.kodi_type = kodi_type - elif item.plex_id is not None and item.plex_type != v.PLEX_TYPE_CLIP: + elif item.plex_type != v.PLEX_TYPE_CLIP: with PlexDB(lock=False) as plexdb: - db_element = plexdb.item_by_id(item.plex_id) + db_element = plexdb.item_by_id(item.plex_id, + plex_type=item.plex_type) if db_element: item.kodi_id = db_element['kodi_id'] item.kodi_type = db_element['kodi_type'] @@ -487,6 +442,8 @@ def update_playlist_from_PMS(playlist, playlist_id=None, xml=None): need to fetch a new playqueue If an xml is passed in, the playlist will be overwritten with its info + + Raises PlaylistError if something went wront """ if xml is None: xml = get_PMS_playlist(playlist, playlist_id) @@ -508,8 +465,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None): Returns the first PKC playlist item or raises PlaylistError """ LOG.debug('Initializing the playqueue on the Plex side: %s', playlist) - playlist.clear(kodi=False) verify_kodi_item(plex_id, kodi_item) + playlist.clear(kodi=False) try: if plex_id: item = playlist_item_from_plex(plex_id) @@ -523,6 +480,8 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None): xml = DU().downloadUrl(url="{server}/%ss" % playlist.kind, action_type="POST", parameters=params) + if xml in (None, 401): + raise PlaylistError('Did not receive a valid xml from the PMS') get_playlist_details_from_xml(playlist, xml) # Need to get the details for the playlist item item = playlist_item_from_xml(xml[0]) @@ -706,7 +665,7 @@ def get_PMS_playlist(playlist, playlist_id=None): Fetches the PMS playlist/playqueue as an XML. Pass in playlist_id if we need to fetch a new playlist - Returns None if something went wrong + Raises PlaylistError if something went wrong """ playlist_id = playlist_id if playlist_id else playlist.id if playlist.kind == 'playList': @@ -716,7 +675,7 @@ def get_PMS_playlist(playlist, playlist_id=None): try: xml.attrib except AttributeError: - xml = None + raise PlaylistError('Did not get a valid xml') return xml diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index f9fed4e4..7e78bee9 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -86,7 +86,11 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None): playqueue.clear() for i, child in enumerate(xml): api = API(child) - PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id) + try: + PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id) + except PL.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) @@ -166,6 +170,14 @@ class PlayqueueMonitor(backgroundthread.KillableThread): except PL.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 diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 755dded1..d7eec3d6 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -17,7 +17,7 @@ class PlayUtils(): def __init__(self, api, playqueue_item): """ init with api (PlexAPI wrapper of the PMS xml element) and - playqueue_item (Playlist_Item()) + playqueue_item (PlaylistItem()) """ self.api = api self.item = playqueue_item diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index dbdea11e..80ab3bd2 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -49,13 +49,21 @@ def update_playqueue_from_PMS(playqueue, if transient_token is None: transient_token = playqueue.plex_transient_token with app.APP.lock_playqueues: - xml = PL.get_PMS_playlist(playqueue, playqueue_id) try: - xml.attrib - except AttributeError: + xml = PL.get_PMS_playlist(playqueue, playqueue_id) + except PL.PlaylistError: LOG.error('Could now download playqueue %s', playqueue_id) return - playqueue.clear() + if playqueue.id == playqueue_id: + # This seems to be happening ONLY if a Plex Companion device + # reconnects and Kodi is already playing something - silly, really + # For all other cases, a new playqueue is generated by Plex + LOG.debug('Update for existing playqueue detected') + new = False + else: + new = True + playqueue.clear() + # Get new metadata for the playqueue first try: PL.get_playlist_details_from_xml(playqueue, xml) except PL.PlaylistError: @@ -63,10 +71,33 @@ def update_playqueue_from_PMS(playqueue, return playqueue.repeat = 0 if not repeat else int(repeat) playqueue.plex_transient_token = transient_token - playback.play_xml(playqueue, - xml, - offset=offset, - start_plex_id=start_plex_id) + if new: + playback.play_xml(playqueue, + xml, + offset=offset, + start_plex_id=start_plex_id) + return + # Updates to playqueues could potentially become a bit more ugly... + if app.APP.is_playing: + try: + playerid = js.get_player_ids()[0] + except IndexError: + LOG.error('Unexpectately could not get Kodi player id') + return + if app.PLAYSTATE.player_states[playerid]['plex_id'] == start_plex_id: + # Nothing to do - let's not seek to avoid jumps in playback + return + pos = playqueue.position_from_plex_id(start_plex_id) + LOG.debug('Skipping to position %s for %s', pos, playqueue) + js.skipto(pos) + if offset: + js.seek_to(offset) + return + # Need to initiate playback again using our existing playqueue + app.APP.player.play(playqueue.kodi_pl, + None, + False, + playqueue.position_from_plex_id(start_plex_id)) class PlexCompanion(backgroundthread.KillableThread): @@ -165,7 +196,7 @@ class PlexCompanion(backgroundthread.KillableThread): update_playqueue_from_PMS(playqueue, playqueue_id=container_key, repeat=query.get('repeat'), - offset=data.get('offset'), + offset=utils.cast(int, data.get('offset')), transient_token=data.get('token'), start_plex_id=key) diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 1b7e8fbc..a89fbf16 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -820,16 +820,15 @@ def get_plex_sections(): return xml -def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie', - trailers=False): +def init_plex_playqueue(plex_id, plex_type, trailers=False): """ Returns raw API metadata XML dump for a playlist with e.g. trailers. """ url = "{server}/playQueues" args = { - 'type': mediatype, - 'uri': ('library://{0}/item/%2Flibrary%2Fmetadata%2F{1}'.format( - librarySectionUUID, plex_id)), + 'type': plex_type, + 'uri': ('server://%s/com.plexapp.plugins.library/library/metadata/%s' % + (app.CONN.machine_identifier, plex_id)), 'includeChapters': '1', 'shuffle': '0', 'repeat': '0' diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 2656d58f..b21ac258 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -362,7 +362,7 @@ KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = { REMAP_TYPE_FROM_PLEXTYPE = { PLEX_TYPE_MOVIE: 'movie', - PLEX_TYPE_CLIP: 'clip', + PLEX_TYPE_CLIP: 'movie', PLEX_TYPE_SHOW: 'tv', PLEX_TYPE_SEASON: 'tv', PLEX_TYPE_EPISODE: 'tv', @@ -400,20 +400,6 @@ TRANSLATION_FROM_PLEXTYPE = { PLEX_TYPE_PHOTO: 1, } -REMAP_TYPE_FROM_PLEXTYPE = { - 'movie': 'movie', - 'show': 'tv', - 'season': 'tv', - 'episode': 'tv', - 'artist': 'music', - 'album': 'music', - 'song': 'music', - 'track': 'music', - 'clip': 'clip', - 'photo': 'photo' -} - - PLEX_TYPE_FROM_WEBSOCKET = { 1: PLEX_TYPE_MOVIE, 2: PLEX_TYPE_SHOW,