From 5bc9a60ad4733c61404cedfbb646d029a618837d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Thu, 11 May 2017 20:18:38 +0200 Subject: [PATCH 01/58] Update German translation --- .../resource.language.de_DE/strings.po | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/language/resource.language.de_DE/strings.po b/resources/language/resource.language.de_DE/strings.po index 7d42fee6..aa898677 100644 --- a/resources/language/resource.language.de_DE/strings.po +++ b/resources/language/resource.language.de_DE/strings.po @@ -2078,3 +2078,23 @@ msgid "" msgstr "" "Neue Plex Musikbibliothek gefunden. Entschuldigung, aber Kodi muss nun " "aufgrund der gemachten Änderungen neu gestartet werden." + +# Shown during sync process +msgctxt "#39712" +msgid "downloaded" +msgstr "geladen" + +# Shown during sync process +msgctxt "#39713" +msgid "processed" +msgstr "verarbeitet" + +# Shown during sync process +msgctxt "#39714" +msgid "Sync" +msgstr "Sync" + +# Shown during sync process +msgctxt "#39715" +msgid "items" +msgstr "Einträge" From b555ccce00de86231c5825e6b98133640c9d526a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 08:22:36 +0200 Subject: [PATCH 02/58] Companion: ensure sockets get closed - Should fix #293 --- resources/lib/PlexCompanion.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index a233e103..9b961c5f 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -144,7 +144,24 @@ 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 @@ -245,11 +262,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 ##===----") From 85e59e32071bb835833072484f0e9bf26bc6cea7 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 09:44:05 +0200 Subject: [PATCH 03/58] Wait a bit longer --- resources/lib/monitor_kodi_play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/monitor_kodi_play.py b/resources/lib/monitor_kodi_play.py index b7968eeb..c9827fac 100644 --- a/resources/lib/monitor_kodi_play.py +++ b/resources/lib/monitor_kodi_play.py @@ -35,7 +35,7 @@ class Monitor_Kodi_Play(Thread): queue.put(window('plex_play_new_item')) window('plex_play_new_item', clear=True) else: - sleep(20) + sleep(50) # Put one last item into the queue to let playback_starter end queue.put(None) log.info("----===## Kodi_Play_Client stopped ##===----") From 6629fb41e3f686049d5a740eeb45295c574ed824 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 12:52:12 +0200 Subject: [PATCH 04/58] Code refactoring: avoid window vars in loops --- default.py | 6 +- resources/lib/PlexAPI.py | 6 +- resources/lib/PlexCompanion.py | 16 +-- resources/lib/artwork.py | 25 ++-- resources/lib/command_pipeline.py | 68 ++++++++++ resources/lib/downloadutils.py | 5 +- resources/lib/entrypoint.py | 35 +++--- resources/lib/initialsetup.py | 3 +- resources/lib/library_sync/fanart.py | 19 ++- resources/lib/library_sync/get_metadata.py | 12 +- .../lib/library_sync/process_metadata.py | 11 +- resources/lib/library_sync/sync_info.py | 10 +- resources/lib/librarysync.py | 86 +++++++------ resources/lib/monitor_kodi_play.py | 41 ------ resources/lib/playback_starter.py | 4 +- resources/lib/playqueue.py | 18 +-- resources/lib/state.py | 22 ++++ resources/lib/userclient.py | 59 ++++----- resources/lib/utils.py | 117 +++++++++--------- resources/lib/websocket_client.py | 37 +++--- service.py | 38 ++---- 21 files changed, 327 insertions(+), 311 deletions(-) create mode 100644 resources/lib/command_pipeline.py delete mode 100644 resources/lib/monitor_kodi_play.py create mode 100644 resources/lib/state.py 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/lib/PlexAPI.py b/resources/lib/PlexAPI.py index fbd28e7c..282c5104 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 ############################################################################### @@ -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' @@ -2563,7 +2566,8 @@ class API(): if forceCheck is False: # Validate the path is correct with user intervention if self.askToValidate(path): - window('plex_shouldStop', value="true") + import state + 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 9b961c5f..48f6e831 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, ThreadMethods 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 +@ThreadMethods(add_suspends=[state.PMS_STATUS]) class PlexCompanion(Thread): """ """ @@ -164,8 +164,8 @@ class PlexCompanion(Thread): 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() @@ -213,12 +213,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: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4efc05d4..a8b7cef6 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,7 +13,8 @@ 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 + ThreadMethods, dialog, exists_dir +import state # Disable annoying requests warnings import requests.packages.urllib3 @@ -126,8 +127,8 @@ def double_urldecode(text): return unquote(unquote(text)) -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@ThreadMethods(add_stops=[state.STOP_SYNC], + add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN]) class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() @@ -140,22 +141,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 +173,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') diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py new file mode 100644 index 00000000..9be330a6 --- /dev/null +++ b/resources/lib/command_pipeline.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Thread +from Queue import Queue + +from xbmc import sleep + +from utils import window, ThreadMethods +import state + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethods +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 + 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..d1c07d9f 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 @@ -274,10 +276,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..3bdfac4d 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 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,6 +81,7 @@ def togglePlexTV(): settings('plex_status', value="Not logged in to plex.tv") window('plex_token', clear=True) + plex_command('PLEX_TOKEN', '') window('plex_username', clear=True) else: log.info('Login to plex.tv') @@ -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)') @@ -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..9c41f924 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 ############################################################################### @@ -496,7 +497,7 @@ 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: diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 7f9fc074..483da89a 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -5,11 +5,11 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ - ThreadMethodsAdditionalSuspend +from utils import ThreadMethods, window import plexdb_functions as plexdb import itemtypes import variables as v +import state ############################################################################### @@ -18,9 +18,8 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN], + add_stops=[state.STOP_SYNC]) class Process_Fanart_Thread(Thread): """ Threaded download of additional fanart in the background @@ -55,14 +54,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..4aa44266 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -5,9 +5,10 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window +from utils import ThreadMethods, window from PlexFunctions import GetPlexMetadata, GetAllPlexChildren import sync_info +import state ############################################################################### @@ -16,8 +17,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. @@ -48,7 +48,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 +79,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..7b44ed33 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -5,19 +5,18 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods +from utils import ThreadMethods import itemtypes import sync_info +import state ############################################################################### - log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.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 +69,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..13caec52 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -4,7 +4,8 @@ from threading import Thread, Lock from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang +from utils import ThreadMethods, language as lang +import state ############################################################################### @@ -18,8 +19,7 @@ LOCK = Lock() ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. @@ -53,13 +53,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..c1362531 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,\ + ThreadMethods, 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 + advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes 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 +@ThreadMethods(add_stops=[state.STOP_SYNC], + add_suspends=[state.SUSPEND_LIBRARY_THREAD]) class LibrarySync(Thread): """ """ @@ -300,7 +299,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 +322,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)) @@ -759,8 +758,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 +804,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 +825,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 +897,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 +926,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 +951,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 +986,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 +1023,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 +1040,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 +1065,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 +1171,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 +1276,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 +1285,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 +1298,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 +1314,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 +1334,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,8 +1347,7 @@ 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 @@ -1394,7 +1391,7 @@ 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()) @@ -1433,6 +1430,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 +1441,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 @@ -1476,12 +1474,12 @@ class LibrarySync(Thread): 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 @@ -1523,6 +1521,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 +1546,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 +1565,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 +1605,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 +1617,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 c9827fac..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(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/playback_starter.py b/resources/lib/playback_starter.py index b6b14c18..836f587d 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -152,12 +152,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/playqueue.py b/resources/lib/playqueue.py index a496574b..8e52e071 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,12 +5,13 @@ 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, ThreadMethods import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API from playbackutils import PlaybackUtils import variables as v +import state ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -21,8 +22,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### -@ThreadMethodsAdditionalSuspend('plex_serverStatus') -@ThreadMethods +@ThreadMethods(add_suspends=[state.PMS_STATUS]) class Playqueue(Thread): """ Monitors Kodi's playqueues for changes on the Kodi side @@ -153,7 +153,7 @@ class Playqueue(Thread): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.threadStopped(): + if self.thread_stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -193,8 +193,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 +203,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/state.py b/resources/lib/state.py new file mode 100644 index 00000000..9fffcaea --- /dev/null +++ b/resources/lib/state.py @@ -0,0 +1,22 @@ +# -*- 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 + +PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 04f97d16..ec355f61 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, ThreadMethods import downloadutils import PlexAPI from PlexFunctions import GetMachineIdentifier +import state ############################################################################### @@ -24,8 +24,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_Userclient') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_USER_CLIENT]) class UserClient(threading.Thread): # Borg - multiple instances, shared state @@ -118,25 +117,6 @@ 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') @@ -171,7 +151,10 @@ class UserClient(threading.Thread): # 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) @@ -202,7 +185,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( @@ -284,6 +267,7 @@ class UserClient(threading.Thread): window('plex_authenticated', clear=True) window('pms_token', clear=True) + state.PLEX_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) @@ -291,6 +275,7 @@ class UserClient(threading.Thread): window('currUserId', clear=True) window('plex_username', clear=True) window('plex_restricteduser', clear=True) + state.RESTRICTED_USER = False settings('username', value='') settings('userid', value='') @@ -310,32 +295,32 @@ class UserClient(threading.Thread): 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' self.resetClient() xbmc.sleep(2000) 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 @@ -345,8 +330,9 @@ class UserClient(threading.Thread): log.info("Current user: %s" % self.currUser) log.info("Current userId: %s" % self.currUserId) 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 +340,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 +348,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..e00f414c 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(1) + window('plex_command', value='%s-%s' % (key, value)) + + def settings(setting, value=None): """ Get or add addon setting. Returns unicode @@ -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) @@ -906,78 +920,61 @@ 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 ThreadMethods(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 suspend_thread + thread_stopped(): returns True if thread is stopped (or should stop ;-)) + ALSO stops if PKC should exit Also adds the following class attributes: - _threadStopped - _threadSuspended + _thread_stopped + _thread_suspended + + invoke with either + @NewThreadMethods + class MyClass(): + or + @NewThreadMethods(add_stops=[state.SUSPEND_LIBRARY_TRHEAD], + add_suspends=[state.WHATEVER, state.WHATEVER2]) + class MyClass(): """ + if cls is None: + return partial(ThreadMethods, + add_stops=add_stops, + add_suspends=add_suspends) + # Make sure we have an iterable + add_stops = add_stops or [] + add_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): + return self._thread_suspended or any(add_suspends) + cls.thread_suspended = thread_suspended - def threadStopped(self): - return self._threadStopped or (window('plex_terminateNow') == 'true') - cls.threadStopped = threadStopped + def thread_stopped(self): + return self._thread_stopped or state.STOP_PKC or any(add_stops) + cls.thread_stopped = thread_stopped # Return class to render this a decorator return cls diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 7ded4456..e04a7f6e 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, ThreadMethods from companion import process_command +import state ############################################################################### @@ -22,8 +22,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD]) class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -62,11 +61,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 +73,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__) @@ -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 @@ -218,9 +216,7 @@ class Alexa_Websocket(WebSocket): 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'))) + % (window('currUserId'), self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt @@ -252,11 +248,10 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass - def threadSuspended(self): + def thread_suspended(self): """ Overwrite to ignore library sync stuff and allow to check for - plex_restricteduser + RESTRICTED_USER and PLEX_TOKEN """ - return (self._threadSuspended or - window('plex_restricteduser') == 'true' or - not window('plex_token')) + return self._thread_suspended or state.RESTRICTED_USER \ + or not state.PLEX_TOKEN diff --git a/service.py b/service.py index 176607dd..ef174ce2 100644 --- a/service.py +++ b/service.py @@ -42,10 +42,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 ############################################################################### @@ -105,7 +106,6 @@ class Service(): # Reset window props for profile switch properties = [ - "plex_online", "plex_serverStatus", "plex_onWake", "plex_dbCheck", "plex_kodiScan", "plex_shouldStop", "currUserId", "plex_dbScan", @@ -113,10 +113,9 @@ class Service(): "plex_runLibScan", "plex_username", "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) @@ -141,8 +140,8 @@ class Service(): 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() @@ -261,7 +260,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', @@ -301,7 +300,7 @@ class Service(): if window('plex_authenticated') == 'true': # 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: @@ -321,27 +320,7 @@ class Service(): # 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 +328,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 From f83a30441936ee166d76becb606e15da871ef8dc Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 12:52:27 +0200 Subject: [PATCH 05/58] Fix OperationalError when resetting PKC --- resources/lib/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index e00f414c..f362a957 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -361,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() @@ -374,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() @@ -387,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() @@ -411,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() From 7d6310601c99423e4ead29ab002a12a782953285 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 13:25:46 +0200 Subject: [PATCH 06/58] Fix possible OperationalErrors --- resources/lib/artwork.py | 3 +-- resources/lib/kodidb_functions.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index a8b7cef6..4a6e352d 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -236,8 +236,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() 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( From 4b06a89f702eb5dab8e8e36e5761c30fbfb55c0d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 14:37:21 +0200 Subject: [PATCH 07/58] Try shutting down PKC more cleanly --- resources/lib/kodimonitor.py | 5 +++++ service.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) 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/service.py b/service.py index ef174ce2..08119cb5 100644 --- a/service.py +++ b/service.py @@ -133,9 +133,16 @@ 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 @@ -161,7 +168,7 @@ class Service(): welcome_msg = True counter = 0 - while not monitor.abortRequested(): + while not __stop_PKC(): if tryEncode(window('plex_kodiProfile')) != kodiProfile: # Profile change happened, terminate this thread and others @@ -241,14 +248,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 @@ -316,7 +322,6 @@ 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) From 0520fbda3e27372f6fb3ed25cffc9dfeee6cb802 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 14:58:01 +0200 Subject: [PATCH 08/58] Fix user switch --- resources/lib/userclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index ec355f61..1a9cbf72 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -315,6 +315,7 @@ class UserClient(threading.Thread): elif state.PMS_STATUS == "401": # Unauthorized access, revoke token state.PMS_STATUS = 'Auth' + window('plex_serverStatus', value='Auth') self.resetClient() xbmc.sleep(2000) From c3b0d5f2a7df97c00444637595194fa5e1947c8d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 15:32:12 +0200 Subject: [PATCH 09/58] Remove obsolete import --- service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.py b/service.py index 08119cb5..0bfd43b7 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 ############################################################################### From 071c68969aecade9eb77c3e132ea0fd0721991ba Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 15:32:25 +0200 Subject: [PATCH 10/58] Sleep longer --- resources/lib/userclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 1a9cbf72..d8fdc965 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -317,7 +317,7 @@ class UserClient(threading.Thread): 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 From e5a422635d78dce6e10da947cd39a9c43c24c6ce Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 12 May 2017 15:32:46 +0200 Subject: [PATCH 11/58] Also check authentication via state --- resources/lib/playback_starter.py | 3 ++- resources/lib/state.py | 2 ++ resources/lib/userclient.py | 2 ++ service.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 836f587d..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 diff --git a/resources/lib/state.py b/resources/lib/state.py index 9fffcaea..6d9df0be 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -19,4 +19,6 @@ SUSPEND_USER_CLIENT = False # Plex home user? Then "False". Along with window('plex_restricteduser') RESTRICTED_USER = False +# Along with window('plex_authenticated') +AUTHENTICATED = False PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index d8fdc965..d26ab694 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -159,6 +159,7 @@ class UserClient(threading.Thread): 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') @@ -266,6 +267,7 @@ 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) diff --git a/service.py b/service.py index 0bfd43b7..a37d60c1 100644 --- a/service.py +++ b/service.py @@ -303,7 +303,7 @@ 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 state.SUSPEND_LIBRARY_THREAD = False From 05aa97c0adc73fd01c0380313d34b2cca8a33ae6 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:39:19 +0200 Subject: [PATCH 12/58] Revert "Also check authentication via state" This reverts commit e5a422635d78dce6e10da947cd39a9c43c24c6ce. --- resources/lib/playback_starter.py | 3 +-- resources/lib/state.py | 2 -- resources/lib/userclient.py | 2 -- service.py | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index f0ac27f5..836f587d 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -17,7 +17,6 @@ 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__) @@ -40,7 +39,7 @@ class Playback_Starter(Thread): """ log.info("Process_play called with plex_id %s, kodi_id %s" % (plex_id, kodi_id)) - if not state.AUTHENTICATED: + if window('plex_authenticated') != "true": log.error('Not yet authenticated for PMS, abort starting playback') # Todo: Warn user with dialog return diff --git a/resources/lib/state.py b/resources/lib/state.py index 6d9df0be..9fffcaea 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -19,6 +19,4 @@ SUSPEND_USER_CLIENT = False # Plex home user? Then "False". Along with window('plex_restricteduser') RESTRICTED_USER = False -# Along with window('plex_authenticated') -AUTHENTICATED = False PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index d26ab694..d8fdc965 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -159,7 +159,6 @@ class UserClient(threading.Thread): 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') @@ -267,7 +266,6 @@ 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) diff --git a/service.py b/service.py index a37d60c1..0bfd43b7 100644 --- a/service.py +++ b/service.py @@ -303,7 +303,7 @@ class Service(): sound=False) log.info("Server %s is online and ready." % server) window('plex_online', value="true") - if state.AUTHENTICATED: + if window('plex_authenticated') == 'true': # Server got offline when we were authenticated. # Hence resume threads state.SUSPEND_LIBRARY_THREAD = False From 40b597987927d0f365638f8a70d26e4ea2220fc6 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:40:27 +0200 Subject: [PATCH 13/58] Revert "Remove obsolete import" This reverts commit c3b0d5f2a7df97c00444637595194fa5e1947c8d. --- service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.py b/service.py index 0bfd43b7..08119cb5 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 +from xbmc import translatePath, Monitor, sleep from xbmcaddon import Addon ############################################################################### From 219aa2d0abcef81ef935bc5eb52111705c0d7b97 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:40:39 +0200 Subject: [PATCH 14/58] Revert "Fix user switch" This reverts commit 0520fbda3e27372f6fb3ed25cffc9dfeee6cb802. --- resources/lib/userclient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index d8fdc965..584a1d94 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -315,7 +315,6 @@ class UserClient(threading.Thread): elif state.PMS_STATUS == "401": # Unauthorized access, revoke token state.PMS_STATUS = 'Auth' - window('plex_serverStatus', value='Auth') self.resetClient() xbmc.sleep(3000) From a1f1e332b321a9a251c107e3e427338ff9312999 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:41:02 +0200 Subject: [PATCH 15/58] Revert "Try shutting down PKC more cleanly" This reverts commit 4b06a89f702eb5dab8e8e36e5761c30fbfb55c0d. --- resources/lib/kodimonitor.py | 5 ----- service.py | 15 +++++---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 579c9033..4e19721e 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -14,7 +14,6 @@ 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 ############################################################################### @@ -138,10 +137,6 @@ 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/service.py b/service.py index 08119cb5..ef174ce2 100644 --- a/service.py +++ b/service.py @@ -133,16 +133,9 @@ 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 @@ -168,7 +161,7 @@ class Service(): welcome_msg = True counter = 0 - while not __stop_PKC(): + while not monitor.abortRequested(): if tryEncode(window('plex_kodiProfile')) != kodiProfile: # Profile change happened, terminate this thread and others @@ -248,13 +241,14 @@ class Service(): # Server went offline break - if monitor.waitForAbort(3): + if monitor.waitForAbort(5): # 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 self.__stop_PKC(): + while not monitor.abortRequested(): server = self.user.getServer() if server is False: # No server info set in add-on settings @@ -322,6 +316,7 @@ 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) From b6197231fa7c858c880df7b9abe296cf290b1c4e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:41:22 +0200 Subject: [PATCH 16/58] Revert "Code refactoring: avoid window vars in loops" This reverts commit 6629fb41e3f686049d5a740eeb45295c574ed824. --- default.py | 6 +- resources/lib/PlexAPI.py | 6 +- resources/lib/PlexCompanion.py | 16 +-- resources/lib/artwork.py | 25 ++-- resources/lib/command_pipeline.py | 68 ---------- resources/lib/downloadutils.py | 5 +- resources/lib/entrypoint.py | 33 ++--- resources/lib/initialsetup.py | 3 +- resources/lib/library_sync/fanart.py | 19 +-- resources/lib/library_sync/get_metadata.py | 12 +- .../lib/library_sync/process_metadata.py | 11 +- resources/lib/library_sync/sync_info.py | 10 +- resources/lib/librarysync.py | 86 ++++++------- resources/lib/monitor_kodi_play.py | 41 ++++++ resources/lib/playback_starter.py | 4 +- resources/lib/playqueue.py | 18 +-- resources/lib/state.py | 22 ---- resources/lib/userclient.py | 59 +++++---- resources/lib/utils.py | 117 +++++++++--------- resources/lib/websocket_client.py | 37 +++--- service.py | 38 ++++-- 21 files changed, 310 insertions(+), 326 deletions(-) delete mode 100644 resources/lib/command_pipeline.py create mode 100644 resources/lib/monitor_kodi_play.py delete mode 100644 resources/lib/state.py diff --git a/default.py b/default.py index 96983316..9f30f61c 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_command'): + while window('plex_play_new_item'): sleep(50) - window('plex_command', - value='play_%s' % argv[2]) + window('plex_play_new_item', + value='%s%s' % ('play', argv[2])) # Wait for the result while not pickl_window('plex_result'): sleep(50) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 282c5104..fbd28e7c 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -53,7 +53,6 @@ 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 ############################################################################### @@ -880,8 +879,6 @@ 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' @@ -2566,8 +2563,7 @@ class API(): if forceCheck is False: # Validate the path is correct with user intervention if self.askToValidate(path): - import state - state.STOP_SYNC = True + window('plex_shouldStop', value="true") path = None window('plex_pathverified', value='true') else: diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 48f6e831..9b961c5f 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -7,14 +7,13 @@ from urllib import urlencode from xbmc import sleep, executebuiltin -from utils import settings, ThreadMethods +from utils import settings, ThreadMethodsAdditionalSuspend, ThreadMethods 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 ############################################################################### @@ -23,7 +22,8 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.PMS_STATUS]) +@ThreadMethodsAdditionalSuspend('plex_serverStatus') +@ThreadMethods class PlexCompanion(Thread): """ """ @@ -164,8 +164,8 @@ class PlexCompanion(Thread): httpd = self.httpd # Cache for quicker while loops client = self.client - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended # Start up instances requestMgr = httppersist.RequestMgr() @@ -213,12 +213,12 @@ class PlexCompanion(Thread): if httpd: t = Thread(target=httpd.handle_request) - while not thread_stopped(): + while not threadStopped(): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - while thread_suspended(): - if thread_stopped(): + while threadSuspended(): + if threadStopped(): break sleep(1000) try: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4a6e352d..5d3ba04f 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,8 +13,7 @@ from xbmc import executeJSONRPC, sleep, translatePath from xbmcvfs import exists from utils import window, settings, language as lang, kodiSQL, tryEncode, \ - ThreadMethods, dialog, exists_dir -import state + ThreadMethods, ThreadMethodsAdditionalStop, dialog, exists_dir # Disable annoying requests warnings import requests.packages.urllib3 @@ -127,8 +126,8 @@ def double_urldecode(text): return unquote(unquote(text)) -@ThreadMethods(add_stops=[state.STOP_SYNC], - add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN]) +@ThreadMethodsAdditionalStop('plex_shouldStop') +@ThreadMethods class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() @@ -141,16 +140,22 @@ 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): - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended queue = self.queue sleep_between = self.sleep_between - while not thread_stopped(): + while not threadStopped(): # In the event the server goes offline - while thread_suspended(): + while threadSuspended(): # Set in service.py - if thread_stopped(): + if threadStopped(): # Abort was requested while waiting. We should exit log.info("---===### Stopped Image_Cache_Thread ###===---") return @@ -173,7 +178,7 @@ class Image_Cache_Thread(Thread): # download. All is well break except requests.ConnectionError: - if thread_stopped(): + if threadStopped(): # Kodi terminated break # Server thinks its a DOS attack, ('error 10053') diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py deleted file mode 100644 index 9be330a6..00000000 --- a/resources/lib/command_pipeline.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################### -import logging -from threading import Thread -from Queue import Queue - -from xbmc import sleep - -from utils import window, ThreadMethods -import state - -############################################################################### -log = logging.getLogger("PLEX."+__name__) - -############################################################################### - - -@ThreadMethods -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 - 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 d1c07d9f..8d78d8ba 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -9,8 +9,6 @@ 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 @@ -276,11 +274,10 @@ class DownloadUtils(): self.unauthorizedAttempts): log.warn('We seem to be truly unauthorized for PMS' ' %s ' % url) - if state.PMS_STATUS not in ('401', 'Auth'): + if window('plex_serverStatus') 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 3bdfac4d..8ab911cd 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, plex_command + CatchExceptions, JSONRPC, exists_dir 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') - plex_command('SUSPEND_USER_CLIENT', 'False') - plex_command('SUSPEND_LIBRARY_THREAD', 'False') + window('suspend_Userclient', clear=True) + window('suspend_LibraryThread', clear=True) return log.info("User chose server %s" % server['name']) @@ -81,7 +81,6 @@ def togglePlexTV(): settings('plex_status', value="Not logged in to plex.tv") window('plex_token', clear=True) - plex_command('PLEX_TOKEN', '') window('plex_username', clear=True) else: log.info('Login to plex.tv') @@ -101,7 +100,7 @@ def resetAuth(): resp = dialog('yesno', heading="{plex}", line1=lang(39206)) if resp == 1: log.info("Reset login attempts.") - plex_command('PMS_STATUS', 'Auth') + window('plex_serverStatus', value="Auth") else: executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') @@ -965,19 +964,22 @@ def enterPMS(): def __LogIn(): """ - Resets (clears) window properties to enable (re-)login + Resets (clears) window properties to enable (re-)login: + suspend_Userclient + plex_runLibScan: set to 'full' to trigger lib sync - SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed - out! + suspend_LibraryThread is cleared in service.py if user was signed out! """ window('plex_runLibScan', value='full') # Restart user client - plex_command('SUSPEND_USER_CLIENT', 'False') + window('suspend_Userclient', clear=True) def __LogOut(): """ - Finishes lib scans, logs out user. + Finishes lib scans, logs out user. The following window attributes are set: + suspend_LibraryThread: 'true' + suspend_Userclient: 'true' Returns True if successfully signed out, False otherwise """ @@ -989,7 +991,7 @@ def __LogOut(): time=3000, sound=False) # Pause library sync thread - plex_command('SUSPEND_LIBRARY_THREAD', 'True') + window('suspend_LibraryThread', value='true') # Wait max for 10 seconds for all lib scans to shutdown counter = 0 while window('plex_dbScan') == 'true': @@ -997,18 +999,17 @@ 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 - plex_command('SUSPEND_LIBRARY_THREAD', 'False') + window('suspend_LibraryThread', clear=True) log.error("Could not stop library sync, aborting") return False counter += 1 sleep(50) log.debug("Successfully stopped library sync") - counter = 0 # Log out currently signed in user: - window('plex_serverStatus', value='401') - plex_command('PMS_STATUS', '401') + window('plex_serverStatus', value="401") # Above method needs to have run its course! Hence wait + counter = 0 while window('plex_serverStatus') == "401": if counter > 100: # 'Failed to reset PKC. Try to restart Kodi.' @@ -1018,5 +1019,5 @@ def __LogOut(): counter += 1 sleep(50) # Suspend the user client during procedure - plex_command('SUSPEND_USER_CLIENT', 'True') + window('suspend_Userclient', value='true') return True diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 9c41f924..54bd9940 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -13,7 +13,6 @@ from userclient import UserClient from PlexAPI import PlexAPI from PlexFunctions import GetMachineIdentifier, get_PMS_settings -import state ############################################################################### @@ -497,7 +496,7 @@ class InitialSetup(): # Open Settings page now? You will need to restart! goToSettings = dialog.yesno(heading=lang(29999), line1=lang(39017)) if goToSettings: - state.PMS_STATUS = 'Stop' + window('plex_serverStatus', value="Stop") xbmc.executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') else: diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 483da89a..7f9fc074 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -5,11 +5,11 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods, window +from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ + ThreadMethodsAdditionalSuspend import plexdb_functions as plexdb import itemtypes import variables as v -import state ############################################################################### @@ -18,8 +18,9 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN], - add_stops=[state.STOP_SYNC]) +@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') +@ThreadMethodsAdditionalStop('plex_shouldStop') +@ThreadMethods class Process_Fanart_Thread(Thread): """ Threaded download of additional fanart in the background @@ -54,14 +55,14 @@ class Process_Fanart_Thread(Thread): Do the work """ log.debug("---===### Starting FanartSync ###===---") - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended queue = self.queue - while not thread_stopped(): + while not threadStopped(): # In the event the server goes offline - while thread_suspended(): + while threadSuspended() or window('plex_dbScan'): # Set in service.py - if thread_stopped(): + if threadStopped(): # 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 4aa44266..5fd25859 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -5,10 +5,9 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods, window +from utils import ThreadMethodsAdditionalStop, ThreadMethods, window from PlexFunctions import GetPlexMetadata, GetAllPlexChildren import sync_info -import state ############################################################################### @@ -17,7 +16,8 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. @@ -48,7 +48,7 @@ class Threaded_Get_Metadata(Thread): continue else: self.queue.task_done() - if self.thread_stopped(): + if self.threadStopped(): # 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 +79,8 @@ class Threaded_Get_Metadata(Thread): # cache local variables because it's faster queue = self.queue out_queue = self.out_queue - thread_stopped = self.thread_stopped - while thread_stopped() is False: + threadStopped = self.threadStopped + while threadStopped() 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 7b44ed33..e6765b41 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -5,18 +5,19 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods +from utils import ThreadMethodsAdditionalStop, ThreadMethods import itemtypes import sync_info -import state ############################################################################### + log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods class Threaded_Process_Metadata(Thread): """ Not yet implemented for more than 1 thread - if ever. Only to be called by @@ -69,9 +70,9 @@ class Threaded_Process_Metadata(Thread): item_fct = getattr(itemtypes, self.item_type) # cache local variables because it's faster queue = self.queue - thread_stopped = self.thread_stopped + threadStopped = self.threadStopped with item_fct() as item_class: - while thread_stopped() is False: + while threadStopped() 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 13caec52..3be8f70b 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -4,8 +4,7 @@ from threading import Thread, Lock from xbmc import sleep -from utils import ThreadMethods, language as lang -import state +from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang ############################################################################### @@ -19,7 +18,8 @@ LOCK = Lock() ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. @@ -53,13 +53,13 @@ class Threaded_Show_Sync_Info(Thread): # cache local variables because it's faster total = self.total dialog = self.dialog - thread_stopped = self.thread_stopped + threadStopped = self.threadStopped dialog.create("%s %s: %s %s" % (lang(39714), self.item_type, str(total), lang(39715))) total = 2 * total totalProgress = 0 - while thread_stopped() is False: + while threadStopped() 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 c1362531..60e1a0e9 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -10,9 +10,10 @@ import xbmcgui from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ - ThreadMethods, create_actor_db_index, dialog, LogTime, getScreensaver,\ + ThreadMethods, ThreadMethodsAdditionalStop, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ - advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes + advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes, \ + ThreadMethodsAdditionalSuspend, create_actor_db_index, dialog import downloadutils import itemtypes import plexdb_functions as plexdb @@ -29,7 +30,6 @@ 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,8 +38,9 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.STOP_SYNC], - add_suspends=[state.SUSPEND_LIBRARY_THREAD]) +@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') +@ThreadMethodsAdditionalStop('plex_shouldStop') +@ThreadMethods class LibrarySync(Thread): """ """ @@ -299,7 +300,7 @@ class LibrarySync(Thread): # Do the processing for itemtype in process: - if self.thread_stopped(): + if self.threadStopped(): xbmc.executebuiltin('InhibitIdleShutdown(false)') setScreensaver(value=screensaver) return False @@ -322,7 +323,7 @@ class LibrarySync(Thread): window('plex_scancrashed', clear=True) elif window('plex_scancrashed') == '401': window('plex_scancrashed', clear=True) - if state.PMS_STATUS not in ('401', 'Auth'): + if window('plex_serverStatus') not in ('401', 'Auth'): # Plex server had too much and returned ERROR self.dialog.ok(lang(29999), lang(39409)) @@ -758,8 +759,8 @@ class LibrarySync(Thread): for thread in threads: # Threads might already have quit by themselves (e.g. Kodi exit) try: - thread.stop_thread() - except AttributeError: + thread.stopThread() + except: pass log.debug("Stop sent to all threads") # Wait till threads are indeed dead @@ -804,7 +805,7 @@ class LibrarySync(Thread): # PROCESS MOVIES ##### self.updatelist = [] for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False # Get items per view viewId = view['id'] @@ -825,7 +826,7 @@ class LibrarySync(Thread): log.info("Processed view") # Update viewstate for EVERY item for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -897,7 +898,7 @@ class LibrarySync(Thread): # PROCESS TV Shows ##### self.updatelist = [] for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False # Get items per view viewId = view['id'] @@ -926,7 +927,7 @@ class LibrarySync(Thread): # PROCESS TV Seasons ##### # Cycle through tv shows for tvShowId in allPlexTvShowsId: - if self.thread_stopped(): + if self.threadStopped(): return False # Grab all seasons to tvshow from PMS seasons = GetAllPlexChildren(tvShowId) @@ -951,7 +952,7 @@ class LibrarySync(Thread): # PROCESS TV Episodes ##### # Cycle through tv shows for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False # Grab all episodes to tvshow from PMS episodes = GetAllPlexLeaves(view['id']) @@ -986,7 +987,7 @@ class LibrarySync(Thread): # Update viewstate: for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1023,7 +1024,7 @@ class LibrarySync(Thread): for kind in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_SONG): - if self.thread_stopped(): + if self.threadStopped(): return False log.debug("Start processing music %s" % kind) self.allKodiElementsId = {} @@ -1040,7 +1041,7 @@ class LibrarySync(Thread): # Update viewstate for EVERY item for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False self.PlexUpdateWatched(view['id'], itemType) @@ -1065,7 +1066,7 @@ class LibrarySync(Thread): except ValueError: pass for view in views: - if self.thread_stopped(): + if self.threadStopped(): return False # Get items per view itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) @@ -1171,7 +1172,7 @@ class LibrarySync(Thread): now = getUnixTimestamp() deleteListe = [] for i, item in enumerate(self.itemsToProcess): - if self.thread_stopped(): + if self.threadStopped(): # Chances are that Kodi gets shut down break if item['state'] == 9: @@ -1276,8 +1277,8 @@ class LibrarySync(Thread): # movie or episode) continue typus = int(item.get('type', 0)) - status = int(item.get('state', 0)) - if status == 9 or (typus in (1, 4, 10) and status == 5): + state = int(item.get('state', 0)) + if state == 9 or (typus in (1, 4, 10) and state == 5): # Only process deleted items OR movies, episodes, tracks/songs plex_id = str(item.get('itemID', '0')) if plex_id == '0': @@ -1285,7 +1286,7 @@ class LibrarySync(Thread): continue try: if (now - self.just_processed[plex_id] < - self.ignore_just_processed and status != 9): + self.ignore_just_processed and state != 9): log.debug('We just processed %s: ignoring' % plex_id) continue except KeyError: @@ -1298,7 +1299,7 @@ class LibrarySync(Thread): else: # Haven't added this element to the queue yet self.itemsToProcess.append({ - 'state': status, + 'state': state, 'type': typus, 'ratingKey': plex_id, 'timestamp': getUnixTimestamp(), @@ -1314,8 +1315,8 @@ class LibrarySync(Thread): with plexdb.Get_Plex_DB() as plex_db: for item in data: # Drop buffering messages immediately - status = item.get('state') - if status == 'buffering': + state = item.get('state') + if state == 'buffering': continue ratingKey = item.get('ratingKey') kodiInfo = plex_db.getItem_byId(ratingKey) @@ -1334,7 +1335,8 @@ class LibrarySync(Thread): } else: # PMS is ours - get all current sessions - self.sessionKeys = GetPMSStatus(state.PLEX_TOKEN) + self.sessionKeys = GetPMSStatus( + window('plex_token')) log.debug('Updated current sessions. They are: %s' % self.sessionKeys) if sessionKey not in self.sessionKeys: @@ -1347,7 +1349,8 @@ 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 (not state.PLEX_TOKEN and currSess['userId'] == '1'): + if (window('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 @@ -1391,7 +1394,7 @@ class LibrarySync(Thread): 'file_id': kodiInfo[1], 'kodi_type': kodiInfo[4], 'viewOffset': resume, - 'state': status, + 'state': state, 'duration': currSess['duration'], 'viewCount': currSess['viewCount'], 'lastViewedAt': DateToKodi(getUnixTimestamp()) @@ -1430,7 +1433,6 @@ 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 @@ -1441,8 +1443,8 @@ class LibrarySync(Thread): def run_internal(self): # Re-assign handles to have faster calls - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended installSyncDone = self.installSyncDone enableBackgroundSync = self.enableBackgroundSync fullSync = self.fullSync @@ -1474,12 +1476,12 @@ class LibrarySync(Thread): if settings('FanartTV') == 'true': self.fanartthread.start() - while not thread_stopped(): + while not threadStopped(): # In the event the server goes offline - while thread_suspended(): + while threadSuspended(): # Set in service.py - if thread_stopped(): + if threadStopped(): # Abort was requested while waiting. We should exit log.info("###===--- LibrarySync Stopped ---===###") return @@ -1521,7 +1523,6 @@ 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() @@ -1546,7 +1547,6 @@ 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,26 +1565,23 @@ class LibrarySync(Thread): break # Currently no db scan, so we can start a new scan - elif state.DB_SCAN is False: + elif window('plex_dbScan') != "true": # 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 @@ -1605,7 +1602,6 @@ 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) @@ -1617,37 +1613,31 @@ 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 thread_stopped(): + if fullSync() is False and not threadStopped(): 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 new file mode 100644 index 00000000..c9827fac --- /dev/null +++ b/resources/lib/monitor_kodi_play.py @@ -0,0 +1,41 @@ +# -*- 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(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/playback_starter.py b/resources/lib/playback_starter.py index 836f587d..b6b14c18 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -152,12 +152,12 @@ class Playback_Starter(Thread): pickle_me(result) def run(self): - queue = self.mgr.command_pipeline.playback_queue + queue = self.mgr.monitor_kodi_play.playback_queue log.info("----===## Starting Playback_Starter ##===----") while True: item = queue.get() if item is None: - # Need to shutdown - initiated by command_pipeline + # Need to shutdown - initiated by monitor_kodi_play break else: self.triage(item) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 8e52e071..a496574b 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,13 +5,12 @@ from threading import RLock, Thread from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, ThreadMethods +from utils import window, ThreadMethods, ThreadMethodsAdditionalSuspend import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API from playbackutils import PlaybackUtils import variables as v -import state ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -22,7 +21,8 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### -@ThreadMethods(add_suspends=[state.PMS_STATUS]) +@ThreadMethodsAdditionalSuspend('plex_serverStatus') +@ThreadMethods class Playqueue(Thread): """ Monitors Kodi's playqueues for changes on the Kodi side @@ -153,7 +153,7 @@ class Playqueue(Thread): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.thread_stopped(): + if self.threadStopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -193,8 +193,8 @@ class Playqueue(Thread): log.debug('Done comparing playqueues') def run(self): - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended log.info("----===## Starting PlayQueue client ##===----") # Initialize the playqueues, if Kodi already got items in them for playqueue in self.playqueues: @@ -203,9 +203,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 thread_stopped(): - while thread_suspended(): - if thread_stopped(): + while not threadStopped(): + while threadSuspended(): + if threadStopped(): break sleep(1000) with lock: diff --git a/resources/lib/state.py b/resources/lib/state.py deleted file mode 100644 index 9fffcaea..00000000 --- a/resources/lib/state.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- 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 - -PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 584a1d94..93f5e019 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 +from utils import window, settings, language as lang, ThreadMethods, \ + ThreadMethodsAdditionalSuspend import downloadutils import PlexAPI from PlexFunctions import GetMachineIdentifier -import state ############################################################################### @@ -24,7 +24,8 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_USER_CLIENT]) +@ThreadMethodsAdditionalSuspend('suspend_Userclient') +@ThreadMethods class UserClient(threading.Thread): # Borg - multiple instances, shared state @@ -117,6 +118,25 @@ 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') @@ -151,10 +171,7 @@ class UserClient(threading.Thread): # 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) @@ -185,7 +202,7 @@ class UserClient(threading.Thread): # Give attempts at entering password / selecting user if self.retry >= 2: log.error("Too many retries to login.") - state.PMS_STATUS = 'Stop' + window('plex_serverStatus', value="Stop") dialog.ok(lang(33001), lang(39023)) xbmc.executebuiltin( @@ -267,7 +284,6 @@ class UserClient(threading.Thread): window('plex_authenticated', clear=True) window('pms_token', clear=True) - state.PLEX_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) @@ -275,7 +291,6 @@ class UserClient(threading.Thread): window('currUserId', clear=True) window('plex_username', clear=True) window('plex_restricteduser', clear=True) - state.RESTRICTED_USER = False settings('username', value='') settings('userid', value='') @@ -295,32 +310,32 @@ class UserClient(threading.Thread): def run(self): log.info("----===## Starting UserClient ##===----") - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended - while not thread_stopped(): - while thread_suspended(): - if thread_stopped(): + while not self.threadStopped(): + while self.threadSuspended(): + if self.threadStopped(): break xbmc.sleep(1000) - if state.PMS_STATUS == "Stop": + status = window('plex_serverStatus') + + if status == "Stop": xbmc.sleep(500) continue # Verify the connection status to server - elif state.PMS_STATUS == "restricted": + elif status == "restricted": # Parental control is restricting access self.HasAccess = False - elif state.PMS_STATUS == "401": + elif status == "401": # Unauthorized access, revoke token - state.PMS_STATUS = 'Auth' + window('plex_serverStatus', value="Auth") self.resetClient() xbmc.sleep(3000) if self.auth and (self.currUser is None): # Try to authenticate user - if not state.PMS_STATUS or state.PMS_STATUS == "Auth": + if not status or status == "Auth": # Set auth flag because we no longer need # to authenticate the user self.auth = False @@ -330,9 +345,8 @@ class UserClient(threading.Thread): log.info("Current user: %s" % self.currUser) log.info("Current userId: %s" % self.currUserId) self.retry = 0 - state.SUSPEND_LIBRARY_THREAD = False + window('suspend_LibraryThread', clear=True) window('plex_serverStatus', clear=True) - state.PMS_STATUS = False if not self.auth and (self.currUser is None): # Loop if no server found @@ -340,7 +354,7 @@ class UserClient(threading.Thread): # The status Stop is for when user cancelled password dialog. # Or retried too many times - if server and state.PMS_STATUS != "Stop": + if server and status != "Stop": # Only if there's information found to login log.debug("Server found: %s" % server) self.auth = True @@ -348,4 +362,5 @@ 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 f362a957..32233f75 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, partial +from functools import wraps from calendar import timegm from os.path import join from os import remove, walk, makedirs @@ -25,7 +25,6 @@ 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 ############################################################################### @@ -77,19 +76,6 @@ 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(1) - window('plex_command', value='%s-%s' % (key, value)) - - def settings(setting, value=None): """ Get or add addon setting. Returns unicode @@ -333,7 +319,7 @@ def reset(): return # first stop any db sync - plex_command('STOP_SYNC', 'True') + window('plex_shouldStop', value="true") count = 10 while window('plex_dbScan') == "true": log.debug("Sync is running, will retry: %s..." % count) @@ -920,61 +906,78 @@ def LogTime(func): return wrapper -def ThreadMethods(cls=None, add_stops=None, add_suspends=None): +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): """ Decorator to add the following methods to a threading class: - suspend_thread(): pauses the thread - resume_thread(): resumes the thread - stop_thread(): stopps/kills the thread + suspendThread(): pauses the thread + resumeThread(): resumes the thread + stopThread(): stopps/kills the thread - thread_suspended(): returns True if thread is suspend_thread - thread_stopped(): returns True if thread is stopped (or should stop ;-)) - ALSO stops if PKC should exit + threadSuspended(): returns True if thread is suspend_thread + threadStopped(): returns True if thread is stopped (or should stop ;-)) + ALSO stops if Kodi is exited Also adds the following class attributes: - _thread_stopped - _thread_suspended - - invoke with either - @NewThreadMethods - class MyClass(): - or - @NewThreadMethods(add_stops=[state.SUSPEND_LIBRARY_TRHEAD], - add_suspends=[state.WHATEVER, state.WHATEVER2]) - class MyClass(): + _threadStopped + _threadSuspended """ - if cls is None: - return partial(ThreadMethods, - add_stops=add_stops, - add_suspends=add_suspends) - # Make sure we have an iterable - add_stops = add_stops or [] - add_suspends = add_suspends or [] # Attach new attributes to class - cls._thread_stopped = False - cls._thread_suspended = False + cls._threadStopped = False + cls._threadSuspended = False # Define new class methods and attach them to class - def stop_thread(self): - self._thread_stopped = True - cls.stop_thread = stop_thread + def stopThread(self): + self._threadStopped = True + cls.stopThread = stopThread - def suspend_thread(self): - self._thread_suspended = True - cls.suspend_thread = suspend_thread + def suspendThread(self): + self._threadSuspended = True + cls.suspendThread = suspendThread - def resume_thread(self): - self._thread_suspended = False - cls.resume_thread = resume_thread + def resumeThread(self): + self._threadSuspended = False + cls.resumeThread = resumeThread - def thread_suspended(self): - return self._thread_suspended or any(add_suspends) - cls.thread_suspended = thread_suspended + def threadSuspended(self): + return self._threadSuspended + cls.threadSuspended = threadSuspended - def thread_stopped(self): - return self._thread_stopped or state.STOP_PKC or any(add_stops) - cls.thread_stopped = thread_stopped + def threadStopped(self): + return self._threadStopped or (window('plex_terminateNow') == 'true') + cls.threadStopped = threadStopped # Return class to render this a decorator return cls diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index e04a7f6e..7ded4456 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, ThreadMethods +from utils import window, settings, ThreadMethodsAdditionalSuspend, \ + ThreadMethods from companion import process_command -import state ############################################################################### @@ -22,7 +22,8 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD]) +@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') +@ThreadMethods class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -61,11 +62,11 @@ class WebSocket(Thread): counter = 0 handshake_counter = 0 - thread_stopped = self.thread_stopped - thread_suspended = self.thread_suspended - while not thread_stopped(): + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended + while not threadStopped(): # In the event the server goes offline - while thread_suspended(): + while threadSuspended(): # Set in service.py if self.ws is not None: try: @@ -73,7 +74,7 @@ class WebSocket(Thread): except: pass self.ws = None - if thread_stopped(): + if threadStopped(): # Abort was requested while waiting. We should exit log.info("##===---- %s Stopped ----===##" % self.__class__.__name__) @@ -159,15 +160,16 @@ 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 - # Need to use plex.tv token, if any. NOT user token - if state.PLEX_TOKEN: - uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN + if token: + uri += '?X-Plex-Token=%s' % token sslopt = {} if settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE @@ -216,7 +218,9 @@ class Alexa_Websocket(WebSocket): 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, state.PLEX_TOKEN)) + % (window('currUserId'), + self.plex_client_Id, + window('plex_token'))) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt @@ -248,10 +252,11 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass - def thread_suspended(self): + def threadSuspended(self): """ Overwrite to ignore library sync stuff and allow to check for - RESTRICTED_USER and PLEX_TOKEN + plex_restricteduser """ - return self._thread_suspended or state.RESTRICTED_USER \ - or not state.PLEX_TOKEN + return (self._threadSuspended or + window('plex_restricteduser') == 'true' or + not window('plex_token')) diff --git a/service.py b/service.py index ef174ce2..176607dd 100644 --- a/service.py +++ b/service.py @@ -42,11 +42,10 @@ from playqueue import Playqueue import PlexAPI from PlexCompanion import PlexCompanion -from command_pipeline import Monitor_Window +from monitor_kodi_play import Monitor_Kodi_Play from playback_starter import Playback_Starter from artwork import Image_Cache_Thread import variables as v -import state ############################################################################### @@ -106,6 +105,7 @@ class Service(): # Reset window props for profile switch properties = [ + "plex_online", "plex_serverStatus", "plex_onWake", "plex_dbCheck", "plex_kodiScan", "plex_shouldStop", "currUserId", "plex_dbScan", @@ -113,9 +113,10 @@ class Service(): "plex_runLibScan", "plex_username", "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_command", "plex_result", "plex_force_transcode_pix" + "plex_play_new_item", "plex_result", "plex_force_transcode_pix" ] for prop in properties: window(prop, clear=True) @@ -140,8 +141,8 @@ class Service(): kodiProfile = v.KODI_PROFILE # Detect playback start early on - self.command_pipeline = Monitor_Window(self) - self.command_pipeline.start() + self.monitor_kodi_play = Monitor_Kodi_Play(self) + self.monitor_kodi_play.start() # Server auto-detect initialsetup.InitialSetup().setup() @@ -260,7 +261,7 @@ class Service(): self.server_online = False window('plex_online', value="false") # Suspend threads - state.SUSPEND_LIBRARY_THREAD = True + window('suspend_LibraryThread', value='true') log.error("Plex Media Server went offline") if settings('show_pms_offline') == 'true': dialog('notification', @@ -300,7 +301,7 @@ class Service(): if window('plex_authenticated') == 'true': # Server got offline when we were authenticated. # Hence resume threads - state.SUSPEND_LIBRARY_THREAD = False + window('suspend_LibraryThread', clear=True) # Start the userclient thread if not self.user_running: @@ -320,7 +321,27 @@ class Service(): # Terminating PlexKodiConnect # Tell all threads to terminate (e.g. several lib sync threads) - state.STOP_PKC = True + 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') try: downloadutils.DownloadUtils().stopSession() except: @@ -328,7 +349,6 @@ 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 From 6980a09171fba9cfc2c2df50bcf28e4532de3a59 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:50:24 +0200 Subject: [PATCH 17/58] Fix TypeError for Plex Companion --- resources/lib/playqueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index a496574b..60656813 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -157,7 +157,7 @@ class Playqueue(Thread): # Chances are that we got an empty Kodi playlist due to # Kodi exit return - if (old_item['file'].startswith('plugin://') and + if (old_item.file.startswith('plugin://') and not old_item['file'].startswith(PLUGIN)): # Ignore media by other addons continue From 9a85c14c1d6d8c3cb07009c1f45f227701f26e46 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 16:53:51 +0200 Subject: [PATCH 18/58] Update Czech --- .../resource.language.cs_CZ/strings.po | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/language/resource.language.cs_CZ/strings.po b/resources/language/resource.language.cs_CZ/strings.po index f00c289a..4397bce7 100644 --- a/resources/language/resource.language.cs_CZ/strings.po +++ b/resources/language/resource.language.cs_CZ/strings.po @@ -2051,3 +2051,23 @@ msgid "" msgstr "" "Byla zjištěna nová hudební knihovna Plexu. Omlouváme se, ale pro provedení " "změn budeme muset restartovat Kodi." + +# Shown during sync process +msgctxt "#39712" +msgid "downloaded" +msgstr "staženo" + +# Shown during sync process +msgctxt "#39713" +msgid "processed" +msgstr "zpracováno" + +# Shown during sync process +msgctxt "#39714" +msgid "Sync" +msgstr "Synchronizace" + +# Shown during sync process +msgctxt "#39715" +msgid "items" +msgstr "položek" From 3a2f4b8c2b918d7ebcb7682e37b336d113a7fadc Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 14 May 2017 17:01:57 +0200 Subject: [PATCH 19/58] Version bump --- README.md | 3 ++- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c14e0e8..9c9b2ea5 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.18-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..1c9b86d9 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -44,7 +44,14 @@ Gebruik op eigen risico 使用風險由您自己承擔 Usar a su propio riesgo - version 1.7.17 (beta only) + 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..d42d018d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +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 From a63b90dd5fb4c1052865e097a941102bf5fea6d0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 10:09:50 +0200 Subject: [PATCH 20/58] Revert "Revert "Code refactoring: avoid window vars in loops"" This reverts commit b6197231fa7c858c880df7b9abe296cf290b1c4e. --- default.py | 6 +- resources/lib/PlexAPI.py | 6 +- resources/lib/PlexCompanion.py | 16 +-- resources/lib/artwork.py | 25 ++-- resources/lib/command_pipeline.py | 68 ++++++++++ resources/lib/downloadutils.py | 5 +- resources/lib/entrypoint.py | 35 +++--- resources/lib/initialsetup.py | 3 +- resources/lib/library_sync/fanart.py | 19 ++- resources/lib/library_sync/get_metadata.py | 12 +- .../lib/library_sync/process_metadata.py | 11 +- resources/lib/library_sync/sync_info.py | 10 +- resources/lib/librarysync.py | 86 +++++++------ resources/lib/monitor_kodi_play.py | 41 ------ resources/lib/playback_starter.py | 4 +- resources/lib/playqueue.py | 18 +-- resources/lib/state.py | 22 ++++ resources/lib/userclient.py | 59 ++++----- resources/lib/utils.py | 117 +++++++++--------- resources/lib/websocket_client.py | 37 +++--- service.py | 38 ++---- 21 files changed, 327 insertions(+), 311 deletions(-) create mode 100644 resources/lib/command_pipeline.py delete mode 100644 resources/lib/monitor_kodi_play.py create mode 100644 resources/lib/state.py 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/lib/PlexAPI.py b/resources/lib/PlexAPI.py index fbd28e7c..282c5104 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 ############################################################################### @@ -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' @@ -2563,7 +2566,8 @@ class API(): if forceCheck is False: # Validate the path is correct with user intervention if self.askToValidate(path): - window('plex_shouldStop', value="true") + import state + 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 9b961c5f..48f6e831 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, ThreadMethods 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 +@ThreadMethods(add_suspends=[state.PMS_STATUS]) class PlexCompanion(Thread): """ """ @@ -164,8 +164,8 @@ class PlexCompanion(Thread): 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() @@ -213,12 +213,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: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 5d3ba04f..4a6e352d 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,7 +13,8 @@ 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 + ThreadMethods, dialog, exists_dir +import state # Disable annoying requests warnings import requests.packages.urllib3 @@ -126,8 +127,8 @@ def double_urldecode(text): return unquote(unquote(text)) -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@ThreadMethods(add_stops=[state.STOP_SYNC], + add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN]) class Image_Cache_Thread(Thread): xbmc_host = 'localhost' xbmc_port, xbmc_username, xbmc_password = setKodiWebServerDetails() @@ -140,22 +141,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 +173,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') diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py new file mode 100644 index 00000000..9be330a6 --- /dev/null +++ b/resources/lib/command_pipeline.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +from threading import Thread +from Queue import Queue + +from xbmc import sleep + +from utils import window, ThreadMethods +import state + +############################################################################### +log = logging.getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethods +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 + 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..d1c07d9f 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 @@ -274,10 +276,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..3bdfac4d 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 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,6 +81,7 @@ def togglePlexTV(): settings('plex_status', value="Not logged in to plex.tv") window('plex_token', clear=True) + plex_command('PLEX_TOKEN', '') window('plex_username', clear=True) else: log.info('Login to plex.tv') @@ -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)') @@ -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..9c41f924 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 ############################################################################### @@ -496,7 +497,7 @@ 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: diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 7f9fc074..483da89a 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -5,11 +5,11 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ - ThreadMethodsAdditionalSuspend +from utils import ThreadMethods, window import plexdb_functions as plexdb import itemtypes import variables as v +import state ############################################################################### @@ -18,9 +18,8 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN], + add_stops=[state.STOP_SYNC]) class Process_Fanart_Thread(Thread): """ Threaded download of additional fanart in the background @@ -55,14 +54,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..4aa44266 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -5,9 +5,10 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, window +from utils import ThreadMethods, window from PlexFunctions import GetPlexMetadata, GetAllPlexChildren import sync_info +import state ############################################################################### @@ -16,8 +17,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. @@ -48,7 +48,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 +79,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..7b44ed33 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -5,19 +5,18 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods +from utils import ThreadMethods import itemtypes import sync_info +import state ############################################################################### - log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.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 +69,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..13caec52 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -4,7 +4,8 @@ from threading import Thread, Lock from xbmc import sleep -from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang +from utils import ThreadMethods, language as lang +import state ############################################################################### @@ -18,8 +19,7 @@ LOCK = Lock() ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. @@ -53,13 +53,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..c1362531 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,\ + ThreadMethods, 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 + advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes 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 +@ThreadMethods(add_stops=[state.STOP_SYNC], + add_suspends=[state.SUSPEND_LIBRARY_THREAD]) class LibrarySync(Thread): """ """ @@ -300,7 +299,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 +322,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)) @@ -759,8 +758,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 +804,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 +825,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 +897,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 +926,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 +951,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 +986,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 +1023,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 +1040,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 +1065,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 +1171,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 +1276,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 +1285,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 +1298,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 +1314,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 +1334,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,8 +1347,7 @@ 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 @@ -1394,7 +1391,7 @@ 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()) @@ -1433,6 +1430,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 +1441,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 @@ -1476,12 +1474,12 @@ class LibrarySync(Thread): 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 @@ -1523,6 +1521,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 +1546,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 +1565,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 +1605,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 +1617,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 c9827fac..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(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/playback_starter.py b/resources/lib/playback_starter.py index b6b14c18..836f587d 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -152,12 +152,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/playqueue.py b/resources/lib/playqueue.py index 60656813..7e716559 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,12 +5,13 @@ 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, ThreadMethods import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API from playbackutils import PlaybackUtils import variables as v +import state ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -21,8 +22,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### -@ThreadMethodsAdditionalSuspend('plex_serverStatus') -@ThreadMethods +@ThreadMethods(add_suspends=[state.PMS_STATUS]) class Playqueue(Thread): """ Monitors Kodi's playqueues for changes on the Kodi side @@ -153,7 +153,7 @@ class Playqueue(Thread): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.threadStopped(): + if self.thread_stopped(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -193,8 +193,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 +203,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/state.py b/resources/lib/state.py new file mode 100644 index 00000000..9fffcaea --- /dev/null +++ b/resources/lib/state.py @@ -0,0 +1,22 @@ +# -*- 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 + +PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 93f5e019..584a1d94 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, ThreadMethods import downloadutils import PlexAPI from PlexFunctions import GetMachineIdentifier +import state ############################################################################### @@ -24,8 +24,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_Userclient') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_USER_CLIENT]) class UserClient(threading.Thread): # Borg - multiple instances, shared state @@ -118,25 +117,6 @@ 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') @@ -171,7 +151,10 @@ class UserClient(threading.Thread): # 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) @@ -202,7 +185,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( @@ -284,6 +267,7 @@ class UserClient(threading.Thread): window('plex_authenticated', clear=True) window('pms_token', clear=True) + state.PLEX_TOKEN = None window('plex_token', clear=True) window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) @@ -291,6 +275,7 @@ class UserClient(threading.Thread): window('currUserId', clear=True) window('plex_username', clear=True) window('plex_restricteduser', clear=True) + state.RESTRICTED_USER = False settings('username', value='') settings('userid', value='') @@ -310,32 +295,32 @@ class UserClient(threading.Thread): 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' self.resetClient() 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 @@ -345,8 +330,9 @@ class UserClient(threading.Thread): log.info("Current user: %s" % self.currUser) log.info("Current userId: %s" % self.currUserId) 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 +340,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 +348,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 32233f75..f362a957 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(1) + window('plex_command', value='%s-%s' % (key, value)) + + def settings(setting, value=None): """ Get or add addon setting. Returns unicode @@ -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) @@ -906,78 +920,61 @@ 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 ThreadMethods(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 suspend_thread + thread_stopped(): returns True if thread is stopped (or should stop ;-)) + ALSO stops if PKC should exit Also adds the following class attributes: - _threadStopped - _threadSuspended + _thread_stopped + _thread_suspended + + invoke with either + @NewThreadMethods + class MyClass(): + or + @NewThreadMethods(add_stops=[state.SUSPEND_LIBRARY_TRHEAD], + add_suspends=[state.WHATEVER, state.WHATEVER2]) + class MyClass(): """ + if cls is None: + return partial(ThreadMethods, + add_stops=add_stops, + add_suspends=add_suspends) + # Make sure we have an iterable + add_stops = add_stops or [] + add_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): + return self._thread_suspended or any(add_suspends) + cls.thread_suspended = thread_suspended - def threadStopped(self): - return self._threadStopped or (window('plex_terminateNow') == 'true') - cls.threadStopped = threadStopped + def thread_stopped(self): + return self._thread_stopped or state.STOP_PKC or any(add_stops) + cls.thread_stopped = thread_stopped # Return class to render this a decorator return cls diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 7ded4456..e04a7f6e 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, ThreadMethods from companion import process_command +import state ############################################################################### @@ -22,8 +22,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethods +@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD]) class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -62,11 +61,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 +73,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__) @@ -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 @@ -218,9 +216,7 @@ class Alexa_Websocket(WebSocket): 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'))) + % (window('currUserId'), self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt @@ -252,11 +248,10 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass - def threadSuspended(self): + def thread_suspended(self): """ Overwrite to ignore library sync stuff and allow to check for - plex_restricteduser + RESTRICTED_USER and PLEX_TOKEN """ - return (self._threadSuspended or - window('plex_restricteduser') == 'true' or - not window('plex_token')) + return self._thread_suspended or state.RESTRICTED_USER \ + or not state.PLEX_TOKEN diff --git a/service.py b/service.py index 176607dd..ef174ce2 100644 --- a/service.py +++ b/service.py @@ -42,10 +42,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 ############################################################################### @@ -105,7 +106,6 @@ class Service(): # Reset window props for profile switch properties = [ - "plex_online", "plex_serverStatus", "plex_onWake", "plex_dbCheck", "plex_kodiScan", "plex_shouldStop", "currUserId", "plex_dbScan", @@ -113,10 +113,9 @@ class Service(): "plex_runLibScan", "plex_username", "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) @@ -141,8 +140,8 @@ class Service(): 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() @@ -261,7 +260,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', @@ -301,7 +300,7 @@ class Service(): if window('plex_authenticated') == 'true': # 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: @@ -321,27 +320,7 @@ class Service(): # 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 +328,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 From bce800abc2b37350a6f64dcf78f742539cb03aea Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 10:09:57 +0200 Subject: [PATCH 21/58] Revert "Revert "Try shutting down PKC more cleanly"" This reverts commit a1f1e332b321a9a251c107e3e427338ff9312999. --- resources/lib/kodimonitor.py | 5 +++++ service.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) 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/service.py b/service.py index ef174ce2..08119cb5 100644 --- a/service.py +++ b/service.py @@ -133,9 +133,16 @@ 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 @@ -161,7 +168,7 @@ class Service(): welcome_msg = True counter = 0 - while not monitor.abortRequested(): + while not __stop_PKC(): if tryEncode(window('plex_kodiProfile')) != kodiProfile: # Profile change happened, terminate this thread and others @@ -241,14 +248,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 @@ -316,7 +322,6 @@ 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) From ba9a24d70ebf041f8903f8622a4e71b9ab186b17 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 10:10:10 +0200 Subject: [PATCH 22/58] Revert "Revert "Remove obsolete import"" This reverts commit 40b597987927d0f365638f8a70d26e4ea2220fc6. --- service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.py b/service.py index 08119cb5..0bfd43b7 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 ############################################################################### From 2615ecfd793e51f744db7dd1a5dabd108d35cc16 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 10:10:14 +0200 Subject: [PATCH 23/58] Revert "Revert "Also check authentication via state"" This reverts commit 05aa97c0adc73fd01c0380313d34b2cca8a33ae6. --- resources/lib/playback_starter.py | 3 ++- resources/lib/state.py | 2 ++ resources/lib/userclient.py | 2 ++ service.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 836f587d..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 diff --git a/resources/lib/state.py b/resources/lib/state.py index 9fffcaea..6d9df0be 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -19,4 +19,6 @@ SUSPEND_USER_CLIENT = False # Plex home user? Then "False". Along with window('plex_restricteduser') RESTRICTED_USER = False +# Along with window('plex_authenticated') +AUTHENTICATED = False PLEX_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 584a1d94..5757c702 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -159,6 +159,7 @@ class UserClient(threading.Thread): 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') @@ -266,6 +267,7 @@ 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) diff --git a/service.py b/service.py index 0bfd43b7..a37d60c1 100644 --- a/service.py +++ b/service.py @@ -303,7 +303,7 @@ 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 state.SUSPEND_LIBRARY_THREAD = False From 06727fca712f506e2a5ed087026566fa185e3eea Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 13:55:24 +0200 Subject: [PATCH 24/58] Fix thread methods --- resources/lib/PlexAPI.py | 1 - resources/lib/PlexCompanion.py | 5 +- resources/lib/artwork.py | 7 +-- resources/lib/command_pipeline.py | 4 +- resources/lib/library_sync/fanart.py | 7 +-- resources/lib/library_sync/get_metadata.py | 5 +- .../lib/library_sync/process_metadata.py | 5 +- resources/lib/library_sync/sync_info.py | 5 +- resources/lib/librarysync.py | 6 +- resources/lib/playqueue.py | 5 +- resources/lib/state.py | 5 +- resources/lib/userclient.py | 4 +- resources/lib/utils.py | 57 ++++++++++++------- resources/lib/websocket_client.py | 17 ++---- 14 files changed, 68 insertions(+), 65 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 282c5104..7a6ce0c6 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2566,7 +2566,6 @@ class API(): if forceCheck is False: # Validate the path is correct with user intervention if self.askToValidate(path): - import state state.STOP_SYNC = True path = None window('plex_pathverified', value='true') diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index 48f6e831..af2971e8 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -7,14 +7,13 @@ from urllib import urlencode from xbmc import sleep, executebuiltin -from utils import settings, 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 ############################################################################### @@ -23,7 +22,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.PMS_STATUS]) +@thread_methods(add_suspends=['PMS_STATUS']) class PlexCompanion(Thread): """ """ diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 4a6e352d..54e1d9a3 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,8 +13,7 @@ from xbmc import executeJSONRPC, sleep, translatePath from xbmcvfs import exists from utils import window, settings, language as lang, kodiSQL, tryEncode, \ - ThreadMethods, dialog, exists_dir -import state + thread_methods, dialog, exists_dir # Disable annoying requests warnings import requests.packages.urllib3 @@ -127,8 +126,8 @@ def double_urldecode(text): return unquote(unquote(text)) -@ThreadMethods(add_stops=[state.STOP_SYNC], - add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN]) +@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() diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 9be330a6..2d6dcb35 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -6,7 +6,7 @@ from Queue import Queue from xbmc import sleep -from utils import window, ThreadMethods +from utils import window, thread_methods import state ############################################################################### @@ -15,7 +15,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods +@thread_methods class Monitor_Window(Thread): """ Monitors window('plex_command') for new entries that we need to take care diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index 483da89a..1fdcb4e7 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -5,11 +5,10 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods, window +from utils import thread_methods import plexdb_functions as plexdb import itemtypes import variables as v -import state ############################################################################### @@ -18,8 +17,8 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD, state.DB_SCAN], - add_stops=[state.STOP_SYNC]) +@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 diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index 4aa44266..ed3e187e 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -5,10 +5,9 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods, window +from utils import thread_methods, window from PlexFunctions import GetPlexMetadata, GetAllPlexChildren import sync_info -import state ############################################################################### @@ -17,7 +16,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) class Threaded_Get_Metadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index 7b44ed33..c4c599a4 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -5,10 +5,9 @@ from Queue import Empty from xbmc import sleep -from utils import ThreadMethods +from utils import thread_methods import itemtypes import sync_info -import state ############################################################################### log = getLogger("PLEX."+__name__) @@ -16,7 +15,7 @@ log = getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@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 diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index 13caec52..b2dd98d8 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -4,8 +4,7 @@ from threading import Thread, Lock from xbmc import sleep -from utils import ThreadMethods, language as lang -import state +from utils import thread_methods, language as lang ############################################################################### @@ -19,7 +18,7 @@ LOCK = Lock() ############################################################################### -@ThreadMethods(add_stops=[state.SUSPEND_LIBRARY_THREAD]) +@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD']) class Threaded_Show_Sync_Info(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index c1362531..71fb096a 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -10,7 +10,7 @@ import xbmcgui from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ - ThreadMethods, create_actor_db_index, dialog, LogTime, getScreensaver,\ + thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes import downloadutils @@ -38,8 +38,8 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_stops=[state.STOP_SYNC], - add_suspends=[state.SUSPEND_LIBRARY_THREAD]) +@thread_methods(add_stops=['STOP_SYNC'], + add_suspends=['SUSPEND_LIBRARY_THREAD']) class LibrarySync(Thread): """ """ diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 7e716559..8c156482 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,13 +5,12 @@ from threading import RLock, Thread from xbmc import sleep, Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO -from utils import window, ThreadMethods +from utils import window, thread_methods import playlist_func as PL from PlexFunctions import ConvertPlexToKodiTime, GetAllPlexChildren from PlexAPI import API from playbackutils import PlaybackUtils import variables as v -import state ############################################################################### log = logging.getLogger("PLEX."+__name__) @@ -22,7 +21,7 @@ PLUGIN = 'plugin://%s' % v.ADDON_ID ############################################################################### -@ThreadMethods(add_suspends=[state.PMS_STATUS]) +@thread_methods(add_suspends=['PMS_STATUS']) class Playqueue(Thread): """ Monitors Kodi's playqueues for changes on the Kodi side diff --git a/resources/lib/state.py b/resources/lib/state.py index 6d9df0be..46bf020a 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -4,13 +4,14 @@ # 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' +# 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 diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 5757c702..e544dd3a 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -10,7 +10,7 @@ import xbmcaddon from xbmcvfs import exists -from utils import window, settings, language as lang, ThreadMethods +from utils import window, settings, language as lang, thread_methods import downloadutils import PlexAPI @@ -24,7 +24,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_USER_CLIENT]) +@thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) class UserClient(threading.Thread): # Borg - multiple instances, shared state diff --git a/resources/lib/utils.py b/resources/lib/utils.py index f362a957..b767b25d 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -86,7 +86,7 @@ def plex_command(key, value): value: either 'True' or 'False' """ while window('plex_command'): - xbmc.sleep(1) + xbmc.sleep(5) window('plex_command', value='%s-%s' % (key, value)) @@ -920,7 +920,7 @@ def LogTime(func): return wrapper -def ThreadMethods(cls=None, add_stops=None, add_suspends=None): +def thread_methods(cls=None, add_stops=None, add_suspends=None): """ Decorator to add the following methods to a threading class: @@ -928,52 +928,69 @@ def ThreadMethods(cls=None, add_stops=None, add_suspends=None): resume_thread(): resumes the thread stop_thread(): stopps/kills the thread - thread_suspended(): returns True if thread is suspend_thread + thread_suspended(): returns True if thread is suspended thread_stopped(): returns True if thread is stopped (or should stop ;-)) - ALSO stops if PKC should exit + ALSO returns True if PKC should exit Also adds the following class attributes: - _thread_stopped - _thread_suspended + __thread_stopped + __thread_suspended + __stops + __suspends invoke with either - @NewThreadMethods + @Newthread_methods class MyClass(): or - @NewThreadMethods(add_stops=[state.SUSPEND_LIBRARY_TRHEAD], - add_suspends=[state.WHATEVER, state.WHATEVER2]) + @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(ThreadMethods, + return partial(thread_methods, add_stops=add_stops, add_suspends=add_suspends) - # Make sure we have an iterable - add_stops = add_stops or [] - add_suspends = add_suspends or [] + # 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._thread_stopped = False - cls._thread_suspended = False + cls.__thread_stopped = False + cls.__thread_suspended = False # Define new class methods and attach them to class def stop_thread(self): - self._thread_stopped = True + self.__thread_stopped = True cls.stop_thread = stop_thread def suspend_thread(self): - self._thread_suspended = True + self.__thread_suspended = True cls.suspend_thread = suspend_thread def resume_thread(self): - self._thread_suspended = False + self.__thread_suspended = False cls.resume_thread = resume_thread def thread_suspended(self): - return self._thread_suspended or any(add_suspends) + 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 thread_stopped(self): - return self._thread_stopped or state.STOP_PKC or any(add_stops) + 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 diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index e04a7f6e..3c9a0264 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -11,7 +11,7 @@ from ssl import CERT_NONE from xbmc import sleep -from utils import window, settings, ThreadMethods +from utils import window, settings, thread_methods from companion import process_command import state @@ -22,7 +22,7 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethods(add_suspends=[state.SUSPEND_LIBRARY_THREAD]) +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -140,10 +140,10 @@ 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: @@ -209,6 +209,7 @@ class PMS_Websocket(WebSocket): window('plex_online', value='false') +@thread_methods(add_suspends=['RESTRICTED_USER', 'PLEX_TOKEN']) class Alexa_Websocket(WebSocket): """ Websocket connection to talk to Amazon Alexa @@ -247,11 +248,3 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass - - def thread_suspended(self): - """ - Overwrite to ignore library sync stuff and allow to check for - RESTRICTED_USER and PLEX_TOKEN - """ - return self._thread_suspended or state.RESTRICTED_USER \ - or not state.PLEX_TOKEN From 8b99247ba580cce98096ce948436b09edfd19f99 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:02:52 +0200 Subject: [PATCH 25/58] Revert "Revert "Fix user switch"" This reverts commit 219aa2d0abcef81ef935bc5eb52111705c0d7b97. --- resources/lib/userclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index e544dd3a..e5ed9ccd 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -317,6 +317,7 @@ class UserClient(threading.Thread): elif state.PMS_STATUS == "401": # Unauthorized access, revoke token state.PMS_STATUS = 'Auth' + window('plex_serverStatus', value='Auth') self.resetClient() xbmc.sleep(3000) From 7a174f5a7e6ef9853b1aaa4531a85c0c1d21f84c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:20:43 +0200 Subject: [PATCH 26/58] Fix Alexa thread suspend --- resources/lib/websocket_client.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 3c9a0264..7c995632 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -209,7 +209,6 @@ class PMS_Websocket(WebSocket): window('plex_online', value='false') -@thread_methods(add_suspends=['RESTRICTED_USER', 'PLEX_TOKEN']) class Alexa_Websocket(WebSocket): """ Websocket connection to talk to Amazon Alexa @@ -248,3 +247,15 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass + + def thread_suspended(self): + """ + Overwrite method since we need to check for 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 From b532b74b844c1c78d9ba95d51c8b741f48443a4d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:34:52 +0200 Subject: [PATCH 27/58] Fix websockets --- resources/lib/websocket_client.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 7c995632..021cd4cb 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -22,7 +22,6 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) class WebSocket(Thread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) @@ -150,6 +149,7 @@ class WebSocket(Thread): pass +@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD']) class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion @@ -211,8 +211,13 @@ 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' @@ -248,6 +253,24 @@ class Alexa_Websocket(WebSocket): def IOError_response(self): pass + # 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 method since we need to check for plex token From 209cd783155fe2cea67ad642cec082e0d31cb830 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:42:28 +0200 Subject: [PATCH 28/58] Fix WindowsError or alike when deleting video nodes --- resources/lib/videonodes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index f6561ad8..b3c10ee4 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -68,10 +68,10 @@ class VideoNodes(object): "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 From f3edcecd7ddc0b34715fd384816428445b33e66f Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:51:18 +0200 Subject: [PATCH 29/58] Remove obsolete tweaks for advancedsettings.xml --- resources/lib/initialsetup.py | 3 --- resources/lib/librarysync.py | 5 +---- resources/lib/utils.py | 11 ----------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 9c41f924..37489633 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -478,9 +478,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)): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 71fb096a..e8a35783 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -12,7 +12,7 @@ from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ - advancedsettings_tweaks, tryDecode, deletePlaylists, deleteNodes + tryDecode, deletePlaylists, deleteNodes import downloadutils import itemtypes import plexdb_functions as plexdb @@ -1468,9 +1468,6 @@ 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() diff --git a/resources/lib/utils.py b/resources/lib/utils.py index b767b25d..779f6954 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -646,17 +646,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/")) From 858c792d041cae14932a88742c314f4a30881201 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 14:53:12 +0200 Subject: [PATCH 30/58] Remove restart on first setup --- resources/lib/initialsetup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 37489633..ea374718 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -497,9 +497,3 @@ class InitialSetup(): 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! From 6804fcf543f5ed807fd1cc0499c4611b659ad35b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:07:16 +0200 Subject: [PATCH 31/58] Only set advancedsettings tweaks if Music enabled --- resources/lib/librarysync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index e8a35783..e4151d43 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -473,7 +473,7 @@ class LibrarySync(Thread): """ Compare the views to Plex """ - if self.direct_paths is True: + if self.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 From 88d16239091dcdc3071b79605d634df9a40926ad Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:23:39 +0200 Subject: [PATCH 32/58] Companion: Fix AttributeError --- resources/lib/playqueue.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 8c156482..4cb6916e 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -156,10 +156,14 @@ class Playqueue(Thread): # 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 AttributeError: + # were not passed a filename; ignore + pass if new_item.get('id') is None: identical = old_item.file == new_item['file'] else: From 8e70955ded8c7b402a74e9554f0a9b3bfeb1db8e Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:27:32 +0200 Subject: [PATCH 33/58] Move check whether Kodi quit --- resources/lib/playqueue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 4cb6916e..45ca52b1 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -146,16 +146,16 @@ 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.thread_stopped(): - # Chances are that we got an empty Kodi playlist due to - # Kodi exit - return try: if (old_item.file.startswith('plugin://') and not old_item['file'].startswith(PLUGIN)): From 8ae68a7158fdcbc3e4bc55edcf1ae8f3194b000d Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:42:12 +0200 Subject: [PATCH 34/58] Add state for direct paths --- resources/lib/initialsetup.py | 1 + resources/lib/itemtypes.py | 21 ++++++++++----------- resources/lib/librarysync.py | 3 +-- resources/lib/playbackutils.py | 3 ++- resources/lib/state.py | 4 +++- resources/lib/userclient.py | 2 ++ 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index ea374718..1f441db1 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -451,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)): diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f0bac9a3..f997c883 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,8 +36,6 @@ class Items(object): """ def __init__(self): - self.directpath = window('useDirectPaths') == 'true' - self.artwork = artwork.Artwork() self.userid = window('currUserId') self.server = window('pms_server') @@ -268,8 +267,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 +568,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 +891,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 +1115,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 +1633,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/librarysync.py b/resources/lib/librarysync.py index e4151d43..25e1df1c 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -71,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')) @@ -473,7 +472,7 @@ class LibrarySync(Thread): """ Compare the views to Plex """ - if self.direct_paths is True and self.enableMusic 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 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/state.py b/resources/lib/state.py index 46bf020a..6bbec4e3 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -19,7 +19,9 @@ PMS_STATUS = False 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_TOKEN = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index e5ed9ccd..c890da01 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -163,6 +163,8 @@ class UserClient(threading.Thread): 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') From 056d4c86a3cf55a1821fdb6bb339c1755bf86739 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:45:59 +0200 Subject: [PATCH 35/58] Remove obsolete userid attribute --- resources/lib/downloadutils.py | 8 -------- resources/lib/userclient.py | 1 - 2 files changed, 9 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index d1c07d9f..139c0634 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -49,13 +49,6 @@ class DownloadUtils(): 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 @@ -110,7 +103,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 diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index c890da01..37d55d2b 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -287,7 +287,6 @@ class UserClient(threading.Thread): # Reset token in downloads self.doUtils.setToken('') - self.doUtils.setUserId('') self.doUtils.setUsername('') self.currToken = None From 583bfe0777bfc223736fc689846cb5a3f456e8d2 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:47:27 +0200 Subject: [PATCH 36/58] Remove obsolete attribute --- resources/lib/downloadutils.py | 8 -------- resources/lib/userclient.py | 1 - 2 files changed, 9 deletions(-) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 139c0634..a30ab4d9 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -42,13 +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 setServer(self, server): """ Reserved for userclient only @@ -103,7 +96,6 @@ class DownloadUtils(): # Set other stuff self.setServer(window('pms_server')) self.setToken(window('pms_token')) - self.setUsername(window('plex_username')) # Counters to declare PMS dead or unauthorized # Use window variables because start of movies will be called with a diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 37d55d2b..9d92e550 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -287,7 +287,6 @@ class UserClient(threading.Thread): # Reset token in downloads self.doUtils.setToken('') - self.doUtils.setUsername('') self.currToken = None self.auth = True From ae6057cd3645f06c5572317924a221faaf9c048a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:48:41 +0200 Subject: [PATCH 37/58] Remove obsolete module --- resources/lib/read_embyserver.py | 574 ------------------------------- 1 file changed, 574 deletions(-) delete mode 100644 resources/lib/read_embyserver.py diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py deleted file mode 100644 index badedca6..00000000 --- a/resources/lib/read_embyserver.py +++ /dev/null @@ -1,574 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import logging - -import xbmc - -import downloadutils -from utils import window, settings, kodiSQL - -################################################################################################# - -log = logging.getLogger("EMBY."+__name__) - -################################################################################################# - - -class Read_EmbyServer(): - - limitIndex = int(settings('limitindex')) - - - def __init__(self): - - self.doUtils = downloadutils.DownloadUtils().downloadUrl - - self.userId = window('emby_currUser') - self.server = window('emby_server%s' % self.userId) - - - def split_list(self, itemlist, size): - # Split up list in pieces of size. Will generate a list of lists - return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] - - def getItem(self, itemid): - # This will return the full item - item = {} - - result = self.doUtils("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid) - if result: - item = result - - return item - - def getItems(self, itemlist): - - items = [] - - itemlists = self.split_list(itemlist, 50) - for itemlist in itemlists: - # Will return basic information - params = { - - 'Ids': ",".join(itemlist), - 'Fields': "Etag" - } - url = "{server}/emby/Users/{UserId}/Items?&format=json" - result = self.doUtils(url, parameters=params) - if result: - items.extend(result['Items']) - - return items - - def getFullItems(self, itemlist): - - items = [] - - itemlists = self.split_list(itemlist, 50) - for itemlist in itemlists: - - params = { - - "Ids": ",".join(itemlist), - "Fields": ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" - ) - } - url = "{server}/emby/Users/{UserId}/Items?format=json" - result = self.doUtils(url, parameters=params) - if result: - items.extend(result['Items']) - - return items - - def getView_embyId(self, itemid): - # Returns ancestors using embyId - viewId = None - - url = "{server}/emby/Items/%s/Ancestors?UserId={UserId}&format=json" % itemid - for view in self.doUtils(url): - - if view['Type'] == "CollectionFolder": - # Found view - viewId = view['Id'] - - # Compare to view table in emby database - emby = kodiSQL('plex') - cursor_emby = emby.cursor() - query = ' '.join(( - - "SELECT view_name, media_type", - "FROM view", - "WHERE view_id = ?" - )) - cursor_emby.execute(query, (viewId,)) - result = cursor_emby.fetchone() - try: - viewName = result[0] - mediatype = result[1] - except TypeError: - viewName = None - mediatype = None - - cursor_emby.close() - - return [viewName, viewId, mediatype] - - def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, - limit=None, sortorder="Ascending", filter_type=""): - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': recursive, - 'Limit': limit, - 'SortBy': sortby, - 'SortOrder': sortorder, - 'Filters': filter, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - return self.doUtils("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) - - def getTvChannels(self): - - params = { - - 'EnableImages': True, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" - return self.doUtils(url, parameters=params) - - def getTvRecordings(self, groupid): - - if groupid == "root": - groupid = "" - - params = { - - 'GroupId': groupid, - 'EnableImages': True, - 'Fields': ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" - ) - } - url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" - return self.doUtils(url, parameters=params) - - def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None): - - items = { - - 'Items': [], - 'TotalRecordCount': 0 - } - - # Get total number of items - url = "{server}/emby/Users/{UserId}/Items?format=json" - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - '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 = 0 - jump = self.limitIndex - throttled = False - highestjump = 0 - - while index < total: - # Get items by chunk to increase retrieval speed at scale - params = { - - 'ParentId': parentid, - 'IncludeItemTypes': itemtype, - 'CollapseBoxSetItems': False, - 'IsVirtualUnaired': False, - 'IsMissing': False, - 'Recursive': True, - 'StartIndex': index, - 'Limit': jump, - 'SortBy': sortby, - 'SortOrder': "Ascending", - } - if basic: - params['Fields'] = "Etag" - else: - params['Fields'] = ( - - "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," - "CommunityRating,OfficialRating,CumulativeRunTimeTicks," - "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," - "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," - "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," - "MediaSources,VoteCount" - ) - result = self.doUtils(url, parameters=params) - try: - items['Items'].extend(result['Items']) - except TypeError: - # Something happened to the connection - if not throttled: - throttled = True - log.info("Throttle activated.") - - if jump == highestjump: - # We already tried with the highestjump, but it failed. Reset value. - log.info("Reset highest value.") - highestjump = 0 - - # Lower the number by half - if highestjump: - throttled = False - jump = highestjump - log.info("Throttle deactivated.") - else: - jump = int(jump/4) - log.debug("Set jump limit to recover: %s" % jump) - - retry = 0 - while window('emby_online') != "true": - # Wait server to come back online - if retry == 5: - log.info("Unable to reconnect to server. Abort process.") - return items - - retry += 1 - if xbmc.Monitor().waitForAbort(1): - # Abort was requested while waiting. - return items - else: - # Request succeeded - index += jump - - if dialog: - percentage = int((float(index) / float(total))*100) - dialog.update(percentage) - - if jump > 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 From bfe174dc0926daa1fe28bb5d50da837af6abc278 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 15:57:30 +0200 Subject: [PATCH 38/58] Add state for Plex user ID --- resources/lib/itemtypes.py | 1 - resources/lib/librarysync.py | 6 +++--- resources/lib/playutils.py | 1 - resources/lib/plexbmchelper/subscribers.py | 2 -- resources/lib/state.py | 3 +++ resources/lib/userclient.py | 9 +++------ resources/lib/websocket_client.py | 3 ++- service.py | 2 +- 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index f997c883..4cfa311a 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -37,7 +37,6 @@ class Items(object): def __init__(self): self.artwork = artwork.Artwork() - self.userid = window('currUserId') self.server = window('pms_server') def __enter__(self): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 25e1df1c..bbcf5263 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1352,13 +1352,13 @@ class LibrarySync(Thread): # 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.CURRENT_USER_ID or currSess['username'] == window('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.CURRENT_USER_ID, currSess['username'], currSess['userId'])) continue @@ -1397,7 +1397,7 @@ class LibrarySync(Thread): }) log.debug('Update playstate for user %s with id %s: %s' % (window('plex_username'), - window('currUserId'), + state.CURRENT_USER_ID, items[-1])) # Now tell Kodi where we are for item in items: diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 6f6415de..1cc97b44 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -24,7 +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') diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index b8f4e20b..1f8b8aca 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -79,8 +79,6 @@ class SubscriptionManager: ret += ' />' return ret - # pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server')) - # userId = str(WINDOW.getProperty('currUserId')) pbmc_server = window('pms_server') if pbmc_server: (self.protocol, self.server, self.port) = \ diff --git a/resources/lib/state.py b/resources/lib/state.py index 6bbec4e3..2e9784f7 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -22,6 +22,9 @@ 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_TOKEN = None +# Plex ID of the current user (e.g. for plex.tv) as a STRING +CURRENT_USER_ID = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 9d92e550..34d7948f 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -39,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 @@ -122,7 +121,6 @@ class UserClient(threading.Thread): log.debug('Loading current user') doUtils = self.doUtils - self.currUserId = userId self.currToken = usertoken self.currServer = self.getServer() self.ssl = self.getSSLverify() @@ -144,7 +142,7 @@ class UserClient(threading.Thread): return False # Set to windows property - window('currUserId', value=userId) + state.CURRENT_USER_ID = userId or None window('plex_username', value=username) # This is the token for the current PMS (might also be '') window('pms_token', value=self.currToken) @@ -276,7 +274,7 @@ class UserClient(threading.Thread): window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) window('plex_servername', clear=True) - window('currUserId', clear=True) + state.CURRENT_USER_ID = None window('plex_username', clear=True) window('plex_restricteduser', clear=True) state.RESTRICTED_USER = False @@ -291,7 +289,6 @@ class UserClient(threading.Thread): self.currToken = None self.auth = True self.currUser = None - self.currUserId = None self.retry = 0 @@ -331,7 +328,7 @@ 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.CURRENT_USER_ID) self.retry = 0 state.SUSPEND_LIBRARY_THREAD = False window('plex_serverStatus', clear=True) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 021cd4cb..93bed8f0 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -221,7 +221,8 @@ class Alexa_Websocket(WebSocket): 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, state.PLEX_TOKEN)) + % (state.CURRENT_USER_ID, + self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) return uri, sslopt diff --git a/service.py b/service.py index a37d60c1..e35b96ee 100644 --- a/service.py +++ b/service.py @@ -108,7 +108,7 @@ class Service(): 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", "pms_server", "plex_machineIdentifier", "plex_servername", From f2575289b206ffcc403742232719ac66bdd5f033 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 16:00:43 +0200 Subject: [PATCH 39/58] Remove attribute --- resources/lib/playutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index 1cc97b44..47320379 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -24,7 +24,6 @@ class PlayUtils(): self.API = PlexAPI.API(item) self.doUtils = DownloadUtils().downloadUrl - self.server = window('pms_server') self.machineIdentifier = window('plex_machineIdentifier') def getPlayUrl(self, partNumber=None): @@ -334,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: From 9ae68b66f7b4c27e66a11c204488dbf035814ebb Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 16:14:17 +0200 Subject: [PATCH 40/58] Add state for Plex username --- resources/lib/command_pipeline.py | 5 +++++ resources/lib/entrypoint.py | 4 ++-- resources/lib/librarysync.py | 6 +++--- resources/lib/state.py | 1 + resources/lib/userclient.py | 4 ++-- service.py | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 2d6dcb35..64fc799c 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -61,6 +61,11 @@ class Monitor_Window(Thread): 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 diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 3bdfac4d..f690bc01 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -82,7 +82,7 @@ def togglePlexTV(): window('plex_token', clear=True) plex_command('PLEX_TOKEN', '') - window('plex_username', clear=True) + plex_command('PLEX_USERNAME', '') else: log.info('Login to plex.tv') import initialsetup @@ -147,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 diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index bbcf5263..4f67d542 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1354,10 +1354,10 @@ class LibrarySync(Thread): pass elif not (currSess['userId'] == state.CURRENT_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'), + % (state.PLEX_USERNAME, state.CURRENT_USER_ID, currSess['username'], currSess['userId'])) @@ -1396,7 +1396,7 @@ class LibrarySync(Thread): 'lastViewedAt': DateToKodi(getUnixTimestamp()) }) log.debug('Update playstate for user %s with id %s: %s' - % (window('plex_username'), + % (state.PLEX_USERNAME, state.CURRENT_USER_ID, items[-1])) # Now tell Kodi where we are diff --git a/resources/lib/state.py b/resources/lib/state.py index 2e9784f7..684e1aad 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -27,4 +27,5 @@ DIRECT_PATHS = False AUTHENTICATED = False PLEX_TOKEN = None # Plex ID of the current user (e.g. for plex.tv) as a STRING +PLEX_USERNAME = None CURRENT_USER_ID = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index 34d7948f..fa6d9a51 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -143,7 +143,7 @@ class UserClient(threading.Thread): # Set to windows property state.CURRENT_USER_ID = userId or None - window('plex_username', value=username) + 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 @@ -275,7 +275,7 @@ class UserClient(threading.Thread): window('plex_machineIdentifier', clear=True) window('plex_servername', clear=True) state.CURRENT_USER_ID = None - window('plex_username', clear=True) + state.PLEX_USERNAME = None window('plex_restricteduser', clear=True) state.RESTRICTED_USER = False diff --git a/service.py b/service.py index e35b96ee..ec8ca54a 100644 --- a/service.py +++ b/service.py @@ -110,7 +110,7 @@ class Service(): "plex_dbCheck", "plex_kodiScan", "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", "kodiplextimeoffset", "countError", "countUnauthorized", From f7da47f2d5588d51cb94b315802669f2a7d9514a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 16:15:16 +0200 Subject: [PATCH 41/58] Rename Plex user id --- resources/lib/librarysync.py | 6 +++--- resources/lib/state.py | 2 +- resources/lib/userclient.py | 6 +++--- resources/lib/websocket_client.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 4f67d542..336d5d5e 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -1352,13 +1352,13 @@ class LibrarySync(Thread): # Hence must be us (since several users require plex.tv # token for PKC) pass - elif not (currSess['userId'] == state.CURRENT_USER_ID + elif not (currSess['userId'] == state.PLEX_USER_ID or currSess['username'] == state.PLEX_USERNAME): log.debug('Our username %s, userid %s did not match ' 'the session username %s with userid %s' % (state.PLEX_USERNAME, - state.CURRENT_USER_ID, + state.PLEX_USER_ID, currSess['username'], currSess['userId'])) continue @@ -1397,7 +1397,7 @@ class LibrarySync(Thread): }) log.debug('Update playstate for user %s with id %s: %s' % (state.PLEX_USERNAME, - state.CURRENT_USER_ID, + state.PLEX_USER_ID, items[-1])) # Now tell Kodi where we are for item in items: diff --git a/resources/lib/state.py b/resources/lib/state.py index 684e1aad..24fc2522 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -28,4 +28,4 @@ AUTHENTICATED = False PLEX_TOKEN = None # Plex ID of the current user (e.g. for plex.tv) as a STRING PLEX_USERNAME = None -CURRENT_USER_ID = None +PLEX_USER_ID = None diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index fa6d9a51..ca90ab1b 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -142,7 +142,7 @@ class UserClient(threading.Thread): return False # Set to windows property - state.CURRENT_USER_ID = userId or None + 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) @@ -274,7 +274,7 @@ class UserClient(threading.Thread): window('pms_server', clear=True) window('plex_machineIdentifier', clear=True) window('plex_servername', clear=True) - state.CURRENT_USER_ID = None + state.PLEX_USER_ID = None state.PLEX_USERNAME = None window('plex_restricteduser', clear=True) state.RESTRICTED_USER = False @@ -328,7 +328,7 @@ 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" % state.CURRENT_USER_ID) + log.info("Current userId: %s" % state.PLEX_USER_ID) self.retry = 0 state.SUSPEND_LIBRARY_THREAD = False window('plex_serverStatus', clear=True) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index 93bed8f0..0ae40da8 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -221,7 +221,7 @@ class Alexa_Websocket(WebSocket): def getUri(self): self.plex_client_Id = window('plex_client_Id') uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' - % (state.CURRENT_USER_ID, + % (state.PLEX_USER_ID, self.plex_client_Id, state.PLEX_TOKEN)) sslopt = {} log.debug("Uri: %s, sslopt: %s" % (uri, sslopt)) From c7db36bd5dcca54f58943dd3d73429189aa18fad Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 19:20:16 +0200 Subject: [PATCH 42/58] Companion: don't tell seekRange --- resources/lib/plexbmchelper/subscribers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 1f8b8aca..a109d00a 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -75,7 +75,6 @@ class SubscriptionManager: time = 0 ret = "\n"+' Date: Wed, 17 May 2017 19:53:46 +0200 Subject: [PATCH 43/58] Companion: Fix returning OK, status 200 --- resources/lib/plexbmchelper/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index 784a1e77..2bd41e61 100644 --- a/resources/lib/plexbmchelper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -61,7 +61,7 @@ def getXMLHeader(): def getOKMsg(): - return getXMLHeader() + '' + return def timeToMillis(time): From 87d5f7cce9a1257a2506538c4901c6b8c6825271 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 20:14:21 +0200 Subject: [PATCH 44/58] Revert "Companion: Fix returning OK, status 200" This reverts commit cd121defa1547d9aa7a2bd0f91679c01f366e604. --- resources/lib/plexbmchelper/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/functions.py b/resources/lib/plexbmchelper/functions.py index 2bd41e61..784a1e77 100644 --- a/resources/lib/plexbmchelper/functions.py +++ b/resources/lib/plexbmchelper/functions.py @@ -61,7 +61,7 @@ def getXMLHeader(): def getOKMsg(): - return + return getXMLHeader() + '' def timeToMillis(time): From befee050433207f7283b154a46483329e8d4a394 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 20:18:24 +0200 Subject: [PATCH 45/58] Companion: Fix returning OK, status 200 - Also considering Android now --- resources/lib/plexbmchelper/listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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:') From 20cf51432fe20e025ca3576b0544325b0b4a64d1 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Wed, 17 May 2017 20:22:16 +0200 Subject: [PATCH 46/58] Companion: send transient token --- resources/lib/PlexCompanion.py | 3 +++ resources/lib/player.py | 4 ++++ resources/lib/plexbmchelper/subscribers.py | 15 ++++++++++----- resources/lib/state.py | 9 +++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/resources/lib/PlexCompanion.py b/resources/lib/PlexCompanion.py index af2971e8..c172ca38 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/PlexCompanion.py @@ -14,6 +14,7 @@ from PlexFunctions import ParseContainerKey, GetPlexMetadata from PlexAPI import API import player import variables as v +import state ############################################################################### @@ -76,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']) 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/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index a109d00a..9553857e 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -5,6 +5,7 @@ import threading import downloadutils from utils import window import PlexFunctions as pf +import state from functions import * ############################################################################### @@ -68,12 +69,12 @@ 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"+' Date: Wed, 17 May 2017 20:26:06 +0200 Subject: [PATCH 47/58] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c9b2ea5..b9cccfe6 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.18-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.19-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) diff --git a/addon.xml b/addon.xml index 1c9b86d9..02a80536 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -44,7 +44,14 @@ Gebruik op eigen risico 使用風險由您自己承擔 Usar a su propio riesgo - version 1.7.18 (beta only) + 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 diff --git a/changelog.txt b/changelog.txt index d42d018d..201962d1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +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 From 1bbb0e151d8cddfd7bab553c7f0392f97ef294b1 Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 18 May 2017 07:37:28 +0200 Subject: [PATCH 48/58] Increase logging --- resources/lib/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 779f6954..bfbc983e 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -844,7 +844,8 @@ def playlistXSP(mediatype, tagname, viewid, viewtype="", delete=False): % (itemtypes.get(mediatype, mediatype), plname, tagname)) except Exception as e: log.error("Failed to create playlist: %s" % xsppath) - log.error(e) + import traceback + log.exception("Traceback:\n%s" % traceback.format_exc()) return log.info("Successfully added playlist: %s" % tagname) From 6ff20b737a6623618408244d604afe39bdd0769e Mon Sep 17 00:00:00 2001 From: Croneter Date: Thu, 18 May 2017 07:51:17 +0200 Subject: [PATCH 49/58] Increase timeout - Hopefully fixes #282 --- resources/lib/PlexAPI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 7a6ce0c6..0093ef82 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -629,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): From eba270c63df19ac3453445b48146f18be000bc95 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 20 May 2017 18:46:31 +0200 Subject: [PATCH 50/58] Use SSL settings when checking server connection - Hopefully solves #282 --- resources/lib/initialsetup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 1f441db1..f044dfad 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -157,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) From f785b828caf0606571d54cccf9394d0ebb9cd35a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 20 May 2017 18:49:40 +0200 Subject: [PATCH 51/58] Companion: Fix TypeError --- resources/lib/playqueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 45ca52b1..6df21b0d 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -161,7 +161,7 @@ class Playqueue(Thread): not old_item['file'].startswith(PLUGIN)): # Ignore media by other addons continue - except AttributeError: + except (TypeError, AttributeError): # were not passed a filename; ignore pass if new_item.get('id') is None: From b0d1fcfe8a757de84d0ba85bbbd19c83fb51d4cf Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 20 May 2017 18:52:51 +0200 Subject: [PATCH 52/58] Fix TypeError when PMS connection lost --- resources/lib/userclient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index ca90ab1b..587bf1c7 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -127,6 +127,8 @@ class UserClient(threading.Thread): 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, From ad5744f4359718392c191e15db783bcd75ec317b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 20 May 2017 20:24:47 +0200 Subject: [PATCH 53/58] Fix for Windows usernames with non-ASCII chars - Fixes #286 --- resources/lib/PlexAPI.py | 8 +++--- resources/lib/artwork.py | 6 ++--- resources/lib/entrypoint.py | 16 +++++------ resources/lib/librarysync.py | 4 +-- resources/lib/utils.py | 52 ++++++++++++++++-------------------- resources/lib/variables.py | 23 ++++++++-------- resources/lib/videonodes.py | 33 +++++++++++++---------- service.py | 9 ++++--- 8 files changed, 76 insertions(+), 75 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 7a6ce0c6..ac205fbc 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -2553,14 +2553,14 @@ 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: diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 54e1d9a3..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, \ - thread_methods, dialog, exists_dir + thread_methods, dialog, exists_dir, tryDecode # Disable annoying requests warnings import requests.packages.urllib3 @@ -222,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) @@ -423,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/entrypoint.py b/resources/lib/entrypoint.py index f690bc01..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, plex_command + CatchExceptions, JSONRPC, exists_dir, plex_command, tryDecode import downloadutils from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ @@ -489,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://') @@ -502,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, @@ -537,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) @@ -550,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, diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 336d5d5e..6d4571ed 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -12,7 +12,7 @@ from xbmcvfs import exists from utils import window, settings, getUnixTimestamp, sourcesXML,\ thread_methods, create_actor_db_index, dialog, LogTime, getScreensaver,\ setScreensaver, playlistXSP, language as lang, DateToKodi, reset,\ - tryDecode, deletePlaylists, deleteNodes + tryDecode, deletePlaylists, deleteNodes, tryEncode import downloadutils import itemtypes import plexdb_functions as plexdb @@ -1507,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.") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index bfbc983e..396488ea 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -111,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') @@ -125,7 +125,7 @@ def exists_dir(path): answ = 0 else: # Folder exists. Delete file again. - delete(dummyfile) + delete(tryEncode(dummyfile)) answ = 1 return answ @@ -401,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() @@ -425,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) @@ -688,7 +688,7 @@ def sourcesXML(): def passwordsXML(): # To add network credentials - path = xbmc.translatePath("special://userdata/") + path = tryDecode(xbmc.translatePath("special://userdata/")) xmlpath = "%spasswords.xml" % path try: @@ -801,7 +801,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) @@ -810,12 +810,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) @@ -830,28 +830,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) - import traceback - log.exception("Traceback:\n%s" % traceback.format_exc()) - 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'): @@ -859,7 +853,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-'): 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 b3c10ee4..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,9 +62,10 @@ 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: if exists_dir(nodepath): @@ -77,8 +77,10 @@ class VideoNodes(object): # 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/service.py b/service.py index ec8ca54a..601a2a59 100644 --- a/service.py +++ b/service.py @@ -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 @@ -86,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', @@ -170,12 +171,12 @@ class Service(): counter = 0 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: From 46dcec43c92acd98bfb775961408bbfca96d041c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 20 May 2017 20:28:12 +0200 Subject: [PATCH 54/58] Version bump --- README.md | 2 +- addon.xml | 11 +++++++++-- changelog.txt | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b9cccfe6..0f5419d2 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.19-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.20-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) diff --git a/addon.xml b/addon.xml index 02a80536..318c195c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -44,7 +44,14 @@ Gebruik op eigen risico 使用風險由您自己承擔 Usar a su propio riesgo - version 1.7.19 (beta only) + 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 diff --git a/changelog.txt b/changelog.txt index 201962d1..efcc89cd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +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 From 845885edbd2210cbf24ea21e25841be8af8675af Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 22 May 2017 20:38:56 +0200 Subject: [PATCH 55/58] Fix Playback and watched status not syncing - Fixes #300 --- resources/lib/plexbmchelper/subscribers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 9553857e..1d7a8b88 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -156,7 +156,7 @@ class SubscriptionManager: def notifyServer(self, players): for typus, p in players.iteritems(): info = self.playerprops[p.get('playerid')] - # self._sendNotification(info) + self._sendNotification(info) self.lastinfo[typus] = info # Cross the one of the list try: From 66fe13786fd29af26748b65bf70ca729b75be323 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 22 May 2017 20:54:04 +0200 Subject: [PATCH 56/58] Fix PKC syncing progress to wrong account - Fixes #297 --- resources/lib/plexbmchelper/subscribers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 1d7a8b88..0f8c56e4 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -3,6 +3,7 @@ import re import threading import downloadutils +from clientinfo import getXArgsDeviceInfo from utils import window import PlexFunctions as pf import state @@ -169,6 +170,7 @@ class SubscriptionManager: # self._sendNotification(self.lastinfo[typus]) def _sendNotification(self, info): + xargs = getXArgsDeviceInfo() params = { 'containerKey': self.containerKey or "/library/metadata/900000", 'key': self.lastkey or "/library/metadata/900000", @@ -178,7 +180,7 @@ class SubscriptionManager: 'duration': info['duration'] } if state.PLEX_TRANSIENT_TOKEN: - params['token'] = state.PLEX_TRANSIENT_TOKEN + xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN if info.get('playQueueID'): params['containerKey'] = '/playQueues/%s' % info['playQueueID'] params['playQueueVersion'] = info['playQueueVersion'] @@ -187,7 +189,7 @@ class SubscriptionManager: url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) - self.doUtils(url, parameters=params) + self.doUtils(url, parameters=params, headerOptions=xargs) log.debug("Sent server notification with parameters: %s to %s" % (params, url)) From 179e97b20006cb32328bf8b8fd255c9aa98118df Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 22 May 2017 21:31:19 +0200 Subject: [PATCH 57/58] Warn user if a xml cannot be parsed --- .../resource.language.en_gb/strings.po | 5 +++ resources/lib/utils.py | 37 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) 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/utils.py b/resources/lib/utils.py index 396488ea..0a64942b 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -536,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 @@ -620,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 @@ -653,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() @@ -690,18 +712,25 @@ def passwordsXML(): # To add network credentials 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 From d3d74405ab9007155a1f8780e1334b207248176b Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Mon, 22 May 2017 21:41:53 +0200 Subject: [PATCH 58/58] Version bump --- README.md | 2 +- addon.xml | 9 +++++++-- changelog.txt | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0f5419d2..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.20-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) diff --git a/addon.xml b/addon.xml index 318c195c..39c83f4d 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + @@ -44,7 +44,12 @@ Gebruik op eigen risico 使用風險由您自己承擔 Usar a su propio riesgo - version 1.7.20 (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 diff --git a/changelog.txt b/changelog.txt index efcc89cd..37b20604 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +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