diff --git a/README.md b/README.md index 8ce19325..1edf79a8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-2.7.6-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.7.6-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.7.7-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.7.7-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 139b9bf4..87087cf1 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -77,7 +77,15 @@ Нативна інтеграція Plex в Kodi Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик! Використовуйте на свій ризик - version 2.7.6: + version 2.7.7: +- Fix sync not working due to non-ASCII Plex library names +- Fix PKC synching playstate to wrong user on profile switch. Be aware that Kodi profile switches are error-prone +- Fix playback sometimes not being reported for direct paths +- Fix float() argument must be a string or a number +- Fix nodes for skin use +- Fix 'NoneType' object has no attribute 'kodi_path' + +version 2.7.6: - Make 2.7.5 available for everyone version 2.7.5: diff --git a/changelog.txt b/changelog.txt index 321cbab8..098e0f99 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,11 @@ +version 2.7.7: +- Fix sync not working due to non-ASCII Plex library names +- Fix PKC synching playstate to wrong user on profile switch. Be aware that Kodi profile switches are error-prone +- Fix playback sometimes not being reported for direct paths +- Fix float() argument must be a string or a number +- Fix nodes for skin use +- Fix 'NoneType' object has no attribute 'kodi_path' + version 2.7.6: - Make 2.7.5 available for everyone diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 229fa20c..3370f2dd 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -84,7 +84,7 @@ def show_main_menu(content_type=None): # Get nodes from the window props totalnodes = int(utils.window('Plex.nodes.total') or 0) for i in range(totalnodes): - path = utils.window('Plex.nodes.%s.content' % i) + path = utils.window('Plex.nodes.%s.index' % i) if not path: continue label = utils.window('Plex.nodes.%s.title' % i) @@ -114,9 +114,9 @@ def show_main_menu(content_type=None): continue # Add ANOTHER menu item that uses add-on paths instead of direct # paths in order to let the user navigate into all submenus - addon_path = utils.window('Plex.nodes.%s.addon_path' % i) + addon_index = utils.window('Plex.nodes.%s.addon_index' % i) # Append "(More...)" to the label - directory_item('%s (%s)' % (label, utils.lang(22082)), addon_path) + directory_item('%s (%s)' % (label, utils.lang(22082)), addon_index) # Playlists if content_type != 'image': path = 'plugin://%s?mode=playlists' % v.ADDON_ID diff --git a/resources/lib/kodi_db/__init__.py b/resources/lib/kodi_db/__init__.py index 4778d422..70e79bf9 100644 --- a/resources/lib/kodi_db/__init__.py +++ b/resources/lib/kodi_db/__init__.py @@ -22,12 +22,9 @@ def kodiid_from_filename(path, kodi_type=None, db_type=None): """ kodi_id = None path = utils.try_decode(path) - try: - filename = path.rsplit('/', 1)[1] - path = path.rsplit('/', 1)[0] + '/' - except IndexError: - filename = path.rsplit('\\', 1)[1] - path = path.rsplit('\\', 1)[0] + '\\' + path, filename = path_ops.path.split(path) + # Make sure path ends in either '/' or '\' + path = path_ops.path.join(path, '') if kodi_type == v.KODI_TYPE_SONG or db_type == 'music': with KodiMusicDB(lock=False) as kodidb: try: diff --git a/resources/lib/library_sync/full_sync.py b/resources/lib/library_sync/full_sync.py index 80973562..218bf23d 100644 --- a/resources/lib/library_sync/full_sync.py +++ b/resources/lib/library_sync/full_sync.py @@ -258,6 +258,7 @@ class FullSync(common.fullsync_mixin): for section in (x for x in sections.SECTIONS if x.section_type == kind[1]): if self.isCanceled(): + LOG.debug('Need to exit now') return if not section.sync_to_kodi: LOG.info('User chose to not sync section %s', section) @@ -283,6 +284,8 @@ class FullSync(common.fullsync_mixin): self.section_success = False else: queue.put(element) + except Exception: + utils.ERROR(notify=True) finally: queue.put(None) @@ -410,7 +413,7 @@ class FullSync(common.fullsync_mixin): def _run(self): self.current_sync = timing.plex_now() # Get latest Plex libraries and build playlist and video node files - if not sections.sync_from_pms(self): + if self.isCanceled() or not sections.sync_from_pms(self): return self.successful = True try: @@ -422,10 +425,7 @@ class FullSync(common.fullsync_mixin): # Actual syncing - do only new items first LOG.info('Running full_library_sync with repair=%s', self.repair) - if not self.full_library_sync(): - self.successful = False - return - if self.isCanceled(): + if self.isCanceled() or not self.full_library_sync(): self.successful = False return if common.PLAYLIST_SYNC_ENABLED and not playlists.full_sync(): @@ -437,6 +437,7 @@ class FullSync(common.fullsync_mixin): self.dialog.close() if self.threader: self.threader.shutdown() + self.threader = None if not self.successful and not self.isCanceled(): # "ERROR in library sync" utils.dialog('notification', diff --git a/resources/lib/library_sync/sections.py b/resources/lib/library_sync/sections.py index cfa3bdce..800000e6 100644 --- a/resources/lib/library_sync/sections.py +++ b/resources/lib/library_sync/sections.py @@ -99,7 +99,7 @@ class Section(object): "'section_type': '{self.section_type}', " "'sync_to_kodi': {self.sync_to_kodi}, " "'last_sync': {self.last_sync}" - "}}").format(self=self) + "}}").format(self=self).encode('utf-8') __str__ = __repr__ def __nonzero__(self): @@ -246,20 +246,27 @@ class Section(object): } if not self.sync_to_kodi: args['synched'] = 'false' - addon_path = self.addon_path(args) + addon_index = self.addon_path(args) if self.sync_to_kodi and self.section_type in v.PLEX_VIDEOTYPES: - path = 'library://video/Plex-%s' % self.section_id + path = 'library://video/Plex-{0}/{0}_all.xml' + path = path.format(self.section_id) + index = 'library://video/Plex-%s' % self.section_id else: # No xmls to link to - let's show the listings on the fly - path = addon_path + index = addon_index + args['key'] = '/library/sections/%s/all' % self.section_id + path = self.addon_path(args) + # .index will list all possible nodes for this library + utils.window('%s.index' % self.node, value=index) utils.window('%s.title' % self.node, value=self.name) utils.window('%s.type' % self.node, value=self.content) utils.window('%s.content' % self.node, value=path) + # .path leads to all elements of this library utils.window('%s.path' % self.node, value='ActivateWindow(Videos,%s,return)' % path) utils.window('%s.id' % self.node, value=str(self.section_id)) # To let the user navigate into this node when selecting widgets - utils.window('%s.addon_path' % self.node, value=addon_path) + utils.window('%s.addon_index' % self.node, value=addon_index) if not self.sync_to_kodi: self.remove_files_from_kodi() return @@ -307,7 +314,7 @@ class Section(object): xml = getattr(nodes, 'node_%s' % node_type)(self, node_name) self._write_xml(xml, xml_name) self.order += 1 - self._window_node(path, node_name, node_type) + self._window_node(path, node_name, node_type, pms_node) def _write_xml(self, xml, xml_name): LOG.debug('Creating xml for section %s: %s', self.name, xml_name) @@ -329,17 +336,17 @@ class Section(object): utils.indent(xml) etree.ElementTree(xml).write(self.playlist_path, encoding='utf-8') - def _window_node(self, path, node_name, node_type): + def _window_node(self, path, node_name, node_type, pms_node): """ Will save this section's node to the Kodi window variables Uses the same conventions/logic as Emby for Kodi does """ - if self.section_type == v.PLEX_TYPE_ARTIST: - window_path = 'ActivateWindow(Music,%s,return)' % path - elif self.section_type == v.PLEX_TYPE_PHOTO: + if pms_node or not self.sync_to_kodi: # Check: elif node_type in ('browse', 'homevideos', 'photos'): window_path = path + elif self.section_type == v.PLEX_TYPE_ARTIST: + window_path = 'ActivateWindow(Music,%s,return)' % path else: window_path = 'ActivateWindow(Videos,%s,return)' % path # if node_type == 'all': diff --git a/resources/lib/library_sync/time.py b/resources/lib/library_sync/time.py index 197a084b..55ccb7b2 100644 --- a/resources/lib/library_sync/time.py +++ b/resources/lib/library_sync/time.py @@ -90,9 +90,9 @@ def sync_pms_time(): # Toggle watched state back PF.scrobble(plex_id, 'unwatched') try: - plextime = xml[0].get('lastViewedAt') - except (IndexError, TypeError, AttributeError): - LOG.error('Could not get lastViewedAt - aborting') + plextime = xml[0].attrib['lastViewedAt'] + except (IndexError, TypeError, AttributeError, KeyError): + LOG.warn('Could not get lastViewedAt - aborting') return False # Calculate time offset Kodi-PMS diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index a76d6529..96fd6cfd 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -372,19 +372,15 @@ def verify_kodi_item(plex_id, kodi_item): raise PlaylistError LOG.debug('Starting research for Kodi id since we didnt get one: %s', kodi_item) - kodi_id, _ = kodiid_from_filename(kodi_item['file'], - v.KODI_TYPE_MOVIE) - kodi_item['type'] = v.KODI_TYPE_MOVIE - if kodi_id is None: - kodi_id, _ = kodiid_from_filename(kodi_item['file'], - v.KODI_TYPE_EPISODE) - kodi_item['type'] = v.KODI_TYPE_EPISODE - if kodi_id is None: - kodi_id, _ = kodiid_from_filename(kodi_item['file'], - v.KODI_TYPE_SONG) - kodi_item['type'] = v.KODI_TYPE_SONG + # Try the VIDEO DB first - will find both movies and episodes + kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'], + db_type='video') + if not kodi_id: + # No movie or episode found - try MUSIC DB now for songs + kodi_id, kodi_type = kodiid_from_filename(kodi_item['file'], + db_type='music') kodi_item['id'] = kodi_id - kodi_item['type'] = None if kodi_id is None else kodi_item['type'] + kodi_item['type'] = None if kodi_id is None else kodi_type LOG.debug('Research results for kodi_item: %s', kodi_item) return kodi_item diff --git a/resources/lib/playlists/__init__.py b/resources/lib/playlists/__init__.py index 231b1865..c223303a 100644 --- a/resources/lib/playlists/__init__.py +++ b/resources/lib/playlists/__init__.py @@ -202,6 +202,8 @@ def _full_sync(): return False playlist = db.get_playlist(plex_id=plex_id) LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist) + if playlist is None: + continue try: kodi_pl.delete(playlist) except PlaylistError: diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 597d8c14..afa47fe5 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -289,7 +289,9 @@ class SubscriptionMgr(object): # To avoid RuntimeError, don't use self.lastplayers for playerid in (0, 1, 2): self.last_params['state'] = 'stopped' - self._send_pms_notification(playerid, self.last_params) + self._send_pms_notification(playerid, + self.last_params, + timeout=0.0001) def _plex_stream_index(self, playerid, stream_type): """ @@ -399,7 +401,11 @@ class SubscriptionMgr(object): self.last_params = params return params - def _send_pms_notification(self, playerid, params): + def _send_pms_notification(self, playerid, params, timeout=None): + """ + Pass a really low timeout in seconds if shutting down Kodi and we don't + need the PMS' response + """ serv = self._server_by_host(self.server) playqueue = PQ.PLAYQUEUES[playerid] xargs = params_pms() @@ -416,7 +422,8 @@ class SubscriptionMgr(object): DU().downloadUrl(url, authenticate=False, parameters=xargs, - headerOverride=HEADERS_PMS) + headerOverride=HEADERS_PMS, + timeout=timeout) LOG.debug("Sent server notification with parameters: %s to %s", xargs, url) diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 90e8616d..fa929595 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -451,12 +451,15 @@ class Service(object): self.choose_plex_libraries() elif plex_command == 'RESET-PKC': utils.reset() + elif plex_command == 'EXIT-PKC': + LOG.info('Received command from another instance to quit') + app.APP.stop_pkc = True if task: backgroundthread.BGThreader.addTasksToFront([task]) continue if app.APP.suspend: - app.APP.monitor.waitForAbort(0.1) + xbmc.sleep(100) continue # Before proceeding, need to make sure: @@ -495,7 +498,7 @@ class Service(object): if utils.settings('enable_alexa') == 'true': self.alexa.start() - app.APP.monitor.waitForAbort(0.1) + xbmc.sleep(100) # EXITING PKC # Tell all threads to terminate (e.g. several lib sync threads) @@ -506,26 +509,37 @@ class Service(object): library_sync.clear_window_vars() # Will block until threads have quit app.APP.stop_threads() - utils.window('plex_service_started', clear=True) - LOG.info("======== STOP %s ========", v.ADDON_NAME) def start(): # Safety net - Kody starts PKC twice upon first installation! if utils.window('plex_service_started') == 'true': - EXIT = True - else: - utils.window('plex_service_started', value='true') - EXIT = False - - # Delay option + LOG.info('Another service.py instance is already running - shutting ' + 'it down now') + # Telling the other Python instance of PKC to shut down now + i = 0 + while utils.window('plexkodiconnect.command'): + xbmc.sleep(20) + i += 1 + if i > 300: + LOG.error('Could not tell other PKC instance to shut down') + return + utils.window('plexkodiconnect.command', value='EXIT-PKC') + # Telling successful - now wait for actual shut-down + i = 0 + while utils.window('plex_service_started'): + xbmc.sleep(20) + i += 1 + if i > 300: + LOG.error('Could not shut down other PKC instance') + return + utils.window('plex_service_started', value='true') DELAY = int(utils.settings('startupDelay')) - LOG.info("Delaying Plex startup by: %s sec...", DELAY) - if EXIT: - LOG.error('PKC service.py already started - exiting this instance') - elif DELAY and xbmc.Monitor().waitForAbort(DELAY): + if DELAY and xbmc.Monitor().waitForAbort(DELAY): # Start the service LOG.info("Abort requested while waiting. PKC not started.") else: Service().ServiceEntryPoint() + utils.window('plex_service_started', clear=True) + LOG.info("======== STOP PlexKodiConnect service ========")