diff --git a/addon.xml b/addon.xml index 4803b5a2..703d6adc 100644 --- a/addon.xml +++ b/addon.xml @@ -3,8 +3,8 @@ - - + + video audio image diff --git a/default.py b/default.py index 71fc5cc7..96ae547a 100644 --- a/default.py +++ b/default.py @@ -1,47 +1,19 @@ # -*- coding: utf-8 -*- ############################################################################### -from __future__ import unicode_literals # nice to fix os.walk unicode +from __future__ import absolute_import, division, unicode_literals import logging -from os import path as os_path -from sys import path as sys_path, argv +from sys import argv from urlparse import parse_qsl - -from xbmc import translatePath, sleep, executebuiltin -from xbmcaddon import Addon +from xbmc import sleep, executebuiltin from xbmcgui import ListItem from xbmcplugin import setResolvedUrl -_addon = Addon(id='plugin.video.plexkodiconnect') -try: - _addon_path = _addon.getAddonInfo('path').decode('utf-8') -except TypeError: - _addon_path = _addon.getAddonInfo('path').decode() -try: - _base_resource = translatePath(os_path.join( - _addon_path, - 'resources', - 'lib')).decode('utf-8') -except TypeError: - _base_resource = translatePath(os_path.join( - _addon_path, - 'resources', - 'lib')).decode() -sys_path.append(_base_resource) +from resources.lib import entrypoint, utils, pickler, pkc_listitem, \ + variables as v, loghandler ############################################################################### -import entrypoint -from utils import window, reset, passwords_xml, language as lang, dialog, \ - plex_command -from pickler import unpickle_me, pickl_window -from PKC_listitem import convert_PKC_to_listitem -import variables as v - -############################################################################### - -import loghandler - loghandler.config() log = logging.getLogger('PLEX.default') @@ -104,7 +76,7 @@ class Main(): entrypoint.create_new_pms() elif mode == 'reset': - reset() + utils.reset() elif mode == 'togglePlexTV': entrypoint.toggle_plex_tv_sign_in() @@ -113,41 +85,41 @@ class Main(): entrypoint.reset_authorization() elif mode == 'passwords': - passwords_xml() + utils.passwords_xml() elif mode == 'switchuser': entrypoint.switch_plex_user() elif mode in ('manualsync', 'repair'): - if window('plex_online') != 'true': + if pickler.pickl_window('plex_online') != 'true': # Server is not online, do not run the sync - dialog('ok', lang(29999), lang(39205)) + utils.dialog('ok', utils.lang(29999), utils.lang(39205)) log.error('Not connected to a PMS.') else: if mode == 'repair': log.info('Requesting repair lib sync') - plex_command('RUN_LIB_SCAN', 'repair') + utils.plex_command('RUN_LIB_SCAN', 'repair') elif mode == 'manualsync': log.info('Requesting full library scan') - plex_command('RUN_LIB_SCAN', 'full') + utils.plex_command('RUN_LIB_SCAN', 'full') elif mode == 'texturecache': log.info('Requesting texture caching of all textures') - plex_command('RUN_LIB_SCAN', 'textures') + utils.plex_command('RUN_LIB_SCAN', 'textures') elif mode == 'chooseServer': entrypoint.choose_pms_server() elif mode == 'refreshplaylist': log.info('Requesting playlist/nodes refresh') - plex_command('RUN_LIB_SCAN', 'views') + utils.plex_command('RUN_LIB_SCAN', 'views') elif mode == 'deviceid': self.deviceid() elif mode == 'fanart': log.info('User requested fanarttv refresh') - plex_command('RUN_LIB_SCAN', 'fanart') + utils.plex_command('RUN_LIB_SCAN', 'fanart') elif '/extrafanart' in argv[0]: plexpath = argv[2][1:] @@ -171,40 +143,40 @@ class Main(): """ request = '%s&handle=%s' % (argv[2], HANDLE) # Put the request into the 'queue' - plex_command('PLAY', request) + utils.plex_command('PLAY', request) if HANDLE == -1: # Handle -1 received, not waiting for main thread return # Wait for the result - while not pickl_window('plex_result'): + while not pickler.pickl_window('plex_result'): sleep(50) - result = unpickle_me() + result = pickler.unpickle_me() if result is None: log.error('Error encountered, aborting') - dialog('notification', - heading='{plex}', - message=lang(30128), - icon='{error}', - time=3000) + utils.dialog('notification', + heading='{plex}', + message=utils.lang(30128), + icon='{error}', + time=3000) setResolvedUrl(HANDLE, False, ListItem()) elif result.listitem: - listitem = convert_PKC_to_listitem(result.listitem) + listitem = pkc_listitem.convert_pkc_to_listitem(result.listitem) setResolvedUrl(HANDLE, True, listitem) @staticmethod def deviceid(): - deviceId_old = window('plex_client_Id') + deviceId_old = pickler.pickl_window('plex_client_Id') from clientinfo import getDeviceId try: deviceId = getDeviceId(reset=True) except Exception as e: log.error('Failed to generate a new device Id: %s' % e) - dialog('ok', lang(29999), lang(33032)) + utils.dialog('ok', utils.lang(29999), utils.lang(33032)) else: log.info('Successfully removed old device ID: %s New deviceId:' '%s' % (deviceId_old, deviceId)) # 'Kodi will now restart to apply the changes' - dialog('ok', lang(29999), lang(33033)) + utils.dialog('ok', utils.lang(29999), utils.lang(33033)) executebuiltin('RestartApp') diff --git a/resources/lib/PKC_listitem.py b/resources/lib/PKC_listitem.py index e34a0019..00956888 100644 --- a/resources/lib/PKC_listitem.py +++ b/resources/lib/PKC_listitem.py @@ -3,11 +3,11 @@ from xbmcgui import ListItem -def convert_PKC_to_listitem(PKC_listitem): +def convert_pkc_to_listitem(pkc_listitem): """ - Insert a PKC_listitem and you will receive a valid XBMC listitem + Insert a PKCListItem() and you will receive a valid XBMC listitem """ - data = PKC_listitem.data + data = pkc_listitem.data listitem = ListItem(label=data.get('label'), label2=data.get('label2'), path=data.get('path')) @@ -26,7 +26,7 @@ def convert_PKC_to_listitem(PKC_listitem): return listitem -class PKC_ListItem(object): +class PKCListItem(object): """ Imitates xbmcgui.ListItem and its functions. Pass along PKC_Listitem().data when pickling! diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index d778ad6e..c6256a63 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -12,18 +12,17 @@ import requests import xbmc from xbmcvfs import exists -from utils import settings, language as lang, kodi_sql, try_encode, try_decode,\ - thread_methods, dialog, exists_dir -import state +from . import utils +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.artwork') # Disable annoying requests warnings requests.packages.urllib3.disable_warnings() ARTWORK_QUEUE = Queue() IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC'] -if not settings('imageSyncDuringPlayback') == 'true': +if not utils.settings('imageSyncDuringPlayback') == 'true': IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC') ############################################################################### @@ -37,7 +36,7 @@ def double_urldecode(text): return unquote(unquote(text)) -@thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS) +@utils.thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS) class Image_Cache_Thread(Thread): sleep_between = 50 # Potentially issues with limited number of threads @@ -73,20 +72,20 @@ class Image_Cache_Thread(Thread): 'Window.IsVisible(DialogAddonSettings.xml)'): # Avoid saving '0' all the time set_zero = True - settings('caching_artwork_count', value='0') + utils.settings('caching_artwork_count', value='0') xbmc.sleep(1000) continue set_zero = False if isinstance(url, ArtworkSyncMessage): if state.IMAGE_SYNC_NOTIFICATIONS: - dialog('notification', - heading=lang(29999), - message=url.message, - icon='{plex}', - sound=False) + utils.dialog('notification', + heading=utils.lang(29999), + message=url.message, + icon='{plex}', + sound=False) queue.task_done() continue - url = double_urlencode(try_encode(url)) + url = double_urlencode(utils.try_encode(url)) sleeptime = 0 while True: try: @@ -133,14 +132,15 @@ class Image_Cache_Thread(Thread): if (counter > 20 and not xbmc.getCondVisibility( 'Window.IsVisible(DialogAddonSettings.xml)')): counter = 0 - settings('caching_artwork_count', value=str(queue.qsize())) + utils.settings('caching_artwork_count', + value=str(queue.qsize())) # Sleep for a bit to reduce CPU strain xbmc.sleep(sleep_between) LOG.info("---===### Stopped Image_Cache_Thread ###===---") class Artwork(): - enableTextureCache = settings('enableTextureCache') == "true" + enableTextureCache = utils.settings('enableTextureCache') == "true" if enableTextureCache: queue = ARTWORK_QUEUE @@ -156,7 +156,7 @@ class Artwork(): artworks = list() # Get all posters and fanart/background for video and music for kind in ('video', 'music'): - connection = kodi_sql(kind) + connection = utils.kodi_sql(kind) cursor = connection.cursor() for typus in ('poster', 'fanart'): cursor.execute('SELECT url FROM art WHERE type == ?', @@ -164,7 +164,7 @@ class Artwork(): artworks.extend(cursor.fetchall()) connection.close() artworks_to_cache = list() - connection = kodi_sql('texture') + connection = utils.kodi_sql('texture') cursor = connection.cursor() for url in artworks: query = 'SELECT url FROM texture WHERE url == ? LIMIT 1' @@ -175,40 +175,41 @@ class Artwork(): if not artworks_to_cache: LOG.info('Caching of major images to Kodi texture cache done') # Set to "None" - settings('caching_artwork_count', value=lang(30069)) + utils.settings('caching_artwork_count', value=utils.lang(30069)) return length = len(artworks_to_cache) LOG.info('Caching has not been completed - caching %s major images', length) - settings('caching_artwork_count', value=str(length)) + utils.settings('caching_artwork_count', value=str(length)) # Caching %s Plex images - self.queue.put(ArtworkSyncMessage(lang(30006) % length)) + self.queue.put(ArtworkSyncMessage(utils.lang(30006) % length)) for i, url in enumerate(artworks_to_cache): self.queue.put(url[0]) # Plex image caching done - self.queue.put(ArtworkSyncMessage(lang(30007))) + self.queue.put(ArtworkSyncMessage(utils.lang(30007))) def fullTextureCacheSync(self): """ This method will sync all Kodi artwork to textures13.db and cache them locally. This takes diskspace! """ - if not dialog('yesno', "Image Texture Cache", lang(39250)): + if not utils.dialog('yesno', "Image Texture Cache", utils.lang(39250)): return LOG.info("Doing Image Cache Sync") # ask to rest all existing or not - if dialog('yesno', "Image Texture Cache", lang(39251)): + if utils.dialog('yesno', "Image Texture Cache", utils.lang(39251)): LOG.info("Resetting all cache data first") # Remove all existing textures first - path = try_decode(xbmc.translatePath("special://thumbnails/")) - if exists_dir(path): + path = utils.try_decode( + xbmc.translatePath("special://thumbnails/")) + if utils.exists_dir(path): rmtree(path, ignore_errors=True) self.restore_cache_directories() # remove all existing data from texture DB - connection = kodi_sql('texture') + connection = utils.kodi_sql('texture') cursor = connection.cursor() query = 'SELECT tbl_name FROM sqlite_master WHERE type=?' cursor.execute(query, ('table', )) @@ -221,7 +222,7 @@ class Artwork(): connection.close() # Cache all entries in video DB - connection = kodi_sql('video') + connection = utils.kodi_sql('video') cursor = connection.cursor() # dont include actors query = "SELECT url FROM art WHERE media_type != ?" @@ -234,7 +235,7 @@ class Artwork(): for url in result: self.cache_texture(url[0]) # Cache all entries in music DB - connection = kodi_sql('music') + connection = utils.kodi_sql('music') cursor = connection.cursor() cursor.execute("SELECT url FROM art") result = cursor.fetchall() @@ -309,7 +310,7 @@ class Artwork(): """ Deleted the cached artwork with path url (if it exists) """ - connection = kodi_sql('texture') + connection = utils.kodi_sql('texture') cursor = connection.cursor() try: cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1", @@ -323,7 +324,7 @@ class Artwork(): path = xbmc.translatePath("special://thumbnails/%s" % cachedurl) LOG.debug("Deleting cached thumbnail: %s", path) if exists(path): - rmtree(try_decode(path), ignore_errors=True) + rmtree(utils.try_decode(path), ignore_errors=True) cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) connection.commit() finally: @@ -336,8 +337,8 @@ class Artwork(): "a", "b", "c", "d", "e", "f", "Video", "plex") for path in paths: - makedirs(try_decode(xbmc.translatePath("special://thumbnails/%s" - % path))) + makedirs(utils.try_decode( + xbmc.translatePath("special://thumbnails/%s" % path))) class ArtworkSyncMessage(object): diff --git a/resources/lib/clientinfo.py b/resources/lib/clientinfo.py index 05294313..edf67f0e 100644 --- a/resources/lib/clientinfo.py +++ b/resources/lib/clientinfo.py @@ -3,12 +3,12 @@ ############################################################################### from logging import getLogger -from utils import window, settings -import variables as v +from . import utils +from . import variables as v ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger('PLEX.clientinfo') ############################################################################### @@ -43,8 +43,8 @@ def getXArgsDeviceInfo(options=None, include_token=True): 'X-Plex-Client-Identifier': getDeviceId(), 'X-Plex-Provides': 'client,controller,player,pubsub-player', } - if include_token and window('pms_token'): - xargs['X-Plex-Token'] = window('pms_token') + if include_token and utils.window('pms_token'): + xargs['X-Plex-Token'] = utils.window('pms_token') if options is not None: xargs.update(options) return xargs @@ -60,26 +60,26 @@ def getDeviceId(reset=False): """ if reset is True: v.PKC_MACHINE_IDENTIFIER = None - window('plex_client_Id', clear=True) - settings('plex_client_Id', value="") + utils.window('plex_client_Id', clear=True) + utils.settings('plex_client_Id', value="") client_id = v.PKC_MACHINE_IDENTIFIER if client_id: return client_id - client_id = settings('plex_client_Id') + client_id = utils.settings('plex_client_Id') # Because Kodi appears to cache file settings!! if client_id != "" and reset is False: v.PKC_MACHINE_IDENTIFIER = client_id - window('plex_client_Id', value=client_id) - log.info("Unique device Id plex_client_Id loaded: %s", client_id) + utils.window('plex_client_Id', value=client_id) + LOG.info("Unique device Id plex_client_Id loaded: %s", client_id) return client_id - log.info("Generating a new deviceid.") + LOG.info("Generating a new deviceid.") from uuid import uuid4 client_id = str(uuid4()) - settings('plex_client_Id', value=client_id) + utils.settings('plex_client_Id', value=client_id) v.PKC_MACHINE_IDENTIFIER = client_id - window('plex_client_Id', value=client_id) - log.info("Unique device Id plex_client_Id generated: %s", client_id) + utils.window('plex_client_Id', value=client_id) + LOG.info("Unique device Id plex_client_Id generated: %s", client_id) return client_id diff --git a/resources/lib/command_pipeline.py b/resources/lib/command_pipeline.py index 6b889445..9604a0e1 100644 --- a/resources/lib/command_pipeline.py +++ b/resources/lib/command_pipeline.py @@ -2,18 +2,17 @@ ############################################################################### import logging from threading import Thread - from xbmc import sleep -from utils import window, thread_methods -import state +from . import utils +from . import state ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = logging.getLogger('PLEX.command_pipeline') ############################################################################### -@thread_methods +@utils.thread_methods class Monitor_Window(Thread): """ Monitors window('plex_command') for new entries that we need to take care @@ -26,9 +25,9 @@ class Monitor_Window(Thread): queue = state.COMMAND_PIPELINE_QUEUE LOG.info("----===## Starting Kodi_Play_Client ##===----") while not stopped(): - if window('plex_command'): - value = window('plex_command') - window('plex_command', clear=True) + 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': diff --git a/resources/lib/companion.py b/resources/lib/companion.py index 60aa29ce..d48aedb1 100644 --- a/resources/lib/companion.py +++ b/resources/lib/companion.py @@ -2,18 +2,17 @@ Processes Plex companion inputs from the plexbmchelper to Kodi commands """ from logging import getLogger - from xbmc import Player -from variables import ALEXA_TO_COMPANION -import playqueue as PQ -from PlexFunctions import GetPlexKeyNumber -import json_rpc as js -import state +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 ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.companion') ############################################################################### @@ -25,7 +24,7 @@ def skip_to(params): Does not seem to be implemented yet by Plex! """ playqueue_item_id = params.get('playQueueItemID') - _, plex_id = GetPlexKeyNumber(params.get('key')) + _, plex_id = PF.GetPlexKeyNumber(params.get('key')) LOG.debug('Skipping to playQueueItemID %s, plex_id %s', playqueue_item_id, plex_id) found = True @@ -51,8 +50,8 @@ def convert_alexa_to_companion(dictionary): The params passed by Alexa must first be converted to Companion talk """ for key in dictionary: - if key in ALEXA_TO_COMPANION: - dictionary[ALEXA_TO_COMPANION[key]] = dictionary[key] + if key in v.ALEXA_TO_COMPANION: + dictionary[v.ALEXA_TO_COMPANION[key]] = dictionary[key] del dictionary[key] diff --git a/resources/lib/context.py b/resources/lib/context.py index ef9f67c9..3f629689 100644 --- a/resources/lib/context.py +++ b/resources/lib/context.py @@ -2,15 +2,14 @@ ############################################################################### from logging import getLogger from os.path import join - import xbmcgui from xbmcaddon import Addon -from utils import window +from . import utils ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.context') ADDON = Addon('plugin.video.plexkodiconnect') ACTION_PARENT_DIR = 9 @@ -44,8 +43,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog): return self.selected_option def onInit(self): - if window('PlexUserImage'): - self.getControl(USER_IMAGE).setImage(window('PlexUserImage')) + if utils.window('PlexUserImage'): + self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage')) 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 f9b4a2ca..65d80d80 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -1,35 +1,33 @@ # -*- coding: utf-8 -*- ############################################################################### from logging import getLogger - from xbmcaddon import Addon import xbmc -import xbmcplugin import xbmcgui -import context -import plexdb_functions as plexdb -from utils import window, settings, dialog, language as lang -import PlexFunctions as PF -from PlexAPI import API -import playqueue as PQ -import variables as v -import state +from . import context +from . import plexdb_functions as plexdb +from . import utils +from . import plex_functions as PF +from .plex_api import API +from . import playqueue as PQ +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.context_entry') OPTIONS = { - 'Refresh': lang(30410), - 'Delete': lang(30409), - 'Addon': lang(30408), - # 'AddFav': lang(30405), - # 'RemoveFav': lang(30406), - # 'RateSong': lang(30407), - 'Transcode': lang(30412), - 'PMS_Play': lang(30415), # Use PMS to start playback - 'Extras': lang(30235) + 'Refresh': utils.lang(30410), + 'Delete': utils.lang(30409), + 'Addon': utils.lang(30408), + # 'AddFav': utils.lang(30405), + # 'RemoveFav': utils.lang(30406), + # 'RateSong': utils.lang(30407), + 'Transcode': utils.lang(30412), + 'PMS_Play': utils.lang(30415), # Use PMS to start playback + 'Extras': utils.lang(30235) } ############################################################################### @@ -98,8 +96,8 @@ class ContextMenu(object): options.append(OPTIONS['Transcode']) # Delete item, only if the Plex Home main user is logged in - if (window('plex_restricteduser') != 'true' and - window('plex_allows_mediaDeletion') == 'true'): + if (utils.window('plex_restricteduser') != 'true' and + utils.window('plex_allows_mediaDeletion') == 'true'): options.append(OPTIONS['Delete']) # Addon settings options.append(OPTIONS['Addon']) @@ -138,14 +136,14 @@ class ContextMenu(object): Delete item on PMS """ delete = True - if settings('skipContextMenu') != "true": - if not dialog("yesno", heading="{plex}", line1=lang(33041)): + if utils.settings('skipContextMenu') != "true": + if not utils.dialog("yesno", heading="{plex}", line1=utils.lang(33041)): LOG.info("User skipped deletion for: %s", self.plex_id) delete = False if delete: LOG.info("Deleting Plex item with id %s", self.plex_id) if PF.delete_item_from_pms(self.plex_id) is False: - dialog("ok", heading="{plex}", line1=lang(30414)) + utils.dialog("ok", heading="{plex}", line1=utils.lang(30414)) def _PMS_play(self): """ diff --git a/resources/lib/downloadutils.py b/resources/lib/downloadutils.py index 3c28ab11..420b7f33 100644 --- a/resources/lib/downloadutils.py +++ b/resources/lib/downloadutils.py @@ -5,10 +5,9 @@ from logging import getLogger import xml.etree.ElementTree as etree import requests -from utils import window, language as lang, dialog -import clientinfo as client - -import state +from . import utils +from . import clientinfo +from . import state ############################################################################### @@ -16,7 +15,7 @@ import state import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.downloadutils') ############################################################################### @@ -75,22 +74,22 @@ class DownloadUtils(): # Start session self.s = requests.Session() - self.deviceId = client.getDeviceId() + self.deviceId = clientinfo.getDeviceId() # Attach authenticated header to the session - self.s.headers = client.getXArgsDeviceInfo() + self.s.headers = clientinfo.getXArgsDeviceInfo() self.s.encoding = 'utf-8' # Set SSL settings self.setSSL() # Set other stuff - self.setServer(window('pms_server')) + 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 if reset is True: - window('countUnauthorized', value='0') - window('countError', value='0') + utils.window('countUnauthorized', value='0') + utils.window('countError', value='0') # Retry connections to the server self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1)) @@ -110,7 +109,7 @@ class DownloadUtils(): LOG.info('Request session stopped') def getHeader(self, options=None): - header = client.getXArgsDeviceInfo() + header = clientinfo.getXArgsDeviceInfo() if options is not None: header.update(options) return header @@ -227,9 +226,9 @@ class DownloadUtils(): else: # We COULD contact the PMS, hence it ain't dead if authenticate is True: - window('countError', value='0') + utils.window('countError', value='0') if r.status_code != 401: - window('countUnauthorized', value='0') + utils.window('countUnauthorized', value='0') if r.status_code == 204: # No body in the response @@ -247,9 +246,10 @@ class DownloadUtils(): LOG.info(r.text) if '401 Unauthorized' in r.text: # Truly unauthorized - window('countUnauthorized', - value=str(int(window('countUnauthorized')) + 1)) - if (int(window('countUnauthorized')) >= + utils.window( + 'countUnauthorized', + value=str(int(utils.window('countUnauthorized')) + 1)) + if (int(utils.window('countUnauthorized')) >= self.unauthorizedAttempts): LOG.warn('We seem to be truly unauthorized for PMS' ' %s ', url) @@ -258,11 +258,11 @@ class DownloadUtils(): LOG.debug('Setting PMS server status to ' 'unauthorized') state.PMS_STATUS = '401' - window('plex_serverStatus', value="401") - dialog('notification', - lang(29999), - lang(30017), - icon='{error}') + utils.window('plex_serverStatus', value="401") + utils.dialog('notification', + utils.lang(29999), + utils.lang(30017), + icon='{error}') else: # there might be other 401 where e.g. PMS under strain LOG.info('PMS might only be under strain') @@ -312,12 +312,12 @@ class DownloadUtils(): if authenticate is True: # Make the addon aware of status try: - window('countError', - value=str(int(window('countError')) + 1)) - if int(window('countError')) >= self.connectionAttempts: + utils.window('countError', + value=str(int(utils.window('countError')) + 1)) + if int(utils.window('countError')) >= self.connectionAttempts: LOG.warn('Failed to connect to %s too many times. ' 'Declare PMS dead', url) - window('plex_online', value="false") + utils.window('plex_online', value="false") except ValueError: # 'countError' not yet set pass diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index febc2993..1c78436a 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -10,23 +10,19 @@ from os import walk, makedirs from os.path import basename, join from sys import argv from urllib import urlencode - import xbmcplugin from xbmc import sleep, executebuiltin, translatePath from xbmcgui import ListItem -from utils import window, settings, language as lang, dialog, try_encode, \ - catch_exceptions, exists_dir, plex_command, try_decode -import downloadutils - -from PlexFunctions import GetPlexMetadata, GetPlexSectionResults, \ - GetMachineIdentifier -from PlexAPI import API -import json_rpc as js -import variables as v +from . import utils +from .downloadutils import DownloadUtils as DU +from .plex_api import API +from . import plex_functions as PF +from . import json_rpc as js +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.entrypoint') try: HANDLE = int(argv[1]) @@ -47,8 +43,8 @@ def choose_pms_server(): server = setup.pick_pms(showDialog=True) if server is None: LOG.error('We did not connect to a new PMS, aborting') - plex_command('SUSPEND_USER_CLIENT', 'False') - plex_command('SUSPEND_LIBRARY_THREAD', 'False') + utils.plex_command('SUSPEND_USER_CLIENT', 'False') + utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False') return LOG.info("User chose server %s", server['name']) @@ -65,12 +61,12 @@ def choose_pms_server(): _log_in() LOG.info("Choosing new PMS complete") # ' connected' - dialog('notification', - lang(29999), - '%s %s' % (server['name'], lang(39220)), - icon='{plex}', - time=3000, - sound=False) + utils.dialog('notification', + utils.lang(29999), + '%s %s' % (server['name'], utils.lang(39220)), + icon='{plex}', + time=3000, + sound=False) def toggle_plex_tv_sign_in(): @@ -78,38 +74,38 @@ 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 settings('plexToken'): + if utils.settings('plexToken'): LOG.info('Reseting plex.tv credentials in settings') - settings('plexLogin', value="") - settings('plexToken', value="") - settings('plexid', value="") - settings('plexHomeSize', value="1") - settings('plexAvatar', value="") - settings('plex_status', value=lang(39226)) + utils.settings('plexLogin', value="") + utils.settings('plexToken', value="") + utils.settings('plexid', value="") + utils.settings('plexHomeSize', value="1") + utils.settings('plexAvatar', value="") + utils.settings('plex_status', value=utils.lang(39226)) - window('plex_token', clear=True) - plex_command('PLEX_TOKEN', '') - plex_command('PLEX_USERNAME', '') + utils.window('plex_token', clear=True) + utils.plex_command('PLEX_TOKEN', '') + utils.plex_command('PLEX_USERNAME', '') else: LOG.info('Login to plex.tv') import initialsetup initialsetup.InitialSetup().plex_tv_sign_in() - dialog('notification', - lang(29999), - lang(39221), - icon='{plex}', - time=3000, - sound=False) + 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 """ - resp = dialog('yesno', heading="{plex}", line1=lang(39206)) + resp = utils.dialog('yesno', heading="{plex}", line1=utils.lang(39206)) if resp == 1: LOG.info("Reset login attempts.") - plex_command('PMS_STATUS', 'Auth') + utils.plex_command('PMS_STATUS', 'Auth') else: executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') @@ -138,17 +134,17 @@ def show_main_menu(content_type=None): LOG.debug('Do main listing with content_type: %s', content_type) xbmcplugin.setContent(HANDLE, 'files') # Get emby nodes from the window props - plexprops = window('Plex.nodes.total') + plexprops = utils.window('Plex.nodes.total') if plexprops: totalnodes = int(plexprops) for i in range(totalnodes): - path = window('Plex.nodes.%s.index' % i) + path = utils.window('Plex.nodes.%s.index' % i) if not path: - path = window('Plex.nodes.%s.content' % i) + path = utils.window('Plex.nodes.%s.content' % i) if not path: continue - label = window('Plex.nodes.%s.title' % i) - node_type = window('Plex.nodes.%s.type' % i) + label = utils.window('Plex.nodes.%s.title' % i) + node_type = utils.window('Plex.nodes.%s.type' % i) # because we do not use seperate entrypoints for each content type, # we need to figure out which items to show in each listing. for # now we just only show picture nodes in the picture library video @@ -161,21 +157,19 @@ def show_main_menu(content_type=None): # Plex Watch later if content_type not in ('image', 'audio'): - directory_item(lang(39211), + directory_item(utils.lang(39211), "plugin://%s?mode=watchlater" % v.ADDON_ID) # Plex Channels - directory_item(lang(30173), - "plugin://%s?mode=channels" % v.ADDON_ID) + directory_item(utils.lang(30173), "plugin://%s?mode=channels" % v.ADDON_ID) # Plex user switch - directory_item('%s%s' % (lang(39200), settings('username')), + directory_item('%s%s' % (utils.lang(39200), utils.settings('username')), "plugin://%s?mode=switchuser" % v.ADDON_ID) # some extra entries for settings and stuff - directory_item(lang(39201), - "plugin://%s?mode=settings" % v.ADDON_ID) - directory_item(lang(39203), + directory_item(utils.lang(39201), "plugin://%s?mode=settings" % v.ADDON_ID) + directory_item(utils.lang(39203), "plugin://%s?mode=refreshplaylist" % v.ADDON_ID) - directory_item(lang(39204), + directory_item(utils.lang(39204), "plugin://%s?mode=manualsync" % v.ADDON_ID) xbmcplugin.endOfDirectory(HANDLE) @@ -188,7 +182,7 @@ def switch_plex_user(): # Guess these user avatars are a future feature. Skipping for now # Delete any userimages. Since there's always only 1 user: position = 0 # position = 0 - # window('EmbyAdditionalUserImage.%s' % position, clear=True) + # utils.window('EmbyAdditionalUserImage.%s' % position, clear=True) LOG.info("Plex home user switch requested") if not _log_out(): return @@ -258,7 +252,8 @@ def create_listitem(item, append_show_title=False, append_sxxexx=False): listitem.setArt({'icon': 'DefaultTVShows.png'}) listitem.setProperty('fanart_image', item['art'].get('tvshow.fanart', '')) try: - listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) + listitem.addContextMenuItems([(utils.lang(30032), + 'XBMC.Action(Info)',)]) except TypeError: # Kodi fuck-up pass @@ -287,7 +282,7 @@ def next_up_episodes(tagname, limit): 'properties': ['title', 'studio', 'mpaa', 'file', 'art'] } for item in js.get_tv_shows(params): - if settings('ignoreSpecialsNextEpisodes') == "true": + if utils.settings('ignoreSpecialsNextEpisodes') == "true": params = { 'tvshowid': item['tvshowid'], 'sort': {'method': "episode"}, @@ -383,8 +378,8 @@ def recent_episodes(mediatype, tagname, limit): # if the addon is called with recentepisodes parameter, # we return the recentepisodes list of the given tagname xbmcplugin.setContent(HANDLE, 'episodes') - append_show_title = settings('RecentTvAppendShow') == 'true' - append_sxxexx = settings('RecentTvAppendSeason') == 'true' + append_show_title = utils.settings('RecentTvAppendShow') == 'true' + append_sxxexx = utils.settings('RecentTvAppendSeason') == 'true' # First we get a list of all the TV shows - filtered by tag show_ids = set() params = { @@ -401,7 +396,7 @@ def recent_episodes(mediatype, tagname, limit): "dateadded", "lastplayed"], "limits": {"end": limit} } - if settings('TVShowWatched') == 'false': + if utils.settings('TVShowWatched') == 'false': params['filter'] = { 'operator': "lessthan", 'field': "playcount", @@ -444,7 +439,7 @@ def get_video_files(plex_id, params): LOG.info('No Plex ID found, abort getting Extras') return xbmcplugin.endOfDirectory(HANDLE) - item = GetPlexMetadata(plex_id) + item = PF.GetPlexMetadata(plex_id) try: path = item[0][0][0].attrib['file'] except (TypeError, IndexError, AttributeError, KeyError): @@ -459,17 +454,17 @@ def get_video_files(plex_id, params): path = path.replace('\\', '\\\\') # Directory only, get rid of filename path = path.replace(basename(path), '') - if exists_dir(path): + if utils.exists_dir(path): for root, dirs, files in walk(path): for directory in dirs: - item_path = try_encode(join(root, directory)) + item_path = utils.try_encode(join(root, directory)) listitem = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=item_path, listitem=listitem, isFolder=True) for file in files: - item_path = try_encode(join(root, file)) + item_path = utils.try_encode(join(root, file)) listitem = ListItem(item_path, path=item_path) xbmcplugin.addDirectoryItem(handle=HANDLE, url=file, @@ -480,7 +475,7 @@ def get_video_files(plex_id, params): xbmcplugin.endOfDirectory(HANDLE) -@catch_exceptions(warnuser=False) +@utils.catch_exceptions(warnuser=False) def extra_fanart(plex_id, plex_path): """ Get extrafanart for listitem @@ -497,12 +492,12 @@ def extra_fanart(plex_id, plex_path): # We need to store the images locally for this to work # because of the caching system in xbmc - fanart_dir = try_decode(translatePath( + fanart_dir = utils.try_decode(translatePath( "special://thumbnails/plex/%s/" % plex_id)) - if not exists_dir(fanart_dir): + if not utils.exists_dir(fanart_dir): # Download the images to the cache directory makedirs(fanart_dir) - xml = GetPlexMetadata(plex_id) + xml = PF.GetPlexMetadata(plex_id) if xml is None: LOG.error('Could not download metadata for %s', plex_id) return xbmcplugin.endOfDirectory(HANDLE) @@ -511,19 +506,20 @@ def extra_fanart(plex_id, plex_path): backdrops = api.artwork()['Backdrop'] for count, backdrop in enumerate(backdrops): # Same ordering as in artwork - art_file = try_encode(join(fanart_dir, "fanart%.3d.jpg" % count)) + art_file = utils.try_encode(join(fanart_dir, + "fanart%.3d.jpg" % count)) listitem = ListItem("%.3d" % count, path=art_file) xbmcplugin.addDirectoryItem( handle=HANDLE, url=art_file, listitem=listitem) - copyfile(backdrop, try_decode(art_file)) + copyfile(backdrop, utils.try_decode(art_file)) else: LOG.info("Found cached backdrop.") # Use existing cached images for root, _, files in walk(fanart_dir): for file in files: - art_file = try_encode(join(root, file)) + art_file = utils.try_encode(join(root, file)) listitem = ListItem(file, path=art_file) xbmcplugin.addDirectoryItem(handle=HANDLE, url=art_file, @@ -541,13 +537,13 @@ def on_deck_episodes(viewid, tagname, limit): limit: Max. number of items to retrieve, e.g. 50 """ xbmcplugin.setContent(HANDLE, 'episodes') - append_show_title = settings('OnDeckTvAppendShow') == 'true' - append_sxxexx = settings('OnDeckTvAppendSeason') == 'true' - if settings('OnDeckTVextended') == 'false': + append_show_title = utils.settings('OnDeckTvAppendShow') == 'true' + append_sxxexx = utils.settings('OnDeckTvAppendSeason') == 'true' + if utils.settings('OnDeckTVextended') == 'false': # Chances are that this view is used on Kodi startup # Wait till we've connected to a PMS. At most 30s counter = 0 - while window('plex_authenticated') != 'true': + while utils.window('plex_authenticated') != 'true': counter += 1 if counter == 300: LOG.error('Aborting On Deck view, we were not authenticated ' @@ -555,13 +551,12 @@ def on_deck_episodes(viewid, tagname, limit): xbmcplugin.endOfDirectory(HANDLE, False) return sleep(100) - xml = downloadutils.DownloadUtils().downloadUrl( - '{server}/library/sections/%s/onDeck' % viewid) + xml = DU().downloadUrl('{server}/library/sections/%s/onDeck' % viewid) if xml in (None, 401): LOG.error('Could not download PMS xml for view %s', viewid) xbmcplugin.endOfDirectory(HANDLE, False) return - direct_paths = settings('useDirectPaths') == '1' + direct_paths = utils.settings('useDirectPaths') == '1' counter = 0 for item in xml: api = API(item) @@ -580,7 +575,7 @@ def on_deck_episodes(viewid, tagname, limit): break xbmcplugin.endOfDirectory( handle=HANDLE, - cacheToDisc=settings('enableTextureCache') == 'true') + cacheToDisc=utils.settings('enableTextureCache') == 'true') return # if the addon is called with nextup parameter, @@ -610,7 +605,7 @@ def on_deck_episodes(viewid, tagname, limit): "dateadded", "lastplayed" ], } - if settings('ignoreSpecialsNextEpisodes') == "true": + if utils.settings('ignoreSpecialsNextEpisodes') == "true": params['filter'] = { 'and': [ {'operator': "lessthan", 'field': "playcount", 'value': "1"}, @@ -663,37 +658,34 @@ def watchlater(): """ Listing for plex.tv Watch Later section (if signed in to plex.tv) """ - if window('plex_token') == '': + if utils.window('plex_token') == '': LOG.error('No watch later - not signed in to plex.tv') return xbmcplugin.endOfDirectory(HANDLE, False) - if window('plex_restricteduser') == 'true': + if utils.window('plex_restricteduser') == 'true': LOG.error('No watch later - restricted user') return xbmcplugin.endOfDirectory(HANDLE, False) - xml = downloadutils.DownloadUtils().downloadUrl( - 'https://plex.tv/pms/playlists/queue/all', - authenticate=False, - headerOptions={'X-Plex-Token': window('plex_token')}) + xml = DU().downloadUrl('https://plex.tv/pms/playlists/queue/all', + authenticate=False, + headerOptions={'X-Plex-Token': utils.window('plex_token')}) if xml in (None, 401): LOG.error('Could not download watch later list from plex.tv') return xbmcplugin.endOfDirectory(HANDLE, False) - LOG.info('Displaying watch later plex.tv items') xbmcplugin.setContent(HANDLE, 'movies') - direct_paths = settings('useDirectPaths') == '1' + direct_paths = utils.settings('useDirectPaths') == '1' for item in xml: __build_item(item, direct_paths) - xbmcplugin.endOfDirectory( handle=HANDLE, - cacheToDisc=settings('enableTextureCache') == 'true') + cacheToDisc=utils.settings('enableTextureCache') == 'true') def channels(): """ Listing for Plex Channels """ - xml = downloadutils.DownloadUtils().downloadUrl('{server}/channels/all') + xml = DU().downloadUrl('{server}/channels/all') try: xml[0].attrib except (ValueError, AttributeError, IndexError, TypeError): @@ -708,7 +700,7 @@ def channels(): __build_folder(item) xbmcplugin.endOfDirectory( handle=HANDLE, - cacheToDisc=settings('enableTextureCache') == 'true') + cacheToDisc=utils.settings('enableTextureCache') == 'true') def browse_plex(key=None, plex_section_id=None): @@ -717,9 +709,9 @@ def browse_plex(key=None, plex_section_id=None): be used directly for PMS url {server}) or the plex_section_id """ if key: - xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % key) + xml = DU().downloadUrl('{server}%s' % key) else: - xml = GetPlexSectionResults(plex_section_id) + xml = PF.GetPlexSectionResults(plex_section_id) try: xml[0].attrib except (ValueError, AttributeError, IndexError, TypeError): @@ -735,7 +727,7 @@ def browse_plex(key=None, plex_section_id=None): artists = False albums = False musicvideos = False - direct_paths = settings('useDirectPaths') == '1' + direct_paths = utils.settings('useDirectPaths') == '1' for item in xml: if item.tag == 'Directory': __build_folder(item, plex_section_id=plex_section_id) @@ -802,7 +794,7 @@ def browse_plex(key=None, plex_section_id=None): xbmcplugin.endOfDirectory( handle=HANDLE, - cacheToDisc=settings('enableTextureCache') == 'true') + cacheToDisc=utils.settings('enableTextureCache') == 'true') def __build_folder(xml_element, plex_section_id=None): @@ -854,7 +846,7 @@ def extras(plex_id): Lists all extras for plex_id """ xbmcplugin.setContent(HANDLE, 'movies') - xml = GetPlexMetadata(plex_id) + xml = PF.GetPlexMetadata(plex_id) try: xml[0].attrib except (TypeError, IndexError, KeyError): @@ -874,41 +866,47 @@ 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:" - dialog('ok', lang(29999), lang(39215), '192.168.1.2', 'plex.myServer.org') - address = dialog('input', "Enter PMS IP or URL") + utils.dialog('ok', + utils.lang(29999), + utils.lang(39215), + '192.168.1.2', + 'plex.myServer.org') + address = utils.dialog('input', "Enter PMS IP or URL") if address == '': return - port = dialog('input', "Enter PMS port", '32400', type='{numeric}') + 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 = dialog('yesno', lang(29999), lang(39217)) + https = utils.dialog('yesno', 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 = GetMachineIdentifier(url) + machine_identifier = PF.GetMachineIdentifier(url) if machine_identifier is None: # "Error contacting url # Abort (Yes) or save address anyway (No)" - if dialog('yesno', - lang(29999), - '%s %s. %s' % (lang(39218), url, lang(39219))): + if utils.dialog('yesno', + utils.lang(29999), + '%s %s. %s' % (utils.lang(39218), + url, + utils.lang(39219))): return else: - settings('plex_machineIdentifier', '') + utils.settings('plex_machineIdentifier', '') else: - settings('plex_machineIdentifier', machine_identifier) + 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) - settings('https', value=https) - settings('ipaddress', value=address) - settings('port', value=port) + 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 - settings('sslverify', value='false') + utils.settings('sslverify', value='false') # Sign out to trigger new login if _log_out(): @@ -923,9 +921,9 @@ def _log_in(): SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed out! """ - plex_command('RUN_LIB_SCAN', 'full') + utils.plex_command('RUN_LIB_SCAN', 'full') # Restart user client - plex_command('SUSPEND_USER_CLIENT', 'False') + utils.plex_command('SUSPEND_USER_CLIENT', 'False') def _log_out(): @@ -935,22 +933,22 @@ def _log_out(): Returns True if successfully signed out, False otherwise """ # Resetting, please wait - dialog('notification', - lang(29999), - lang(39207), - icon='{plex}', - time=3000, - sound=False) + utils.dialog('notification', + utils.lang(29999), + utils.lang(39207), + icon='{plex}', + time=3000, + sound=False) # Pause library sync thread - plex_command('SUSPEND_LIBRARY_THREAD', 'True') + utils.plex_command('SUSPEND_LIBRARY_THREAD', 'True') # Wait max for 10 seconds for all lib scans to shutdown counter = 0 - while window('plex_dbScan') == 'true': + while utils.window('plex_dbScan') == 'true': if counter > 200: # Failed to reset PMS and plex.tv connects. Try to restart Kodi. - dialog('ok', lang(29999), lang(39208)) + utils.dialog('ok', utils.lang(29999), utils.lang(39208)) # Resuming threads, just in case - plex_command('SUSPEND_LIBRARY_THREAD', 'False') + utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False') LOG.error("Could not stop library sync, aborting") return False counter += 1 @@ -959,17 +957,17 @@ def _log_out(): counter = 0 # Log out currently signed in user: - window('plex_serverStatus', value='401') - plex_command('PMS_STATUS', '401') + utils.window('plex_serverStatus', value='401') + utils.plex_command('PMS_STATUS', '401') # Above method needs to have run its course! Hence wait - while window('plex_serverStatus') == "401": + while utils.window('plex_serverStatus') == "401": if counter > 100: # 'Failed to reset PKC. Try to restart Kodi.' - dialog('ok', lang(29999), lang(39208)) + utils.dialog('ok', 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 - plex_command('SUSPEND_USER_CLIENT', 'True') + utils.plex_command('SUSPEND_USER_CLIENT', 'True') return True diff --git a/resources/lib/initialsetup.py b/resources/lib/initialsetup.py index 259d5c15..04c4cb4a 100644 --- a/resources/lib/initialsetup.py +++ b/resources/lib/initialsetup.py @@ -6,23 +6,22 @@ import xml.etree.ElementTree as etree from xbmc import executebuiltin, translatePath -from utils import settings, window, language as lang, try_decode, dialog, \ - XmlKodiSetting, reboot_kodi -from migration import check_migration -from downloadutils import DownloadUtils as DU -from userclient import UserClient -from clientinfo import getDeviceId -import PlexFunctions as PF -import plex_tv -import json_rpc as js -import playqueue as PQ -from videonodes import VideoNodes -import state -import variables as v +from . import utils +from . import migration +from .downloadutils import DownloadUtils as DU +from . import videonodes +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 variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.initialsetup') ############################################################################### @@ -48,27 +47,28 @@ def reload_pkc(): reload(state) # Reset window props for prop in WINDOW_PROPERTIES: - window(prop, clear=True) + utils.window(prop, clear=True) # Clear video nodes properties - VideoNodes().clearProperties() + videonodes.VideoNodes().clearProperties() # Initializing - state.VERIFY_SSL_CERT = settings('sslverify') == 'true' - state.SSL_CERT_PATH = settings('sslcert') \ - if settings('sslcert') != 'None' else None - state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 - state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) - state.SYNC_DIALOG = settings('dbSyncIndicator') == 'true' - state.ENABLE_MUSIC = settings('enableMusic') == 'true' - state.BACKGROUND_SYNC_DISABLED = settings( + 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( - settings('backgroundsync_saftyMargin')) - state.REPLACE_SMB_PATH = settings('replaceSMB') == 'true' - state.REMAP_PATH = settings('remapSMB') == 'true' - state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) - state.FETCH_PMS_ITEM_NUMBER = settings('fetch_pms_item_number') - state.FORCE_RELOAD_SKIN = settings('forceReloadSkinOnPlaybackStop') == 'true' + 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) @@ -76,9 +76,9 @@ def reload_pkc(): set_replace_paths() set_webserver() # To detect Kodi profile switches - window('plex_kodiProfile', - value=try_decode(translatePath("special://profile"))) - getDeviceId() + 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') @@ -92,7 +92,7 @@ def set_replace_paths(): for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values(): for arg in ('Org', 'New'): key = 'remapSMB%s%s' % (typus, arg) - value = settings(key) + value = utils.settings(key) if '://' in value: protocol = value.split('://', 1)[0] value = value.replace(protocol, protocol.lower()) @@ -129,8 +129,8 @@ def _write_pms_settings(url, token): for entry in xml: if entry.attrib.get('id', '') == 'allowMediaDeletion': value = 'true' if entry.get('value', '1') == '1' else 'false' - settings('plex_allows_mediaDeletion', value=value) - window('plex_allows_mediaDeletion', value=value) + utils.settings('plex_allows_mediaDeletion', value=value) + utils.window('plex_allows_mediaDeletion', value=value) class InitialSetup(object): @@ -140,8 +140,8 @@ class InitialSetup(object): """ def __init__(self): LOG.debug('Entering initialsetup class') - self.server = UserClient().get_server() - self.serverid = settings('plex_machineIdentifier') + 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' @@ -149,7 +149,7 @@ class InitialSetup(object): self.plex_token = plexdict['plexToken'] self.plexid = plexdict['plexid'] # Token for the PMS, not plex.tv - self.pms_token = settings('accessToken') + self.pms_token = utils.settings('accessToken') if self.plex_token: LOG.debug('Found a plex.tv token in the settings') @@ -179,20 +179,20 @@ class InitialSetup(object): # HTTP Error: unauthorized. Token is no longer valid LOG.info('plex.tv connection returned HTTP %s', str(chk)) # Delete token in the settings - settings('plexToken', value='') - settings('plexLogin', value='') + utils.settings('plexToken', value='') + utils.settings('plexLogin', value='') # Could not login, please try again - dialog('ok', lang(29999), lang(39009)) + utils.dialog('ok', utils.lang(29999), utils.lang(39009)) answer = self.plex_tv_sign_in() elif chk is False or chk >= 400: # Problems connecting to plex.tv. Network or internet issue? LOG.info('Problems connecting to plex.tv; connection returned ' 'HTTP %s', str(chk)) - dialog('ok', lang(29999), lang(39010)) + utils.dialog('ok', utils.lang(29999), utils.lang(39010)) answer = False else: LOG.info('plex.tv connection with token successful') - settings('plex_status', value=lang(39227)) + utils.settings('plex_status', value=utils.lang(39227)) # Refresh the info from Plex.tv xml = DU().downloadUrl('https://plex.tv/users/account', authenticate=False, @@ -202,11 +202,12 @@ class InitialSetup(object): except (AttributeError, KeyError): LOG.error('Failed to update Plex info from plex.tv') else: - settings('plexLogin', value=self.plex_login) + utils.settings('plexLogin', value=self.plex_login) home = 'true' if xml.attrib.get('home') == '1' else 'false' - settings('plexhome', value=home) - settings('plexAvatar', value=xml.attrib.get('thumb')) - settings('plexHomeSize', value=xml.attrib.get('homeSize', '1')) + utils.settings('plexhome', value=home) + utils.settings('plexAvatar', value=xml.attrib.get('thumb')) + utils.settings('plexHomeSize', + value=xml.attrib.get('homeSize', '1')) LOG.info('Updated Plex info from plex.tv') return answer @@ -233,7 +234,7 @@ class InitialSetup(object): LOG.warn('Could not retrieve machineIdentifier') answer = False else: - settings('plex_machineIdentifier', value=self.serverid) + utils.settings('plex_machineIdentifier', value=self.serverid) elif answer is True: temp_server_id = PF.GetMachineIdentifier(self.server) if temp_server_id != self.serverid: @@ -325,7 +326,7 @@ class InitialSetup(object): if item.get('machineIdentifier') == self.serverid: server = item if server is None: - name = settings('plex_servername') + 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) @@ -356,18 +357,18 @@ class InitialSetup(object): """ https_updated = False # Searching for PMS - dialog('notification', - heading='{plex}', - message=lang(30001), - icon='{plex}', - time=5000) + utils.dialog('notification', + heading='{plex}', + message=utils.lang(30001), + icon='{plex}', + time=5000) while True: if https_updated is False: serverlist = PF.discover_pms(self.plex_token) # Exit if no servers found if not serverlist: LOG.warn('No plex media servers found!') - dialog('ok', lang(29999), lang(39011)) + utils.dialog('ok', utils.lang(29999), utils.lang(39011)) return # Get a nicer list dialoglist = [] @@ -375,10 +376,10 @@ class InitialSetup(object): if server['local']: # server is in the same network as client. # Add"local" - msg = lang(39022) + msg = utils.lang(39022) else: # Add 'remote' - msg = lang(39054) + msg = utils.lang(39054) if server.get('ownername'): # Display username if its not our PMS dialoglist.append('%s (%s, %s)' @@ -389,7 +390,7 @@ class InitialSetup(object): dialoglist.append('%s (%s)' % (server['name'], msg)) # Let user pick server from a list - resp = dialog('select', lang(39012), dialoglist) + resp = utils.dialog('select', utils.lang(39012), dialoglist) if resp == -1: # User cancelled return @@ -406,17 +407,19 @@ class InitialSetup(object): LOG.warn('Not yet authorized for Plex server %s', server['name']) # Please sign in to plex.tv - dialog('ok', - lang(29999), - lang(39013) + server['name'], - lang(39014)) + utils.dialog('ok', + utils.lang(29999), + utils.lang(39013) + server['name'], + utils.lang(39014)) if self.plex_tv_sign_in() is False: # Exit while loop if user cancels return # Problems connecting elif chk >= 400 or chk is False: # Problems connecting to server. Pick another server? - answ = dialog('yesno', lang(29999), lang(39015)) + answ = utils.dialog('yesno', + utils.lang(29999), + utils.lang(39015)) # Exit while loop if user chooses No if not answ: return @@ -429,30 +432,31 @@ class InitialSetup(object): """ Saves server to file settings """ - settings('plex_machineIdentifier', server['machineIdentifier']) - settings('plex_servername', server['name']) - settings('plex_serverowned', 'true' if server['owned'] else 'false') + utils.settings('plex_machineIdentifier', server['machineIdentifier']) + utils.settings('plex_servername', server['name']) + utils.settings('plex_serverowned', + 'true' if server['owned'] else 'false') # Careful to distinguish local from remote PMS if server['local']: scheme = server['scheme'] - settings('ipaddress', server['ip']) - settings('port', server['port']) + utils.settings('ipaddress', server['ip']) + utils.settings('port', server['port']) LOG.debug("Setting SSL verify to false, because server is " "local") - settings('sslverify', 'false') + utils.settings('sslverify', 'false') else: baseURL = server['baseURL'].split(':') scheme = baseURL[0] - settings('ipaddress', baseURL[1].replace('//', '')) - settings('port', baseURL[2]) + utils.settings('ipaddress', baseURL[1].replace('//', '')) + utils.settings('port', baseURL[2]) LOG.debug("Setting SSL verify to true, because server is not " "local") - settings('sslverify', 'true') + utils.settings('sslverify', 'true') if scheme == 'https': - settings('https', 'true') + utils.settings('https', 'true') else: - settings('https', 'false') + utils.settings('https', 'false') # And finally do some logging LOG.debug("Writing to Kodi user settings file") LOG.debug("PMS machineIdentifier: %s, ip: %s, port: %s, https: %s ", @@ -468,9 +472,9 @@ class InitialSetup(object): """ LOG.info("Initial setup called.") try: - with XmlKodiSetting('advancedsettings.xml', - force_create=True, - top_element='advancedsettings') as xml: + with utils.XmlKodiSetting('advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml: # Get current Kodi video cache setting cache = xml.get_setting(['cache', 'memorysize']) # Disable foreground "Loading media information from files" @@ -493,13 +497,13 @@ class InitialSetup(object): # Kodi default cache if no setting is set cache = str(cache.text) if cache is not None else '20971520' LOG.info('Current Kodi video memory cache in bytes: %s', cache) - settings('kodi_video_cache', value=cache) + utils.settings('kodi_video_cache', value=cache) # Hack to make PKC Kodi master lock compatible try: - with XmlKodiSetting('sources.xml', - force_create=True, - top_element='sources') as xml: + with utils.XmlKodiSetting('sources.xml', + force_create=True, + top_element='sources') as xml: root = xml.set_setting(['video']) count = 2 for source in root.findall('.//path'): @@ -526,21 +530,21 @@ class InitialSetup(object): pass # Do we need to migrate stuff? - check_migration() + migration.check_migration() # Reload the server IP cause we might've deleted it during migration - self.server = UserClient().get_server() + self.server = userclient.UserClient().get_server() # Display a warning if Kodi puts ALL movies into the queue, basically # breaking playback reporting for PKC if js.settings_getsettingvalue('videoplayer.autoplaynextitem'): LOG.warn('Kodi setting videoplayer.autoplaynextitem is enabled!') - if settings('warned_setting_videoplayer.autoplaynextitem') == 'false': + if utils.settings('warned_setting_videoplayer.autoplaynextitem') == 'false': # Only warn once - settings('warned_setting_videoplayer.autoplaynextitem', - value='true') + utils.settings('warned_setting_videoplayer.autoplaynextitem', + value='true') # Warning: Kodi setting "Play next video automatically" is # enabled. This could break PKC. Deactivate? - if dialog('yesno', lang(29999), lang(30003)): + if utils.dialog('yesno', utils.lang(29999), utils.lang(30003)): js.settings_setsettingvalue('videoplayer.autoplaynextitem', False) # Set any video library updates to happen in the background in order to @@ -556,7 +560,7 @@ class InitialSetup(object): self.server, self.serverid) _write_pms_settings(self.server, self.pms_token) if reboot is True: - reboot_kodi() + utils.reboot_kodi() return # If not already retrieved myplex info, optionally let user sign in @@ -570,78 +574,91 @@ class InitialSetup(object): self.write_pms_to_settings(server) # User already answered the installation questions - if settings('InstallQuestionsAnswered') == 'true': + if utils.settings('InstallQuestionsAnswered') == 'true': if reboot is True: - reboot_kodi() + utils.reboot_kodi() return # Additional settings where the user needs to choose # Direct paths (\\NAS\mymovie.mkv) or addon (http)? goto_settings = False - if dialog('yesno', - lang(29999), - lang(39027), - lang(39028), - nolabel="Addon (Default)", - yeslabel="Native (Direct Paths)"): + if utils.dialog('yesno', + utils.lang(29999), + utils.lang(39027), + utils.lang(39028), + nolabel="Addon (Default)", + yeslabel="Native (Direct Paths)"): LOG.debug("User opted to use direct paths.") - settings('useDirectPaths', value="1") + utils.settings('useDirectPaths', value="1") state.DIRECT_PATHS = True # Are you on a system where you would like to replace paths # \\NAS\mymovie.mkv with smb://NAS/mymovie.mkv? (e.g. Windows) - if dialog('yesno', heading=lang(29999), line1=lang(39033)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39033)): LOG.debug("User chose to replace paths with smb") else: - settings('replaceSMB', value="false") + utils.settings('replaceSMB', value="false") # complete replace all original Plex library paths with custom SMB - if dialog('yesno', heading=lang(29999), line1=lang(39043)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39043)): LOG.debug("User chose custom smb paths") - settings('remapSMB', value="true") + utils.settings('remapSMB', value="true") # Please enter your custom smb paths in the settings under # "Sync Options" and then restart Kodi - dialog('ok', heading=lang(29999), line1=lang(39044)) + utils.dialog('ok', + heading=utils.lang(29999), + line1=utils.lang(39044)) goto_settings = True # Go to network credentials? - if dialog('yesno', - heading=lang(29999), - line1=lang(39029), - line2=lang(39030)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39029), + line2=utils.lang(39030)): LOG.debug("Presenting network credentials dialog.") from utils import passwords_xml passwords_xml() # Disable Plex music? - if dialog('yesno', heading=lang(29999), line1=lang(39016)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39016)): LOG.debug("User opted to disable Plex music library.") - settings('enableMusic', value="false") + utils.settings('enableMusic', value="false") # Download additional art from FanArtTV - if dialog('yesno', heading=lang(29999), line1=lang(39061)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39061)): LOG.debug("User opted to use FanArtTV") - settings('FanartTV', value="true") + utils.settings('FanartTV', value="true") # Do you want to replace your custom user ratings with an indicator of # how many versions of a media item you posses? - if dialog('yesno', heading=lang(29999), line1=lang(39718)): + if utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39718)): LOG.debug("User opted to replace user ratings with version number") - settings('indicate_media_versions', value="true") + utils.settings('indicate_media_versions', value="true") # If you use several Plex libraries of one kind, e.g. "Kids Movies" and # "Parents Movies", be sure to check https://goo.gl/JFtQV9 - # dialog.ok(heading=lang(29999), line1=lang(39076)) + # dialog.ok(heading=utils.lang(29999), line1=utils.lang(39076)) # Need to tell about our image source for collections: themoviedb.org - # dialog.ok(heading=lang(29999), line1=lang(39717)) + # dialog.ok(heading=utils.lang(29999), line1=utils.lang(39717)) # Make sure that we only ask these questions upon first installation - settings('InstallQuestionsAnswered', value='true') + utils.settings('InstallQuestionsAnswered', value='true') if goto_settings is False: # Open Settings page now? You will need to restart! - goto_settings = dialog('yesno', - heading=lang(29999), - line1=lang(39017)) + goto_settings = utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39017)) if goto_settings: state.PMS_STATUS = 'Stop' - executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)') + executebuiltin( + 'Addon.Openutils.settings(plugin.video.plexkodiconnect)') elif reboot is True: - reboot_kodi() + utils.reboot_kodi() diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 37372cce..0facc6e9 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -4,18 +4,17 @@ from logging import getLogger from ntpath import dirname from datetime import datetime -from artwork import Artwork -from utils import window, kodi_sql, catch_exceptions -import plexdb_functions as plexdb -import kodidb_functions as kodidb - -from PlexAPI import API -from PlexFunctions import GetPlexMetadata -import variables as v -import state +from . import artwork +from . import utils +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from .plex_api import API +from . import plex_functions as PF +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.itemtypes') # Note: always use same order of URL arguments, NOT urlencode: # plex_id=&plex_type=&mode=play @@ -32,8 +31,8 @@ class Items(object): kodiType: optional argument; e.g. 'video' or 'music' """ def __init__(self): - self.artwork = Artwork() - self.server = window('pms_server') + self.artwork = artwork.Artwork() + self.server = utils.window('pms_server') self.plexconn = None self.plexcursor = None self.kodiconn = None @@ -45,9 +44,9 @@ class Items(object): """ Open DB connections and cursors """ - self.plexconn = kodi_sql('plex') + self.plexconn = utils.kodi_sql('plex') self.plexcursor = self.plexconn.cursor() - self.kodiconn = kodi_sql('video') + self.kodiconn = utils.kodi_sql('video') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.KodiDBMethods(self.kodicursor) @@ -63,7 +62,7 @@ class Items(object): self.kodiconn.close() return self - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def getfanart(self, plex_id, refresh=False): """ Tries to get additional fanart for movies (+sets) and TV shows. @@ -95,7 +94,7 @@ class Items(object): LOG.debug('Already got all fanart for Plex id %s', plex_id) return True - xml = GetPlexMetadata(plex_id) + xml = PF.GetPlexMetadata(plex_id) if xml is None: # Did not receive a valid XML - skip that item for now LOG.error("Could not get metadata for %s. Skipping that item " @@ -183,7 +182,7 @@ class Movies(Items): """ Used for plex library-type movies """ - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): """ Process single movie @@ -513,7 +512,7 @@ class TVShows(Items): """ For Plex library-type TV shows """ - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_update(self, item, viewtag=None, viewid=None): """ Process a single show @@ -722,7 +721,7 @@ class TVShows(Items): tags.extend(collections) self.kodi_db.modify_tags(showid, v.KODI_TYPE_SHOW, tags) - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_updateSeason(self, item, viewtag=None, viewid=None): """ Process a single season of a certain tv show @@ -768,7 +767,7 @@ class TVShows(Items): view_id=viewid, checksum=checksum) - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_updateEpisode(self, item, viewtag=None, viewid=None): """ Process single episode @@ -998,7 +997,7 @@ class TVShows(Items): runtime, playcount, dateplayed, - None) # Do send None, we check here + None) # Do send None, we check here if not state.DIRECT_PATHS: # need to set a SECOND file entry for a path without plex show id filename = api.file_name(force_first_media=True) @@ -1014,9 +1013,9 @@ class TVShows(Items): runtime, playcount, dateplayed, - None) # Do send None - 2nd entry + None) # Do send None - 2nd entry - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def remove(self, plex_id): """ Remove the entire TV shows object (show, season or episode) including @@ -1139,16 +1138,16 @@ class Music(Items): OVERWRITE this method, because we need to open another DB. Open DB connections and cursors """ - self.plexconn = kodi_sql('plex') + self.plexconn = utils.kodi_sql('plex') self.plexcursor = self.plexconn.cursor() # Here it is, not 'video' but 'music' - self.kodiconn = kodi_sql('music') + self.kodiconn = utils.kodi_sql('music') self.kodicursor = self.kodiconn.cursor() self.plex_db = plexdb.Plex_DB_Functions(self.plexcursor) self.kodi_db = kodidb.KodiDBMethods(self.kodicursor) return self - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_updateArtist(self, item, viewtag=None, viewid=None): """ Adds a single artist @@ -1236,7 +1235,7 @@ class Music(Items): v.KODI_TYPE_ARTIST, kodicursor) - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None, scan_children=True): """ @@ -1362,7 +1361,7 @@ class Music(Items): artist_id = plex_db.getItem_byId(parent_id)[0] except TypeError: LOG.info('Artist %s does not yet exist in Plex DB', parent_id) - artist = GetPlexMetadata(parent_id) + artist = PF.GetPlexMetadata(parent_id) try: artist[0].attrib except (TypeError, IndexError, AttributeError): @@ -1393,13 +1392,16 @@ class Music(Items): self.genres, v.KODI_TYPE_ALBUM) # Update artwork - artwork.modify_artwork(artworks, album_id, v.KODI_TYPE_ALBUM, kodicursor) + artwork.modify_artwork(artworks, + album_id, + v.KODI_TYPE_ALBUM, + kodicursor) # Add all children - all tracks if scan_children: for child in children: self.add_updateSong(child, viewtag, viewid) - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def add_updateSong(self, item, viewtag=None, viewid=None): """ Process single song @@ -1459,7 +1461,7 @@ class Music(Items): if disc == 1: track = tracknumber else: - track = disc*2**16 + tracknumber + track = disc * 2 ** 16 + tracknumber year = api.year() _, duration = api.resume_runtime() rating = userdata['UserRating'] @@ -1573,7 +1575,7 @@ class Music(Items): # No album found. Let's create it LOG.info("Album database entry missing.") plex_album_id = api.parent_plex_id() - album = GetPlexMetadata(plex_album_id) + album = PF.GetPlexMetadata(plex_album_id) if album is None or album == 401: LOG.error('Could not download album, abort') return @@ -1664,7 +1666,8 @@ class Music(Items): idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) VALUES (?, ?, ?, ?, ?) ''' - kodicursor.execute(query, (songid, albumid, track, title, duration)) + kodicursor.execute(query, + (songid, albumid, track, title, duration)) # Link song to artists artist_loop = [{ 'Name': api.grandparent_title(), @@ -1680,7 +1683,7 @@ class Music(Items): artistid = artist_edb[0] except TypeError: # Artist is missing from plex database, add it. - artist_xml = GetPlexMetadata(artist_eid) + artist_xml = PF.GetPlexMetadata(artist_eid) if artist_xml is None or artist_xml == 401: LOG.error('Error getting artist, abort') return @@ -1718,9 +1721,12 @@ class Music(Items): artwork.modify_artwork(artworks, songid, v.KODI_TYPE_SONG, kodicursor) if item.get('parentKey') is None: # Update album artwork - artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) + artwork.modify_artwork(artworks, + albumid, + v.KODI_TYPE_ALBUM, + kodicursor) - @catch_exceptions(warnuser=True) + @utils.catch_exceptions(warnuser=True) def remove(self, plex_id): """ Completely remove the item with plex_id from the Kodi and Plex DBs. @@ -1768,7 +1774,8 @@ class Music(Items): ##### IF ARTIST ##### elif kodi_type == v.KODI_TYPE_ARTIST: # Delete songs, album, artist - albums = self.plex_db.getItem_byParentId(kodi_id, v.KODI_TYPE_ALBUM) + albums = self.plex_db.getItem_byParentId(kodi_id, + v.KODI_TYPE_ALBUM) for album in albums: songs = self.plex_db.getItem_byParentId(album[1], v.KODI_TYPE_SONG) diff --git a/resources/lib/json_rpc.py b/resources/lib/json_rpc.py index bc1589d0..b989c97b 100644 --- a/resources/lib/json_rpc.py +++ b/resources/lib/json_rpc.py @@ -3,9 +3,10 @@ Collection of functions using the Kodi JSON RPC interface. See http://kodi.wiki/view/JSON-RPC_API """ from json import loads, dumps -from utils import millis_to_kodi_time from xbmc import executeJSONRPC +from . import utils + class JsonRPC(object): """ @@ -152,7 +153,7 @@ def seek_to(offset): for playerid in get_player_ids(): JsonRPC("Player.Seek").execute( {"playerid": playerid, - "value": millis_to_kodi_time(offset)}) + "value": utils.millis_to_kodi_time(offset)}) def smallforward(): diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py index f4ccd5fa..c021df0a 100644 --- a/resources/lib/kodidb_functions.py +++ b/resources/lib/kodidb_functions.py @@ -7,14 +7,14 @@ from logging import getLogger from ntpath import dirname from sqlite3 import IntegrityError -import artwork -from utils import kodi_sql, try_decode, unix_timestamp, unix_date_to_kodi -import variables as v -import state +from . import artwork +from . import utils +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.kodidb_functions') ############################################################################### @@ -35,7 +35,7 @@ class GetKodiDB(object): self.db_type = db_type def __enter__(self): - self.kodiconn = kodi_sql(self.db_type) + self.kodiconn = utils.kodi_sql(self.db_type) kodi_db = KodiDBMethods(self.kodiconn.cursor()) return kodi_db @@ -118,7 +118,7 @@ class KodiDBMethods(object): if pathid is None: self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path") pathid = self.cursor.fetchone()[0] + 1 - datetime = unix_date_to_kodi(unix_timestamp()) + datetime = utils.unix_date_to_kodi(utils.unix_timestamp()) query = ''' INSERT INTO path(idPath, strPath, dateAdded) VALUES (?, ?, ?) @@ -209,13 +209,14 @@ class KodiDBMethods(object): INSERT INTO files(idFile, idPath, strFilename, dateAdded) VALUES (?, ?, ?, ?) ''' - self.cursor.execute(query, (file_id, path_id, filename, date_added)) + self.cursor.execute(query, + (file_id, path_id, filename, date_added)) return file_id def obsolete_file_ids(self): """ Returns a list of (idFile,) tuples (ints) of all Kodi file ids that do - not have a dateAdded set (dateAdded is NULL) and the filename start with + not have a dateAdded set (dateAdded NULL) and the filename start with 'plugin://plugin.video.plexkodiconnect' These entries should be deleted as they're created falsely by Kodi. """ @@ -1236,7 +1237,7 @@ def kodiid_from_filename(path, kodi_type=None, db_type=None): Returns None, if not possible """ kodi_id = None - path = try_decode(path) + path = utils.try_decode(path) try: filename = path.rsplit('/', 1)[1] path = path.rsplit('/', 1)[0] + '/' diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index 23114773..e20302dd 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -5,29 +5,25 @@ from logging import getLogger from json import loads from threading import Thread import copy - import xbmc from xbmcgui import Window -import plexdb_functions as plexdb -import kodidb_functions as kodidb -from utils import window, settings, plex_command, thread_methods, try_encode, \ - kodi_time_to_millis, unix_date_to_kodi, unix_timestamp -from PlexFunctions import scrobble -from downloadutils import DownloadUtils as DU -from kodidb_functions import kodiid_from_filename -from plexbmchelper.subscribers import LOCKER -from playback import playback_triage -from initialsetup import set_replace_paths -import playqueue as PQ -import json_rpc as js -import playlist_func as PL -import state -import variables as v +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from . import utils +from . import plex_functions as PF +from .downloadutils import DownloadUtils as DU +from . import playback +from . import initialsetup +from . import playqueue as PQ +from . import json_rpc as js +from . import playlist_func as PL +from . import state +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.kodimonitor') # settings: window-variable WINDOW_SETTINGS = { @@ -65,6 +61,7 @@ class KodiMonitor(xbmc.Monitor): def __init__(self): self.xbmcplayer = xbmc.Player() self._already_slept = False + self.hack_replay = None xbmc.Monitor.__init__(self) for playerid in state.PLAYER_STATES: state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE) @@ -91,14 +88,14 @@ class KodiMonitor(xbmc.Monitor): changed = False # Reset the window variables from the settings variables for settings_value, window_value in WINDOW_SETTINGS.iteritems(): - if window(window_value) != settings(settings_value): + if utils.window(window_value) != utils.settings(settings_value): changed = True LOG.debug('PKC window settings changed: %s is now %s', - settings_value, settings(settings_value)) - window(window_value, value=settings(settings_value)) + settings_value, utils.settings(settings_value)) + utils.window(window_value, value=utils.settings(settings_value)) # Reset the state variables in state.py for settings_value, state_name in STATE_SETTINGS.iteritems(): - new = settings(settings_value) + new = utils.settings(settings_value) if new == 'true': new = True elif new == 'false': @@ -110,19 +107,17 @@ class KodiMonitor(xbmc.Monitor): setattr(state, state_name, new) if state_name == 'FETCH_PMS_ITEM_NUMBER': LOG.info('Requesting playlist/nodes refresh') - plex_command('RUN_LIB_SCAN', 'views') + utils.plex_command('RUN_LIB_SCAN', 'views') # Special cases, overwrite all internal settings - set_replace_paths() - state.BACKGROUND_SYNC_DISABLED = settings( + initialsetup.set_replace_paths() + state.BACKGROUND_SYNC_DISABLED = utils.settings( 'enableBackgroundSync') == 'false' - state.FULL_SYNC_INTERVALL = int(settings('fullSyncInterval')) * 60 + state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60 state.BACKGROUNDSYNC_SAFTYMARGIN = int( - settings('backgroundsync_saftyMargin')) - state.SYNC_THREAD_NUMBER = int(settings('syncThreadNumber')) - state.SSL_CERT_PATH = settings('sslcert') \ - if settings('sslcert') != 'None' else None - # Never set through the user - # state.KODI_PLEX_TIME_OFFSET = float(settings('kodiplextimeoffset')) + utils.settings('backgroundsync_saftyMargin')) + state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber')) + state.SSL_CERT_PATH = utils.settings('sslcert') \ + if utils.settings('sslcert') != 'None' else None if changed is True: # Assume that the user changed the settings so that we can now find # the path to all media files @@ -137,13 +132,22 @@ class KodiMonitor(xbmc.Monitor): data = loads(data, 'utf-8') LOG.debug("Method: %s Data: %s", method, data) + # Hack + if not method == 'Player.OnStop': + self.hack_replay = None + if method == "Player.OnPlay": state.SUSPEND_SYNC = True self.PlayBackStart(data) elif method == "Player.OnStop": # Should refresh our video nodes, e.g. on deck # xbmc.executebuiltin('ReloadSkin()') - if data.get('end'): + if (self.hack_replay and not data.get('end') and + self.hack_replay == data['item']): + # Hack for add-on paths + self.hack_replay = None + self._hack_addon_paths_replay_video() + elif data.get('end'): if state.PKC_CAUSED_STOP is True: state.PKC_CAUSED_STOP = False LOG.debug('PKC caused this playback stop - ignoring') @@ -182,28 +186,60 @@ class KodiMonitor(xbmc.Monitor): else: # notify the server if playcount > 0: - scrobble(itemid, 'watched') + PF.scrobble(itemid, 'watched') else: - scrobble(itemid, 'unwatched') + PF.scrobble(itemid, 'unwatched') elif method == "VideoLibrary.OnRemove": pass elif method == "System.OnSleep": # Connection is going to sleep LOG.info("Marking the server as offline. SystemOnSleep activated.") - window('plex_online', value="sleep") + utils.window('plex_online', value="sleep") elif method == "System.OnWake": # Allow network to wake up xbmc.sleep(10000) - window('plex_online', value="false") + utils.window('plex_online', value="false") elif method == "GUI.OnScreensaverDeactivated": - if settings('dbSyncScreensaver') == "true": + if utils.settings('dbSyncScreensaver') == "true": xbmc.sleep(5000) - plex_command('RUN_LIB_SCAN', 'full') + utils.plex_command('RUN_LIB_SCAN', 'full') elif method == "System.OnQuit": LOG.info('Kodi OnQuit detected - shutting down') state.STOP_PKC = True - @LOCKER.lockthis + @staticmethod + @state.LOCKER_SUBSCRIBER.lockthis + def _hack_addon_paths_replay_video(): + """ + Hack we need for RESUMABLE items because Kodi lost the path of the + last played item that is now being replayed (see playback.py's + Player().play()) Also see playqueue.py _compare_playqueues() + + Needed if user re-starts the same video from the library using addon + paths. (Video is only added to playqueue, then immediately stoppen. + There is no playback initialized by Kodi.) Log excerpts: + Method: Playlist.OnAdd Data: + {u'item': {u'type': u'movie', u'id': 4}, + u'playlistid': 1, + u'position': 0} + Now we would hack! + Method: Player.OnStop Data: + {u'item': {u'type': u'movie', u'id': 4}, + u'end': False} + (within the same micro-second!) + """ + LOG.info('Detected re-start of playback of last item') + old = state.OLD_PLAYER_STATES[1] + kwargs = { + 'plex_id': old['plex_id'], + 'plex_type': old['plex_type'], + 'path': old['file'], + 'resolve': False + } + thread = Thread(target=playback.playback_triage, kwargs=kwargs) + thread.start() + + @state.LOCKER_SUBSCRIBER.lockthis def _playlist_onadd(self, data): """ Called if an item is added to a Kodi playlist. Example data dict: @@ -219,23 +255,12 @@ class KodiMonitor(xbmc.Monitor): if 'id' not in data['item']: return old = state.OLD_PLAYER_STATES[data['playlistid']] - if (not state.DIRECT_PATHS and data['position'] == 0 and + if (not state.DIRECT_PATHS and + data['position'] == 0 and data['playlistid'] == 1 and not PQ.PLAYQUEUES[data['playlistid']].items and data['item']['type'] == old['kodi_type'] and data['item']['id'] == old['kodi_id']): - # Hack we need for RESUMABLE items because Kodi lost the path of the - # last played item that is now being replayed (see playback.py's - # Player().play()) Also see playqueue.py _compare_playqueues() - LOG.info('Detected re-start of playback of last item') - kwargs = { - 'plex_id': old['plex_id'], - 'plex_type': old['plex_type'], - 'path': old['file'], - 'resolve': False - } - thread = Thread(target=playback_triage, kwargs=kwargs) - thread.start() - return + self.hack_replay = data['item'] def _playlist_onremove(self, data): """ @@ -247,7 +272,7 @@ class KodiMonitor(xbmc.Monitor): """ pass - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def _playlist_onclear(self, data): """ Called if a Kodi playlist is cleared. Example data dict: @@ -271,7 +296,7 @@ class KodiMonitor(xbmc.Monitor): plex_type = None # If using direct paths and starting playback from a widget if not kodi_id and kodi_type and path: - kodi_id, _ = kodiid_from_filename(path, kodi_type) + kodi_id, _ = kodidb.kodiid_from_filename(path, kodi_type) if kodi_id: with plexdb.Get_Plex_DB() as plex_db: plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type) @@ -323,7 +348,7 @@ class KodiMonitor(xbmc.Monitor): json_item.get('type'), json_item.get('file')) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def PlayBackStart(self, data): """ Called whenever playback is started. Example data: @@ -430,7 +455,7 @@ class KodiMonitor(xbmc.Monitor): LOG.debug('Set the player state: %s', status) -@thread_methods +@utils.thread_methods class SpecialMonitor(Thread): """ Detect the resume dialog for widgets. @@ -439,8 +464,8 @@ class SpecialMonitor(Thread): def run(self): LOG.info("----====# Starting Special Monitor #====----") # "Start from beginning", "Play from beginning" - strings = (try_encode(xbmc.getLocalizedString(12021)), - try_encode(xbmc.getLocalizedString(12023))) + strings = (utils.try_encode(xbmc.getLocalizedString(12021)), + utils.try_encode(xbmc.getLocalizedString(12023))) while not self.stopped(): if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'): if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings: @@ -461,7 +486,7 @@ class SpecialMonitor(Thread): LOG.info("#====---- Special Monitor Stopped ----====#") -@LOCKER.lockthis +@state.LOCKER_SUBSCRIBER.lockthis def _playback_cleanup(ended=False): """ PKC cleanup after playback ends/is stopped. Pass ended=True if Kodi @@ -505,12 +530,12 @@ def _record_playstate(status, ended): # Item not (yet) in Kodi library LOG.debug('No playstate update due to Plex id not found: %s', status) return - totaltime = float(kodi_time_to_millis(status['totaltime'])) / 1000 + totaltime = float(utils.kodi_time_to_millis(status['totaltime'])) / 1000 if ended: progress = 0.99 time = v.IGNORE_SECONDS_AT_START + 1 else: - time = float(kodi_time_to_millis(status['time'])) / 1000 + time = float(utils.kodi_time_to_millis(status['time'])) / 1000 try: progress = time / totaltime except ZeroDivisionError: @@ -518,7 +543,7 @@ def _record_playstate(status, ended): LOG.debug('Playback progress %s (%s of %s seconds)', progress, time, totaltime) playcount = status['playcount'] - last_played = unix_date_to_kodi(unix_timestamp()) + last_played = utils.unix_date_to_kodi(utils.unix_timestamp()) if playcount is None: LOG.debug('playcount not found, looking it up in the Kodi DB') with kodidb.GetKodiDB('video') as kodi_db: diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py index a13aa956..9c9088b8 100644 --- a/resources/lib/library_sync/fanart.py +++ b/resources/lib/library_sync/fanart.py @@ -2,15 +2,14 @@ from logging import getLogger from threading import Thread from Queue import Empty - import xbmc -from utils import thread_methods, settings, language as lang, dialog -import plexdb_functions as plexdb -import itemtypes -from artwork import ArtworkSyncMessage -import variables as v -import state +from .. import utils +from .. import plexdb_functions as plexdb +from .. import itemtypes +from .. import artwork +from .. import variables as v +from .. import state ############################################################################### @@ -19,10 +18,10 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### -@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', - 'DB_SCAN', - 'STOP_SYNC', - 'SUSPEND_SYNC']) +@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', + 'DB_SCAN', + 'STOP_SYNC', + 'SUSPEND_SYNC']) class ThreadedProcessFanart(Thread): """ Threaded download of additional fanart in the background @@ -68,17 +67,17 @@ class ThreadedProcessFanart(Thread): 'Window.IsVisible(DialogAddonSettings.xml)'): # Avoid saving '0' all the time set_zero = True - settings('fanarttv_lookups', value='0') + utils.settings('fanarttv_lookups', value='0') xbmc.sleep(200) continue set_zero = False - if isinstance(item, ArtworkSyncMessage): + if isinstance(item, artwork.ArtworkSyncMessage): if state.IMAGE_SYNC_NOTIFICATIONS: - dialog('notification', - heading=lang(29999), - message=item.message, - icon='{plex}', - sound=False) + utils.dialog('notification', + heading=utils.lang(29999), + message=item.message, + icon='{plex}', + sound=False) queue.task_done() continue @@ -96,6 +95,6 @@ class ThreadedProcessFanart(Thread): if (counter > 20 and not xbmc.getCondVisibility( 'Window.IsVisible(DialogAddonSettings.xml)')): counter = 0 - settings('fanarttv_lookups', value=str(queue.qsize())) + utils.settings('fanarttv_lookups', value=str(queue.qsize())) queue.task_done() LOG.debug("---===### Stopped FanartSync ###===---") diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py index 2850d74a..fb9a8097 100644 --- a/resources/lib/library_sync/get_metadata.py +++ b/resources/lib/library_sync/get_metadata.py @@ -2,12 +2,11 @@ from logging import getLogger from threading import Thread from Queue import Empty - from xbmc import sleep -from utils import thread_methods, window -from PlexFunctions import GetPlexMetadata, GetAllPlexChildren -import sync_info +from .. import utils +from .. import plex_functions as PF +from . import sync_info ############################################################################### @@ -16,9 +15,9 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', - 'STOP_SYNC', - 'SUSPEND_SYNC']) +@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', + 'STOP_SYNC', + 'SUSPEND_SYNC']) class ThreadedGetMetadata(Thread): """ Threaded download of Plex XML metadata for a certain library item. @@ -79,7 +78,7 @@ class ThreadedGetMetadata(Thread): sleep(20) continue # Download Metadata - xml = GetPlexMetadata(item['plex_id']) + xml = PF.GetPlexMetadata(item['plex_id']) if xml is None: # Did not receive a valid XML - skip that item for now LOG.error("Could not get metadata for %s. Skipping that item " @@ -93,14 +92,14 @@ class ThreadedGetMetadata(Thread): elif xml == 401: LOG.error('HTTP 401 returned by PMS. Too much strain? ' 'Cancelling sync for now') - window('plex_scancrashed', value='401') + utils.window('plex_scancrashed', value='401') # Kill remaining items in queue (for main thread to cont.) queue.task_done() break item['xml'] = xml if item.get('get_children') is True: - children_xml = GetAllPlexChildren(item['plex_id']) + children_xml = PF.GetAllPlexChildren(item['plex_id']) try: children_xml[0].attrib except (TypeError, IndexError, AttributeError): diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index 7ce4f5f7..e7930a95 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -2,12 +2,11 @@ from logging import getLogger from threading import Thread from Queue import Empty - from xbmc import sleep -from utils import thread_methods -import itemtypes -import sync_info +from .. import utils +from .. import itemtypes +from . import sync_info ############################################################################### LOG = getLogger("PLEX." + __name__) @@ -15,9 +14,9 @@ LOG = getLogger("PLEX." + __name__) ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', - 'STOP_SYNC', - 'SUSPEND_SYNC']) +@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', + 'STOP_SYNC', + 'SUSPEND_SYNC']) class ThreadedProcessMetadata(Thread): """ Not yet implemented for more than 1 thread - if ever. Only to be called by diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py index a292bd7a..fd22520b 100644 --- a/resources/lib/library_sync/sync_info.py +++ b/resources/lib/library_sync/sync_info.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from logging import getLogger from threading import Thread, Lock - from xbmc import sleep from xbmcgui import DialogProgressBG -from utils import thread_methods, language as lang +from .. import utils ############################################################################### @@ -19,9 +18,9 @@ LOCK = Lock() ############################################################################### -@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', - 'STOP_SYNC', - 'SUSPEND_SYNC']) +@utils.thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', + 'STOP_SYNC', + 'SUSPEND_SYNC']) class ThreadedShowSyncInfo(Thread): """ Threaded class to show the Kodi statusbar of the metadata download. @@ -44,7 +43,10 @@ class ThreadedShowSyncInfo(Thread): total = self.total dialog = DialogProgressBG('dialoglogProgressBG') dialog.create("%s %s: %s %s" - % (lang(39714), self.item_type, str(total), lang(39715))) + % (utils.lang(39714), + self.item_type, + unicode(total), + utils.lang(39715))) total = 2 * total total_progress = 0 @@ -55,15 +57,15 @@ class ThreadedShowSyncInfo(Thread): view_name = PROCESSING_VIEW_NAME total_progress = get_progress + process_progress try: - percentage = int(float(total_progress) / float(total)*100.0) + percentage = int(float(total_progress) / float(total) * 100.0) except ZeroDivisionError: percentage = 0 dialog.update(percentage, message="%s %s. %s %s: %s" % (get_progress, - lang(39712), + utils.lang(39712), process_progress, - lang(39713), + utils.lang(39713), view_name)) # Sleep for x milliseconds sleep(200) diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 898fb093..931999ed 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -5,34 +5,27 @@ from threading import Thread import Queue from random import shuffle import copy - import xbmc from xbmcvfs import exists -import utils -from utils import window, settings, dialog, language as lang, try_decode, \ - try_encode -from downloadutils import DownloadUtils as DU -import itemtypes -import plexdb_functions as plexdb -import kodidb_functions as kodidb -import artwork -import videonodes -import variables as v - -import PlexFunctions as PF -import PlexAPI -from library_sync.get_metadata import ThreadedGetMetadata -from library_sync.process_metadata import ThreadedProcessMetadata -import library_sync.sync_info as sync_info -from library_sync.fanart import ThreadedProcessFanart -import music -import playlists -import state +from . import utils +from .downloadutils import DownloadUtils as DU +from . import itemtypes +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from . import artwork +from . import videonodes +from . import plex_functions as PF +from .plex_api import API +from .library_sync import get_metadata, process_metadata, fanart, sync_info +from . import music +from . import playlists +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.librarysync') ############################################################################### @@ -47,10 +40,10 @@ class LibrarySync(Thread): self.views = [] self.session_keys = {} self.fanartqueue = Queue.Queue() - self.fanartthread = ThreadedProcessFanart(self.fanartqueue) + self.fanartthread = fanart.ThreadedProcessFanart(self.fanartqueue) # How long should we wait at least to process new/changed PMS items? self.vnodes = videonodes.VideoNodes() - self.install_sync_done = settings('SyncInstallRunDone') == 'true' + self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true' # Show sync dialog even if user deactivated? self.force_dialog = True # Need to be set accordingly later @@ -91,16 +84,16 @@ class LibrarySync(Thread): if state.SYNC_DIALOG is not True and self.force_dialog is not True: return if icon == "plex": - dialog('notification', - heading='{plex}', - message=message, - icon='{plex}', - sound=False) + utils.dialog('notification', + heading='{plex}', + message=message, + icon='{plex}', + sound=False) elif icon == "error": - dialog('notification', - heading='{plex}', - message=message, - icon='{error}') + utils.dialog('notification', + heading='{plex}', + message=message, + icon='{error}') @staticmethod def sync_pms_time(): @@ -200,7 +193,8 @@ class LibrarySync(Thread): # Calculate time offset Kodi-PMS state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime) - settings('kodiplextimeoffset', value=str(state.KODI_PLEX_TIME_OFFSET)) + utils.settings('kodiplextimeoffset', + value=str(state.KODI_PLEX_TIME_OFFSET)) LOG.info("Time offset Koditime - Plextime in seconds: %s", str(state.KODI_PLEX_TIME_OFFSET)) return True @@ -288,15 +282,15 @@ class LibrarySync(Thread): if state.ENABLE_MUSIC: xbmc.executebuiltin('UpdateLibrary(music)') - if window('plex_scancrashed') == 'true': + if utils.window('plex_scancrashed') == 'true': # Show warning if itemtypes.py crashed at some point - dialog('ok', heading='{plex}', line1=lang(39408)) - window('plex_scancrashed', clear=True) - elif window('plex_scancrashed') == '401': - window('plex_scancrashed', clear=True) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39408)) + utils.window('plex_scancrashed', clear=True) + elif utils.window('plex_scancrashed') == '401': + utils.window('plex_scancrashed', clear=True) if state.PMS_STATUS not in ('401', 'Auth'): # Plex server had too much and returned ERROR - dialog('ok', heading='{plex}', line1=lang(39409)) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39409)) return True def _process_view(self, folder_item, kodi_db, plex_db, totalnodes): @@ -495,7 +489,7 @@ class LibrarySync(Thread): # totalnodes += 1 # Save total - window('Plex.nodes.total', str(totalnodes)) + utils.window('Plex.nodes.total', str(totalnodes)) # Get rid of old items (view has been deleted on Plex side) if self.old_views: @@ -524,11 +518,11 @@ class LibrarySync(Thread): elif item['kodi_type'] in v.KODI_AUDIOTYPES: delete_music.append(item) - dialog('notification', - heading='{plex}', - message=lang(30052), - icon='{plex}', - sound=False) + utils.dialog('notification', + heading='{plex}', + message=utils.lang(30052), + icon='{plex}', + sound=False) for item in delete_movies: with itemtypes.Movies() as movie_db: movie_db.remove(item['plex_id']) @@ -638,8 +632,8 @@ class LibrarySync(Thread): def process_updatelist(self, item_class): """ - Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes them - by then calling item_classs.() + Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes + them by then calling item_classs.() Input: item_class: 'Movies', 'TVShows', ... @@ -665,13 +659,15 @@ class LibrarySync(Thread): # Spawn GetMetadata threads for downloading threads = [] for _ in range(min(state.SYNC_THREAD_NUMBER, item_number)): - thread = ThreadedGetMetadata(download_queue, process_queue) + thread = get_metadata.ThreadedGetMetadata(download_queue, + process_queue) thread.setDaemon(True) thread.start() threads.append(thread) LOG.debug("%s download threads spawned", len(threads)) # Spawn one more thread to process Metadata, once downloaded - thread = ThreadedProcessMetadata(process_queue, item_class) + thread = process_metadata.ThreadedProcessMetadata(process_queue, + item_class) thread.setDaemon(True) thread.start() threads.append(thread) @@ -702,7 +698,7 @@ class LibrarySync(Thread): except: pass LOG.debug("Sync threads finished") - if (settings('FanartTV') == 'true' and + if (utils.settings('FanartTV') == 'true' and item_class in ('Movies', 'TVShows')): for item in self.updatelist: if item['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): @@ -1099,7 +1095,7 @@ class LibrarySync(Thread): continue else: successful = self.process_newitems(item) - if successful and settings('FanartTV') == 'true': + if successful and utils.settings('FanartTV') == 'true': if item['type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW): self.fanartqueue.put({ 'plex_id': item['ratingKey'], @@ -1294,7 +1290,7 @@ class LibrarySync(Thread): if kodi_info is None: # Item not (yet) in Kodi library continue - if settings('plex_serverowned') == 'false': + if utils.settings('plex_serverowned') == 'false': # Not our PMS, we are not authorized to get the sessions # On the bright side, it must be us playing :-) self.session_keys[session_key] = {} @@ -1312,7 +1308,7 @@ class LibrarySync(Thread): self.session_keys[session_key]['file_id'] = kodi_info[1] self.session_keys[session_key]['kodi_type'] = kodi_info[4] session = self.session_keys[session_key] - if settings('plex_serverowned') != 'false': + if utils.settings('plex_serverowned') != 'false': # Identify the user - same one as signed on with PKC? Skip # update if neither session's username nor userid match # (Owner sometime's returns id '1', not always) @@ -1339,7 +1335,7 @@ class LibrarySync(Thread): LOG.error('Could not get up-to-date xml for item %s', plex_id) continue - api = PlexAPI.API(xml[0]) + api = API(xml[0]) userdata = api.userdata() session['duration'] = userdata['Runtime'] session['viewCount'] = userdata['PlayCount'] @@ -1378,7 +1374,8 @@ class LibrarySync(Thread): resume, session['duration'], session['file_id'], - utils.unix_date_to_kodi(utils.unix_timestamp()), + utils.unix_date_to_kodi( + utils.unix_timestamp()), plex_type) def sync_fanart(self, missing_only=True, refresh=False): @@ -1389,7 +1386,7 @@ class LibrarySync(Thread): missing_only=True False will start look-up for EVERY item refresh=False True will force refresh all external fanart """ - if settings('FanartTV') == 'false': + if utils.settings('FanartTV') == 'false': return with plexdb.Get_Plex_DB() as plex_db: if missing_only: @@ -1407,7 +1404,8 @@ class LibrarySync(Thread): # Shuffle the list to not always start out identically shuffle(items) # Checking FanartTV for %s items - self.fanartqueue.put(artwork.ArtworkSyncMessage(lang(30018) % len(items))) + self.fanartqueue.put(artwork.ArtworkSyncMessage( + utils.lang(30018) % len(items))) for i, item in enumerate(items): self.fanartqueue.put({ 'plex_id': item['plex_id'], @@ -1415,7 +1413,7 @@ class LibrarySync(Thread): 'refresh': refresh }) # FanartTV lookup completed - self.fanartqueue.put(artwork.ArtworkSyncMessage(lang(30019))) + self.fanartqueue.put(artwork.ArtworkSyncMessage(utils.lang(30019))) def triage_lib_scans(self): """ @@ -1424,27 +1422,27 @@ class LibrarySync(Thread): """ if state.RUN_LIB_SCAN in ("full", "repair"): LOG.info('Full library scan requested, starting') - window('plex_dbScan', value="true") + utils.window('plex_dbScan', value="true") state.DB_SCAN = True success = self.maintain_views() if success and state.RUN_LIB_SCAN == "full": success = self.full_sync() elif success: success = self.full_sync(repair=True) - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False if success: # Full library sync finished - self.show_kodi_note(lang(39407)) + self.show_kodi_note(utils.lang(39407)) elif not self.suspend_item_sync(): self.force_dialog = True # ERROR in library sync - self.show_kodi_note(lang(39410), icon='error') + self.show_kodi_note(utils.lang(39410), icon='error') self.force_dialog = False # Reset views was requested from somewhere else elif state.RUN_LIB_SCAN == "views": LOG.info('Refresh playlist and nodes requested, starting') - window('plex_dbScan', value="true") + utils.window('plex_dbScan', value="true") state.DB_SCAN = True # First remove playlists utils.delete_playlists() @@ -1455,28 +1453,28 @@ class LibrarySync(Thread): # Ran successfully LOG.info("Refresh playlists/nodes completed") # "Plex playlists/nodes refreshed" - self.show_kodi_note(lang(39405)) + self.show_kodi_note(utils.lang(39405)) else: # Failed LOG.error("Refresh playlists/nodes failed") # "Plex playlists/nodes refresh failed" - self.show_kodi_note(lang(39406), icon="error") - window('plex_dbScan', clear=True) + self.show_kodi_note(utils.lang(39406), icon="error") + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False elif state.RUN_LIB_SCAN == 'fanart': # Only look for missing fanart (No) # or refresh all fanart (Yes) - refresh = dialog('yesno', - heading='{plex}', - line1=lang(39223), - nolabel=lang(39224), - yeslabel=lang(39225)) + refresh = utils.dialog('yesno', + heading='{plex}', + line1=utils.lang(39223), + nolabel=utils.lang(39224), + yeslabel=utils.lang(39225)) self.sync_fanart(missing_only=not refresh, refresh=refresh) elif state.RUN_LIB_SCAN == 'textures': state.DB_SCAN = True - window('plex_dbScan', value="true") + utils.window('plex_dbScan', value="true") artwork.Artwork().fullTextureCacheSync() - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False else: raise NotImplementedError('Library scan not defined: %s' @@ -1489,12 +1487,12 @@ class LibrarySync(Thread): self._run_internal() except Exception as e: state.DB_SCAN = False - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) LOG.error('LibrarySync thread crashed. Error message: %s', e) import traceback LOG.error("Traceback:\n%s", traceback.format_exc()) # Library sync thread has crashed - dialog('ok', heading='{plex}', line1=lang(39400)) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39400)) raise def _run_internal(self): @@ -1504,21 +1502,21 @@ class LibrarySync(Thread): last_sync = 0 last_processing = 0 last_time_sync = 0 - one_day_in_seconds = 60*60*24 + one_day_in_seconds = 60 * 60 * 24 # Link to Websocket queue queue = state.WEBSOCKET_QUEUE - if (not exists(try_encode(v.DB_VIDEO_PATH)) or - not exists(try_encode(v.DB_TEXTURE_PATH)) or + if (not exists(utils.try_encode(v.DB_VIDEO_PATH)) or + not exists(utils.try_encode(v.DB_TEXTURE_PATH)) or (state.ENABLE_MUSIC and - not exists(try_encode(v.DB_MUSIC_PATH)))): + not exists(utils.try_encode(v.DB_MUSIC_PATH)))): # Database does not exists LOG.error("The current Kodi version is incompatible " "to know which Kodi versions are supported.") - LOG.error('Current Kodi version: %s', try_decode( + LOG.error('Current Kodi version: %s', utils.try_decode( xbmc.getInfoLabel('System.BuildVersion'))) # "Current Kodi version is unsupported, cancel lib sync" - dialog('ok', heading='{plex}', line1=lang(39403)) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39403)) return # Do some initializing @@ -1526,14 +1524,14 @@ class LibrarySync(Thread): self.initialize_plex_db() # Run start up sync state.DB_SCAN = True - window('plex_dbScan', value="true") - LOG.info("Db version: %s", settings('dbCreatedWithVersion')) + utils.window('plex_dbScan', value="true") + LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion')) LOG.info('Refreshing video nodes and playlists now') # Setup the paths for addon-paths (even when using direct paths) with kodidb.GetKodiDB('video') as kodi_db: kodi_db.setup_path_table() - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False playlist_monitor = None @@ -1549,7 +1547,7 @@ class LibrarySync(Thread): if not self.install_sync_done: # Very first sync upon installation or reset of Kodi DB state.DB_SCAN = True - window('plex_dbScan', value='true') + utils.window('plex_dbScan', value='true') # Initialize time offset Kodi - PMS self.sync_pms_time() last_time_sync = utils.unix_timestamp() @@ -1562,9 +1560,9 @@ class LibrarySync(Thread): LOG.error('Initial maintain_views not successful') elif self.full_sync(): LOG.info('Initial start-up full sync successful') - settings('SyncInstallRunDone', value='true') + utils.settings('SyncInstallRunDone', value='true') self.install_sync_done = True - settings('dbCreatedWithVersion', v.ADDON_VERSION) + utils.settings('dbCreatedWithVersion', v.ADDON_VERSION) self.force_dialog = False initial_sync_done = True kodi_db_version_checked = True @@ -1576,27 +1574,29 @@ class LibrarySync(Thread): else: LOG.error('Initial start-up full sync unsuccessful') xbmc.executebuiltin('InhibitIdleShutdown(false)') - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False elif not kodi_db_version_checked: # Install sync was already done, don't force-show dialogs self.force_dialog = False # Verify the validity of the database - current_version = settings('dbCreatedWithVersion') - if not utils.compare_version(current_version, v.MIN_DB_VERSION): + current_version = utils.settings('dbCreatedWithVersion') + if not utils.compare_version(current_version, + v.MIN_DB_VERSION): LOG.warn("Db version out of date: %s minimum version " "required: %s", current_version, v.MIN_DB_VERSION) # DB out of date. Proceed to recreate? - resp = dialog('yesno', - heading=lang(29999), - line1=lang(39401)) + resp = utils.dialog('yesno', + heading=utils.lang(29999), + line1=utils.lang(39401)) if not resp: LOG.warn("Db version out of date! USER IGNORED!") # PKC may not work correctly until reset - dialog('ok', - heading='{plex}', - line1=lang(29999) + lang(39402)) + utils.dialog('ok', + heading='{plex}', + line1='%s%s' % (utils.lang(29999), + utils.lang(39402))) else: utils.reset(ask_user=False) break @@ -1606,7 +1606,7 @@ class LibrarySync(Thread): # First sync upon PKC restart. Skipped if very first sync upon # PKC installation has been completed state.DB_SCAN = True - window('plex_dbScan', value="true") + utils.window('plex_dbScan', value="true") LOG.info('Doing initial sync on Kodi startup') if state.SUSPEND_SYNC: LOG.warning('Forcing startup sync even if Kodi is playing') @@ -1627,7 +1627,7 @@ class LibrarySync(Thread): playlist_monitor = playlists.kodi_playlist_monitor() else: LOG.info('Startup sync has not yet been successful') - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False # Currently no db scan, so we can start a new scan @@ -1646,23 +1646,23 @@ class LibrarySync(Thread): not self.suspend_item_sync()): LOG.info('Doing scheduled full library scan') state.DB_SCAN = True - window('plex_dbScan', value="true") + utils.window('plex_dbScan', value="true") success = self.maintain_views() if success: success = self.full_sync() if not success and not self.suspend_item_sync(): LOG.error('Could not finish scheduled full sync') self.force_dialog = True - self.show_kodi_note(lang(39410), + self.show_kodi_note(utils.lang(39410), icon='error') self.force_dialog = False elif success: last_sync = now # Full library sync finished successfully - self.show_kodi_note(lang(39407)) + self.show_kodi_note(utils.lang(39407)) else: LOG.info('Full sync interrupted') - window('plex_dbScan', clear=True) + utils.window('plex_dbScan', clear=True) state.DB_SCAN = False elif now - last_time_sync > one_day_in_seconds: LOG.info('Starting daily time sync') diff --git a/resources/lib/migration.py b/resources/lib/migration.py index 41aceb02..00c15228 100644 --- a/resources/lib/migration.py +++ b/resources/lib/migration.py @@ -1,29 +1,31 @@ from logging import getLogger -import variables as v -from utils import compare_version, settings + +from . import variables as v +from . import utils ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger('PLEX.migration') def check_migration(): - log.info('Checking whether we need to migrate something') - last_migration = settings('last_migrated_PKC_version') + LOG.info('Checking whether we need to migrate something') + last_migration = utils.settings('last_migrated_PKC_version') if last_migration == v.ADDON_VERSION: - log.info('Already migrated to PKC version %s' % v.ADDON_VERSION) + LOG.info('Already migrated to PKC version %s' % v.ADDON_VERSION) # Ensure later migration if user downgraded PKC! - settings('last_migrated_PKC_version', value=v.ADDON_VERSION) + utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION) return - if not compare_version(last_migration, '1.8.2'): - log.info('Migrating to version 1.8.1') + if not utils.compare_version(last_migration, '1.8.2'): + LOG.info('Migrating to version 1.8.1') # Set the new PKC theMovieDB key - settings('themoviedbAPIKey', value='19c90103adb9e98f2172c6a6a3d85dc4') + utils.settings('themoviedbAPIKey', + value='19c90103adb9e98f2172c6a6a3d85dc4') - if not compare_version(last_migration, '2.0.25'): - log.info('Migrating to version 2.0.24') + if not utils.compare_version(last_migration, '2.0.25'): + LOG.info('Migrating to version 2.0.24') # Need to re-connect with PMS to pick up on plex.direct URIs - settings('ipaddress', value='') - settings('port', value='') + utils.settings('ipaddress', value='') + utils.settings('port', value='') - settings('last_migrated_PKC_version', value=v.ADDON_VERSION) + utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION) diff --git a/resources/lib/music.py b/resources/lib/music.py index 30e7ab61..518f4747 100644 --- a/resources/lib/music.py +++ b/resources/lib/music.py @@ -3,12 +3,12 @@ from logging import getLogger from re import compile as re_compile from xml.etree.ElementTree import ParseError -from utils import XmlKodiSetting, reboot_kodi, language as lang -from PlexAPI import API -import variables as v +from . import utils +from .plex_api import API +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.music') REGEX_MUSICPATH = re_compile(r'''^\^(.+)\$$''') ############################################################################### @@ -37,9 +37,10 @@ def excludefromscan_music_folders(xml): omit_check=True) paths.append(__turn_to_regex(path)) try: - with XmlKodiSetting('advancedsettings.xml', - force_create=True, - top_element='advancedsettings') as xml_file: + with utils.XmlKodiSetting( + 'advancedsettings.xml', + force_create=True, + top_element='advancedsettings') as xml_file: parent = xml_file.set_setting(['audio', 'excludefromscan']) for path in paths: for element in parent: @@ -71,7 +72,7 @@ def excludefromscan_music_folders(xml): if reboot is True: # 'New Plex music library detected. Sorry, but we need to # restart Kodi now due to the changes made.' - reboot_kodi(lang(39711)) + utils.reboot_kodi(utils.lang(39711)) def __turn_to_regex(path): diff --git a/resources/lib/pickler.py b/resources/lib/pickler.py index 3e277104..d7450b27 100644 --- a/resources/lib/pickler.py +++ b/resources/lib/pickler.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- ############################################################################### from cPickle import dumps, loads - from xbmcgui import Window from xbmc import log, LOGDEBUG ############################################################################### WINDOW = Window(10000) -PREFIX = 'PLEX.%s: ' % __name__ +PREFIX = 'PLEX.pickler: ' ############################################################################### diff --git a/resources/lib/playback.py b/resources/lib/playback.py index 87d0e1ec..7be1f5d4 100644 --- a/resources/lib/playback.py +++ b/resources/lib/playback.py @@ -4,27 +4,25 @@ Used to kick off Kodi playback from logging import getLogger from threading import Thread from os.path import join - from xbmc import Player, sleep -from PlexAPI import API -from PlexFunctions import GetPlexMetadata, init_plex_playqueue -from downloadutils import DownloadUtils as DU -import plexdb_functions as plexdb -import kodidb_functions as kodidb -import playlist_func as PL -import playqueue as PQ -from playutils import PlayUtils -from PKC_listitem import PKC_ListItem -from pickler import pickle_me, Playback_Successful -import json_rpc as js -from utils import settings, dialog, language as lang, try_encode -from plexbmchelper.subscribers import LOCKER -import variables as v -import state +from .plex_api import API +from . import plex_functions as PF +from . import utils +from .downloadutils import DownloadUtils as DU +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from . import playlist_func as PL +from . import playqueue as PQ +from . import json_rpc as js +from . import pickler +from .playutils import PlayUtils +from .pkc_listitem import PKCListItem +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.playback') # Do we need to return ultimately with a setResolvedUrl? RESOLVE = True # We're "failing" playback with a video of 0 length @@ -32,13 +30,13 @@ NULL_VIDEO = join(v.ADDON_FOLDER, 'addons', v.ADDON_ID, 'empty_video.mp4') ############################################################################### -@LOCKER.lockthis +@state.LOCKER_SUBSCRIBER.lockthis def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): """ Hit this function for addon path playback, Plex trailers, etc. Will setup playback first, then on second call complete playback. - Will set Playback_Successful() with potentially a PKC_ListItem() attached + Will set Playback_Successful() with potentially a PKCListItem() attached (to be consumed by setResolvedURL in default.py) If trailers or additional (movie-)parts are added, default.py is released @@ -50,14 +48,14 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): service.py Python instance """ LOG.info('playback_triage called with plex_id %s, plex_type %s, path %s, ' - 'resolve %s,', plex_id, plex_type, path, resolve) + 'resolve %s', plex_id, plex_type, path, resolve) global RESOLVE # If started via Kodi context menu, we never resolve RESOLVE = resolve if not state.CONTEXT_MENU_PLAY else False if not state.AUTHENTICATED: LOG.error('Not yet authenticated for PMS, abort starting playback') # "Unauthorized for PMS" - dialog('notification', lang(29999), lang(30017)) + utils.dialog('notification', utils.lang(29999), utils.lang(30017)) _ensure_resolve(abort=True) return playqueue = PQ.get_playqueue_from_type( @@ -74,7 +72,10 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): except KeyError: LOG.error('Still no position - abort') # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') _ensure_resolve(abort=True) return # HACK to detect playback of playlists for add-on paths @@ -97,9 +98,15 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True): try: item = playqueue.items[pos] except IndexError: + LOG.debug('PKC playqueue yet empty, need to initialize playback') initiate = True else: - initiate = True if item.plex_id != plex_id else False + if item.plex_id != plex_id: + LOG.debug('Received new plex_id %s, expected %s. Init playback', + plex_id, item.plex_id) + initiate = True + else: + initiate = False if initiate: _playback_init(plex_id, plex_type, playqueue, pos) else: @@ -124,13 +131,16 @@ def _playlist_playback(plex_id, plex_type): for the next item in line :-) (by the way: trying to get active Kodi player id will return []) """ - xml = GetPlexMetadata(plex_id) + xml = PF.GetPlexMetadata(plex_id) try: xml[0].attrib except (IndexError, TypeError, AttributeError): LOG.error('Could not get a PMS xml for plex id %s', plex_id) # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') _ensure_resolve(abort=True) return # Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback @@ -150,13 +160,16 @@ def _playback_init(plex_id, plex_type, playqueue, pos): Playback setup if Kodi starts playing an item for the first time. """ LOG.info('Initializing PKC playback') - xml = GetPlexMetadata(plex_id) + xml = PF.GetPlexMetadata(plex_id) try: xml[0].attrib except (IndexError, TypeError, AttributeError): LOG.error('Could not get a PMS xml for plex id %s', plex_id) # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') _ensure_resolve(abort=True) return if playqueue.kodi_pl.size() > 1: @@ -179,10 +192,12 @@ def _playback_init(plex_id, plex_type, playqueue, pos): api = API(xml[0]) trailers = False if (plex_type == v.PLEX_TYPE_MOVIE and not api.resume_point() and - settings('enableCinema') == "true"): - if settings('askCinema') == "true": + utils.settings('enableCinema') == "true"): + if utils.settings('askCinema') == "true": # "Play trailers?" - trailers = dialog('yesno', lang(29999), lang(33016)) + trailers = utils.dialog('yesno', + utils.lang(29999), + utils.lang(33016)) trailers = True if trailers else False else: trailers = True @@ -198,15 +213,18 @@ def _playback_init(plex_id, plex_type, playqueue, pos): playqueue.clear() if plex_type != v.PLEX_TYPE_CLIP: # Post to the PMS to create a playqueue - in any case due to Companion - xml = init_plex_playqueue(plex_id, - xml.attrib.get('librarySectionUUID'), - mediatype=plex_type, - trailers=trailers) + xml = PF.init_plex_playqueue(plex_id, + xml.attrib.get('librarySectionUUID'), + mediatype=plex_type, + trailers=trailers) if xml is None: LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', plex_id, xml.attrib.get('librarySectionUUID')) # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') # Do NOT use _ensure_resolve() because we resolved above already state.CONTEXT_MENU_PLAY = False state.FORCE_TRANSCODE = False @@ -253,9 +271,13 @@ def _ensure_resolve(abort=False): # Because playback won't start with context menu play state.PKC_CAUSED_STOP = True state.PKC_CAUSED_STOP_DONE = False - result = Playback_Successful() - result.listitem = PKC_ListItem(path=NULL_VIDEO) - pickle_me(result) + if not abort: + result = pickler.Playback_Successful() + result.listitem = PKCListItem(path=NULL_VIDEO) + pickler.pickle_me(result) + else: + # Shows PKC error message + pickler.pickle_me(None) if abort: # Reset some playback variables state.CONTEXT_MENU_PLAY = False @@ -308,7 +330,7 @@ def _prep_playlist_stack(xml): # Need to redirect again to PKC to conclude playback path = api.path() listitem = api.create_listitem() - listitem.setPath(try_encode(path)) + listitem.setPath(utils.try_encode(path)) else: # Will add directly via the Kodi DB path = None @@ -373,8 +395,8 @@ def _conclude_playback(playqueue, pos): return PKC listitem attached to result """ LOG.info('Concluding playback for playqueue position %s', pos) - result = Playback_Successful() - listitem = PKC_ListItem() + result = pickler.Playback_Successful() + listitem = PKCListItem() item = playqueue.items[pos] if item.xml is not None: # Got a Plex element @@ -385,7 +407,7 @@ def _conclude_playback(playqueue, pos): playurl = playutils.getPlayUrl() else: playurl = item.file - listitem.setPath(try_encode(playurl)) + listitem.setPath(utils.try_encode(playurl)) if item.playmethod == 'DirectStream': listitem.setSubtitles(api.cache_external_subs()) elif item.playmethod == 'Transcode': @@ -405,7 +427,7 @@ def _conclude_playback(playqueue, pos): listitem.setProperty('resumetime', str(item.offset)) # Reset the resumable flag result.listitem = listitem - pickle_me(result) + pickler.pickle_me(result) LOG.info('Done concluding playback') @@ -424,7 +446,7 @@ def process_indirect(key, offset, resolve=True): key, offset, resolve) global RESOLVE RESOLVE = resolve - result = Playback_Successful() + result = pickler.Playback_Successful() if key.startswith('http') or key.startswith('{server}'): xml = DU().downloadUrl(key) elif key.startswith('/system/services'): @@ -441,7 +463,7 @@ def process_indirect(key, offset, resolve=True): offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) # Todo: implement offset api = API(xml[0]) - listitem = PKC_ListItem() + listitem = PKCListItem() api.create_listitem(listitem) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) @@ -462,14 +484,14 @@ def process_indirect(key, offset, resolve=True): return playurl = xml[0].attrib['key'] item.file = playurl - listitem.setPath(try_encode(playurl)) + listitem.setPath(utils.try_encode(playurl)) playqueue.items.append(item) if resolve is True: result.listitem = listitem - pickle_me(result) + pickler.pickle_me(result) else: thread = Thread(target=Player().play, - args={'item': try_encode(playurl), + args={'item': utils.try_encode(playurl), 'listitem': listitem}) thread.setDaemon(True) LOG.info('Done initializing PKC playback, starting Kodi player') diff --git a/resources/lib/playback_starter.py b/resources/lib/playback_starter.py index 86433bcd..ff20b519 100644 --- a/resources/lib/playback_starter.py +++ b/resources/lib/playback_starter.py @@ -4,16 +4,16 @@ from logging import getLogger from threading import Thread from urlparse import parse_qsl -import playback -from context_entry import ContextMenu -import state -import json_rpc as js -from pickler import pickle_me, Playback_Successful -import kodidb_functions as kodidb +from . import playback +from . import context_entry +from . import json_rpc as js +from . import pickler +from . import kodidb_functions as kodidb +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.playback_starter') ############################################################################### @@ -29,7 +29,7 @@ class PlaybackStarter(Thread): except ValueError: # E.g. other add-ons scanning for Extras folder LOG.debug('Detected 3rd party add-on call - ignoring') - pickle_me(Playback_Successful()) + pickler.pickle_me(pickler.Playback_Successful()) return params = dict(parse_qsl(params)) mode = params.get('mode') @@ -54,10 +54,10 @@ class PlaybackStarter(Thread): else: LOG.error('Could not find tv show id for %s', item) if resolve: - pickle_me(Playback_Successful()) + pickler.pickle_me(pickler.Playback_Successful()) elif mode == 'context_menu': - ContextMenu(kodi_id=params.get('kodi_id'), - kodi_type=params.get('kodi_type')) + context_entry.ContextMenu(kodi_id=params.get('kodi_id'), + kodi_type=params.get('kodi_type')) def run(self): queue = state.COMMAND_PIPELINE_QUEUE diff --git a/resources/lib/playlist_func.py b/resources/lib/playlist_func.py index 0c587551..83ee8653 100644 --- a/resources/lib/playlist_func.py +++ b/resources/lib/playlist_func.py @@ -8,18 +8,18 @@ import urllib from urlparse import parse_qsl, urlsplit from re import compile as re_compile -import plexdb_functions as plexdb -from downloadutils import DownloadUtils as DU -from utils import try_decode, try_encode -from PlexAPI import API -from PlexFunctions import GetPlexMetadata -from kodidb_functions import kodiid_from_filename -import json_rpc as js -import variables as v +from .plex_api import API +from . import plex_functions as PF +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from .downloadutils import DownloadUtils as DU +from . import utils +from . import json_rpc as js +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.playlist_func') REGEX = re_compile(r'''metadata%2F(\d+)''') ############################################################################### @@ -51,13 +51,14 @@ class PlaylistObjectBaseclase(object): continue if isinstance(getattr(self, key), str): answ += '\'%s\': \'%s\', ' % (key, - try_decode(getattr(self, key))) + utils.try_decode(getattr(self, + key))) elif isinstance(getattr(self, key), unicode): answ += '\'%s\': \'%s\', ' % (key, getattr(self, key)) else: # e.g. int answ += '\'%s\': %s, ' % (key, unicode(getattr(self, key))) - return try_encode(answ + '}}') + return utils.try_encode(answ + '}}') class Playlist_Object(PlaylistObjectBaseclase): @@ -228,7 +229,8 @@ class Playlist_Item(object): continue if isinstance(getattr(self, key), str): answ += '\'%s\': \'%s\', ' % (key, - try_decode(getattr(self, key))) + utils.try_decode(getattr(self, + key))) elif isinstance(getattr(self, key), unicode): answ += '\'%s\': \'%s\', ' % (key, getattr(self, key)) else: @@ -238,7 +240,7 @@ class Playlist_Item(object): answ += '\'xml\': None}}' else: answ += '\'xml\': \'%s\'}}' % self.xml.tag - return try_encode(answ) + return utils.try_encode(answ) def plex_stream_index(self, kodi_stream_index, stream_type): """ @@ -317,7 +319,7 @@ def playlist_item_from_kodi(kodi_item): item.plex_type = query.get('itemType') if item.plex_id is None and item.file is not None: item.uri = ('library://whatever/item/%s' - % urllib.quote(try_encode(item.file), safe='')) + % urllib.quote(utils.try_encode(item.file), safe='')) else: # TO BE VERIFIED - PLEX DOESN'T LIKE PLAYLIST ADDS IN THIS MANNER item.uri = ('library://%s/item/library%%2Fmetadata%%2F%s' % @@ -343,17 +345,20 @@ def verify_kodi_item(plex_id, kodi_item): # Need more info since we don't have kodi_id nor type. Use file path. if (kodi_item['file'].startswith('plugin') or kodi_item['file'].startswith('http')): - raise PlaylistError('kodi_item cannot be used for Plex playback') + LOG.error('kodi_item %s cannot be used for Plex playback', kodi_item) + raise PlaylistError LOG.debug('Starting research for Kodi id since we didnt get one: %s', kodi_item) - kodi_id, _ = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_MOVIE) + kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'], + v.KODI_TYPE_MOVIE) kodi_item['type'] = v.KODI_TYPE_MOVIE if kodi_id is None: - kodi_id, _ = kodiid_from_filename(kodi_item['file'], - v.KODI_TYPE_EPISODE) + kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'], + v.KODI_TYPE_EPISODE) kodi_item['type'] = v.KODI_TYPE_EPISODE if kodi_id is None: - kodi_id, _ = kodiid_from_filename(kodi_item['file'], v.KODI_TYPE_SONG) + kodi_id, _ = kodidb.kodiid_from_filename(kodi_item['file'], + v.KODI_TYPE_SONG) kodi_item['type'] = v.KODI_TYPE_SONG kodi_item['id'] = kodi_id kodi_item['type'] = None if kodi_id is None else kodi_item['type'] @@ -519,8 +524,9 @@ def init_plex_playqueue(playlist, plex_id=None, kodi_item=None): # Need to get the details for the playlist item item = playlist_item_from_xml(xml[0]) except (KeyError, IndexError, TypeError): - raise PlaylistError('Could not init Plex playlist with plex_id %s and ' - 'kodi_item %s' % (plex_id, kodi_item)) + LOG.error('Could not init Plex playlist: plex_id %s, kodi_item %s', + plex_id, kodi_item) + raise PlaylistError playlist.items.append(item) LOG.debug('Initialized the playqueue on the Plex side: %s', playlist) return item @@ -683,7 +689,7 @@ def add_item_to_kodi_playlist(playlist, pos, kodi_id=None, kodi_type=None, item = playlist_item_from_kodi( {'id': kodi_id, 'type': kodi_type, 'file': file}) if item.plex_id is not None: - xml = GetPlexMetadata(item.plex_id) + xml = PF.GetPlexMetadata(item.plex_id) item.xml = xml[-1] playlist.items.insert(pos, item) return item @@ -863,7 +869,7 @@ def get_plextype_from_xml(xml): except IndexError: LOG.error('Could not get plex_id from xml: %s', xml.attrib) return - new_xml = GetPlexMetadata(plex_id) + new_xml = PF.GetPlexMetadata(plex_id) try: new_xml[0].attrib except (TypeError, IndexError, AttributeError): diff --git a/resources/lib/playlists.py b/resources/lib/playlists.py index ea7880c4..f0c99ded 100644 --- a/resources/lib/playlists.py +++ b/resources/lib/playlists.py @@ -2,28 +2,21 @@ from logging import getLogger import os import sys -from threading import Lock - from xbmcvfs import exists -from watchdog.events import FileSystemEventHandler -from watchdog.observers import Observer -import playlist_func as PL -from PlexAPI import API -import kodidb_functions as kodidb -import plexdb_functions as plexdb -import utils -import variables as v -import state +from .watchdog.events import FileSystemEventHandler +from .watchdog.observers import Observer +from . import playlist_func as PL +from .plex_api import API +from . import kodidb_functions as kodidb +from . import plexdb_functions as plexdb +from . import utils +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) - -# Necessary to temporarily hold back librarysync/websocket listener when doing -# a full sync -LOCK = Lock() -LOCKER = utils.LockFunction(LOCK) +LOG = getLogger('PLEX.playlists') # Which playlist formates are supported by PKC? SUPPORTED_FILETYPES = ( @@ -274,7 +267,7 @@ def _kodi_playlist_identical(xml_element): pass -@LOCKER.lockthis +@state.LOCKER_PLAYLISTS.lockthis def process_websocket(plex_id, updated_at, state): """ Hit by librarysync to process websocket messages concerning playlists @@ -302,7 +295,7 @@ def process_websocket(plex_id, updated_at, state): pass -@LOCKER.lockthis +@state.LOCKER_PLAYLISTS.lockthis def full_sync(): """ Full sync of playlists between Kodi and Plex. Returns True is successful, @@ -438,7 +431,7 @@ class PlaylistEventhandler(FileSystemEventHandler): EVENT_TYPE_DELETED: self.on_deleted, } event_type = event.event_type - with LOCK: + with state.LOCK_PLAYLISTS: _method_map[event_type](event) def on_created(self, event): diff --git a/resources/lib/playqueue.py b/resources/lib/playqueue.py index 2ef97858..232caf0a 100644 --- a/resources/lib/playqueue.py +++ b/resources/lib/playqueue.py @@ -4,21 +4,18 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly from logging import getLogger from threading import Thread from re import compile as re_compile +import xbmc -from xbmc import Player, PlayList, PLAYLIST_MUSIC, PLAYLIST_VIDEO, sleep - -from utils import thread_methods -import playlist_func as PL -from PlexFunctions import GetAllPlexChildren -from PlexAPI import API -from plexbmchelper.subscribers import LOCK -from playback import play_xml -import json_rpc as js -import variables as v -import state +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 ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.playqueue') PLUGIN = 'plugin://%s' % v.ADDON_ID REGEX = re_compile(r'''plex_id=(\d+)''') @@ -37,7 +34,7 @@ def init_playqueues(): LOG.debug('Playqueues have already been initialized') return # Initialize Kodi playqueues - with LOCK: + with state.LOCK_SUBSCRIBER: for i in (0, 1, 2): # Just in case the Kodi response is not sorted correctly for queue in js.get_playlists(): @@ -48,12 +45,12 @@ def init_playqueues(): playqueue.type = queue['type'] # Initialize each Kodi playlist if playqueue.type == v.KODI_TYPE_AUDIO: - playqueue.kodi_pl = PlayList(PLAYLIST_MUSIC) + playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) elif playqueue.type == v.KODI_TYPE_VIDEO: - playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) + playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) else: # Currently, only video or audio playqueues available - playqueue.kodi_pl = PlayList(PLAYLIST_VIDEO) + playqueue.kodi_pl = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) # Overwrite 'picture' with 'photo' playqueue.type = v.KODI_TYPE_PHOTO PLAYQUEUES.append(playqueue) @@ -65,7 +62,7 @@ def get_playqueue_from_type(kodi_playlist_type): Returns the playqueue according to the kodi_playlist_type ('video', 'audio', 'picture') passed in """ - with LOCK: + with state.LOCK_SUBSCRIBER: for playqueue in PLAYQUEUES: if playqueue.type == kodi_playlist_type: break @@ -81,7 +78,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None): Returns the Playlist_Object """ - xml = GetAllPlexChildren(plex_id) + xml = PF.GetAllPlexChildren(plex_id) try: xml[0].attrib except (TypeError, IndexError, AttributeError): @@ -95,41 +92,11 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None): PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id()) playqueue.plex_transient_token = transient_token LOG.debug('Firing up Kodi player') - Player().play(playqueue.kodi_pl, None, False, 0) + xbmc.Player().play(playqueue.kodi_pl, None, False, 0) return playqueue -def update_playqueue_from_PMS(playqueue, - playqueue_id=None, - repeat=None, - offset=None, - transient_token=None): - """ - Completely updates the Kodi playqueue with the new Plex playqueue. Pass - in playqueue_id if we need to fetch a new playqueue - - repeat = 0, 1, 2 - offset = time offset in Plextime (milliseconds) - """ - LOG.info('New playqueue %s received from Plex companion with offset ' - '%s, repeat %s', playqueue_id, offset, repeat) - # Safe transient token from being deleted - if transient_token is None: - transient_token = playqueue.plex_transient_token - with LOCK: - xml = PL.get_PMS_playlist(playqueue, playqueue_id) - playqueue.clear() - try: - PL.get_playlist_details_from_xml(playqueue, xml) - except PL.PlaylistError: - LOG.error('Could not get playqueue ID %s', playqueue_id) - return - playqueue.repeat = 0 if not repeat else int(repeat) - playqueue.plex_transient_token = transient_token - play_xml(playqueue, xml, offset) - - -@thread_methods(add_suspends=['PMS_STATUS']) +@utils.thread_methods(add_suspends=['PMS_STATUS']) class PlayqueueMonitor(Thread): """ Unfortunately, Kodi does not tell if items within a Kodi playqueue @@ -222,8 +189,8 @@ class PlayqueueMonitor(Thread): while suspended(): if stopped(): break - sleep(1000) - with LOCK: + xbmc.sleep(1000) + with state.LOCK_SUBSCRIBER: for playqueue in PLAYQUEUES: kodi_pl = js.playlist_get_items(playqueue.playlistid) if playqueue.old_kodi_pl != kodi_pl: @@ -236,5 +203,5 @@ class PlayqueueMonitor(Thread): # compare old and new playqueue self._compare_playqueues(playqueue, kodi_pl) playqueue.old_kodi_pl = list(kodi_pl) - sleep(200) + xbmc.sleep(200) LOG.info("----===## PlayqueueMonitor stopped ##===----") diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index cad9b47e..536cfaa0 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -2,13 +2,13 @@ ############################################################################### from logging import getLogger -from downloadutils import DownloadUtils as DU -from utils import window, settings, language as lang, dialog, try_encode -import variables as v +from .downloadutils import DownloadUtils as DU +from . import utils +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.playutils') ############################################################################### @@ -46,7 +46,8 @@ class PlayUtils(): 'maxVideoBitrate': self.get_bitrate(), 'videoResolution': self.get_resolution(), 'videoQuality': '100', - 'mediaBufferSize': int(settings('kodi_video_cache'))/1024, + 'mediaBufferSize': int( + utils.settings('kodi_video_cache')) / 1024, }) self.item.playmethod = 'Transcode' LOG.info("The playurl is: %s", playurl) @@ -71,15 +72,15 @@ class PlayUtils(): return playurl # set to either 'Direct Stream=1' or 'Transcode=2' # and NOT to 'Direct Play=0' - if settings('playType') != "0": + if utils.settings('playType') != "0": # User forcing to play via HTTP LOG.info("User chose to not direct play") return if self.mustTranscode(): return return self.api.validate_playurl(path, - self.api.plex_type(), - force_check=True) + self.api.plex_type(), + force_check=True) def mustTranscode(self): """ @@ -106,7 +107,7 @@ class PlayUtils(): # e.g. trailers. Avoids TypeError with "'h265' in codec" LOG.info('No codec from PMS, not transcoding.') return False - if ((settings('transcodeHi10P') == 'true' and + if ((utils.settings('transcodeHi10P') == 'true' and videoCodec['bitDepth'] == '10') and ('h264' in codec)): LOG.info('Option to transcode 10bit h264 video content enabled.') @@ -139,7 +140,7 @@ class PlayUtils(): if self.api.plex_type() == 'track': return True # set to 'Transcode=2' - if settings('playType') == "2": + if utils.settings('playType') == "2": # User forcing to play via HTTP LOG.info("User chose to transcode") return False @@ -149,7 +150,7 @@ class PlayUtils(): def get_max_bitrate(self): # get the addon video quality - videoQuality = settings('maxVideoQualities') + videoQuality = utils.settings('maxVideoQualities') bitrate = { '0': 320, '1': 720, @@ -180,13 +181,13 @@ class PlayUtils(): '2': 720, '3': 1080 } - return H265[settings('transcodeH265')] + return H265[utils.settings('transcodeH265')] def get_bitrate(self): """ Get the desired transcoding bitrate from the settings """ - videoQuality = settings('transcoderVideoQualities') + videoQuality = utils.settings('transcoderVideoQualities') bitrate = { '0': 320, '1': 720, @@ -207,7 +208,7 @@ class PlayUtils(): """ Get the desired transcoding resolutions from the settings """ - chosen = settings('transcoderVideoQualities') + chosen = utils.settings('transcoderVideoQualities') res = { '0': '420x420', '1': '576x320', @@ -244,7 +245,7 @@ class PlayUtils(): audio_streams = [] subtitle_streams_list = [] # No subtitles as an option - subtitle_streams = [lang(39706)] + subtitle_streams = [utils.lang(39706)] downloadable_streams = [] download_subs = [] # selectAudioIndex = "" @@ -264,35 +265,35 @@ class PlayUtils(): codec = stream.attrib.get('codec') channellayout = stream.attrib.get('audioChannelLayout', "") try: - track = "%s %s - %s %s" % (audio_numb+1, + track = "%s %s - %s %s" % (audio_numb + 1, stream.attrib['language'], codec, channellayout) except KeyError: - track = "%s %s - %s %s" % (audio_numb+1, - lang(39707), # unknown + track = "%s %s - %s %s" % (audio_numb + 1, + utils.lang(39707), # unknown codec, channellayout) audio_streams_list.append(index) - audio_streams.append(try_encode(track)) + audio_streams.append(utils.try_encode(track)) audio_numb += 1 # Subtitles elif typus == "3": try: - track = "%s %s" % (sub_num+1, stream.attrib['language']) + track = "%s %s" % (sub_num + 1, stream.attrib['language']) except KeyError: - track = "%s %s (%s)" % (sub_num+1, - lang(39707), # unknown + track = "%s %s (%s)" % (sub_num + 1, + utils.lang(39707), # unknown stream.attrib.get('codec')) default = stream.attrib.get('default') forced = stream.attrib.get('forced') downloadable = stream.attrib.get('key') if default: - track = "%s - %s" % (track, lang(39708)) # Default + track = "%s - %s" % (track, utils.lang(39708)) # Default if forced: - track = "%s - %s" % (track, lang(39709)) # Forced + track = "%s - %s" % (track, utils.lang(39709)) # Forced if downloadable: # We do know the language - temporarily download if 'language' in stream.attrib: @@ -303,23 +304,23 @@ class PlayUtils(): # We don't know the language - no need to download else: path = self.api.attach_plex_token_to_url( - "%s%s" % (window('pms_server'), + "%s%s" % (utils.window('pms_server'), stream.attrib['key'])) downloadable_streams.append(index) - download_subs.append(try_encode(path)) + download_subs.append(utils.try_encode(path)) else: - track = "%s (%s)" % (track, lang(39710)) # burn-in + track = "%s (%s)" % (track, utils.lang(39710)) # burn-in if stream.attrib.get('selected') == '1' and downloadable: # Only show subs without asking user if they can be # turned off default_sub = index subtitle_streams_list.append(index) - subtitle_streams.append(try_encode(track)) + subtitle_streams.append(utils.try_encode(track)) sub_num += 1 if audio_numb > 1: - resp = dialog('select', lang(33013), audio_streams) + resp = utils.dialog('select', utils.lang(33013), audio_streams) if resp > -1: # User selected some audio track args = { @@ -335,14 +336,14 @@ class PlayUtils(): return select_subs_index = None - if (settings('pickPlexSubtitles') == 'true' and + if (utils.settings('pickPlexSubtitles') == 'true' and default_sub is not None): LOG.info('Using default Plex subtitle: %s', default_sub) select_subs_index = default_sub else: - resp = dialog('select', lang(33014), subtitle_streams) + resp = utils.dialog('select', utils.lang(33014), subtitle_streams) if resp > 0: - select_subs_index = subtitle_streams_list[resp-1] + select_subs_index = subtitle_streams_list[resp - 1] else: # User selected no subtitles or backed out of dialog select_subs_index = '' diff --git a/resources/lib/PlexAPI.py b/resources/lib/plex_api.py similarity index 94% rename from resources/lib/PlexAPI.py rename to resources/lib/plex_api.py index c6416d2f..5bec86c5 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/plex_api.py @@ -32,30 +32,27 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt from logging import getLogger from re import compile as re_compile, sub from urllib import urlencode, unquote -from os.path import basename, join -from os import makedirs - +import os from xbmcgui import ListItem from xbmcvfs import exists -import clientinfo as client -from downloadutils import DownloadUtils as DU -from utils import window, settings, language as lang, try_decode, try_encode, \ - unix_date_to_kodi, exists_dir, slugify, dialog, escape_html -import PlexFunctions as PF -import plexdb_functions as plexdb -import kodidb_functions as kodidb -import variables as v -import state +from .downloadutils import DownloadUtils as DU +from . import clientinfo +from . import utils +from . import plex_functions as PF +from . import plexdb_functions as plexdb +from . import kodidb_functions as kodidb +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.plex_api') REGEX_IMDB = re_compile(r'''/(tt\d+)''') REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''') -if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH): - makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH) +if not utils.exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH): + os.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH) ############################################################################### @@ -73,7 +70,7 @@ class API(object): # which media part in the XML response shall we look at? self.part = 0 self.mediastream = None - self.server = window('pms_server') + self.server = utils.window('pms_server') def set_part_number(self, number=None): """ @@ -206,7 +203,7 @@ class API(object): ans = None if ans is not None: try: - ans = try_decode(unquote(ans)) + ans = utils.try_decode(unquote(ans)) except UnicodeDecodeError: # Sometimes, Plex seems to have encoded in latin1 ans = unquote(ans).decode('latin1') @@ -218,23 +215,23 @@ class API(object): Will always use addon paths, never direct paths """ extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower() - if (window('plex_force_transcode_pix') == 'true' or + if (utils.window('plex_force_transcode_pix') == 'true' or extension not in v.KODI_SUPPORTED_IMAGES): # Let Plex transcode # max width/height supported by plex image transcoder is 1920x1080 path = self.server + PF.transcode_image_path( self.item[0][0].get('key'), - window('pms_token'), + utils.window('pms_token'), "%s%s" % (self.server, self.item[0][0].get('key')), 1920, 1080) else: path = self.attach_plex_token_to_url( - '%s%s' % (window('pms_server'), + '%s%s' % (utils.window('pms_server'), self.item[0][0].attrib['key'])) # Attach Plex id to url to let it be picked up by our playqueue agent # later - return try_encode('%s&plex_id=%s' % (path, self.plex_id())) + return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id())) def tv_show_path(self): """ @@ -261,7 +258,7 @@ class API(object): """ res = self.item.get('addedAt') if res is not None: - res = unix_date_to_kodi(res) + res = utils.unix_date_to_kodi(res) else: res = '2000-01-01 10:00:00' return res @@ -298,7 +295,7 @@ class API(object): played = True if playcount else False try: - last_played = unix_date_to_kodi(int(item['lastViewedAt'])) + last_played = utils.unix_date_to_kodi(int(item['lastViewedAt'])) except (KeyError, ValueError): last_played = None @@ -426,7 +423,7 @@ class API(object): """ answ = self.item.get('guid') if answ is not None: - answ = escape_html(answ) + answ = utils.escape_html(answ) return answ def provider(self, providername=None): @@ -459,7 +456,7 @@ class API(object): """ Returns the title of the element as unicode or 'Missing Title Name' """ - return try_decode(self.item.get('title', 'Missing Title Name')) + return utils.try_decode(self.item.get('title', 'Missing Title Name')) def titles(self): """ @@ -660,12 +657,12 @@ class API(object): url may or may not already contain a '?' """ - if window('pms_token') == '': + if utils.window('pms_token') == '': return url if '?' not in url: - url = "%s?X-Plex-Token=%s" % (url, window('pms_token')) + url = "%s?X-Plex-Token=%s" % (url, utils.window('pms_token')) else: - url = "%s&X-Plex-Token=%s" % (url, window('pms_token')) + url = "%s&X-Plex-Token=%s" % (url, utils.window('pms_token')) return url def item_id(self): @@ -811,12 +808,12 @@ class API(object): track['channels'] = stream.get('channels') # 'unknown' if we cannot get language track['language'] = stream.get( - 'languageCode', lang(39310)).lower() + 'languageCode', utils.lang(39310)).lower() audiotracks.append(track) elif media_type == 3: # Subtitle streams # 'unknown' if we cannot get language subtitlelanguages.append( - stream.get('languageCode', lang(39310)).lower()) + stream.get('languageCode', utils.lang(39310)).lower()) return { 'video': videotracks, 'audio': audiotracks, @@ -969,7 +966,7 @@ class API(object): LOG.info('Start movie set/collection lookup on themoviedb with %s', item.get('title', '')) - api_key = settings('themoviedbAPIKey') + api_key = utils.settings('themoviedbAPIKey') if media_type == v.PLEX_TYPE_SHOW: media_type = 'tv' title = item.get('title', '') @@ -980,7 +977,7 @@ class API(object): parameters = { 'api_key': api_key, 'language': v.KODILANGUAGE, - 'query': try_encode(title) + 'query': utils.try_encode(title) } data = DU().downloadUrl(url, authenticate=False, @@ -1106,8 +1103,8 @@ class API(object): try: data.get('poster_path') except AttributeError: - LOG.debug('Could not find TheMovieDB poster paths for %s in ' - 'the language %s', title, language) + LOG.debug('Could not find TheMovieDB poster paths for %s' + ' in the language %s', title, language) continue if not poster and data.get('poster_path'): poster = ('https://image.tmdb.org/t/p/original%s' % @@ -1123,7 +1120,7 @@ class API(object): media_id: IMDB id for movies, tvdb id for TV shows """ - api_key = settings('FanArtTVAPIKey') + api_key = utils.settings('FanArtTVAPIKey') typus = self.plex_type() if typus == v.PLEX_TYPE_SHOW: typus = 'tv' @@ -1239,17 +1236,17 @@ class API(object): count += 1 if (count > 1 and ( (self.plex_type() != 'clip' and - settings('bestQuality') == 'false') + utils.settings('bestQuality') == 'false') or (self.plex_type() == 'clip' and - settings('bestTrailer') == 'false'))): + utils.settings('bestTrailer') == 'false'))): # Several streams/files available. dialoglist = [] for entry in self.item.iterfind('./Media'): # Get additional info (filename / languages) filename = None if 'file' in entry[0].attrib: - filename = basename(entry[0].attrib['file']) + filename = os.path.basename(entry[0].attrib['file']) # Languages of audio streams languages = [] for stream in entry[0]: @@ -1258,12 +1255,13 @@ class API(object): languages.append(stream.attrib['language']) languages = ', '.join(languages) if filename: - option = try_encode(filename) + option = utils.try_encode(filename) if languages: if option: - option = '%s (%s): ' % (option, try_encode(languages)) + option = '%s (%s): ' % (option, + utils.try_encode(languages)) else: - option = '%s: ' % try_encode(languages) + option = '%s: ' % utils.try_encode(languages) if 'videoResolution' in entry.attrib: option = '%s%sp ' % (option, entry.get('videoResolution')) @@ -1278,7 +1276,7 @@ class API(object): option = '%s%s ' % (option, entry.get('audioCodec')) dialoglist.append(option) - media = dialog('select', 'Select stream', dialoglist) + media = utils.dialog('select', 'Select stream', dialoglist) else: media = 0 self.mediastream = media @@ -1309,7 +1307,7 @@ class API(object): self.mediastream_number() if quality is None: quality = {} - xargs = client.getXArgsDeviceInfo() + xargs = clientinfo.getXArgsDeviceInfo() # For DirectPlay, path/key of PART is needed # trailers are 'clip' with PMS xmls if action == "DirectStream": @@ -1334,19 +1332,19 @@ class API(object): transcode_path = self.server + \ '/video/:/transcode/universal/start.m3u8?' args = { - 'audioBoost': settings('audioBoost'), + 'audioBoost': utils.settings('audioBoost'), 'autoAdjustQuality': 0, 'directPlay': 0, 'directStream': 1, 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' - 'session': window('plex_client_Id'), + 'session': utils.window('plex_client_Id'), 'fastSeek': 1, 'path': path, 'mediaIndex': self.mediastream, 'partIndex': self.part, 'hasMDE': 1, 'location': 'lan', - 'subtitleSize': settings('subtitleSize') + 'subtitleSize': utils.settings('subtitleSize') } # Look like Android to let the PMS use the transcoding profile xargs.update(headers) @@ -1403,7 +1401,7 @@ class API(object): Returns the path to the downloaded subtitle or None """ - path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename) + path = os.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename) response = DU().downloadUrl(url, return_response=True) try: response.status_code @@ -1417,7 +1415,7 @@ class API(object): filer.write(response.content) except UnicodeEncodeError: LOG.debug('Need to slugify the filename %s', path) - path = slugify(path) + path = utils.slugify(path) with open(path, 'wb') as filer: filer.write(response.content) return path @@ -1561,7 +1559,8 @@ class API(object): listitem.setInfo('video', infoLabels=metadata) try: # Add context menu entry for information screen - listitem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)]) + listitem.addContextMenuItems([(utils.lang(30032), + 'XBMC.Action(Info)',)]) except TypeError: # Kodi fuck-up pass @@ -1607,20 +1606,20 @@ class API(object): # exist() needs a / or \ at the end to work for directories if folder is False: # files - check = exists(try_encode(path)) + check = exists(utils.try_encode(path)) else: # directories if "\\" in path: if not path.endswith('\\'): # Add the missing backslash - check = exists_dir(path + "\\") + check = utils.exists_dir(path + "\\") else: - check = exists_dir(path) + check = utils.exists_dir(path) else: if not path.endswith('/'): - check = exists_dir(path + "/") + check = utils.exists_dir(path + "/") else: - check = exists_dir(path) + check = utils.exists_dir(path) if not check: if force_check is False: @@ -1649,25 +1648,11 @@ class API(object): LOG.warn('Cannot access file: %s', url) # Kodi cannot locate the file #s. Please verify your PKC settings. Stop # syncing? - resp = dialog('yesno', heading='{plex}', line1=lang(39031) % url) + resp = utils.dialog('yesno', + heading='{plex}', + line1=utils.lang(39031) % url) return resp - def set_listitem_artwork(self, listitem): - """ - Set all artwork to the listitem - """ - allartwork = self.artwork() - listitem.setArt(self.artwork()) - for arttype in arttypes: - art = arttypes[arttype] - if art == "Backdrop": - # Backdrop is a list, grab the first backdrop - self._set_listitem_artprop(listitem, - arttype, - allartwork[art][0]) - else: - self._set_listitem_artprop(listitem, arttype, allartwork[art]) - @staticmethod def _set_listitem_artprop(listitem, arttype, path): if arttype in ( diff --git a/resources/lib/PlexCompanion.py b/resources/lib/plex_companion.py similarity index 76% rename from resources/lib/PlexCompanion.py rename to resources/lib/plex_companion.py index 89d5a7a9..49969759 100644 --- a/resources/lib/PlexCompanion.py +++ b/resources/lib/plex_companion.py @@ -6,30 +6,57 @@ from threading import Thread from Queue import Empty from socket import SHUT_RDWR from urllib import urlencode - from xbmc import sleep, executebuiltin, Player -from utils import settings, thread_methods, language as lang, dialog -from plexbmchelper import listener, plexgdm, subscribers, httppersist -from plexbmchelper.subscribers import LOCKER -from PlexFunctions import ParseContainerKey, GetPlexMetadata, DownloadChunks -from PlexAPI import API -from playlist_func import get_pms_playqueue, get_plextype_from_xml, \ - get_playlist_details_from_xml -from playback import playback_triage, play_xml -import json_rpc as js -import variables as v -import state -import playqueue as PQ +from .plexbmchelper import listener, plexgdm, subscribers, httppersist +from .plex_api import API +from . import utils +from . import plex_functions as PF +from . import playlist_func as PL +from . import playback +from . import json_rpc as js +from . import playqueue as PQ +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.plex_companion') ############################################################################### -@thread_methods(add_suspends=['PMS_STATUS']) +def update_playqueue_from_PMS(playqueue, + playqueue_id=None, + repeat=None, + offset=None, + transient_token=None): + """ + Completely updates the Kodi playqueue with the new Plex playqueue. Pass + in playqueue_id if we need to fetch a new playqueue + + repeat = 0, 1, 2 + offset = time offset in Plextime (milliseconds) + """ + LOG.info('New playqueue %s received from Plex companion with offset ' + '%s, repeat %s', playqueue_id, offset, repeat) + # Safe transient token from being deleted + if transient_token is None: + transient_token = playqueue.plex_transient_token + with state.LOCK_SUBSCRIBER: + xml = PL.get_PMS_playlist(playqueue, playqueue_id) + playqueue.clear() + try: + PL.get_playlist_details_from_xml(playqueue, xml) + except PL.PlaylistError: + LOG.error('Could not get playqueue ID %s', playqueue_id) + return + playqueue.repeat = 0 if not repeat else int(repeat) + playqueue.plex_transient_token = transient_token + playback.play_xml(playqueue, xml, offset) + + +@utils.thread_methods(add_suspends=['PMS_STATUS']) class PlexCompanion(Thread): """ Plex Companion monitoring class. Invoke only once @@ -47,9 +74,9 @@ class PlexCompanion(Thread): self.subscription_manager = None Thread.__init__(self) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def _process_alexa(self, data): - xml = GetPlexMetadata(data['key']) + xml = PF.GetPlexMetadata(data['key']) try: xml[0].attrib except (AttributeError, IndexError, TypeError): @@ -62,28 +89,33 @@ class PlexCompanion(Thread): api.plex_id(), transient_token=data.get('token')) elif data['containerKey'].startswith('/playQueues/'): - _, container_key, _ = ParseContainerKey(data['containerKey']) - xml = DownloadChunks('{server}/playQueues/%s?' % container_key) + _, container_key, _ = PF.ParseContainerKey(data['containerKey']) + xml = PF.DownloadChunks('{server}/playQueues/%s?' % container_key) if xml is None: # "Play error" - dialog('notification', lang(29999), lang(30128), icon='{error}') + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) playqueue.clear() - get_playlist_details_from_xml(playqueue, xml) + PL.get_playlist_details_from_xml(playqueue, xml) playqueue.plex_transient_token = data.get('token') if data.get('offset') != '0': offset = float(data['offset']) / 1000.0 else: offset = None - play_xml(playqueue, xml, offset) + playback.play_xml(playqueue, xml, offset) else: state.PLEX_TRANSIENT_TOKEN = data.get('token') if data.get('offset') != '0': state.RESUMABLE = True state.RESUME_PLAYBACK = True - playback_triage(api.plex_id(), api.plex_type(), resolve=False) + playback.playback_triage(api.plex_id(), + api.plex_type(), + resolve=False) @staticmethod def _process_node(data): @@ -99,17 +131,17 @@ class PlexCompanion(Thread): executebuiltin('RunPlugin(plugin://%s?%s)' % (v.ADDON_ID, urlencode(params))) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def _process_playlist(self, data): # Get the playqueue ID - _, container_key, query = ParseContainerKey(data['containerKey']) + _, container_key, query = PF.ParseContainerKey(data['containerKey']) try: playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[data['type']]) except KeyError: # E.g. Plex web does not supply the media type # Still need to figure out the type (video vs. music vs. pix) - xml = GetPlexMetadata(data['key']) + xml = PF.GetPlexMetadata(data['key']) try: xml[0].attrib except (AttributeError, IndexError, TypeError): @@ -118,14 +150,13 @@ class PlexCompanion(Thread): api = API(xml[0]) playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()]) - PQ.update_playqueue_from_PMS( - playqueue, - playqueue_id=container_key, - repeat=query.get('repeat'), - offset=data.get('offset'), - transient_token=data.get('token')) + update_playqueue_from_PMS(playqueue, + playqueue_id=container_key, + repeat=query.get('repeat'), + offset=data.get('offset'), + transient_token=data.get('token')) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def _process_streams(self, data): """ Plex Companion client adjusted audio or subtitle stream @@ -147,17 +178,17 @@ class PlexCompanion(Thread): else: LOG.error('Unknown setStreams command: %s', data) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def _process_refresh(self, data): """ example data: {'playQueueID': '8475', 'commandID': '11'} """ - xml = get_pms_playqueue(data['playQueueID']) + xml = PL.get_pms_playqueue(data['playQueueID']) if xml is None: return if len(xml) == 0: LOG.debug('Empty playqueue received - clearing playqueue') - plex_type = get_plextype_from_xml(xml) + plex_type = PL.get_plextype_from_xml(xml) if plex_type is None: return playqueue = PQ.get_playqueue_from_type( @@ -166,7 +197,7 @@ class PlexCompanion(Thread): return playqueue = PQ.get_playqueue_from_type( v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[xml[0].attrib['type']]) - PQ.update_playqueue_from_PMS(playqueue, data['playQueueID']) + update_playqueue_from_PMS(playqueue, data['playQueueID']) def _process_tasks(self, task): """ @@ -231,7 +262,7 @@ class PlexCompanion(Thread): self.player) self.subscription_manager = subscription_manager - if settings('plexCompanion') == 'true': + if utils.settings('plexCompanion') == 'true': # Start up httpd start_count = 0 while True: diff --git a/resources/lib/PlexFunctions.py b/resources/lib/plex_functions.py similarity index 94% rename from resources/lib/PlexFunctions.py rename to resources/lib/plex_functions.py index de165fb1..89da000b 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/plex_functions.py @@ -7,18 +7,17 @@ from re import compile as re_compile from copy import deepcopy from time import time from threading import Thread - from xbmc import sleep -from downloadutils import DownloadUtils as DU -from utils import settings, try_encode, try_decode -from variables import PLEX_TO_KODI_TIMEFACTOR -import plex_tv +from .downloadutils import DownloadUtils as DU +from . import utils +from . import plex_tv +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.plex_functions') -CONTAINERSIZE = int(settings('limitindex')) +CONTAINERSIZE = int(utils.settings('limitindex')) REGEX_PLEX_KEY = re_compile(r'''/(.+)/(\d+)$''') REGEX_PLEX_DIRECT = re_compile(r'''\.plex\.direct:\d+$''') @@ -36,7 +35,7 @@ def ConvertPlexToKodiTime(plexTime): """ if plexTime is None: return None - return int(float(plexTime) * PLEX_TO_KODI_TIMEFACTOR) + return int(float(plexTime) * v.PLEX_TO_KODI_TIMEFACTOR) def GetPlexKeyNumber(plexKey): @@ -90,13 +89,13 @@ def GetMethodFromPlexType(plexType): def GetPlexLoginFromSettings(): """ Returns a dict: - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') + 'plexLogin': utils.settings('plexLogin'), + 'plexToken': utils.settings('plexToken'), + 'plexhome': utils.settings('plexhome'), + 'plexid': utils.settings('plexid'), + 'myplexlogin': utils.settings('myplexlogin'), + 'plexAvatar': utils.settings('plexAvatar'), + 'plexHomeSize': utils.settings('plexHomeSize') Returns strings or unicode @@ -106,13 +105,13 @@ def GetPlexLoginFromSettings(): plexhome is 'true' if plex home is used (the default) """ return { - 'plexLogin': settings('plexLogin'), - 'plexToken': settings('plexToken'), - 'plexhome': settings('plexhome'), - 'plexid': settings('plexid'), - 'myplexlogin': settings('myplexlogin'), - 'plexAvatar': settings('plexAvatar'), - 'plexHomeSize': settings('plexHomeSize') + 'plexLogin': utils.settings('plexLogin'), + 'plexToken': utils.settings('plexToken'), + 'plexhome': utils.settings('plexhome'), + 'plexid': utils.settings('plexid'), + 'myplexlogin': utils.settings('myplexlogin'), + 'plexAvatar': utils.settings('plexAvatar'), + 'plexHomeSize': utils.settings('plexHomeSize') } @@ -140,7 +139,7 @@ def check_connection(url, token=None, verifySSL=None): if token is not None: header_options = {'X-Plex-Token': token} if verifySSL is True: - verifySSL = None if settings('sslverify') == 'true' else False + verifySSL = None if utils.settings('sslverify') == 'true' else False if 'plex.tv' in url: url = 'https://plex.tv/api/home/users' LOG.debug("Checking connection to server %s with verifySSL=%s", @@ -303,11 +302,11 @@ def _plex_gdm(): } for line in response['data'].split('\n'): if 'Content-Type:' in line: - pms['product'] = try_decode(line.split(':')[1].strip()) + pms['product'] = utils.try_decode(line.split(':')[1].strip()) elif 'Host:' in line: pms['baseURL'] = line.split(':')[1].strip() elif 'Name:' in line: - pms['name'] = try_decode(line.split(':')[1].strip()) + pms['name'] = utils.try_decode(line.split(':')[1].strip()) elif 'Port:' in line: pms['port'] = line.split(':')[1].strip() elif 'Resource-Identifier:' in line: @@ -336,7 +335,7 @@ def _pms_list_from_plex_tv(token): queue = Queue() thread_queue = [] - max_age_in_seconds = 2*60*60*24 + max_age_in_seconds = 2 * 60 * 60 * 24 for device in xml.findall('Device'): if 'server' not in device.get('provides'): # No PMS - skip @@ -355,7 +354,7 @@ def _pms_list_from_plex_tv(token): 'token': device.get('accessToken'), 'ownername': device.get('sourceTitle'), 'product': device.get('product'), # e.g. 'Plex Media Server' - 'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e...' + 'version': device.get('productVersion'), # e.g. '1.11.2.4772-3e..' 'device': device.get('device'), # e.g. 'PC' or 'Windows' 'platform': device.get('platform'), # e.g. 'Windows', 'Android' 'local': device.get('publicAddressMatches') == '1', @@ -634,7 +633,7 @@ def init_plex_playqueue(itemid, librarySectionUUID, mediatype='movie', 'repeat': '0' } if trailers is True: - args['extrasPrefixCount'] = settings('trailerNumber') + args['extrasPrefixCount'] = utils.settings('trailerNumber') xml = DU().downloadUrl(url + '?' + urlencode(args), action_type="POST") try: xml[0].tag @@ -791,7 +790,7 @@ def GetUserArtworkURL(username): Returns the URL for the user's Avatar. Or False if something went wrong. """ - users = plex_tv.list_home_users(settings('plexToken')) + users = plex_tv.list_home_users(utils.settings('plexToken')) url = '' # If an error is encountered, set to False if not users: @@ -818,13 +817,13 @@ def transcode_image_path(key, AuthToken, path, width, height): final path to image file """ # external address - can we get a transcoding request for external images? - if key.startswith('http://') or key.startswith('https://'): + if key.startswith('http://') or key.startswith('https://'): path = key elif key.startswith('/'): # internal full path. path = 'http://127.0.0.1:32400' + key else: # internal path, add-on path = 'http://127.0.0.1:32400' + path + '/' + key - path = try_encode(path) + path = utils.try_encode(path) # This is bogus (note the extra path component) but ATV is stupid when it # comes to caching images, it doesn't use querystrings. Fortunately PMS is # lenient... diff --git a/resources/lib/plex_tv.py b/resources/lib/plex_tv.py index fa28cb67..4dccd592 100644 --- a/resources/lib/plex_tv.py +++ b/resources/lib/plex_tv.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- from logging import getLogger - from xbmc import sleep, executebuiltin -from downloadutils import DownloadUtils as DU -from utils import dialog, language as lang, settings, try_encode -import variables as v -import state +from .downloadutils import DownloadUtils as DU +from . import utils +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.plex_tx') ############################################################################### @@ -39,7 +38,7 @@ def choose_home_user(token): username = user['title'] userlist.append(username) # To take care of non-ASCII usernames - userlist_coded.append(try_encode(username)) + userlist_coded.append(utils.try_encode(username)) usernumber = len(userlist) username = '' usertoken = '' @@ -47,12 +46,14 @@ def choose_home_user(token): while trials < 3: if usernumber > 1: # Select user - user_select = dialog('select', lang(29999) + lang(39306), - userlist_coded) + user_select = utils.dialog( + 'select', + '%s%s' % (utils.lang(29999), utils.lang(39306)), + userlist_coded) if user_select == -1: LOG.info("No user selected.") - settings('username', value='') - executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + utils.settings('username', value='') + executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID) return False # Only 1 user received, choose that one else: @@ -64,11 +65,11 @@ def choose_home_user(token): pin = None if user['protected'] == '1': LOG.debug('Asking for users PIN') - pin = dialog('input', - lang(39307) + selected_user, - '', - type='{numeric}', - option='{hide}') + pin = utils.dialog('input', + '%s%s' % (utils.lang(39307), selected_user), + '', + type='{numeric}', + option='{hide}') # User chose to cancel # Plex bug: don't call url for protected user with empty PIN if not pin: @@ -78,7 +79,7 @@ def choose_home_user(token): result = switch_home_user(user['id'], pin, token, - settings('plex_machineIdentifier')) + utils.settings('plex_machineIdentifier')) if result: # Successfully retrieved username: break out of while loop username = result['username'] @@ -88,15 +89,16 @@ def choose_home_user(token): else: trials += 1 # Could not login user, please try again - if not dialog('yesno', - heading='{plex}', - line1=lang(39308) + selected_user, - line2=lang(39309)): + if not utils.dialog('yesno', + heading='{plex}', + line1='%s%s' % (utils.lang(39308), + selected_user), + line2=utils.lang(39309)): # User chose to cancel break if not username: LOG.error('Failed signing in a user to plex.tv') - executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID) + executebuiltin('Addon.Openutils.settings(%s)' % v.ADDON_ID) return False return { 'username': username, @@ -123,7 +125,7 @@ def switch_home_user(userid, pin, token, machineIdentifier): for the machineIdentifier that was chosen } - settings('userid') and settings('username') with new plex token + utils.settings('userid') and utils.settings('username') with new plex token """ LOG.info('Switching to user %s', userid) url = 'https://plex.tv/api/home/users/' + userid + '/switch' @@ -143,12 +145,12 @@ def switch_home_user(userid, pin, token, machineIdentifier): token = answer.attrib.get('authenticationToken', '') # Write to settings file - settings('username', username) - settings('accessToken', token) - settings('userid', answer.attrib.get('id', '')) - settings('plex_restricteduser', - 'true' if answer.attrib.get('restricted', '0') == '1' - else 'false') + utils.settings('username', username) + utils.settings('accessToken', token) + utils.settings('userid', answer.attrib.get('id', '')) + utils.settings('plex_restricteduser', + 'true' if answer.attrib.get('restricted', '0') == '1' + else 'false') state.RESTRICTED_USER = True if \ answer.attrib.get('restricted', '0') == '1' else False @@ -239,15 +241,15 @@ def sign_in_with_pin(): code, identifier = get_pin() if not code: # Problems trying to contact plex.tv. Try again later - dialog('ok', heading='{plex}', line1=lang(39303)) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39303)) return False # Go to https://plex.tv/pin and enter the code: # Or press No to cancel the sign in. - answer = dialog('yesno', - heading='{plex}', - line1=lang(39304) + "\n\n", - line2=code + "\n\n", - line3=lang(39311)) + answer = utils.dialog('yesno', + heading='{plex}', + line1='%s%s' % (utils.lang(39304), "\n\n"), + line2='%s%s' % (code, "\n\n"), + line3=utils.lang(39311)) if not answer: return False count = 0 @@ -261,7 +263,7 @@ def sign_in_with_pin(): count += 1 if xml is False: # Could not sign in to plex.tv Try again later - dialog('ok', heading='{plex}', line1=lang(39305)) + utils.dialog('ok', heading='{plex}', line1=utils.lang(39305)) return False # Parse xml userid = xml.attrib.get('id') @@ -282,15 +284,15 @@ def sign_in_with_pin(): 'plexid': userid, 'homesize': home_size } - settings('plexLogin', username) - settings('plexToken', token) - settings('plexhome', home) - settings('plexid', userid) - settings('plexAvatar', avatar) - settings('plexHomeSize', home_size) + utils.settings('plexLogin', username) + utils.settings('plexToken', token) + utils.settings('plexhome', home) + utils.settings('plexid', userid) + utils.settings('plexAvatar', avatar) + utils.settings('plexHomeSize', home_size) # Let Kodi log into plex.tv on startup from now on - settings('myplexlogin', 'true') - settings('plex_status', value=lang(39227)) + utils.settings('myplexlogin', 'true') + utils.settings('plex_status', value=utils.lang(39227)) return result diff --git a/resources/lib/plexbmchelper/httppersist.py b/resources/lib/plexbmchelper/httppersist.py index 2d65bb60..3b23b1f6 100644 --- a/resources/lib/plexbmchelper/httppersist.py +++ b/resources/lib/plexbmchelper/httppersist.py @@ -7,7 +7,7 @@ from socket import error as socket_error ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.httppersist') ############################################################################### diff --git a/resources/lib/plexbmchelper/listener.py b/resources/lib/plexbmchelper/listener.py index 5e86257e..a3b03fd8 100644 --- a/resources/lib/plexbmchelper/listener.py +++ b/resources/lib/plexbmchelper/listener.py @@ -6,19 +6,18 @@ from re import sub from SocketServer import ThreadingMixIn from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from urlparse import urlparse, parse_qs +import xbmc -from xbmc import sleep, Player, Monitor - -from companion import process_command -import json_rpc as js -from clientinfo import getXArgsDeviceInfo -import variables as v +from .. import companion +from .. import json_rpc as js +from .. import clientinfo +from .. import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) -PLAYER = Player() -MONITOR = Monitor() +LOG = getLogger('PLEX.listener') +PLAYER = xbmc.Player() +MONITOR = xbmc.Monitor() # Hack we need in order to keep track of the open connections from Plex Web CLIENT_DICT = {} @@ -122,7 +121,7 @@ class MyHandler(BaseHTTPRequestHandler): RESOURCES_XML.format( title=v.DEVICENAME, machineIdentifier=v.PKC_MACHINE_IDENTIFIER), - getXArgsDeviceInfo(include_token=False)) + clientinfo.getXArgsDeviceInfo(include_token=False)) elif request_path == 'player/timeline/poll': # Plex web does polling if connected to PKC via Companion # Only reply if there is indeed something playing @@ -188,7 +187,7 @@ class MyHandler(BaseHTTPRequestHandler): code=500) elif "/subscribe" in request_path: self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) + clientinfo.getXArgsDeviceInfo(include_token=False)) protocol = params.get('protocol') host = self.client_address[0] port = params.get('port') @@ -201,14 +200,14 @@ class MyHandler(BaseHTTPRequestHandler): command_id) elif "/unsubscribe" in request_path: self.response(v.COMPANION_OK_MESSAGE, - getXArgsDeviceInfo(include_token=False)) + clientinfo.getXArgsDeviceInfo(include_token=False)) uuid = self.headers.get('X-Plex-Client-Identifier') \ or self.client_address[0] sub_mgr.remove_subscriber(uuid) else: # Throw it to companion.py - process_command(request_path, params) - self.response('', getXArgsDeviceInfo(include_token=False)) + companion.process_command(request_path, params) + self.response('', clientinfo.getXArgsDeviceInfo(include_token=False)) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): diff --git a/resources/lib/plexbmchelper/plexgdm.py b/resources/lib/plexbmchelper/plexgdm.py index 4c88998c..d430acac 100644 --- a/resources/lib/plexbmchelper/plexgdm.py +++ b/resources/lib/plexbmchelper/plexgdm.py @@ -25,16 +25,15 @@ import logging import socket import threading import time - from xbmc import sleep -import downloadutils -from utils import window, settings, dialog, language -import variables as v +from ..downloadutils import DownloadUtils as DU +from .. import utils +from .. import variables as v ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = logging.getLogger('PLEX.plexgdm') ############################################################################### @@ -49,7 +48,7 @@ class plexgdm: self._multicast_address = '239.0.0.250' self.discover_group = (self._multicast_address, 32414) self.client_register_group = (self._multicast_address, 32413) - self.client_update_port = int(settings('companionUpdatePort')) + self.client_update_port = int(utils.settings('companionUpdatePort')) self.server_list = [] self.discovery_interval = 120 @@ -58,7 +57,7 @@ class plexgdm: self._registration_is_running = False self.client_registered = False - self.download = downloadutils.DownloadUtils().downloadUrl + self.download = DU().downloadUrl def clientDetails(self): self.client_data = ( @@ -120,14 +119,15 @@ class plexgdm: log.error("Unable to bind to port [%s] - Plex Companion will not " "be registered. Change the Plex Companion update port!" % self.client_update_port) - if settings('companion_show_gdm_port_warning') == 'true': - if dialog('yesno', - language(29999), - 'Port %s' % self.client_update_port, - language(39079), - yeslabel=language(30013), # Never show again - nolabel=language(30012)): # OK - settings('companion_show_gdm_port_warning', value='false') + if utils.settings('companion_show_gdm_port_warning') == 'true': + if utils.dialog('yesno', + utils.lang(29999), + 'Port %s' % self.client_update_port, + utils.lang(39079), + yeslabel=utils.lang(30013), # Never show again + nolabel=utils.lang(30012)): # OK + utils.settings('companion_show_gdm_port_warning', + value='false') from xbmc import executebuiltin executebuiltin( 'Addon.OpenSettings(plugin.video.plexkodiconnect)') @@ -189,7 +189,7 @@ class plexgdm: log.info("Server list is empty. Unable to check") return False for server in self.server_list: - if server['uuid'] == window('plex_machineIdentifier'): + if server['uuid'] == utils.window('plex_machineIdentifier'): media_server = server['server'] media_port = server['port'] scheme = server['protocol'] @@ -223,7 +223,7 @@ class plexgdm: return self.server_list def discover(self): - currServer = window('pms_server') + currServer = utils.window('pms_server') if not currServer: return currServerProt, currServerIP, currServerPort = \ @@ -240,9 +240,9 @@ class plexgdm: 'owned': '1', 'role': 'master', 'server': currServerIP, - 'serverName': window('plex_servername'), + 'serverName': utils.window('plex_servername'), 'updated': int(time.time()), - 'uuid': window('plex_machineIdentifier'), + 'uuid': utils.window('plex_machineIdentifier'), 'version': 'irrelevant' }] @@ -305,5 +305,5 @@ class plexgdm: def start_all(self, daemon=False): self.start_discovery(daemon) - if settings('plexCompanion') == 'true': + if utils.settings('plexCompanion') == 'true': self.start_registration(daemon) diff --git a/resources/lib/plexbmchelper/subscribers.py b/resources/lib/plexbmchelper/subscribers.py index 2f01c462..3dffcd1e 100644 --- a/resources/lib/plexbmchelper/subscribers.py +++ b/resources/lib/plexbmchelper/subscribers.py @@ -3,22 +3,17 @@ Manages getting playstate from Kodi and sending it to the PMS as well as subscribed Plex Companion clients. """ from logging import getLogger -from threading import Thread, RLock +from threading import Thread -from downloadutils import DownloadUtils as DU -from utils import window, kodi_time_to_millis, LockFunction -import state -import variables as v -import json_rpc as js -import playqueue as PQ +from ..downloadutils import DownloadUtils as DU +from .. import utils +from .. import state +from .. import variables as v +from .. import json_rpc as js +from .. import playqueue as PQ ############################################################################### - -LOG = getLogger("PLEX." + __name__) -# Need to lock all methods and functions messing with subscribers or state -LOCK = RLock() -LOCKER = LockFunction(LOCK) - +LOG = getLogger('PLEX.subscribers') ############################################################################### # What is Companion controllable? @@ -150,7 +145,7 @@ class SubscriptionMgr(object): position = info['position'] return position - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def msg(self, players): """ Returns a timeline xml as str @@ -206,12 +201,12 @@ class SubscriptionMgr(object): if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO, v.PLEX_PLAYLIST_TYPE_PHOTO): self.location = 'fullScreenVideo' self.stop_sent_to_web = False - pbmc_server = window('pms_server') + pbmc_server = utils.window('pms_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 = kodi_time_to_millis(info['totaltime']) + duration = utils.kodi_time_to_millis(info['totaltime']) shuffle = '1' if info['shuffled'] else '0' mute = '1' if info['muted'] is True else '0' answ = { @@ -219,11 +214,11 @@ class SubscriptionMgr(object): 'protocol': self.protocol, 'address': self.server, 'port': self.port, - 'machineIdentifier': window('plex_machineIdentifier'), + 'machineIdentifier': utils.window('plex_machineIdentifier'), 'state': status, 'type': ptype, 'itemType': ptype, - 'time': kodi_time_to_millis(info['time']), + 'time': utils.kodi_time_to_millis(info['time']), 'duration': duration, 'seekRange': '0-%s' % duration, 'shuffle': shuffle, @@ -231,7 +226,7 @@ class SubscriptionMgr(object): 'volume': info['volume'], 'mute': mute, 'mediaIndex': 0, # Still to implement from here - 'partIndex':0, + 'partIndex': 0, 'partCount': 1, 'providerIdentifier': 'com.plexapp.plugins.library', } @@ -302,7 +297,7 @@ class SubscriptionMgr(object): return playqueue.items[position].plex_stream_index( info[STREAM_DETAILS[stream_type]]['index'], stream_type) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def update_command_id(self, uuid, command_id): """ Updates the Plex Companien client with the machine identifier uuid with @@ -320,8 +315,6 @@ class SubscriptionMgr(object): for player in players.values(): info = state.PLAYER_STATES[player['playerid']] playqueue = PQ.PLAYQUEUES[player['playerid']] - LOG.debug('playqueue is: %s', playqueue) - LOG.debug('info is: %s', info) position = self._get_correct_position(info, playqueue) try: item = playqueue.items[position] @@ -334,7 +327,7 @@ class SubscriptionMgr(object): return False return True - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def notify(self): """ Causes PKC to tell the PMS and Plex Companion players to receive a @@ -360,8 +353,6 @@ class SubscriptionMgr(object): def _notify_server(self, players): for typus, player in players.iteritems(): - LOG.debug('player is %s', player) - LOG.debug('typus is %s', typus) self._send_pms_notification( player['playerid'], self._get_pms_params(player['playerid'])) try: @@ -386,8 +377,8 @@ class SubscriptionMgr(object): 'state': status, 'ratingKey': item.plex_id, 'key': '/library/metadata/%s' % item.plex_id, - 'time': kodi_time_to_millis(info['time']), - 'duration': kodi_time_to_millis(info['totaltime']) + 'time': utils.kodi_time_to_millis(info['time']), + 'duration': utils.kodi_time_to_millis(info['totaltime']) } if info['container_key'] is not None: # params['containerKey'] = info['container_key'] @@ -419,7 +410,7 @@ class SubscriptionMgr(object): LOG.debug("Sent server notification with parameters: %s to %s", xargs, url) - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def add_subscriber(self, protocol, host, port, uuid, command_id): """ Adds a new Plex Companion subscriber to PKC. @@ -434,7 +425,7 @@ class SubscriptionMgr(object): self.subscribers[subscriber.uuid] = subscriber return subscriber - @LOCKER.lockthis + @state.LOCKER_SUBSCRIBER.lockthis def remove_subscriber(self, uuid): """ Removes a connected Plex Companion subscriber with machine identifier diff --git a/resources/lib/plexdb_functions.py b/resources/lib/plexdb_functions.py index 782da994..9a0e3866 100644 --- a/resources/lib/plexdb_functions.py +++ b/resources/lib/plexdb_functions.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- - ############################################################################### - -from utils import kodi_sql -import variables as v +from . import utils +from . import variables as v ############################################################################### @@ -17,7 +15,7 @@ class Get_Plex_DB(): and the db gets closed """ def __enter__(self): - self.plexconn = kodi_sql('plex') + self.plexconn = utils.kodi_sql('plex') return Plex_DB_Functions(self.plexconn.cursor()) def __exit__(self, type, value, traceback): diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py new file mode 100644 index 00000000..8d38df2d --- /dev/null +++ b/resources/lib/service_entry.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +############################################################################### +import logging +import sys +import xbmc + +from . import utils +from . import userclient +from . import initialsetup +from . import kodimonitor +from . import librarysync +from . import websocket_client +from . import plex_companion +from . import plex_functions as PF +from . import command_pipeline +from . import playback_starter +from . import playqueue +from . import artwork +from . import variables as v +from . import state +from . import loghandler + +############################################################################### +loghandler.config() +LOG = logging.getLogger("PLEX.service_entry") +############################################################################### + + +class Service(): + + server_online = True + warn_auth = True + + user = None + ws = None + library = None + plexcompanion = None + + user_running = False + ws_running = False + alexa_running = False + library_running = False + plexcompanion_running = False + kodimonitor_running = False + playback_starter_running = False + image_cache_thread_running = False + + def __init__(self): + # Initial logging + LOG.info("======== START %s ========", v.ADDON_NAME) + LOG.info("Platform: %s", v.PLATFORM) + LOG.info("KODI Version: %s", v.KODILONGVERSION) + LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) + LOG.info("PKC Direct Paths: %s", + utils.settings('useDirectPaths') == '1') + LOG.info("Number of sync threads: %s", + utils.settings('syncThreadNumber')) + LOG.info("Full sys.argv received: %s", sys.argv) + self.monitor = xbmc.Monitor() + # Load/Reset PKC entirely - important for user/Kodi profile switch + initialsetup.reload_pkc() + + def _stop_pkc(self): + """ + Kodi's abortRequested is really unreliable :-( + """ + return self.monitor.abortRequested() or state.STOP_PKC + + def ServiceEntryPoint(self): + # Important: Threads depending on abortRequest will not trigger + # if profile switch happens more than once. + _stop_pkc = self._stop_pkc + monitor = self.monitor + + # Server auto-detect + initialsetup.InitialSetup().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() + self.ws = websocket_client.PMS_Websocket() + self.alexa = websocket_client.Alexa_Websocket() + self.library = librarysync.LibrarySync() + self.plexcompanion = plex_companion.PlexCompanion() + self.specialmonitor = kodimonitor.SpecialMonitor() + self.playback_starter = playback_starter.PlaybackStarter() + self.playqueue = playqueue.PlayqueueMonitor() + if utils.settings('enableTextureCache') == "true": + self.image_cache_thread = artwork.Image_Cache_Thread() + + welcome_msg = True + counter = 0 + while not _stop_pkc(): + + if utils.window('plex_kodiProfile') != v.KODI_PROFILE: + # Profile change happened, terminate this thread and others + LOG.info("Kodi profile was: %s and changed to: %s. " + "Terminating old PlexKodiConnect thread.", + v.KODI_PROFILE, utils.window('plex_kodiProfile')) + break + + # 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 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.library_running: + self.library_running = True + self.library.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() + if (not self.image_cache_thread_running and + utils.settings('enableTextureCache') == "true"): + self.image_cache_thread_running = True + self.image_cache_thread.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 + 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 + + # 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): + # 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 + utils.window('plex_service_started', clear=True) + LOG.info("======== STOP %s ========", v.ADDON_NAME) + + +def start(): + # Safety net - Kody starts PKC twice upon first installation! + if utils.window('plex_service_started') == 'true': + EXIT = True + else: + utils.window('plex_service_started', value='true') + EXIT = False + + # Delay option + DELAY = int(utils.settings('startupDelay')) + + LOG.info("Delaying Plex startup by: %s sec...", DELAY) + if EXIT: + LOG.error('PKC service.py already started - exiting this instance') + elif DELAY and xbmc.Monitor().waitForAbort(DELAY): + # Start the service + LOG.info("Abort requested while waiting. PKC not started.") + else: + Service().ServiceEntryPoint() diff --git a/resources/lib/state.py b/resources/lib/state.py index ce27325f..0687825a 100644 --- a/resources/lib/state.py +++ b/resources/lib/state.py @@ -1,5 +1,48 @@ # -*- coding: utf-8 -*- # THREAD SAFE +from threading import Lock, RLock +from functools import wraps + + +class LockFunction(object): + """ + Decorator for class methods and functions to lock them with lock. + + Initialize this class first + lockfunction = LockFunction(lock), where lock is a threading.Lock() object + + To then lock a function or method: + + @lockfunction.lockthis + def some_function(args, kwargs) + """ + def __init__(self, lock): + self.lock = lock + + def lockthis(self, func): + """ + Use this method to actually lock a function or method + """ + @wraps(func) + def wrapper(*args, **kwargs): + """ + Wrapper construct + """ + with self.lock: + result = func(*args, **kwargs) + return result + return wrapper + + +# LOCKS +#################### +# Need to lock all methods and functions messing with subscribers +LOCK_SUBSCRIBER = RLock() +LOCKER_SUBSCRIBER = LockFunction(LOCK_SUBSCRIBER) +# Necessary to temporarily hold back librarysync/websocket listener when doing +# a full sync +LOCK_PLAYLISTS = Lock() +LOCKER_PLAYLISTS = LockFunction(LOCK_PLAYLISTS) # Quit PKC STOP_PKC = False diff --git a/resources/lib/userclient.py b/resources/lib/userclient.py index cefa31b7..d52450d9 100644 --- a/resources/lib/userclient.py +++ b/resources/lib/userclient.py @@ -7,20 +7,20 @@ from xbmc import sleep, executebuiltin, translatePath import xbmcaddon from xbmcvfs import exists -from utils import window, settings, language as lang, thread_methods, dialog -from downloadutils import DownloadUtils as DU -import plex_tv -import PlexFunctions as PF -import state +from .downloadutils import DownloadUtils as DU +from . import utils +from . import plex_tv +from . import plex_functions as PF +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.userclient') ############################################################################### -@thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) +@utils.thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) class UserClient(Thread): """ Manage Plex users @@ -54,11 +54,11 @@ class UserClient(Thread): Get the current PMS' URL """ # Original host - self.server_name = settings('plex_servername') - https = settings('https') == "true" - host = settings('ipaddress') - port = settings('port') - self.machine_identifier = settings('plex_machineIdentifier') + 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 @@ -74,7 +74,8 @@ class UserClient(Thread): self.machine_identifier = PF.GetMachineIdentifier(server) if not self.machine_identifier: self.machine_identifier = '' - settings('plex_machineIdentifier', value=self.machine_identifier) + utils.settings('plex_machineIdentifier', + value=self.machine_identifier) LOG.debug('Returning active server: %s', server) return server @@ -84,15 +85,15 @@ class UserClient(Thread): Do we need to verify the SSL certificate? Return None if that is the case, else False """ - return None if settings('sslverify') == 'true' else False + return None if utils.settings('sslverify') == 'true' else False @staticmethod def get_ssl_certificate(): """ Client side certificate """ - return None if settings('sslcert') == 'None' \ - else settings('sslcert') + return None if utils.settings('sslcert') == 'None' \ + else utils.settings('sslcert') def set_user_prefs(self): """ @@ -103,7 +104,7 @@ class UserClient(Thread): if self.token: url = PF.GetUserArtworkURL(self.user) if url: - window('PlexUserImage', value=url) + utils.window('PlexUserImage', value=url) @staticmethod def check_access(): @@ -141,29 +142,32 @@ class UserClient(Thread): state.PLEX_USER_ID = user_id or None state.PLEX_USERNAME = username # This is the token for the current PMS (might also be '') - window('pms_token', value=usertoken) + 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 - window('plex_token', value=settings('plexToken')) - state.PLEX_TOKEN = settings('plexToken') or None - window('plex_restricteduser', value=settings('plex_restricteduser')) + 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 settings('plex_restricteduser') == 'true' else False - window('pms_server', value=self.server) - window('plex_machineIdentifier', value=self.machine_identifier) - window('plex_servername', value=self.server_name) - window('plex_authenticated', value='true') + if utils.settings('plex_restricteduser') == 'true' else False + utils.window('pms_server', value=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 - window('useDirectPaths', value='true' - if settings('useDirectPaths') == "1" else 'false') - state.DIRECT_PATHS = True if settings('useDirectPaths') == "1" \ + 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 settings('indicate_media_versions') == "true" else False - window('plex_force_transcode_pix', value='true' - if settings('force_transcode_pix') == "1" else 'false') + 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() @@ -173,9 +177,9 @@ class UserClient(Thread): self.set_user_prefs() # Writing values to settings file - settings('username', value=username) - settings('userid', value=user_id) - settings('accessToken', value=usertoken) + utils.settings('username', value=username) + utils.settings('userid', value=user_id) + utils.settings('accessToken', value=usertoken) return True def authenticate(self): @@ -188,9 +192,9 @@ class UserClient(Thread): if self.retry >= 2: LOG.error("Too many retries to login.") state.PMS_STATUS = 'Stop' - dialog('ok', lang(33001), lang(39023)) + utils.dialog('ok', utils.lang(33001), utils.lang(39023)) executebuiltin( - 'Addon.OpenSettings(plugin.video.plexkodiconnect)') + 'Addon.Openutils.settings(plugin.video.plexkodiconnect)') return False # Get /profile/addon_data @@ -209,10 +213,10 @@ class UserClient(Thread): return False # If there is a username in the settings, try authenticating - username = settings('username') - userId = settings('userid') - usertoken = settings('accessToken') - enforceLogin = settings('enforceUserLogin') + 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') @@ -225,15 +229,15 @@ class UserClient(Thread): return True elif answ == 401: LOG.error("User token no longer valid. Sign user out") - settings('username', value='') - settings('userid', value='') - settings('accessToken', value='') + 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 = settings('plexToken') + plextoken = utils.settings('plexToken') if plextoken: LOG.info("Trying to connect to plex.tv to get a user list") userInfo = plex_tv.choose_home_user(plextoken) @@ -268,24 +272,24 @@ class UserClient(Thread): self.do_utils.stopSession() except AttributeError: pass - window('plex_authenticated', clear=True) + utils.window('plex_authenticated', clear=True) state.AUTHENTICATED = False - window('pms_token', clear=True) + utils.window('pms_token', clear=True) state.PLEX_TOKEN = None state.PLEX_TRANSIENT_TOKEN = None state.PMS_TOKEN = None - window('plex_token', clear=True) - window('pms_server', clear=True) - window('plex_machineIdentifier', clear=True) - window('plex_servername', clear=True) + utils.window('plex_token', clear=True) + utils.window('pms_server', clear=True) + utils.window('plex_machineIdentifier', clear=True) + utils.window('plex_servername', clear=True) state.PLEX_USER_ID = None state.PLEX_USERNAME = None - window('plex_restricteduser', clear=True) + utils.window('plex_restricteduser', clear=True) state.RESTRICTED_USER = False - settings('username', value='') - settings('userid', value='') - settings('accessToken', value='') + utils.settings('username', value='') + utils.settings('userid', value='') + utils.settings('accessToken', value='') self.token = None self.auth = True @@ -313,7 +317,7 @@ class UserClient(Thread): elif state.PMS_STATUS == "401": # Unauthorized access, revoke token state.PMS_STATUS = 'Auth' - window('plex_serverStatus', value='Auth') + utils.window('plex_serverStatus', value='Auth') self.reset_client() sleep(3000) @@ -330,7 +334,7 @@ class UserClient(Thread): LOG.info("Current userId: %s", state.PLEX_USER_ID) self.retry = 0 state.SUSPEND_LIBRARY_THREAD = False - window('plex_serverStatus', clear=True) + utils.window('plex_serverStatus', clear=True) state.PMS_STATUS = False if not self.auth and (self.user is None): diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 62f92556..a2f96dc9 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -4,6 +4,10 @@ Various functions and decorators for PKC """ ############################################################################### from logging import getLogger +import xbmc +import xbmcaddon +import xbmcgui +from xbmcvfs import exists, delete import os from cProfile import Profile from pstats import Stats @@ -20,17 +24,12 @@ import hashlib import re import unicodedata -import xbmc -import xbmcaddon -import xbmcgui -from xbmcvfs import exists, delete - -import variables as v -import state +from . import variables as v +from . import state ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.utils') WINDOW = xbmcgui.Window(10000) ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect') @@ -51,7 +50,7 @@ def reboot_kodi(message=None): Set optional custom message """ - message = message or language(33033) + message = message or lang(33033) dialog('ok', heading='{plex}', line1=message) xbmc.executebuiltin('RestartApp') @@ -130,7 +129,7 @@ def exists_dir(path): return answ -def language(stringid): +def lang(stringid): """ Central string retrieval from strings.po """ @@ -194,7 +193,7 @@ def dialog(typus, *args, **kwargs): kwargs['option'] = types[kwargs['option']] if 'heading' in kwargs: kwargs['heading'] = kwargs['heading'].replace("{plex}", - language(29999)) + lang(29999)) dia = xbmcgui.Dialog() types = { 'yesno': dia.yesno, @@ -487,8 +486,8 @@ def wipe_database(): connection.commit() cursor.close() # Reset the artwork sync status in the PKC settings - settings('caching_artwork_count', value=language(39310)) - settings('fanarttv_lookups', value=language(39310)) + settings('caching_artwork_count', value=lang(39310)) + settings('fanarttv_lookups', value=lang(39310)) # reset the install run flag settings('SyncInstallRunDone', value="false") @@ -500,8 +499,8 @@ def reset(ask_user=True): """ # Are you sure you want to reset your local Kodi database? if ask_user and not dialog('yesno', - heading='{plex} %s ' % language(30132), - line1=language(39600)): + heading='{plex} %s ' % lang(30132), + line1=lang(39600)): return # first stop any db sync @@ -513,8 +512,8 @@ def reset(ask_user=True): if count == 0: # Could not stop the database from running. Please try again later. dialog('ok', - heading='{plex} %s' % language(30132), - line1=language(39601)) + heading='{plex} %s' % lang(30132), + line1=lang(39601)) return xbmc.sleep(1000) @@ -524,8 +523,8 @@ def reset(ask_user=True): # Reset all PlexKodiConnect Addon settings? (this is usually NOT # recommended and unnecessary!) if ask_user and dialog('yesno', - heading='{plex} %s ' % language(30132), - line1=language(39603)): + heading='{plex} %s ' % lang(30132), + line1=lang(39603)): # Delete the settings addon = xbmcaddon.Addon() addondir = try_decode(xbmc.translatePath(addon.getAddonInfo('profile'))) @@ -708,7 +707,7 @@ class XmlKodiSetting(object): LOG.error('Error parsing %s', self.path) # "Kodi cannot parse {0}. PKC will not function correctly. Please # visit {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( + dialog('ok', lang(29999), lang(39716).format( self.filename, 'http://kodi.wiki')) self.__exit__(etree.ParseError, None, None) @@ -861,7 +860,7 @@ def passwords_xml(): LOG.error('Error parsing %s', xmlpath) # "Kodi cannot parse {0}. PKC will not function correctly. Please visit # {1} and correct your file!" - dialog('ok', language(29999), language(39716).format( + dialog('ok', lang(29999), lang(39716).format( 'passwords.xml', 'http://forum.kodi.tv/')) return else: @@ -1192,33 +1191,3 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None): # Return class to render this a decorator return cls - - -class LockFunction(object): - """ - Decorator for class methods and functions to lock them with lock. - - Initialize this class first - lockfunction = LockFunction(lock), where lock is a threading.Lock() object - - To then lock a function or method: - - @lockfunction.lockthis - def some_function(args, kwargs) - """ - def __init__(self, lock): - self.lock = lock - - def lockthis(self, func): - """ - Use this method to actually lock a function or method - """ - @wraps(func) - def wrapper(*args, **kwargs): - """ - Wrapper construct - """ - with self.lock: - result = func(*args, **kwargs) - return result - return wrapper diff --git a/resources/lib/variables.py b/resources/lib/variables.py index 3ebb36d1..b1ef2d98 100644 --- a/resources/lib/variables.py +++ b/resources/lib/variables.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import os - import xbmc from xbmcaddon import Addon diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py index 85b624b2..a960880d 100644 --- a/resources/lib/videonodes.py +++ b/resources/lib/videonodes.py @@ -4,18 +4,16 @@ from logging import getLogger from distutils import dir_util import xml.etree.ElementTree as etree from os import makedirs - import xbmc from xbmcvfs import exists -from utils import window, settings, language as lang, try_encode, indent, \ - normalize_nodes, exists_dir, try_decode -import variables as v -import state +from . import utils +from . import variables as v +from . import state ############################################################################### -log = getLogger("PLEX."+__name__) +LOG = getLogger('PLEX.videonodes') ############################################################################### # Paths are strings, NOT unicode! @@ -30,21 +28,26 @@ class VideoNodes(object): root = etree.Element('node', attrib={'order': "%s" % order}) elif roottype == 1: # Filter - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"}) + root = etree.Element('node', + attrib={'order': "%s" % order, 'type': "filter"}) etree.SubElement(root, 'match').text = "all" # Add tag rule - rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"}) + rule = etree.SubElement(root, + 'rule', + attrib={'field': "tag", 'operator': "is"}) etree.SubElement(rule, 'value').text = tagname else: # Folder - root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"}) + root = etree.Element('node', + attrib={'order': "%s" % order, 'type': "folder"}) etree.SubElement(root, 'label').text = label etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.plexkodiconnect/icon.png" return root - def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False): + def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, + delete=False): # Plex: reassign mediatype due to Kodi inner workings # How many items do we get at most? limit = state.FETCH_PMS_ITEM_NUMBER @@ -63,32 +66,32 @@ class VideoNodes(object): dirname = viewid # Returns strings - path = try_decode(xbmc.translatePath( + path = utils.try_decode(xbmc.translatePath( "special://profile/library/video/")) - nodepath = try_decode(xbmc.translatePath( + nodepath = utils.try_decode(xbmc.translatePath( "special://profile/library/video/Plex-%s/" % dirname)) if delete: - if exists_dir(nodepath): + if utils.exists_dir(nodepath): from shutil import rmtree rmtree(nodepath) - log.info("Sucessfully removed videonode: %s." % tagname) + LOG.info("Sucessfully removed videonode: %s." % tagname) return # Verify the video directory - if not exists_dir(path): + if not utils.exists_dir(path): dir_util.copy_tree( - src=try_decode( + src=utils.try_decode( xbmc.translatePath("special://xbmc/system/library/video")), - dst=try_decode( + dst=utils.try_decode( xbmc.translatePath("special://profile/library/video")), preserve_mode=0) # do not copy permission bits! # Create the node directory if mediatype != "photos": - if not exists_dir(nodepath): + if not utils.exists_dir(nodepath): # folder does not exist yet - log.debug('Creating folder %s' % nodepath) + LOG.debug('Creating folder %s' % nodepath) makedirs(nodepath) # Create index entry @@ -97,13 +100,13 @@ class VideoNodes(object): path = "library://video/Plex-%s/" % dirname for i in range(1, indexnumber): # Verify to make sure we don't create duplicates - if window('Plex.nodes.%s.index' % i) == path: + if utils.window('Plex.nodes.%s.index' % i) == path: return if mediatype == "photos": path = "plugin://plugin.video.plexkodiconnect?mode=browseplex&key=/library/sections/%s&id=%s" % (viewid, viewid) - window('Plex.nodes.%s.index' % indexnumber, value=path) + utils.window('Plex.nodes.%s.index' % indexnumber, value=path) # Root if not mediatype == "photos": @@ -119,7 +122,7 @@ class VideoNodes(object): tagname=tagname, roottype=0) try: - indent(root) + utils.indent(root) except: pass etree.ElementTree(root).write(nodeXML, encoding="UTF-8") @@ -222,14 +225,15 @@ class VideoNodes(object): # Get label stringid = nodes[node] if node != "1": - label = lang(stringid) + label = utils.lang(stringid) if not label: label = xbmc.getLocalizedString(stringid) else: label = stringid # Set window properties - if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all": + if ((mediatype == "homevideos" or mediatype == "photos") and + nodetype == "all"): # Custom query path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s" % (viewid, mediatype)) @@ -278,34 +282,39 @@ class VideoNodes(object): templabel = label embynode = "Plex.nodes.%s" % indexnumber - window('%s.title' % embynode, value=templabel) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=mediatype) + utils.window('%s.title' % embynode, value=templabel) + utils.window('%s.path' % embynode, value=windowpath) + utils.window('%s.content' % embynode, value=path) + utils.window('%s.type' % embynode, value=mediatype) else: embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype) - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) + utils.window('%s.title' % embynode, value=label) + utils.window('%s.path' % embynode, value=windowpath) + utils.window('%s.content' % embynode, value=path) if mediatype == "photos": - # For photos, we do not create a node in videos but we do want the window props - # to be created. - # To do: add our photos nodes to kodi picture sources somehow + # For photos, we do not create a node in videos but we do want + # the window props to be created. To do: add our photos nodes to + # kodi picture sources somehow continue - if exists(try_encode(nodeXML)): + if exists(utils.try_encode(nodeXML)): # Don't recreate xml if already exists continue # Create the root if (nodetype in ("nextepisodes", "ondeck", 'recentepisodes', 'browsefiles') or mediatype == "homevideos"): # Folder type with plugin path - root = self.commonRoot(order=sortorder[node], label=label, tagname=tagname, roottype=2) + root = self.commonRoot(order=sortorder[node], + label=label, + tagname=tagname, + roottype=2) etree.SubElement(root, 'path').text = path etree.SubElement(root, 'content').text = "episodes" else: - root = self.commonRoot(order=sortorder[node], label=label, tagname=tagname) + root = self.commonRoot(order=sortorder[node], + label=label, + tagname=tagname) if nodetype in ('recentepisodes', 'inprogressepisodes'): etree.SubElement(root, 'content').text = "episodes" else: @@ -313,20 +322,24 @@ class VideoNodes(object): # Elements per nodetype if nodetype == "all": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "sorttitle" elif nodetype == "recent": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, + 'order', + {'direction': "descending"}).text = "dateadded" etree.SubElement(root, 'limit').text = limit - if settings('MovieShowWatched') == 'false': + if utils.settings('MovieShowWatched') == 'false': rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" - elif nodetype == "inprogress": - etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) + etree.SubElement(root, + 'rule', + {'field': "inprogress", 'operator': "true"}) etree.SubElement(root, 'limit').text = limit etree.SubElement( root, @@ -335,54 +348,67 @@ class VideoNodes(object): ).text = 'lastplayed' elif nodetype == "genres": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "sorttitle" etree.SubElement(root, 'group').text = "genres" - elif nodetype == "unwatched": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" - rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "sorttitle" + rule = etree.SubElement(root, + "rule", + {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" - elif nodetype == "sets": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "sorttitle" etree.SubElement(root, 'group').text = "tags" - elif nodetype == "random": - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "random" etree.SubElement(root, 'limit').text = limit - elif nodetype == "recommended": - etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" + etree.SubElement(root, + 'order', + {'direction': "descending"}).text = "rating" etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + rule = etree.SubElement(root, + 'rule', + {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" - rule2 = etree.SubElement(root, 'rule', - attrib={'field': "rating", 'operator': "greaterthan"}) + rule2 = etree.SubElement(root, + 'rule', + attrib={'field': "rating", 'operator': "greaterthan"}) etree.SubElement(rule2, 'value').text = "7" - elif nodetype == "recentepisodes": # Kodi Isengard, Jarvis - etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" + etree.SubElement(root, + 'order', + {'direction': "descending"}).text = "dateadded" etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) + rule = etree.SubElement(root, + 'rule', + {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" - elif nodetype == "inprogressepisodes": # Kodi Isengard, Jarvis etree.SubElement(root, 'limit').text = limit - rule = etree.SubElement(root, 'rule', - attrib={'field': "inprogress", 'operator':"true"}) - + rule = etree.SubElement(root, + 'rule', + attrib={'field': "inprogress", 'operator':"true"}) try: - indent(root) + utils.indent(root) except: pass etree.ElementTree(root).write(nodeXML, encoding="UTF-8") def singleNode(self, indexnumber, tagname, mediatype, itemtype): - tagname = try_encode(tagname) - cleantagname = try_decode(normalize_nodes(tagname)) - nodepath = try_decode(xbmc.translatePath( + tagname = utils.try_encode(tagname) + cleantagname = utils.try_decode(utils.normalize_nodes(tagname)) + nodepath = utils.try_decode(xbmc.translatePath( "special://profile/library/video/")) nodeXML = "%splex_%s.xml" % (nodepath, cleantagname) path = "library://video/plex_%s.xml" % cleantagname @@ -393,12 +419,12 @@ class VideoNodes(object): windowpath = "ActivateWindow(Video,%s,return)" % path # Create the video node directory - if not exists_dir(nodepath): + if not utils.exists_dir(nodepath): # We need to copy over the default items dir_util.copy_tree( - src=try_decode( + src=utils.try_decode( xbmc.translatePath("special://xbmc/system/library/video")), - dst=try_decode( + dst=utils.try_decode( xbmc.translatePath("special://profile/library/video")), preserve_mode=0) # do not copy permission bits! @@ -407,14 +433,14 @@ class VideoNodes(object): 'Favorite tvshows': 30181, 'channels': 30173 } - label = lang(labels[tagname]) + label = utils.lang(labels[tagname]) embynode = "Plex.nodes.%s" % indexnumber - window('%s.title' % embynode, value=label) - window('%s.path' % embynode, value=windowpath) - window('%s.content' % embynode, value=path) - window('%s.type' % embynode, value=itemtype) + utils.window('%s.title' % embynode, value=label) + utils.window('%s.path' % embynode, value=windowpath) + utils.window('%s.content' % embynode, value=path) + utils.window('%s.type' % embynode, value=itemtype) - if exists(try_encode(nodeXML)): + if exists(utils.try_encode(nodeXML)): # Don't recreate xml if already exists return @@ -423,23 +449,26 @@ class VideoNodes(object): label=label, tagname=tagname, roottype=2) - etree.SubElement(root, 'path').text = "plugin://plugin.video.plexkodiconnect/?id=0&mode=channels" + etree.SubElement(root, + 'path').text = "plugin://plugin.video.plexkodiconnect/?id=0&mode=channels" else: root = self.commonRoot(order=1, label=label, tagname=tagname) - etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" + etree.SubElement(root, + 'order', + {'direction': "ascending"}).text = "sorttitle" etree.SubElement(root, 'content').text = mediatype try: - indent(root) + utils.indent(root) except: pass etree.ElementTree(root).write(nodeXML, encoding="UTF-8") def clearProperties(self): - log.info("Clearing nodes properties.") - plexprops = window('Plex.nodes.total') + LOG.info("Clearing nodes properties.") + plexprops = utils.window('Plex.nodes.total') propnames = [ "index","path","title","content", "inprogress.content","inprogress.title", @@ -457,4 +486,5 @@ class VideoNodes(object): totalnodes = int(plexprops) for i in range(totalnodes): for prop in propnames: - window('Plex.nodes.%s.%s' % (str(i), prop), clear=True) + utils.window('Plex.nodes.%s.%s' % (str(i), prop), + clear=True) diff --git a/resources/lib/watchdog/events.py b/resources/lib/watchdog/events.py index 7c1f0750..24cfec62 100644 --- a/resources/lib/watchdog/events.py +++ b/resources/lib/watchdog/events.py @@ -88,9 +88,9 @@ Event Handler Classes import os.path import logging import re -from pathtools.patterns import match_any_paths -from watchdog.utils import has_attribute -from watchdog.utils import unicode_paths +from ..pathtools.patterns import match_any_paths +from .utils import has_attribute +from .utils import unicode_paths EVENT_TYPE_MOVED = 'moved' diff --git a/resources/lib/watchdog/observers/__init__.py b/resources/lib/watchdog/observers/__init__.py index d84fac90..32148a17 100644 --- a/resources/lib/watchdog/observers/__init__.py +++ b/resources/lib/watchdog/observers/__init__.py @@ -55,8 +55,8 @@ Class Platforms Note """ import warnings -from watchdog.utils import platform -from watchdog.utils import UnsupportedLibc +from ..utils import platform +from ..utils import UnsupportedLibc if platform.is_linux(): try: diff --git a/resources/lib/watchdog/observers/api.py b/resources/lib/watchdog/observers/api.py index 30f2e9a3..35a7748d 100644 --- a/resources/lib/watchdog/observers/api.py +++ b/resources/lib/watchdog/observers/api.py @@ -18,9 +18,9 @@ from __future__ import with_statement import threading -from watchdog.utils import BaseThread -from watchdog.utils.compat import queue -from watchdog.utils.bricks import SkipRepeatsQueue +from ..utils import BaseThread +from ..utils.compat import queue +from ..utils.bricks import SkipRepeatsQueue DEFAULT_EMITTER_TIMEOUT = 1 # in seconds. DEFAULT_OBSERVER_TIMEOUT = 1 # in seconds. diff --git a/resources/lib/watchdog/observers/fsevents.py b/resources/lib/watchdog/observers/fsevents.py index 67481480..d2519e28 100644 --- a/resources/lib/watchdog/observers/fsevents.py +++ b/resources/lib/watchdog/observers/fsevents.py @@ -30,7 +30,7 @@ import threading import unicodedata import _watchdog_fsevents as _fsevents -from watchdog.events import ( +from ..events import ( FileDeletedEvent, FileModifiedEvent, FileCreatedEvent, @@ -41,8 +41,8 @@ from watchdog.events import ( DirMovedEvent ) -from watchdog.utils.dirsnapshot import DirectorySnapshot -from watchdog.observers.api import ( +from ..utils.dirsnapshot import DirectorySnapshot +from ..observers.api import ( BaseObserver, EventEmitter, DEFAULT_EMITTER_TIMEOUT, diff --git a/resources/lib/watchdog/observers/fsevents2.py b/resources/lib/watchdog/observers/fsevents2.py index 9b6ebcba..e2d7c667 100644 --- a/resources/lib/watchdog/observers/fsevents2.py +++ b/resources/lib/watchdog/observers/fsevents2.py @@ -24,9 +24,9 @@ import os import logging import unicodedata from threading import Thread -from watchdog.utils.compat import queue +from ..utils.compat import queue -from watchdog.events import ( +from ..events import ( FileDeletedEvent, FileModifiedEvent, FileCreatedEvent, @@ -36,7 +36,7 @@ from watchdog.events import ( DirCreatedEvent, DirMovedEvent ) -from watchdog.observers.api import ( +from ..observers.api import ( BaseObserver, EventEmitter, DEFAULT_EMITTER_TIMEOUT, diff --git a/resources/lib/watchdog/observers/inotify.py b/resources/lib/watchdog/observers/inotify.py index 9f3a4dfc..f8e87235 100644 --- a/resources/lib/watchdog/observers/inotify.py +++ b/resources/lib/watchdog/observers/inotify.py @@ -73,14 +73,14 @@ import os import threading from .inotify_buffer import InotifyBuffer -from watchdog.observers.api import ( +from ..observers.api import ( EventEmitter, BaseObserver, DEFAULT_EMITTER_TIMEOUT, DEFAULT_OBSERVER_TIMEOUT ) -from watchdog.events import ( +from ..events import ( DirDeletedEvent, DirModifiedEvent, DirMovedEvent, @@ -92,7 +92,7 @@ from watchdog.events import ( generate_sub_moved_events, generate_sub_created_events, ) -from watchdog.utils import unicode_paths +from ..utils import unicode_paths class InotifyEmitter(EventEmitter): diff --git a/resources/lib/watchdog/observers/inotify_buffer.py b/resources/lib/watchdog/observers/inotify_buffer.py index dce2ae12..274c847a 100644 --- a/resources/lib/watchdog/observers/inotify_buffer.py +++ b/resources/lib/watchdog/observers/inotify_buffer.py @@ -15,9 +15,9 @@ # limitations under the License. import logging -from watchdog.utils import BaseThread -from watchdog.utils.delayed_queue import DelayedQueue -from watchdog.observers.inotify_c import Inotify +from ..utils import BaseThread +from ..utils.delayed_queue import DelayedQueue +from ..observers.inotify_c import Inotify logger = logging.getLogger(__name__) diff --git a/resources/lib/watchdog/observers/inotify_c.py b/resources/lib/watchdog/observers/inotify_c.py index 5f208b6a..6c7f1ffc 100644 --- a/resources/lib/watchdog/observers/inotify_c.py +++ b/resources/lib/watchdog/observers/inotify_c.py @@ -24,8 +24,8 @@ import ctypes import ctypes.util from functools import reduce from ctypes import c_int, c_char_p, c_uint32 -from watchdog.utils import has_attribute -from watchdog.utils import UnsupportedLibc +from ..utils import has_attribute +from ..utils import UnsupportedLibc def _load_libc(): diff --git a/resources/lib/watchdog/observers/kqueue.py b/resources/lib/watchdog/observers/kqueue.py index 9ace9232..a5838d18 100644 --- a/resources/lib/watchdog/observers/kqueue.py +++ b/resources/lib/watchdog/observers/kqueue.py @@ -78,7 +78,7 @@ Collections and Utility Classes """ from __future__ import with_statement -from watchdog.utils import platform +from ..utils import platform import threading import errno @@ -94,18 +94,18 @@ if sys.version_info < (2, 7, 0): else: import select -from pathtools.path import absolute_path +from ...pathtools.path import absolute_path -from watchdog.observers.api import ( +from ..observers.api import ( BaseObserver, EventEmitter, DEFAULT_OBSERVER_TIMEOUT, DEFAULT_EMITTER_TIMEOUT ) -from watchdog.utils.dirsnapshot import DirectorySnapshot +from ..utils.dirsnapshot import DirectorySnapshot -from watchdog.events import ( +from ..events import ( DirMovedEvent, DirDeletedEvent, DirCreatedEvent, diff --git a/resources/lib/watchdog/observers/polling.py b/resources/lib/watchdog/observers/polling.py index 3b1d995b..96e5f42b 100644 --- a/resources/lib/watchdog/observers/polling.py +++ b/resources/lib/watchdog/observers/polling.py @@ -38,16 +38,17 @@ from __future__ import with_statement import os import threading from functools import partial -from watchdog.utils import stat as default_stat -from watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff -from watchdog.observers.api import ( +from ..utils import stat as default_stat +from ..utils.dirsnapshot import DirectorySnapshot, \ + DirectorySnapshotDiff +from ..observers.api import ( EventEmitter, BaseObserver, DEFAULT_OBSERVER_TIMEOUT, DEFAULT_EMITTER_TIMEOUT ) -from watchdog.events import ( +from ..events import ( DirMovedEvent, DirDeletedEvent, DirCreatedEvent, diff --git a/resources/lib/watchdog/observers/read_directory_changes.py b/resources/lib/watchdog/observers/read_directory_changes.py index 623a2356..d68cae57 100644 --- a/resources/lib/watchdog/observers/read_directory_changes.py +++ b/resources/lib/watchdog/observers/read_directory_changes.py @@ -24,7 +24,7 @@ import threading import os.path import time -from watchdog.events import ( +from ..events import ( DirCreatedEvent, DirDeletedEvent, DirMovedEvent, @@ -37,14 +37,14 @@ from watchdog.events import ( generate_sub_created_events, ) -from watchdog.observers.api import ( +from ..observers.api import ( EventEmitter, BaseObserver, DEFAULT_OBSERVER_TIMEOUT, DEFAULT_EMITTER_TIMEOUT ) -from watchdog.observers.winapi import ( +from ..observers.winapi import ( read_events, get_directory_handle, close_directory_handle, diff --git a/resources/lib/watchdog/tricks/__init__.py b/resources/lib/watchdog/tricks/__init__.py index 7e1c9fe2..dcffaf57 100644 --- a/resources/lib/watchdog/tricks/__init__.py +++ b/resources/lib/watchdog/tricks/__init__.py @@ -22,8 +22,8 @@ import signal import subprocess import time -from watchdog.utils import echo, has_attribute -from watchdog.events import PatternMatchingEventHandler +from ..utils import echo, has_attribute +from ..events import PatternMatchingEventHandler class Trick(PatternMatchingEventHandler): diff --git a/resources/lib/watchdog/utils/__init__.py b/resources/lib/watchdog/utils/__init__.py index e0f7ad1b..e912033c 100644 --- a/resources/lib/watchdog/utils/__init__.py +++ b/resources/lib/watchdog/utils/__init__.py @@ -33,9 +33,8 @@ Classes import os import sys import threading -import watchdog.utils.platform -from watchdog.utils.compat import Event -from collections import namedtuple +from . import platform +from .compat import Event if sys.version_info[0] == 2 and platform.is_windows(): diff --git a/resources/lib/watchdog/utils/compat.py b/resources/lib/watchdog/utils/compat.py index 0f6e7947..b1ae6674 100644 --- a/resources/lib/watchdog/utils/compat.py +++ b/resources/lib/watchdog/utils/compat.py @@ -24,6 +24,6 @@ except ImportError: if sys.version_info < (2, 7): - from watchdog.utils.event_backport import Event + from .event_backport import Event else: from threading import Event \ No newline at end of file diff --git a/resources/lib/watchdog/utils/dirsnapshot.py b/resources/lib/watchdog/utils/dirsnapshot.py index 20abe221..7d56fe0a 100644 --- a/resources/lib/watchdog/utils/dirsnapshot.py +++ b/resources/lib/watchdog/utils/dirsnapshot.py @@ -47,8 +47,7 @@ Classes import errno import os from stat import S_ISDIR -from watchdog.utils import platform -from watchdog.utils import stat as default_stat +from . import stat as default_stat class DirectorySnapshotDiff(object): diff --git a/resources/lib/watchdog/utils/unicode_paths.py b/resources/lib/watchdog/utils/unicode_paths.py index 501a2f15..c1e3ceed 100644 --- a/resources/lib/watchdog/utils/unicode_paths.py +++ b/resources/lib/watchdog/utils/unicode_paths.py @@ -24,7 +24,7 @@ import sys -from watchdog.utils import platform +from . import platform try: # Python 2 diff --git a/resources/lib/watchdog/watchmedo.py b/resources/lib/watchdog/watchmedo.py index ce891f83..29c63e46 100644 --- a/resources/lib/watchdog/watchmedo.py +++ b/resources/lib/watchdog/watchmedo.py @@ -37,8 +37,8 @@ except ImportError: from io import StringIO from argh import arg, aliases, ArghParser, expects_obj -from watchdog.version import VERSION_STRING -from watchdog.utils import load_class +from .version import VERSION_STRING +from .utils import load_class logging.basicConfig(level=logging.INFO) diff --git a/resources/lib/websocket.py b/resources/lib/websocket.py index 8c551623..ac8aff70 100644 --- a/resources/lib/websocket.py +++ b/resources/lib/websocket.py @@ -48,11 +48,11 @@ import logging import traceback import sys -import utils +from . import utils ############################################################################### -LOG = logging.getLogger("PLEX." + __name__) +LOG = logging.getLogger('PLEX.websocket') ############################################################################### diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index ce15f30e..4018ff9c 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -7,17 +7,16 @@ from json import loads import xml.etree.ElementTree as etree from threading import Thread from ssl import CERT_NONE - from xbmc import sleep -from utils import window, settings, thread_methods -from companion import process_command -import state -import variables as v +from . import utils +from . import companion +from . import state +from . import variables as v ############################################################################### -LOG = getLogger("PLEX." + __name__) +LOG = getLogger('PLEX.websocket_client') ############################################################################### @@ -140,14 +139,14 @@ class WebSocket(Thread): LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__) -@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', - 'BACKGROUND_SYNC_DISABLED']) +@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', + 'BACKGROUND_SYNC_DISABLED']) class PMS_Websocket(WebSocket): """ Websocket connection with the PMS for Plex Companion """ def getUri(self): - server = window('pms_server') + server = utils.window('pms_server') # Get the appropriate prefix for the websocket if server.startswith('https'): server = "wss%s" % server[5:] @@ -158,7 +157,7 @@ class PMS_Websocket(WebSocket): if state.PLEX_TOKEN: uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN sslopt = {} - if settings('sslverify') == "false": + if utils.settings('sslverify') == "false": sslopt["cert_reqs"] = CERT_NONE LOG.debug("%s: Uri: %s, sslopt: %s", self.__class__.__name__, uri, sslopt) @@ -206,7 +205,7 @@ class Alexa_Websocket(WebSocket): """ Websocket connection to talk to Amazon Alexa. - Can't use thread_methods! + Can't use utils.thread_methods! """ thread_stopped = False thread_suspended = False @@ -244,9 +243,9 @@ class Alexa_Websocket(WebSocket): LOG.error('%s: Could not parse Alexa message', self.__class__.__name__) return - process_command(message.attrib['path'][1:], message.attrib) + companion.process_command(message.attrib['path'][1:], message.attrib) - # Path in thread_methods + # Path in utils.thread_methods def stop(self): self.thread_stopped = True diff --git a/service.py b/service.py index 5b62d549..39657f3b 100644 --- a/service.py +++ b/service.py @@ -1,298 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################### -from __future__ import unicode_literals # nice to fix os.walk unicode -from logging import getLogger -from os import path as os_path -from sys import path as sys_path, argv - -from xbmc import translatePath, Monitor -from xbmcaddon import Addon - -############################################################################### - -_ADDON = Addon(id='plugin.video.plexkodiconnect') -try: - _ADDON_PATH = _ADDON.getAddonInfo('path').decode('utf-8') -except TypeError: - _ADDON_PATH = _ADDON.getAddonInfo('path').decode() -try: - _BASE_RESOURCE = translatePath(os_path.join( - _ADDON_PATH, - 'resources', - 'lib')).decode('utf-8') -except TypeError: - _BASE_RESOURCE = translatePath(os_path.join( - _ADDON_PATH, - 'resources', - 'lib')).decode() -sys_path.append(_BASE_RESOURCE) - -############################################################################### - -from utils import settings, window, language as lang, dialog -from userclient import UserClient -import initialsetup -from kodimonitor import KodiMonitor, SpecialMonitor -from librarysync import LibrarySync -from websocket_client import PMS_Websocket, Alexa_Websocket - -from PlexFunctions import check_connection -from PlexCompanion import PlexCompanion -from command_pipeline import Monitor_Window -from playback_starter import PlaybackStarter -from playqueue import PlayqueueMonitor -from artwork import Image_Cache_Thread -import variables as v -import state - -############################################################################### -import loghandler - -loghandler.config() -LOG = getLogger("PLEX.service") -############################################################################### +from __future__ import absolute_import, division, unicode_literals +from resources.lib import service_entry -class Service(): - - server_online = True - warn_auth = True - - user = None - ws = None - library = None - plexCompanion = None - - user_running = False - ws_running = False - alexa_running = False - library_running = False - plexCompanion_running = False - kodimonitor_running = False - playback_starter_running = False - image_cache_thread_running = False - - def __init__(self): - # Initial logging - LOG.info("======== START %s ========", v.ADDON_NAME) - LOG.info("Platform: %s", v.PLATFORM) - LOG.info("KODI Version: %s", v.KODILONGVERSION) - LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION) - LOG.info("PKC Direct Paths: %s", settings('useDirectPaths') == '1') - LOG.info("Number of sync threads: %s", settings('syncThreadNumber')) - LOG.info("Full sys.argv received: %s", argv) - self.monitor = Monitor() - # Load/Reset PKC entirely - important for user/Kodi profile switch - initialsetup.reload_pkc() - - def __stop_PKC(self): - """ - Kodi's abortRequested is really unreliable :-( - """ - return self.monitor.abortRequested() or state.STOP_PKC - - def ServiceEntryPoint(self): - # Important: Threads depending on abortRequest will not trigger - # if profile switch happens more than once. - __stop_PKC = self.__stop_PKC - monitor = self.monitor - kodiProfile = v.KODI_PROFILE - - # Server auto-detect - initialsetup.InitialSetup().setup() - - # Detect playback start early on - self.command_pipeline = Monitor_Window() - self.command_pipeline.start() - - # Initialize important threads, handing over self for callback purposes - self.user = UserClient() - self.ws = PMS_Websocket() - self.alexa = Alexa_Websocket() - self.library = LibrarySync() - self.plexCompanion = PlexCompanion() - self.specialMonitor = SpecialMonitor() - self.playback_starter = PlaybackStarter() - self.playqueue = PlayqueueMonitor() - if settings('enableTextureCache') == "true": - self.image_cache_thread = Image_Cache_Thread() - - welcome_msg = True - counter = 0 - while not __stop_PKC(): - - if window('plex_kodiProfile') != kodiProfile: - # Profile change happened, terminate this thread and others - LOG.info("Kodi profile was: %s and changed to: %s. " - "Terminating old PlexKodiConnect thread.", - kodiProfile, window('plex_kodiProfile')) - break - - # Before proceeding, need to make sure: - # 1. Server is online - # 2. User is set - # 3. User has access to the server - - if 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 not self.kodimonitor_running: - # Start up events - self.warn_auth = True - if welcome_msg is True: - # Reset authentication warnings - welcome_msg = False - dialog('notification', - lang(29999), - "%s %s" % (lang(33000), - self.user.user), - icon='{plex}', - time=2000, - sound=False) - # Start monitoring kodi events - self.kodimonitor_running = 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 - settings('enable_alexa') == 'true'): - self.alexa_running = True - self.alexa.start() - # Start the syncing thread - if not self.library_running: - self.library_running = True - self.library.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() - if (not self.image_cache_thread_running and - settings('enableTextureCache') == "true"): - self.image_cache_thread_running = True - self.image_cache_thread.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 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 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 - window('plex_online', value="false") - # Suspend threads - state.SUSPEND_LIBRARY_THREAD = True - LOG.error("Plex Media Server went offline") - if settings('show_pms_offline') == 'true': - dialog('notification', - lang(33001), - "%s %s" % (lang(29999), 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 - 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 - settings('show_pms_offline') == 'true'): - dialog('notification', - lang(29999), - lang(33003), - icon='{plex}', - time=5000, - sound=False) - LOG.info("Server %s is online and ready.", server) - window('plex_online', value="true") - if state.AUTHENTICATED: - # Server got offline when we were authenticated. - # Hence resume threads - state.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): - # 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 - window('plex_service_started', clear=True) - LOG.info("======== STOP %s ========", v.ADDON_NAME) - - -# Safety net - Kody starts PKC twice upon first installation! -if window('plex_service_started') == 'true': - EXIT = True -else: - window('plex_service_started', value='true') - EXIT = False - -# Delay option -DELAY = int(settings('startupDelay')) - -LOG.info("Delaying Plex startup by: %s sec...", DELAY) -if EXIT: - LOG.error('PKC service.py already started - exiting this instance') -elif DELAY and Monitor().waitForAbort(DELAY): - # Start the service - LOG.info("Abort requested while waiting. PKC not started.") -else: - Service().ServiceEntryPoint() +if __name__ == "__main__": + service_entry.start()