diff --git a/README.md b/README.md index 1c14e0e8..750a1a59 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![stable version](https://img.shields.io/badge/stable_version-1.7.7-blue.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect/bin/repository.plexkodiconnect/repository.plexkodiconnect-1.0.0.zip) -[![beta version](https://img.shields.io/badge/beta_version-1.7.17-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.zip) +[![beta version](https://img.shields.io/badge/beta_version-1.7.21-red.svg?maxAge=60&style=flat) ](https://dl.bintray.com/croneter/PlexKodiConnect_BETA/bin-BETA/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.0.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) @@ -63,6 +63,7 @@ PKC synchronizes your media from your Plex server to the native Kodi database. H + Danish, thanks @FIGHT + Italian, thanks @nikkux, @chicco83 + Dutch, thanks @mvanbaak + + French, thanks @lflforce, @ahivert, @Nox71, @CotzaDev, @vinch100, @Polymorph31, @jbnitro + Chinese Traditional, thanks @old2tan + Chinese Simplified, thanks @everdream + [Please help translating](https://www.transifex.com/croneter/pkc) diff --git a/addon.xml b/addon.xml index c8b7d5eb..39c83f4d 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -44,7 +44,33 @@ Gebruik op eigen risico 使用風險由您自己承擔 Usar a su propio riesgo - version 1.7.17 (beta only) + version 1.7.21 (beta only) +- Fix Playback and watched status not syncing +- Fix PKC syncing progress to wrong account +- Warn user if a xml cannot be parsed + +version 1.7.20 (beta only) +- Fix for Windows usernames with non-ASCII chars +- Companion: Fix TypeError +- Use SSL settings when checking server connection +- Fix TypeError when PMS connection lost +- Increase timeout + +version 1.7.19 (beta only) +- Big code refactoring +- Many Plex Companion fixes +- Fix WindowsError or alike when deleting video nodes +- Remove restart on first setup +- Only set advancedsettings tweaks if Music enabled + +version 1.7.18 (beta only) +- Fix OperationalError when resetting PKC +- Fix possible OperationalErrors +- Companion: ensure sockets get closed +- Fix TypeError for Plex Companion +- Update Czech + +version 1.7.17 (beta only) - Don't add media by other add-ons to queue - Fix KeyError for Plex Companion - Repace Kodi mkdirs with os.makedirs diff --git a/changelog.txt b/changelog.txt index 866eb44d..37b20604 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,29 @@ +version 1.7.21 (beta only) +- Fix Playback and watched status not syncing +- Fix PKC syncing progress to wrong account +- Warn user if a xml cannot be parsed + +version 1.7.20 (beta only) +- Fix for Windows usernames with non-ASCII chars +- Companion: Fix TypeError +- Use SSL settings when checking server connection +- Fix TypeError when PMS connection lost +- Increase timeout + +version 1.7.19 (beta only) +- Big code refactoring +- Many Plex Companion fixes +- Fix WindowsError or alike when deleting video nodes +- Remove restart on first setup +- Only set advancedsettings tweaks if Music enabled + +version 1.7.18 (beta only) +- Fix OperationalError when resetting PKC +- Fix possible OperationalErrors +- Companion: ensure sockets get closed +- Fix TypeError for Plex Companion +- Update Czech + version 1.7.17 (beta only) - Don't add media by other add-ons to queue - Fix KeyError for Plex Companion diff --git a/default.py b/default.py index 9f30f61c..96983316 100644 --- a/default.py +++ b/default.py @@ -170,10 +170,10 @@ class Main(): Start up playback_starter in main Python thread """ # Put the request into the 'queue' - while window('plex_play_new_item'): + while window('plex_command'): sleep(50) - window('plex_play_new_item', - value='%s%s' % ('play', argv[2])) + window('plex_command', + value='play_%s' % argv[2]) # Wait for the result while not pickl_window('plex_result'): sleep(50) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index e43dbf53..aac7adc3 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1919,3 +1919,8 @@ msgstr "" msgctxt "#39715" msgid "items" msgstr "" + +# Error message if an xml, e.g. advancedsettings.xml cannot be parsed (xml is screwed up; formated the wrong way). Do NOT replace {0} and {1}! +msgctxt "#39716" +msgid "Kodi cannot parse {0}. PKC will not function correctly. Please visit {1} and correct your file!" +msgstr "" diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index fbd28e7c..19e3d2cf 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -53,6 +53,7 @@ from utils import window, settings, language as lang, tryDecode, tryEncode, \ from PlexFunctions import PMSHttpsEnabled import plexdb_functions as plexdb import variables as v +import state ############################################################################### @@ -628,7 +629,7 @@ class PlexAPI(): authenticate=False, headerOptions={'X-Plex-Token': PMS['token']}, verifySSL=False, - timeout=3) + timeout=10) try: xml.attrib['machineIdentifier'] except (AttributeError, KeyError): @@ -879,6 +880,8 @@ class PlexAPI(): settings('plex_restricteduser', 'true' if answer.attrib.get('restricted', '0') == '1' else 'false') + state.RESTRICTED_USER = True if \ + answer.attrib.get('restricted', '0') == '1' else False # Get final token to the PMS we've chosen url = 'https://plex.tv/api/resources?includeHttps=1' @@ -2550,20 +2553,20 @@ class API(): if "\\" in path: if not path.endswith('\\'): # Add the missing backslash - check = exists_dir(tryEncode(path + "\\")) + check = exists_dir(path + "\\") else: - check = exists_dir(tryEncode(path)) + check = exists_dir(path) else: if not path.endswith('/'): - check = exists_dir(tryEncode(path + "/")) + check = exists_dir(path + "/") else: - check = exists_dir(tryEncode(path)) + check = exists_dir(path) if not check: if forceCheck is False: # Validate the path is correct with user intervention if self.askToValidate(path): - window('plex_shouldStop', value="true") + state.STOP_SYNC = True path = None window('plex_pathverified', value='true') else: diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index a233e103..c172ca38 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -7,13 +7,14 @@ from urllib import urlencode from xbmc import sleep, executebuiltin -from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods +from utils import settings, thread_methods from plexbmchelper import listener, plexgdm, subscribers, functions, \ httppersist, plexsettings from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API import player import variables as v +import state ############################################################################### @@ -22,8 +23,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('plex_serverStatus') -@ThreadMethods +@thread_methods(add_suspends=['PMS_STATUS']) class PlexCompanion(Thread): """ """ @@ -77,6 +77,8 @@ class PlexCompanion(Thread): log.debug('Processing: %s' % task) data = task['data'] + # Get the token of the user flinging media (might be different one) + state.PLEX_TRANSIENT_TOKEN = data.get('token') if task['action'] == 'alexa': # e.g. Alexa xml = GetPlexMetadata(data['key']) @@ -144,11 +146,28 @@ class PlexCompanion(Thread): offset=data.get('offset')) def run(self): - httpd = False + # Ensure that sockets will be closed no matter what + try: + self.__run() + finally: + try: + self.httpd.socket.shutdown(SHUT_RDWR) + except AttributeError: + pass + finally: + try: + self.httpd.socket.close() + except AttributeError: + pass + log.info("----===## Plex Companion stopped ##===----") + + def __run(self): + self.httpd = False + httpd = self.httpd # Cache for quicker while loops client = self.client - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended # Start up instances requestMgr = httppersist.RequestMgr() @@ -196,12 +215,12 @@ class PlexCompanion(Thread): if httpd: t = Thread(target=httpd.handle_request) - while not threadStopped(): + while not thread_stopped(): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - while threadSuspended(): - if threadStopped(): + while thread_suspended(): + if thread_stopped(): break sleep(1000) try: @@ -245,11 +264,3 @@ class PlexCompanion(Thread): sleep(50) client.stop_all() - if httpd: - try: - httpd.socket.shutdown(SHUT_RDWR) - except: - pass - finally: - httpd.socket.close() - log.info("----===## Plex Companion stopped ##===----") diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4efc05d4..1a310921 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,7 +13,7 @@ from xbmc import executeJSONRPC, sleep, translatePath from xbmcvfs import exists from utils import window, settings, language as lang, kodiSQL, tryEncode, \ - ThreadMethods, ThreadMethodsAdditionalStop, dialog, exists_dir + thread_methods, dialog, exists_dir, tryDecode # Disable annoying requests warnings import requests.packages.urllib3 @@ -126,8 +126,8 @@ def double_urldecode(text): return unquote(unquote(text)) -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@thread_methods(add_stops=['STOP_SYNC'], + add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN']) class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() @@ -140,22 +140,16 @@ class Image_Cache_Thread(Thread): self.queue = ARTWORK_QUEUE Thread.__init__(self) - def threadSuspended(self): - # Overwrite method to add TWO additional suspends - return (self._threadSuspended or - window('suspend_LibraryThread') or - window('plex_dbScan')) - def run(self): - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended queue = self.queue sleep_between = self.sleep_between - while not threadStopped(): + while not thread_stopped(): # In the event the server goes offline - while threadSuspended(): + while thread_suspended(): # Set in service.py - if threadStopped(): + if thread_stopped(): # Abort was requested while waiting. We should exit log.info("---===### Stopped Image_Cache_Thread ###===---") return @@ -178,7 +172,7 @@ class Image_Cache_Thread(Thread): # download. All is well break except requests.ConnectionError: - if threadStopped(): + if thread_stopped(): # Kodi terminated break # Server thinks its a DOS attack, ('error 10053') @@ -228,7 +222,7 @@ class Artwork(): if dialog('yesno', "Image Texture Cache", lang(39251)): log.info("Resetting all cache data first") # Remove all existing textures first - path = translatePath("special://thumbnails/") + path = tryDecode(translatePath("special://thumbnails/")) if exists_dir(path): rmtree(path, ignore_errors=True) @@ -241,8 +235,7 @@ class Artwork(): for row in rows: tableName = row[0] if tableName != "version": - query = "DELETE FROM ?" - cursor.execute(query, (tableName,)) + cursor.execute("DELETE FROM %s" % tableName) connection.commit() connection.close() @@ -430,7 +423,7 @@ class Artwork(): path = translatePath("special://thumbnails/%s" % cachedurl) log.debug("Deleting cached thumbnail: %s" % path) if exists(path): - rmtree(path, ignore_errors=True) + rmtree(tryDecode(path), ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py new file mode 100644 index 00000000..64fc799c --- /dev/null +++ b/resources/lib/command_pipeline.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Thread +from Queue import Queue + +from xbmc import sleep + +from utils import window, thread_methods +import state + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +@thread_methods +class Monitor_Window(Thread): + """ + Monitors window('plex_command') for new entries that we need to take care + of, e.g. for new plays initiated on the Kodi side with addon paths. + + Possible values of window('plex_command'): + 'play_....': to start playback using playback_starter + + Adjusts state.py accordingly + """ + # Borg - multiple instances, shared state + def __init__(self, callback=None): + self.mgr = callback + self.playback_queue = Queue() + Thread.__init__(self) + + def run(self): + thread_stopped = self.thread_stopped + queue = self.playback_queue + log.info("----===## Starting Kodi_Play_Client ##===----") + while not thread_stopped(): + if window('plex_command'): + value = window('plex_command') + window('plex_command', clear=True) + if value.startswith('play_'): + queue.put(value) + + elif value == 'SUSPEND_LIBRARY_THREAD-True': + state.SUSPEND_LIBRARY_THREAD = True + elif value == 'SUSPEND_LIBRARY_THREAD-False': + state.SUSPEND_LIBRARY_THREAD = False + elif value == 'STOP_SYNC-True': + state.STOP_SYNC = True + elif value == 'STOP_SYNC-False': + state.STOP_SYNC = False + elif value == 'PMS_STATUS-Auth': + state.PMS_STATUS = 'Auth' + elif value == 'PMS_STATUS-401': + state.PMS_STATUS = '401' + elif value == 'SUSPEND_USER_CLIENT-True': + state.SUSPEND_USER_CLIENT = True + elif value == 'SUSPEND_USER_CLIENT-False': + state.SUSPEND_USER_CLIENT = False + elif value.startswith('PLEX_TOKEN-'): + state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None + elif value.startswith('PLEX_USERNAME-'): + state.PLEX_USERNAME = \ + value.replace('PLEX_USERNAME-', '') or None + else: + raise NotImplementedError('%s not implemented' % value) + else: + sleep(50) + # Put one last item into the queue to let playback_starter end + queue.put(None) + log.info("----===## Kodi_Play_Client stopped ##===----") diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 8d78d8ba..a30ab4d9 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -9,6 +9,8 @@ import xml.etree.ElementTree as etree from utils import settings, window, language as lang, dialog import clientinfo as client +import state + ############################################################################### # Disable annoying requests warnings @@ -40,20 +42,6 @@ class DownloadUtils(): def __init__(self): self.__dict__ = self._shared_state - def setUsername(self, username): - """ - Reserved for userclient only - """ - self.username = username - log.debug("Set username: %s" % username) - - def setUserId(self, userId): - """ - Reserved for userclient only - """ - self.userId = userId - log.debug("Set userId: %s" % userId) - def setServer(self, server): """ Reserved for userclient only @@ -108,8 +96,6 @@ class DownloadUtils(): # Set other stuff self.setServer(window('pms_server')) self.setToken(window('pms_token')) - self.setUserId(window('currUserId')) - self.setUsername(window('plex_username')) # Counters to declare PMS dead or unauthorized # Use window variables because start of movies will be called with a @@ -274,10 +260,11 @@ class DownloadUtils(): self.unauthorizedAttempts): log.warn('We seem to be truly unauthorized for PMS' ' %s ' % url) - if window('plex_serverStatus') not in ('401', 'Auth'): + if state.PMS_STATUS not in ('401', 'Auth'): # Tell userclient token has been revoked. log.debug('Setting PMS server status to ' 'unauthorized') + state.PMS_STATUS = '401' window('plex_serverStatus', value="401") dialog('notification', lang(29999), diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 8ab911cd..4745afea 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -12,7 +12,7 @@ from xbmc import sleep, executebuiltin, translatePath from xbmcgui import ListItem from utils import window, settings, language as lang, dialog, tryEncode, \ - CatchExceptions, JSONRPC, exists_dir + CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode import downloadutils from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ @@ -42,8 +42,8 @@ def chooseServer(): server = setup.PickPMS(showDialog=True) if server is None: log.error('We did not connect to a new PMS, aborting') - window('suspend_Userclient', clear=True) - window('suspend_LibraryThread', clear=True) + plex_command('SUSPEND_USER_CLIENT', 'False') + plex_command('SUSPEND_LIBRARY_THREAD', 'False') return log.info("User chose server %s" % server['name']) @@ -81,7 +81,8 @@ def togglePlexTV(): settings('plex_status', value="Not logged in to plex.tv") window('plex_token', clear=True) - window('plex_username', clear=True) + plex_command('PLEX_TOKEN', '') + plex_command('PLEX_USERNAME', '') else: log.info('Login to plex.tv') import initialsetup @@ -100,7 +101,7 @@ def resetAuth(): resp = dialog('yesno', heading="{plex}", line1=lang(39206)) if resp == 1: log.info("Reset login attempts.") - window('plex_serverStatus', value="Auth") + plex_command('PMS_STATUS', 'Auth') else: executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') @@ -146,7 +147,7 @@ def doMainListing(content_type=None): addDirectoryItem(lang(30173), "plugin://%s?mode=channels" % v.ADDON_ID) # Plex user switch - addDirectoryItem(lang(39200) + window('plex_username'), + addDirectoryItem(lang(39200), "plugin://%s?mode=switchuser" % v.ADDON_ID) # some extra entries for settings and stuff @@ -488,7 +489,6 @@ def getVideoFiles(plexId, params): except: log.error('Could not get file path for item %s' % plexId) return xbmcplugin.endOfDirectory(HANDLE) - path = tryEncode(path) # Assign network protocol if path.startswith('\\\\'): path = path.replace('\\\\', 'smb://') @@ -501,14 +501,14 @@ def getVideoFiles(plexId, params): if exists_dir(path): for root, dirs, files in walk(path): for directory in dirs: - item_path = join(root, directory) + item_path = tryEncode(join(root, directory)) li = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=item_path, listitem=li, isFolder=True) for file in files: - item_path = join(root, file) + item_path = tryEncode(join(root, file)) li = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=file, @@ -536,7 +536,8 @@ def getExtraFanArt(plexid, plexPath): # We need to store the images locally for this to work # because of the caching system in xbmc - fanartDir = translatePath("special://thumbnails/plex/%s/" % plexid) + fanartDir = tryDecode(translatePath( + "special://thumbnails/plex/%s/" % plexid)) if not exists_dir(fanartDir): # Download the images to the cache directory makedirs(fanartDir) @@ -549,19 +550,19 @@ def getExtraFanArt(plexid, plexPath): backdrops = api.getAllArtwork()['Backdrop'] for count, backdrop in enumerate(backdrops): # Same ordering as in artwork - fanartFile = join(fanartDir, "fanart%.3d.jpg" % count) + fanartFile = tryEncode(join(fanartDir, "fanart%.3d.jpg" % count)) li = ListItem("%.3d" % count, path=fanartFile) xbmcplugin.addDirectoryItem( handle=HANDLE, url=fanartFile, listitem=li) - copyfile(backdrop, fanartFile) + copyfile(backdrop, tryDecode(fanartFile)) else: log.info("Found cached backdrop.") # Use existing cached images for root, dirs, files in walk(fanartDir): for file in files: - fanartFile = join(root, file) + fanartFile = tryEncode(join(root, file)) li = ListItem(file, path=fanartFile) xbmcplugin.addDirectoryItem(handle=HANDLE, url=fanartFile, @@ -964,22 +965,19 @@ def enterPMS(): def __LogIn(): """ - Resets (clears) window properties to enable (re-)login: - suspend_Userclient - plex_runLibScan: set to 'full' to trigger lib sync + Resets (clears) window properties to enable (re-)login - suspend_LibraryThread is cleared in service.py if user was signed out! + SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed + out! """ window('plex_runLibScan', value='full') # Restart user client - window('suspend_Userclient', clear=True) + plex_command('SUSPEND_USER_CLIENT', 'False') def __LogOut(): """ - Finishes lib scans, logs out user. The following window attributes are set: - suspend_LibraryThread: 'true' - suspend_Userclient: 'true' + Finishes lib scans, logs out user. Returns True if successfully signed out, False otherwise """ @@ -991,7 +989,7 @@ def __LogOut(): time=3000, sound=False) # Pause library sync thread - window('suspend_LibraryThread', value='true') + plex_command('SUSPEND_LIBRARY_THREAD', 'True') # Wait max for 10 seconds for all lib scans to shutdown counter = 0 while window('plex_dbScan') == 'true': @@ -999,17 +997,18 @@ def __LogOut(): # Failed to reset PMS and plex.tv connects. Try to restart Kodi. dialog('ok', lang(29999), lang(39208)) # Resuming threads, just in case - window('suspend_LibraryThread', clear=True) + plex_command('SUSPEND_LIBRARY_THREAD', 'False') log.error("Could not stop library sync, aborting") return False counter += 1 sleep(50) log.debug("Successfully stopped library sync") - # Log out currently signed in user: - window('plex_serverStatus', value="401") - # Above method needs to have run its course! Hence wait counter = 0 + # Log out currently signed in user: + window('plex_serverStatus', value='401') + plex_command('PMS_STATUS', '401') + # Above method needs to have run its course! Hence wait while window('plex_serverStatus') == "401": if counter > 100: # 'Failed to reset PKC. Try to restart Kodi.' @@ -1019,5 +1018,5 @@ def __LogOut(): counter += 1 sleep(50) # Suspend the user client during procedure - window('suspend_Userclient', value='true') + plex_command('SUSPEND_USER_CLIENT', 'True') return True diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 54bd9940..f044dfad 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -13,6 +13,7 @@ from userclient import UserClient from PlexAPI import PlexAPI from PlexFunctions import GetMachineIdentifier, get_PMS_settings +import state ############################################################################### @@ -156,7 +157,7 @@ class InitialSetup(): verifySSL = False else: url = server['baseURL'] - verifySSL = None + verifySSL = True chk = self.plx.CheckConnection(url, token=server['accesstoken'], verifySSL=verifySSL) @@ -450,6 +451,7 @@ class InitialSetup(): yeslabel="Native (Direct Paths)"): log.debug("User opted to use direct paths.") settings('useDirectPaths', value="1") + state.DIRECT_PATHS = True # Are you on a system where you would like to replace paths # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) if dialog.yesno(heading=lang(29999), line1=lang(39033)): @@ -477,9 +479,6 @@ class InitialSetup(): if dialog.yesno(heading=lang(29999), line1=lang(39016)): log.debug("User opted to disable Plex music library.") settings('enableMusic', value="false") - else: - from utils import advancedsettings_tweaks - advancedsettings_tweaks() # Download additional art from FanArtTV if dialog.yesno(heading=lang(29999), line1=lang(39061)): @@ -496,12 +495,6 @@ class InitialSetup(): # Open Settings page now? You will need to restart! goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) if goToSettings: - window('plex_serverStatus', value="Stop") + state.PMS_STATUS = 'Stop' xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') - else: - # "Kodi will now restart to apply the changes" - dialog.ok(heading=lang(29999), line1=lang(33033)) - xbmc.executebuiltin('RestartApp') - # We should always restart to ensure e.g. Kodi settings for Music - # are in use! diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f0bac9a3..4cfa311a 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -16,6 +16,7 @@ import kodidb_functions as kodidb import PlexAPI from PlexFunctions import GetPlexMetadata import variables as v +import state ############################################################################### @@ -35,10 +36,7 @@ class Items(object): """ def __init__(self): - self.directpath = window('useDirectPaths') == 'true' - self.artwork = artwork.Artwork() - self.userid = window('currUserId') self.server = window('pms_server') def __enter__(self): @@ -268,8 +266,8 @@ class Movies(Items): break # GET THE FILE AND PATH ##### - doIndirect = not self.directpath - if self.directpath: + doIndirect = not state.DIRECT_PATHS + if state.DIRECT_PATHS: # Direct paths is set the Kodi way playurl = API.getFilePath(forceFirstMediaStream=True) if playurl is None: @@ -569,8 +567,8 @@ class TVShows(Items): studio = None # GET THE FILE AND PATH ##### - doIndirect = not self.directpath - if self.directpath: + doIndirect = not state.DIRECT_PATHS + if state.DIRECT_PATHS: # Direct paths is set the Kodi way playurl = API.getTVShowPath() if playurl is None: @@ -892,9 +890,9 @@ class TVShows(Items): seasonid = self.kodi_db.addSeason(showid, season) # GET THE FILE AND PATH ##### - doIndirect = not self.directpath + doIndirect = not state.DIRECT_PATHS playurl = API.getFilePath(forceFirstMediaStream=True) - if self.directpath: + if state.DIRECT_PATHS: # Direct paths is set the Kodi way if playurl is None: # Something went wrong, trying to use non-direct paths @@ -1116,7 +1114,7 @@ class TVShows(Items): self.kodi_db.addStreams(fileid, streams, runtime) # Process playstates self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed) - if not self.directpath and resume: + if not state.DIRECT_PATHS and resume: # Create additional entry for widgets. This is only required for plugin/episode. temppathid = self.kodi_db.getPath("plugin://plugin.video.plexkodiconnect/tvshows/") tempfileid = self.kodi_db.addFile(filename, temppathid) @@ -1634,8 +1632,8 @@ class Music(Items): mood = ' / '.join(moods) # GET THE FILE AND PATH ##### - doIndirect = not self.directpath - if self.directpath: + doIndirect = not state.DIRECT_PATHS + if state.DIRECT_PATHS: # Direct paths is set the Kodi way playurl = API.getFilePath(forceFirstMediaStream=True) if playurl is None: diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index 9e595aab..503dfcc2 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -1409,8 +1409,8 @@ class Kodidb_Functions(): ID = 'idEpisode' elif kodi_type == v.KODI_TYPE_SONG: ID = 'idSong' - query = '''UPDATE ? SET userrating = ? WHERE ? = ?''' - self.cursor.execute(query, (kodi_type, userrating, ID, kodi_id)) + query = '''UPDATE %s SET userrating = ? WHERE ? = ?''' % kodi_type + self.cursor.execute(query, (userrating, ID, kodi_id)) def create_entry_uniqueid(self): self.cursor.execute( diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 4e19721e..579c9033 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,6 +14,7 @@ from PlexFunctions import scrobble from kodidb_functions import get_kodiid_from_filename from PlexAPI import API from variables import REMAP_TYPE_FROM_PLEXTYPE +import state ############################################################################### @@ -137,6 +138,10 @@ class KodiMonitor(Monitor): sleep(5000) window('plex_runLibScan', value="full") + elif method == "System.OnQuit": + log.info('Kodi OnQuit detected - shutting down') + state.STOP_PKC = True + def PlayBackStart(self, data): """ Called whenever a playback is started diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 7f9fc074..1fdcb4e7 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -5,8 +5,7 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ - ThreadMethodsAdditionalSuspend +from utils import thread_methods import plexdb_functions as plexdb import itemtypes import variables as v @@ -18,9 +17,8 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', 'DB_SCAN'], + add_stops=['STOP_SYNC']) class Process_Fanart_Thread(Thread): """ Threaded download of additional fanart in the background @@ -55,14 +53,14 @@ class Process_Fanart_Thread(Thread): Do the work """ log.debug("---===### Starting FanartSync ###===---") - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended queue = self.queue - while not threadStopped(): + while not thread_stopped(): # In the event the server goes offline - while threadSuspended() or window('plex_dbScan'): + while thread_suspended(): # Set in service.py - if threadStopped(): + if thread_stopped(): # Abort was requested while waiting. We should exit log.info("---===### Stopped FanartSync ###===---") return diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index 5fd25859..ed3e187e 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -5,7 +5,7 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window +from utils import thread_methods, window from PlexFunctions import GetPlexMetadata, GetAllPlexChildren import sync_info @@ -16,8 +16,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. @@ -48,7 +47,7 @@ class Threaded_Get_Metadata(Thread): continue else: self.queue.task_done() - if self.threadStopped(): + if self.thread_stopped(): # Shutdown from outside requested; purge out_queue as well while not self.out_queue.empty(): # Still try because remaining item might have been taken @@ -79,8 +78,8 @@ class Threaded_Get_Metadata(Thread): # cache local variables because it's faster queue = self.queue out_queue = self.out_queue - threadStopped = self.threadStopped - while threadStopped() is False: + thread_stopped = self.thread_stopped + while thread_stopped() is False: # grabs Plex item from queue try: item = queue.get(block=False) diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index e6765b41..c4c599a4 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -5,19 +5,17 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods +from utils import thread_methods import itemtypes import sync_info ############################################################################### - log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) class Threaded_Process_Metadata(Thread): """ Not yet implemented for more than 1 thread - if ever. Only to be called by @@ -70,9 +68,9 @@ class Threaded_Process_Metadata(Thread): item_fct = getattr(itemtypes, self.item_type) # cache local variables because it's faster queue = self.queue - threadStopped = self.threadStopped + thread_stopped = self.thread_stopped with item_fct() as item_class: - while threadStopped() is False: + while thread_stopped() is False: # grabs item from queue try: item = queue.get(block=False) diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 3be8f70b..b2dd98d8 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -4,7 +4,7 @@ from threading import Thread, Lock from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang +from utils import thread_methods, language as lang ############################################################################### @@ -18,8 +18,7 @@ LOCK = Lock() ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. @@ -53,13 +52,13 @@ class Threaded_Show_Sync_Info(Thread): # cache local variables because it's faster total = self.total dialog = self.dialog - threadStopped = self.threadStopped + thread_stopped = self.thread_stopped dialog.create("%s %s: %s %s" % (lang(39714), self.item_type, str(total), lang(39715))) total = 2 * total totalProgress = 0 - while threadStopped() is False: + while thread_stopped() is False: with LOCK: get_progress = GET_METADATA_COUNT process_progress = PROCESS_METADATA_COUNT diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 60e1a0e9..6d4571ed 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -10,10 +10,9 @@ import xbmcgui from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ - ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ + thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ - advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes, \ - ThreadMethodsAdditionalSuspend, create_actor_db_index, dialog + tryDecode, deletePlaylists, deleteNodes, tryEncode import downloadutils import itemtypes import plexdb_functions as plexdb @@ -30,6 +29,7 @@ from library_sync.process_metadata import Threaded_Process_Metadata import library_sync.sync_info as sync_info from library_sync.fanart import Process_Fanart_Thread import music +import state ############################################################################### @@ -38,9 +38,8 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@thread_methods(add_stops=['STOP_SYNC'], + add_suspends=['SUSPEND_LIBRARY_THREAD']) class LibrarySync(Thread): """ """ @@ -72,7 +71,6 @@ class LibrarySync(Thread): self.enableMusic = settings('enableMusic') == "true" self.enableBackgroundSync = settings( 'enableBackgroundSync') == "true" - self.direct_paths = settings('useDirectPaths') == '1' # Init for replacing paths window('remapSMB', value=settings('remapSMB')) @@ -300,7 +298,7 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: - if self.threadStopped(): + if self.thread_stopped(): xbmc.executebuiltin('InhibitIdleShutdown(false)') setScreensaver(value=screensaver) return False @@ -323,7 +321,7 @@ class LibrarySync(Thread): window('plex_scancrashed', clear=True) elif window('plex_scancrashed') == '401': window('plex_scancrashed', clear=True) - if window('plex_serverStatus') not in ('401', 'Auth'): + if state.PMS_STATUS not in ('401', 'Auth'): # Plex server had too much and returned ERROR self.dialog.ok(lang(29999), lang(39409)) @@ -474,7 +472,7 @@ class LibrarySync(Thread): """ Compare the views to Plex """ - if self.direct_paths is True: + if state.DIRECT_PATHS is True and self.enableMusic is True: if music.set_excludefromscan_music_folders() is True: log.info('Detected new Music library - restarting now') # 'New Plex music library detected. Sorry, but we need to @@ -759,8 +757,8 @@ class LibrarySync(Thread): for thread in threads: # Threads might already have quit by themselves (e.g. Kodi exit) try: - thread.stopThread() - except: + thread.stop_thread() + except AttributeError: pass log.debug("Stop sent to all threads") # Wait till threads are indeed dead @@ -805,7 +803,7 @@ class LibrarySync(Thread): # PROCESS MOVIES ##### self.updatelist = [] for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False # Get items per view viewId = view['id'] @@ -826,7 +824,7 @@ class LibrarySync(Thread): log.info("Processed view") # Update viewstate for EVERY item for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -898,7 +896,7 @@ class LibrarySync(Thread): # PROCESS TV Shows ##### self.updatelist = [] for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False # Get items per view viewId = view['id'] @@ -927,7 +925,7 @@ class LibrarySync(Thread): # PROCESS TV Seasons ##### # Cycle through tv shows for tvShowId in allPlexTvShowsId: - if self.threadStopped(): + if self.thread_stopped(): return False # Grab all seasons to tvshow from PMS seasons = GetAllPlexChildren(tvShowId) @@ -952,7 +950,7 @@ class LibrarySync(Thread): # PROCESS TV Episodes ##### # Cycle through tv shows for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False # Grab all episodes to tvshow from PMS episodes = GetAllPlexLeaves(view['id']) @@ -987,7 +985,7 @@ class LibrarySync(Thread): # Update viewstate: for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1024,7 +1022,7 @@ class LibrarySync(Thread): for kind in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_SONG): - if self.threadStopped(): + if self.thread_stopped(): return False log.debug("Start processing music %s" % kind) self.allKodiElementsId = {} @@ -1041,7 +1039,7 @@ class LibrarySync(Thread): # Update viewstate for EVERY item for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1066,7 +1064,7 @@ class LibrarySync(Thread): except ValueError: pass for view in views: - if self.threadStopped(): + if self.thread_stopped(): return False # Get items per view itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) @@ -1172,7 +1170,7 @@ class LibrarySync(Thread): now = getUnixTimestamp() deleteListe = [] for i, item in enumerate(self.itemsToProcess): - if self.threadStopped(): + if self.thread_stopped(): # Chances are that Kodi gets shut down break if item['state'] == 9: @@ -1277,8 +1275,8 @@ class LibrarySync(Thread): # movie or episode) continue typus = int(item.get('type', 0)) - state = int(item.get('state', 0)) - if state == 9 or (typus in (1, 4, 10) and state == 5): + status = int(item.get('state', 0)) + if status == 9 or (typus in (1, 4, 10) and status == 5): # Only process deleted items OR movies, episodes, tracks/songs plex_id = str(item.get('itemID', '0')) if plex_id == '0': @@ -1286,7 +1284,7 @@ class LibrarySync(Thread): continue try: if (now - self.just_processed[plex_id] < - self.ignore_just_processed and state != 9): + self.ignore_just_processed and status != 9): log.debug('We just processed %s: ignoring' % plex_id) continue except KeyError: @@ -1299,7 +1297,7 @@ class LibrarySync(Thread): else: # Haven't added this element to the queue yet self.itemsToProcess.append({ - 'state': state, + 'state': status, 'type': typus, 'ratingKey': plex_id, 'timestamp': getUnixTimestamp(), @@ -1315,8 +1313,8 @@ class LibrarySync(Thread): with plexdb.Get_Plex_DB() as plex_db: for item in data: # Drop buffering messages immediately - state = item.get('state') - if state == 'buffering': + status = item.get('state') + if status == 'buffering': continue ratingKey = item.get('ratingKey') kodiInfo = plex_db.getItem_byId(ratingKey) @@ -1335,8 +1333,7 @@ class LibrarySync(Thread): } else: # PMS is ours - get all current sessions - self.sessionKeys = GetPMSStatus( - window('plex_token')) + self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) log.debug('Updated current sessions. They are: %s' % self.sessionKeys) if sessionKey not in self.sessionKeys: @@ -1349,20 +1346,19 @@ class LibrarySync(Thread): # Identify the user - same one as signed on with PKC? Skip # update if neither session's username nor userid match # (Owner sometime's returns id '1', not always) - if (window('plex_token') == '' and - currSess['userId'] == '1'): + if (not state.PLEX_TOKEN and currSess['userId'] == '1'): # PKC not signed in to plex.tv. Plus owner of PMS is # playing (the '1'). # Hence must be us (since several users require plex.tv # token for PKC) pass - elif not (currSess['userId'] == window('currUserId') + elif not (currSess['userId'] == state.PLEX_USER_ID or - currSess['username'] == window('plex_username')): + currSess['username'] == state.PLEX_USERNAME): log.debug('Our username %s, userid %s did not match ' 'the session username %s with userid %s' - % (window('plex_username'), - window('currUserId'), + % (state.PLEX_USERNAME, + state.PLEX_USER_ID, currSess['username'], currSess['userId'])) continue @@ -1394,14 +1390,14 @@ class LibrarySync(Thread): 'file_id': kodiInfo[1], 'kodi_type': kodiInfo[4], 'viewOffset': resume, - 'state': state, + 'state': status, 'duration': currSess['duration'], 'viewCount': currSess['viewCount'], 'lastViewedAt': DateToKodi(getUnixTimestamp()) }) log.debug('Update playstate for user %s with id %s: %s' - % (window('plex_username'), - window('currUserId'), + % (state.PLEX_USERNAME, + state.PLEX_USER_ID, items[-1])) # Now tell Kodi where we are for item in items: @@ -1433,6 +1429,7 @@ class LibrarySync(Thread): try: self.run_internal() except Exception as e: + state.DB_SCAN = False window('plex_dbScan', clear=True) log.error('LibrarySync thread crashed. Error message: %s' % e) import traceback @@ -1443,8 +1440,8 @@ class LibrarySync(Thread): def run_internal(self): # Re-assign handles to have faster calls - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended installSyncDone = self.installSyncDone enableBackgroundSync = self.enableBackgroundSync fullSync = self.fullSync @@ -1470,18 +1467,15 @@ class LibrarySync(Thread): # Ensure that DBs exist if called for very first time self.initializeDBs() - if self.enableMusic: - advancedsettings_tweaks() - if settings('FanartTV') == 'true': self.fanartthread.start() - while not threadStopped(): + while not thread_stopped(): # In the event the server goes offline - while threadSuspended(): + while thread_suspended(): # Set in service.py - if threadStopped(): + if thread_stopped(): # Abort was requested while waiting. We should exit log.info("###===--- LibrarySync Stopped ---===###") return @@ -1513,7 +1507,7 @@ class LibrarySync(Thread): # Also runs when first installed # Verify the video database can be found videoDb = v.DB_VIDEO_PATH - if not exists(videoDb): + if not exists(tryEncode(videoDb)): # Database does not exists log.error("The current Kodi version is incompatible " "to know which Kodi versions are supported.") @@ -1523,6 +1517,7 @@ class LibrarySync(Thread): self.dialog.ok(heading=lang(29999), line1=lang(39403)) break # Run start up sync + state.DB_SCAN = True window('plex_dbScan', value="true") log.info("Db version: %s" % settings('dbCreatedWithVersion')) lastTimeSync = getUnixTimestamp() @@ -1547,6 +1542,7 @@ class LibrarySync(Thread): log.info("Initial start-up full sync starting") librarySync = fullSync() window('plex_dbScan', clear=True) + state.DB_SCAN = False if librarySync: log.info("Initial start-up full sync successful") startupComplete = True @@ -1565,23 +1561,26 @@ class LibrarySync(Thread): break # Currently no db scan, so we can start a new scan - elif window('plex_dbScan') != "true": + elif state.DB_SCAN is False: # Full scan was requested from somewhere else, e.g. userclient if window('plex_runLibScan') in ("full", "repair"): log.info('Full library scan requested, starting') window('plex_dbScan', value="true") + state.DB_SCAN = True if window('plex_runLibScan') == "full": fullSync() elif window('plex_runLibScan') == "repair": fullSync(repair=True) window('plex_runLibScan', clear=True) window('plex_dbScan', clear=True) + state.DB_SCAN = False # Full library sync finished self.showKodiNote(lang(39407), forced=False) # Reset views was requested from somewhere else elif window('plex_runLibScan') == "views": log.info('Refresh playlist and nodes requested, starting') window('plex_dbScan', value="true") + state.DB_SCAN = True window('plex_runLibScan', clear=True) # First remove playlists @@ -1602,6 +1601,7 @@ class LibrarySync(Thread): forced=True, icon="error") window('plex_dbScan', clear=True) + state.DB_SCAN = False elif window('plex_runLibScan') == 'fanart': window('plex_runLibScan', clear=True) # Only look for missing fanart (No) @@ -1613,31 +1613,37 @@ class LibrarySync(Thread): yeslabel=lang(39225))) elif window('plex_runLibScan') == 'del_textures': window('plex_runLibScan', clear=True) + state.DB_SCAN = True window('plex_dbScan', value="true") import artwork artwork.Artwork().fullTextureCacheSync() window('plex_dbScan', clear=True) + state.DB_SCAN = False else: now = getUnixTimestamp() if (now - lastSync > fullSyncInterval and not xbmcplayer.isPlaying()): lastSync = now log.info('Doing scheduled full library scan') + state.DB_SCAN = True window('plex_dbScan', value="true") - if fullSync() is False and not threadStopped(): + if fullSync() is False and not thread_stopped(): log.error('Could not finish scheduled full sync') self.showKodiNote(lang(39410), forced=True, icon='error') window('plex_dbScan', clear=True) + state.DB_SCAN = False # Full library sync finished self.showKodiNote(lang(39407), forced=False) elif now - lastTimeSync > oneDay: lastTimeSync = now log.info('Starting daily time sync') + state.DB_SCAN = True window('plex_dbScan', value="true") self.syncPMStime() window('plex_dbScan', clear=True) + state.DB_SCAN = False elif enableBackgroundSync: # Check back whether we should process something # Only do this once every while (otherwise, potentially diff --git a/resources/lib/monitor_kodi_play.py b/resources/lib/monitor_kodi_play.py deleted file mode 100644 index b7968eeb..00000000 --- a/resources/lib/monitor_kodi_play.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -import logging -from threading import Thread -from Queue import Queue - -from xbmc import sleep - -from utils import window, ThreadMethods - -############################################################################### -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -@ThreadMethods -class Monitor_Kodi_Play(Thread): - """ - Monitors for new plays initiated on the Kodi side with addon paths. - Immediately throws them into a queue to be processed by playback_starter - """ - # Borg - multiple instances, shared state - def __init__(self, callback=None): - self.mgr = callback - self.playback_queue = Queue() - Thread.__init__(self) - - def run(self): - threadStopped = self.threadStopped - queue = self.playback_queue - log.info("----===## Starting Kodi_Play_Client ##===----") - while not threadStopped(): - if window('plex_play_new_item'): - queue.put(window('plex_play_new_item')) - window('plex_play_new_item', clear=True) - else: - sleep(20) - # Put one last item into the queue to let playback_starter end - queue.put(None) - log.info("----===## Kodi_Play_Client stopped ##===----") diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index b6b14c18..f0ac27f5 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -17,6 +17,7 @@ import variables as v from downloadutils import DownloadUtils from PKC_listitem import convert_PKC_to_listitem import plexdb_functions as plexdb +import state ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -39,7 +40,7 @@ class Playback_Starter(Thread): """ log.info("Process_play called with plex_id %s, kodi_id %s" % (plex_id, kodi_id)) - if window('plex_authenticated') != "true": + if not state.AUTHENTICATED: log.error('Not yet authenticated for PMS, abort starting playback') # Todo: Warn user with dialog return @@ -152,12 +153,12 @@ class Playback_Starter(Thread): pickle_me(result) def run(self): - queue = self.mgr.monitor_kodi_play.playback_queue + queue = self.mgr.command_pipeline.playback_queue log.info("----===## Starting Playback_Starter ##===----") while True: item = queue.get() if item is None: - # Need to shutdown - initiated by monitor_kodi_play + # Need to shutdown - initiated by command_pipeline break else: self.triage(item) diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 00fb5551..1ca853d4 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -22,6 +22,7 @@ from playlist_func import add_item_to_kodi_playlist, \ from pickler import Playback_Successful from plexdb_functions import Get_Plex_DB import variables as v +import state ############################################################################### @@ -187,7 +188,7 @@ class PlaybackUtils(): kodi_type) elif contextmenu_play: - if window('useDirectPaths') == 'true': + if state.DIRECT_PATHS: # Cannot add via JSON with full metadata because then we # Would be using the direct path log.debug("Adding contextmenu item for direct paths") diff --git a/resources/lib/player.py b/resources/lib/player.py index b5f2389f..5b465ae5 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -13,6 +13,7 @@ import downloadutils import plexdb_functions as plexdb import kodidb_functions as kodidb import variables as v +import state ############################################################################### @@ -308,6 +309,9 @@ class Player(xbmc.Player): 'plex_playbackProps', 'plex_forcetranscode'): window(item, clear=True) + # We might have saved a transient token from a user flinging media via + # Companion + state.PLEX_TRANSIENT_TOKEN = None log.debug("Cleared playlist properties.") def onPlayBackEnded(self): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index a496574b..6df21b0d 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,7 +5,7 @@ from threading import RLock, Thread from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend +from utils import window, thread_methods import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API @@ -21,8 +21,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### -@ThreadMethodsAdditionalSuspend('plex_serverStatus') -@ThreadMethods +@thread_methods(add_suspends=['PMS_STATUS']) class Playqueue(Thread): """ Monitors Kodi's playqueues for changes on the Kodi side @@ -147,20 +146,24 @@ class Playqueue(Thread): index = list(range(0, len(old))) log.debug('Comparing new Kodi playqueue %s with our play queue %s' % (new, old)) + if self.thread_stopped(): + # Chances are that we got an empty Kodi playlist due to + # Kodi exit + return for i, new_item in enumerate(new): if (new_item['file'].startswith('plugin://') and not new_item['file'].startswith(PLUGIN)): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.threadStopped(): - # Chances are that we got an empty Kodi playlist due to - # Kodi exit - return - if (old_item['file'].startswith('plugin://') and - not old_item['file'].startswith(PLUGIN)): - # Ignore media by other addons - continue + try: + if (old_item.file.startswith('plugin://') and + not old_item['file'].startswith(PLUGIN)): + # Ignore media by other addons + continue + except (TypeError, AttributeError): + # were not passed a filename; ignore + pass if new_item.get('id') is None: identical = old_item.file == new_item['file'] else: @@ -193,8 +196,8 @@ class Playqueue(Thread): log.debug('Done comparing playqueues') def run(self): - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended log.info("----===## Starting PlayQueue client ##===----") # Initialize the playqueues, if Kodi already got items in them for playqueue in self.playqueues: @@ -203,9 +206,9 @@ class Playqueue(Thread): PL.init_Plex_playlist(playqueue, kodi_item=item) else: PL.add_item_to_PMS_playlist(playqueue, i, kodi_item=item) - while not threadStopped(): - while threadSuspended(): - if threadStopped(): + while not thread_stopped(): + while thread_suspended(): + if thread_stopped(): break sleep(1000) with lock: diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 6f6415de..47320379 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -24,8 +24,6 @@ class PlayUtils(): self.API = PlexAPI.API(item) self.doUtils = DownloadUtils().downloadUrl - self.userid = window('currUserId') - self.server = window('pms_server') self.machineIdentifier = window('plex_machineIdentifier') def getPlayUrl(self, partNumber=None): @@ -335,7 +333,8 @@ class PlayUtils(): # We don't know the language - no need to download else: path = self.API.addPlexCredentialsToUrl( - "%s%s" % (self.server, stream.attrib['key'])) + "%s%s" % (window('pms_server'), + stream.attrib['key'])) downloadable_streams.append(index) download_subs.append(tryEncode(path)) else: diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index a3294705..c07e9c00 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -163,7 +163,7 @@ class MyHandler(BaseHTTPRequestHandler): else: # Throw it to companion.py process_command(request_path, params, self.server.queue) - self.response(getOKMsg(), js.getPlexHeaders()) + self.response('', js.getPlexHeaders()) subMgr.notify() except: log.error('Error encountered. Traceback:') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b8f4e20b..0f8c56e4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -3,8 +3,10 @@ import re import threading import downloadutils +from clientinfo import getXArgsDeviceInfo from utils import window import PlexFunctions as pf +import state from functions import * ############################################################################### @@ -68,19 +70,16 @@ class SubscriptionManager: info = self.getPlayerProperties(playerid) # save this info off so the server update can use it too self.playerprops[playerid] = info; - state = info['state'] + status = info['state'] time = info['time'] else: - state = "stopped" + status = "stopped" time = 0 - ret = "\n"+' highestjump: - # Adjust with the latest number, if it's greater - highestjump = jump - - if throttled: - # We needed to adjust the number of item requested. - # keep increasing until the connection times out again - # to find the highest value - increment = int(jump*0.33) - if not increment: # Incase the increment is 0 - increment = 10 - - jump += increment - log.info("Increase jump limit to: %s" % jump) - return items - - def getViews(self, mediatype="", root=False, sortedlist=False): - # Build a list of user views - views = [] - mediatype = mediatype.lower() - - if not root: - url = "{server}/emby/Users/{UserId}/Views?format=json" - else: # Views ungrouped - url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" - - result = self.doUtils(url) - try: - items = result['Items'] - except TypeError: - log.debug("Error retrieving views for type: %s" % mediatype) - else: - for item in items: - - item['Name'] = item['Name'] - if item['Type'] == "Channel": - # Filter view types - continue - - # 3/4/2016 OriginalCollectionType is added - itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) - - # 11/29/2015 Remove this once OriginalCollectionType is added to stable server. - # Assumed missing is mixed then. - '''if itemtype is None: - url = "{server}/emby/Library/MediaFolders?format=json" - result = self.doUtils(url) - - for folder in result['Items']: - if item['Id'] == folder['Id']: - itemtype = folder.get('CollectionType', "mixed")''' - - if item['Name'] not in ('Collections', 'Trailers'): - - if sortedlist: - views.append({ - - 'name': item['Name'], - 'type': itemtype, - 'id': item['Id'] - }) - - elif (itemtype == mediatype or - (itemtype == "mixed" and mediatype in ("movies", "tvshows"))): - - views.append({ - - 'name': item['Name'], - 'type': itemtype, - 'id': item['Id'] - }) - - return views - - def verifyView(self, parentid, itemid): - - belongs = False - params = { - - 'ParentId': parentid, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'Ids': itemid - } - result = self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) - try: - total = result['TotalRecordCount'] - except TypeError: - # Something happened to the connection - pass - else: - if total: - belongs = True - - return belongs - - def getMovies(self, parentId, basic=False, dialog=None): - - return self.getSection(parentId, "Movie", basic=basic, dialog=dialog) - - def getBoxset(self, dialog=None): - - return self.getSection(None, "BoxSet", dialog=dialog) - - def getMovies_byBoxset(self, boxsetid): - return self.getSection(boxsetid, "Movie") - - def getMusicVideos(self, parentId, basic=False, dialog=None): - - return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) - - def getHomeVideos(self, parentId): - - return self.getSection(parentId, "Video") - - def getShows(self, parentId, basic=False, dialog=None): - - return self.getSection(parentId, "Series", basic=basic, dialog=dialog) - - def getSeasons(self, showId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - params = { - - 'IsVirtualUnaired': False, - 'Fields': "Etag" - } - url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId - result = self.doUtils(url, parameters=params) - if result: - items = result - - return items - - def getEpisodes(self, parentId, basic=False, dialog=None): - - return self.getSection(parentId, "Episode", basic=basic, dialog=dialog) - - def getEpisodesbyShow(self, showId): - - return self.getSection(showId, "Episode") - - def getEpisodesbySeason(self, seasonId): - - return self.getSection(seasonId, "Episode") - - def getArtists(self, dialog=None): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - # Get total number of items - url = "{server}/emby/Artists?UserId={UserId}&format=json" - params = { - - 'Recursive': True, - 'Limit': 1 - } - result = self.doUtils(url, parameters=params) - try: - total = result['TotalRecordCount'] - items['TotalRecordCount'] = total - - except TypeError: # Failed to retrieve - log.debug("%s:%s Failed to retrieve the server response." % (url, params)) - - else: - index = 1 - jump = self.limitIndex - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'Recursive': True, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': "SortName", - 'SortOrder': "Ascending", - 'Fields': ( - - "Etag,Genres,SortName,Studios,Writer,ProductionYear," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," - "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" - ) - } - result = self.doUtils(url, parameters=params) - items['Items'].extend(result['Items']) - - index += jump - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - return items - - def getAlbums(self, basic=False, dialog=None): - - return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) - - def getAlbumsbyArtist(self, artistId): - - return self.getSection(artistId, "MusicAlbum", sortby="DateCreated") - - def getSongs(self, basic=False, dialog=None): - - return self.getSection(None, "Audio", basic=basic, dialog=dialog) - - def getSongsbyAlbum(self, albumId): - - return self.getSection(albumId, "Audio") - - def getAdditionalParts(self, itemId): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId - result = self.doUtils(url) - if result: - items = result - - return items - - def sortby_mediatype(self, itemids): - - sorted_items = {} - - # Sort items - items = self.getFullItems(itemids) - for item in items: - - mediatype = item.get('Type') - if mediatype: - sorted_items.setdefault(mediatype, []).append(item) - - return sorted_items - - def updateUserRating(self, itemid, favourite=None): - # Updates the user rating to Emby - doUtils = self.doUtils - - if favourite: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, action_type="POST") - elif not favourite: - url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid - doUtils(url, action_type="DELETE") - else: - log.info("Error processing user rating.") - - log.info("Update user rating to emby for itemid: %s | favourite: %s" % (itemid, favourite)) - - def refreshItem(self, itemid): - - url = "{server}/emby/Items/%s/Refresh?format=json" % itemid - params = { - - 'Recursive': True, - 'ImageRefreshMode': "FullRefresh", - 'MetadataRefreshMode': "FullRefresh", - 'ReplaceAllImages': False, - 'ReplaceAllMetadata': True - - } - self.doUtils(url, postBody=params, action_type="POST") - - def deleteItem(self, itemid): - - url = "{server}/emby/Items/%s?format=json" % itemid - self.doUtils(url, action_type="DELETE") \ No newline at end of file diff --git a/resources/lib/state.py b/resources/lib/state.py new file mode 100644 index 00000000..865f4556 --- /dev/null +++ b/resources/lib/state.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# THREAD SAFE + +# Quit PKC +STOP_PKC = False + + +# Usually triggered by another Python instance - will have to be set (by +# polling window) through e.g. librarysync thread +SUSPEND_LIBRARY_THREAD = False +# Set if user decided to cancel sync +STOP_SYNC = False +# Set if a Plex-Kodi DB sync is being done - along with +# window('plex_dbScan') set to 'true' +DB_SCAN = False +# Plex Media Server Status - along with window('plex_serverStatus') +PMS_STATUS = False +# When the userclient needs to wait +SUSPEND_USER_CLIENT = False +# Plex home user? Then "False". Along with window('plex_restricteduser') +RESTRICTED_USER = False +# Direct Paths (True) or Addon Paths (False)? Along with +# window('useDirectPaths') +DIRECT_PATHS = False + +# Along with window('plex_authenticated') +AUTHENTICATED = False +# plex.tv username +PLEX_USERNAME = None +# Token for that user for plex.tv +PLEX_TOKEN = None +# Plex ID of that user (e.g. for plex.tv) as a STRING +PLEX_USER_ID = None +# Token passed along, e.g. if playback initiated by Plex Companion. Might be +# another user playing something! Token identifies user +PLEX_TRANSIENT_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 04f97d16..587bf1c7 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -10,12 +10,12 @@ import xbmcaddon from xbmcvfs import exists -from utils import window, settings, language as lang, ThreadMethods, \ - ThreadMethodsAdditionalSuspend +from utils import window, settings, language as lang, thread_methods import downloadutils import PlexAPI from PlexFunctions import GetMachineIdentifier +import state ############################################################################### @@ -24,8 +24,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_Userclient') -@ThreadMethods +@thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) class UserClient(threading.Thread): # Borg - multiple instances, shared state @@ -40,7 +39,6 @@ class UserClient(threading.Thread): self.retry = 0 self.currUser = None - self.currUserId = None self.currServer = None self.currToken = None self.HasAccess = True @@ -118,37 +116,19 @@ class UserClient(threading.Thread): def hasAccess(self): # Plex: always return True for now return True - # hasAccess is verified in service.py - url = "{server}/emby/Users?format=json" - result = self.doUtils.downloadUrl(url) - - if result is False: - # Access is restricted, set in downloadutils.py via exception - log.info("Access is restricted.") - self.HasAccess = False - - elif window('plex_online') != "true": - # Server connection failed - pass - - elif window('plex_serverStatus') == "restricted": - log.info("Access is granted.") - self.HasAccess = True - window('plex_serverStatus', clear=True) - xbmcgui.Dialog().notification(lang(29999), - lang(33007)) def loadCurrUser(self, username, userId, usertoken, authenticated=False): log.debug('Loading current user') doUtils = self.doUtils - self.currUserId = userId self.currToken = usertoken self.currServer = self.getServer() self.ssl = self.getSSLverify() self.sslcert = self.getSSL() if authenticated is False: + if self.currServer is None: + return False log.debug('Testing validity of current token') res = PlexAPI.PlexAPI().CheckConnection(self.currServer, token=self.currToken, @@ -164,21 +144,27 @@ class UserClient(threading.Thread): return False # Set to windows property - window('currUserId', value=userId) - window('plex_username', value=username) + state.PLEX_USER_ID = userId or None + state.PLEX_USERNAME = username # This is the token for the current PMS (might also be '') window('pms_token', value=self.currToken) # This is the token for plex.tv for the current user # Is only '' if user is not signed in to plex.tv window('plex_token', value=settings('plexToken')) + state.PLEX_TOKEN = settings('plexToken') or None window('plex_restricteduser', value=settings('plex_restricteduser')) + state.RESTRICTED_USER = True \ + if settings('plex_restricteduser') == 'true' else False window('pms_server', value=self.currServer) window('plex_machineIdentifier', value=self.machineIdentifier) window('plex_servername', value=self.servername) window('plex_authenticated', value='true') + state.AUTHENTICATED = True window('useDirectPaths', value='true' if settings('useDirectPaths') == "1" else 'false') + state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \ + else False window('plex_force_transcode_pix', value='true' if settings('force_transcode_pix') == "1" else 'false') @@ -202,7 +188,7 @@ class UserClient(threading.Thread): # Give attempts at entering password / selecting user if self.retry >= 2: log.error("Too many retries to login.") - window('plex_serverStatus', value="Stop") + state.PMS_STATUS = 'Stop' dialog.ok(lang(33001), lang(39023)) xbmc.executebuiltin( @@ -283,14 +269,17 @@ class UserClient(threading.Thread): self.doUtils.stopSession() window('plex_authenticated', clear=True) + state.AUTHENTICATED = False window('pms_token', clear=True) + state.PLEX_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) window('plex_servername', clear=True) - window('currUserId', clear=True) - window('plex_username', clear=True) + state.PLEX_USER_ID = None + state.PLEX_USERNAME = None window('plex_restricteduser', clear=True) + state.RESTRICTED_USER = False settings('username', value='') settings('userid', value='') @@ -298,44 +287,42 @@ class UserClient(threading.Thread): # Reset token in downloads self.doUtils.setToken('') - self.doUtils.setUserId('') - self.doUtils.setUsername('') self.currToken = None self.auth = True self.currUser = None - self.currUserId = None self.retry = 0 def run(self): log.info("----===## Starting UserClient ##===----") - while not self.threadStopped(): - while self.threadSuspended(): - if self.threadStopped(): + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended + while not thread_stopped(): + while thread_suspended(): + if thread_stopped(): break xbmc.sleep(1000) - status = window('plex_serverStatus') - - if status == "Stop": + if state.PMS_STATUS == "Stop": xbmc.sleep(500) continue # Verify the connection status to server - elif status == "restricted": + elif state.PMS_STATUS == "restricted": # Parental control is restricting access self.HasAccess = False - elif status == "401": + elif state.PMS_STATUS == "401": # Unauthorized access, revoke token - window('plex_serverStatus', value="Auth") + state.PMS_STATUS = 'Auth' + window('plex_serverStatus', value='Auth') self.resetClient() - xbmc.sleep(2000) + xbmc.sleep(3000) if self.auth and (self.currUser is None): # Try to authenticate user - if not status or status == "Auth": + if not state.PMS_STATUS or state.PMS_STATUS == "Auth": # Set auth flag because we no longer need # to authenticate the user self.auth = False @@ -343,10 +330,11 @@ class UserClient(threading.Thread): # Successfully authenticated and loaded a user log.info("Successfully authenticated!") log.info("Current user: %s" % self.currUser) - log.info("Current userId: %s" % self.currUserId) + log.info("Current userId: %s" % state.PLEX_USER_ID) self.retry = 0 - window('suspend_LibraryThread', clear=True) + state.SUSPEND_LIBRARY_THREAD = False window('plex_serverStatus', clear=True) + state.PMS_STATUS = False if not self.auth and (self.currUser is None): # Loop if no server found @@ -354,7 +342,7 @@ class UserClient(threading.Thread): # The status Stop is for when user cancelled password dialog. # Or retried too many times - if server and status != "Stop": + if server and state.PMS_STATUS != "Stop": # Only if there's information found to login log.debug("Server found: %s" % server) self.auth = True @@ -362,5 +350,4 @@ class UserClient(threading.Thread): # Minimize CPU load xbmc.sleep(100) - self.doUtils.stopSession() log.info("##===---- UserClient Stopped ----===##") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index e94f7555..0a64942b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -11,7 +11,7 @@ from StringIO import StringIO from time import localtime, strftime, strptime from unicodedata import normalize import xml.etree.ElementTree as etree -from functools import wraps +from functools import wraps, partial from calendar import timegm from os.path import join from os import remove, walk, makedirs @@ -25,6 +25,7 @@ from xbmcvfs import exists, delete from variables import DB_VIDEO_PATH, DB_MUSIC_PATH, DB_TEXTURE_PATH, \ DB_PLEX_PATH, KODI_PROFILE, KODIVERSION +import state ############################################################################### @@ -76,6 +77,19 @@ def pickl_window(property, value=None, clear=False, windowid=10000): return win.getProperty(property) +def plex_command(key, value): + """ + Used to funnel states between different Python instances. NOT really thread + safe - let's hope the Kodi user can't click fast enough + + key: state.py variable + value: either 'True' or 'False' + """ + while window('plex_command'): + xbmc.sleep(5) + window('plex_command', value='%s-%s' % (key, value)) + + def settings(setting, value=None): """ Get or add addon setting. Returns unicode @@ -97,12 +111,12 @@ def exists_dir(path): Safe way to check whether the directory path exists already (broken in Kodi <17) - Feed with encoded string + Feed with encoded string or unicode """ if KODIVERSION >= 17: - answ = exists(path) + answ = exists(tryEncode(path)) else: - dummyfile = join(path, 'dummyfile.txt') + dummyfile = join(tryDecode(path), 'dummyfile.txt') try: with open(dummyfile, 'w') as f: f.write('text') @@ -111,7 +125,7 @@ def exists_dir(path): answ = 0 else: # Folder exists. Delete file again. - delete(dummyfile) + delete(tryEncode(dummyfile)) answ = 1 return answ @@ -319,7 +333,7 @@ def reset(): return # first stop any db sync - window('plex_shouldStop', value="true") + plex_command('STOP_SYNC', 'True') count = 10 while window('plex_dbScan') == "true": log.debug("Sync is running, will retry: %s..." % count) @@ -347,7 +361,7 @@ def reset(): for row in rows: tablename = row[0] if tablename != "version": - cursor.execute("DELETE FROM ?", (tablename,)) + cursor.execute("DELETE FROM %s" % tablename) connection.commit() cursor.close() @@ -360,7 +374,7 @@ def reset(): for row in rows: tablename = row[0] if tablename != "version": - cursor.execute("DELETE FROM ?", (tablename, )) + cursor.execute("DELETE FROM %s" % tablename) connection.commit() cursor.close() @@ -373,7 +387,7 @@ def reset(): for row in rows: tablename = row[0] if tablename != "version": - cursor.execute("DELETE FROM ?", (tablename, )) + cursor.execute("DELETE FROM %s" % tablename) cursor.execute('DROP table IF EXISTS plex') cursor.execute('DROP table IF EXISTS view') connection.commit() @@ -387,7 +401,7 @@ def reset(): # Remove all existing textures first path = xbmc.translatePath("special://thumbnails/") if exists(path): - rmtree(path, ignore_errors=True) + rmtree(tryDecode(path), ignore_errors=True) # remove all existing data from texture DB connection = kodiSQL('texture') cursor = connection.cursor() @@ -397,7 +411,7 @@ def reset(): for row in rows: tableName = row[0] if(tableName != "version"): - cursor.execute("DELETE FROM ?", (tableName, )) + cursor.execute("DELETE FROM %s" % tableName) connection.commit() cursor.close() @@ -411,7 +425,7 @@ def reset(): line1=language(39603)): # Delete the settings addon = xbmcaddon.Addon() - addondir = xbmc.translatePath(addon.getAddonInfo('profile')) + addondir = tryDecode(xbmc.translatePath(addon.getAddonInfo('profile'))) dataPath = "%ssettings.xml" % addondir log.info("Deleting: settings.xml") remove(dataPath) @@ -522,9 +536,16 @@ def guisettingsXML(): try: xmlparse = etree.parse(xmlpath) - except: + except IOError: # Document is blank or missing root = etree.Element('settings') + except etree.ParseError: + log.error('Error parsing %s' % xmlpath) + # "Kodi cannot parse {0}. PKC will not function correctly. Please visit + # {1} and correct your file!" + dialog('ok', language(29999), language(39716).format( + 'guisettings.xml', 'http://kodi.wiki/view/userdata')) + return else: root = xmlparse.getroot() return root @@ -606,6 +627,14 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None, return None, None # Create topmost xml entry tree = etree.ElementTree(element=etree.Element('advancedsettings')) + except etree.ParseError: + log.error('Error parsing %s' % path) + # "Kodi cannot parse {0}. PKC will not function correctly. Please visit + # {1} and correct your file!" + dialog('ok', language(29999), language(39716).format( + 'advancedsettings.xml', + 'http://kodi.wiki/view/Advancedsettings.xml')) + return None, None root = tree.getroot() element = root @@ -632,17 +661,6 @@ def advancedsettings_xml(node_list, new_value=None, attrib=None, return element, tree -def advancedsettings_tweaks(): - """ - Kodi tweaks - - Changes advancedsettings.xml, musiclibrary: - backgroundupdate set to "true" - """ - advancedsettings_xml(['musiclibrary', 'backgroundupdate'], - new_value='true') - - def sourcesXML(): # To make Master lock compatible path = tryDecode(xbmc.translatePath("special://profile/")) @@ -650,8 +668,15 @@ def sourcesXML(): try: xmlparse = etree.parse(xmlpath) - except: # Document is blank or missing + except IOError: # Document is blank or missing root = etree.Element('sources') + except etree.ParseError: + log.error('Error parsing %s' % xmlpath) + # "Kodi cannot parse {0}. PKC will not function correctly. Please visit + # {1} and correct your file!" + dialog('ok', language(29999), language(39716).format( + 'sources.xml', 'http://kodi.wiki/view/sources.xml')) + return else: root = xmlparse.getroot() @@ -685,20 +710,27 @@ def sourcesXML(): def passwordsXML(): # To add network credentials - path = xbmc.translatePath("special://userdata/") + path = tryDecode(xbmc.translatePath("special://userdata/")) xmlpath = "%spasswords.xml" % path + dialog = xbmcgui.Dialog() try: xmlparse = etree.parse(xmlpath) - except: + except IOError: # Document is blank or missing root = etree.Element('passwords') skipFind = True + except etree.ParseError: + log.error('Error parsing %s' % xmlpath) + # "Kodi cannot parse {0}. PKC will not function correctly. Please visit + # {1} and correct your file!" + dialog.ok(language(29999), language(39716).format( + 'passwords.xml', 'http://forum.kodi.tv/')) + return else: root = xmlparse.getroot() skipFind = False - dialog = xbmcgui.Dialog() credentials = settings('networkCreds') if credentials: # Present user with options @@ -798,7 +830,7 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): """ Feed with tagname as unicode """ - path = xbmc.translatePath("special://profile/playlists/video/") + path = tryDecode(xbmc.translatePath("special://profile/playlists/video/")) if viewtype == "mixed": plname = "%s - %s" % (tagname, mediatype) xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype) @@ -807,12 +839,12 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): xsppath = "%sPlex %s.xsp" % (path, viewid) # Create the playlist directory - if not exists(path): + if not exists(tryEncode(path)): log.info("Creating directory: %s" % path) makedirs(path) # Only add the playlist if it doesn't already exists - if exists(xsppath): + if exists(tryEncode(xsppath)): log.info('Path %s does exist' % xsppath) if delete: remove(xsppath) @@ -827,27 +859,22 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): 'show': 'tvshows' } log.info("Writing playlist file to: %s" % xsppath) - try: - with open(xsppath, 'wb'): - tryEncode( - '\n' - '\n\t' - 'Plex %s\n\t' - 'all\n\t' - '\n\t\t' - '%s\n\t' - '\n' - '\n' - % (itemtypes.get(mediatype, mediatype), plname, tagname)) - except Exception as e: - log.error("Failed to create playlist: %s" % xsppath) - log.error(e) - return + with open(xsppath, 'wb'): + tryEncode( + '\n' + '\n\t' + 'Plex %s\n\t' + 'all\n\t' + '\n\t\t' + '%s\n\t' + '\n' + '\n' + % (itemtypes.get(mediatype, mediatype), plname, tagname)) log.info("Successfully added playlist: %s" % tagname) def deletePlaylists(): # Clean up the playlists - path = xbmc.translatePath("special://profile/playlists/video/") + path = tryDecode(xbmc.translatePath("special://profile/playlists/video/")) for root, _, files in walk(path): for file in files: if file.startswith('Plex'): @@ -855,7 +882,7 @@ def deletePlaylists(): def deleteNodes(): # Clean up video nodes - path = xbmc.translatePath("special://profile/library/video/") + path = tryDecode(xbmc.translatePath("special://profile/library/video/")) for root, dirs, _ in walk(path): for directory in dirs: if directory.startswith('Plex-'): @@ -906,78 +933,78 @@ def LogTime(func): return wrapper -def ThreadMethodsAdditionalStop(windowAttribute): - """ - Decorator to replace stopThread method to include the Kodi windowAttribute - - Use with any sync threads. @ThreadMethods still required FIRST - """ - def wrapper(cls): - def threadStopped(self): - return (self._threadStopped or - (window('plex_terminateNow') == "true") or - window(windowAttribute) == "true") - cls.threadStopped = threadStopped - return cls - return wrapper - - -def ThreadMethodsAdditionalSuspend(windowAttribute): - """ - Decorator to replace threadSuspended(): thread now also suspends if a - Kodi windowAttribute is set to 'true', e.g. 'suspend_LibraryThread' - - Use with any library sync threads. @ThreadMethods still required FIRST - """ - def wrapper(cls): - def threadSuspended(self): - return (self._threadSuspended or - window(windowAttribute) == 'true') - cls.threadSuspended = threadSuspended - return cls - return wrapper - - -def ThreadMethods(cls): +def thread_methods(cls=None, add_stops=None, add_suspends=None): """ Decorator to add the following methods to a threading class: - suspendThread(): pauses the thread - resumeThread(): resumes the thread - stopThread(): stopps/kills the thread + suspend_thread(): pauses the thread + resume_thread(): resumes the thread + stop_thread(): stopps/kills the thread - threadSuspended(): returns True if thread is suspend_thread - threadStopped(): returns True if thread is stopped (or should stop ;-)) - ALSO stops if Kodi is exited + thread_suspended(): returns True if thread is suspended + thread_stopped(): returns True if thread is stopped (or should stop ;-)) + ALSO returns True if PKC should exit Also adds the following class attributes: - _threadStopped - _threadSuspended + __thread_stopped + __thread_suspended + __stops + __suspends + + invoke with either + @Newthread_methods + class MyClass(): + or + @Newthread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'], + add_suspends=['DB_SCAN', 'WHATEVER']) + class MyClass(): """ + # So we don't need to invoke with () + if cls is None: + return partial(thread_methods, + add_stops=add_stops, + add_suspends=add_suspends) + # Because we need a reference, not a copy of the immutable objects in + # state, we need to look up state every time explicitly + cls.__stops = ['STOP_PKC'] + if add_stops is not None: + cls.__stops.extend(add_stops) + cls.__suspends = add_suspends or [] + # Attach new attributes to class - cls._threadStopped = False - cls._threadSuspended = False + cls.__thread_stopped = False + cls.__thread_suspended = False # Define new class methods and attach them to class - def stopThread(self): - self._threadStopped = True - cls.stopThread = stopThread + def stop_thread(self): + self.__thread_stopped = True + cls.stop_thread = stop_thread - def suspendThread(self): - self._threadSuspended = True - cls.suspendThread = suspendThread + def suspend_thread(self): + self.__thread_suspended = True + cls.suspend_thread = suspend_thread - def resumeThread(self): - self._threadSuspended = False - cls.resumeThread = resumeThread + def resume_thread(self): + self.__thread_suspended = False + cls.resume_thread = resume_thread - def threadSuspended(self): - return self._threadSuspended - cls.threadSuspended = threadSuspended + def thread_suspended(self): + if self.__thread_suspended is True: + return True + for suspend in self.__suspends: + if getattr(state, suspend): + return True + return False + cls.thread_suspended = thread_suspended - def threadStopped(self): - return self._threadStopped or (window('plex_terminateNow') == 'true') - cls.threadStopped = threadStopped + def thread_stopped(self): + if self.__thread_stopped is True: + return True + for stop in self.__stops: + if getattr(state, stop): + return True + return False + cls.thread_stopped = thread_stopped # Return class to render this a decorator return cls diff --git a/resources/lib/variables.py b/resources/lib/variables.py index fa3b8be5..95042299 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -2,7 +2,8 @@ import xbmc from xbmcaddon import Addon -# Paths are in string, not unicode! +# Paths are in unicode, otherwise Windows will throw fits +# For any file operations with KODI function, use encoded strings! def tryDecode(string, encoding='utf-8'): @@ -29,7 +30,7 @@ ADDON_VERSION = _ADDON.getAddonInfo('version') KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) KODIVERSION = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion') -KODI_PROFILE = xbmc.translatePath("special://profile") +KODI_PROFILE = tryDecode(xbmc.translatePath("special://profile")) if xbmc.getCondVisibility('system.platform.osx'): PLATFORM = "MacOSX" @@ -70,8 +71,8 @@ _DB_VIDEO_VERSION = { 17: 107, # Krypton 18: 108 # Leia } -DB_VIDEO_PATH = xbmc.translatePath( - "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION]) +DB_VIDEO_PATH = tryDecode(xbmc.translatePath( + "special://database/MyVideos%s.db" % _DB_VIDEO_VERSION[KODIVERSION])) _DB_MUSIC_VERSION = { 13: 46, # Gotham @@ -81,8 +82,8 @@ _DB_MUSIC_VERSION = { 17: 60, # Krypton 18: 62 # Leia } -DB_MUSIC_PATH = xbmc.translatePath( - "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION]) +DB_MUSIC_PATH = tryDecode(xbmc.translatePath( + "special://database/MyMusic%s.db" % _DB_MUSIC_VERSION[KODIVERSION])) _DB_TEXTURE_VERSION = { 13: 13, # Gotham @@ -92,13 +93,13 @@ _DB_TEXTURE_VERSION = { 17: 13, # Krypton 18: 13 # Leia } -DB_TEXTURE_PATH = xbmc.translatePath( - "special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION]) +DB_TEXTURE_PATH = tryDecode(xbmc.translatePath( + "special://database/Textures%s.db" % _DB_TEXTURE_VERSION[KODIVERSION])) -DB_PLEX_PATH = xbmc.translatePath("special://database/plex.db") +DB_PLEX_PATH = tryDecode(xbmc.translatePath("special://database/plex.db")) -EXTERNAL_SUBTITLE_TEMP_PATH = xbmc.translatePath( - "special://profile/addon_data/%s/temp/" % ADDON_ID) +EXTERNAL_SUBTITLE_TEMP_PATH = tryDecode(xbmc.translatePath( + "special://profile/addon_data/%s/temp/" % ADDON_ID)) # Multiply Plex time by this factor to receive Kodi time diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index f6561ad8..12aed035 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -3,14 +3,13 @@ import logging from shutil import copytree import xml.etree.ElementTree as etree -from os import remove, listdir, makedirs -from os.path import isfile, join +from os import makedirs import xbmc from xbmcvfs import exists from utils import window, settings, language as lang, tryEncode, indent, \ - normalize_nodes, exists_dir + normalize_nodes, exists_dir, tryDecode import variables as v ############################################################################### @@ -63,22 +62,25 @@ class VideoNodes(object): dirname = viewid # Returns strings - path = xbmc.translatePath("special://profile/library/video/") - nodepath = xbmc.translatePath( - "special://profile/library/video/Plex-%s/" % dirname) + path = tryDecode(xbmc.translatePath( + "special://profile/library/video/")) + nodepath = tryDecode(xbmc.translatePath( + "special://profile/library/video/Plex-%s/" % dirname)) if delete: - files = [f for f in listdir(nodepath) if isfile(join(nodepath, f))] - for file in files: - remove(nodepath + file) - log.info("Sucessfully removed videonode: %s." % tagname) + if exists_dir(nodepath): + from shutil import rmtree + rmtree(nodepath) + log.info("Sucessfully removed videonode: %s." % tagname) return # Verify the video directory if not exists_dir(path): copytree( - src=xbmc.translatePath("special://xbmc/system/library/video"), - dst=xbmc.translatePath("special://profile/library/video")) + src=tryDecode(xbmc.translatePath( + "special://xbmc/system/library/video")), + dst=tryDecode(xbmc.translatePath( + "special://profile/library/video"))) # Create the node directory if mediatype != "photos": @@ -290,7 +292,7 @@ class VideoNodes(object): # To do: add our photos nodes to kodi picture sources somehow continue - if exists(nodeXML): + if exists(tryEncode(nodeXML)): # Don't recreate xml if already exists continue @@ -377,8 +379,9 @@ class VideoNodes(object): def singleNode(self, indexnumber, tagname, mediatype, itemtype): tagname = tryEncode(tagname) - cleantagname = normalize_nodes(tagname) - nodepath = xbmc.translatePath("special://profile/library/video/") + cleantagname = tryDecode(normalize_nodes(tagname)) + nodepath = tryDecode(xbmc.translatePath( + "special://profile/library/video/")) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) path = "library://video/plex_%s.xml" % cleantagname if v.KODIVERSION >= 17: @@ -391,8 +394,10 @@ class VideoNodes(object): if not exists_dir(nodepath): # We need to copy over the default items copytree( - src=xbmc.translatePath("special://xbmc/system/library/video"), - dst=xbmc.translatePath("special://profile/library/video")) + src=tryDecode(xbmc.translatePath( + "special://xbmc/system/library/video")), + dst=tryDecode(xbmc.translatePath( + "special://profile/library/video"))) labels = { 'Favorite movies': 30180, @@ -406,7 +411,7 @@ class VideoNodes(object): window('%s.content' % embynode, value=path) window('%s.type' % embynode, value=itemtype) - if exists(nodeXML): + if exists(tryEncode(nodeXML)): # Don't recreate xml if already exists return diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 7ded4456..0ae40da8 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -11,9 +11,9 @@ from ssl import CERT_NONE from xbmc import sleep -from utils import window, settings, ThreadMethodsAdditionalSuspend, \ - ThreadMethods +from utils import window, settings, thread_methods from companion import process_command +import state ############################################################################### @@ -22,8 +22,6 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethods class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -62,11 +60,11 @@ class WebSocket(Thread): counter = 0 handshake_counter = 0 - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended - while not threadStopped(): + thread_stopped = self.thread_stopped + thread_suspended = self.thread_suspended + while not thread_stopped(): # In the event the server goes offline - while threadSuspended(): + while thread_suspended(): # Set in service.py if self.ws is not None: try: @@ -74,7 +72,7 @@ class WebSocket(Thread): except: pass self.ws = None - if threadStopped(): + if thread_stopped(): # Abort was requested while waiting. We should exit log.info("##===---- %s Stopped ----===##" % self.__class__.__name__) @@ -141,16 +139,17 @@ class WebSocket(Thread): def stopThread(self): """ - Overwrite this method from ThreadMethods to close websockets + Overwrite this method from thread_methods to close websockets """ log.info("Stopping %s thread." % self.__class__.__name__) - self._threadStopped = True + self.__threadStopped = True try: self.ws.shutdown() except: pass +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion @@ -160,16 +159,15 @@ class PMS_Websocket(WebSocket): def getUri(self): server = window('pms_server') - # Need to use plex.tv token, if any. NOT user token - token = window('plex_token') # 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 - if token: - uri += '?X-Plex-Token=%s' % token + # Need to use plex.tv token, if any. NOT user token + if state.PLEX_TOKEN: + uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN sslopt = {} if settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE @@ -213,14 +211,18 @@ class PMS_Websocket(WebSocket): class Alexa_Websocket(WebSocket): """ - Websocket connection to talk to Amazon Alexa + Websocket connection to talk to Amazon Alexa. + + Can't use thread_methods! """ + __thread_stopped = False + __thread_suspended = False + def getUri(self): self.plex_client_Id = window('plex_client_Id') uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' - % (window('currUserId'), - self.plex_client_Id, - window('plex_token'))) + % (state.PLEX_USER_ID, + self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt @@ -252,11 +254,32 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass - def threadSuspended(self): + # Path in thread_methods + def stop_thread(self): + self.__thread_stopped = True + + def suspend_thread(self): + self.__thread_suspended = True + + def resume_thread(self): + self.__thread_suspended = False + + def thread_stopped(self): + if self.__thread_stopped is True: + return True + if state.STOP_PKC: + return True + return False + + # The culprit + def thread_suspended(self): """ - Overwrite to ignore library sync stuff and allow to check for - plex_restricteduser + Overwrite method since we need to check for plex token """ - return (self._threadSuspended or - window('plex_restricteduser') == 'true' or - not window('plex_token')) + if self.__thread_suspended is True: + return True + if not state.PLEX_TOKEN: + return True + if state.RESTRICTED_USER: + return True + return False diff --git a/service.py b/service.py index 176607dd..601a2a59 100644 --- a/service.py +++ b/service.py @@ -6,7 +6,7 @@ import logging from os import path as os_path from sys import path as sys_path, argv -from xbmc import translatePath, Monitor, sleep +from xbmc import translatePath, Monitor from xbmcaddon import Addon ############################################################################### @@ -30,7 +30,8 @@ sys_path.append(_base_resource) ############################################################################### -from utils import settings, window, language as lang, dialog, tryEncode +from utils import settings, window, language as lang, dialog, tryEncode, \ + tryDecode from userclient import UserClient import initialsetup from kodimonitor import KodiMonitor @@ -42,10 +43,11 @@ from playqueue import Playqueue import PlexAPI from PlexCompanion import PlexCompanion -from monitor_kodi_play import Monitor_Kodi_Play +from command_pipeline import Monitor_Window from playback_starter import Playback_Starter from artwork import Image_Cache_Thread import variables as v +import state ############################################################################### @@ -85,7 +87,7 @@ class Service(): window('plex_logLevel', value=str(logLevel)) window('plex_kodiProfile', - value=translatePath("special://profile")) + value=tryDecode(translatePath("special://profile"))) window('plex_context', value='true' if settings('enableContext') == "true" else "") window('fetch_pms_item_number', @@ -105,18 +107,16 @@ class Service(): # Reset window props for profile switch properties = [ - "plex_online", "plex_serverStatus", "plex_onWake", "plex_dbCheck", "plex_kodiScan", - "plex_shouldStop", "currUserId", "plex_dbScan", + "plex_shouldStop", "plex_dbScan", "plex_initialScan", "plex_customplayqueue", "plex_playbackProps", - "plex_runLibScan", "plex_username", "pms_token", "plex_token", + "plex_runLibScan", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", - "suspend_LibraryThread", "plex_terminateNow", "kodiplextimeoffset", "countError", "countUnauthorized", "plex_restricteduser", "plex_allows_mediaDeletion", - "plex_play_new_item", "plex_result", "plex_force_transcode_pix" + "plex_command", "plex_result", "plex_force_transcode_pix" ] for prop in properties: window(prop, clear=True) @@ -134,15 +134,22 @@ class Service(): logLevel = 0 return logLevel + def __stop_PKC(self): + """ + Kodi's abortRequested is really unreliable :-( + """ + return self.monitor.abortRequested() or state.STOP_PKC + def ServiceEntryPoint(self): # Important: Threads depending on abortRequest will not trigger # if profile switch happens more than once. + __stop_PKC = self.__stop_PKC monitor = self.monitor kodiProfile = v.KODI_PROFILE # Detect playback start early on - self.monitor_kodi_play = Monitor_Kodi_Play(self) - self.monitor_kodi_play.start() + self.command_pipeline = Monitor_Window(self) + self.command_pipeline.start() # Server auto-detect initialsetup.InitialSetup().setup() @@ -162,14 +169,14 @@ class Service(): welcome_msg = True counter = 0 - while not monitor.abortRequested(): + while not __stop_PKC(): - if tryEncode(window('plex_kodiProfile')) != kodiProfile: + if window('plex_kodiProfile') != kodiProfile: # Profile change happened, terminate this thread and others log.warn("Kodi profile was: %s and changed to: %s. " "Terminating old PlexKodiConnect thread." % (kodiProfile, - tryEncode(window('plex_kodiProfile')))) + window('plex_kodiProfile'))) break # Before proceeding, need to make sure: @@ -242,14 +249,13 @@ class Service(): # Server went offline break - if monitor.waitForAbort(5): + if monitor.waitForAbort(3): # Abort was requested while waiting. We should exit break - sleep(50) else: # Wait until Plex server is online # or Kodi is shut down. - while not monitor.abortRequested(): + while not self.__stop_PKC(): server = self.user.getServer() if server is False: # No server info set in add-on settings @@ -261,7 +267,7 @@ class Service(): self.server_online = False window('plex_online', value="false") # Suspend threads - window('suspend_LibraryThread', value='true') + state.SUSPEND_LIBRARY_THREAD = True log.error("Plex Media Server went offline") if settings('show_pms_offline') == 'true': dialog('notification', @@ -298,10 +304,10 @@ class Service(): sound=False) log.info("Server %s is online and ready." % server) window('plex_online', value="true") - if window('plex_authenticated') == 'true': + if state.AUTHENTICATED: # Server got offline when we were authenticated. # Hence resume threads - window('suspend_LibraryThread', clear=True) + state.SUSPEND_LIBRARY_THREAD = False # Start the userclient thread if not self.user_running: @@ -317,31 +323,10 @@ class Service(): if monitor.waitForAbort(0.05): # Abort was requested while waiting. We should exit break - # Terminating PlexKodiConnect # Tell all threads to terminate (e.g. several lib sync threads) - window('plex_terminateNow', value='true') - try: - self.plexCompanion.stopThread() - except: - log.warn('plexCompanion already shut down') - try: - self.library.stopThread() - except: - log.warn('Library sync already shut down') - try: - self.ws.stopThread() - except: - log.warn('Websocket client already shut down') - try: - self.alexa.stopThread() - except: - log.warn('Websocket client already shut down') - try: - self.user.stopThread() - except: - log.warn('User client already shut down') + state.STOP_PKC = True try: downloadutils.DownloadUtils().stopSession() except: @@ -349,6 +334,7 @@ class Service(): window('plex_service_started', clear=True) log.warn("======== STOP %s ========" % v.ADDON_NAME) + # Safety net - Kody starts PKC twice upon first installation! if window('plex_service_started') == 'true': exit = True