diff --git a/README.md b/README.md index 3a8ef6b4..ecbbc262 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![stable version](https://img.shields.io/badge/stable_version-2.6.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.6.5-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.0-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.0-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 38f26907..bd857f9a 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -77,7 +77,35 @@ Нативна інтеграція Plex в Kodi Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик! Використовуйте на свій ризик - version 2.6.5: + version 2.7.0: +- WARNING: You will need to reset the Kodi database if you're using the stable version of PKC! +- Version 2.6.6-9 for everyone +- Choose which Plex libraries get synched to Kodi + +version 2.6.9 (beta only): +- Fix PKC crashing on resetting the database + +version 2.6.8 (beta only): +- Choose which Plex libraries get synched to Kodi +- Fix PKC becoming unresponsive +- Fix rare case where thousands of identical playlists could be generated +- Fix movies or shows disappearing in fringe cases +- Fix processing of collections in special cases +- Implement Codacy suggestions + +version 2.6.7 (beta only): +- Fix "Unauthorized for PMS" e.g. on switching Plex users +- Improve error messages when playback failes + +version 2.6.6 (beta only): +- WARNING: You will need to reset the Kodi database! +- Greatly speed up sync for episodes, especially for large libraries +- Allow websocket redirects. Never allow insecure HTTPs connections for Kodi Leia +- Optimize headers for communication with PMS to appear like a Plex Media Player +- Fix PMS log entries 'Unable to find client profile for device' +- Improve sync dialog + +version 2.6.5: - Fix extras not playing - Hide "Verify SSL certificate" setting for Kodi 18 Krypton - Improve logging diff --git a/changelog.txt b/changelog.txt index 5a9b1cd2..b921b6a8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,31 @@ +version 2.7.0: +- WARNING: You will need to reset the Kodi database if you're using the stable version of PKC! +- Version 2.6.6-9 for everyone +- Choose which Plex libraries get synched to Kodi + +version 2.6.9 (beta only): +- Fix PKC crashing on resetting the database + +version 2.6.8 (beta only): +- Choose which Plex libraries get synched to Kodi +- Fix PKC becoming unresponsive +- Fix rare case where thousands of identical playlists could be generated +- Fix movies or shows disappearing in fringe cases +- Fix processing of collections in special cases +- Implement Codacy suggestions + +version 2.6.7 (beta only): +- Fix "Unauthorized for PMS" e.g. on switching Plex users +- Improve error messages when playback failes + +version 2.6.6 (beta only): +- WARNING: You will need to reset the Kodi database! +- Greatly speed up sync for episodes, especially for large libraries +- Allow websocket redirects. Never allow insecure HTTPs connections for Kodi Leia +- Optimize headers for communication with PMS to appear like a Plex Media Player +- Fix PMS log entries 'Unable to find client profile for device' +- Improve sync dialog + version 2.6.5: - Fix extras not playing - Hide "Verify SSL certificate" setting for Kodi 18 Krypton diff --git a/default.py b/default.py index e4e549f8..82179057 100644 --- a/default.py +++ b/default.py @@ -141,6 +141,10 @@ class Main(): elif mode == 'hub': entrypoint.hub(params.get('type')) + elif mode == 'select-libraries': + LOG.info('User requested to select Plex libraries') + transfer.plex_command('select-libraries') + else: entrypoint.show_main_menu(content_type=params.get('content_type')) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index cea960a1..ba4650ea 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -558,6 +558,12 @@ msgctxt "#30523" msgid "Also show sync progress for playstate and user data" msgstr "" +# PKC Settings - Sync Options +msgctxt "#30524" +msgid "Select Plex libraries to sync" +msgstr "" + + # PKC Settings - Playback msgctxt "#30527" msgid "Ignore specials in next episodes" @@ -1138,9 +1144,9 @@ msgctxt "#39211" msgid "Watch later" msgstr "" -# String attached at the end to get something like "PMS Name is offline" +# Error message pop-up if {0} cannot be contacted. {0} will be replaced by e.g. the PMS' name msgctxt "#39213" -msgid "is offline" +msgid "{0} offline" msgstr "" msgctxt "#39215" diff --git a/resources/lib/app/account.py b/resources/lib/app/account.py index 112df454..3a8ffb7f 100644 --- a/resources/lib/app/account.py +++ b/resources/lib/app/account.py @@ -10,12 +10,22 @@ LOG = getLogger('PLEX.account') class Account(object): def __init__(self, entrypoint=False): + self.plex_login = None + self.plex_login_id = None + self.plex_username = None + self.plex_user_id = None + self.plex_token = None + self.pms_token = None + self.avatar = None + self.myplexlogin = None + self.restricted_user = None + self.force_login = None + self._session = None + self.authenticated = False if entrypoint: self.load_entrypoint() else: - self.authenticated = False utils.window('plex_authenticated', clear=True) - self._session = None self.load() def set_authenticated(self): diff --git a/resources/lib/app/application.py b/resources/lib/app/application.py index c6226f51..1a119cc4 100644 --- a/resources/lib/app/application.py +++ b/resources/lib/app/application.py @@ -1,27 +1,32 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals +from logging import getLogger import Queue from threading import Lock, RLock +import xbmc + from .. import utils +LOG = getLogger('PLEX.app') + class App(object): """ This class is used to store variables across PKC modules """ def __init__(self, entrypoint=False): + self.fetch_pms_item_number = None + self.force_reload_skin = None if entrypoint: self.load_entrypoint() else: self.load() # Quit PKC? self.stop_pkc = False - # Shall we completely suspend PKC and our threads? + # This will suspend the main thread also self.suspend = False - # Shall we only suspend threads? - self._suspend_threads = False # Need to lock all methods and functions messing with Plex Companion subscribers self.lock_subscriber = RLock() # Need to lock everything messing with Kodi/PKC playqueues @@ -38,6 +43,130 @@ class App(object): self.monitor = None # xbmc.Player() instance self.player = None + # All thread instances + self.threads = [] + # Instance of FanartThread() + self.fanart_thread = None + # Instance of ImageCachingThread() + self.caching_thread = None + + @property + def is_playing(self): + return self.player.isPlaying() + + @property + def is_playing_video(self): + return self.player.isPlayingVideo() + + def register_fanart_thread(self, thread): + self.fanart_thread = thread + self.threads.append(thread) + + def deregister_fanart_thread(self, thread): + self.fanart_thread = None + self.threads.remove(thread) + + def suspend_fanart_thread(self, block=True): + try: + self.fanart_thread.suspend(block=block) + except AttributeError: + pass + + def resume_fanart_thread(self): + try: + self.fanart_thread.resume() + except AttributeError: + pass + + def register_caching_thread(self, thread): + self.caching_thread = thread + self.threads.append(thread) + + def deregister_caching_thread(self, thread): + self.caching_thread = None + self.threads.remove(thread) + + def suspend_caching_thread(self, block=True): + try: + self.caching_thread.suspend(block=block) + except AttributeError: + pass + + def resume_caching_thread(self): + try: + self.caching_thread.resume() + except AttributeError: + pass + + def register_thread(self, thread): + """ + Hit with thread [backgroundthread.Killablethread instance] to register + any and all threads + """ + self.threads.append(thread) + + def deregister_thread(self, thread): + """ + Sync thread has done it's work and is e.g. about to die + """ + self.threads.remove(thread) + + def suspend_threads(self, block=True): + """ + Suspend all threads' activity with or without blocking. + Returns True only if PKC shutdown requested + """ + LOG.debug('Suspending threads: %s', self.threads) + for thread in self.threads: + thread.suspend() + if block: + while True: + for thread in self.threads: + if not thread.suspend_reached: + LOG.debug('Waiting for thread to suspend: %s', thread) + # Send suspend signal again in case self.threads + # changed + thread.suspend() + if self.monitor.waitForAbort(0.1): + return True + break + else: + break + return xbmc.abortRequested + + def resume_threads(self, block=True): + """ + Resume all thread activity with or without blocking. + Returns True only if PKC shutdown requested + """ + LOG.debug('Resuming threads: %s', self.threads) + for thread in self.threads: + thread.resume() + if block: + while True: + for thread in self.threads: + if thread.suspend_reached: + LOG.debug('Waiting for thread to resume: %s', thread) + if self.monitor.waitForAbort(0.1): + return True + break + else: + break + return xbmc.abortRequested + + def stop_threads(self, block=True): + """ + Stop all threads. Will block until all threads are stopped + Will NOT quit if PKC should exit! + """ + LOG.debug('Killing threads: %s', self.threads) + for thread in self.threads: + thread.abort() + if block: + while self.threads: + LOG.debug('Waiting for threads to exit: %s', self.threads) + if xbmc.sleep(100): + return True def load(self): # Number of items to fetch and display in widgets @@ -48,11 +177,3 @@ class App(object): def load_entrypoint(self): self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number')) - - @property - def suspend_threads(self): - return self._suspend_threads or self.suspend - - @suspend_threads.setter - def suspend_threads(self, value): - self._suspend_threads = value diff --git a/resources/lib/app/connection.py b/resources/lib/app/connection.py index 2adcfb34..303ff165 100644 --- a/resources/lib/app/connection.py +++ b/resources/lib/app/connection.py @@ -10,13 +10,25 @@ LOG = getLogger('PLEX.connection') class Connection(object): def __init__(self, entrypoint=False): + self.verify_ssl_cert = None + self.ssl_cert_path = None + self.machine_identifier = None + self.server_name = None + self.https = None + self.host = None + self.port = None + self.server = None + self.online = False + self.webserver_host = None + self.webserver_port = None + self.webserver_username = None + self.webserver_password = None + if entrypoint: self.load_entrypoint() else: self.load_webserver() self.load() - # TODO: Delete - self.pms_server = None # Token passed along, e.g. if playback initiated by Plex Companion. Might be # another user playing something! Token identifies user self.plex_transient_token = None @@ -57,7 +69,6 @@ class Connection(object): self.server = 'https://%s:%s' % (self.host, self.port) else: self.server = 'http://%s:%s' % (self.host, self.port) - utils.window('pms_server', value=self.server) self.online = False LOG.debug('Set server %s (%s) to %s', self.server_name, self.machine_identifier, self.server) @@ -81,8 +92,7 @@ class Connection(object): LOG.debug('Clearing connection settings') self.machine_identifier = None self.server_name = None - self.http = None + self.https = None self.host = None self.port = None self.server = None - utils.window('pms_server', clear=True) diff --git a/resources/lib/app/libsync.py b/resources/lib/app/libsync.py index a994dadc..7bb7f82c 100644 --- a/resources/lib/app/libsync.py +++ b/resources/lib/app/libsync.py @@ -19,34 +19,71 @@ def remove_trailing_slash(path): class Sync(object): def __init__(self, entrypoint=False): - self.load() + # Direct Paths (True) or Addon Paths (False)? + self.direct_paths = None + # Is synching of Plex music enabled? + self.enable_music = None + # Do we sync artwork from the PMS to Kodi? + self.artwork = None + # Path remapping mechanism (e.g. smb paths) + # Do we replace \\myserver\path to smb://myserver/path? + self.replace_smb_path = None + # Do we generally remap? + self.remap_path = None + self.force_transcode_pix = None + # Mappings for REMAP_PATH: + self.remapSMBmovieOrg = None + self.remapSMBmovieNew = None + self.remapSMBtvOrg = None + self.remapSMBtvNew = None + self.remapSMBmusicOrg = None + self.remapSMBmusicNew = None + self.remapSMBphotoOrg = None + self.remapSMBphotoNew = None + # Escape path? + self.escape_path = None + # Shall we replace custom user ratings with the number of versions available? + self.indicate_media_versions = None + # Will sync movie trailer differently: either play trailer directly or show + # all the Plex extras for the user to choose + self.show_extras_instead_of_playing_trailer = None + # Only sync specific Plex playlists to Kodi? + self.sync_specific_plex_playlists = None + # Only sync specific Kodi playlists to Plex? + self.sync_specific_kodi_playlists = None + # Shall we show Kodi dialogs when synching? + self.sync_dialog = None + + # How often shall we sync? + self.full_sync_intervall = None + # Background Sync disabled? + self.background_sync_disabled = None + # How long shall we wait with synching a new item to make sure Plex got all + # metadata? + self.backgroundsync_saftymargin = None + # How many threads to download Plex metadata on sync? + self.sync_thread_number = None + + # Shall Kodi show dialogs for syncing/caching images? (e.g. images left + # to sync) + self.image_sync_notifications = None + # Do we need to run a special library scan? self.run_lib_scan = None # Set if user decided to cancel sync self.stop_sync = False - # Set during media playback if PKC should not do any syncs. Will NOT - # suspend synching of playstate progress - self.suspend_sync = False # Could we access the paths? self.path_verified = False - # Set if a Plex-Kodi DB sync is being done - along with - # window('plex_dbScan') set to 'true' - self.db_scan = False + + self.load() def load(self): - # Direct Paths (True) or Addon Paths (False)? self.direct_paths = utils.settings('useDirectPaths') == '1' - # Is synching of Plex music enabled? self.enable_music = utils.settings('enableMusic') == 'true' - # Do we sync artwork from the PMS to Kodi? self.artwork = utils.settings('usePlexArtwork') == 'true' - # Path remapping mechanism (e.g. smb paths) - # Do we replace \\myserver\path to smb://myserver/path? self.replace_smb_path = utils.settings('replaceSMB') == 'true' - # Do we generally remap? self.remap_path = utils.settings('remapSMB') == 'true' self.force_transcode_pix = utils.settings('force_transcode_pix') == 'true' - # Mappings for REMAP_PATH: self.remapSMBmovieOrg = remove_trailing_slash(utils.settings('remapSMBmovieOrg')) self.remapSMBmovieNew = remove_trailing_slash(utils.settings('remapSMBmovieNew')) self.remapSMBtvOrg = remove_trailing_slash(utils.settings('remapSMBtvOrg')) @@ -55,30 +92,16 @@ class Sync(object): self.remapSMBmusicNew = remove_trailing_slash(utils.settings('remapSMBmusicNew')) self.remapSMBphotoOrg = remove_trailing_slash(utils.settings('remapSMBphotoOrg')) self.remapSMBphotoNew = remove_trailing_slash(utils.settings('remapSMBphotoNew')) - # Escape path? self.escape_path = utils.settings('escapePath') == 'true' - # Shall we replace custom user ratings with the number of versions available? self.indicate_media_versions = utils.settings('indicate_media_versions') == "true" - # Will sync movie trailer differently: either play trailer directly or show - # all the Plex extras for the user to choose self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true' - # Only sync specific Plex playlists to Kodi? self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true' - # Only sync specific Kodi playlists to Plex? self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true' - # Shall we show Kodi dialogs when synching? self.sync_dialog = utils.settings('dbSyncIndicator') == 'true' - # How often shall we sync? self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60 - # Background Sync disabled? self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false' - # How long shall we wait with synching a new item to make sure Plex got all - # metadata? self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin')) - # How many threads to download Plex metadata on sync? self.sync_thread_number = int(utils.settings('syncThreadNumber')) - # Shall Kodi show dialogs for syncing/caching images? (e.g. images left - # to sync) self.image_sync_notifications = utils.settings('imageSyncNotifications') == 'true' diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 555cb70c..d8a394b9 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -18,8 +18,6 @@ requests.packages.urllib3.disable_warnings() TIMEOUT = (35.1, 35.1) BATCH_SIZE = 500 -IMAGE_CACHING_SUSPENDS = [] - def double_urlencode(text): return quote_plus(quote_plus(text)) @@ -30,10 +28,17 @@ def double_urldecode(text): class ImageCachingThread(backgroundthread.KillableThread): - def isSuspended(self): - return any(IMAGE_CACHING_SUSPENDS) + def __init__(self): + super(ImageCachingThread, self).__init__() + self.suspend_points = [(self, '_suspended')] + if not utils.settings('imageSyncDuringPlayback') == 'true': + self.suspend_points.append((app.APP, 'is_playing_video')) - def _url_generator(self, kind, kodi_type): + def isSuspended(self): + return any(getattr(obj, txt) for obj, txt in self.suspend_points) + + @staticmethod + def _url_generator(kind, kodi_type): """ Main goal is to close DB connection between calls """ @@ -60,11 +65,13 @@ class ImageCachingThread(backgroundthread.KillableThread): def run(self): LOG.info("---===### Starting ImageCachingThread ###===---") + app.APP.register_caching_thread(self) try: self._run() except Exception: utils.ERROR() finally: + app.APP.deregister_caching_thread(self) LOG.info("---===### Stopped ImageCachingThread ###===---") def _run(self): @@ -74,14 +81,8 @@ class ImageCachingThread(backgroundthread.KillableThread): for kind in kinds: for kodi_type in ('poster', 'fanart'): for url in self._url_generator(kind, kodi_type): - if self.isCanceled(): + if self.wait_while_suspended(): return - while self.isSuspended(): - # Set in service.py - if self.isCanceled(): - # Abort was requested while waiting. We should exit - return - app.APP.monitor.waitForAbort(1) cache_url(url) # Toggles Image caching completed to Yes utils.settings('plex_status_image_caching', value=utils.lang(107)) diff --git a/resources/lib/backgroundthread.py b/resources/lib/backgroundthread.py index 7e9ac465..80f099a0 100644 --- a/resources/lib/backgroundthread.py +++ b/resources/lib/backgroundthread.py @@ -77,7 +77,10 @@ class KillableThread(threading.Thread): def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): self._canceled = False + # Set to True to set the thread to suspended self._suspended = False + # Thread will return True only if suspended state is reached + self.suspend_reached = False super(KillableThread, self).__init__(group, target, name, args, kwargs) def isCanceled(self): @@ -94,11 +97,16 @@ class KillableThread(threading.Thread): """ self._canceled = True - def suspend(self): + def suspend(self, block=False): """ Call to suspend this thread """ self._suspended = True + if block: + while not self.suspend_reached: + LOG.debug('Waiting for thread to suspend: %s', self) + if app.APP.monitor.waitForAbort(0.1): + return def resume(self): """ @@ -106,6 +114,25 @@ class KillableThread(threading.Thread): """ self._suspended = False + def wait_while_suspended(self): + """ + Blocks until thread is not suspended anymore or the thread should + exit. + Returns True only if the thread should exit (=isCanceled()) + """ + while self.isSuspended(): + try: + self.suspend_reached = True + # Set in service.py + if self.isCanceled(): + # Abort was requested while waiting. We should exit + return True + if app.APP.monitor.waitForAbort(0.1): + return True + finally: + self.suspend_reached = False + return self.isCanceled() + def isSuspended(self): """ Returns True if the thread is suspended @@ -198,7 +225,8 @@ class BackgroundWorker(object): self._abort = False self._task = None - def _runTask(self, task): + @staticmethod + def _runTask(task): if task._canceled: return try: diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index d576ed44..54378598 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -32,12 +32,11 @@ def getXArgsDeviceInfo(options=None, include_token=True): "Content-Type": "application/x-www-form-urlencoded", # "Access-Control-Allow-Origin": "*", # 'X-Plex-Language': 'en', - 'X-Plex-Device': v.ADDON_NAME, - 'X-Plex-Client-Platform': v.PLATFORM, + 'X-Plex-Device': v.DEVICE, + 'X-Plex-Model': v.MODEL, 'X-Plex-Device-Name': v.DEVICENAME, 'X-Plex-Platform': v.PLATFORM, - # 'X-Plex-Platform-Version': 'unknown', - # 'X-Plex-Model': 'unknown', + 'X-Plex-Platform-Version': v.PLATFORM_VERSION, 'X-Plex-Product': v.ADDON_NAME, 'X-Plex-Version': v.ADDON_VERSION, 'X-Plex-Client-Identifier': getDeviceId(), diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index c419f8b1..21384bf2 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -167,6 +167,7 @@ class DownloadUtils(): kwargs['timeout'] = timeout # ACTUAL DOWNLOAD HAPPENING HERE + success = False try: r = self._doDownload(s, action_type, **kwargs) @@ -176,44 +177,37 @@ class DownloadUtils(): LOG.warn(e) if reraise: raise - except exceptions.ConnectionError as e: # Connection error LOG.warn("Server unreachable at: %s", url) LOG.warn(e) if reraise: raise - except exceptions.Timeout as e: LOG.warn("Server timeout at: %s", url) LOG.warn(e) if reraise: raise - except exceptions.HTTPError as e: LOG.warn('HTTP Error at %s', url) LOG.warn(e) if reraise: raise - except exceptions.TooManyRedirects as e: LOG.warn("Too many redirects connecting to: %s", url) LOG.warn(e) if reraise: raise - except exceptions.RequestException as e: LOG.warn("Unknown error connecting to: %s", url) LOG.warn(e) if reraise: raise - except SystemExit: LOG.info('SystemExit detected, aborting download') self.stopSession() if reraise: raise - except Exception: LOG.warn('Unknown error while downloading. Traceback:') import traceback @@ -223,6 +217,7 @@ class DownloadUtils(): # THE RESPONSE ##### else: + success = True # We COULD contact the PMS, hence it ain't dead if authenticate is True: self.count_error = 0 @@ -300,12 +295,12 @@ class DownloadUtils(): url, r.status_code) return True - # And now deal with the consequences of the exceptions - if authenticate is True: - # Make the addon aware of status - self.count_error += 1 - if self.count_error >= self.connection_attempts: - LOG.warn('Failed to connect to %s too many times. ' - 'Declare PMS dead', url) - app.CONN.online = False - return + finally: + if not success and authenticate: + # Deal with the consequences of the exceptions + # Make the addon aware of status + self.count_error += 1 + if self.count_error >= self.connection_attempts: + LOG.warn('Failed to connect to %s too many times. ' + 'Declare PMS dead', url) + app.CONN.online = False diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 16cea0cf..884832c1 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -69,7 +69,8 @@ class InitialSetup(object): utils.settings('plex_allows_mediaDeletion', value=value) utils.window('plex_allows_mediaDeletion', value=value) - def enter_new_pms_address(self): + @staticmethod + def enter_new_pms_address(): LOG.info('Start getting manual PMS address and port') # "Enter your Plex Media Server's IP or URL. Examples are:" utils.messageDialog(utils.lang(29999), @@ -196,12 +197,12 @@ class InitialSetup(object): LOG.error('Failed to update Plex info from plex.tv') else: utils.settings('plexLogin', value=self.plex_login) - home = 'true' if xml.attrib.get('home') == '1' else 'false' utils.settings('plexAvatar', value=xml.attrib.get('thumb')) LOG.info('Updated Plex info from plex.tv') return answer - def check_existing_pms(self): + @staticmethod + def check_existing_pms(): """ Check the PMS that was set in file settings. Will return False if we need to reconnect, because: diff --git a/resources/lib/itemtypes/common.py b/resources/lib/itemtypes/common.py index c8c47877..4cf0c7c7 100644 --- a/resources/lib/itemtypes/common.py +++ b/resources/lib/itemtypes/common.py @@ -110,7 +110,7 @@ class ItemBase(object): kodi_type) def update_playstate(self, mark_played, view_count, resume, duration, - kodi_fileid, lastViewedAt, plex_type): + kodi_fileid, kodi_fileid_2, lastViewedAt): """ Use with websockets, not xml """ @@ -128,5 +128,11 @@ class ItemBase(object): resume, duration, view_count, - timing.plex_date_to_kodi(lastViewedAt), - plex_type) + timing.plex_date_to_kodi(lastViewedAt)) + if kodi_fileid_2: + # Our dirty hack for episodes + self.kodidb.set_resume(kodi_fileid_2, + resume, + duration, + view_count, + timing.plex_date_to_kodi(lastViewedAt)) diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index faa30e9d..6a388d26 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -189,7 +189,7 @@ class Movie(ItemBase): # e.g. when added via websocket LOG.debug('Costly looking up Plex collection %s: %s', plex_set_id, set_name) - for index, coll_plex_id in api.collections_match(): + for index, coll_plex_id in api.collections_match(section_id): # Get Plex artwork for collections - a pain if index == plex_set_id: set_xml = PF.GetPlexMetadata(coll_plex_id) @@ -214,8 +214,7 @@ class Movie(ItemBase): resume, runtime, playcount, - dateplayed, - v.PLEX_TYPE_MOVIE) + dateplayed) self.plexdb.add_movie(plex_id=plex_id, checksum=api.checksum(), section_id=section_id, @@ -279,8 +278,7 @@ class Movie(ItemBase): userdata['Resume'], userdata['Runtime'], userdata['PlayCount'], - userdata['LastPlayedDate'], - plex_type) + userdata['LastPlayedDate']) self.kodidb.update_userrating(db_item['kodi_id'], db_item['kodi_type'], userdata['UserRating']) diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index f8fd5759..2fd7acd6 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -32,8 +32,13 @@ class TvShowMixin(object): userdata['Resume'], userdata['Runtime'], userdata['PlayCount'], - userdata['LastPlayedDate'], - plex_type) + userdata['LastPlayedDate']) + if db_item['kodi_fileid_2']: + self.kodidb.set_resume(db_item['kodi_fileid_2'], + userdata['Resume'], + userdata['Runtime'], + userdata['PlayCount'], + userdata['LastPlayedDate']) return True def remove(self, plex_id, plex_type=None): @@ -54,7 +59,7 @@ class TvShowMixin(object): # EPISODE ##### if db_item['plex_type'] == v.PLEX_TYPE_EPISODE: # Delete episode, verify season and tvshow - self.remove_episode(db_item['kodi_id'], db_item['kodi_fileid']) + self.remove_episode(db_item) # Season verification if (db_item['season_id'] and not self.plexdb.season_has_episodes(db_item['season_id'])): @@ -72,7 +77,7 @@ class TvShowMixin(object): # Remove episodes, season, verify tvshow episodes = list(self.plexdb.episode_by_season(db_item['plex_id'])) for episode in episodes: - self.remove_episode(episode['kodi_id'], episode['kodi_fileid']) + self.remove_episode(episode) self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE) # Remove season self.remove_season(db_item['kodi_id']) @@ -91,8 +96,7 @@ class TvShowMixin(object): self.plexdb.remove(season['plex_id'], v.PLEX_TYPE_SEASON) episodes = list(self.plexdb.episode_by_show(db_item['plex_id'])) for episode in episodes: - self.remove_episode(episode['kodi_id'], - episode['kodi_fileid']) + self.remove_episode(episode) self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE) self.remove_show(db_item['kodi_id']) @@ -120,17 +124,19 @@ class TvShowMixin(object): self.kodidb.remove_season(kodi_id) LOG.debug("Removed season: %s", kodi_id) - def remove_episode(self, kodi_id, file_id): + def remove_episode(self, db_item): """ Remove an episode, and episode only from the Kodi DB (not Plex DB) """ - self.kodidb.modify_people(kodi_id, v.KODI_TYPE_EPISODE) - self.kodidb.remove_file(file_id, plex_type=v.PLEX_TYPE_EPISODE) - self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_EPISODE) - self.kodidb.remove_episode(kodi_id) - self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE) - self.kodidb.remove_ratings(kodi_id, v.KODI_TYPE_EPISODE) - LOG.debug("Removed episode: %s", kodi_id) + self.kodidb.modify_people(db_item['kodi_id'], v.KODI_TYPE_EPISODE) + self.kodidb.remove_file(db_item['kodi_fileid']) + if db_item['kodi_fileid_2']: + self.kodidb.remove_file(db_item['kodi_fileid_2']) + self.kodidb.delete_artwork(db_item['kodi_id'], v.KODI_TYPE_EPISODE) + self.kodidb.remove_episode(db_item['kodi_id']) + self.kodidb.remove_uniqueid(db_item['kodi_id'], v.KODI_TYPE_EPISODE) + self.kodidb.remove_ratings(db_item['kodi_id'], v.KODI_TYPE_EPISODE) + LOG.debug("Removed episode: %s", db_item['kodi_id']) class Show(TvShowMixin, ItemBase): @@ -367,6 +373,7 @@ class Episode(TvShowMixin, ItemBase): update_item = True kodi_id = episode['kodi_id'] old_kodi_fileid = episode['kodi_fileid'] + old_kodi_fileid_2 = episode['kodi_fileid_2'] kodi_pathid = episode['kodi_pathid'] peoples = api.people() @@ -452,6 +459,15 @@ class Episode(TvShowMixin, ItemBase): playurl = filename # Root path tvshows/ already saved in Kodi DB kodi_pathid = self.kodidb.add_path(path) + if not app.SYNC.direct_paths: + # need to set a 2nd file entry for a path without plex show id + # This fixes e.g. context menu and widgets working as they + # should + # A dirty hack, really + path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID + # filename_2 is exactly the same as filename + # so WITH plex show id! + kodi_pathid_2 = self.kodidb.add_path(path_2) # UPDATE THE EPISODE ##### if update_item: @@ -459,9 +475,17 @@ class Episode(TvShowMixin, ItemBase): kodi_fileid = self.kodidb.modify_file(filename, kodi_pathid, api.date_created()) + if not app.SYNC.direct_paths: + kodi_fileid_2 = self.kodidb.modify_file(filename, + kodi_pathid_2, + api.date_created()) + else: + kodi_fileid_2 = None if kodi_fileid != old_kodi_fileid: self.kodidb.remove_file(old_kodi_fileid) + if not app.SYNC.direct_paths: + self.kodidb.remove_file(old_kodi_fileid_2) ratingid = self.kodidb.get_ratingid(kodi_id, v.KODI_TYPE_EPISODE) self.kodidb.update_ratings(kodi_id, @@ -502,7 +526,7 @@ class Episode(TvShowMixin, ItemBase): airs_before_episode, playurl, kodi_pathid, - kodi_fileid, + kodi_fileid, # and NOT kodi_fileid_2 parent_id, userdata['UserRating'], kodi_id) @@ -510,8 +534,13 @@ class Episode(TvShowMixin, ItemBase): api.resume_point(), api.runtime(), userdata['PlayCount'], - userdata['LastPlayedDate'], - v.PLEX_TYPE_EPISODE) + userdata['LastPlayedDate']) + if not app.SYNC.direct_paths: + self.kodidb.set_resume(kodi_fileid_2, + api.resume_point(), + api.runtime(), + userdata['PlayCount'], + userdata['LastPlayedDate']) self.plexdb.add_episode(plex_id=plex_id, checksum=api.checksum(), section_id=section_id, @@ -521,6 +550,7 @@ class Episode(TvShowMixin, ItemBase): parent_id=parent_id, kodi_id=kodi_id, kodi_fileid=kodi_fileid, + kodi_fileid_2=kodi_fileid_2, kodi_pathid=kodi_pathid, last_sync=self.last_sync) # OR ADD THE EPISODE ##### @@ -529,6 +559,12 @@ class Episode(TvShowMixin, ItemBase): kodi_fileid = self.kodidb.add_file(filename, kodi_pathid, api.date_created()) + if not app.SYNC.direct_paths: + kodi_fileid_2 = self.kodidb.add_file(filename, + kodi_pathid_2, + api.date_created()) + else: + kodi_fileid_2 = None rating_id = self.kodidb.add_ratingid() self.kodidb.add_ratings(rating_id, @@ -552,7 +588,7 @@ class Episode(TvShowMixin, ItemBase): kodi_id, v.KODI_TYPE_EPISODE) self.kodidb.add_episode(kodi_id, - kodi_fileid, + kodi_fileid, # and NOT kodi_fileid_2 api.title(), api.plot(), rating_id, @@ -574,8 +610,13 @@ class Episode(TvShowMixin, ItemBase): api.resume_point(), api.runtime(), userdata['PlayCount'], - userdata['LastPlayedDate'], - None) # Do send None to avoid episode loop + userdata['LastPlayedDate']) + if not app.SYNC.direct_paths: + self.kodidb.set_resume(kodi_fileid_2, + api.resume_point(), + api.runtime(), + userdata['PlayCount'], + userdata['LastPlayedDate']) self.plexdb.add_episode(plex_id=plex_id, checksum=api.checksum(), section_id=section_id, @@ -585,26 +626,10 @@ class Episode(TvShowMixin, ItemBase): parent_id=parent_id, kodi_id=kodi_id, kodi_fileid=kodi_fileid, + kodi_fileid_2=kodi_fileid_2, kodi_pathid=kodi_pathid, last_sync=self.last_sync) - if not app.SYNC.direct_paths: - # need to set a SECOND file entry for a path without plex show id - filename = api.file_name(force_first_media=True) - path = 'plugin://%s.tvshows/' % v.ADDON_ID - # Filename is exactly the same, WITH plex show id! - filename = ('%s%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s' - % (path, show_id, plex_id, v.PLEX_TYPE_EPISODE, - filename)) - kodi_pathid = self.kodidb.add_path(path) - second_kodi_fileid = self.kodidb.add_file(filename, - kodi_pathid, - api.date_created()) - self.kodidb.set_resume(second_kodi_fileid, - api.resume_point(), - api.runtime(), - userdata['PlayCount'], - userdata['LastPlayedDate'], - None) # Do send None - 2nd entry - self.kodidb.modify_streams(kodi_fileid, + + self.kodidb.modify_streams(kodi_fileid, # and NOT kodi_fileid_2 api.mediastreams(), api.runtime()) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 5eaf3eea..98fd92fb 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -9,6 +9,9 @@ from .. import path_ops, timing, variables as v, app LOG = getLogger('PLEX.kodi_db.video') +MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID +SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID + class KodiVideoDB(common.KodiDBBase): db_kind = 'video' @@ -23,7 +26,7 @@ class KodiVideoDB(common.KodiDBBase): For some reason, Kodi ignores this if done via itemtypes while e.g. adding or updating items. (addPath method does NOT work) """ - path_id = self.get_path('plugin://%s.movies/' % v.ADDON_ID) + path_id = self.get_path(MOVIE_PATH) if path_id is None: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") path_id = self.cursor.fetchone()[0] + 1 @@ -37,13 +40,13 @@ class KodiVideoDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (path_id, - 'plugin://%s.movies/' % v.ADDON_ID, + MOVIE_PATH, 'movies', 'metadata.local', 1, 0)) # And TV shows - path_id = self.get_path('plugin://%s.tvshows/' % v.ADDON_ID) + path_id = self.get_path(SHOW_PATH) if path_id is None: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") path_id = self.cursor.fetchone()[0] + 1 @@ -57,7 +60,7 @@ class KodiVideoDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (path_id, - 'plugin://%s.tvshows/' % v.ADDON_ID, + SHOW_PATH, 'tvshows', 'metadata.local', 1, @@ -199,29 +202,13 @@ class KodiVideoDB(common.KodiDBBase): pass @common.catch_operationalerrors - def remove_file(self, file_id, remove_orphans=True, plex_type=None): + def remove_file(self, file_id, remove_orphans=True): """ Removes the entry for file_id from the files table. Will also delete entries from the associated tables: bookmark, settings, streamdetails. If remove_orphans is true, this method will delete any orphaned path entries in the Kodi path table - - Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for - add-on paths """ - if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE: - # Hack for the 2 entries for episodes for addon paths - self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1', - (file_id, )) - filename = self.cursor.fetchone() - if not filename: - LOG.error('Could not find file_id %s', file_id) - return - for new_id in self.cursor.execute('SELECT idFile FROM files WHERE strFilename = ? LIMIT 2', - (filename[0], )): - self.remove_file(new_id[0], remove_orphans=remove_orphans) - return - self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1', (file_id,)) try: @@ -243,8 +230,12 @@ class KodiVideoDB(common.KodiDBBase): self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? LIMIT 1', (path_id,)) if self.cursor.fetchone() is None: - self.cursor.execute('DELETE FROM path WHERE idPath = ?', - (path_id,)) + # Make sure we're not deleting our root paths! + query = ''' + DELETE FROM path + WHERE idPath = ? AND strPath NOT IN (?, ?) + ''' + self.cursor.execute(query, (path_id, MOVIE_PATH, SHOW_PATH)) @common.catch_operationalerrors def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, @@ -612,31 +603,11 @@ class KodiVideoDB(common.KodiDBBase): @common.catch_operationalerrors def set_resume(self, file_id, resume_seconds, total_seconds, playcount, - dateplayed, plex_type): + dateplayed): """ Adds a resume marker for a video library item. Will even set 2, considering add-on path widget hacks. """ - if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE: - # Need to make sure to set a SECOND bookmark entry for another, - # second file_id that points to the path .tvshows instead of - # .tvshows/" + utils.dialog('notification', + heading='{plex}', + message='%s %s' % (utils.lang(30052), section['section_name']), + icon='{plex}', + sound=False) + if section['plex_type'] == v.PLEX_TYPE_PHOTO: + # not synced - just remove the link in our Plex sections table + pass + else: + if not _delete_kodi_db_items(section['section_id'], section['plex_type']): + return + # Only remove Plex entry if we've removed all items first with PlexDB() as plexdb: - old_sections = [plexdb.section(x) for x in old_sections] - LOG.info("Removing entire Plex library sections: %s", old_sections) - for section in old_sections: - # "Deleting " - utils.dialog('notification', - heading='{plex}', - message='%s %s' % (utils.lang(30052), section[1]), - icon='{plex}', - sound=False) - if section[2] == v.PLEX_TYPE_PHOTO: - # not synced - just remove the link in our Plex sections table - pass + plexdb.remove_section(section['section_id']) + + +def choose_libraries(): + """ + Displays a dialog for the user to select the libraries he wants synched + + Returns True if this was successful, False if not + """ + # Re-set value in order to make sure we got the lastest user input + app.SYNC.enable_music = utils.settings('enableMusic') == 'true' + import xbmcgui + sections = [] + preselect = [] + index = 0 + for section in SECTIONS: + if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST: + LOG.info('Ignoring music section: %s', section) + continue + elif section['plex_type'] == v.PLEX_TYPE_PHOTO: + continue + else: + sections.append(section['section_name']) + if section['sync_to_kodi']: + preselect.append(index) + index += 1 + # "Select Plex libraries to sync" + selected = xbmcgui.Dialog().multiselect(utils.lang(30524), + sections, + preselect=preselect, + useDetails=False) + if selected is None: + # User canceled + return False + index = 0 + with PlexDB() as plexdb: + for section in SECTIONS: + if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST: + continue + elif section['plex_type'] == v.PLEX_TYPE_PHOTO: + continue else: - if not _delete_kodi_db_items(section[0], section[2]): - return - # Only remove Plex entry if we've removed all items first - with PlexDB() as plexdb: - plexdb.remove_section(section[0]) - finally: - common.update_kodi_library() + sync = True if index in selected else False + plexdb.update_section_sync(section['section_id'], sync) + index += 1 + sections = list(plexdb.all_sections()) + LOG.info('Plex libraries to sync: %s', sections) + utils.settings('sections_asked_for_machine_identifier', + value=app.CONN.machine_identifier) + return True diff --git a/resources/lib/library_sync/videonodes.py b/resources/lib/library_sync/videonodes.py index 54c1e51f..92ea29f8 100644 --- a/resources/lib/library_sync/videonodes.py +++ b/resources/lib/library_sync/videonodes.py @@ -476,7 +476,7 @@ class VideoNodes(object): "unwatched.content","unwatched.path", "recent.title","recent.content","recent.path", "recentepisodes.title","recentepisodes.content", - "recentepisodes.path","inprogressepisodes.title", + "recentepisodes.path", "inprogressepisodes.title", "inprogressepisodes.content","inprogressepisodes.path" ] diff --git a/resources/lib/library_sync/websocket.py b/resources/lib/library_sync/websocket.py index 63b72ebe..c74ac6db 100644 --- a/resources/lib/library_sync/websocket.py +++ b/resources/lib/library_sync/websocket.py @@ -23,10 +23,6 @@ WEBSOCKET_MESSAGES = [] PLAYSTATE_SESSIONS = {} -def interrupt_processing(): - return app.APP.stop_pkc or app.APP.suspend_threads or app.SYNC.stop_sync - - def multi_delete(input_list, delete_list): """ Deletes the list items of input_list at the positions in delete_list @@ -81,9 +77,6 @@ def process_websocket_messages(): update_kodi_video_library, update_kodi_music_library = False, False delete_list = [] for i, message in enumerate(WEBSOCKET_MESSAGES): - if interrupt_processing(): - # Chances are that Kodi gets shut down - break if message['state'] == 9: successful, video, music = process_delete_message(message) elif now - message['timestamp'] < app.SYNC.backgroundsync_saftymargin: @@ -285,10 +278,14 @@ def process_playing(data): PLAYSTATE_SESSIONS) # Attach Kodi info to the session try: - PLAYSTATE_SESSIONS[session_key]['file_id'] = typus['kodi_fileid'] + PLAYSTATE_SESSIONS[session_key]['kodi_fileid'] = typus['kodi_fileid'] except KeyError: # media type without file - no need to do anything continue + if typus['plex_type'] == v.PLEX_TYPE_EPISODE: + PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = typus['kodi_fileid_2'] + else: + PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = None PLAYSTATE_SESSIONS[session_key]['kodi_id'] = typus['kodi_id'] PLAYSTATE_SESSIONS[session_key]['kodi_type'] = typus['kodi_type'] session = PLAYSTATE_SESSIONS[session_key] @@ -357,9 +354,9 @@ def process_playing(data): session['viewCount'], resume, session['duration'], - session['file_id'], - timing.unix_timestamp(), - v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']]) + session['kodi_fileid'], + session['kodi_fileid_2'], + timing.unix_timestamp()) def cache_artwork(plex_id, plex_type, kodi_id=None, kodi_type=None): diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 0c2f5582..66c396a0 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -11,7 +11,6 @@ from .plex_api import API from .plex_db import PlexDB from . import plex_functions as PF from . import utils -from .downloadutils import DownloadUtils as DU from .kodi_db import KodiVideoDB from . import playlist_func as PL from . import playqueue as PQ @@ -50,10 +49,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): global RESOLVE # If started via Kodi context menu, we never resolve RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False - if not app.ACCOUNT.authenticated: - LOG.error('Not yet authenticated for PMS, abort starting playback') - # "Unauthorized for PMS" - utils.dialog('notification', utils.lang(29999), utils.lang(30017)) + if not app.CONN.online or not app.ACCOUNT.authenticated: + if not app.CONN.online: + LOG.error('PMS not online for playback') + # "{0} offline" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39213).format(app.CONN.server_name), + icon='{plex}') + else: + LOG.error('Not yet authenticated for PMS, abort starting playback') + # "Unauthorized for PMS" + utils.dialog('notification', utils.lang(29999), utils.lang(30017)) _ensure_resolve(abort=True) return with app.APP.lock_playqueues: @@ -135,16 +142,8 @@ def _playlist_playback(plex_id, plex_type): for the next item in line :-) (by the way: trying to get active Kodi player id will return []) """ - xml = PF.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" - utils.dialog('notification', - utils.lang(29999), - utils.lang(30128), - icon='{error}') + xml = PF.GetPlexMetadata(plex_id, reraise=True) + if xml in (None, 401): _ensure_resolve(abort=True) return # Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback @@ -164,16 +163,9 @@ def _playback_init(plex_id, plex_type, playqueue, pos): Playback setup if Kodi starts playing an item for the first time. """ LOG.info('Initializing PKC playback') - xml = PF.GetPlexMetadata(plex_id) - try: - xml[0].attrib - except (IndexError, TypeError, AttributeError): + xml = PF.GetPlexMetadata(plex_id, reraise=True) + if xml in (None, 401): LOG.error('Could not get a PMS xml for plex id %s', plex_id) - # "Play error" - utils.dialog('notification', - utils.lang(29999), - utils.lang(30128), - icon='{error}') _ensure_resolve(abort=True) return if playqueue.kodi_pl.size() > 1: @@ -182,6 +174,11 @@ def _playback_init(plex_id, plex_type, playqueue, pos): _init_existing_kodi_playlist(playqueue, pos) except PL.PlaylistError: LOG.error('Playback_init for existing Kodi playlist failed') + # "Play error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') _ensure_resolve(abort=True) return # Now we need to use setResolvedUrl for the item at position ZERO @@ -259,12 +256,10 @@ def _ensure_resolve(abort=False): will be destroyed. """ if RESOLVE: - if not abort: - # Releases the other Python thread without a ListItem - transfer.send(True) - else: - # Shows PKC error message - transfer.send(None) + # Releases the other Python thread without a ListItem + transfer.send(True) + # Shows PKC error message + # transfer.send(None) if abort: # Reset some playback variables app.PLAYSTATE.context_menu_play = False @@ -418,7 +413,10 @@ def _conclude_playback(playqueue, pos): LOG.info('Resuming playback at %s', item.offset) if v.KODIVERSION >= 18 and api: # Kodi 18 Alpha 3 broke StartOffset - percent = item.offset / api.runtime() * 100.0 + try: + percent = item.offset / api.runtime() * 100.0 + except ZeroDivisionError: + percent = 0.0 LOG.debug('Resuming at %s percent', percent) listitem.setProperty('StartPercent', str(percent)) else: @@ -446,21 +444,20 @@ def process_indirect(key, offset, resolve=True): key, offset, resolve) global RESOLVE RESOLVE = resolve + offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None if key.startswith('http') or key.startswith('{server}'): - xml = DU().downloadUrl(key) + xml = PF.get_playback_xml(key, app.CONN.server_name) elif key.startswith('/system/services'): - xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key) + xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key, + 'plexapp.com', + authenticate=False, + token=app.ACCOUNT.plex_token) else: - xml = DU().downloadUrl('{server}%s' % key) - try: - xml[0].attrib - except (TypeError, IndexError, AttributeError): - LOG.error('Could not download PMS metadata') + xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name) + if xml is None: _ensure_resolve(abort=True) return - if offset != '0': - offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) - # Todo: implement offset + api = API(xml[0]) listitem = transfer.PKCListItem() api.create_listitem(listitem) @@ -469,19 +466,31 @@ def process_indirect(key, offset, resolve=True): playqueue.clear() item = PL.Playlist_Item() item.xml = xml[0] - item.offset = int(offset) + 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 - xml = DU().downloadUrl('http://node.plexapp.com:32400%s' - % xml[0][0][0].attrib['key']) try: - xml[0].attrib + xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' + % xml[0][0][0].attrib['key'], + 'plexapp.com', + authenticate=False, + token=app.ACCOUNT.plex_token) except (TypeError, IndexError, AttributeError): - LOG.error('Could not download last xml for playurl') + LOG.error('XML malformed: %s', xml.attrib) + xml = None + if xml is None: _ensure_resolve(abort=True) return - playurl = xml[0].attrib['key'] + + try: + playurl = xml[0].attrib['key'] + except (TypeError, IndexError, AttributeError): + LOG.error('Last xml malformed: %s', xml.attrib) + _ensure_resolve(abort=True) + return + item.file = playurl listitem.setPath(utils.try_encode(playurl)) playqueue.items.append(item) @@ -533,7 +542,7 @@ def threaded_playback(kodi_playlist, startpos, offset): app.APP.player.play(kodi_playlist, None, False, startpos) if offset and offset != '0': i = 0 - while not app.APP.player.isPlaying(): + while not app.APP.is_playing: app.APP.monitor.waitForAbort(0.1) i += 1 if i > 100: diff --git a/resources/lib/playlists/__init__.py b/resources/lib/playlists/__init__.py index 59f650f6..9d35125c 100644 --- a/resources/lib/playlists/__init__.py +++ b/resources/lib/playlists/__init__.py @@ -43,7 +43,7 @@ IGNORE_PLEX_PLAYLIST_CHANGE = list() def isCanceled(): - return app.APP.stop_pkc or app.SYNC.stop_sync or app.APP.suspend_threads + return app.APP.stop_pkc or app.SYNC.stop_sync def kodi_playlist_monitor(): diff --git a/resources/lib/playlists/kodi_pl.py b/resources/lib/playlists/kodi_pl.py index 84afd07f..f131d79d 100644 --- a/resources/lib/playlists/kodi_pl.py +++ b/resources/lib/playlists/kodi_pl.py @@ -17,7 +17,7 @@ LOG = getLogger('PLEX.playlists.kodi_pl') ############################################################################### -REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''') +REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''') def create(plex_id): @@ -53,6 +53,10 @@ def create(plex_id): '%s_01.m3u' % name[:min(len(name), 248)]) else: number = int(occurance.group(1)) + 1 + if number > 3: + LOG.error('Detected spanning tree issue, abort sync for %s', + playlist) + raise PlaylistError('Spanning tree warning') basename = re.sub(REGEX_FILE_NUMBERING, '', path) path = '%s_%02d.m3u' % (basename, number) LOG.debug('Kodi playlist path: %s', path) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 26daec86..3d35a983 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -97,12 +97,6 @@ class PlayqueueMonitor(backgroundthread.KillableThread): (playlist) are swapped. This is what this monitor is for. Don't replace this mechanism till Kodi's implementation of playlists has improved """ - def isSuspended(self): - """ - Returns True if the thread is suspended - """ - return self._suspended or app.APP.suspend_threads - def _compare_playqueues(self, playqueue, new): """ Used to poll the Kodi playqueue and update the Plex playqueue if needed @@ -193,11 +187,17 @@ class PlayqueueMonitor(backgroundthread.KillableThread): def run(self): LOG.info("----===## Starting PlayqueueMonitor ##===----") + app.APP.register_thread(self) + try: + self._run() + finally: + app.APP.deregister_thread(self) + LOG.info("----===## PlayqueueMonitor stopped ##===----") + + def _run(self): while not self.isCanceled(): - while self.isSuspended(): - if self.isCanceled(): - break - app.APP.monitor.waitForAbort(1) + if self.wait_while_suspended(): + return with app.APP.lock_playqueues: for playqueue in PLAYQUEUES: kodi_pl = js.playlist_get_items(playqueue.playlistid) @@ -212,4 +212,3 @@ class PlayqueueMonitor(backgroundthread.KillableThread): self._compare_playqueues(playqueue, kodi_pl) playqueue.old_kodi_pl = list(kodi_pl) app.APP.monitor.waitForAbort(0.2) - LOG.info("----===## PlayqueueMonitor stopped ##===----") diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index b7143273..e5780051 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -147,7 +147,8 @@ class PlayUtils(): return False return True - def get_max_bitrate(self): + @staticmethod + def get_max_bitrate(): # get the addon video quality videoQuality = utils.settings('maxVideoQualities') bitrate = { @@ -167,7 +168,8 @@ class PlayUtils(): # max bit rate supported by server (max signed 32bit integer) return bitrate.get(videoQuality, 2147483) - def getH265(self): + @staticmethod + def getH265(): """ Returns the user settings for transcoding h265: boundary resolutions of 480, 720 or 1080 as an int @@ -182,7 +184,8 @@ class PlayUtils(): } return H265[utils.settings('transcodeH265')] - def get_bitrate(self): + @staticmethod + def get_bitrate(): """ Get the desired transcoding bitrate from the settings """ @@ -203,7 +206,8 @@ class PlayUtils(): # max bit rate supported by server (max signed 32bit integer) return bitrate.get(videoQuality, 2147483) - def get_resolution(self): + @staticmethod + def get_resolution(): """ Get the desired transcoding resolutions from the settings """ diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index 29418253..f3e86c76 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -1256,7 +1256,7 @@ class API(object): """ return self.item.get('librarySectionID') - def collections_match(self): + def collections_match(self, section_id): """ Downloads one additional xml from the PMS in order to return a list of tuples [(collection_id, plex_id), ...] for all collections of the @@ -1264,7 +1264,7 @@ class API(object): Pass in the collection id of e.g. the movie's metadata """ if self.collections is None: - self.collections = PF.collections(self.library_section_id()) + self.collections = PF.collections(section_id) if self.collections is None: LOG.error('Could not download collections for %s', self.library_section_id()) @@ -1769,7 +1769,7 @@ class API(object): if force_check is False: # Validate the path is correct with user intervention if self.ask_to_validate(path): - app.SYNC.stop_sync = True + app.APP.stop_threads(block=False) path = None app.SYNC.path_verified = True else: diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 01b7f259..37fd6e4d 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -80,13 +80,8 @@ class PlexCompanion(backgroundthread.KillableThread): self.subscription_manager = None super(PlexCompanion, self).__init__() - def isSuspended(self): - """ - Returns True if the thread is suspended - """ - return self._suspended or app.APP.suspend - - def _process_alexa(self, data): + @staticmethod + def _process_alexa(data): xml = PF.GetPlexMetadata(data['key']) try: xml[0].attrib @@ -141,7 +136,8 @@ class PlexCompanion(backgroundthread.KillableThread): executebuiltin('RunPlugin(plugin://%s?%s)' % (v.ADDON_ID, urlencode(params))) - def _process_playlist(self, data): + @staticmethod + def _process_playlist(data): # Get the playqueue ID _, container_key, query = PF.ParseContainerKey(data['containerKey']) try: @@ -165,7 +161,8 @@ class PlexCompanion(backgroundthread.KillableThread): offset=data.get('offset'), transient_token=data.get('token')) - def _process_streams(self, data): + @staticmethod + def _process_streams(data): """ Plex Companion client adjusted audio or subtitle stream """ @@ -186,7 +183,8 @@ class PlexCompanion(backgroundthread.KillableThread): else: LOG.error('Unknown setStreams command: %s', data) - def _process_refresh(self, data): + @staticmethod + def _process_refresh(data): """ example data: {'playQueueID': '8475', 'commandID': '11'} """ @@ -245,6 +243,7 @@ class PlexCompanion(backgroundthread.KillableThread): """ Ensure that sockets will be closed no matter what """ + app.APP.register_thread(self) try: self._run() finally: @@ -257,7 +256,8 @@ class PlexCompanion(backgroundthread.KillableThread): self.httpd.socket.close() except AttributeError: pass - LOG.info("----===## Plex Companion stopped ##===----") + app.APP.deregister_thread(self) + LOG.info("----===## Plex Companion stopped ##===----") def _run(self): httpd = self.httpd @@ -303,10 +303,8 @@ class PlexCompanion(backgroundthread.KillableThread): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - while self.isSuspended(): - if self.isCanceled(): - break - app.APP.monitor.waitForAbort(1) + if self.wait_while_suspended(): + break try: message_count += 1 if httpd: diff --git a/resources/lib/plex_db/common.py b/resources/lib/plex_db/common.py index dcacabfd..f29ad99f 100644 --- a/resources/lib/plex_db/common.py +++ b/resources/lib/plex_db/common.py @@ -194,7 +194,8 @@ def initialize(): section_name TEXT, plex_type TEXT, kodi_tagid INTEGER, - sync_to_kodi INTEGER) + sync_to_kodi INTEGER, + last_sync INTEGER) ''') plexdb.cursor.execute(''' CREATE TABLE IF NOT EXISTS movie( @@ -239,6 +240,7 @@ def initialize(): parent_id INTEGER, kodi_id INTEGER, kodi_fileid INTEGER, + kodi_fileid_2 INTEGER, kodi_pathid INTEGER, fanart_synced INTEGER, last_sync INTEGER) diff --git a/resources/lib/plex_db/sections.py b/resources/lib/plex_db/sections.py index f214b955..a65e3cec 100644 --- a/resources/lib/plex_db/sections.py +++ b/resources/lib/plex_db/sections.py @@ -4,43 +4,39 @@ from __future__ import absolute_import, division, unicode_literals class Sections(object): - def section_ids(self): + def all_sections(self): """ - Returns an iterator for section Plex ids for all sections - """ - self.cursor.execute('SELECT section_id FROM sections') - return (x[0] for x in self.cursor) - - def section_infos(self): - """ - Returns an iterator for dicts for all Plex libraries: - { - 'section_id' - 'section_name' - 'plex_type' - 'kodi_tagid' - 'sync_to_kodi' - } + Returns an iterator for all sections """ self.cursor.execute('SELECT * FROM sections') - return ({'section_id': x[0], - 'section_name': x[1], - 'plex_type': x[2], - 'kodi_tagid': x[3], - 'sync_to_kodi': x[4]} for x in self.cursor) + return (self.entry_to_section(x) for x in self.cursor) def section(self, section_id): """ - For section_id, returns the tuple (or None) + For section_id, returns the dict section_id INTEGER PRIMARY KEY, section_name TEXT, plex_type TEXT, kodi_tagid INTEGER, - sync_to_kodi INTEGER + sync_to_kodi BOOL, + last_sync INTEGER """ self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1', (section_id, )) - return self.cursor.fetchone() + return self.entry_to_section(self.cursor.fetchone()) + + @staticmethod + def entry_to_section(entry): + if not entry: + return + return { + 'section_id': entry[0], + 'section_name': entry[1], + 'plex_type': entry[2], + 'kodi_tagid': entry[3], + 'sync_to_kodi': entry[4] == 1, + 'last_sync': entry[5] + } def section_id_by_name(self, section_name): """ @@ -54,22 +50,35 @@ class Sections(object): pass def add_section(self, section_id, section_name, plex_type, kodi_tagid, - sync_to_kodi=True): + sync_to_kodi, last_sync): """ Appends a Plex section to the Plex sections table sync=False: Plex library won't be synced to Kodi """ query = ''' INSERT OR REPLACE INTO sections( - section_id, section_name, plex_type, kodi_tagid, sync_to_kodi) - VALUES (?, ?, ?, ?, ?) + section_id, + section_name, + plex_type, + kodi_tagid, + sync_to_kodi, + last_sync) + VALUES (?, ?, ?, ?, ?, ?) ''' self.cursor.execute(query, (section_id, section_name, plex_type, kodi_tagid, - sync_to_kodi)) + sync_to_kodi, + last_sync)) + + def update_section(self, section_id, section_name): + """ + Updates the section with section_id + """ + query = 'UPDATE sections SET section_name = ? WHERE section_id = ?' + self.cursor.execute(query, (section_name, section_id)) def remove_section(self, section_id): """ @@ -77,3 +86,35 @@ class Sections(object): """ self.cursor.execute('DELETE FROM sections WHERE section_id = ?', (section_id, )) + + def update_section_sync(self, section_id, sync_to_kodi): + """ + Updates whether we should sync sections_id (sync=True) or not + """ + if sync_to_kodi: + query = ''' + UPDATE sections + SET sync_to_kodi = ? + WHERE section_id = ? + ''' + else: + # Set last_sync = 0 in order to force a full sync if reactivated + query = ''' + UPDATE sections + SET sync_to_kodi = ?, last_sync = 0 + WHERE section_id = ? + ''' + self.cursor.execute(query, (sync_to_kodi, section_id)) + + def update_section_last_sync(self, section_id, last_sync): + """ + Updates the timestamp for the section + """ + self.cursor.execute('UPDATE sections SET last_sync = ? WHERE section_id = ?', + (last_sync, section_id)) + + def force_full_sync(self): + """ + Sets the last_sync flag to 0 for every section + """ + self.cursor.execute('UPDATE sections SET last_sync = 0') diff --git a/resources/lib/plex_db/tvshows.py b/resources/lib/plex_db/tvshows.py index 4994a7d5..ae643b0e 100644 --- a/resources/lib/plex_db/tvshows.py +++ b/resources/lib/plex_db/tvshows.py @@ -59,7 +59,7 @@ class TVShows(object): def add_episode(self, plex_id, checksum, section_id, show_id, grandparent_id, season_id, parent_id, kodi_id, kodi_fileid, - kodi_pathid, last_sync): + kodi_fileid_2, kodi_pathid, last_sync): """ Appends or replaces an entry into the plex table """ @@ -75,10 +75,11 @@ class TVShows(object): parent_id, kodi_id, kodi_fileid, + kodi_fileid_2, kodi_pathid, fanart_synced, last_sync) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (plex_id, checksum, @@ -89,6 +90,7 @@ class TVShows(object): parent_id, kodi_id, kodi_fileid, + kodi_fileid_2, kodi_pathid, 0, last_sync)) @@ -129,21 +131,6 @@ class TVShows(object): return self.entry_to_season(self.cursor.fetchone()) def episode(self, plex_id): - """ - Returns the show info as a tuple for the TV show with plex_id: - plex_id INTEGER PRIMARY KEY, - checksum INTEGER UNIQUE, - section_id INTEGER, - show_id INTEGER, # plex_id of the parent show - grandparent_id INTEGER, # kodi_id of the parent show - season_id INTEGER, # plex_id of the parent season - parent_id INTEGER, # kodi_id of the parent season - kodi_id INTEGER, - kodi_fileid INTEGER, - kodi_pathid INTEGER, - fanart_synced INTEGER, - last_sync INTEGER - """ if plex_id is None: return self.cursor.execute('SELECT * FROM episode WHERE plex_id = ? LIMIT 1', @@ -166,9 +153,10 @@ class TVShows(object): 'parent_id': entry[6], 'kodi_id': entry[7], 'kodi_fileid': entry[8], - 'kodi_pathid': entry[9], - 'fanart_synced': entry[10], - 'last_sync': entry[11] + 'kodi_fileid_2': entry[9], + 'kodi_pathid': entry[10], + 'fanart_synced': entry[11], + 'last_sync': entry[12] } @staticmethod diff --git a/resources/lib/plex_functions.py b/resources/lib/plex_functions.py index 580705d0..41f5427f 100644 --- a/resources/lib/plex_functions.py +++ b/resources/lib/plex_functions.py @@ -9,7 +9,7 @@ from copy import deepcopy from time import time from threading import Thread -from .downloadutils import DownloadUtils as DU +from .downloadutils import DownloadUtils as DU, exceptions from . import backgroundthread, utils, plex_tv, variables as v, app ############################################################################### @@ -454,7 +454,7 @@ def _poke_pms(pms, queue): url, pms['uuid'], xml.get('machineIdentifier')) -def GetPlexMetadata(key): +def GetPlexMetadata(key, reraise=False): """ Returns raw API metadata for key as an etree XML. @@ -481,18 +481,71 @@ def GetPlexMetadata(key): # 'includeConcerts': 1 } url = url + '?' + urlencode(arguments) - xml = DU().downloadUrl(url) - if xml == 401: - # Either unauthorized (taken care of by doUtils) or PMS under strain - return 401 - # Did we receive a valid XML? try: - xml.attrib - # Nope we did not receive a valid XML - except AttributeError: - LOG.error("Error retrieving metadata for %s", url) - xml = None - return xml + xml = DU().downloadUrl(url, reraise=reraise) + except exceptions.RequestException: + # "PMS offline" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39213).format(app.CONN.server_name), + icon='{plex}') + except Exception: + # "Error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30135), + icon='{error}') + else: + if xml == 401: + # Either unauthorized (taken care of by doUtils) or PMS under strain + return 401 + # Did we receive a valid XML? + try: + xml[0].attrib + # Nope we did not receive a valid XML + except (TypeError, IndexError, AttributeError): + LOG.error("Error retrieving metadata for %s", url) + xml = None + return xml + + +def get_playback_xml(url, server_name, authenticate=True, token=None): + """ + Returns None if something went wrong + """ + header_options = {'X-Plex-Token': token} if not authenticate else None + try: + xml = DU().downloadUrl(url, + authenticate=authenticate, + headerOptions=header_options, + reraise=True) + except exceptions.RequestException: + # "{0} offline" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39213).format(server_name), + icon='{plex}') + except Exception as e: + LOG.error(e) + import traceback + LOG.error("Traceback:\n%s", traceback.format_exc()) + # "Play error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') + else: + try: + xml[0].attrib + except (TypeError, IndexError, AttributeError): + LOG.error('Could not get a valid xml, unfortunately') + # "Play error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') + else: + return xml def GetAllPlexChildren(key): diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 7efd94bf..d50c2a20 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -40,7 +40,7 @@ RESOURCES_XML = ('%s\n' '\n') % (v.XML_HEADER, v.ADDON_NAME, v.PLATFORM, - v.ADDON_VERSION) + v.PLATFORM_VERSION) class MyHandler(BaseHTTPRequestHandler): """ @@ -134,7 +134,7 @@ class MyHandler(BaseHTTPRequestHandler): CLIENT_DICT[self.client_address[0]] = [] tracker = CLIENT_DICT[self.client_address[0]] tracker.append(self.client_address[1]) - while (not app.APP.player.isPlaying() and + while (not app.APP.is_playing and not app.APP.monitor.abortRequested() and sub_mgr.stop_sent_to_web and not (len(tracker) >= 4 and diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 077c46c2..e94feaba 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -45,6 +45,9 @@ class plexgdm: self.discover_message = 'M-SEARCH * HTTP/1.0' self.client_header = '* HTTP/1.0' self.client_data = None + self.update_sock = None + self.discover_t = None + self.register_t = None self._multicast_address = '239.0.0.250' self.discover_group = (self._multicast_address, 32414) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 07614cab..597d8c14 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -9,7 +9,7 @@ from logging import getLogger from threading import Thread from ..downloadutils import DownloadUtils as DU -from .. import utils, timing +from .. import timing from .. import app from .. import variables as v from .. import json_rpc as js @@ -50,7 +50,7 @@ HEADERS_PMS = { 'Accept': 'text/plain, */*; q=0.01', 'Accept-Language': 'en', 'Accept-Encoding': 'gzip, deflate', - 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM) + 'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.DEVICE) } @@ -64,14 +64,13 @@ def params_pms(): # 'audioDecoders=mp3,aac,dts{bitrate:800000&channels:2},' # 'ac3{bitrate:800000&channels:2}', 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, - 'X-Plex-Device': v.PLATFORM, + 'X-Plex-Device': v.DEVICE, 'X-Plex-Device-Name': v.DEVICENAME, # 'X-Plex-Device-Screen-Resolution': '1916x1018,1920x1080', - 'X-Plex-Model': 'unknown', + 'X-Plex-Model': v.MODEL, 'X-Plex-Platform': v.PLATFORM, - 'X-Plex-Platform-Version': 'unknown', + 'X-Plex-Platform-Version': v.PLATFORM_VERSION, 'X-Plex-Product': v.ADDON_NAME, - 'X-Plex-Provider-Version': v.ADDON_VERSION, 'X-Plex-Version': v.ADDON_VERSION, 'hasMDE': '1', # 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'], @@ -89,7 +88,7 @@ def headers_companion_client(): 'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER, 'X-Plex-Device-Name': v.DEVICENAME, 'X-Plex-Platform': v.PLATFORM, - 'X-Plex-Platform-Version': 'unknown', + 'X-Plex-Platform-Version': v.PLATFORM_VERSION, 'X-Plex-Product': v.ADDON_NAME, 'X-Plex-Version': v.ADDON_VERSION, 'Accept-Encoding': 'gzip, deflate', diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index 17ee3959..da7966d8 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -7,9 +7,9 @@ import xbmc import xbmcgui from . import utils, clientinfo, timing -from . import initialsetup, artwork +from . import initialsetup from . import kodimonitor -from . import sync +from . import sync, library_sync from . import websocket_client from . import plex_companion from . import plex_functions as PF, playqueue as PQ @@ -27,9 +27,8 @@ LOG = logging.getLogger("PLEX.service") ############################################################################### WINDOW_PROPERTIES = ( - "plex_dbScan", "pms_token", "plex_token", "pms_server", - "plex_authenticated", "plex_restricteduser", "plex_allows_mediaDeletion", - "plexkodiconnect.command", "plex_result") + "pms_token", "plex_token", "plex_authenticated", "plex_restricteduser", + "plex_allows_mediaDeletion", "plexkodiconnect.command", "plex_result") # "Start from beginning", "Play from beginning" STRINGS = (utils.try_encode(utils.lang(12021)), @@ -98,8 +97,7 @@ class Service(): # Load/Reset PKC entirely - important for user/Kodi profile switch # Clear video nodes properties - from .library_sync import videonodes - videonodes.VideoNodes().clearProperties() + library_sync.VideoNodes().clearProperties() clientinfo.getDeviceId() # Init time-offset between Kodi and Plex timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0) @@ -108,12 +106,16 @@ class Service(): self.server_has_been_online = True self.welcome_msg = True self.connection_check_counter = 0 + self.setup = None + self.alexa = None + self.playqueue = None # Flags for other threads self.connection_check_running = False self.auth_running = False self._init_done = True - def isCanceled(self): + @staticmethod + def isCanceled(): return xbmc.abortRequested or app.APP.stop_pkc def on_connection_check(self, result): @@ -126,9 +128,10 @@ class Service(): # Alert the user and suppress future warning if app.CONN.online: # PMS was online before - app.CONN.online = False - app.APP.suspend_threads = True LOG.warn("Plex Media Server went offline") + app.CONN.online = False + app.APP.suspend_threads() + LOG.debug('Threads suspended') if utils.settings('show_pms_offline') == 'true': utils.dialog('notification', utils.lang(33001), @@ -165,32 +168,21 @@ class Service(): if app.ACCOUNT.authenticated: # Server got offline when we were authenticated. # Hence resume threads - app.APP.suspend_threads = False + app.APP.resume_threads() app.CONN.online = True finally: self.connection_check_running = False - def log_out(self): + @staticmethod + def log_out(): """ Ensures that lib sync threads are suspended; signs out user """ LOG.info('Log-out requested') - app.APP.suspend_threads = True - i = 0 - while app.SYNC.db_scan: - i += 1 - app.APP.monitor.waitForAbort(0.1) - if i > 150: - LOG.error('Could not stop library sync, aborting log-out') - # Failed to reset PMS and plex.tv connects. Try to restart Kodi - utils.messageDialog(utils.lang(29999), utils.lang(39208)) - # Resuming threads, just in case - app.APP.suspend_threads = False - return False - LOG.info('Successfully stopped library sync') + app.APP.suspend_threads() + LOG.info('Successfully suspended threads') app.ACCOUNT.log_out() LOG.info('User has been logged out') - return True def choose_pms_server(self, manual=False): LOG.info("Choosing PMS server requested, starting") @@ -202,15 +194,16 @@ class Service(): if not server: LOG.info('We did not connect to a new PMS, aborting') return False - LOG.info("User chose server %s", server['name']) - if server['machineIdentifier'] == app.CONN.machine_identifier: + LOG.info("User chose server %s with url %s", + server['name'], server['baseURL']) + if (server['machineIdentifier'] == app.CONN.machine_identifier and + server['baseURL'] == app.CONN.server): LOG.info('User chose old PMS to connect to') return False # Save changes to to file self.setup.save_pms_settings(server['baseURL'], server['token']) self.setup.write_pms_to_settings(server) - if not self.log_out(): - return False + self.log_out() # Wipe Kodi and Plex database as well as playlists and video nodes utils.wipe_database() app.CONN.load() @@ -220,20 +213,23 @@ class Service(): self.welcome_msg = False # Force a full sync app.SYNC.run_lib_scan = 'full' + # Enable the main loop to continue + app.APP.suspend = False LOG.info("Choosing new PMS complete") return True def switch_plex_user(self): - if not self.log_out(): - return False + self.log_out() # First remove playlists of old user utils.delete_playlists() # Remove video nodes utils.delete_nodes() app.ACCOUNT.set_unauthenticated() # Force full sync after login - utils.settings('lastfullsync', value='0') + library_sync.force_full_sync() app.SYNC.run_lib_scan = 'full' + # Enable the main loop to display user selection dialog + app.APP.suspend = False return True def toggle_plex_tv(self): @@ -246,6 +242,8 @@ class Service(): if self.setup.plex_tv_sign_in(): self.setup.write_credentials_to_settings() app.ACCOUNT.load() + # Enable the main loop to continue + app.APP.suspend = False def authenticate(self): """ @@ -265,22 +263,19 @@ class Service(): icon='{plex}', time=2000, sound=False) - app.APP.suspend_threads = False + app.APP.resume_threads() self.auth_running = False def enter_new_pms_address(self): server = self.setup.enter_new_pms_address() if not server: return - if not self.log_out(): - return False - # Save changes to to file + self.log_out() + # Save changes to to file self.setup.save_pms_settings(server['baseURL'], server['token']) self.setup.write_pms_to_settings(server) if not v.KODIVERSION >= 18: utils.settings('sslverify', value='false') - if not self.log_out(): - return False # Wipe Kodi and Plex database as well as playlists and video nodes utils.wipe_database() app.CONN.load() @@ -290,9 +285,38 @@ class Service(): self.welcome_msg = False # Force a full sync app.SYNC.run_lib_scan = 'full' - LOG.info("Choosing new PMS complete") + # Enable the main loop to continue + app.APP.suspend = False + LOG.info("Entering PMS address complete") return True + def choose_plex_libraries(self): + if not app.CONN.online: + LOG.error('PMS not online to choose libraries') + # "{0} offline" + utils.dialog('notification', + utils.lang(29999), + utils.lang(39213).format(app.CONN.server_name or ''), + icon='{plex}') + return + if not app.ACCOUNT.authenticated: + LOG.error('Not yet authenticated for PMS to choose libraries') + # "Unauthorized for PMS" + utils.dialog('notification', utils.lang(29999), utils.lang(30017)) + return + app.APP.suspend_threads() + from .library_sync import sections + try: + # Get newest sections from the PMS + if not sections.sync_from_pms(self): + return + if not sections.choose_libraries(): + return + # Force a full sync + app.SYNC.run_lib_scan = 'full' + finally: + app.APP.resume_threads() + def _do_auth(self): LOG.info('Authenticating user') if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate @@ -323,6 +347,8 @@ class Service(): if not user: LOG.info('No user received') app.APP.suspend = True + app.APP.suspend_threads() + LOG.debug('Threads suspended') return False username = user.title user_id = user.id @@ -355,7 +381,10 @@ class Service(): app.ACCOUNT.load() continue else: + LOG.debug('Suspending threads') app.APP.suspend = True + app.APP.suspend_threads() + LOG.debug('Threads suspended') return False elif res >= 400: LOG.error('Answer from PMS is not as expected') @@ -378,13 +407,6 @@ class Service(): app.init() app.APP.monitor = kodimonitor.KodiMonitor() app.APP.player = xbmc.Player() - artwork.IMAGE_CACHING_SUSPENDS = [ - app.APP.suspend_threads, - app.SYNC.stop_sync, - app.SYNC.db_scan - ] - if not utils.settings('imageSyncDuringPlayback') == 'true': - artwork.IMAGE_CACHING_SUSPENDS.append(app.SYNC.suspend_sync) # Initialize the PKC playqueues PQ.init_playqueues() @@ -444,6 +466,8 @@ class Service(): app.SYNC.run_lib_scan = 'fanart' elif plex_command == 'textures-scan': app.SYNC.run_lib_scan = 'textures' + elif plex_command == 'select-libraries': + self.choose_plex_libraries() elif plex_command == 'RESET-PKC': utils.reset() if task: @@ -505,7 +529,10 @@ class Service(): # EXITING PKC # Tell all threads to terminate (e.g. several lib sync threads) + LOG.debug('Aborting all threads') app.APP.stop_pkc = True + # Will block until threads have quit + app.APP.stop_threads() utils.window('plex_service_started', clear=True) LOG.info("======== STOP %s ========", v.ADDON_NAME) diff --git a/resources/lib/sync.py b/resources/lib/sync.py index dacca413..8808972b 100644 --- a/resources/lib/sync.py +++ b/resources/lib/sync.py @@ -15,19 +15,6 @@ if library_sync.PLAYLIST_SYNC_ENABLED: LOG = getLogger('PLEX.sync') -def set_library_scan_toggle(boolean=True): - """ - Make sure to hit this function before starting large scans - """ - if not boolean: - # Deactivate - app.SYNC.db_scan = False - utils.window('plex_dbScan', clear=True) - else: - app.SYNC.db_scan = True - utils.window('plex_dbScan', value="true") - - class Sync(backgroundthread.KillableThread): """ The one and only library sync thread. Spawn only 1! @@ -35,24 +22,18 @@ class Sync(backgroundthread.KillableThread): def __init__(self): self.sync_successful = False self.last_full_sync = 0 - self.fanart = None - # Show sync dialog even if user deactivated? - self.force_dialog = False + self.fanart_thread = None self.image_cache_thread = None # Lock used to wait on a full sync, e.g. on initial sync # self.lock = backgroundthread.threading.Lock() super(Sync, self).__init__() - def isSuspended(self): - return self._suspended or app.APP.suspend_threads - def triage_lib_scans(self): """ Decides what to do if app.SYNC.run_lib_scan has been set. E.g. manually triggered full or repair syncs """ if app.SYNC.run_lib_scan in ("full", "repair"): - set_library_scan_toggle() LOG.info('Full library scan requested, starting') self.start_library_sync(show_dialog=True, repair=app.SYNC.run_lib_scan == 'repair', @@ -89,23 +70,16 @@ class Sync(backgroundthread.KillableThread): """ self.sync_successful = successful self.last_full_sync = timing.unix_timestamp() - set_library_scan_toggle(boolean=False) if not successful: LOG.warn('Could not finish scheduled full sync') - # try: - # self.lock.release() - # except backgroundthread.threading.ThreadError: - # pass + app.APP.resume_fanart_thread() + app.APP.resume_caching_thread() def start_library_sync(self, show_dialog=None, repair=False, block=False): - set_library_scan_toggle(boolean=True) + app.APP.suspend_fanart_thread(block=True) + app.APP.suspend_caching_thread(block=True) show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog library_sync.start(show_dialog, repair, self.on_library_scan_finished) - # if block: - # self.lock.acquire() - # Will block until scan is finished - # self.lock.acquire() - # self.lock.release() def start_fanart_download(self, refresh): if not utils.settings('FanartTV') == 'true': @@ -114,11 +88,11 @@ class Sync(backgroundthread.KillableThread): if not app.SYNC.artwork: LOG.info('Not synching Plex PMS artwork, not getting artwork') return False - elif self.fanart is None or not self.fanart.is_alive(): + elif self.fanart_thread is None or not self.fanart_thread.is_alive(): LOG.info('Start downloading additional fanart with refresh %s', refresh) - self.fanart = library_sync.FanartThread(self.on_fanart_download_finished, refresh) - self.fanart.start() + self.fanart_thread = library_sync.FanartThread(self.on_fanart_download_finished, refresh) + self.fanart_thread.start() return True else: LOG.info('Still downloading fanart') @@ -144,16 +118,21 @@ class Sync(backgroundthread.KillableThread): self.image_cache_thread.start() def run(self): + LOG.info("---===### Starting Sync Thread ###===---") + app.APP.register_thread(self) try: self._run_internal() except Exception: - app.SYNC.db_scan = False - utils.window('plex_dbScan', clear=True) utils.ERROR(txt='sync.py crashed', notify=True) raise + finally: + try: + app.APP.deregister_thread(self) + except ValueError: + pass + LOG.info("###===--- Sync Thread Stopped ---===###") def _run_internal(self): - LOG.info("---===### Starting Sync Thread ###===---") install_sync_done = utils.settings('SyncInstallRunDone') == 'true' playlist_monitor = None initial_sync_done = False @@ -170,6 +149,8 @@ class Sync(backgroundthread.KillableThread): v.MIN_DB_VERSION): LOG.warn("Db version out of date: %s minimum version " "required: %s", current_version, v.MIN_DB_VERSION) + # In order to not wait for this thread to suspend + app.APP.deregister_thread(self) # DB out of date. Proceed to recreate? if not utils.yesno_dialog(utils.lang(29999), utils.lang(39401)): @@ -186,17 +167,10 @@ class Sync(backgroundthread.KillableThread): while not self.isCanceled(): # In the event the server goes offline - while self.isSuspended(): - if self.isCanceled(): - # Abort was requested while waiting. We should exit - LOG.info("###===--- Sync Thread Stopped ---===###") - return - app.APP.monitor.waitForAbort(1) - + if self.wait_while_suspended(): + return if not install_sync_done: # Very FIRST sync ever upon installation or reset of Kodi DB - set_library_scan_toggle() - self.force_dialog = True # Initialize time offset Kodi - PMS library_sync.sync_pms_time() last_time_sync = timing.unix_timestamp() @@ -217,7 +191,6 @@ class Sync(backgroundthread.KillableThread): else: LOG.error('Initial start-up full sync unsuccessful') app.APP.monitor.waitForAbort(1) - self.force_dialog = False xbmc.executebuiltin('InhibitIdleShutdown(false)') elif not initial_sync_done: @@ -237,13 +210,10 @@ class Sync(backgroundthread.KillableThread): app.APP.monitor.waitForAbort(1) # Currently no db scan, so we could start a new scan - elif app.SYNC.db_scan is False: + else: # Full scan was requested from somewhere else if app.SYNC.run_lib_scan is not None: - # Force-show dialogs since they are user-initiated - self.force_dialog = True self.triage_lib_scans() - self.force_dialog = False # Reset the flag app.SYNC.run_lib_scan = None continue @@ -251,7 +221,7 @@ class Sync(backgroundthread.KillableThread): # Standard syncs - don't force-show dialogs now = timing.unix_timestamp() if (now - self.last_full_sync > app.SYNC.full_sync_intervall and - not app.SYNC.suspend_sync): + not app.APP.is_playing_video): LOG.info('Doing scheduled full library scan') self.start_library_sync() elif now - last_time_sync > one_day_in_seconds: @@ -287,4 +257,3 @@ class Sync(backgroundthread.KillableThread): DU().stopSession() except AttributeError: pass - LOG.info("###===--- Sync Thread Stopped ---===###") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index df021cde..2fa3e7f7 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -223,7 +223,7 @@ def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False): LOG.error('Error encountered: %s - %s', txt, short) if cancel_sync: from . import app - app.SYNC.stop_sync = True + app.APP.stop_threads(block=False) if hide_tb: return short @@ -340,14 +340,14 @@ def valid_filename(text): text = re.sub(r'(?! )\s', '', text) # ASCII characters 0 to 31 (non-printable, just in case) text = re.sub(u'[\x00-\x1f]', '', text) - if v.PLATFORM == 'Windows': + if v.DEVICE == 'Windows': # Whitespace at the end of the filename is illegal text = text.strip() # Dot at the end of a filename is illegal text = re.sub(r'\.+$', '', text) # Illegal Windows characters text = re.sub(r'[/\\:*?"<>|\^]', '', text) - elif v.PLATFORM == 'MacOSX': + elif v.DEVICE == 'MacOSX': # Colon is illegal text = re.sub(r':', '', text) # Files cannot begin with a dot @@ -466,7 +466,7 @@ def wipe_database(): kodi_db.reset_cached_images() # reset the install run flag settings('SyncInstallRunDone', value="false") - settings('lastfullsync', value="0") + settings('sections_asked_for_machine_identifier', value='') init_dbs() LOG.info('Wiping done') if settings('kodi_db_has_been_wiped_clean') != 'true': @@ -502,19 +502,7 @@ def reset(ask_user=True): return from . import app # first stop any db sync - app.APP.suspend_threads = True - count = 15 - while app.SYNC.db_scan: - LOG.info("Sync is running, will retry: %s...", count) - count -= 1 - if count == 0: - LOG.error('Could not stop PKC syncing process to reset the DB') - # Could not stop the database from running. Please try again later. - messageDialog(lang(29999), lang(39601)) - app.APP.suspend_threads = False - return - xbmc.sleep(1000) - + app.APP.suspend_threads() # Reset all PlexKodiConnect Addon settings? (this is usually NOT # recommended and unnecessary!) if ask_user and yesno_dialog(lang(29999), lang(39603)): diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 34b48c93..6640ed18 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, unicode_literals import os import sys import re +import platform import xbmc from xbmcaddon import Addon @@ -51,23 +52,28 @@ KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') KODI_PROFILE = try_decode(xbmc.translatePath("special://profile")) if xbmc.getCondVisibility('system.platform.osx'): - PLATFORM = "MacOSX" + DEVICE = "MacOSX" elif xbmc.getCondVisibility("system.platform.uwp"): - PLATFORM = "Microsoft UWP" + DEVICE = "Microsoft UWP" elif xbmc.getCondVisibility('system.platform.atv2'): - PLATFORM = "AppleTV2" + DEVICE = "AppleTV2" elif xbmc.getCondVisibility('system.platform.ios'): - PLATFORM = "iOS" + DEVICE = "iOS" elif xbmc.getCondVisibility('system.platform.windows'): - PLATFORM = "Windows" + DEVICE = "Windows" elif xbmc.getCondVisibility('system.platform.raspberrypi'): - PLATFORM = "RaspberryPi" + DEVICE = "RaspberryPi" elif xbmc.getCondVisibility('system.platform.linux'): - PLATFORM = "Linux" + DEVICE = "Linux" elif xbmc.getCondVisibility('system.platform.android'): - PLATFORM = "Android" + DEVICE = "Android" else: - PLATFORM = "Unknown" + DEVICE = "Unknown" + +MODEL = platform.release() or 'Unknown' +# Plex' own platform for e.g. Plex Media Player +PLATFORM = 'Konvergo' +PLATFORM_VERSION = '2.26.0.947-1e21fa2b' DEVICENAME = try_decode(_ADDON.getSetting('deviceName')) if not DEVICENAME: @@ -91,7 +97,7 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort')) PKC_MACHINE_IDENTIFIER = None # Minimal PKC version needed for the Kodi database - otherwise need to recreate -MIN_DB_VERSION = '2.6.1' +MIN_DB_VERSION = '2.6.8' # Supported databases SUPPORTED_VIDEO_DB = { @@ -637,7 +643,7 @@ def database_paths(): # Encoding to be used for our m3u playlist files # m3u files do not have encoding specified by definition, unfortunately. -if PLATFORM == 'Windows': +if DEVICE == 'Windows': M3U_ENCODING = 'mbcs' else: M3U_ENCODING = sys.getfilesystemencoding() diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py index 7c4e3452..8f9e372b 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/websocket.py @@ -105,6 +105,19 @@ class WebSocketTimeoutException(WebSocketException): pass +class WebsocketRedirect(WebSocketException): + """ + WebsocketRedirect will be raised if a status code 301 is returned + The Exception will be instantiated with a dict containing all response + headers; which should contain the redirect address under the key 'location' + + Access the headers via the attribute headers + """ + def __init__(self, headers): + self.headers = headers + super(WebsocketRedirect, self).__init__() + + DEFAULT_TIMEOUT = None TRACE_ENABLED = False @@ -162,10 +175,10 @@ def _parse_url(url): port = parsed.port is_secure = False - if scheme == "ws": + if scheme == "ws" or scheme == 'http': if not port: port = 80 - elif scheme == "wss": + elif scheme == "wss" or scheme == 'https': is_secure = True if not port: port = 443 @@ -500,6 +513,9 @@ class WebSocket(object): LOG.debug("-----------------------") status, resp_headers = self._read_headers() + if status == 301: + # Redirect + raise WebsocketRedirect(resp_headers) if status != 101: self.close() raise WebSocketException("Handshake Status %d" % status) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 2068d2f8..82c80881 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -18,6 +18,7 @@ class WebSocket(backgroundthread.KillableThread): def __init__(self): self.ws = None + self.redirect_uri = None super(WebSocket, self).__init__() def process(self, opcode, message): @@ -46,20 +47,20 @@ class WebSocket(backgroundthread.KillableThread): def run(self): LOG.info("----===## Starting %s ##===----", self.__class__.__name__) + app.APP.register_thread(self) counter = 0 while not self.isCanceled(): # In the event the server goes offline - while self.isSuspended(): + if self.isSuspended(): # Set in service.py if self.ws is not None: self.ws.close() self.ws = None - if self.isCanceled(): + if self.wait_while_suspended(): # Abort was requested while waiting. We should exit LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) return - app.APP.monitor.waitForAbort(1) try: self.process(*self.receive(self.ws)) except websocket.WebSocketTimeoutException: @@ -91,6 +92,16 @@ class WebSocket(backgroundthread.KillableThread): self.__class__.__name__) self.ws = None app.APP.monitor.waitForAbort(1) + except websocket.WebsocketRedirect as e: + LOG.info('301 redirect detected') + self.redirect_uri = e.headers.get('location', e.headers.get('Location')) + if self.redirect_uri: + self.redirect_uri.decode('utf-8') + counter += 1 + if counter >= 10: + LOG.info('%s: Repeated WebsocketRedirect detected. Stopping now', + self.__class__.__name__) + break except websocket.WebSocketException as e: LOG.info('%s: WebSocketException: %s', self.__class__.__name__, e) @@ -125,6 +136,7 @@ class WebSocket(backgroundthread.KillableThread): # Close websocket connection on shutdown if self.ws is not None: self.ws.close() + app.APP.deregister_thread(self) LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) @@ -136,23 +148,25 @@ class PMS_Websocket(WebSocket): """ Returns True if the thread is suspended """ - return (self._suspended or - app.APP.suspend_threads or - app.SYNC.background_sync_disabled) + return self._suspended or app.SYNC.background_sync_disabled def getUri(self): - server = app.CONN.server - # Get the appropriate prefix for the websocket - if server.startswith('https'): - server = "wss%s" % server[5:] + if self.redirect_uri: + uri = self.redirect_uri + self.redirect_uri = None else: - server = "ws%s" % server[4:] - uri = "%s/:/websockets/notifications" % server - # Need to use plex.tv token, if any. NOT user token - if app.ACCOUNT.plex_token: - uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token + server = app.CONN.server + # Get the appropriate prefix for the websocket + if server.startswith('https'): + server = "wss%s" % server[5:] + else: + server = "ws%s" % server[4:] + uri = "%s/:/websockets/notifications" % server + # Need to use plex.tv token, if any. NOT user token + if app.ACCOUNT.plex_token: + uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token sslopt = {} - if utils.settings('sslverify') == "false": + if v.KODIVERSION == 17 and utils.settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE LOG.debug("%s: Uri: %s, sslopt: %s", self.__class__.__name__, uri, sslopt) @@ -186,11 +200,6 @@ class PMS_Websocket(WebSocket): # Drop everything we're not interested in if typus not in ('playing', 'timeline', 'activity'): return - elif typus == 'activity' and app.SYNC.db_scan is True: - # Only add to processing if PKC is NOT doing a lib scan (and thus - # possibly causing these reprocessing messages en mass) - LOG.debug('%s: Dropping message as PKC is currently synching', - self.__class__.__name__) else: # Put PMS message on queue and let libsync take care of it app.APP.websocket_queue.put(message) @@ -209,10 +218,14 @@ class Alexa_Websocket(WebSocket): app.ACCOUNT.restricted_user) def getUri(self): - uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' - % (app.ACCOUNT.plex_user_id, - v.PKC_MACHINE_IDENTIFIER, - app.ACCOUNT.plex_token)) + if self.redirect_uri: + uri = self.redirect_uri + self.redirect_uri = None + else: + uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' + % (app.ACCOUNT.plex_user_id, + v.PKC_MACHINE_IDENTIFIER, + app.ACCOUNT.plex_token)) sslopt = {} LOG.debug("%s: Uri: %s, sslopt: %s", self.__class__.__name__, uri, sslopt) diff --git a/resources/lib/windows/kodigui.py b/resources/lib/windows/kodigui.py index 9bd917e7..7dc455ed 100644 --- a/resources/lib/windows/kodigui.py +++ b/resources/lib/windows/kodigui.py @@ -206,7 +206,7 @@ class ControlledWindow(ControlledBase, BaseWindow): if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): self.doClose() return - except: + except Exception: traceback.print_exc() BaseWindow.onAction(self, action) @@ -218,7 +218,7 @@ class ControlledDialog(ControlledBase, BaseDialog): if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK): self.doClose() return - except: + except Exception: traceback.print_exc() BaseDialog.onAction(self, action) @@ -228,7 +228,8 @@ DUMMY_LIST_ITEM = xbmcgui.ListItem() class ManagedListItem(object): - def __init__(self, label='', label2='', iconImage='', thumbnailImage='', path='', data_source=None, properties=None): + def __init__(self, label='', label2='', iconImage='', thumbnailImage='', + path='', data_source=None, properties=None): self._listItem = xbmcgui.ListItem(label, label2, iconImage, thumbnailImage, path) self.dataSource = data_source self.properties = {} @@ -608,13 +609,15 @@ class ManagedControlList(object): def getViewPosition(self): try: return int(xbmc.getInfoLabel('Container({0}).Position'.format(self.controlID))) - except: + except Exception: return 0 def getViewRange(self): viewPosition = self.getViewPosition() selected = self.getSelectedPosition() - return range(max(selected - viewPosition, 0), min(selected + (self._maxViewIndex - viewPosition) + 1, self.size() - 1)) + return range(max(selected - viewPosition, 0), + min(selected + (self._maxViewIndex - viewPosition) + 1, + self.size() - 1)) def positionIsValid(self, pos): return 0 <= pos < self.size() @@ -809,7 +812,7 @@ class SafeControlEdit(object): if self.processOffControlAction(action.getButtonCode()): self._win.setFocusId(self.controlID) return - except: + except Exception: traceback.print_exc() self._winOnAction(action) diff --git a/resources/settings.xml b/resources/settings.xml index fae79370..d1fc8f1e 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -49,9 +49,11 @@ + + @@ -84,7 +86,6 @@ -