diff --git a/default.py b/default.py index f48e0651..4b8ab071 100644 --- a/default.py +++ b/default.py @@ -94,9 +94,6 @@ class Main(): elif mode == 'togglePlexTV': entrypoint.toggle_plex_tv_sign_in() - elif mode == 'resetauth': - entrypoint.reset_authorization() - elif mode == 'passwords': utils.passwords_xml() @@ -111,14 +108,14 @@ class Main(): else: if mode == 'repair': log.info('Requesting repair lib sync') - utils.plex_command('RUN_LIB_SCAN', 'repair') + utils.plex_command('repair-scan') elif mode == 'manualsync': log.info('Requesting full library scan') - utils.plex_command('RUN_LIB_SCAN', 'full') + utils.plex_command('full-scan') elif mode == 'texturecache': log.info('Requesting texture caching of all textures') - utils.plex_command('RUN_LIB_SCAN', 'textures') + utils.plex_command('textures-scan') elif mode == 'chooseServer': entrypoint.choose_pms_server() @@ -128,7 +125,7 @@ class Main(): elif mode == 'fanart': log.info('User requested fanarttv refresh') - utils.plex_command('RUN_LIB_SCAN', 'fanart') + utils.plex_command('fanart-scan') elif '/extrafanart' in path: plexpath = arguments[1:] @@ -158,7 +155,7 @@ class Main(): """ request = '%s&handle=%s' % (argv[2], HANDLE) # Put the request into the 'queue' - utils.plex_command('PLAY', request) + utils.plex_command('PLAY-%s' % request) if HANDLE == -1: # Handle -1 received, not waiting for main thread return diff --git a/resources/lib/app/__init__.py b/resources/lib/app/__init__.py new file mode 100644 index 00000000..1210986f --- /dev/null +++ b/resources/lib/app/__init__.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Used to save PKC's application state and share between modules. Be careful +if you invoke another PKC Python instance (!!) when e.g. PKC.movies is called +""" +from __future__ import absolute_import, division, unicode_literals +from .account import Account +from .application import App +from .connection import Connection +from .libsync import Sync +from .playstate import PlayState + +ACCOUNT = None +APP = None +CONN = None +SYNC = None +PLAYSTATE = None + + +def init(): + global ACCOUNT, APP, CONN, SYNC, PLAYSTATE + ACCOUNT = Account() + APP = App() + CONN = Connection() + SYNC = Sync() + PLAYSTATE = PlayState() diff --git a/resources/lib/app/account.py b/resources/lib/app/account.py new file mode 100644 index 00000000..305ab4ad --- /dev/null +++ b/resources/lib/app/account.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +from .. import utils + +LOG = getLogger('PLEX.account') + + +class Account(object): + def __init__(self): + # Along with window('plex_authenticated') + self.authenticated = False + self._session = None + utils.window('plex_authenticated', clear=True) + self.load() + + def set_authenticated(self): + self.authenticated = True + utils.window('plex_authenticated', value='true') + # Start download session + from .. import downloadutils + self._session = downloadutils.DownloadUtils() + self._session.startSession(reset=True) + + def set_unauthenticated(self): + self.authenticated = False + utils.window('plex_authenticated', clear=True) + + def load(self): + LOG.debug('Loading account settings') + # plex.tv username + self.plex_username = utils.settings('username') or None + # Plex ID of that user (e.g. for plex.tv) as a STRING + self.plex_user_id = utils.settings('userid') or None + # Token for that user for plex.tv + self.plex_token = utils.settings('plexToken') or None + # Plex token for the active PMS for the active user + # (might be diffent to plex_token) + self.pms_token = utils.settings('accessToken') or None + self.avatar = utils.settings('plexAvatar') or None + self.myplexlogin = utils.settings('myplexlogin') == 'true' + + # Plex home user? Then "False" + self.restricted_user = True \ + if utils.settings('plex_restricteduser') == 'true' else False + # Force user to enter Pin if set? + self.force_login = utils.settings('enforceUserLogin') == 'true' + + # Also load these settings to Kodi window variables - they'll be + # available for other PKC Python instances + utils.window('plex_restricteduser', + value='true' if self.restricted_user else 'false') + utils.window('plex_token', value=self.plex_token or '') + utils.window('pms_token', value=self.pms_token or '') + utils.window('plexAvatar', value=self.avatar or '') + LOG.debug('Loaded user %s, %s with plex token %s... and pms token %s...', + self.plex_username, self.plex_user_id, + self.plex_token[:5] if self.plex_token else None, + self.pms_token[:5] if self.pms_token else None) + LOG.debug('User is restricted Home user: %s', self.restricted_user) + + def clear(self): + LOG.debug('Clearing account settings') + self.plex_username = None + self.plex_user_id = None + self.plex_token = None + self.pms_token = None + self.avatar = None + self.restricted_user = None + self.authenticated = False + self._session = None + + utils.settings('username', value='') + utils.settings('userid', value='') + utils.settings('plex_restricteduser', value='') + utils.settings('plexToken', value='') + utils.settings('accessToken', value='') + utils.settings('plexAvatar', value='') + + utils.window('plex_restricteduser', clear=True) + utils.window('plex_token', clear=True) + utils.window('pms_token', clear=True) + utils.window('plexAvatar', clear=True) + utils.window('plex_authenticated', clear=True) diff --git a/resources/lib/app/application.py b/resources/lib/app/application.py new file mode 100644 index 00000000..c2dc4440 --- /dev/null +++ b/resources/lib/app/application.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +import Queue +from threading import Lock, RLock + +from .. import utils + + +class App(object): + """ + This class is used to store variables across PKC modules + """ + def __init__(self, only_reload_settings=False): + self.load_settings() + if only_reload_settings: + return + # Quit PKC? + self.stop_pkc = False + + # Need to lock all methods and functions messing with Plex Companion subscribers + self.lock_subscriber = RLock() + # Need to lock everything messing with Kodi/PKC playqueues + self.lock_playqueues = RLock() + # Necessary to temporarily hold back librarysync/websocket listener when doing + # a full sync + self.lock_playlists = Lock() + + # Plex Companion Queue() + self.companion_queue = Queue.Queue(maxsize=100) + # Command Pipeline Queue() + self.command_pipeline_queue = Queue.Queue() + # Websocket_client queue to communicate with librarysync + self.websocket_queue = Queue.Queue() + + def load_settings(self): + # Number of items to fetch and display in widgets + self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number')) + # Hack to force Kodi widget for "in progress" to show up if it was empty + # before + self.force_reload_skin = utils.settings('forceReloadSkinOnPlaybackStop') == 'true' + # Stemming from the PKC settings.xml + self.kodi_plex_time_offset = float(utils.settings('kodiplextimeoffset')) diff --git a/resources/lib/app/connection.py b/resources/lib/app/connection.py new file mode 100644 index 00000000..5acbc9f3 --- /dev/null +++ b/resources/lib/app/connection.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +from .. import utils, json_rpc as js + +LOG = getLogger('PLEX.connection') + + +class Connection(object): + def __init__(self, only_reload_settings=False): + self.load_webserver() + self.load() + if only_reload_settings: + return + # TODO: Delete + self.pms_server = None + # Plex Media Server Status - along with window('plex_serverStatus') + # Values: + # 'Stop': set if e.g. + # '401': Token has been revoked - PKC yet to delete tokens + # 'Auth': + self.pms_status = False + # Token passed along, e.g. if playback initiated by Plex Companion. Might be + # another user playing something! Token identifies user + self.plex_transient_token = None + + def load_webserver(self): + """ + PKC needs Kodi webserver to work correctly + """ + LOG.debug('Loading Kodi webserver details') + # Kodi webserver details + if js.get_setting('services.webserver') in (None, False): + # Enable the webserver, it is disabled + js.set_setting('services.webserver', True) + self.webserver_host = 'localhost' + self.webserver_port = js.get_setting('services.webserverport') + self.webserver_username = js.get_setting('services.webserverusername') + self.webserver_password = js.get_setting('services.webserverpassword') + + def load(self): + LOG.debug('Loading connection settings') + # Shall we verify SSL certificates? "None" will leave SSL enabled + self.verify_ssl_cert = None if utils.settings('sslverify') == 'true' \ + else False + # Do we have an ssl certificate for PKC we need to use? + self.ssl_cert_path = utils.settings('sslcert') \ + if utils.settings('sslcert') != 'None' else None + + self.machine_identifier = utils.settings('plex_machineIdentifier') or None + self.server_name = utils.settings('plex_servername') or None + self.https = utils.settings('https') == 'true' + self.host = utils.settings('ipaddress') or None + self.port = int(utils.settings('port')) if utils.settings('port') else None + if not self.host: + self.server = None + elif self.https: + self.server = 'https://%s:%s' % (self.host, self.port) + else: + self.server = 'http://%s:%s' % (self.host, self.port) + utils.window('pms_server', value=self.server) + LOG.debug('Set server %s (%s) to %s', + self.server_name, self.machine_identifier, self.server) + + def clear(self): + LOG.debug('Clearing connection settings') + self.machine_identifier = None + self.server_name = None + self.http = None + self.host = None + self.port = None + self.server = None + utils.window('pms_server', clear=True) diff --git a/resources/lib/app/libsync.py b/resources/lib/app/libsync.py new file mode 100644 index 00000000..cc0e9da9 --- /dev/null +++ b/resources/lib/app/libsync.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals + +from .. import utils + + +class Sync(object): + def __init__(self, only_reload_settings=False): + self.load_settings() + if only_reload_settings: + return + # Do we need to run a special library scan? + self.run_lib_scan = None + # Usually triggered by another Python instance - will have to be set (by + # polling window) through e.g. librarysync thread + self.suspend_library_thread = False + # Set if user decided to cancel sync + self.stop_sync = False + # Set during media playback if PKC should not do any syncs. Will NOT + # suspend synching of playstate progress + self.suspend_sync = False + # Could we access the paths? + self.path_verified = False + # Set if a Plex-Kodi DB sync is being done - along with + # window('plex_dbScan') set to 'true' + self.db_scan = False + + def load_settings(self): + # Direct Paths (True) or Addon Paths (False)? Along with + # window('useDirectPaths') + self.direct_paths = True if utils.settings('useDirectPaths') == '1' \ + else False + # Is synching of Plex music enabled? + self.enable_music = utils.settings('enableMusic') == 'true' + # Path remapping mechanism (e.g. smb paths) + # Do we replace \\myserver\path to smb://myserver/path? + self.replace_smb_path = utils.settings('replaceSMB') == 'true' + # Do we generally remap? + self.remap_path = utils.settings('remapSMB') == 'true' + # Mappings for REMAP_PATH: + self.remapSMBmovieOrg = utils.settings('remapSMBmovieOrg') + self.remapSMBmovieNew = utils.settings('remapSMBmovieNew') + self.remapSMBtvOrg = utils.settings('remapSMBtvOrg') + self.remapSMBtvNew = utils.settings('remapSMBtvNew') + self.remapSMBmusicOrg = utils.settings('remapSMBmusicOrg') + self.remapSMBmusicNew = utils.settings('remapSMBmusicNew') + self.remapSMBphotoOrg = utils.settings('remapSMBphotoOrg') + self.remapSMBphotoNew = utils.settings('remapSMBphotoNew') + # Shall we replace custom user ratings with the number of versions available? + self.indicate_media_versions = True \ + if utils.settings('indicate_media_versions') == "true" else False + # Will sync movie trailer differently: either play trailer directly or show + # all the Plex extras for the user to choose + self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true' + # Only sync specific Plex playlists to Kodi? + self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true' + # Only sync specific Kodi playlists to Plex? + self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true' + # Shall we show Kodi dialogs when synching? + self.sync_dialog = utils.settings('dbSyncIndicator') == 'true' + + # How often shall we sync? + self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60 + # Background Sync disabled? + self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false' + # How long shall we wait with synching a new item to make sure Plex got all + # metadata? + self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin')) + # How many threads to download Plex metadata on sync? + self.sync_thread_number = int(utils.settings('syncThreadNumber')) + + # Shall Kodi show dialogs for syncing/caching images? (e.g. images left + # to sync) + self.image_sync_notifications = utils.settings('imageSyncNotifications') == 'true' diff --git a/resources/lib/app/playstate.py b/resources/lib/app/playstate.py new file mode 100644 index 00000000..4c104cac --- /dev/null +++ b/resources/lib/app/playstate.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals + + +class PlayState(object): + # "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate! + template = { + 'type': None, + 'time': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'totaltime': { + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'milliseconds': 0}, + 'speed': 0, + 'shuffled': False, + 'repeat': 'off', + 'position': None, + 'playlistid': None, + 'currentvideostream': -1, + 'currentaudiostream': -1, + 'subtitleenabled': False, + 'currentsubtitle': -1, + 'file': None, + 'kodi_id': None, + 'kodi_type': None, + 'plex_id': None, + 'plex_type': None, + 'container_key': None, + 'volume': 100, + 'muted': False, + 'playmethod': None, + 'playcount': None + } + + def __init__(self): + # Kodi player states - here, initial values are set + self.player_states = { + 0: {}, + 1: {}, + 2: {} + } + # The LAST playstate once playback is finished + self.old_player_states = { + 0: {}, + 1: {}, + 2: {} + } + self.played_info = {} + + # Set by SpecialMonitor - did user choose to resume playback or start from the + # beginning? + self.resume_playback = False + # Was the playback initiated by the user using the Kodi context menu? + self.context_menu_play = False + # Set by context menu - shall we force-transcode the next playing item? + self.force_transcode = False + # Which Kodi player is/has been active? (either int 1, 2 or 3) + self.active_players = set() + + # Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set + # up our own playlist from the very beginning + self.pkc_caused_stop = False + # Flag if the 0 length PKC video has already failed so we can start resolving + # playback (set in player.py) + self.pkc_caused_stop_done = True diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 6188949e..ca996bbf 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -7,8 +7,7 @@ import requests import xbmc from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB -from . import backgroundthread, utils -from . import state +from . import app, backgroundthread, utils LOG = getLogger('PLEX.artwork') @@ -19,13 +18,7 @@ requests.packages.urllib3.disable_warnings() # download is successful TIMEOUT = (35.1, 35.1) -IMAGE_CACHING_SUSPENDS = [ - state.SUSPEND_LIBRARY_THREAD, - state.STOP_SYNC, - state.DB_SCAN -] -if not utils.settings('imageSyncDuringPlayback') == 'true': - IMAGE_CACHING_SUSPENDS.append(state.SUSPEND_SYNC) +IMAGE_CACHING_SUSPENDS = [] def double_urlencode(text): @@ -37,19 +30,9 @@ def double_urldecode(text): class ImageCachingThread(backgroundthread.KillableThread): - def __init__(self): - self._canceled = False - super(ImageCachingThread, self).__init__() - - def isCanceled(self): - return self._canceled or state.STOP_PKC - def isSuspended(self): return any(IMAGE_CACHING_SUSPENDS) - def cancel(self): - self._canceled = True - @staticmethod def _art_url_generator(): for kind in (KodiVideoDB, KodiMusicDB): @@ -88,18 +71,18 @@ def cache_url(url): try: requests.head( url="http://%s:%s/image/image://%s" - % (state.WEBSERVER_HOST, - state.WEBSERVER_PORT, + % (app.CONN.webserver_host, + app.CONN.webserver_port, url), - auth=(state.WEBSERVER_USERNAME, - state.WEBSERVER_PASSWORD), + auth=(app.CONN.webserver_username, + app.CONN.webserver_password), timeout=TIMEOUT) except requests.Timeout: # We don't need the result, only trigger Kodi to start the # download. All is well break except requests.ConnectionError: - if state.STOP_PKC: + if app.APP.stop_pkc: # Kodi terminated break # Server thinks its a DOS attack, ('error 10053') diff --git a/resources/lib/backgroundthread.py b/resources/lib/backgroundthread.py index 74029f95..83a7a847 100644 --- a/resources/lib/backgroundthread.py +++ b/resources/lib/backgroundthread.py @@ -14,7 +14,6 @@ LOG = getLogger('PLEX.' + __name__) class KillableThread(threading.Thread): - pass '''A thread class that supports raising exception in the thread from another thread. ''' @@ -77,6 +76,45 @@ class KillableThread(threading.Thread): # except KillThreadException: # self.onKilled() + def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): + self._canceled = False + self._suspended = False + super(KillableThread, self).__init__(group, target, name, args, kwargs) + + def isCanceled(self): + """ + Returns True if the thread is stopped + """ + if self._canceled or xbmc.abortRequested: + return True + return False + + def abort(self): + """ + Call to stop this thread + """ + self._canceled = True + + def suspend(self): + """ + Call to suspend this thread + """ + self._suspended = True + + def resume(self): + """ + Call to revive a suspended thread back to life + """ + self._suspended = False + + def isSuspended(self): + """ + Returns True if the thread is suspended + """ + if self._suspended: + return True + return False + class Tasks(list): def add(self, task): diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py deleted file mode 100644 index 3107d825..00000000 --- a/resources/lib/command_pipeline.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, unicode_literals -import logging -from threading import Thread -from xbmc import sleep - -from . import utils -from . import state - -############################################################################### -LOG = logging.getLogger('PLEX.command_pipeline') -############################################################################### - - -@utils.thread_methods -class Monitor_Window(Thread): - """ - Monitors window('plex_command') for new entries that we need to take care - of, e.g. for new plays initiated on the Kodi side with addon paths. - - Adjusts state.py accordingly - """ - def run(self): - stopped = self.stopped - queue = state.COMMAND_PIPELINE_QUEUE - LOG.info("----===## Starting Kodi_Play_Client ##===----") - while not stopped(): - if utils.window('plex_command'): - value = utils.window('plex_command') - utils.window('plex_command', clear=True) - if value.startswith('PLAY-'): - queue.put(value.replace('PLAY-', '')) - elif value == 'SUSPEND_LIBRARY_THREAD-True': - state.SUSPEND_LIBRARY_THREAD = True - elif value == 'SUSPEND_LIBRARY_THREAD-False': - state.SUSPEND_LIBRARY_THREAD = False - elif value == 'STOP_SYNC-True': - state.STOP_SYNC = True - elif value == 'STOP_SYNC-False': - state.STOP_SYNC = False - elif value == 'PMS_STATUS-Auth': - state.PMS_STATUS = 'Auth' - elif value == 'PMS_STATUS-401': - state.PMS_STATUS = '401' - elif value == 'SUSPEND_USER_CLIENT-True': - state.SUSPEND_USER_CLIENT = True - elif value == 'SUSPEND_USER_CLIENT-False': - state.SUSPEND_USER_CLIENT = False - elif value.startswith('PLEX_TOKEN-'): - state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None - elif value.startswith('PLEX_USERNAME-'): - state.PLEX_USERNAME = \ - value.replace('PLEX_USERNAME-', '') or None - elif value.startswith('RUN_LIB_SCAN-'): - state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '') - elif value.startswith('CONTEXT_menu?'): - queue.put('dummy?mode=context_menu&%s' - % value.replace('CONTEXT_menu?', '')) - elif value.startswith('NAVIGATE'): - queue.put(value.replace('NAVIGATE-', '')) - else: - raise NotImplementedError('%s not implemented' % value) - else: - sleep(50) - # Put one last item into the queue to let playback_starter end - queue.put(None) - LOG.info("----===## Kodi_Play_Client stopped ##===----") diff --git a/resources/lib/companion.py b/resources/lib/companion.py index e94f4ae1..bb068159 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -7,11 +7,8 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger from xbmc import Player -from . import playqueue as PQ -from . import plex_functions as PF -from . import json_rpc as js -from . import variables as v -from . import state +from . import playqueue as PQ, plex_functions as PF +from . import json_rpc as js, variables as v, app ############################################################################### @@ -68,12 +65,12 @@ def process_command(request_path, params): if request_path == 'player/playback/playMedia': # We need to tell service.py action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist' - state.COMPANION_QUEUE.put({ + app.APP.companion_queue.put({ 'action': action, 'data': params }) elif request_path == 'player/playback/refreshPlayQueue': - state.COMPANION_QUEUE.put({ + app.APP.companion_queue.put({ 'action': 'refreshPlayQueue', 'data': params }) @@ -115,7 +112,7 @@ def process_command(request_path, params): elif request_path == "player/navigation/back": js.input_back() elif request_path == "player/playback/setStreams": - state.COMPANION_QUEUE.put({ + app.APP.companion_queue.put({ 'action': 'setStreams', 'data': params }) diff --git a/resources/lib/context.py b/resources/lib/context.py index db9b5f75..bf88e1ba 100644 --- a/resources/lib/context.py +++ b/resources/lib/context.py @@ -43,8 +43,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog): return self.selected_option def onInit(self): - if utils.window('PlexUserImage'): - self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage')) + if utils.window('plexAvatar'): + self.getControl(USER_IMAGE).setImage(utils.window('plexAvatar')) height = 479 + (len(self._options) * 55) LOG.debug("options: %s", self._options) self.list_ = self.getControl(LIST) diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index d6ab7e1a..1f8c47e8 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -8,7 +8,7 @@ import xbmcgui from .plex_api import API from .plex_db import PlexDB from . import context, plex_functions as PF, playqueue as PQ -from . import utils, variables as v, state +from . import utils, variables as v, app ############################################################################### @@ -84,7 +84,7 @@ class ContextMenu(object): # if user uses direct paths, give option to initiate playback via PMS if self.api and self.api.extras(): options.append(OPTIONS['Extras']) - if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES: + if app.PLAYSTATE.direct_paths and self.kodi_type in v.KODI_VIDEOTYPES: options.append(OPTIONS['PMS_Play']) if self.kodi_type in v.KODI_VIDEOTYPES: options.append(OPTIONS['Transcode']) @@ -112,7 +112,7 @@ class ContextMenu(object): """ selected = self._selected_option if selected == OPTIONS['Transcode']: - state.FORCE_TRANSCODE = True + app.PLAYSTATE.force_transcode = True self._PMS_play() elif selected == OPTIONS['PMS_Play']: self._PMS_play() @@ -146,7 +146,7 @@ class ContextMenu(object): playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type]) playqueue.clear() - state.CONTEXT_MENU_PLAY = True + app.PLAYSTATE.context_menu_play = True handle = self.api.path(force_first_media=False, force_addon=True) xbmc.executebuiltin('RunPlugin(%s)' % handle) diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index d68dfb81..c1ed884b 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -4,9 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger import requests -from . import utils -from . import clientinfo -from . import state +from . import utils, clientinfo, app ############################################################################### @@ -14,7 +12,7 @@ from . import state import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() -LOG = getLogger('PLEX.downloadutils') +LOG = getLogger('PLEX.download') ############################################################################### @@ -39,25 +37,16 @@ class DownloadUtils(): def __init__(self): self.__dict__ = self._shared_state - def setServer(self, server): - """ - Reserved for userclient only - """ - self.server = server - LOG.debug("Set server: %s", server) - def setSSL(self, verifySSL=None, certificate=None): """ - Reserved for userclient only - verifySSL must be 'true' to enable certificate validation certificate must be path to certificate or 'None' """ if verifySSL is None: - verifySSL = state.VERIFY_SSL_CERT + verifySSL = app.CONN.verify_ssl_cert if certificate is None: - certificate = state.SSL_CERT_PATH + certificate = app.CONN.ssl_cert_path # Set the session's parameters self.s.verify = verifySSL if certificate: @@ -67,8 +56,7 @@ class DownloadUtils(): def startSession(self, reset=False): """ - User should be authenticated when this method is called (via - userclient) + User should be authenticated when this method is called """ # Start session self.s = requests.Session() @@ -80,9 +68,6 @@ class DownloadUtils(): # Set SSL settings self.setSSL() - # Set other stuff - self.setServer(utils.window('pms_server')) - # Counters to declare PMS dead or unauthorized # Use window variables because start of movies will be called with a # new plugin instance - it's impossible to share data otherwise @@ -94,7 +79,7 @@ class DownloadUtils(): self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1)) - LOG.info("Requests session started on: %s", self.server) + LOG.info("Requests session started on: %s", app.CONN.server) def stopSession(self): try: @@ -155,7 +140,7 @@ class DownloadUtils(): self.startSession() s = self.s # Replace for the real values - url = url.replace("{server}", self.server) + url = url.replace("{server}", app.CONN.server) else: # User is not (yet) authenticated. Used to communicate with # plex.tv and to check for PMS servers @@ -164,9 +149,9 @@ class DownloadUtils(): headerOptions = self.getHeader(options=headerOptions) else: headerOptions = headerOverride - kwargs['verify'] = state.VERIFY_SSL_CERT - if state.SSL_CERT_PATH: - kwargs['cert'] = state.SSL_CERT_PATH + kwargs['verify'] = app.CONN.verify_ssl_cert + if app.CONN.ssl_cert_path: + kwargs['cert'] = app.CONN.ssl_cert_path # Set the variables we were passed (fallback to request session # otherwise - faster) @@ -252,12 +237,11 @@ class DownloadUtils(): self.unauthorizedAttempts): LOG.warn('We seem to be truly unauthorized for PMS' ' %s ', url) - if state.PMS_STATUS not in ('401', 'Auth'): - # Tell userclient token has been revoked. + if app.CONN.pms_status not in ('401', 'Auth'): + # Tell others token has been revoked. LOG.debug('Setting PMS server status to ' 'unauthorized') - state.PMS_STATUS = '401' - utils.window('plex_serverStatus', value="401") + app.CONN.pms_status = '401' utils.dialog('notification', utils.lang(29999), utils.lang(30017), diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 174bca9c..60a8034c 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -20,8 +20,8 @@ from .plex_api import API from . import plex_functions as PF from . import json_rpc as js from . import variables as v -# Be careful - your using state in another Python instance! -from . import state +# Be careful - your using app in another Python instance! +from . import app ############################################################################### LOG = getLogger('PLEX.entrypoint') @@ -34,34 +34,7 @@ def choose_pms_server(): Lets user choose from list of PMS """ LOG.info("Choosing PMS server requested, starting") - - setup = initialsetup.InitialSetup() - server = setup.pick_pms(showDialog=True) - if server is None: - LOG.error('We did not connect to a new PMS, aborting') - utils.plex_command('SUSPEND_USER_CLIENT', 'False') - utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False') - return - - LOG.info("User chose server %s", server['name']) - setup.write_pms_to_settings(server) - - if not _log_out(): - return - - # Wipe Kodi and Plex database as well as playlists and video nodes - utils.wipe_database() - - # Log in again - _log_in() - LOG.info("Choosing new PMS complete") - # ' connected' - utils.dialog('notification', - utils.lang(29999), - '%s %s' % (server['name'], utils.lang(39220)), - icon='{plex}', - time=3000, - sound=False) + utils.plex_command('choose_pms_server') def toggle_plex_tv_sign_in(): @@ -69,37 +42,8 @@ def toggle_plex_tv_sign_in(): Signs out of Plex.tv if there was a token saved and thus deletes the token. Or signs in to plex.tv if the user was not logged in before. """ - if utils.settings('plexToken'): - LOG.info('Reseting plex.tv credentials in settings') - utils.settings('plexLogin', value="") - utils.settings('plexToken', value="") - utils.settings('plexid', value="") - utils.settings('plexAvatar', value="") - utils.settings('plex_status', value=utils.lang(39226)) - - utils.window('plex_token', clear=True) - utils.plex_command('PLEX_TOKEN', '') - utils.plex_command('PLEX_USERNAME', '') - else: - LOG.info('Login to plex.tv') - initialsetup.InitialSetup().plex_tv_sign_in() - utils.dialog('notification', - utils.lang(29999), - utils.lang(39221), - icon='{plex}', - time=3000, - sound=False) - - -def reset_authorization(): - """ - User tried login and failed too many times. Reset # of logins - """ - if utils.yesno_dialog(utils.lang(29999), utils.lang(39206)): - LOG.info("Reset login attempts.") - utils.plex_command('PMS_STATUS', 'Auth') - else: - executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') + LOG.info('Toggle of Plex.tv sign-in requested') + utils.plex_command('toggle_plex_tv_sign_in') def directory_item(label, path, folder=True): @@ -185,13 +129,7 @@ def switch_plex_user(): # position = 0 # utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) LOG.info("Plex home user switch requested") - if not _log_out(): - return - # First remove playlists of old user - utils.delete_playlists() - # Remove video nodes - utils.delete_nodes() - _log_in() + utils.plex_command('switch_plex_user') def create_listitem(item, append_show_title=False, append_sxxexx=False): @@ -573,13 +511,13 @@ def on_deck_episodes(viewid, tagname, limit): return # We're using another python instance - need to load some vars if utils.settings('useDirectPaths') == '1': - state.DIRECT_PATHS = True - state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true' - state.REMAP_PATH = utils.settings('remapSMB') == 'true' - if state.REMAP_PATH: + app.SYNC.direct_paths = True + app.SYNC.replace_smb_path = utils.settings('replaceSMB') == 'true' + app.SYNC.remap_path = utils.settings('remapSMB') == 'true' + if app.SYNC.remap_path: initialsetup.set_replace_paths() # Let's NOT check paths for widgets! - state.PATH_VERIFIED = True + app.SYNC.path_verified = True counter = 0 for item in xml: api = API(item) @@ -964,107 +902,5 @@ def create_new_pms(): """ Opens dialogs for the user the plug in the PMS details """ - # "Enter your Plex Media Server's IP or URL. Examples are:" - utils.messageDialog(utils.lang(29999), - '%s\n%s\n%s' % (utils.lang(39215), - '192.168.1.2', - 'plex.myServer.org')) - address = utils.dialog('input', "Enter PMS IP or URL") - if address == '': - return - port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}') - if port == '': - return - url = '%s:%s' % (address, port) - # "Does your Plex Media Server support SSL connections? - # (https instead of http)" - https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217)) - if https: - url = 'https://%s' % url - else: - url = 'http://%s' % url - https = 'true' if https else 'false' - machine_identifier = PF.GetMachineIdentifier(url) - if machine_identifier is None: - # "Error contacting url - # Abort (Yes) or save address anyway (No)" - if utils.yesno_dialog(utils.lang(29999), - '%s %s. %s' % (utils.lang(39218), - url, - utils.lang(39219))): - return - else: - utils.settings('plex_machineIdentifier', '') - else: - utils.settings('plex_machineIdentifier', machine_identifier) - LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s', - https, address, port, machine_identifier) - utils.settings('https', value=https) - utils.settings('ipaddress', value=address) - utils.settings('port', value=port) - # Chances are this is a local PMS, so disable SSL certificate check - utils.settings('sslverify', value='false') - - # Sign out to trigger new login - if _log_out(): - # Only login again if logout was successful - _log_in() - - -def _log_in(): - """ - Resets (clears) window properties to enable (re-)login - - SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed - out! - """ - utils.plex_command('RUN_LIB_SCAN', 'full') - # Restart user client - utils.plex_command('SUSPEND_USER_CLIENT', 'False') - - -def _log_out(): - """ - Finishes lib scans, logs out user. - - Returns True if successfully signed out, False otherwise - """ - # Resetting, please wait - utils.dialog('notification', - utils.lang(29999), - utils.lang(39207), - icon='{plex}', - time=3000, - sound=False) - # Pause library sync thread - utils.plex_command('SUSPEND_LIBRARY_THREAD', 'True') - # Wait max for 10 seconds for all lib scans to shutdown - counter = 0 - while utils.window('plex_dbScan') == 'true': - if counter > 200: - # Failed to reset PMS and plex.tv connects. Try to restart Kodi. - utils.messageDialog(utils.lang(29999), utils.lang(39208)) - # Resuming threads, just in case - utils.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") - - counter = 0 - # Log out currently signed in user: - utils.window('plex_serverStatus', value='401') - utils.plex_command('PMS_STATUS', '401') - # Above method needs to have run its course! Hence wait - while utils.window('plex_serverStatus') == "401": - if counter > 100: - # 'Failed to reset PKC. Try to restart Kodi.' - utils.messageDialog(utils.lang(29999), utils.lang(39208)) - LOG.error("Could not sign out user, aborting") - return False - counter += 1 - sleep(50) - # Suspend the user client during procedure - utils.plex_command('SUSPEND_USER_CLIENT', 'True') - return True + LOG.info('Request to manually enter new PMS address') + utils.plex_command('enter_new_pms_address') diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index c359944b..a6e7633e 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -2,22 +2,18 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from Queue import Queue -from xbmc import executebuiltin, translatePath +from xbmc import executebuiltin from . import utils from .utils import etree from . import path_ops from . import migration from .downloadutils import DownloadUtils as DU -from . import userclient -from . import clientinfo from . import plex_functions as PF from . import plex_tv from . import json_rpc as js -from . import playqueue as PQ -from . import state +from . import app from . import variables as v ############################################################################### @@ -30,96 +26,6 @@ if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH): path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH) -WINDOW_PROPERTIES = ( - "plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan", - "plex_customplayqueue", "plex_playbackProps", - "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", - "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", - "countError", "countUnauthorized", "plex_restricteduser", - "plex_allows_mediaDeletion", "plex_command", "plex_result", - "plex_force_transcode_pix" -) - - -def reload_pkc(): - """ - Will reload state.py entirely and then initiate some values from the Kodi - settings file - """ - LOG.info('Start (re-)loading PKC settings') - # Reset state.py - reload(state) - # Reset window props - for prop in WINDOW_PROPERTIES: - utils.window(prop, clear=True) - # Clear video nodes properties - from .library_sync import videonodes - videonodes.VideoNodes().clearProperties() - - # Initializing - state.VERIFY_SSL_CERT = utils.settings('sslverify') == 'true' - state.SSL_CERT_PATH = utils.settings('sslcert') \ - if utils.settings('sslcert') != 'None' else None - state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60 - state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber')) - state.SYNC_DIALOG = utils.settings('dbSyncIndicator') == 'true' - state.ENABLE_MUSIC = utils.settings('enableMusic') == 'true' - state.BACKGROUND_SYNC_DISABLED = utils.settings( - 'enableBackgroundSync') == 'false' - state.BACKGROUNDSYNC_SAFTYMARGIN = int( - utils.settings('backgroundsync_saftyMargin')) - state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true' - state.REMAP_PATH = utils.settings('remapSMB') == 'true' - state.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset')) - state.FETCH_PMS_ITEM_NUMBER = utils.settings('fetch_pms_item_number') - state.FORCE_RELOAD_SKIN = \ - utils.settings('forceReloadSkinOnPlaybackStop') == 'true' - # Init some Queues() - state.COMMAND_PIPELINE_QUEUE = Queue() - state.COMPANION_QUEUE = Queue(maxsize=100) - state.WEBSOCKET_QUEUE = Queue() - set_replace_paths() - set_webserver() - # To detect Kodi profile switches - utils.window('plex_kodiProfile', - value=utils.try_decode(translatePath("special://profile"))) - clientinfo.getDeviceId() - # Initialize the PKC playqueues - PQ.init_playqueues() - LOG.info('Done (re-)loading PKC settings') - - -def set_replace_paths(): - """ - Sets our values for direct paths correctly (including using lower-case - protocols like smb:// and NOT SMB://) - """ - for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): - for arg in ('Org', 'New'): - key = 'remapSMB%s%s' % (typus, arg) - value = utils.settings(key) - if '://' in value: - protocol = value.split('://', 1)[0] - value = value.replace(protocol, protocol.lower()) - setattr(state, key, value) - - -def set_webserver(): - """ - Set the Kodi webserver details - used to set the texture cache - """ - if js.get_setting('services.webserver') in (None, False): - # Enable the webserver, it is disabled - js.set_setting('services.webserver', True) - # Set standard port and username - # set_setting('services.webserverport', 8080) - # set_setting('services.webserverusername', 'kodi') - # Webserver already enabled - state.WEBSERVER_PORT = js.get_setting('services.webserverport') - state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername') - state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword') - - def _write_pms_settings(url, token): """ Sets certain settings for server by asking for the PMS' settings @@ -145,11 +51,8 @@ class InitialSetup(object): """ def __init__(self): LOG.debug('Entering initialsetup class') - self.server = userclient.UserClient().get_server() - self.serverid = utils.settings('plex_machineIdentifier') # Get Plex credentials from settings file, if they exist plexdict = PF.GetPlexLoginFromSettings() - self.myplexlogin = plexdict['myplexlogin'] == 'true' self.plex_login = plexdict['plexLogin'] self.plex_token = plexdict['plexToken'] self.plexid = plexdict['plexid'] @@ -158,6 +61,48 @@ class InitialSetup(object): if self.plex_token: LOG.debug('Found a plex.tv token in the settings') + def enter_new_pms_address(self): + # "Enter your Plex Media Server's IP or URL. Examples are:" + utils.messageDialog(utils.lang(29999), + '%s\n%s\n%s' % (utils.lang(39215), + '192.168.1.2', + 'plex.myServer.org')) + address = utils.dialog('input', "Enter PMS IP or URL") + if address == '': + return False + port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}') + if port == '': + return False + url = '%s:%s' % (address, port) + # "Does your Plex Media Server support SSL connections? + # (https instead of http)" + https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217)) + if https: + url = 'https://%s' % url + else: + url = 'http://%s' % url + https = 'true' if https else 'false' + machine_identifier = PF.GetMachineIdentifier(url) + if machine_identifier is None: + # "Error contacting url + # Abort (Yes) or save address anyway (No)" + if utils.yesno_dialog(utils.lang(29999), + '%s %s. %s' % (utils.lang(39218), + url, + utils.lang(39219))): + return False + else: + utils.settings('plex_machineIdentifier', '') + else: + utils.settings('plex_machineIdentifier', machine_identifier) + LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s', + https, address, port, machine_identifier) + utils.settings('https', value=https) + utils.settings('ipaddress', value=address) + utils.settings('port', value=port) + # Chances are this is a local PMS, so disable SSL certificate check + utils.settings('sslverify', value='false') + def plex_tv_sign_in(self): """ Signs (freshly) in to plex.tv (will be saved to file settings) @@ -227,26 +172,26 @@ class InitialSetup(object): not set before """ answer = True - chk = PF.check_connection(self.server, verifySSL=False) + chk = PF.check_connection(app.CONN.server, verifySSL=False) if chk is False: - LOG.warn('Could not reach PMS %s', self.server) + LOG.warn('Could not reach PMS %s', app.CONN.server) answer = False - if answer is True and not self.serverid: + if answer is True and not app.CONN.machine_identifier: LOG.info('No PMS machineIdentifier found for %s. Trying to ' - 'get the PMS unique ID', self.server) - self.serverid = PF.GetMachineIdentifier(self.server) - if self.serverid is None: + 'get the PMS unique ID', app.CONN.server) + app.CONN.machine_identifier = PF.GetMachineIdentifier(app.CONN.server) + if app.CONN.machine_identifier is None: LOG.warn('Could not retrieve machineIdentifier') answer = False else: - utils.settings('plex_machineIdentifier', value=self.serverid) + utils.settings('plex_machineIdentifier', value=app.CONN.machine_identifier) elif answer is True: - temp_server_id = PF.GetMachineIdentifier(self.server) - if temp_server_id != self.serverid: + temp_server_id = PF.GetMachineIdentifier(app.CONN.server) + if temp_server_id != app.CONN.machine_identifier: LOG.warn('The current PMS %s was expected to have a ' 'unique machineIdentifier of %s. But we got ' '%s. Pick a new server to be sure', - self.server, self.serverid, temp_server_id) + app.CONN.server, app.CONN.machine_identifier, temp_server_id) answer = False return answer @@ -305,7 +250,7 @@ class InitialSetup(object): """ server = None # If no server is set, let user choose one - if not self.server or not self.serverid: + if not app.CONN.server or not app.CONN.machine_identifier: showDialog = True if showDialog is True: server = self._user_pick_pms() @@ -328,13 +273,13 @@ class InitialSetup(object): if https_updated is False: serverlist = PF.discover_pms(self.plex_token) for item in serverlist: - if item.get('machineIdentifier') == self.serverid: + if item.get('machineIdentifier') == app.CONN.machine_identifier: server = item if server is None: name = utils.settings('plex_servername') LOG.warn('The PMS you have used before with a unique ' 'machineIdentifier of %s and name %s is ' - 'offline', self.serverid, name) + 'offline', app.CONN.machine_identifier, name) return chk = self._check_pms_connectivity(server) if chk == 504 and https_updated is False: @@ -535,7 +480,8 @@ class InitialSetup(object): # Do we need to migrate stuff? migration.check_migration() # Reload the server IP cause we might've deleted it during migration - self.server = userclient.UserClient().get_server() + app.CONN.load() + app.CONN.server = app.CONN.server # Display a warning if Kodi puts ALL movies into the queue, basically # breaking playback reporting for PKC @@ -556,19 +502,19 @@ class InitialSetup(object): # If a Plex server IP has already been set # return only if the right machine identifier is found - if self.server: - LOG.info("PMS is already set: %s. Checking now...", self.server) + if app.CONN.server: + LOG.info("PMS is already set: %s. Checking now...", app.CONN.server) if self.check_existing_pms(): LOG.info("Using PMS %s with machineIdentifier %s", - self.server, self.serverid) - _write_pms_settings(self.server, self.pms_token) + app.CONN.server, app.CONN.machine_identifier) + _write_pms_settings(app.CONN.server, self.pms_token) if reboot is True: utils.reboot_kodi() return # If not already retrieved myplex info, optionally let user sign in # to plex.tv. This DOES get called on very first install run - if not self.plex_token and self.myplexlogin: + if not self.plex_token and app.ACCOUNT.myplexlogin: self.plex_tv_sign_in() server = self.pick_pms() diff --git a/resources/lib/itemtypes/common.py b/resources/lib/itemtypes/common.py index f800a3cd..0bead0d2 100644 --- a/resources/lib/itemtypes/common.py +++ b/resources/lib/itemtypes/common.py @@ -7,7 +7,7 @@ from ntpath import dirname from ..plex_api import API from ..plex_db import PlexDB from ..kodi_db import KodiVideoDB -from .. import utils +from .. import utils, timing LOG = getLogger('PLEX.itemtypes.common') @@ -135,5 +135,5 @@ class ItemBase(object): resume, duration, view_count, - utils.unix_date_to_kodi(lastViewedAt), + timing.plex_date_to_kodi(lastViewedAt), plex_type) diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index a37edf05..7b635e18 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase from ..plex_api import API -from .. import state, variables as v, plex_functions as PF +from .. import app, variables as v, plex_functions as PF LOG = getLogger('PLEX.movies') @@ -50,8 +50,8 @@ class Movie(ItemBase): studios = api.music_studio_list() # GET THE FILE AND PATH ##### - do_indirect = not state.DIRECT_PATHS - if state.DIRECT_PATHS: + do_indirect = not app.SYNC.direct_paths + if app.SYNC.direct_paths: # Direct paths is set the Kodi way playurl = api.file_path(force_first_media=True) if playurl is None: diff --git a/resources/lib/itemtypes/music.py b/resources/lib/itemtypes/music.py index 6463dc26..41d83081 100644 --- a/resources/lib/itemtypes/music.py +++ b/resources/lib/itemtypes/music.py @@ -7,7 +7,7 @@ from .common import ItemBase from ..plex_api import API from ..plex_db import PlexDB from ..kodi_db import KodiMusicDB -from .. import plex_functions as PF, utils, state, variables as v +from .. import plex_functions as PF, utils, timing, app, variables as v LOG = getLogger('PLEX.music') @@ -165,12 +165,11 @@ class Artist(MusicMixin, ItemBase): # Kodi doesn't allow that. In case that happens we just merge the # artist entries. kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId) - # Create the reference in plex table self.kodidb.update_artist(api.list_to_string(api.genre_list()), api.plot(), thumb, fanart, - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), kodi_id) # Update artwork self.kodidb.modify_artwork(artworks, @@ -256,7 +255,7 @@ class Album(MusicMixin, ItemBase): thumb, api.music_studio(), userdata['UserRating'], - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'album', kodi_id) else: @@ -270,7 +269,7 @@ class Album(MusicMixin, ItemBase): thumb, api.music_studio(), userdata['UserRating'], - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'album', kodi_id) # OR ADD THE ALBUM ##### @@ -289,7 +288,7 @@ class Album(MusicMixin, ItemBase): thumb, api.music_studio(), userdata['UserRating'], - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'album') else: self.kodidb.add_album_17(kodi_id, @@ -303,7 +302,7 @@ class Album(MusicMixin, ItemBase): thumb, api.music_studio(), userdata['UserRating'], - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'album') self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name()) self.kodidb.add_discography(artist_id, name, api.year()) @@ -397,7 +396,7 @@ class Song(MusicMixin, ItemBase): None, None, None, - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'single') else: self.kodidb.add_album_17(kodi_id, @@ -411,7 +410,7 @@ class Song(MusicMixin, ItemBase): None, None, None, - utils.unix_date_to_kodi(self.last_sync), + timing.unix_date_to_kodi(self.last_sync), 'single') else: album = self.plexdb.album(album_id) @@ -469,8 +468,8 @@ class Song(MusicMixin, ItemBase): mood = api.list_to_string(moods) # GET THE FILE AND PATH ##### - do_indirect = not state.DIRECT_PATHS - if state.DIRECT_PATHS: + do_indirect = not app.SYNC.direct_paths + if app.SYNC.direct_paths: # Direct paths is set the Kodi way playurl = api.file_path(force_first_media=True) if playurl is None: @@ -489,8 +488,7 @@ class Song(MusicMixin, ItemBase): path = playurl.replace(filename, "") if do_indirect: # Plex works a bit differently - path = "%s%s" % (utils.window('pms_server'), - xml[0][0].get('key')) + path = "%s%s" % (app.CONN.server, xml[0][0].get('key')) path = api.attach_plex_token_to_url(path) filename = path.rsplit('/', 1)[1] path = path.replace(filename, '') diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index d29c0ab4..283438cd 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -5,7 +5,7 @@ from logging import getLogger from .common import ItemBase, process_path from ..plex_api import API -from .. import plex_functions as PF, state, variables as v +from .. import plex_functions as PF, app, variables as v LOG = getLogger('PLEX.tvshows') @@ -135,7 +135,7 @@ class Show(ItemBase, TvShowMixin): studio = api.list_to_string(studios) # GET THE FILE AND PATH ##### - if state.DIRECT_PATHS: + if app.SYNC.direct_paths: # Direct paths is set the Kodi way playurl = api.validate_playurl(api.tv_show_path(), api.plex_type(), @@ -378,8 +378,8 @@ class Episode(ItemBase, TvShowMixin): parent_id = season['kodi_id'] # GET THE FILE AND PATH ##### - do_indirect = not state.DIRECT_PATHS - if state.DIRECT_PATHS: + do_indirect = not app.SYNC.direct_paths + if app.SYNC.direct_paths: playurl = api.file_path(force_first_media=True) if playurl is None: do_indirect = True @@ -513,7 +513,7 @@ class Episode(ItemBase, TvShowMixin): userdata['PlayCount'], userdata['LastPlayedDate'], None) # Do send None, we check here - if not state.DIRECT_PATHS: + if not app.SYNC.direct_paths: # need to set a SECOND file entry for a path without plex show id filename = api.file_name(force_first_media=True) path = 'plugin://%s.tvshows/' % v.ADDON_ID diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index 443d54da..d5646379 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -8,7 +8,7 @@ from __future__ import absolute_import, division, unicode_literals from json import loads, dumps from xbmc import executeJSONRPC -from . import utils +from . import timing class JsonRPC(object): @@ -156,7 +156,7 @@ def seek_to(offset): for playerid in get_player_ids(): JsonRPC("Player.Seek").execute( {"playerid": playerid, - "value": utils.millis_to_kodi_time(offset)}) + "value": timing.millis_to_kodi_time(offset)}) def smallforward(): diff --git a/resources/lib/kodi_db/__init__.py b/resources/lib/kodi_db/__init__.py index 9b19639e..e28efc63 100644 --- a/resources/lib/kodi_db/__init__.py +++ b/resources/lib/kodi_db/__init__.py @@ -7,7 +7,7 @@ from .video import KodiVideoDB from .music import KodiMusicDB from .texture import KodiTextureDB -from .. import path_ops, utils, variables as v +from .. import path_ops, utils, timing, variables as v LOG = getLogger('PLEX.kodi_db') @@ -68,7 +68,7 @@ def setup_kodi_default_entries(): VALUES (?, ?, ?) ''', (v.DB_MUSIC_VERSION[v.KODIVERSION], 0, - utils.unix_date_to_kodi(utils.unix_timestamp()))) + timing.kodi_now())) def reset_cached_images(): diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 758a215d..87f34168 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -5,7 +5,7 @@ from logging import getLogger from sqlite3 import IntegrityError from . import common -from .. import path_ops, utils, variables as v, state +from .. import path_ops, timing, variables as v, app LOG = getLogger('PLEX.kodi_db.video') @@ -74,12 +74,11 @@ class KodiVideoDB(common.KodiDBBase): if pathid is None: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 - datetime = utils.unix_date_to_kodi(utils.unix_timestamp()) self.cursor.execute(''' INSERT INTO path(idPath, strPath, dateAdded) VALUES (?, ?, ?) ''', - (pathid, parentpath, datetime)) + (pathid, parentpath, timing.kodi_now())) if parentpath != path: # In case we end up having media in the filesystem root, C:\ parent_id = self.parent_path_id(parentpath) @@ -198,7 +197,7 @@ class KodiVideoDB(common.KodiDBBase): Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for add-on paths """ - if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE: + if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE: # Hack for the 2 entries for episodes for addon paths self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1', (file_id, )) @@ -598,7 +597,7 @@ class KodiVideoDB(common.KodiDBBase): Adds a resume marker for a video library item. Will even set 2, considering add-on path widget hacks. """ - if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE: + if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE: # Need to make sure to set a SECOND bookmark entry for another, # second file_id that points to the path .tvshows instead of # .tvshows/ 100: @@ -229,19 +229,19 @@ def _playback_init(plex_id, plex_type, playqueue, pos): utils.lang(30128), icon='{error}') # Do NOT use _ensure_resolve() because we resolved above already - state.CONTEXT_MENU_PLAY = False - state.FORCE_TRANSCODE = False - state.RESUME_PLAYBACK = False + app.PLAYSTATE.context_menu_play = False + app.PLAYSTATE.force_transcode = False + app.PLAYSTATE.resume_playback = False return PL.get_playlist_details_from_xml(playqueue, xml) stack = _prep_playlist_stack(xml) _process_stack(playqueue, stack) # Always resume if playback initiated via PMS and there IS a resume # point - offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None + offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None # Reset some playback variables - state.CONTEXT_MENU_PLAY = False - state.FORCE_TRANSCODE = False + app.PLAYSTATE.context_menu_play = False + app.PLAYSTATE.force_transcode = False # New thread to release this one sooner (e.g. harddisk spinning up) thread = Thread(target=threaded_playback, args=(playqueue.kodi_pl, pos, offset)) @@ -272,8 +272,8 @@ def _ensure_resolve(abort=False): LOG.debug('Passing dummy path to Kodi') # if not state.CONTEXT_MENU_PLAY: # Because playback won't start with context menu play - state.PKC_CAUSED_STOP = True - state.PKC_CAUSED_STOP_DONE = False + app.PLAYSTATE.pkc_caused_stop = True + app.PLAYSTATE.pkc_caused_stop_done = False if not abort: result = pickler.Playback_Successful() result.listitem = PKCListItem(path=v.NULL_VIDEO) @@ -283,9 +283,9 @@ def _ensure_resolve(abort=False): pickler.pickle_me(None) if abort: # Reset some playback variables - state.CONTEXT_MENU_PLAY = False - state.FORCE_TRANSCODE = False - state.RESUME_PLAYBACK = False + app.PLAYSTATE.context_menu_play = False + app.PLAYSTATE.force_transcode = False + app.PLAYSTATE.resume_playback = False def _init_existing_kodi_playlist(playqueue, pos): @@ -299,7 +299,7 @@ def _init_existing_kodi_playlist(playqueue, pos): LOG.error('No Kodi items returned') raise PL.PlaylistError('No Kodi items returned') item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos]) - item.force_transcode = state.FORCE_TRANSCODE + item.force_transcode = app.PLAYSTATE.force_transcode # playqueue.py will add the rest - this will likely put the PMS under # a LOT of strain if the following Kodi setting is enabled: # Settings -> Player -> Videos -> Play next video automatically @@ -310,7 +310,7 @@ def _prep_playlist_stack(xml): stack = [] for item in xml: api = API(item) - if (state.CONTEXT_MENU_PLAY is False and + if (app.PLAYSTATE.context_menu_play is False and api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)): # If user chose to play via PMS or force transcode, do not # use the item path stored in the Kodi DB @@ -378,7 +378,7 @@ def _process_stack(playqueue, stack): playlist_item.offset = item['offset'] playlist_item.part = item['part'] playlist_item.id = item['id'] - playlist_item.force_transcode = state.FORCE_TRANSCODE + playlist_item.force_transcode = app.PLAYSTATE.force_transcode pos += 1 @@ -413,7 +413,7 @@ def _conclude_playback(playqueue, pos): playurl = item.file if not playurl: LOG.info('Did not get a playurl, aborting playback silently') - state.RESUME_PLAYBACK = False + app.PLAYSTATE.resume_playback = False pickler.pickle_me(result) return listitem.setPath(utils.try_encode(playurl)) @@ -422,8 +422,8 @@ def _conclude_playback(playqueue, pos): elif item.playmethod == 'Transcode': playutils.audio_subtitle_prefs(listitem) - if state.RESUME_PLAYBACK is True: - state.RESUME_PLAYBACK = False + if app.PLAYSTATE.resume_playback is True: + app.PLAYSTATE.resume_playback = False if (item.offset is None and item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)): with PlexDB() as plexdb: diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 324d3fe4..844d1345 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -10,7 +10,7 @@ from . import playback from . import context_entry from . import json_rpc as js from . import pickler -from . import state +from . import app ############################################################################### @@ -61,7 +61,7 @@ class PlaybackStarter(Thread): kodi_type=params.get('kodi_type')) def run(self): - queue = state.COMMAND_PIPELINE_QUEUE + queue = app.APP.command_pipeline_queue LOG.info("----===## Starting PlaybackStarter ##===----") while True: item = queue.get() diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index bc715e98..20e31d60 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -16,7 +16,7 @@ from .downloadutils import DownloadUtils as DU from . import utils from . import json_rpc as js from . import variables as v -from . import state +from . import app ############################################################################### @@ -357,7 +357,7 @@ def verify_kodi_item(plex_id, kodi_item): # Got all the info we need return kodi_item # Special case playlist startup - got type but no id - if (not state.DIRECT_PATHS and state.ENABLE_MUSIC and + if (not app.SYNC.direct_paths and app.SYNC.enable_music and kodi_item.get('type') == v.KODI_TYPE_SONG and kodi_item['file'].startswith('http')): kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'], diff --git a/resources/lib/playlists/__init__.py b/resources/lib/playlists/__init__.py index a3b47567..8d74bcc2 100644 --- a/resources/lib/playlists/__init__.py +++ b/resources/lib/playlists/__init__.py @@ -19,7 +19,7 @@ from . import pms, db, kodi_pl, plex_pl from ..watchdog import events from ..plex_api import API -from .. import utils, path_ops, variables as v, state +from .. import utils, path_ops, variables as v, app ############################################################################### LOG = getLogger('PLEX.playlists') @@ -92,7 +92,7 @@ def websocket(plex_id, status): * 9: 'deleted' """ create = False - with state.LOCK_PLAYLISTS: + with app.APP.lock_playlists: playlist = db.get_playlist(plex_id=plex_id) if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE: LOG.debug('Ignoring detected Plex playlist change for %s', @@ -155,7 +155,7 @@ def full_sync(): fetch the PMS playlists) """ LOG.info('Starting playlist full sync') - with state.LOCK_PLAYLISTS: + with app.APP.lock_playlists: # Need to lock because we're messing with playlists return _full_sync() @@ -283,7 +283,7 @@ def sync_kodi_playlist(path): return False if extension not in SUPPORTED_FILETYPES: return False - if not state.SYNC_SPECIFIC_KODI_PLAYLISTS: + if not app.SYNC.sync_specific_kodi_playlists: return True playlist = Playlist() playlist.kodi_path = path @@ -341,10 +341,10 @@ def sync_plex_playlist(playlist=None, xml=None, plex_id=None): return False name = api.title() typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()] - if (not state.ENABLE_MUSIC and typus == v.PLEX_PLAYLIST_TYPE_AUDIO): + if (not app.SYNC.enable_music and typus == v.PLEX_PLAYLIST_TYPE_AUDIO): LOG.debug('Not synching Plex audio playlist') return False - if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS: + if not app.SYNC.sync_specific_plex_playlists: return True prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower() if name and name.lower().startswith(prefix): @@ -387,7 +387,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler): events.EVENT_TYPE_CREATED: self.on_created, events.EVENT_TYPE_DELETED: self.on_deleted, } - with state.LOCK_PLAYLISTS: + with app.APP.lock_playlists: _method_map[event.event_type](event) def on_created(self, event): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 81c34e0c..1f2a4d8c 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -5,16 +5,11 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly """ from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from threading import Thread import xbmc -from . import utils -from . import playlist_func as PL -from . import plex_functions as PF from .plex_api import API -from . import json_rpc as js -from . import variables as v -from . import state +from . import playlist_func as PL, plex_functions as PF +from . import backgroundthread, utils, json_rpc as js, app, variables as v ############################################################################### LOG = getLogger('PLEX.playqueue') @@ -35,7 +30,7 @@ def init_playqueues(): LOG.debug('Playqueues have already been initialized') return # Initialize Kodi playqueues - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: for i in (0, 1, 2): # Just in case the Kodi response is not sorted correctly for queue in js.get_playlists(): @@ -96,13 +91,18 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None): return playqueue -@utils.thread_methods(add_suspends=['PMS_STATUS']) -class PlayqueueMonitor(Thread): +class PlayqueueMonitor(backgroundthread.KillableThread): """ Unfortunately, Kodi does not tell if items within a Kodi playqueue (playlist) are swapped. This is what this monitor is for. Don't replace this mechanism till Kodi's implementation of playlists has improved """ + def isSuspended(self): + """ + Returns True if the thread is suspended + """ + return self._suspended or app.CONN.pms_status + def _compare_playqueues(self, playqueue, new): """ Used to poll the Kodi playqueue and update the Plex playqueue if needed @@ -117,7 +117,7 @@ class PlayqueueMonitor(Thread): # Ignore new media added by other addons continue for j, old_item in enumerate(old): - if self.stopped(): + if self.isCanceled(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -178,7 +178,7 @@ class PlayqueueMonitor(Thread): for j in range(i, len(index)): index[j] += 1 for i in reversed(index): - if self.stopped(): + if self.isCanceled(): # Chances are that we got an empty Kodi playlist due to # Kodi exit return @@ -192,20 +192,18 @@ class PlayqueueMonitor(Thread): LOG.debug('Done comparing playqueues') def run(self): - stopped = self.stopped - suspended = self.suspended LOG.info("----===## Starting PlayqueueMonitor ##===----") - while not stopped(): - while suspended(): - if stopped(): + while not self.isCanceled(): + while self.isSuspended(): + if self.isCanceled(): break xbmc.sleep(1000) - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: for playqueue in PLAYQUEUES: kodi_pl = js.playlist_get_items(playqueue.playlistid) if playqueue.old_kodi_pl != kodi_pl: - if playqueue.id is None and (not state.DIRECT_PATHS or - state.CONTEXT_MENU_PLAY): + if playqueue.id is None and (not app.PLAYSTATE.direct_paths or + app.PLAYSTATE.context_menu_play): # Only initialize if directly fired up using direct # paths. Otherwise let default.py do its magic LOG.debug('Not yet initiating playback') diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index df3d9d46..b7143273 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger from .downloadutils import DownloadUtils as DU -from . import utils +from . import utils, app from . import variables as v ############################################################################### @@ -304,7 +304,7 @@ class PlayUtils(): # We don't know the language - no need to download else: path = self.api.attach_plex_token_to_url( - "%s%s" % (utils.window('pms_server'), + "%s%s" % (app.CONN.server, stream.attrib['key'])) downloadable_streams.append(index) download_subs.append(utils.try_encode(path)) diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index 8a7fb90f..694deca7 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -42,11 +42,11 @@ from .kodi_db import KodiVideoDB, KodiMusicDB from .utils import cast from .downloadutils import DownloadUtils as DU from . import clientinfo -from . import utils +from . import utils, timing from . import path_ops from . import plex_functions as PF from . import variables as v -from . import state +from . import app ############################################################################### LOG = getLogger('PLEX.plex_api') @@ -121,7 +121,7 @@ class API(object): Pass direct_path=True if you're calling from another Plex python instance - because otherwise direct paths will evaluate to False! """ - direct_paths = direct_paths or state.DIRECT_PATHS + direct_paths = direct_paths or app.SYNC.direct_paths filename = self.file_path(force_first_media=force_first_media) if (not direct_paths or force_addon or self.plex_type() == v.PLEX_TYPE_CLIP): @@ -219,15 +219,15 @@ class API(object): extension not in v.KODI_SUPPORTED_IMAGES): # Let Plex transcode # max width/height supported by plex image transcoder is 1920x1080 - path = state.PMS_SERVER + PF.transcode_image_path( + path = app.CONN.server + PF.transcode_image_path( self.item[0][0].get('key'), - state.PMS_TOKEN, - "%s%s" % (state.PMS_SERVER, self.item[0][0].get('key')), + app.CONN.pms_token, + "%s%s" % (app.CONN.server, self.item[0][0].get('key')), 1920, 1080) else: path = self.attach_plex_token_to_url( - '%s%s' % (state.PMS_SERVER, self.item[0][0].attrib['key'])) + '%s%s' % (app.CONN.server, self.item[0][0].attrib['key'])) # Attach Plex id to url to let it be picked up by our playqueue agent # later return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id())) @@ -263,10 +263,9 @@ class API(object): """ res = self.item.get('addedAt') if res is not None: - res = utils.unix_date_to_kodi(res) + return timing.plex_date_to_kodi(res) else: - res = '2000-01-01 10:00:00' - return res + return '2000-01-01 10:00:00' def viewcount(self): """ @@ -300,11 +299,11 @@ class API(object): played = True if playcount else False try: - last_played = utils.unix_date_to_kodi(int(item['lastViewedAt'])) + last_played = utils.plex_date_to_kodi(int(item['lastViewedAt'])) except (KeyError, ValueError): last_played = None - if state.INDICATE_MEDIA_VERSIONS is True: + if app.SYNC.indicate_media_versions is True: userrating = 0 for _ in self.item.findall('./Media'): userrating += 1 @@ -685,12 +684,12 @@ class API(object): url may or may not already contain a '?' """ - if not state.PMS_TOKEN: + if not app.CONN.pms_token: return url if '?' not in url: - url = "%s?X-Plex-Token=%s" % (url, state.PMS_TOKEN) + url = "%s?X-Plex-Token=%s" % (url, app.CONN.pms_token) else: - url = "%s&X-Plex-Token=%s" % (url, state.PMS_TOKEN) + url = "%s&X-Plex-Token=%s" % (url, app.CONN.pms_token) return url def item_id(self): @@ -770,7 +769,7 @@ class API(object): for extras in self.item.iterfind('Extras'): # There will always be only 1 extras element if (len(extras) > 0 and - state.SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER): + app.SYNC.show_extras_instead_of_playing_trailer): return ('plugin://%s?mode=route_to_extras&plex_id=%s' % (v.ADDON_ID, self.plex_id())) for extra in extras: @@ -888,7 +887,7 @@ class API(object): artwork = '%s?width=%s&height=%s' % (artwork, width, height) artwork = ('%s/photo/:/transcode?width=3840&height=3840&' 'minSize=1&upscale=0&url=%s' - % (state.PMS_SERVER, quote(artwork))) + % (app.CONN.server, quote(artwork))) artwork = self.attach_plex_token_to_url(artwork) return artwork @@ -1406,7 +1405,7 @@ class API(object): # trailers are 'clip' with PMS xmls if action == "DirectStream": path = self.item[self.mediastream][self.part].attrib['key'] - url = state.PMS_SERVER + path + url = app.CONN.server + path # e.g. Trailers already feature an '?'! if '?' in url: url += '&' + urlencode(xargs) @@ -1423,7 +1422,7 @@ class API(object): } # Path/key to VIDEO item of xml PMS response is needed, not part path = self.item.attrib['key'] - transcode_path = state.PMS_SERVER + \ + transcode_path = app.CONN.server + \ '/video/:/transcode/universal/start.m3u8?' args = { 'audioBoost': utils.settings('audioBoost'), @@ -1481,7 +1480,7 @@ class API(object): # We don't know the language - no need to download else: path = self.attach_plex_token_to_url( - "%s%s" % (state.PMS_SERVER, key)) + "%s%s" % (app.CONN.server, key)) externalsubs.append(path) kodiindex += 1 LOG.info('Found external subs: %s', externalsubs) @@ -1735,16 +1734,16 @@ class API(object): if path is None: return typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus] - if state.REMAP_PATH is True: - path = path.replace(getattr(state, 'remapSMB%sOrg' % typus), - getattr(state, 'remapSMB%sNew' % typus), + if app.SYNC.remap_path is True: + path = path.replace(getattr(app.SYNC, 'remapSMB%sOrg' % typus), + getattr(app.SYNC, 'remapSMB%sNew' % typus), 1) # There might be backslashes left over: path = path.replace('\\', '/') - elif state.REPLACE_SMB_PATH is True: + elif app.SYNC.replace_smb_path is True: if path.startswith('\\\\'): path = 'smb:' + path.replace('\\', '/') - if ((state.PATH_VERIFIED and force_check is False) or + if ((app.SYNC.path_verified and force_check is False) or omit_check is True): return path @@ -1769,14 +1768,14 @@ class API(object): if force_check is False: # Validate the path is correct with user intervention if self.ask_to_validate(path): - state.STOP_SYNC = True + app.SYNC.stop_sync = True path = None - state.PATH_VERIFIED = True + app.SYNC.path_verified = True else: path = None elif force_check is False: # Only set the flag if we were not force-checking the path - state.PATH_VERIFIED = True + app.SYNC.path_verified = True return path @staticmethod diff --git a/resources/lib/plex_companion.py b/resources/lib/plex_companion.py index 3f97b704..57a95303 100644 --- a/resources/lib/plex_companion.py +++ b/resources/lib/plex_companion.py @@ -20,7 +20,8 @@ from . import playback from . import json_rpc as js from . import playqueue as PQ from . import variables as v -from . import state +from . import backgroundthread +from . import app ############################################################################### @@ -46,7 +47,7 @@ def update_playqueue_from_PMS(playqueue, # Safe transient token from being deleted if transient_token is None: transient_token = playqueue.plex_transient_token - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: xml = PL.get_PMS_playlist(playqueue, playqueue_id) try: xml.attrib @@ -64,8 +65,7 @@ def update_playqueue_from_PMS(playqueue, playback.play_xml(playqueue, xml, offset) -@utils.thread_methods(add_suspends=['PMS_STATUS']) -class PlexCompanion(Thread): +class PlexCompanion(backgroundthread.KillableThread): """ Plex Companion monitoring class. Invoke only once """ @@ -80,7 +80,13 @@ class PlexCompanion(Thread): self.player = Player() self.httpd = False self.subscription_manager = None - Thread.__init__(self) + super(PlexCompanion, self).__init__() + + def isSuspended(self): + """ + Returns True if the thread is suspended + """ + return self._suspended or app.CONN.pms_status def _process_alexa(self, data): xml = PF.GetPlexMetadata(data['key']) @@ -116,10 +122,9 @@ class PlexCompanion(Thread): offset = None playback.play_xml(playqueue, xml, offset) else: - state.PLEX_TRANSIENT_TOKEN = data.get('token') + app.CONN.plex_transient_token = data.get('token') if data.get('offset') != '0': - state.RESUMABLE = True - state.RESUME_PLAYBACK = True + app.PLAYSTATE.resume_playback = True playback.playback_triage(api.plex_id(), api.plex_type(), resolve=False) @@ -129,7 +134,7 @@ class PlexCompanion(Thread): """ E.g. watch later initiated by Companion. Basically navigating Plex """ - state.PLEX_TRANSIENT_TOKEN = data.get('key') + app.CONN.plex_transient_token = data.get('key') params = { 'mode': 'plex_node', 'key': '{server}%s' % data.get('key'), @@ -221,16 +226,16 @@ class PlexCompanion(Thread): LOG.debug('Processing: %s', task) data = task['data'] if task['action'] == 'alexa': - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: self._process_alexa(data) elif (task['action'] == 'playlist' and data.get('address') == 'node.plexapp.com'): self._process_node(data) elif task['action'] == 'playlist': - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: self._process_playlist(data) elif task['action'] == 'refreshPlayQueue': - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: self._process_refresh(data) elif task['action'] == 'setStreams': try: @@ -260,8 +265,6 @@ class PlexCompanion(Thread): httpd = self.httpd # Cache for quicker while loops client = self.client - stopped = self.stopped - suspended = self.suspended # Start up instances request_mgr = httppersist.RequestMgr() @@ -298,12 +301,12 @@ class PlexCompanion(Thread): if httpd: thread = Thread(target=httpd.handle_request) - while not stopped(): + while not self.isCanceled(): # If we are not authorized, sleep # Otherwise, we trigger a download which leads to a # re-authorizations - while suspended(): - if stopped(): + while self.isSuspended(): + if self.isCanceled(): break sleep(1000) try: @@ -335,13 +338,13 @@ class PlexCompanion(Thread): LOG.warn(traceback.format_exc()) # See if there's anything we need to process try: - task = state.COMPANION_QUEUE.get(block=False) + task = app.APP.companion_queue.get(block=False) except Empty: pass else: # Got instructions, process them self._process_tasks(task) - state.COMPANION_QUEUE.task_done() + app.APP.companion_queue.task_done() # Don't sleep continue sleep(50) diff --git a/resources/lib/plex_db/playlists.py b/resources/lib/plex_db/playlists.py index 6fdd22a5..3c14f259 100644 --- a/resources/lib/plex_db/playlists.py +++ b/resources/lib/plex_db/playlists.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals -from .. import variables as v class Playlists(object): diff --git a/resources/lib/plex_tv.py b/resources/lib/plex_tv.py index b3824908..acbcc12a 100644 --- a/resources/lib/plex_tv.py +++ b/resources/lib/plex_tv.py @@ -5,10 +5,9 @@ from logging import getLogger import time import threading import xbmc -import xbmcgui from .downloadutils import DownloadUtils as DU -from . import utils, variables as v, state +from . import utils, app ############################################################################### LOG = getLogger('PLEX.plex_tv') @@ -87,7 +86,7 @@ def switch_home_user(userid, pin, token, machine_identifier): utils.settings('plex_restricteduser', 'true' if xml.get('restricted', '0') == '1' else 'false') - state.RESTRICTED_USER = True if \ + app.CONN.restricted_user = True if \ xml.get('restricted', '0') == '1' else False # Get final token to the PMS we've chosen @@ -174,7 +173,7 @@ class PinLogin(object): start = time.time() while (not self._abort and time.time() - start < 300 and - not state.STOP_PKC): + not app.APP.stop_pkc): xml = DU().downloadUrl(self.POLL.format(self.id), authenticate=False) try: diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index c1b24a74..d33d21ee 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -31,7 +31,7 @@ import time from xbmc import sleep from ..downloadutils import DownloadUtils as DU -from .. import utils +from .. import utils, app from .. import variables as v ############################################################################### @@ -228,7 +228,7 @@ class plexgdm: return self.server_list def discover(self): - currServer = utils.window('pms_server') + currServer = app.CONN.server if not currServer: return currServerProt, currServerIP, currServerPort = \ diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 131215a6..d1c46410 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -9,8 +9,8 @@ from logging import getLogger from threading import Thread from ..downloadutils import DownloadUtils as DU -from .. import utils -from .. import state +from .. import utils, timing +from .. import app from .. import variables as v from .. import json_rpc as js from .. import playqueue as PQ @@ -101,9 +101,9 @@ def update_player_info(playerid): """ Updates all player info for playerid [int] in state.py. """ - state.PLAYER_STATES[playerid].update(js.get_player_props(playerid)) - state.PLAYER_STATES[playerid]['volume'] = js.get_volume() - state.PLAYER_STATES[playerid]['muted'] = js.get_muted() + app.PLAYSTATE.playerstates[playerid].update(js.get_player_props(playerid)) + app.PLAYSTATE.playerstates[playerid]['volume'] = js.get_volume() + app.PLAYSTATE.playerstates[playerid]['muted'] = js.get_muted() class SubscriptionMgr(object): @@ -189,9 +189,9 @@ class SubscriptionMgr(object): return answ def _timeline_dict(self, player, ptype): - with state.LOCK_PLAYQUEUES: + with app.APP.lock_playqueues: playerid = player['playerid'] - info = state.PLAYER_STATES[playerid] + info = app.PLAYSTATE.player_states[playerid] playqueue = PQ.PLAYQUEUES[playerid] position = self._get_correct_position(info, playqueue) try: @@ -208,12 +208,12 @@ class SubscriptionMgr(object): if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO): self.location = 'fullScreenVideo' - pbmc_server = utils.window('pms_server') + pbmc_server = app.CONN.server if pbmc_server: (self.protocol, self.server, self.port) = pbmc_server.split(':') self.server = self.server.replace('/', '') status = 'paused' if int(info['speed']) == 0 else 'playing' - duration = utils.kodi_time_to_millis(info['totaltime']) + duration = timing.kodi_time_to_millis(info['totaltime']) shuffle = '1' if info['shuffled'] else '0' mute = '1' if info['muted'] is True else '0' answ = { @@ -225,7 +225,7 @@ class SubscriptionMgr(object): 'state': status, 'type': ptype, 'itemType': ptype, - 'time': utils.kodi_time_to_millis(info['time']), + 'time': timing.kodi_time_to_millis(info['time']), 'duration': duration, 'seekRange': '0-%s' % duration, 'shuffle': shuffle, @@ -254,8 +254,8 @@ class SubscriptionMgr(object): if playqueue.items[position].guid: answ['guid'] = item.guid # Temp. token set? - if state.PLEX_TRANSIENT_TOKEN: - answ['token'] = state.PLEX_TRANSIENT_TOKEN + if app.CONN.plex_transient_token: + answ['token'] = app.CONN.plex_transient_token elif playqueue.plex_transient_token: answ['token'] = playqueue.plex_transient_token # Process audio and subtitle streams @@ -301,7 +301,7 @@ class SubscriptionMgr(object): stream_type: 'video', 'audio', 'subtitle' """ playqueue = PQ.PLAYQUEUES[playerid] - info = state.PLAYER_STATES[playerid] + info = app.PLAYSTATE.player_states[playerid] position = self._get_correct_position(info, playqueue) if info[STREAM_DETAILS[stream_type]] == -1: kodi_stream_index = -1 @@ -315,7 +315,7 @@ class SubscriptionMgr(object): Updates the Plex Companien client with the machine identifier uuid with command_id """ - with state.LOCK_SUBSCRIBER: + with app.APP.lock_subscriber: if command_id and self.subscribers.get(uuid): self.subscribers[uuid].command_id = int(command_id) @@ -326,7 +326,7 @@ class SubscriptionMgr(object): playqueues. """ for player in players.values(): - info = state.PLAYER_STATES[player['playerid']] + info = app.PLAYSTATE.player_states[player['playerid']] playqueue = PQ.PLAYQUEUES[player['playerid']] position = self._get_correct_position(info, playqueue) try: @@ -345,7 +345,7 @@ class SubscriptionMgr(object): Causes PKC to tell the PMS and Plex Companion players to receive a notification what's being played. """ - with state.LOCK_SUBSCRIBER: + with app.APP.lock_subscriber: self._cleanup() # Get all the active/playing Kodi players (video, audio, pictures) players = js.get_players() @@ -378,7 +378,7 @@ class SubscriptionMgr(object): self._send_pms_notification(player['playerid'], self.last_params) def _get_pms_params(self, playerid): - info = state.PLAYER_STATES[playerid] + info = app.PLAYSTATE.player_states[playerid] playqueue = PQ.PLAYQUEUES[playerid] position = self._get_correct_position(info, playqueue) try: @@ -390,8 +390,8 @@ class SubscriptionMgr(object): 'state': status, 'ratingKey': item.plex_id, 'key': '/library/metadata/%s' % item.plex_id, - 'time': utils.kodi_time_to_millis(info['time']), - 'duration': utils.kodi_time_to_millis(info['totaltime']) + 'time': timing.kodi_time_to_millis(info['time']), + 'duration': timing.kodi_time_to_millis(info['totaltime']) } if info['container_key'] is not None: # params['containerKey'] = info['container_key'] @@ -407,12 +407,12 @@ class SubscriptionMgr(object): playqueue = PQ.PLAYQUEUES[playerid] xargs = params_pms() xargs.update(params) - if state.PLEX_TRANSIENT_TOKEN: - xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN + if app.CONN.plex_transient_token: + xargs['X-Plex-Token'] = app.CONN.plex_transient_token elif playqueue.plex_transient_token: xargs['X-Plex-Token'] = playqueue.plex_transient_token - elif state.PMS_TOKEN: - xargs['X-Plex-Token'] = state.PMS_TOKEN + elif app.ACCOUNT.pms_token: + xargs['X-Plex-Token'] = app.ACCOUNT.pms_token url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'), serv.get('server', 'localhost'), serv.get('port', '32400')) @@ -434,7 +434,7 @@ class SubscriptionMgr(object): command_id, self, self.request_mgr) - with state.LOCK_SUBSCRIBER: + with app.APP.lock_subscriber: self.subscribers[subscriber.uuid] = subscriber return subscriber @@ -444,7 +444,7 @@ class SubscriptionMgr(object): uuid from PKC notifications. (Calls the cleanup() method of the subscriber) """ - with state.LOCK_SUBSCRIBER: + with app.APP.lock_subscriber: for subscriber in self.subscribers.values(): if subscriber.uuid == uuid or subscriber.host == uuid: subscriber.cleanup() diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py index c852f69e..82ec7685 100644 --- a/resources/lib/service_entry.py +++ b/resources/lib/service_entry.py @@ -5,38 +5,107 @@ import logging import sys import xbmc -from . import utils -from . import userclient -from . import initialsetup +from . import utils, clientinfo, timing +from . import initialsetup, artwork from . import kodimonitor from . import sync from . import websocket_client from . import plex_companion -from . import plex_functions as PF -from . import command_pipeline +from . import plex_functions as PF, playqueue as PQ from . import playback_starter from . import playqueue from . import variables as v -from . import state +from . import app from . import loghandler +from .windows import userselect ############################################################################### loghandler.config() -LOG = logging.getLogger("PLEX.service_entry") +LOG = logging.getLogger("PLEX.service") ############################################################################### +WINDOW_PROPERTIES = ( + "plex_online", "plex_command_processed", "plex_shouldStop", "plex_dbScan", + "plex_customplayqueue", "plex_playbackProps", + "pms_token", "plex_token", "pms_server", "plex_machineIdentifier", + "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", + "countError", "countUnauthorized", "plex_restricteduser", + "plex_allows_mediaDeletion", "plex_command", "plex_result", + "plex_force_transcode_pix" +) + + +def authenticate(): + """ + Authenticate the current user or prompt to log-in + + Returns True if successful, False if not. 'aborted' if user chose to + abort + """ + LOG.info('Authenticating user') + if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: + # Found a user in the settings, try to authenticate + LOG.info('Trying to authenticate with old settings') + res = PF.check_connection(app.CONN.server, + token=app.ACCOUNT.pms_token, + verifySSL=app.CONN.verify_ssl_cert) + if res is False: + LOG.error('Something went wrong while checking connection') + return False + elif res == 401: + LOG.error('User token no longer valid. Sign user out') + app.ACCOUNT.clear() + return False + elif res >= 400: + LOG.error('Answer from PMS is not as expected') + return False + LOG.info('Successfully authenticated using old settings') + app.ACCOUNT.set_authenticated() + return True + + # Could not use settings - try to get Plex user list from plex.tv + if app.ACCOUNT.plex_token: + LOG.info("Trying to connect to plex.tv to get a user list") + user, _ = userselect.start() + if not user: + LOG.info('No user received') + app.CONN.pms_status = 'Stop' + return False + username = user.title + user_id = user.id + token = user.authToken + else: + LOG.info("Trying to authenticate without a token") + username = '' + user_id = '' + token = '' + res = PF.check_connection(app.CONN.server, + token=token, + verifySSL=app.CONN.verify_ssl_cert) + if res is False: + LOG.error('Something went wrong while checking connection') + return False + elif res == 401: + LOG.error('Token not valid') + return False + elif res >= 400: + LOG.error('Answer from PMS is not as expected') + return False + LOG.info('Successfully authenticated') + # Got new values that need to be saved + utils.settings('username', value=username) + utils.settings('userid', value=user_id) + utils.settings('accessToken', value=token) + app.ACCOUNT.load() + app.ACCOUNT.set_authenticated() + return True + class Service(): - - server_online = True - warn_auth = True - - user = None ws = None sync = None plexcompanion = None - user_running = False ws_running = False alexa_running = False sync_running = False @@ -67,28 +136,110 @@ class Service(): utils.settings('syncSpecificPlexPlaylistsPrefix')) LOG.info('XML decoding being used: %s', utils.ETREE) LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion')) + + # Reset window props + for prop in WINDOW_PROPERTIES: + utils.window(prop, clear=True) + + # To detect Kodi profile switches + utils.window('plex_kodiProfile', + value=utils.try_decode(xbmc.translatePath("special://profile"))) + self.monitor = xbmc.Monitor() # Load/Reset PKC entirely - important for user/Kodi profile switch - initialsetup.reload_pkc() + # Clear video nodes properties + from .library_sync import videonodes + videonodes.VideoNodes().clearProperties() + clientinfo.getDeviceId() + # Init time-offset between Kodi and Plex + timing.KODI_PLEX_TIME_OFFSET = utils.settings('kodiplextimeoffset') or 0.0 - def _stop_pkc(self): - return xbmc.abortRequested or state.STOP_PKC + def isCanceled(self): + return xbmc.abortRequested or app.APP.stop_pkc + + def log_out(self): + """ + Ensures that lib sync threads are suspended; signs out user + """ + LOG.info('Log-out requested') + app.SYNC.suspend_library_thread = True + i = 0 + while app.SYNC.db_scan: + i += 1 + xbmc.sleep(50) + if i > 100: + LOG.error('Could not stop library sync, aborting log-out') + # Failed to reset PMS and plex.tv connects. Try to restart Kodi + utils.messageDialog(utils.lang(29999), utils.lang(39208)) + # Resuming threads, just in case + app.SYNC.suspend_library_thread = False + return False + LOG.info('Successfully stopped library sync') + app.ACCOUNT.clear() + LOG.info('User has been logged out') + return True + + def choose_pms_server(self, manual=False): + LOG.info("Choosing PMS server requested, starting") + if manual: + if not self.setup.enter_new_pms_address(): + return False + else: + server = self.setup.pick_pms(showDialog=True) + if server is None: + LOG.info('We did not connect to a new PMS, aborting') + return False + LOG.info("User chose server %s", server['name']) + if server['baseURL'] == app.CONN.server: + LOG.info('User chose old PMS to connect to') + return False + self.setup.write_pms_to_settings(server) + if not self.log_out(): + return False + # Wipe Kodi and Plex database as well as playlists and video nodes + utils.wipe_database() + app.CONN.load() + LOG.info("Choosing new PMS complete") + return True + + def switch_plex_user(self): + if not self.log_out(): + return False + # First remove playlists of old user + utils.delete_playlists() + # Remove video nodes + utils.delete_nodes() + return True + + def toggle_plex_tv(self): + if utils.settings('plexToken'): + LOG.info('Reseting plex.tv credentials in settings') + app.ACCOUNT.clear() + return True + else: + LOG.info('Login to plex.tv') + return self.setup.plex_tv_sign_in() 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 + app.init() + # Some plumbing + artwork.IMAGE_CACHING_SUSPENDS = [ + app.SYNC.suspend_library_thread, + app.SYNC.stop_sync, + app.SYNC.db_scan + ] + if not utils.settings('imageSyncDuringPlayback') == 'true': + artwork.IMAGE_CACHING_SUSPENDS.append(app.SYNC.suspend_sync) + # Initialize the PKC playqueues + PQ.init_playqueues() # Server auto-detect - initialsetup.InitialSetup().setup() + self.setup = initialsetup.InitialSetup() + self.setup.setup() - # Detect playback start early on - self.command_pipeline = command_pipeline.Monitor_Window() - self.command_pipeline.start() - - # Initialize important threads, handing over self for callback purposes - self.user = userclient.UserClient() + # Initialize important threads self.ws = websocket_client.PMS_Websocket() self.alexa = websocket_client.Alexa_Websocket() self.sync = sync.Sync() @@ -97,9 +248,10 @@ class Service(): self.playback_starter = playback_starter.PlaybackStarter() self.playqueue = playqueue.PlayqueueMonitor() + server_online = True welcome_msg = True counter = 0 - while not _stop_pkc(): + while not self.isCanceled(): if utils.window('plex_kodiProfile') != v.KODI_PROFILE: # Profile change happened, terminate this thread and others @@ -108,149 +260,167 @@ class Service(): v.KODI_PROFILE, utils.window('plex_kodiProfile')) break + plex_command = utils.window('plex_command') + if plex_command: + # Commands/user interaction received from other PKC Python + # instances (default.py and context.py instead of service.py) + utils.window('plex_command', clear=True) + if plex_command.startswith('PLAY-'): + # Add-on path playback! + app.APP.command_pipeline_queue.put( + plex_command.replace('PLAY-', '')) + elif plex_command.startswith('NAVIGATE-'): + app.APP.command_pipeline_queue.put( + plex_command.replace('NAVIGATE-', '')) + elif plex_command.startswith('CONTEXT_menu?'): + app.APP.command_pipeline_queue.put( + 'dummy?mode=context_menu&%s' + % plex_command.replace('CONTEXT_menu?', '')) + elif plex_command == 'choose_pms_server': + if self.choose_pms_server(): + utils.window('plex_online', clear=True) + app.ACCOUNT.set_unauthenticated() + server_online = False + welcome_msg = False + elif plex_command == 'switch_plex_user': + if self.switch_plex_user(): + app.ACCOUNT.set_unauthenticated() + elif plex_command == 'enter_new_pms_address': + if self.setup.enter_new_pms_address(): + if self.log_out(): + utils.window('plex_online', clear=True) + app.ACCOUNT.set_unauthenticated() + server_online = False + welcome_msg = False + elif plex_command == 'toggle_plex_tv_sign_in': + if self.toggle_plex_tv(): + app.ACCOUNT.set_unauthenticated() + elif plex_command == 'repair-scan': + app.SYNC.run_lib_scan = 'repair' + elif plex_command == 'full-scan': + app.SYNC.run_lib_scan = 'full' + elif plex_command == 'fanart-scan': + app.SYNC.run_lib_scan = 'fanart' + elif plex_command == 'textures-scan': + app.SYNC.run_lib_scan = 'textures' + continue + # Before proceeding, need to make sure: # 1. Server is online # 2. User is set # 3. User has access to the server - if utils.window('plex_online') == "true": # Plex server is online - # Verify if user is set and has access to the server - if (self.user.user is not None) and self.user.has_access: + if app.CONN.pms_status == 'Stop': + xbmc.sleep(500) + continue + elif app.CONN.pms_status == '401': + # Unauthorized access, revoke token + LOG.info('401 received - revoking token') + app.ACCOUNT.clear() + app.CONN.pms_status = 'Auth' + utils.window('plex_serverStatus', value='Auth') + continue + if not app.ACCOUNT.authenticated: + LOG.info('Not yet authenticated') + # Do authentication + if not authenticate(): + continue + # Start up events + if welcome_msg is True: + # Reset authentication warnings + welcome_msg = False + utils.dialog('notification', + utils.lang(29999), + "%s %s" % (utils.lang(33000), + app.ACCOUNT.plex_username), + icon='{plex}', + time=2000, + sound=False) + # Start monitoring kodi events if not self.kodimonitor_running: - # Start up events - self.warn_auth = True - if welcome_msg is True: - # Reset authentication warnings - welcome_msg = False - utils.dialog('notification', - utils.lang(29999), - "%s %s" % (utils.lang(33000), - self.user.user), - icon='{plex}', - time=2000, - sound=False) - # Start monitoring kodi events self.kodimonitor_running = kodimonitor.KodiMonitor() self.specialmonitor.start() - # Start the Websocket Client - if not self.ws_running: - self.ws_running = True - self.ws.start() - # Start the Alexa thread - if (not self.alexa_running and - utils.settings('enable_alexa') == 'true'): - self.alexa_running = True - self.alexa.start() - # Start the syncing thread - if not self.sync_running: - self.sync_running = True - self.sync.start() - # Start the Plex Companion thread - if not self.plexcompanion_running: - self.plexcompanion_running = True - self.plexcompanion.start() - if not self.playback_starter_running: - self.playback_starter_running = True - self.playback_starter.start() + # Start the Websocket Client + if not self.ws_running: + self.ws_running = True + self.ws.start() + # Start the Alexa thread + if (not self.alexa_running and + utils.settings('enable_alexa') == 'true'): + self.alexa_running = True + self.alexa.start() + # Start the syncing thread + if not self.sync_running: + self.sync_running = True + self.sync.start() + # Start the Plex Companion thread + if not self.plexcompanion_running: + self.plexcompanion_running = True + self.plexcompanion.start() + if not self.playback_starter_running: + self.playback_starter_running = True + self.playback_starter.start() self.playqueue.start() - else: - if (self.user.user is None) and self.warn_auth: - # Alert user is not authenticated and suppress future - # warning - self.warn_auth = False - LOG.warn("Not authenticated yet.") - - # User access is restricted. - # Keep verifying until access is granted - # unless server goes offline or Kodi is shut down. - while self.user.has_access is False: - # Verify access with an API call - self.user.check_access() - - if utils.window('plex_online') != "true": - # Server went offline - break - - if monitor.waitForAbort(3): - # Abort was requested while waiting. We should exit - break else: # Wait until Plex server is online # or Kodi is shut down. - while not self._stop_pkc(): - server = self.user.get_server() - if server is False: - # No server info set in add-on settings - pass - elif PF.check_connection(server, verifySSL=True) is False: - # Server is offline or cannot be reached - # Alert the user and suppress future warning - if self.server_online: - self.server_online = False - utils.window('plex_online', value="false") - # Suspend threads - state.SUSPEND_LIBRARY_THREAD = True - LOG.error("Plex Media Server went offline") - if utils.settings('show_pms_offline') == 'true': - utils.dialog('notification', - utils.lang(33001), - "%s %s" % (utils.lang(29999), - utils.lang(33002)), - icon='{plex}', - sound=False) - counter += 1 - # Periodically check if the IP changed, e.g. per minute - if counter > 20: - counter = 0 - setup = initialsetup.InitialSetup() - tmp = setup.pick_pms() - if tmp is not None: - setup.write_pms_to_settings(tmp) - else: - # Server is online + server = app.CONN.server + if not server: + # No server info set in add-on settings + pass + elif PF.check_connection(server, verifySSL=True) is False: + # Server is offline or cannot be reached + # Alert the user and suppress future warning + if server_online: + server_online = False + utils.window('plex_online', value="false") + # Suspend threads + app.SYNC.suspend_library_thread = True + LOG.warn("Plex Media Server went offline") + if utils.settings('show_pms_offline') == 'true': + utils.dialog('notification', + utils.lang(33001), + "%s %s" % (utils.lang(29999), + utils.lang(33002)), + icon='{plex}', + sound=False) + counter += 1 + # Periodically check if the IP changed, e.g. per minute + if counter > 20: counter = 0 - if not self.server_online: - # Server was offline when Kodi started. - # Wait for server to be fully established. - if monitor.waitForAbort(5): - # Abort was requested while waiting. - break - self.server_online = True - # Alert the user that server is online. - if (welcome_msg is False and - utils.settings('show_pms_offline') == 'true'): - utils.dialog('notification', - utils.lang(29999), - utils.lang(33003), - icon='{plex}', - time=5000, - sound=False) - LOG.info("Server %s is online and ready.", server) - utils.window('plex_online', value="true") - if state.AUTHENTICATED: - # Server got offline when we were authenticated. - # Hence resume threads - state.SUSPEND_LIBRARY_THREAD = False + setup = initialsetup.InitialSetup() + tmp = setup.pick_pms() + if tmp: + setup.write_pms_to_settings(tmp) + app.CONN.load() + else: + # Server is online + counter = 0 + if not server_online: + # Server was offline when Kodi started. + server_online = True + # Alert the user that server is online. + if (welcome_msg is False and + utils.settings('show_pms_offline') == 'true'): + utils.dialog('notification', + utils.lang(29999), + utils.lang(33003), + icon='{plex}', + time=5000, + sound=False) + LOG.info("Server %s is online and ready.", server) + utils.window('plex_online', value="true") + if app.ACCOUNT.authenticated: + # Server got offline when we were authenticated. + # Hence resume threads + app.SYNC.suspend_library_thread = False - # Start the userclient thread - if not self.user_running: - self.user_running = True - self.user.start() - - break - - if monitor.waitForAbort(3): - # Abort was requested while waiting. - break - - if monitor.waitForAbort(0.05): + if self.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) - state.STOP_PKC = True + app.APP.stop_pkc = True utils.window('plex_service_started', clear=True) LOG.info("======== STOP %s ========", v.ADDON_NAME) diff --git a/resources/lib/state.py b/resources/lib/state.py deleted file mode 100644 index 4b0a9dd9..00000000 --- a/resources/lib/state.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -THREAD SAFE -""" -from __future__ import absolute_import, division, unicode_literals -from threading import Lock, RLock - - -# LOCKS -#################### -# Need to lock all methods and functions messing with Plex Companion subscribers -LOCK_SUBSCRIBER = RLock() -# Need to lock everything messing with Kodi/PKC playqueues -LOCK_PLAYQUEUES = RLock() -# Necessary to temporarily hold back librarysync/websocket listener when doing -# a full sync -LOCK_PLAYLISTS = Lock() - -# Quit PKC -STOP_PKC = False - -# URL of our current PMS -PMS_SERVER = None -# 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 during media playback if PKC should not do any syncs. Will NOT -# suspend synching of playstate progress -SUSPEND_SYNC = False -# Could we access the paths? -PATH_VERIFIED = False -# Set if a Plex-Kodi DB sync is being done - along with -# window('plex_dbScan') set to 'true' -DB_SCAN = False -# Plex Media Server Status - along with window('plex_serverStatus') -PMS_STATUS = False -# When the userclient needs to wait -SUSPEND_USER_CLIENT = False -# Plex home user? Then "False". Along with window('plex_restricteduser') -RESTRICTED_USER = False -# Direct Paths (True) or Addon Paths (False)? Along with -# window('useDirectPaths') -DIRECT_PATHS = False -# Shall we replace custom user ratings with the number of versions available? -INDICATE_MEDIA_VERSIONS = False -# Will sync movie trailer differently: either play trailer directly or show -# all the Plex extras for the user to choose -SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER = False -# Do we need to run a special library scan? -RUN_LIB_SCAN = None -# Number of items to fetch and display in widgets -FETCH_PMS_ITEM_NUMBER = None -# Hack to force Kodi widget for "in progress" to show up if it was empty before -FORCE_RELOAD_SKIN = True - -# Stemming from the PKC settings.xml -# Shall we show Kodi dialogs when synching? -SYNC_DIALOG = True -# Shall Kodi show dialogs for syncing/caching images? (e.g. images left to sync) -IMAGE_SYNC_NOTIFICATIONS = True -# Only sync specific Plex playlists to Kodi? -SYNC_SPECIFIC_PLEX_PLAYLISTS = False -# Only sync specific Kodi playlists to Plex? -SYNC_SPECIFIC_KODI_PLAYLISTS = False -# Is synching of Plex music enabled? -ENABLE_MUSIC = True -# How often shall we sync? -FULL_SYNC_INTERVALL = 0 -# Background Sync disabled? -BACKGROUND_SYNC_DISABLED = False -# How long shall we wait with synching a new item to make sure Plex got all -# metadata? -BACKGROUNDSYNC_SAFTYMARGIN = 0 -# How many threads to download Plex metadata on sync? -SYNC_THREAD_NUMBER = 0 -# What's the time offset between the PMS and Kodi? -KODI_PLEX_TIME_OFFSET = 0.0 - -# Path remapping mechanism (e.g. smb paths) -# Do we replace \\myserver\path to smb://myserver/path? -REPLACE_SMB_PATH = False -# Do we generally remap? -REMAP_PATH = False -# Mappings for REMAP_PATH: -remapSMBmovieOrg = None -remapSMBmovieNew = None -remapSMBtvOrg = None -remapSMBtvNew = None -remapSMBmusicOrg = None -remapSMBmusicNew = None -remapSMBphotoOrg = None -remapSMBphotoNew = None - -# Shall we verify SSL certificates? -VERIFY_SSL_CERT = False -# Do we have an ssl certificate for PKC we need to use? -SSL_CERT_PATH = None -# Along with window('plex_authenticated') -AUTHENTICATED = False -# plex.tv username -PLEX_USERNAME = None -# Token for that user for plex.tv -PLEX_TOKEN = None -# Plex token for the active PMS for the active user -# (might be diffent to PLEX_TOKEN) -PMS_TOKEN = None -# Plex ID of that user (e.g. for plex.tv) as a STRING -PLEX_USER_ID = None -# Token passed along, e.g. if playback initiated by Plex Companion. Might be -# another user playing something! Token identifies user -PLEX_TRANSIENT_TOKEN = None - -# Plex Companion Queue() -COMPANION_QUEUE = None -# Command Pipeline Queue() -COMMAND_PIPELINE_QUEUE = None -# Websocket_client queue to communicate with librarysync -WEBSOCKET_QUEUE = None - -# Which Kodi player is/has been active? (either int 1, 2 or 3) -ACTIVE_PLAYERS = set() -# Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set -# up our own playlist from the very beginning -PKC_CAUSED_STOP = False -# Flag if the 0 length PKC video has already failed so we can start resolving -# playback (set in player.py) -PKC_CAUSED_STOP_DONE = True - -# Kodi player states - here, initial values are set -PLAYER_STATES = { - 0: {}, - 1: {}, - 2: {} -} -# The LAST playstate once playback is finished -OLD_PLAYER_STATES = { - 0: {}, - 1: {}, - 2: {} -} -# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate! -PLAYSTATE = { - 'type': None, - 'time': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0}, - 'totaltime': { - 'hours': 0, - 'minutes': 0, - 'seconds': 0, - 'milliseconds': 0}, - 'speed': 0, - 'shuffled': False, - 'repeat': 'off', - 'position': None, - 'playlistid': None, - 'currentvideostream': -1, - 'currentaudiostream': -1, - 'subtitleenabled': False, - 'currentsubtitle': -1, - 'file': None, - 'kodi_id': None, - 'kodi_type': None, - 'plex_id': None, - 'plex_type': None, - 'container_key': None, - 'volume': 100, - 'muted': False, - 'playmethod': None, - 'playcount': None -} -PLAYED_INFO = {} -# Set by SpecialMonitor - did user choose to resume playback or start from the -# beginning? -RESUME_PLAYBACK = False -# Was the playback initiated by the user using the Kodi context menu? -CONTEXT_MENU_PLAY = False -# Set by context menu - shall we force-transcode the next playing item? -FORCE_TRANSCODE = False - -# Kodi webserver details -WEBSERVER_PORT = 8080 -WEBSERVER_USERNAME = 'kodi' -WEBSERVER_PASSWORD = '' -WEBSERVER_HOST = 'localhost' diff --git a/resources/lib/sync.py b/resources/lib/sync.py index 84b96855..056621e3 100644 --- a/resources/lib/sync.py +++ b/resources/lib/sync.py @@ -5,8 +5,8 @@ from logging import getLogger import xbmc from .downloadutils import DownloadUtils as DU -from . import library_sync -from . import backgroundthread, utils, path_ops, artwork, variables as v, state +from . import library_sync, timing +from . import backgroundthread, utils, path_ops, artwork, variables as v, app from . import plex_db, kodi_db LOG = getLogger('PLEX.sync') @@ -18,10 +18,10 @@ def set_library_scan_toggle(boolean=True): """ if not boolean: # Deactivate - state.DB_SCAN = False + app.SYNC.db_scan = False utils.window('plex_dbScan', clear=True) else: - state.DB_SCAN = True + app.SYNC.db_scan = True utils.window('plex_dbScan', value="true") @@ -40,11 +40,8 @@ class Sync(backgroundthread.KillableThread): # self.lock = backgroundthread.threading.Lock() super(Sync, self).__init__() - def isCanceled(self): - return state.STOP_PKC - def isSuspended(self): - return state.SUSPEND_LIBRARY_THREAD + return self._suspended or app.SYNC.suspend_library_thread def show_kodi_note(self, message, icon="plex", force=False): """ @@ -54,7 +51,7 @@ class Sync(backgroundthread.KillableThread): icon: "plex": shows Plex icon "error": shows Kodi error icon """ - if not force and state.SYNC_DIALOG is not True and self.force_dialog is not True: + if not force and app.SYNC.sync_dialog is not True and self.force_dialog is not True: return if icon == "plex": utils.dialog('notification', @@ -70,14 +67,14 @@ class Sync(backgroundthread.KillableThread): def triage_lib_scans(self): """ - Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually + Decides what to do if app.SYNC.run_lib_scan has been set. E.g. manually triggered full or repair syncs """ - if state.RUN_LIB_SCAN in ("full", "repair"): + if app.SYNC.run_lib_scan in ("full", "repair"): set_library_scan_toggle() LOG.info('Full library scan requested, starting') self.start_library_sync(show_dialog=True, - repair=state.RUN_LIB_SCAN == 'repair', + repair=app.SYNC.run_lib_scan == 'repair', block=True) if self.sync_successful: # Full library sync finished @@ -85,7 +82,7 @@ class Sync(backgroundthread.KillableThread): elif not self.isSuspended() and not self.isCanceled(): # ERROR in library sync self.show_kodi_note(utils.lang(39410), icon='error') - elif state.RUN_LIB_SCAN == 'fanart': + elif app.SYNC.run_lib_scan == 'fanart': # Only look for missing fanart (No) or refresh all fanart (Yes) from .windows import optionsdialog refresh = optionsdialog.show(utils.lang(29999), @@ -99,7 +96,7 @@ class Sync(backgroundthread.KillableThread): message=utils.lang(30015), icon='{plex}', sound=False) - elif state.RUN_LIB_SCAN == 'textures': + elif app.SYNC.run_lib_scan == 'textures': LOG.info("Caching of images requested") if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)): return @@ -113,7 +110,7 @@ class Sync(backgroundthread.KillableThread): Hit this after the full sync has finished """ self.sync_successful = successful - self.last_full_sync = utils.unix_timestamp() + self.last_full_sync = timing.unix_timestamp() set_library_scan_toggle(boolean=False) if successful: self.show_kodi_note(utils.lang(39407)) @@ -129,7 +126,7 @@ class Sync(backgroundthread.KillableThread): def start_library_sync(self, show_dialog=None, repair=False, block=False): set_library_scan_toggle(boolean=True) - show_dialog = show_dialog if show_dialog is not None else state.SYNC_DIALOG + show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog library_sync.start(show_dialog, repair, self.on_library_scan_finished) # if block: # self.lock.acquire() @@ -153,7 +150,7 @@ class Sync(backgroundthread.KillableThread): def on_fanart_download_finished(self, successful): # FanartTV lookup completed - if successful and state.SYNC_DIALOG: + if successful and app.SYNC.sync_dialog: utils.dialog('notification', heading='{plex}', message=utils.lang(30019), @@ -174,7 +171,7 @@ class Sync(backgroundthread.KillableThread): try: self._run_internal() except: - state.DB_SCAN = False + app.SYNC.db_scan = False utils.window('plex_dbScan', clear=True) utils.ERROR(txt='sync.py crashed', notify=True) raise @@ -188,12 +185,12 @@ class Sync(backgroundthread.KillableThread): last_time_sync = 0 one_day_in_seconds = 60 * 60 * 24 # Link to Websocket queue - queue = state.WEBSOCKET_QUEUE + queue = app.APP.websocket_queue # Kodi Version supported by PKC? if (not path_ops.exists(v.DB_VIDEO_PATH) or not path_ops.exists(v.DB_TEXTURE_PATH) or - (state.ENABLE_MUSIC and not path_ops.exists(v.DB_MUSIC_PATH))): + (app.SYNC.enable_music and not path_ops.exists(v.DB_MUSIC_PATH))): # Database does not exists LOG.error('The current Kodi version is incompatible') LOG.error('Current Kodi version: %s', utils.try_decode( @@ -242,7 +239,7 @@ class Sync(backgroundthread.KillableThread): self.force_dialog = True # Initialize time offset Kodi - PMS library_sync.sync_pms_time() - last_time_sync = utils.unix_timestamp() + last_time_sync = timing.unix_timestamp() LOG.info('Initial start-up full sync starting') xbmc.executebuiltin('InhibitIdleShutdown(true)') # This call will block until scan is completed @@ -268,9 +265,9 @@ class Sync(backgroundthread.KillableThread): # First sync upon PKC restart. Skipped if very first sync upon # PKC installation has been completed LOG.info('Doing initial sync on Kodi startup') - if state.SUSPEND_SYNC: + if app.SYNC.suspend_sync: LOG.warning('Forcing startup sync even if Kodi is playing') - state.SUSPEND_SYNC = False + app.SYNC.suspend_sync = False self.start_library_sync(block=True) if self.sync_successful: initial_sync_done = True @@ -285,27 +282,27 @@ class Sync(backgroundthread.KillableThread): xbmc.sleep(1000) # Currently no db scan, so we could start a new scan - elif state.DB_SCAN is False: - # Full scan was requested from somewhere else, e.g. userclient - if state.RUN_LIB_SCAN is not None: + elif app.SYNC.db_scan is False: + # Full scan was requested from somewhere else + if app.SYNC.run_lib_scan is not None: # Force-show dialogs since they are user-initiated self.force_dialog = True self.triage_lib_scans() self.force_dialog = False # Reset the flag - state.RUN_LIB_SCAN = None + app.SYNC.run_lib_scan = None continue # Standard syncs - don't force-show dialogs - now = utils.unix_timestamp() - if (now - self.last_full_sync > state.FULL_SYNC_INTERVALL): + now = timing.unix_timestamp() + if (now - self.last_full_sync > app.SYNC.full_sync_intervall): LOG.info('Doing scheduled full library scan') self.start_library_sync() elif now - last_time_sync > one_day_in_seconds: LOG.info('Starting daily time sync') library_sync.sync_pms_time() last_time_sync = now - elif not state.BACKGROUND_SYNC_DISABLED: + elif not app.SYNC.background_sync_disabled: # Check back whether we should process something Only do # this once a while (otherwise, potentially many screen # refreshes lead to flickering) diff --git a/resources/lib/timing.py b/resources/lib/timing.py new file mode 100644 index 00000000..48c8e946 --- /dev/null +++ b/resources/lib/timing.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from datetime import datetime, timedelta +from time import localtime, strftime + +EPOCH = datetime.utcfromtimestamp(0) + +# What's the time offset between the PMS and Kodi? +KODI_PLEX_TIME_OFFSET = 0.0 + + +def unix_timestamp(seconds_into_the_future=None): + """ + Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as + an integer. + + Optionally, pass seconds_into_the_future: positive int's will result in a + future timestamp, negative the past + """ + if seconds_into_the_future: + future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future) + else: + future = datetime.utcnow() + return int((future - EPOCH).total_seconds()) + + +def unix_date_to_kodi(unix_kodi_time): + """ + converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a + propper, human-readable time stamp used by Kodi + + Output: Y-m-d h:m:s = 2009-04-05 23:16:04 + """ + return strftime('%Y-%m-%d %H:%M:%S', localtime(float(unix_kodi_time))) + + +def plex_date_to_kodi(plex_timestamp): + """ + converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a + propper, human-readable time stamp used by Kodi + + Output: Y-m-d h:m:s = 2009-04-05 23:16:04 + """ + return strftime('%Y-%m-%d %H:%M:%S', + localtime(float(plex_timestamp) + KODI_PLEX_TIME_OFFSET)) + + +def kodi_timestamp(plex_timestamp): + return unix_date_to_kodi(plex_timestamp) + + +def kodi_now(): + return unix_date_to_kodi(unix_timestamp()) + + +def millis_to_kodi_time(milliseconds): + """ + Converts time in milliseconds to the time dict used by the Kodi JSON RPC: + { + 'hours': [int], + 'minutes': [int], + 'seconds'[int], + 'milliseconds': [int] + } + Pass in the time in milliseconds as an int + """ + seconds = int(milliseconds / 1000) + minutes = int(seconds / 60) + seconds = seconds % 60 + hours = int(minutes / 60) + minutes = minutes % 60 + milliseconds = milliseconds % 1000 + return {'hours': hours, + 'minutes': minutes, + 'seconds': seconds, + 'milliseconds': milliseconds} + + +def kodi_time_to_millis(time): + """ + Converts the Kodi time dict + { + 'hours': [int], + 'minutes': [int], + 'seconds'[int], + 'milliseconds': [int] + } + to milliseconds [int]. Will not return negative results but 0! + """ + ret = (time['hours'] * 3600 + + time['minutes'] * 60 + + time['seconds']) * 1000 + time['milliseconds'] + return 0 if ret < 0 else ret diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py deleted file mode 100644 index 38b4d912..00000000 --- a/resources/lib/userclient.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, unicode_literals -from logging import getLogger -from threading import Thread - -from xbmc import sleep, executebuiltin - -from .windows import userselect -from .downloadutils import DownloadUtils as DU -from . import utils -from . import path_ops -from . import plex_functions as PF -from . import variables as v -from . import state - -############################################################################### - -LOG = getLogger('PLEX.userclient') - -############################################################################### - - -@utils.thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) -class UserClient(Thread): - """ - Manage Plex users - """ - # Borg - multiple instances, shared state - __shared_state = {} - - def __init__(self): - self.__dict__ = self.__shared_state - - self.auth = True - self.retry = 0 - self.aborted = False - - self.user = None - self.has_access = True - - self.server = None - self.server_name = None - self.machine_identifier = None - self.token = None - self.ssl = None - self.sslcert = None - - self.do_utils = None - - Thread.__init__(self) - - def get_server(self): - """ - Get the current PMS' URL - """ - # Original host - self.server_name = utils.settings('plex_servername') - https = utils.settings('https') == "true" - host = utils.settings('ipaddress') - port = utils.settings('port') - self.machine_identifier = utils.settings('plex_machineIdentifier') - if not host: - LOG.debug("No server information saved.") - return False - server = host + ":" + port - # If https is true - if https: - server = "https://%s" % server - # If https is false - else: - server = "http://%s" % server - # User entered IP; we need to get the machineIdentifier - if not self.machine_identifier: - self.machine_identifier = PF.GetMachineIdentifier(server) - if not self.machine_identifier: - self.machine_identifier = '' - utils.settings('plex_machineIdentifier', - value=self.machine_identifier) - LOG.debug('Returning active server: %s', server) - return server - - @staticmethod - def get_ssl_verify(): - """ - Do we need to verify the SSL certificate? Return None if that is the - case, else False - """ - return None if utils.settings('sslverify') == 'true' else False - - @staticmethod - def get_ssl_certificate(): - """ - Client side certificate - """ - return None if utils.settings('sslcert') == 'None' \ - else utils.settings('sslcert') - - def set_user_prefs(self): - """ - Load a user's profile picture - """ - LOG.debug('Setting user preferences') - # Only try to get user avatar if there is a token - if self.token: - url = PF.GetUserArtworkURL(self.user) - if url: - utils.window('PlexUserImage', value=url) - - @staticmethod - def check_access(): - # Plex: always return True for now - return True - - def load_user(self, username, user_id, usertoken, authenticated=False): - """ - Load the current user's details for PKC - """ - LOG.debug('Loading current user') - self.token = usertoken - self.server = self.get_server() - self.ssl = self.get_ssl_verify() - self.sslcert = self.get_ssl_certificate() - - if authenticated is False: - if self.server is None: - return False - LOG.debug('Testing validity of current token') - res = PF.check_connection(self.server, - token=self.token, - verifySSL=self.ssl) - if res is False: - # PMS probably offline - return False - elif res == 401: - LOG.error('Token is no longer valid') - return 401 - elif res >= 400: - LOG.error('Answer from PMS is not as expected. Retrying') - return False - - # Set to windows property - state.PLEX_USER_ID = user_id or None - state.PLEX_USERNAME = username - # This is the token for the current PMS (might also be '') - utils.window('pms_token', value=usertoken) - state.PMS_TOKEN = usertoken - # This is the token for plex.tv for the current user - # Is only '' if user is not signed in to plex.tv - utils.window('plex_token', value=utils.settings('plexToken')) - state.PLEX_TOKEN = utils.settings('plexToken') or None - utils.window('plex_restricteduser', - value=utils.settings('plex_restricteduser')) - state.RESTRICTED_USER = True \ - if utils.settings('plex_restricteduser') == 'true' else False - utils.window('pms_server', value=self.server) - state.PMS_SERVER = self.server - utils.window('plex_machineIdentifier', value=self.machine_identifier) - utils.window('plex_servername', value=self.server_name) - utils.window('plex_authenticated', value='true') - state.AUTHENTICATED = True - - utils.window('useDirectPaths', - value='true' if utils.settings('useDirectPaths') == "1" - else 'false') - state.DIRECT_PATHS = True if utils.settings('useDirectPaths') == "1" \ - else False - state.INDICATE_MEDIA_VERSIONS = True \ - if utils.settings('indicate_media_versions') == "true" else False - utils.window('plex_force_transcode_pix', - value='true' if utils.settings('force_transcode_pix') == "1" - else 'false') - - # Start DownloadUtils session - self.do_utils = DU() - self.do_utils.startSession(reset=True) - # Set user preferences in settings - self.user = username - self.set_user_prefs() - - # Writing values to settings file - utils.settings('username', value=username) - utils.settings('userid', value=user_id) - utils.settings('accessToken', value=usertoken) - return True - - def authenticate(self): - """ - Authenticate the current user - """ - LOG.debug('Authenticating user') - - # Give attempts at entering password / selecting user - if self.retry > 0: - if not self.aborted: - LOG.error("Too many retries to login.") - state.PMS_STATUS = 'Stop' - # Failed to authenticate. Did you login to plex.tv? - utils.messageDialog(utils.lang(29999),utils.lang(39023)) - executebuiltin( - 'Addon.OpenSettings(plugin.video.plexkodiconnect)') - return False - - # If there's no settings.xml - if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE): - LOG.error("Error, no settings.xml found.") - self.auth = False - return False - server = self.get_server() - # If there is no server we can connect to - if not server: - LOG.info("Missing server information.") - self.auth = False - return False - - # If there is a username in the settings, try authenticating - username = utils.settings('username') - userId = utils.settings('userid') - usertoken = utils.settings('accessToken') - enforceLogin = utils.settings('enforceUserLogin') - # Found a user in the settings, try to authenticate - if username and enforceLogin == 'false': - LOG.debug('Trying to authenticate with old settings') - answ = self.load_user(username, - userId, - usertoken, - authenticated=False) - if answ is True: - # SUCCESS: loaded a user from the settings - return True - elif answ == 401: - LOG.error("User token no longer valid. Sign user out") - utils.settings('username', value='') - utils.settings('userid', value='') - utils.settings('accessToken', value='') - else: - LOG.debug("Could not yet authenticate user") - return False - - # Could not use settings - try to get Plex user list from plex.tv - plextoken = utils.settings('plexToken') - if plextoken: - LOG.info("Trying to connect to plex.tv to get a user list") - user, self.aborted = userselect.start() - if not user: - # FAILURE: Something went wrong, try again - self.auth = True - self.retry += 1 - return False - username = user.title - user_id = user.id - usertoken = user.authToken - else: - LOG.info("Trying to authenticate without a token") - username = '' - user_id = '' - usertoken = '' - - if self.load_user(username, user_id, usertoken, authenticated=False): - # SUCCESS: loaded a user from the settings - return True - # Something went wrong, try again - self.auth = True - self.retry += 1 - return False - - def reset_client(self): - """ - Reset all user settings - """ - LOG.debug("Reset UserClient authentication.") - try: - self.do_utils.stopSession() - except AttributeError: - pass - utils.window('plex_authenticated', clear=True) - state.AUTHENTICATED = False - utils.window('pms_token', clear=True) - state.PLEX_TOKEN = None - state.PLEX_TRANSIENT_TOKEN = None - state.PMS_TOKEN = None - utils.window('plex_token', clear=True) - utils.window('pms_server', clear=True) - state.PMS_SERVER = None - utils.window('plex_machineIdentifier', clear=True) - utils.window('plex_servername', clear=True) - state.PLEX_USER_ID = None - state.PLEX_USERNAME = None - utils.window('plex_restricteduser', clear=True) - state.RESTRICTED_USER = False - - utils.settings('username', value='') - utils.settings('userid', value='') - utils.settings('accessToken', value='') - - self.token = None - self.auth = True - self.user = None - - self.retry = 0 - - def run(self): - """ - Do the work - """ - LOG.info("----===## Starting UserClient ##===----") - stopped = self.stopped - suspended = self.suspended - while not stopped(): - while suspended(): - if stopped(): - break - sleep(1000) - - if state.PMS_STATUS == "Stop": - sleep(500) - continue - - elif state.PMS_STATUS == "401": - # Unauthorized access, revoke token - state.PMS_STATUS = 'Auth' - utils.window('plex_serverStatus', value='Auth') - self.reset_client() - sleep(3000) - - if self.auth and (self.user is None): - # Try to authenticate user - 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 - if self.authenticate(): - # Successfully authenticated and loaded a user - LOG.info("Successfully authenticated!") - LOG.info("Current user: %s", self.user) - LOG.info("Current userId: %s", state.PLEX_USER_ID) - self.retry = 0 - state.SUSPEND_LIBRARY_THREAD = False - utils.window('plex_serverStatus', clear=True) - state.PMS_STATUS = False - - if not self.auth and (self.user is None): - # Loop if no server found - server = self.get_server() - - # The status Stop is for when user cancelled password dialog. - # Or retried too many times - if server and state.PMS_STATUS != "Stop": - # Only if there's information found to login - LOG.debug("Server found: %s", server) - self.auth = True - - # Minimize CPU load - sleep(100) - - LOG.info("##===---- UserClient Stopped ----===##") diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 6cbed1ce..c5cf11dd 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -6,8 +6,7 @@ Various functions and decorators for PKC from __future__ import absolute_import, division, unicode_literals from logging import getLogger from sqlite3 import connect, OperationalError -from datetime import datetime, timedelta -from time import localtime, strftime +from datetime import datetime from unicodedata import normalize try: import xml.etree.cElementTree as etree @@ -19,7 +18,7 @@ except ImportError: import defusedxml.ElementTree as defused_etree # etree parse unsafe from xml.etree.ElementTree import ParseError ETREE = 'ElementTree' -from functools import wraps, partial +from functools import wraps from urllib import quote_plus import hashlib import re @@ -28,7 +27,7 @@ import xbmc import xbmcaddon import xbmcgui -from . import path_ops, variables as v, state +from . import path_ops, variables as v ############################################################################### @@ -36,7 +35,6 @@ LOG = getLogger('PLEX.utils') WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') -EPOCH = datetime.utcfromtimestamp(0) # Grab Plex id from '...plex_id=XXXX....' REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''') @@ -107,17 +105,14 @@ def window(prop, value=None, clear=False, windowid=10000): return try_decode(win.getProperty(prop)) -def plex_command(key, value): +def plex_command(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(20) - window('plex_command', value='%s-%s' % (key, value)) + window('plex_command', value=value) def settings(setting, value=None): @@ -238,7 +233,8 @@ def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False): short = str(sys.exc_info()[1]) LOG.error('Error encountered: %s - %s', txt, short) if cancel_sync: - state.STOP_SYNC = True + import app + app.SYNC.stop_sync = True if hide_tb: return short @@ -275,47 +271,6 @@ class AttributeDict(dict): return self.__unicode__().encode('utf-8') -def millis_to_kodi_time(milliseconds): - """ - Converts time in milliseconds to the time dict used by the Kodi JSON RPC: - { - 'hours': [int], - 'minutes': [int], - 'seconds'[int], - 'milliseconds': [int] - } - Pass in the time in milliseconds as an int - """ - seconds = int(milliseconds / 1000) - minutes = int(seconds / 60) - seconds = seconds % 60 - hours = int(minutes / 60) - minutes = minutes % 60 - milliseconds = milliseconds % 1000 - return {'hours': hours, - 'minutes': minutes, - 'seconds': seconds, - 'milliseconds': milliseconds} - - -def kodi_time_to_millis(time): - """ - Converts the Kodi time dict - { - 'hours': [int], - 'minutes': [int], - 'seconds'[int], - 'milliseconds': [int] - } - to milliseconds [int]. Will not return negative results but 0! - """ - ret = (time['hours'] * 3600 + - time['minutes'] * 60 + - time['seconds']) * 1000 + time['milliseconds'] - ret = 0 if ret < 0 else ret - return ret - - def cast(func, value): """ Cast the specified value to the specified type (returned by func). Currently this @@ -434,47 +389,6 @@ def escape_html(string): return string -def unix_date_to_kodi(stamp): - """ - converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a - propper, human-readable time stamp used by Kodi - - Output: Y-m-d h:m:s = 2009-04-05 23:16:04 - - None if an error was encountered - """ - try: - stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET - date_time = localtime(stamp) - localdate = strftime('%Y-%m-%d %H:%M:%S', date_time) - except: - localdate = None - return localdate - - -def kodi_time_to_plex(stamp): - """ - Returns a Kodi timestamp (int/float) in Plex time (subtracting the - KODI_PLEX_TIME_OFFSET) - """ - return stamp - state.KODI_PLEX_TIME_OFFSET - - -def unix_timestamp(seconds_into_the_future=None): - """ - Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as - an integer. - - Optionally, pass seconds_into_the_future: positive int's will result in a - future timestamp, negative the past - """ - if seconds_into_the_future: - future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future) - else: - future = datetime.utcnow() - return int((future - EPOCH).total_seconds()) - - def kodi_sql(media_type=None): """ Open a connection to the Kodi database. @@ -1135,95 +1049,3 @@ def log_time(func): elapsedtotal, func.__name__) return result return wrapper - - -def thread_methods(cls=None, add_stops=None, add_suspends=None): - """ - Decorator to add the following methods to a threading class: - - suspend(): pauses the thread - resume(): resumes the thread - stop(): stopps/kills the thread - - suspended(): returns True if thread is suspended - stopped(): returns True if thread is stopped (or should stop ;-)) - ALSO returns True if PKC should exit - - Also adds the following class attributes: - thread_stopped - thread_suspended - stops - suspends - - invoke with either - @thread_methods - class MyClass(): - or - @thread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'], - add_suspends=['DB_SCAN', 'WHATEVER']) - class MyClass(): - """ - # So we don't need to invoke with () - if cls is None: - return partial(thread_methods, - add_stops=add_stops, - add_suspends=add_suspends) - # Because we need a reference, not a copy of the immutable objects in - # state, we need to look up state every time explicitly - cls.stops = ['STOP_PKC'] - if add_stops is not None: - cls.stops.extend(add_stops) - cls.suspends = add_suspends or [] - - # Attach new attributes to class - cls.thread_stopped = False - cls.thread_suspended = False - - # Define new class methods and attach them to class - def stop(self): - """ - Call to stop this thread - """ - self.thread_stopped = True - cls.stop = stop - - def suspend(self): - """ - Call to suspend this thread - """ - self.thread_suspended = True - cls.suspend = suspend - - def resume(self): - """ - Call to revive a suspended thread back to life - """ - self.thread_suspended = False - cls.resume = resume - - def suspended(self): - """ - Returns True if the thread is suspended - """ - if self.thread_suspended is True: - return True - for suspend in self.suspends: - if getattr(state, suspend): - return True - return False - cls.suspended = suspended - - def stopped(self): - """ - Returns True if the thread is stopped - """ - if self.thread_stopped is True: - return True - for stop in self.stops: - if getattr(state, stop): - return True - return False - cls.stopped = 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 c796e457..58af5283 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -2,11 +2,10 @@ # -*- coding: utf-8 -*- from logging import getLogger from json import loads -from threading import Thread from ssl import CERT_NONE from xbmc import sleep -from . import websocket, utils, companion, state, variables as v +from . import backgroundthread, websocket, utils, companion, app, variables as v ############################################################################### @@ -15,7 +14,7 @@ LOG = getLogger('PLEX.websocket_client') ############################################################################### -class WebSocket(Thread): +class WebSocket(backgroundthread.KillableThread): opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) def __init__(self): @@ -48,18 +47,15 @@ class WebSocket(Thread): def run(self): LOG.info("----===## Starting %s ##===----", self.__class__.__name__) - counter = 0 - stopped = self.stopped - suspended = self.suspended - while not stopped(): + while not self.isCanceled(): # In the event the server goes offline - while suspended(): + while self.isSuspended(): # Set in service.py if self.ws is not None: self.ws.close() self.ws = None - if stopped(): + if self.isCanceled(): # Abort was requested while waiting. We should exit LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) @@ -133,14 +129,20 @@ class WebSocket(Thread): LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) -@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', - 'BACKGROUND_SYNC_DISABLED']) class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion """ + def isSuspended(self): + """ + Returns True if the thread is suspended + """ + return (self._suspended or + app.SYNC.suspend_library_thread or + app.SYNC.background_sync_disabled) + def getUri(self): - server = utils.window('pms_server') + server = app.CONN.server # Get the appropriate prefix for the websocket if server.startswith('https'): server = "wss%s" % server[5:] @@ -148,8 +150,8 @@ class PMS_Websocket(WebSocket): 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 app.ACCOUNT.plex_token: + uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token sslopt = {} if utils.settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE @@ -185,30 +187,33 @@ class PMS_Websocket(WebSocket): # Drop everything we're not interested in if typus not in ('playing', 'timeline', 'activity'): return - elif typus == 'activity' and state.DB_SCAN is True: + elif typus == 'activity' and app.SYNC.db_scan is True: # Only add to processing if PKC is NOT doing a lib scan (and thus # possibly causing these reprocessing messages en mass) LOG.debug('%s: Dropping message as PKC is currently synching', self.__class__.__name__) else: # Put PMS message on queue and let libsync take care of it - state.WEBSOCKET_QUEUE.put(message) + app.APP.websocket_queue.put(message) class Alexa_Websocket(WebSocket): """ Websocket connection to talk to Amazon Alexa. - - Can't use utils.thread_methods! """ - thread_stopped = False - thread_suspended = False + def isSuspended(self): + """ + Overwrite method since we need to check for plex token + """ + return (self._suspended or + not app.ACCOUNT.plex_token or + app.ACCOUNT.restricted_user) def getUri(self): uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s' - % (state.PLEX_USER_ID, + % (app.ACCOUNT.plex_user_id, v.PKC_MACHINE_IDENTIFIER, - state.PLEX_TOKEN)) + app.ACCOUNT.plex_token)) sslopt = {} LOG.debug("%s: Uri: %s, sslopt: %s", self.__class__.__name__, uri, sslopt) @@ -238,33 +243,3 @@ class Alexa_Websocket(WebSocket): self.__class__.__name__) return companion.process_command(message.attrib['path'][1:], message.attrib) - - # Path in utils.thread_methods - def stop(self): - self.thread_stopped = True - - def suspend(self): - self.thread_suspended = True - - def resume(self): - self.thread_suspended = False - - def stopped(self): - if self.thread_stopped is True: - return True - if state.STOP_PKC: - return True - return False - - # The culprit - def 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 diff --git a/resources/lib/windows/signin.py b/resources/lib/windows/signin.py index 4b083ebf..93989990 100644 --- a/resources/lib/windows/signin.py +++ b/resources/lib/windows/signin.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals import xbmcgui from . import kodigui -from .. import utils, variables as v +from .. import variables as v class Background(kodigui.BaseWindow): diff --git a/resources/settings.xml b/resources/settings.xml index 2d0ccf2b..57fc4cce 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -16,7 +16,6 @@ -