Rather big change of PKC's plumbing

This commit is contained in:
croneter 2018-11-18 14:59:17 +01:00
parent b2615c19bd
commit a045063769
53 changed files with 1246 additions and 1663 deletions

View file

@ -94,9 +94,6 @@ class Main():
elif mode == 'togglePlexTV':
entrypoint.toggle_plex_tv_sign_in()
elif mode == 'resetauth':
entrypoint.reset_authorization()
elif mode == 'passwords':
utils.passwords_xml()
@ -111,14 +108,14 @@ class Main():
else:
if mode == 'repair':
log.info('Requesting repair lib sync')
utils.plex_command('RUN_LIB_SCAN', 'repair')
utils.plex_command('repair-scan')
elif mode == 'manualsync':
log.info('Requesting full library scan')
utils.plex_command('RUN_LIB_SCAN', 'full')
utils.plex_command('full-scan')
elif mode == 'texturecache':
log.info('Requesting texture caching of all textures')
utils.plex_command('RUN_LIB_SCAN', 'textures')
utils.plex_command('textures-scan')
elif mode == 'chooseServer':
entrypoint.choose_pms_server()
@ -128,7 +125,7 @@ class Main():
elif mode == 'fanart':
log.info('User requested fanarttv refresh')
utils.plex_command('RUN_LIB_SCAN', 'fanart')
utils.plex_command('fanart-scan')
elif '/extrafanart' in path:
plexpath = arguments[1:]
@ -158,7 +155,7 @@ class Main():
"""
request = '%s&handle=%s' % (argv[2], HANDLE)
# Put the request into the 'queue'
utils.plex_command('PLAY', request)
utils.plex_command('PLAY-%s' % request)
if HANDLE == -1:
# Handle -1 received, not waiting for main thread
return

View file

@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Used to save PKC's application state and share between modules. Be careful
if you invoke another PKC Python instance (!!) when e.g. PKC.movies is called
"""
from __future__ import absolute_import, division, unicode_literals
from .account import Account
from .application import App
from .connection import Connection
from .libsync import Sync
from .playstate import PlayState
ACCOUNT = None
APP = None
CONN = None
SYNC = None
PLAYSTATE = None
def init():
global ACCOUNT, APP, CONN, SYNC, PLAYSTATE
ACCOUNT = Account()
APP = App()
CONN = Connection()
SYNC = Sync()
PLAYSTATE = PlayState()

View file

@ -0,0 +1,86 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .. import utils
LOG = getLogger('PLEX.account')
class Account(object):
def __init__(self):
# Along with window('plex_authenticated')
self.authenticated = False
self._session = None
utils.window('plex_authenticated', clear=True)
self.load()
def set_authenticated(self):
self.authenticated = True
utils.window('plex_authenticated', value='true')
# Start download session
from .. import downloadutils
self._session = downloadutils.DownloadUtils()
self._session.startSession(reset=True)
def set_unauthenticated(self):
self.authenticated = False
utils.window('plex_authenticated', clear=True)
def load(self):
LOG.debug('Loading account settings')
# plex.tv username
self.plex_username = utils.settings('username') or None
# Plex ID of that user (e.g. for plex.tv) as a STRING
self.plex_user_id = utils.settings('userid') or None
# Token for that user for plex.tv
self.plex_token = utils.settings('plexToken') or None
# Plex token for the active PMS for the active user
# (might be diffent to plex_token)
self.pms_token = utils.settings('accessToken') or None
self.avatar = utils.settings('plexAvatar') or None
self.myplexlogin = utils.settings('myplexlogin') == 'true'
# Plex home user? Then "False"
self.restricted_user = True \
if utils.settings('plex_restricteduser') == 'true' else False
# Force user to enter Pin if set?
self.force_login = utils.settings('enforceUserLogin') == 'true'
# Also load these settings to Kodi window variables - they'll be
# available for other PKC Python instances
utils.window('plex_restricteduser',
value='true' if self.restricted_user else 'false')
utils.window('plex_token', value=self.plex_token or '')
utils.window('pms_token', value=self.pms_token or '')
utils.window('plexAvatar', value=self.avatar or '')
LOG.debug('Loaded user %s, %s with plex token %s... and pms token %s...',
self.plex_username, self.plex_user_id,
self.plex_token[:5] if self.plex_token else None,
self.pms_token[:5] if self.pms_token else None)
LOG.debug('User is restricted Home user: %s', self.restricted_user)
def clear(self):
LOG.debug('Clearing account settings')
self.plex_username = None
self.plex_user_id = None
self.plex_token = None
self.pms_token = None
self.avatar = None
self.restricted_user = None
self.authenticated = False
self._session = None
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('plex_restricteduser', value='')
utils.settings('plexToken', value='')
utils.settings('accessToken', value='')
utils.settings('plexAvatar', value='')
utils.window('plex_restricteduser', clear=True)
utils.window('plex_token', clear=True)
utils.window('pms_token', clear=True)
utils.window('plexAvatar', clear=True)
utils.window('plex_authenticated', clear=True)

View file

@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import Queue
from threading import Lock, RLock
from .. import utils
class App(object):
"""
This class is used to store variables across PKC modules
"""
def __init__(self, only_reload_settings=False):
self.load_settings()
if only_reload_settings:
return
# Quit PKC?
self.stop_pkc = False
# Need to lock all methods and functions messing with Plex Companion subscribers
self.lock_subscriber = RLock()
# Need to lock everything messing with Kodi/PKC playqueues
self.lock_playqueues = RLock()
# Necessary to temporarily hold back librarysync/websocket listener when doing
# a full sync
self.lock_playlists = Lock()
# Plex Companion Queue()
self.companion_queue = Queue.Queue(maxsize=100)
# Command Pipeline Queue()
self.command_pipeline_queue = Queue.Queue()
# Websocket_client queue to communicate with librarysync
self.websocket_queue = Queue.Queue()
def load_settings(self):
# Number of items to fetch and display in widgets
self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number'))
# Hack to force Kodi widget for "in progress" to show up if it was empty
# before
self.force_reload_skin = utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
# Stemming from the PKC settings.xml
self.kodi_plex_time_offset = float(utils.settings('kodiplextimeoffset'))

View file

@ -0,0 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .. import utils, json_rpc as js
LOG = getLogger('PLEX.connection')
class Connection(object):
def __init__(self, only_reload_settings=False):
self.load_webserver()
self.load()
if only_reload_settings:
return
# TODO: Delete
self.pms_server = None
# Plex Media Server Status - along with window('plex_serverStatus')
# Values:
# 'Stop': set if e.g.
# '401': Token has been revoked - PKC yet to delete tokens
# 'Auth':
self.pms_status = False
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
self.plex_transient_token = None
def load_webserver(self):
"""
PKC needs Kodi webserver to work correctly
"""
LOG.debug('Loading Kodi webserver details')
# Kodi webserver details
if js.get_setting('services.webserver') in (None, False):
# Enable the webserver, it is disabled
js.set_setting('services.webserver', True)
self.webserver_host = 'localhost'
self.webserver_port = js.get_setting('services.webserverport')
self.webserver_username = js.get_setting('services.webserverusername')
self.webserver_password = js.get_setting('services.webserverpassword')
def load(self):
LOG.debug('Loading connection settings')
# Shall we verify SSL certificates? "None" will leave SSL enabled
self.verify_ssl_cert = None if utils.settings('sslverify') == 'true' \
else False
# Do we have an ssl certificate for PKC we need to use?
self.ssl_cert_path = utils.settings('sslcert') \
if utils.settings('sslcert') != 'None' else None
self.machine_identifier = utils.settings('plex_machineIdentifier') or None
self.server_name = utils.settings('plex_servername') or None
self.https = utils.settings('https') == 'true'
self.host = utils.settings('ipaddress') or None
self.port = int(utils.settings('port')) if utils.settings('port') else None
if not self.host:
self.server = None
elif self.https:
self.server = 'https://%s:%s' % (self.host, self.port)
else:
self.server = 'http://%s:%s' % (self.host, self.port)
utils.window('pms_server', value=self.server)
LOG.debug('Set server %s (%s) to %s',
self.server_name, self.machine_identifier, self.server)
def clear(self):
LOG.debug('Clearing connection settings')
self.machine_identifier = None
self.server_name = None
self.http = None
self.host = None
self.port = None
self.server = None
utils.window('pms_server', clear=True)

View file

@ -0,0 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from .. import utils
class Sync(object):
def __init__(self, only_reload_settings=False):
self.load_settings()
if only_reload_settings:
return
# Do we need to run a special library scan?
self.run_lib_scan = None
# Usually triggered by another Python instance - will have to be set (by
# polling window) through e.g. librarysync thread
self.suspend_library_thread = False
# Set if user decided to cancel sync
self.stop_sync = False
# Set during media playback if PKC should not do any syncs. Will NOT
# suspend synching of playstate progress
self.suspend_sync = False
# Could we access the paths?
self.path_verified = False
# Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true'
self.db_scan = False
def load_settings(self):
# Direct Paths (True) or Addon Paths (False)? Along with
# window('useDirectPaths')
self.direct_paths = True if utils.settings('useDirectPaths') == '1' \
else False
# Is synching of Plex music enabled?
self.enable_music = utils.settings('enableMusic') == 'true'
# Path remapping mechanism (e.g. smb paths)
# Do we replace \\myserver\path to smb://myserver/path?
self.replace_smb_path = utils.settings('replaceSMB') == 'true'
# Do we generally remap?
self.remap_path = utils.settings('remapSMB') == 'true'
# Mappings for REMAP_PATH:
self.remapSMBmovieOrg = utils.settings('remapSMBmovieOrg')
self.remapSMBmovieNew = utils.settings('remapSMBmovieNew')
self.remapSMBtvOrg = utils.settings('remapSMBtvOrg')
self.remapSMBtvNew = utils.settings('remapSMBtvNew')
self.remapSMBmusicOrg = utils.settings('remapSMBmusicOrg')
self.remapSMBmusicNew = utils.settings('remapSMBmusicNew')
self.remapSMBphotoOrg = utils.settings('remapSMBphotoOrg')
self.remapSMBphotoNew = utils.settings('remapSMBphotoNew')
# Shall we replace custom user ratings with the number of versions available?
self.indicate_media_versions = True \
if utils.settings('indicate_media_versions') == "true" else False
# Will sync movie trailer differently: either play trailer directly or show
# all the Plex extras for the user to choose
self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true'
# Only sync specific Plex playlists to Kodi?
self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true'
# Only sync specific Kodi playlists to Plex?
self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true'
# Shall we show Kodi dialogs when synching?
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
# How often shall we sync?
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
# Background Sync disabled?
self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false'
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
# How many threads to download Plex metadata on sync?
self.sync_thread_number = int(utils.settings('syncThreadNumber'))
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left
# to sync)
self.image_sync_notifications = utils.settings('imageSyncNotifications') == 'true'

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
class PlayState(object):
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
template = {
'type': None,
'time': {
'hours': 0,
'minutes': 0,
'seconds': 0,
'milliseconds': 0},
'totaltime': {
'hours': 0,
'minutes': 0,
'seconds': 0,
'milliseconds': 0},
'speed': 0,
'shuffled': False,
'repeat': 'off',
'position': None,
'playlistid': None,
'currentvideostream': -1,
'currentaudiostream': -1,
'subtitleenabled': False,
'currentsubtitle': -1,
'file': None,
'kodi_id': None,
'kodi_type': None,
'plex_id': None,
'plex_type': None,
'container_key': None,
'volume': 100,
'muted': False,
'playmethod': None,
'playcount': None
}
def __init__(self):
# Kodi player states - here, initial values are set
self.player_states = {
0: {},
1: {},
2: {}
}
# The LAST playstate once playback is finished
self.old_player_states = {
0: {},
1: {},
2: {}
}
self.played_info = {}
# Set by SpecialMonitor - did user choose to resume playback or start from the
# beginning?
self.resume_playback = False
# Was the playback initiated by the user using the Kodi context menu?
self.context_menu_play = False
# Set by context menu - shall we force-transcode the next playing item?
self.force_transcode = False
# Which Kodi player is/has been active? (either int 1, 2 or 3)
self.active_players = set()
# Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set
# up our own playlist from the very beginning
self.pkc_caused_stop = False
# Flag if the 0 length PKC video has already failed so we can start resolving
# playback (set in player.py)
self.pkc_caused_stop_done = True

View file

@ -7,8 +7,7 @@ import requests
import xbmc
from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB
from . import backgroundthread, utils
from . import state
from . import app, backgroundthread, utils
LOG = getLogger('PLEX.artwork')
@ -19,13 +18,7 @@ requests.packages.urllib3.disable_warnings()
# download is successful
TIMEOUT = (35.1, 35.1)
IMAGE_CACHING_SUSPENDS = [
state.SUSPEND_LIBRARY_THREAD,
state.STOP_SYNC,
state.DB_SCAN
]
if not utils.settings('imageSyncDuringPlayback') == 'true':
IMAGE_CACHING_SUSPENDS.append(state.SUSPEND_SYNC)
IMAGE_CACHING_SUSPENDS = []
def double_urlencode(text):
@ -37,19 +30,9 @@ def double_urldecode(text):
class ImageCachingThread(backgroundthread.KillableThread):
def __init__(self):
self._canceled = False
super(ImageCachingThread, self).__init__()
def isCanceled(self):
return self._canceled or state.STOP_PKC
def isSuspended(self):
return any(IMAGE_CACHING_SUSPENDS)
def cancel(self):
self._canceled = True
@staticmethod
def _art_url_generator():
for kind in (KodiVideoDB, KodiMusicDB):
@ -88,18 +71,18 @@ def cache_url(url):
try:
requests.head(
url="http://%s:%s/image/image://%s"
% (state.WEBSERVER_HOST,
state.WEBSERVER_PORT,
% (app.CONN.webserver_host,
app.CONN.webserver_port,
url),
auth=(state.WEBSERVER_USERNAME,
state.WEBSERVER_PASSWORD),
auth=(app.CONN.webserver_username,
app.CONN.webserver_password),
timeout=TIMEOUT)
except requests.Timeout:
# We don't need the result, only trigger Kodi to start the
# download. All is well
break
except requests.ConnectionError:
if state.STOP_PKC:
if app.APP.stop_pkc:
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')

View file

@ -14,7 +14,6 @@ LOG = getLogger('PLEX.' + __name__)
class KillableThread(threading.Thread):
pass
'''A thread class that supports raising exception in the thread from
another thread.
'''
@ -77,6 +76,45 @@ class KillableThread(threading.Thread):
# except KillThreadException:
# self.onKilled()
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
self._canceled = False
self._suspended = False
super(KillableThread, self).__init__(group, target, name, args, kwargs)
def isCanceled(self):
"""
Returns True if the thread is stopped
"""
if self._canceled or xbmc.abortRequested:
return True
return False
def abort(self):
"""
Call to stop this thread
"""
self._canceled = True
def suspend(self):
"""
Call to suspend this thread
"""
self._suspended = True
def resume(self):
"""
Call to revive a suspended thread back to life
"""
self._suspended = False
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
if self._suspended:
return True
return False
class Tasks(list):
def add(self, task):

View file

@ -1,68 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import logging
from threading import Thread
from xbmc import sleep
from . import utils
from . import state
###############################################################################
LOG = logging.getLogger('PLEX.command_pipeline')
###############################################################################
@utils.thread_methods
class Monitor_Window(Thread):
"""
Monitors window('plex_command') for new entries that we need to take care
of, e.g. for new plays initiated on the Kodi side with addon paths.
Adjusts state.py accordingly
"""
def run(self):
stopped = self.stopped
queue = state.COMMAND_PIPELINE_QUEUE
LOG.info("----===## Starting Kodi_Play_Client ##===----")
while not stopped():
if utils.window('plex_command'):
value = utils.window('plex_command')
utils.window('plex_command', clear=True)
if value.startswith('PLAY-'):
queue.put(value.replace('PLAY-', ''))
elif value == 'SUSPEND_LIBRARY_THREAD-True':
state.SUSPEND_LIBRARY_THREAD = True
elif value == 'SUSPEND_LIBRARY_THREAD-False':
state.SUSPEND_LIBRARY_THREAD = False
elif value == 'STOP_SYNC-True':
state.STOP_SYNC = True
elif value == 'STOP_SYNC-False':
state.STOP_SYNC = False
elif value == 'PMS_STATUS-Auth':
state.PMS_STATUS = 'Auth'
elif value == 'PMS_STATUS-401':
state.PMS_STATUS = '401'
elif value == 'SUSPEND_USER_CLIENT-True':
state.SUSPEND_USER_CLIENT = True
elif value == 'SUSPEND_USER_CLIENT-False':
state.SUSPEND_USER_CLIENT = False
elif value.startswith('PLEX_TOKEN-'):
state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None
elif value.startswith('PLEX_USERNAME-'):
state.PLEX_USERNAME = \
value.replace('PLEX_USERNAME-', '') or None
elif value.startswith('RUN_LIB_SCAN-'):
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
elif value.startswith('CONTEXT_menu?'):
queue.put('dummy?mode=context_menu&%s'
% value.replace('CONTEXT_menu?', ''))
elif value.startswith('NAVIGATE'):
queue.put(value.replace('NAVIGATE-', ''))
else:
raise NotImplementedError('%s not implemented' % value)
else:
sleep(50)
# Put one last item into the queue to let playback_starter end
queue.put(None)
LOG.info("----===## Kodi_Play_Client stopped ##===----")

View file

@ -7,11 +7,8 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from xbmc import Player
from . import playqueue as PQ
from . import plex_functions as PF
from . import json_rpc as js
from . import variables as v
from . import state
from . import playqueue as PQ, plex_functions as PF
from . import json_rpc as js, variables as v, app
###############################################################################
@ -68,12 +65,12 @@ def process_command(request_path, params):
if request_path == 'player/playback/playMedia':
# We need to tell service.py
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
state.COMPANION_QUEUE.put({
app.APP.companion_queue.put({
'action': action,
'data': params
})
elif request_path == 'player/playback/refreshPlayQueue':
state.COMPANION_QUEUE.put({
app.APP.companion_queue.put({
'action': 'refreshPlayQueue',
'data': params
})
@ -115,7 +112,7 @@ def process_command(request_path, params):
elif request_path == "player/navigation/back":
js.input_back()
elif request_path == "player/playback/setStreams":
state.COMPANION_QUEUE.put({
app.APP.companion_queue.put({
'action': 'setStreams',
'data': params
})

View file

@ -43,8 +43,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
return self.selected_option
def onInit(self):
if utils.window('PlexUserImage'):
self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage'))
if utils.window('plexAvatar'):
self.getControl(USER_IMAGE).setImage(utils.window('plexAvatar'))
height = 479 + (len(self._options) * 55)
LOG.debug("options: %s", self._options)
self.list_ = self.getControl(LIST)

View file

@ -8,7 +8,7 @@ import xbmcgui
from .plex_api import API
from .plex_db import PlexDB
from . import context, plex_functions as PF, playqueue as PQ
from . import utils, variables as v, state
from . import utils, variables as v, app
###############################################################################
@ -84,7 +84,7 @@ class ContextMenu(object):
# if user uses direct paths, give option to initiate playback via PMS
if self.api and self.api.extras():
options.append(OPTIONS['Extras'])
if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES:
if app.PLAYSTATE.direct_paths and self.kodi_type in v.KODI_VIDEOTYPES:
options.append(OPTIONS['PMS_Play'])
if self.kodi_type in v.KODI_VIDEOTYPES:
options.append(OPTIONS['Transcode'])
@ -112,7 +112,7 @@ class ContextMenu(object):
"""
selected = self._selected_option
if selected == OPTIONS['Transcode']:
state.FORCE_TRANSCODE = True
app.PLAYSTATE.force_transcode = True
self._PMS_play()
elif selected == OPTIONS['PMS_Play']:
self._PMS_play()
@ -146,7 +146,7 @@ class ContextMenu(object):
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
playqueue.clear()
state.CONTEXT_MENU_PLAY = True
app.PLAYSTATE.context_menu_play = True
handle = self.api.path(force_first_media=False, force_addon=True)
xbmc.executebuiltin('RunPlugin(%s)' % handle)

View file

@ -4,9 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import requests
from . import utils
from . import clientinfo
from . import state
from . import utils, clientinfo, app
###############################################################################
@ -14,7 +12,7 @@ from . import state
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
LOG = getLogger('PLEX.downloadutils')
LOG = getLogger('PLEX.download')
###############################################################################
@ -39,25 +37,16 @@ class DownloadUtils():
def __init__(self):
self.__dict__ = self._shared_state
def setServer(self, server):
"""
Reserved for userclient only
"""
self.server = server
LOG.debug("Set server: %s", server)
def setSSL(self, verifySSL=None, certificate=None):
"""
Reserved for userclient only
verifySSL must be 'true' to enable certificate validation
certificate must be path to certificate or 'None'
"""
if verifySSL is None:
verifySSL = state.VERIFY_SSL_CERT
verifySSL = app.CONN.verify_ssl_cert
if certificate is None:
certificate = state.SSL_CERT_PATH
certificate = app.CONN.ssl_cert_path
# Set the session's parameters
self.s.verify = verifySSL
if certificate:
@ -67,8 +56,7 @@ class DownloadUtils():
def startSession(self, reset=False):
"""
User should be authenticated when this method is called (via
userclient)
User should be authenticated when this method is called
"""
# Start session
self.s = requests.Session()
@ -80,9 +68,6 @@ class DownloadUtils():
# Set SSL settings
self.setSSL()
# Set other stuff
self.setServer(utils.window('pms_server'))
# Counters to declare PMS dead or unauthorized
# Use window variables because start of movies will be called with a
# new plugin instance - it's impossible to share data otherwise
@ -94,7 +79,7 @@ class DownloadUtils():
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
LOG.info("Requests session started on: %s", self.server)
LOG.info("Requests session started on: %s", app.CONN.server)
def stopSession(self):
try:
@ -155,7 +140,7 @@ class DownloadUtils():
self.startSession()
s = self.s
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{server}", app.CONN.server)
else:
# User is not (yet) authenticated. Used to communicate with
# plex.tv and to check for PMS servers
@ -164,9 +149,9 @@ class DownloadUtils():
headerOptions = self.getHeader(options=headerOptions)
else:
headerOptions = headerOverride
kwargs['verify'] = state.VERIFY_SSL_CERT
if state.SSL_CERT_PATH:
kwargs['cert'] = state.SSL_CERT_PATH
kwargs['verify'] = app.CONN.verify_ssl_cert
if app.CONN.ssl_cert_path:
kwargs['cert'] = app.CONN.ssl_cert_path
# Set the variables we were passed (fallback to request session
# otherwise - faster)
@ -252,12 +237,11 @@ class DownloadUtils():
self.unauthorizedAttempts):
LOG.warn('We seem to be truly unauthorized for PMS'
' %s ', url)
if state.PMS_STATUS not in ('401', 'Auth'):
# Tell userclient token has been revoked.
if app.CONN.pms_status not in ('401', 'Auth'):
# Tell others token has been revoked.
LOG.debug('Setting PMS server status to '
'unauthorized')
state.PMS_STATUS = '401'
utils.window('plex_serverStatus', value="401")
app.CONN.pms_status = '401'
utils.dialog('notification',
utils.lang(29999),
utils.lang(30017),

View file

@ -20,8 +20,8 @@ from .plex_api import API
from . import plex_functions as PF
from . import json_rpc as js
from . import variables as v
# Be careful - your using state in another Python instance!
from . import state
# Be careful - your using app in another Python instance!
from . import app
###############################################################################
LOG = getLogger('PLEX.entrypoint')
@ -34,34 +34,7 @@ def choose_pms_server():
Lets user choose from list of PMS
"""
LOG.info("Choosing PMS server requested, starting")
setup = initialsetup.InitialSetup()
server = setup.pick_pms(showDialog=True)
if server is None:
LOG.error('We did not connect to a new PMS, aborting')
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
return
LOG.info("User chose server %s", server['name'])
setup.write_pms_to_settings(server)
if not _log_out():
return
# Wipe Kodi and Plex database as well as playlists and video nodes
utils.wipe_database()
# Log in again
_log_in()
LOG.info("Choosing new PMS complete")
# '<PMS> connected'
utils.dialog('notification',
utils.lang(29999),
'%s %s' % (server['name'], utils.lang(39220)),
icon='{plex}',
time=3000,
sound=False)
utils.plex_command('choose_pms_server')
def toggle_plex_tv_sign_in():
@ -69,37 +42,8 @@ def toggle_plex_tv_sign_in():
Signs out of Plex.tv if there was a token saved and thus deletes the token.
Or signs in to plex.tv if the user was not logged in before.
"""
if utils.settings('plexToken'):
LOG.info('Reseting plex.tv credentials in settings')
utils.settings('plexLogin', value="")
utils.settings('plexToken', value="")
utils.settings('plexid', value="")
utils.settings('plexAvatar', value="")
utils.settings('plex_status', value=utils.lang(39226))
utils.window('plex_token', clear=True)
utils.plex_command('PLEX_TOKEN', '')
utils.plex_command('PLEX_USERNAME', '')
else:
LOG.info('Login to plex.tv')
initialsetup.InitialSetup().plex_tv_sign_in()
utils.dialog('notification',
utils.lang(29999),
utils.lang(39221),
icon='{plex}',
time=3000,
sound=False)
def reset_authorization():
"""
User tried login and failed too many times. Reset # of logins
"""
if utils.yesno_dialog(utils.lang(29999), utils.lang(39206)):
LOG.info("Reset login attempts.")
utils.plex_command('PMS_STATUS', 'Auth')
else:
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
LOG.info('Toggle of Plex.tv sign-in requested')
utils.plex_command('toggle_plex_tv_sign_in')
def directory_item(label, path, folder=True):
@ -185,13 +129,7 @@ def switch_plex_user():
# position = 0
# utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
LOG.info("Plex home user switch requested")
if not _log_out():
return
# First remove playlists of old user
utils.delete_playlists()
# Remove video nodes
utils.delete_nodes()
_log_in()
utils.plex_command('switch_plex_user')
def create_listitem(item, append_show_title=False, append_sxxexx=False):
@ -573,13 +511,13 @@ def on_deck_episodes(viewid, tagname, limit):
return
# We're using another python instance - need to load some vars
if utils.settings('useDirectPaths') == '1':
state.DIRECT_PATHS = True
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
if state.REMAP_PATH:
app.SYNC.direct_paths = True
app.SYNC.replace_smb_path = utils.settings('replaceSMB') == 'true'
app.SYNC.remap_path = utils.settings('remapSMB') == 'true'
if app.SYNC.remap_path:
initialsetup.set_replace_paths()
# Let's NOT check paths for widgets!
state.PATH_VERIFIED = True
app.SYNC.path_verified = True
counter = 0
for item in xml:
api = API(item)
@ -964,107 +902,5 @@ def create_new_pms():
"""
Opens dialogs for the user the plug in the PMS details
"""
# "Enter your Plex Media Server's IP or URL. Examples are:"
utils.messageDialog(utils.lang(29999),
'%s\n%s\n%s' % (utils.lang(39215),
'192.168.1.2',
'plex.myServer.org'))
address = utils.dialog('input', "Enter PMS IP or URL")
if address == '':
return
port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}')
if port == '':
return
url = '%s:%s' % (address, port)
# "Does your Plex Media Server support SSL connections?
# (https instead of http)"
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
if https:
url = 'https://%s' % url
else:
url = 'http://%s' % url
https = 'true' if https else 'false'
machine_identifier = PF.GetMachineIdentifier(url)
if machine_identifier is None:
# "Error contacting url
# Abort (Yes) or save address anyway (No)"
if utils.yesno_dialog(utils.lang(29999),
'%s %s. %s' % (utils.lang(39218),
url,
utils.lang(39219))):
return
else:
utils.settings('plex_machineIdentifier', '')
else:
utils.settings('plex_machineIdentifier', machine_identifier)
LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s',
https, address, port, machine_identifier)
utils.settings('https', value=https)
utils.settings('ipaddress', value=address)
utils.settings('port', value=port)
# Chances are this is a local PMS, so disable SSL certificate check
utils.settings('sslverify', value='false')
# Sign out to trigger new login
if _log_out():
# Only login again if logout was successful
_log_in()
def _log_in():
"""
Resets (clears) window properties to enable (re-)login
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
out!
"""
utils.plex_command('RUN_LIB_SCAN', 'full')
# Restart user client
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
def _log_out():
"""
Finishes lib scans, logs out user.
Returns True if successfully signed out, False otherwise
"""
# Resetting, please wait
utils.dialog('notification',
utils.lang(29999),
utils.lang(39207),
icon='{plex}',
time=3000,
sound=False)
# Pause library sync thread
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'True')
# Wait max for 10 seconds for all lib scans to shutdown
counter = 0
while utils.window('plex_dbScan') == 'true':
if counter > 200:
# Failed to reset PMS and plex.tv connects. Try to restart Kodi.
utils.messageDialog(utils.lang(29999), utils.lang(39208))
# Resuming threads, just in case
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
LOG.error("Could not stop library sync, aborting")
return False
counter += 1
sleep(50)
LOG.debug("Successfully stopped library sync")
counter = 0
# Log out currently signed in user:
utils.window('plex_serverStatus', value='401')
utils.plex_command('PMS_STATUS', '401')
# Above method needs to have run its course! Hence wait
while utils.window('plex_serverStatus') == "401":
if counter > 100:
# 'Failed to reset PKC. Try to restart Kodi.'
utils.messageDialog(utils.lang(29999), utils.lang(39208))
LOG.error("Could not sign out user, aborting")
return False
counter += 1
sleep(50)
# Suspend the user client during procedure
utils.plex_command('SUSPEND_USER_CLIENT', 'True')
return True
LOG.info('Request to manually enter new PMS address')
utils.plex_command('enter_new_pms_address')

View file

@ -2,22 +2,18 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from Queue import Queue
from xbmc import executebuiltin, translatePath
from xbmc import executebuiltin
from . import utils
from .utils import etree
from . import path_ops
from . import migration
from .downloadutils import DownloadUtils as DU
from . import userclient
from . import clientinfo
from . import plex_functions as PF
from . import plex_tv
from . import json_rpc as js
from . import playqueue as PQ
from . import state
from . import app
from . import variables as v
###############################################################################
@ -30,96 +26,6 @@ if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
WINDOW_PROPERTIES = (
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
"plex_customplayqueue", "plex_playbackProps",
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
"countError", "countUnauthorized", "plex_restricteduser",
"plex_allows_mediaDeletion", "plex_command", "plex_result",
"plex_force_transcode_pix"
)
def reload_pkc():
"""
Will reload state.py entirely and then initiate some values from the Kodi
settings file
"""
LOG.info('Start (re-)loading PKC settings')
# Reset state.py
reload(state)
# Reset window props
for prop in WINDOW_PROPERTIES:
utils.window(prop, clear=True)
# Clear video nodes properties
from .library_sync import videonodes
videonodes.VideoNodes().clearProperties()
# Initializing
state.VERIFY_SSL_CERT = utils.settings('sslverify') == 'true'
state.SSL_CERT_PATH = utils.settings('sslcert') \
if utils.settings('sslcert') != 'None' else None
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
state.SYNC_DIALOG = utils.settings('dbSyncIndicator') == 'true'
state.ENABLE_MUSIC = utils.settings('enableMusic') == 'true'
state.BACKGROUND_SYNC_DISABLED = utils.settings(
'enableBackgroundSync') == 'false'
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
utils.settings('backgroundsync_saftyMargin'))
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
state.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset'))
state.FETCH_PMS_ITEM_NUMBER = utils.settings('fetch_pms_item_number')
state.FORCE_RELOAD_SKIN = \
utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
# Init some Queues()
state.COMMAND_PIPELINE_QUEUE = Queue()
state.COMPANION_QUEUE = Queue(maxsize=100)
state.WEBSOCKET_QUEUE = Queue()
set_replace_paths()
set_webserver()
# To detect Kodi profile switches
utils.window('plex_kodiProfile',
value=utils.try_decode(translatePath("special://profile")))
clientinfo.getDeviceId()
# Initialize the PKC playqueues
PQ.init_playqueues()
LOG.info('Done (re-)loading PKC settings')
def set_replace_paths():
"""
Sets our values for direct paths correctly (including using lower-case
protocols like smb:// and NOT SMB://)
"""
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
for arg in ('Org', 'New'):
key = 'remapSMB%s%s' % (typus, arg)
value = utils.settings(key)
if '://' in value:
protocol = value.split('://', 1)[0]
value = value.replace(protocol, protocol.lower())
setattr(state, key, value)
def set_webserver():
"""
Set the Kodi webserver details - used to set the texture cache
"""
if js.get_setting('services.webserver') in (None, False):
# Enable the webserver, it is disabled
js.set_setting('services.webserver', True)
# Set standard port and username
# set_setting('services.webserverport', 8080)
# set_setting('services.webserverusername', 'kodi')
# Webserver already enabled
state.WEBSERVER_PORT = js.get_setting('services.webserverport')
state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername')
state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword')
def _write_pms_settings(url, token):
"""
Sets certain settings for server by asking for the PMS' settings
@ -145,11 +51,8 @@ class InitialSetup(object):
"""
def __init__(self):
LOG.debug('Entering initialsetup class')
self.server = userclient.UserClient().get_server()
self.serverid = utils.settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = PF.GetPlexLoginFromSettings()
self.myplexlogin = plexdict['myplexlogin'] == 'true'
self.plex_login = plexdict['plexLogin']
self.plex_token = plexdict['plexToken']
self.plexid = plexdict['plexid']
@ -158,6 +61,48 @@ class InitialSetup(object):
if self.plex_token:
LOG.debug('Found a plex.tv token in the settings')
def enter_new_pms_address(self):
# "Enter your Plex Media Server's IP or URL. Examples are:"
utils.messageDialog(utils.lang(29999),
'%s\n%s\n%s' % (utils.lang(39215),
'192.168.1.2',
'plex.myServer.org'))
address = utils.dialog('input', "Enter PMS IP or URL")
if address == '':
return False
port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}')
if port == '':
return False
url = '%s:%s' % (address, port)
# "Does your Plex Media Server support SSL connections?
# (https instead of http)"
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
if https:
url = 'https://%s' % url
else:
url = 'http://%s' % url
https = 'true' if https else 'false'
machine_identifier = PF.GetMachineIdentifier(url)
if machine_identifier is None:
# "Error contacting url
# Abort (Yes) or save address anyway (No)"
if utils.yesno_dialog(utils.lang(29999),
'%s %s. %s' % (utils.lang(39218),
url,
utils.lang(39219))):
return False
else:
utils.settings('plex_machineIdentifier', '')
else:
utils.settings('plex_machineIdentifier', machine_identifier)
LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s',
https, address, port, machine_identifier)
utils.settings('https', value=https)
utils.settings('ipaddress', value=address)
utils.settings('port', value=port)
# Chances are this is a local PMS, so disable SSL certificate check
utils.settings('sslverify', value='false')
def plex_tv_sign_in(self):
"""
Signs (freshly) in to plex.tv (will be saved to file settings)
@ -227,26 +172,26 @@ class InitialSetup(object):
not set before
"""
answer = True
chk = PF.check_connection(self.server, verifySSL=False)
chk = PF.check_connection(app.CONN.server, verifySSL=False)
if chk is False:
LOG.warn('Could not reach PMS %s', self.server)
LOG.warn('Could not reach PMS %s', app.CONN.server)
answer = False
if answer is True and not self.serverid:
if answer is True and not app.CONN.machine_identifier:
LOG.info('No PMS machineIdentifier found for %s. Trying to '
'get the PMS unique ID', self.server)
self.serverid = PF.GetMachineIdentifier(self.server)
if self.serverid is None:
'get the PMS unique ID', app.CONN.server)
app.CONN.machine_identifier = PF.GetMachineIdentifier(app.CONN.server)
if app.CONN.machine_identifier is None:
LOG.warn('Could not retrieve machineIdentifier')
answer = False
else:
utils.settings('plex_machineIdentifier', value=self.serverid)
utils.settings('plex_machineIdentifier', value=app.CONN.machine_identifier)
elif answer is True:
temp_server_id = PF.GetMachineIdentifier(self.server)
if temp_server_id != self.serverid:
temp_server_id = PF.GetMachineIdentifier(app.CONN.server)
if temp_server_id != app.CONN.machine_identifier:
LOG.warn('The current PMS %s was expected to have a '
'unique machineIdentifier of %s. But we got '
'%s. Pick a new server to be sure',
self.server, self.serverid, temp_server_id)
app.CONN.server, app.CONN.machine_identifier, temp_server_id)
answer = False
return answer
@ -305,7 +250,7 @@ class InitialSetup(object):
"""
server = None
# If no server is set, let user choose one
if not self.server or not self.serverid:
if not app.CONN.server or not app.CONN.machine_identifier:
showDialog = True
if showDialog is True:
server = self._user_pick_pms()
@ -328,13 +273,13 @@ class InitialSetup(object):
if https_updated is False:
serverlist = PF.discover_pms(self.plex_token)
for item in serverlist:
if item.get('machineIdentifier') == self.serverid:
if item.get('machineIdentifier') == app.CONN.machine_identifier:
server = item
if server is None:
name = utils.settings('plex_servername')
LOG.warn('The PMS you have used before with a unique '
'machineIdentifier of %s and name %s is '
'offline', self.serverid, name)
'offline', app.CONN.machine_identifier, name)
return
chk = self._check_pms_connectivity(server)
if chk == 504 and https_updated is False:
@ -535,7 +480,8 @@ class InitialSetup(object):
# Do we need to migrate stuff?
migration.check_migration()
# Reload the server IP cause we might've deleted it during migration
self.server = userclient.UserClient().get_server()
app.CONN.load()
app.CONN.server = app.CONN.server
# Display a warning if Kodi puts ALL movies into the queue, basically
# breaking playback reporting for PKC
@ -556,19 +502,19 @@ class InitialSetup(object):
# If a Plex server IP has already been set
# return only if the right machine identifier is found
if self.server:
LOG.info("PMS is already set: %s. Checking now...", self.server)
if app.CONN.server:
LOG.info("PMS is already set: %s. Checking now...", app.CONN.server)
if self.check_existing_pms():
LOG.info("Using PMS %s with machineIdentifier %s",
self.server, self.serverid)
_write_pms_settings(self.server, self.pms_token)
app.CONN.server, app.CONN.machine_identifier)
_write_pms_settings(app.CONN.server, self.pms_token)
if reboot is True:
utils.reboot_kodi()
return
# If not already retrieved myplex info, optionally let user sign in
# to plex.tv. This DOES get called on very first install run
if not self.plex_token and self.myplexlogin:
if not self.plex_token and app.ACCOUNT.myplexlogin:
self.plex_tv_sign_in()
server = self.pick_pms()

View file

@ -7,7 +7,7 @@ from ntpath import dirname
from ..plex_api import API
from ..plex_db import PlexDB
from ..kodi_db import KodiVideoDB
from .. import utils
from .. import utils, timing
LOG = getLogger('PLEX.itemtypes.common')
@ -135,5 +135,5 @@ class ItemBase(object):
resume,
duration,
view_count,
utils.unix_date_to_kodi(lastViewedAt),
timing.plex_date_to_kodi(lastViewedAt),
plex_type)

View file

@ -5,7 +5,7 @@ from logging import getLogger
from .common import ItemBase
from ..plex_api import API
from .. import state, variables as v, plex_functions as PF
from .. import app, variables as v, plex_functions as PF
LOG = getLogger('PLEX.movies')
@ -50,8 +50,8 @@ class Movie(ItemBase):
studios = api.music_studio_list()
# GET THE FILE AND PATH #####
do_indirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.file_path(force_first_media=True)
if playurl is None:

View file

@ -7,7 +7,7 @@ from .common import ItemBase
from ..plex_api import API
from ..plex_db import PlexDB
from ..kodi_db import KodiMusicDB
from .. import plex_functions as PF, utils, state, variables as v
from .. import plex_functions as PF, utils, timing, app, variables as v
LOG = getLogger('PLEX.music')
@ -165,12 +165,11 @@ class Artist(MusicMixin, ItemBase):
# Kodi doesn't allow that. In case that happens we just merge the
# artist entries.
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
# Create the reference in plex table
self.kodidb.update_artist(api.list_to_string(api.genre_list()),
api.plot(),
thumb,
fanart,
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
kodi_id)
# Update artwork
self.kodidb.modify_artwork(artworks,
@ -256,7 +255,7 @@ class Album(MusicMixin, ItemBase):
thumb,
api.music_studio(),
userdata['UserRating'],
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'album',
kodi_id)
else:
@ -270,7 +269,7 @@ class Album(MusicMixin, ItemBase):
thumb,
api.music_studio(),
userdata['UserRating'],
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'album',
kodi_id)
# OR ADD THE ALBUM #####
@ -289,7 +288,7 @@ class Album(MusicMixin, ItemBase):
thumb,
api.music_studio(),
userdata['UserRating'],
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'album')
else:
self.kodidb.add_album_17(kodi_id,
@ -303,7 +302,7 @@ class Album(MusicMixin, ItemBase):
thumb,
api.music_studio(),
userdata['UserRating'],
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'album')
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
self.kodidb.add_discography(artist_id, name, api.year())
@ -397,7 +396,7 @@ class Song(MusicMixin, ItemBase):
None,
None,
None,
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'single')
else:
self.kodidb.add_album_17(kodi_id,
@ -411,7 +410,7 @@ class Song(MusicMixin, ItemBase):
None,
None,
None,
utils.unix_date_to_kodi(self.last_sync),
timing.unix_date_to_kodi(self.last_sync),
'single')
else:
album = self.plexdb.album(album_id)
@ -469,8 +468,8 @@ class Song(MusicMixin, ItemBase):
mood = api.list_to_string(moods)
# GET THE FILE AND PATH #####
do_indirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.file_path(force_first_media=True)
if playurl is None:
@ -489,8 +488,7 @@ class Song(MusicMixin, ItemBase):
path = playurl.replace(filename, "")
if do_indirect:
# Plex works a bit differently
path = "%s%s" % (utils.window('pms_server'),
xml[0][0].get('key'))
path = "%s%s" % (app.CONN.server, xml[0][0].get('key'))
path = api.attach_plex_token_to_url(path)
filename = path.rsplit('/', 1)[1]
path = path.replace(filename, '')

View file

@ -5,7 +5,7 @@ from logging import getLogger
from .common import ItemBase, process_path
from ..plex_api import API
from .. import plex_functions as PF, state, variables as v
from .. import plex_functions as PF, app, variables as v
LOG = getLogger('PLEX.tvshows')
@ -135,7 +135,7 @@ class Show(ItemBase, TvShowMixin):
studio = api.list_to_string(studios)
# GET THE FILE AND PATH #####
if state.DIRECT_PATHS:
if app.SYNC.direct_paths:
# Direct paths is set the Kodi way
playurl = api.validate_playurl(api.tv_show_path(),
api.plex_type(),
@ -378,8 +378,8 @@ class Episode(ItemBase, TvShowMixin):
parent_id = season['kodi_id']
# GET THE FILE AND PATH #####
do_indirect = not state.DIRECT_PATHS
if state.DIRECT_PATHS:
do_indirect = not app.SYNC.direct_paths
if app.SYNC.direct_paths:
playurl = api.file_path(force_first_media=True)
if playurl is None:
do_indirect = True
@ -513,7 +513,7 @@ class Episode(ItemBase, TvShowMixin):
userdata['PlayCount'],
userdata['LastPlayedDate'],
None) # Do send None, we check here
if not state.DIRECT_PATHS:
if not app.SYNC.direct_paths:
# need to set a SECOND file entry for a path without plex show id
filename = api.file_name(force_first_media=True)
path = 'plugin://%s.tvshows/' % v.ADDON_ID

View file

@ -8,7 +8,7 @@ from __future__ import absolute_import, division, unicode_literals
from json import loads, dumps
from xbmc import executeJSONRPC
from . import utils
from . import timing
class JsonRPC(object):
@ -156,7 +156,7 @@ def seek_to(offset):
for playerid in get_player_ids():
JsonRPC("Player.Seek").execute(
{"playerid": playerid,
"value": utils.millis_to_kodi_time(offset)})
"value": timing.millis_to_kodi_time(offset)})
def smallforward():

View file

@ -7,7 +7,7 @@ from .video import KodiVideoDB
from .music import KodiMusicDB
from .texture import KodiTextureDB
from .. import path_ops, utils, variables as v
from .. import path_ops, utils, timing, variables as v
LOG = getLogger('PLEX.kodi_db')
@ -68,7 +68,7 @@ def setup_kodi_default_entries():
VALUES (?, ?, ?)
''', (v.DB_MUSIC_VERSION[v.KODIVERSION],
0,
utils.unix_date_to_kodi(utils.unix_timestamp())))
timing.kodi_now()))
def reset_cached_images():

View file

@ -5,7 +5,7 @@ from logging import getLogger
from sqlite3 import IntegrityError
from . import common
from .. import path_ops, utils, variables as v, state
from .. import path_ops, timing, variables as v, app
LOG = getLogger('PLEX.kodi_db.video')
@ -74,12 +74,11 @@ class KodiVideoDB(common.KodiDBBase):
if pathid is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
pathid = self.cursor.fetchone()[0] + 1
datetime = utils.unix_date_to_kodi(utils.unix_timestamp())
self.cursor.execute('''
INSERT INTO path(idPath, strPath, dateAdded)
VALUES (?, ?, ?)
''',
(pathid, parentpath, datetime))
(pathid, parentpath, timing.kodi_now()))
if parentpath != path:
# In case we end up having media in the filesystem root, C:\
parent_id = self.parent_path_id(parentpath)
@ -198,7 +197,7 @@ class KodiVideoDB(common.KodiDBBase):
Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for
add-on paths
"""
if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE:
if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE:
# Hack for the 2 entries for episodes for addon paths
self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1',
(file_id, ))
@ -598,7 +597,7 @@ class KodiVideoDB(common.KodiDBBase):
Adds a resume marker for a video library item. Will even set 2,
considering add-on path widget hacks.
"""
if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE:
if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE:
# Need to make sure to set a SECOND bookmark entry for another,
# second file_id that points to the path .tvshows instead of
# .tvshows/<plex show id/!

View file

@ -6,23 +6,16 @@ PKC Kodi Monitoring implementation
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from json import loads
from threading import Thread
import copy
import xbmc
from xbmcgui import Window
from .plex_db import PlexDB
from . import kodi_db
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
from . import utils, timing, plex_functions as PF, playback, initialsetup
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
from . import backgroundthread, app, variables as v
###############################################################################
@ -68,9 +61,9 @@ class KodiMonitor(xbmc.Monitor):
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)
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
for playerid in app.PLAYSTATE.player_states:
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
LOG.info("Kodi monitor started.")
def onScanStarted(self, library):
@ -142,8 +135,8 @@ class KodiMonitor(xbmc.Monitor):
self.hack_replay = None
if method == "Player.OnPlay":
state.SUSPEND_SYNC = True
with state.LOCK_PLAYQUEUES:
app.SYNC.suspend_sync = True
with app.APP.lock_playqueues:
self.PlayBackStart(data)
elif method == "Player.OnStop":
# Should refresh our video nodes, e.g. on deck
@ -152,27 +145,27 @@ class KodiMonitor(xbmc.Monitor):
self.hack_replay == data['item']):
# Hack for add-on paths
self.hack_replay = None
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._hack_addon_paths_replay_video()
elif data.get('end'):
if state.PKC_CAUSED_STOP is True:
state.PKC_CAUSED_STOP = False
if app.PLAYSTATE.pkc_caused_stop is True:
app.PLAYSTATE.pkc_caused_stop = False
LOG.debug('PKC caused this playback stop - ignoring')
else:
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
_playback_cleanup(ended=True)
else:
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
_playback_cleanup()
state.PKC_CAUSED_STOP_DONE = True
state.SUSPEND_SYNC = False
app.PLAYSTATE.pkc_caused_stop_done = True
app.SYNC.suspend_sync = False
elif method == 'Playlist.OnAdd':
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._playlist_onadd(data)
elif method == 'Playlist.OnRemove':
self._playlist_onremove(data)
elif method == 'Playlist.OnClear':
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._playlist_onclear(data)
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
@ -214,7 +207,7 @@ class KodiMonitor(xbmc.Monitor):
utils.plex_command('RUN_LIB_SCAN', 'full')
elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down')
state.STOP_PKC = True
app.APP.stop_pkc = True
@staticmethod
def _hack_addon_paths_replay_video():
@ -237,7 +230,7 @@ class KodiMonitor(xbmc.Monitor):
(within the same micro-second!)
"""
LOG.info('Detected re-start of playback of last item')
old = state.OLD_PLAYER_STATES[1]
old = app.PLAYSTATE.old_player_states[1]
kwargs = {
'plex_id': old['plex_id'],
'plex_type': old['plex_type'],
@ -261,8 +254,8 @@ class KodiMonitor(xbmc.Monitor):
"""
if 'id' not in data['item']:
return
old = state.OLD_PLAYER_STATES[data['playlistid']]
if (not state.DIRECT_PATHS and
old = app.PLAYSTATE.old_player_states[data['playlistid']]
if (not app.SYNC.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
@ -399,7 +392,7 @@ class KodiMonitor(xbmc.Monitor):
else:
pos = info['position'] if info['position'] != -1 else 0
LOG.debug('Detected position %s for %s', pos, playqueue)
status = state.PLAYER_STATES[playerid]
status = app.PLAYSTATE.player_states[playerid]
try:
item = playqueue.items[pos]
except IndexError:
@ -431,7 +424,7 @@ class KodiMonitor(xbmc.Monitor):
plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path)
if not plex_id:
LOG.debug('No Plex id obtained - aborting playback report')
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
return
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
item.file = path
@ -455,7 +448,7 @@ class KodiMonitor(xbmc.Monitor):
else:
container_key = '/library/metadata/%s' % plex_id
# Remember that this player has been active
state.ACTIVE_PLAYERS.add(playerid)
app.PLAYSTATE.active_players.add(playerid)
status.update(info)
LOG.debug('Set the Plex container_key to: %s', container_key)
status['container_key'] = container_key
@ -469,8 +462,7 @@ class KodiMonitor(xbmc.Monitor):
LOG.debug('Set the player state: %s', status)
@utils.thread_methods
class SpecialMonitor(Thread):
class SpecialMonitor(backgroundthread.KillableThread):
"""
Detect the resume dialog for widgets.
Could also be used to detect external players (see Emby implementation)
@ -480,15 +472,15 @@ class SpecialMonitor(Thread):
# "Start from beginning", "Play from beginning"
strings = (utils.try_encode(utils.lang(12021)),
utils.try_encode(utils.lang(12023)))
while not self.stopped():
while not self.isCanceled():
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
# Remember that the item IS indeed resumable
control = int(Window(10106).getFocusId())
state.RESUME_PLAYBACK = True if control == 1001 else False
app.PLAYSTATE.resume_playback = True if control == 1001 else False
else:
# Different context menu is displayed
state.RESUME_PLAYBACK = False
app.PLAYSTATE.resume_playback = False
if xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'):
path = xbmc.getInfoLabel('container.folderpath')
if (isinstance(path, str) and
@ -507,14 +499,14 @@ def _playback_cleanup(ended=False):
timing data otherwise)
"""
LOG.debug('playback_cleanup called. Active players: %s',
state.ACTIVE_PLAYERS)
app.PLAYSTATE.active_players)
# We might have saved a transient token from a user flinging media via
# Companion (if we could not use the playqueue to store the token)
state.PLEX_TRANSIENT_TOKEN = None
for playerid in state.ACTIVE_PLAYERS:
status = state.PLAYER_STATES[playerid]
app.CONN.plex_transient_token = None
for playerid in app.PLAYSTATE.active_players:
status = app.PLAYSTATE.player_states[playerid]
# Remember the last played item later
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status)
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(status)
# Stop transcoding
if status['playmethod'] == 'Transcode':
LOG.debug('Tell the PMS to stop transcoding')
@ -527,9 +519,9 @@ def _playback_cleanup(ended=False):
# started playback via PMS
_record_playstate(status, ended)
# Reset the player's status
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
# As all playback has halted, reset the players that have been active
state.ACTIVE_PLAYERS = set()
app.PLAYSTATE.active_players = set()
LOG.info('Finished PKC playback cleanup')
@ -543,12 +535,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(utils.kodi_time_to_millis(status['totaltime'])) / 1000
totaltime = float(timing.kodi_time_to_millis(status['totaltime'])) / 1000
if ended:
progress = 0.99
time = v.IGNORE_SECONDS_AT_START + 1
else:
time = float(utils.kodi_time_to_millis(status['time'])) / 1000
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
try:
progress = time / totaltime
except ZeroDivisionError:
@ -556,7 +548,7 @@ def _record_playstate(status, ended):
LOG.debug('Playback progress %s (%s of %s seconds)',
progress, time, totaltime)
playcount = status['playcount']
last_played = utils.unix_date_to_kodi(utils.unix_timestamp())
last_played = timing.now()
if playcount is None:
LOG.debug('playcount not found, looking it up in the Kodi DB')
with kodi_db.KodiVideoDB() as kodidb:
@ -582,7 +574,7 @@ def _record_playstate(status, ended):
last_played,
status['plex_type'])
# Hack to force "in progress" widget to appear if it wasn't visible before
if (state.FORCE_RELOAD_SKIN and
if (app.APP.force_reload_skin and
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
LOG.debug('Refreshing skin to update widgets')
xbmc.executebuiltin('ReloadSkin()')

View file

@ -3,13 +3,13 @@
from __future__ import absolute_import, division, unicode_literals
import xbmc
from .. import state
from .. import app
class libsync_mixin(object):
def isCanceled(self):
return (self._canceled or state.STOP_PKC or state.STOP_SYNC or
state.SUSPEND_LIBRARY_THREAD or state.SUSPEND_SYNC)
return (self._canceled or app.APP.stop_pkc or app.SYNC.stop_sync or
app.SYNC.suspend_library_thread or app.SYNC.suspend_sync)
def update_kodi_library(video=True, music=True):

View file

@ -8,7 +8,7 @@ from ..plex_api import API
from ..plex_db import PlexDB
from ..kodi_db import KodiVideoDB
from .. import backgroundthread, utils
from .. import itemtypes, plex_functions as PF, variables as v, state
from .. import itemtypes, plex_functions as PF, variables as v, app
LOG = getLogger('PLEX.sync.fanart')
@ -27,14 +27,11 @@ class FanartThread(backgroundthread.KillableThread):
self.refresh = refresh
super(FanartThread, self).__init__()
def isCanceled(self):
return state.STOP_PKC
def isSuspended(self):
return (state.SUSPEND_LIBRARY_THREAD or
state.STOP_SYNC or
state.DB_SCAN or
state.SUSPEND_SYNC)
return (app.SYNC.suspend_library_thread or
app.SYNC.stop_sync or
app.SYNC.db_scan or
app.SYNC.suspend_sync)
def run(self):
try:
@ -80,8 +77,6 @@ class FanartTask(backgroundthread.Task, common.libsync_mixin):
self.refresh = refresh
def run(self):
if self.isCanceled():
return
process_fanart(self.plex_id, self.plex_type, self.refresh)

View file

@ -7,7 +7,7 @@ import xbmc
from .get_metadata import GetMetadataTask, reset_collections
from .process_metadata import InitNewSection, UpdateLastSync, ProcessMetadata
from . import common, sections
from .. import utils, backgroundthread, variables as v, state
from .. import utils, timing, backgroundthread, variables as v, app
from .. import plex_functions as PF, itemtypes
from ..plex_db import PlexDB
@ -97,7 +97,7 @@ class FullSync(common.libsync_mixin):
if self.isCanceled():
return False
if not self.install_sync_done:
state.PATH_VERIFIED = False
app.SYNC.path_verified = False
try:
# Sync new, updated and deleted items
iterator = PF.SectionItems(section['section_id'],
@ -157,7 +157,7 @@ class FullSync(common.libsync_mixin):
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
]
if state.ENABLE_MUSIC:
if app.SYNC.enable_music:
kinds.extend([
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
@ -181,7 +181,7 @@ class FullSync(common.libsync_mixin):
if self.isCanceled():
return
successful = False
self.current_sync = utils.unix_timestamp()
self.current_sync = timing.unix_timestamp()
# Delete playlist and video node files from Kodi
utils.delete_playlists()
utils.delete_nodes()

View file

@ -102,8 +102,8 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
while not self.isCanceled():
if section is None:
break
LOG.debug('Start processing section %s: %s',
section.plex_type, section.name)
LOG.debug('Start processing section %s (%ss)',
section.name, section.plex_type)
self.current = 1
self.processed = 0
self.total = section.total

View file

@ -9,7 +9,7 @@ from ..utils import cast
from ..plex_db import PlexDB
from .. import kodi_db
from .. import itemtypes
from .. import plex_functions as PF, music, utils, state, variables as v
from .. import plex_functions as PF, music, utils, variables as v, app
LOG = getLogger('PLEX.sync.sections')
@ -29,7 +29,7 @@ def sync_from_pms():
except AttributeError:
LOG.error("Error download PMS sections, abort")
return False
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
if app.SYNC.direct_paths is True and app.SYNC.enable_music is True:
# Will reboot Kodi is new library detected
music.excludefromscan_music_folders(xml=sections)

View file

@ -5,7 +5,7 @@ from logging import getLogger
import xbmc
from .. import plex_functions as PF, utils, variables as v, state
from .. import plex_functions as PF, utils, timing, variables as v
LOG = getLogger('PLEX.sync.time')
@ -84,7 +84,7 @@ def sync_pms_time():
LOG.debug('No timestamp; using 0')
timestamp = utils.cast(int, timestamp)
# Set the timer
koditime = utils.unix_timestamp()
koditime = timing.unix_timestamp()
# Toggle watched state
PF.scrobble(plex_id, 'watched')
# Let the PMS process this first!
@ -100,9 +100,9 @@ def sync_pms_time():
return False
# Calculate time offset Kodi-PMS
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
timing.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
utils.settings('kodiplextimeoffset',
value=str(state.KODI_PLEX_TIME_OFFSET))
value=str(timing.KODI_PLEX_TIME_OFFSET))
LOG.info("Time offset Koditime - Plextime in seconds: %s",
state.KODI_PLEX_TIME_OFFSET)
timing.KODI_PLEX_TIME_OFFSET)
return True

View file

@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from ..utils import etree
from .. import utils, path_ops, variables as v, state
from .. import utils, path_ops, variables as v, app
###############################################################################
@ -44,7 +44,7 @@ class VideoNodes(object):
delete=False):
# Plex: reassign mediatype due to Kodi inner workings
# How many items do we get at most?
limit = state.FETCH_PMS_ITEM_NUMBER
limit = unicode(app.APP.fetch_pms_item_number)
mediatypes = {
'movie': 'movies',
'show': 'tvshows',

View file

@ -10,7 +10,7 @@ from ..plex_api import API
from ..plex_db import PlexDB
from .. import kodi_db
from .. import backgroundthread, playlists, plex_functions as PF, itemtypes
from .. import artwork, utils, variables as v, state
from .. import artwork, utils, timing, variables as v, app
LOG = getLogger('PLEX.sync.websocket')
@ -22,7 +22,7 @@ PLAYSTATE_SESSIONS = {}
def interrupt_processing():
return state.STOP_PKC or state.SUSPEND_LIBRARY_THREAD or state.STOP_SYNC
return app.APP.stop_pkc or app.SYNC.suspend_library_thread or app.SYNC.stop_sync
def multi_delete(input_list, delete_list):
@ -75,7 +75,7 @@ def process_websocket_messages():
9: 'deleted'
"""
global WEBSOCKET_MESSAGES
now = utils.unix_timestamp()
now = timing.unix_timestamp()
update_kodi_video_library, update_kodi_music_library = False, False
delete_list = []
for i, message in enumerate(WEBSOCKET_MESSAGES):
@ -84,7 +84,7 @@ def process_websocket_messages():
break
if message['state'] == 9:
successful, video, music = process_delete_message(message)
elif now - message['timestamp'] < state.BACKGROUNDSYNC_SAFTYMARGIN:
elif now - message['timestamp'] < app.SYNC.backgroundsync_saftymargin:
# We haven't waited long enough for the PMS to finish processing the
# item. Do it later (excepting deletions)
continue
@ -127,7 +127,7 @@ def process_new_item_message(message):
LOG.error('Could not download metadata for %s', message['plex_id'])
return False, False, False
LOG.debug("Processing new/updated PMS item: %s", message['plex_id'])
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](utils.unix_timestamp()) as typus:
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](timing.unix_timestamp()) as typus:
typus.add_update(xml[0],
section_name=xml.get('librarySectionTitle'),
section_id=xml.get('librarySectionID'))
@ -169,7 +169,7 @@ def store_timeline_message(data):
'state': status,
'plex_type': typus,
'plex_id': utils.cast(int, message['itemID']),
'timestamp': utils.unix_timestamp(),
'timestamp': timing.unix_timestamp(),
'attempt': 0
})
elif typus in (v.PLEX_TYPE_MOVIE,
@ -186,7 +186,7 @@ def store_timeline_message(data):
'state': status,
'plex_type': typus,
'plex_id': plex_id,
'timestamp': utils.unix_timestamp(),
'timestamp': timing.unix_timestamp(),
'attempt': 0
})
@ -227,7 +227,7 @@ def store_activity_message(data):
'state': None, # Don't need a state here
'plex_type': typus['plex_type'],
'plex_id': plex_id,
'timestamp': utils.unix_timestamp(),
'timestamp': timing.unix_timestamp(),
'attempt': 0
})
@ -246,7 +246,7 @@ def process_playing(data):
plex_id = utils.cast(int, message['ratingKey'])
skip = False
for pid in (0, 1, 2):
if plex_id == state.PLAYER_STATES[pid]['plex_id']:
if plex_id == app.PLAYSTATE.player_states[pid]['plex_id']:
# Kodi is playing this message - no need to set the playstate
skip = True
if skip:
@ -265,7 +265,7 @@ def process_playing(data):
PLAYSTATE_SESSIONS[session_key] = {}
else:
# PMS is ours - get all current sessions
PLAYSTATE_SESSIONS.update(PF.GetPMSStatus(state.PLEX_TOKEN))
PLAYSTATE_SESSIONS.update(PF.GetPMSStatus(app.CONN.plex_token))
LOG.debug('Updated current sessions. They are: %s',
PLAYSTATE_SESSIONS)
if session_key not in PLAYSTATE_SESSIONS:
@ -281,18 +281,18 @@ def process_playing(data):
# Identify the user - same one as signed on with PKC? Skip
# update if neither session's username nor userid match
# (Owner sometime's returns id '1', not always)
if not state.PLEX_TOKEN and session['userId'] == '1':
if not app.CONN.plex_token and session['userId'] == '1':
# PKC not signed in to plex.tv. Plus owner of PMS is
# playing (the '1').
# Hence must be us (since several users require plex.tv
# token for PKC)
pass
elif not (session['userId'] == state.PLEX_USER_ID or
session['username'] == state.PLEX_USERNAME):
elif not (session['userId'] == app.CONN.plex_user_id or
session['username'] == app.CONN.plex_username):
LOG.debug('Our username %s, userid %s did not match '
'the session username %s with userid %s',
state.PLEX_USERNAME,
state.PLEX_USER_ID,
app.CONN.plex_username,
app.CONN.plex_user_id,
session['username'],
session['userId'])
continue
@ -334,7 +334,7 @@ def process_playing(data):
mark_played = False
LOG.debug('Update playstate for user %s for %s with plex id %s to '
'viewCount %s, resume %s, mark_played %s',
state.PLEX_USERNAME, session['kodi_type'], plex_id,
app.CONN.plex_username, session['kodi_type'], plex_id,
session['viewCount'], resume, mark_played)
func = itemtypes.ITEMTYPE_FROM_KODITYPE[session['kodi_type']]
with func(None) as fkt:
@ -343,7 +343,7 @@ def process_playing(data):
resume,
session['duration'],
session['file_id'],
utils.unix_timestamp(),
timing.unix_timestamp(),
v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']])

View file

@ -21,7 +21,7 @@ from . import pickler
from .playutils import PlayUtils
from .pkc_listitem import PKCListItem
from . import variables as v
from . import state
from . import app
###############################################################################
LOG = getLogger('PLEX.playback')
@ -51,14 +51,14 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
'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:
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
if not app.CONN.authenticated:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
_ensure_resolve(abort=True)
return
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
try:
@ -208,7 +208,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
if RESOLVE:
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
sleep_timer = 0
while not state.PKC_CAUSED_STOP_DONE:
while not app.PLAYSTATE.pkc_caused_stop_done:
sleep(50)
sleep_timer += 1
if sleep_timer > 100:
@ -229,19 +229,19 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
utils.lang(30128),
icon='{error}')
# Do NOT use _ensure_resolve() because we resolved above already
state.CONTEXT_MENU_PLAY = False
state.FORCE_TRANSCODE = False
state.RESUME_PLAYBACK = False
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False
return
PL.get_playlist_details_from_xml(playqueue, xml)
stack = _prep_playlist_stack(xml)
_process_stack(playqueue, stack)
# Always resume if playback initiated via PMS and there IS a resume
# point
offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None
offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None
# Reset some playback variables
state.CONTEXT_MENU_PLAY = False
state.FORCE_TRANSCODE = False
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
# New thread to release this one sooner (e.g. harddisk spinning up)
thread = Thread(target=threaded_playback,
args=(playqueue.kodi_pl, pos, offset))
@ -272,8 +272,8 @@ def _ensure_resolve(abort=False):
LOG.debug('Passing dummy path to Kodi')
# if not state.CONTEXT_MENU_PLAY:
# Because playback won't start with context menu play
state.PKC_CAUSED_STOP = True
state.PKC_CAUSED_STOP_DONE = False
app.PLAYSTATE.pkc_caused_stop = True
app.PLAYSTATE.pkc_caused_stop_done = False
if not abort:
result = pickler.Playback_Successful()
result.listitem = PKCListItem(path=v.NULL_VIDEO)
@ -283,9 +283,9 @@ def _ensure_resolve(abort=False):
pickler.pickle_me(None)
if abort:
# Reset some playback variables
state.CONTEXT_MENU_PLAY = False
state.FORCE_TRANSCODE = False
state.RESUME_PLAYBACK = False
app.PLAYSTATE.context_menu_play = False
app.PLAYSTATE.force_transcode = False
app.PLAYSTATE.resume_playback = False
def _init_existing_kodi_playlist(playqueue, pos):
@ -299,7 +299,7 @@ def _init_existing_kodi_playlist(playqueue, pos):
LOG.error('No Kodi items returned')
raise PL.PlaylistError('No Kodi items returned')
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
item.force_transcode = state.FORCE_TRANSCODE
item.force_transcode = app.PLAYSTATE.force_transcode
# playqueue.py will add the rest - this will likely put the PMS under
# a LOT of strain if the following Kodi setting is enabled:
# Settings -> Player -> Videos -> Play next video automatically
@ -310,7 +310,7 @@ def _prep_playlist_stack(xml):
stack = []
for item in xml:
api = API(item)
if (state.CONTEXT_MENU_PLAY is False and
if (app.PLAYSTATE.context_menu_play is False and
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
# If user chose to play via PMS or force transcode, do not
# use the item path stored in the Kodi DB
@ -378,7 +378,7 @@ def _process_stack(playqueue, stack):
playlist_item.offset = item['offset']
playlist_item.part = item['part']
playlist_item.id = item['id']
playlist_item.force_transcode = state.FORCE_TRANSCODE
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
pos += 1
@ -413,7 +413,7 @@ def _conclude_playback(playqueue, pos):
playurl = item.file
if not playurl:
LOG.info('Did not get a playurl, aborting playback silently')
state.RESUME_PLAYBACK = False
app.PLAYSTATE.resume_playback = False
pickler.pickle_me(result)
return
listitem.setPath(utils.try_encode(playurl))
@ -422,8 +422,8 @@ def _conclude_playback(playqueue, pos):
elif item.playmethod == 'Transcode':
playutils.audio_subtitle_prefs(listitem)
if state.RESUME_PLAYBACK is True:
state.RESUME_PLAYBACK = False
if app.PLAYSTATE.resume_playback is True:
app.PLAYSTATE.resume_playback = False
if (item.offset is None and
item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)):
with PlexDB() as plexdb:

View file

@ -10,7 +10,7 @@ from . import playback
from . import context_entry
from . import json_rpc as js
from . import pickler
from . import state
from . import app
###############################################################################
@ -61,7 +61,7 @@ class PlaybackStarter(Thread):
kodi_type=params.get('kodi_type'))
def run(self):
queue = state.COMMAND_PIPELINE_QUEUE
queue = app.APP.command_pipeline_queue
LOG.info("----===## Starting PlaybackStarter ##===----")
while True:
item = queue.get()

View file

@ -16,7 +16,7 @@ from .downloadutils import DownloadUtils as DU
from . import utils
from . import json_rpc as js
from . import variables as v
from . import state
from . import app
###############################################################################
@ -357,7 +357,7 @@ def verify_kodi_item(plex_id, kodi_item):
# Got all the info we need
return kodi_item
# Special case playlist startup - got type but no id
if (not state.DIRECT_PATHS and state.ENABLE_MUSIC and
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
kodi_item.get('type') == v.KODI_TYPE_SONG and
kodi_item['file'].startswith('http')):
kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'],

View file

@ -19,7 +19,7 @@ from . import pms, db, kodi_pl, plex_pl
from ..watchdog import events
from ..plex_api import API
from .. import utils, path_ops, variables as v, state
from .. import utils, path_ops, variables as v, app
###############################################################################
LOG = getLogger('PLEX.playlists')
@ -92,7 +92,7 @@ def websocket(plex_id, status):
* 9: 'deleted'
"""
create = False
with state.LOCK_PLAYLISTS:
with app.APP.lock_playlists:
playlist = db.get_playlist(plex_id=plex_id)
if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE:
LOG.debug('Ignoring detected Plex playlist change for %s',
@ -155,7 +155,7 @@ def full_sync():
fetch the PMS playlists)
"""
LOG.info('Starting playlist full sync')
with state.LOCK_PLAYLISTS:
with app.APP.lock_playlists:
# Need to lock because we're messing with playlists
return _full_sync()
@ -283,7 +283,7 @@ def sync_kodi_playlist(path):
return False
if extension not in SUPPORTED_FILETYPES:
return False
if not state.SYNC_SPECIFIC_KODI_PLAYLISTS:
if not app.SYNC.sync_specific_kodi_playlists:
return True
playlist = Playlist()
playlist.kodi_path = path
@ -341,10 +341,10 @@ def sync_plex_playlist(playlist=None, xml=None, plex_id=None):
return False
name = api.title()
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
if (not state.ENABLE_MUSIC and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
if (not app.SYNC.enable_music and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
LOG.debug('Not synching Plex audio playlist')
return False
if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
if not app.SYNC.sync_specific_plex_playlists:
return True
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
if name and name.lower().startswith(prefix):
@ -387,7 +387,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
events.EVENT_TYPE_CREATED: self.on_created,
events.EVENT_TYPE_DELETED: self.on_deleted,
}
with state.LOCK_PLAYLISTS:
with app.APP.lock_playlists:
_method_map[event.event_type](event)
def on_created(self, event):

View file

@ -5,16 +5,11 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
"""
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from threading import Thread
import xbmc
from . import utils
from . import playlist_func as PL
from . import plex_functions as PF
from .plex_api import API
from . import json_rpc as js
from . import variables as v
from . import state
from . import playlist_func as PL, plex_functions as PF
from . import backgroundthread, utils, json_rpc as js, app, variables as v
###############################################################################
LOG = getLogger('PLEX.playqueue')
@ -35,7 +30,7 @@ def init_playqueues():
LOG.debug('Playqueues have already been initialized')
return
# Initialize Kodi playqueues
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
for i in (0, 1, 2):
# Just in case the Kodi response is not sorted correctly
for queue in js.get_playlists():
@ -96,13 +91,18 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
return playqueue
@utils.thread_methods(add_suspends=['PMS_STATUS'])
class PlayqueueMonitor(Thread):
class PlayqueueMonitor(backgroundthread.KillableThread):
"""
Unfortunately, Kodi does not tell if items within a Kodi playqueue
(playlist) are swapped. This is what this monitor is for. Don't replace
this mechanism till Kodi's implementation of playlists has improved
"""
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
return self._suspended or app.CONN.pms_status
def _compare_playqueues(self, playqueue, new):
"""
Used to poll the Kodi playqueue and update the Plex playqueue if needed
@ -117,7 +117,7 @@ class PlayqueueMonitor(Thread):
# Ignore new media added by other addons
continue
for j, old_item in enumerate(old):
if self.stopped():
if self.isCanceled():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
@ -178,7 +178,7 @@ class PlayqueueMonitor(Thread):
for j in range(i, len(index)):
index[j] += 1
for i in reversed(index):
if self.stopped():
if self.isCanceled():
# Chances are that we got an empty Kodi playlist due to
# Kodi exit
return
@ -192,20 +192,18 @@ class PlayqueueMonitor(Thread):
LOG.debug('Done comparing playqueues')
def run(self):
stopped = self.stopped
suspended = self.suspended
LOG.info("----===## Starting PlayqueueMonitor ##===----")
while not stopped():
while suspended():
if stopped():
while not self.isCanceled():
while self.isSuspended():
if self.isCanceled():
break
xbmc.sleep(1000)
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
if playqueue.old_kodi_pl != kodi_pl:
if playqueue.id is None and (not state.DIRECT_PATHS or
state.CONTEXT_MENU_PLAY):
if playqueue.id is None and (not app.PLAYSTATE.direct_paths or
app.PLAYSTATE.context_menu_play):
# Only initialize if directly fired up using direct
# paths. Otherwise let default.py do its magic
LOG.debug('Not yet initiating playback')

View file

@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .downloadutils import DownloadUtils as DU
from . import utils
from . import utils, app
from . import variables as v
###############################################################################
@ -304,7 +304,7 @@ class PlayUtils():
# We don't know the language - no need to download
else:
path = self.api.attach_plex_token_to_url(
"%s%s" % (utils.window('pms_server'),
"%s%s" % (app.CONN.server,
stream.attrib['key']))
downloadable_streams.append(index)
download_subs.append(utils.try_encode(path))

View file

@ -42,11 +42,11 @@ from .kodi_db import KodiVideoDB, KodiMusicDB
from .utils import cast
from .downloadutils import DownloadUtils as DU
from . import clientinfo
from . import utils
from . import utils, timing
from . import path_ops
from . import plex_functions as PF
from . import variables as v
from . import state
from . import app
###############################################################################
LOG = getLogger('PLEX.plex_api')
@ -121,7 +121,7 @@ class API(object):
Pass direct_path=True if you're calling from another Plex python
instance - because otherwise direct paths will evaluate to False!
"""
direct_paths = direct_paths or state.DIRECT_PATHS
direct_paths = direct_paths or app.SYNC.direct_paths
filename = self.file_path(force_first_media=force_first_media)
if (not direct_paths or force_addon or
self.plex_type() == v.PLEX_TYPE_CLIP):
@ -219,15 +219,15 @@ class API(object):
extension not in v.KODI_SUPPORTED_IMAGES):
# Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080
path = state.PMS_SERVER + PF.transcode_image_path(
path = app.CONN.server + PF.transcode_image_path(
self.item[0][0].get('key'),
state.PMS_TOKEN,
"%s%s" % (state.PMS_SERVER, self.item[0][0].get('key')),
app.CONN.pms_token,
"%s%s" % (app.CONN.server, self.item[0][0].get('key')),
1920,
1080)
else:
path = self.attach_plex_token_to_url(
'%s%s' % (state.PMS_SERVER, self.item[0][0].attrib['key']))
'%s%s' % (app.CONN.server, self.item[0][0].attrib['key']))
# Attach Plex id to url to let it be picked up by our playqueue agent
# later
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id()))
@ -263,10 +263,9 @@ class API(object):
"""
res = self.item.get('addedAt')
if res is not None:
res = utils.unix_date_to_kodi(res)
return timing.plex_date_to_kodi(res)
else:
res = '2000-01-01 10:00:00'
return res
return '2000-01-01 10:00:00'
def viewcount(self):
"""
@ -300,11 +299,11 @@ class API(object):
played = True if playcount else False
try:
last_played = utils.unix_date_to_kodi(int(item['lastViewedAt']))
last_played = utils.plex_date_to_kodi(int(item['lastViewedAt']))
except (KeyError, ValueError):
last_played = None
if state.INDICATE_MEDIA_VERSIONS is True:
if app.SYNC.indicate_media_versions is True:
userrating = 0
for _ in self.item.findall('./Media'):
userrating += 1
@ -685,12 +684,12 @@ class API(object):
url may or may not already contain a '?'
"""
if not state.PMS_TOKEN:
if not app.CONN.pms_token:
return url
if '?' not in url:
url = "%s?X-Plex-Token=%s" % (url, state.PMS_TOKEN)
url = "%s?X-Plex-Token=%s" % (url, app.CONN.pms_token)
else:
url = "%s&X-Plex-Token=%s" % (url, state.PMS_TOKEN)
url = "%s&X-Plex-Token=%s" % (url, app.CONN.pms_token)
return url
def item_id(self):
@ -770,7 +769,7 @@ class API(object):
for extras in self.item.iterfind('Extras'):
# There will always be only 1 extras element
if (len(extras) > 0 and
state.SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER):
app.SYNC.show_extras_instead_of_playing_trailer):
return ('plugin://%s?mode=route_to_extras&plex_id=%s'
% (v.ADDON_ID, self.plex_id()))
for extra in extras:
@ -888,7 +887,7 @@ class API(object):
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
artwork = ('%s/photo/:/transcode?width=3840&height=3840&'
'minSize=1&upscale=0&url=%s'
% (state.PMS_SERVER, quote(artwork)))
% (app.CONN.server, quote(artwork)))
artwork = self.attach_plex_token_to_url(artwork)
return artwork
@ -1406,7 +1405,7 @@ class API(object):
# trailers are 'clip' with PMS xmls
if action == "DirectStream":
path = self.item[self.mediastream][self.part].attrib['key']
url = state.PMS_SERVER + path
url = app.CONN.server + path
# e.g. Trailers already feature an '?'!
if '?' in url:
url += '&' + urlencode(xargs)
@ -1423,7 +1422,7 @@ class API(object):
}
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key']
transcode_path = state.PMS_SERVER + \
transcode_path = app.CONN.server + \
'/video/:/transcode/universal/start.m3u8?'
args = {
'audioBoost': utils.settings('audioBoost'),
@ -1481,7 +1480,7 @@ class API(object):
# We don't know the language - no need to download
else:
path = self.attach_plex_token_to_url(
"%s%s" % (state.PMS_SERVER, key))
"%s%s" % (app.CONN.server, key))
externalsubs.append(path)
kodiindex += 1
LOG.info('Found external subs: %s', externalsubs)
@ -1735,16 +1734,16 @@ class API(object):
if path is None:
return
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
if state.REMAP_PATH is True:
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
getattr(state, 'remapSMB%sNew' % typus),
if app.SYNC.remap_path is True:
path = path.replace(getattr(app.SYNC, 'remapSMB%sOrg' % typus),
getattr(app.SYNC, 'remapSMB%sNew' % typus),
1)
# There might be backslashes left over:
path = path.replace('\\', '/')
elif state.REPLACE_SMB_PATH is True:
elif app.SYNC.replace_smb_path is True:
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
if ((state.PATH_VERIFIED and force_check is False) or
if ((app.SYNC.path_verified and force_check is False) or
omit_check is True):
return path
@ -1769,14 +1768,14 @@ class API(object):
if force_check is False:
# Validate the path is correct with user intervention
if self.ask_to_validate(path):
state.STOP_SYNC = True
app.SYNC.stop_sync = True
path = None
state.PATH_VERIFIED = True
app.SYNC.path_verified = True
else:
path = None
elif force_check is False:
# Only set the flag if we were not force-checking the path
state.PATH_VERIFIED = True
app.SYNC.path_verified = True
return path
@staticmethod

View file

@ -20,7 +20,8 @@ from . import playback
from . import json_rpc as js
from . import playqueue as PQ
from . import variables as v
from . import state
from . import backgroundthread
from . import app
###############################################################################
@ -46,7 +47,7 @@ def update_playqueue_from_PMS(playqueue,
# Safe transient token from being deleted
if transient_token is None:
transient_token = playqueue.plex_transient_token
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
try:
xml.attrib
@ -64,8 +65,7 @@ def update_playqueue_from_PMS(playqueue,
playback.play_xml(playqueue, xml, offset)
@utils.thread_methods(add_suspends=['PMS_STATUS'])
class PlexCompanion(Thread):
class PlexCompanion(backgroundthread.KillableThread):
"""
Plex Companion monitoring class. Invoke only once
"""
@ -80,7 +80,13 @@ class PlexCompanion(Thread):
self.player = Player()
self.httpd = False
self.subscription_manager = None
Thread.__init__(self)
super(PlexCompanion, self).__init__()
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
return self._suspended or app.CONN.pms_status
def _process_alexa(self, data):
xml = PF.GetPlexMetadata(data['key'])
@ -116,10 +122,9 @@ class PlexCompanion(Thread):
offset = None
playback.play_xml(playqueue, xml, offset)
else:
state.PLEX_TRANSIENT_TOKEN = data.get('token')
app.CONN.plex_transient_token = data.get('token')
if data.get('offset') != '0':
state.RESUMABLE = True
state.RESUME_PLAYBACK = True
app.PLAYSTATE.resume_playback = True
playback.playback_triage(api.plex_id(),
api.plex_type(),
resolve=False)
@ -129,7 +134,7 @@ class PlexCompanion(Thread):
"""
E.g. watch later initiated by Companion. Basically navigating Plex
"""
state.PLEX_TRANSIENT_TOKEN = data.get('key')
app.CONN.plex_transient_token = data.get('key')
params = {
'mode': 'plex_node',
'key': '{server}%s' % data.get('key'),
@ -221,16 +226,16 @@ class PlexCompanion(Thread):
LOG.debug('Processing: %s', task)
data = task['data']
if task['action'] == 'alexa':
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._process_alexa(data)
elif (task['action'] == 'playlist' and
data.get('address') == 'node.plexapp.com'):
self._process_node(data)
elif task['action'] == 'playlist':
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._process_playlist(data)
elif task['action'] == 'refreshPlayQueue':
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
self._process_refresh(data)
elif task['action'] == 'setStreams':
try:
@ -260,8 +265,6 @@ class PlexCompanion(Thread):
httpd = self.httpd
# Cache for quicker while loops
client = self.client
stopped = self.stopped
suspended = self.suspended
# Start up instances
request_mgr = httppersist.RequestMgr()
@ -298,12 +301,12 @@ class PlexCompanion(Thread):
if httpd:
thread = Thread(target=httpd.handle_request)
while not stopped():
while not self.isCanceled():
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
while suspended():
if stopped():
while self.isSuspended():
if self.isCanceled():
break
sleep(1000)
try:
@ -335,13 +338,13 @@ class PlexCompanion(Thread):
LOG.warn(traceback.format_exc())
# See if there's anything we need to process
try:
task = state.COMPANION_QUEUE.get(block=False)
task = app.APP.companion_queue.get(block=False)
except Empty:
pass
else:
# Got instructions, process them
self._process_tasks(task)
state.COMPANION_QUEUE.task_done()
app.APP.companion_queue.task_done()
# Don't sleep
continue
sleep(50)

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from .. import variables as v
class Playlists(object):

View file

@ -5,10 +5,9 @@ from logging import getLogger
import time
import threading
import xbmc
import xbmcgui
from .downloadutils import DownloadUtils as DU
from . import utils, variables as v, state
from . import utils, app
###############################################################################
LOG = getLogger('PLEX.plex_tv')
@ -87,7 +86,7 @@ def switch_home_user(userid, pin, token, machine_identifier):
utils.settings('plex_restricteduser',
'true' if xml.get('restricted', '0') == '1'
else 'false')
state.RESTRICTED_USER = True if \
app.CONN.restricted_user = True if \
xml.get('restricted', '0') == '1' else False
# Get final token to the PMS we've chosen
@ -174,7 +173,7 @@ class PinLogin(object):
start = time.time()
while (not self._abort and
time.time() - start < 300 and
not state.STOP_PKC):
not app.APP.stop_pkc):
xml = DU().downloadUrl(self.POLL.format(self.id),
authenticate=False)
try:

View file

@ -31,7 +31,7 @@ import time
from xbmc import sleep
from ..downloadutils import DownloadUtils as DU
from .. import utils
from .. import utils, app
from .. import variables as v
###############################################################################
@ -228,7 +228,7 @@ class plexgdm:
return self.server_list
def discover(self):
currServer = utils.window('pms_server')
currServer = app.CONN.server
if not currServer:
return
currServerProt, currServerIP, currServerPort = \

View file

@ -9,8 +9,8 @@ from logging import getLogger
from threading import Thread
from ..downloadutils import DownloadUtils as DU
from .. import utils
from .. import state
from .. import utils, timing
from .. import app
from .. import variables as v
from .. import json_rpc as js
from .. import playqueue as PQ
@ -101,9 +101,9 @@ def update_player_info(playerid):
"""
Updates all player info for playerid [int] in state.py.
"""
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
app.PLAYSTATE.playerstates[playerid].update(js.get_player_props(playerid))
app.PLAYSTATE.playerstates[playerid]['volume'] = js.get_volume()
app.PLAYSTATE.playerstates[playerid]['muted'] = js.get_muted()
class SubscriptionMgr(object):
@ -189,9 +189,9 @@ class SubscriptionMgr(object):
return answ
def _timeline_dict(self, player, ptype):
with state.LOCK_PLAYQUEUES:
with app.APP.lock_playqueues:
playerid = player['playerid']
info = state.PLAYER_STATES[playerid]
info = app.PLAYSTATE.player_states[playerid]
playqueue = PQ.PLAYQUEUES[playerid]
position = self._get_correct_position(info, playqueue)
try:
@ -208,12 +208,12 @@ class SubscriptionMgr(object):
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
v.PLEX_PLAYLIST_TYPE_PHOTO):
self.location = 'fullScreenVideo'
pbmc_server = utils.window('pms_server')
pbmc_server = app.CONN.server
if pbmc_server:
(self.protocol, self.server, self.port) = pbmc_server.split(':')
self.server = self.server.replace('/', '')
status = 'paused' if int(info['speed']) == 0 else 'playing'
duration = utils.kodi_time_to_millis(info['totaltime'])
duration = timing.kodi_time_to_millis(info['totaltime'])
shuffle = '1' if info['shuffled'] else '0'
mute = '1' if info['muted'] is True else '0'
answ = {
@ -225,7 +225,7 @@ class SubscriptionMgr(object):
'state': status,
'type': ptype,
'itemType': ptype,
'time': utils.kodi_time_to_millis(info['time']),
'time': timing.kodi_time_to_millis(info['time']),
'duration': duration,
'seekRange': '0-%s' % duration,
'shuffle': shuffle,
@ -254,8 +254,8 @@ class SubscriptionMgr(object):
if playqueue.items[position].guid:
answ['guid'] = item.guid
# Temp. token set?
if state.PLEX_TRANSIENT_TOKEN:
answ['token'] = state.PLEX_TRANSIENT_TOKEN
if app.CONN.plex_transient_token:
answ['token'] = app.CONN.plex_transient_token
elif playqueue.plex_transient_token:
answ['token'] = playqueue.plex_transient_token
# Process audio and subtitle streams
@ -301,7 +301,7 @@ class SubscriptionMgr(object):
stream_type: 'video', 'audio', 'subtitle'
"""
playqueue = PQ.PLAYQUEUES[playerid]
info = state.PLAYER_STATES[playerid]
info = app.PLAYSTATE.player_states[playerid]
position = self._get_correct_position(info, playqueue)
if info[STREAM_DETAILS[stream_type]] == -1:
kodi_stream_index = -1
@ -315,7 +315,7 @@ class SubscriptionMgr(object):
Updates the Plex Companien client with the machine identifier uuid with
command_id
"""
with state.LOCK_SUBSCRIBER:
with app.APP.lock_subscriber:
if command_id and self.subscribers.get(uuid):
self.subscribers[uuid].command_id = int(command_id)
@ -326,7 +326,7 @@ class SubscriptionMgr(object):
playqueues.
"""
for player in players.values():
info = state.PLAYER_STATES[player['playerid']]
info = app.PLAYSTATE.player_states[player['playerid']]
playqueue = PQ.PLAYQUEUES[player['playerid']]
position = self._get_correct_position(info, playqueue)
try:
@ -345,7 +345,7 @@ class SubscriptionMgr(object):
Causes PKC to tell the PMS and Plex Companion players to receive a
notification what's being played.
"""
with state.LOCK_SUBSCRIBER:
with app.APP.lock_subscriber:
self._cleanup()
# Get all the active/playing Kodi players (video, audio, pictures)
players = js.get_players()
@ -378,7 +378,7 @@ class SubscriptionMgr(object):
self._send_pms_notification(player['playerid'], self.last_params)
def _get_pms_params(self, playerid):
info = state.PLAYER_STATES[playerid]
info = app.PLAYSTATE.player_states[playerid]
playqueue = PQ.PLAYQUEUES[playerid]
position = self._get_correct_position(info, playqueue)
try:
@ -390,8 +390,8 @@ class SubscriptionMgr(object):
'state': status,
'ratingKey': item.plex_id,
'key': '/library/metadata/%s' % item.plex_id,
'time': utils.kodi_time_to_millis(info['time']),
'duration': utils.kodi_time_to_millis(info['totaltime'])
'time': timing.kodi_time_to_millis(info['time']),
'duration': timing.kodi_time_to_millis(info['totaltime'])
}
if info['container_key'] is not None:
# params['containerKey'] = info['container_key']
@ -407,12 +407,12 @@ class SubscriptionMgr(object):
playqueue = PQ.PLAYQUEUES[playerid]
xargs = params_pms()
xargs.update(params)
if state.PLEX_TRANSIENT_TOKEN:
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
if app.CONN.plex_transient_token:
xargs['X-Plex-Token'] = app.CONN.plex_transient_token
elif playqueue.plex_transient_token:
xargs['X-Plex-Token'] = playqueue.plex_transient_token
elif state.PMS_TOKEN:
xargs['X-Plex-Token'] = state.PMS_TOKEN
elif app.ACCOUNT.pms_token:
xargs['X-Plex-Token'] = app.ACCOUNT.pms_token
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
serv.get('server', 'localhost'),
serv.get('port', '32400'))
@ -434,7 +434,7 @@ class SubscriptionMgr(object):
command_id,
self,
self.request_mgr)
with state.LOCK_SUBSCRIBER:
with app.APP.lock_subscriber:
self.subscribers[subscriber.uuid] = subscriber
return subscriber
@ -444,7 +444,7 @@ class SubscriptionMgr(object):
uuid from PKC notifications.
(Calls the cleanup() method of the subscriber)
"""
with state.LOCK_SUBSCRIBER:
with app.APP.lock_subscriber:
for subscriber in self.subscribers.values():
if subscriber.uuid == uuid or subscriber.host == uuid:
subscriber.cleanup()

View file

@ -5,38 +5,107 @@ import logging
import sys
import xbmc
from . import utils
from . import userclient
from . import initialsetup
from . import utils, clientinfo, timing
from . import initialsetup, artwork
from . import kodimonitor
from . import sync
from . import websocket_client
from . import plex_companion
from . import plex_functions as PF
from . import command_pipeline
from . import plex_functions as PF, playqueue as PQ
from . import playback_starter
from . import playqueue
from . import variables as v
from . import state
from . import app
from . import loghandler
from .windows import userselect
###############################################################################
loghandler.config()
LOG = logging.getLogger("PLEX.service_entry")
LOG = logging.getLogger("PLEX.service")
###############################################################################
WINDOW_PROPERTIES = (
"plex_online", "plex_command_processed", "plex_shouldStop", "plex_dbScan",
"plex_customplayqueue", "plex_playbackProps",
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
"countError", "countUnauthorized", "plex_restricteduser",
"plex_allows_mediaDeletion", "plex_command", "plex_result",
"plex_force_transcode_pix"
)
def authenticate():
"""
Authenticate the current user or prompt to log-in
Returns True if successful, False if not. 'aborted' if user chose to
abort
"""
LOG.info('Authenticating user')
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login:
# Found a user in the settings, try to authenticate
LOG.info('Trying to authenticate with old settings')
res = PF.check_connection(app.CONN.server,
token=app.ACCOUNT.pms_token,
verifySSL=app.CONN.verify_ssl_cert)
if res is False:
LOG.error('Something went wrong while checking connection')
return False
elif res == 401:
LOG.error('User token no longer valid. Sign user out')
app.ACCOUNT.clear()
return False
elif res >= 400:
LOG.error('Answer from PMS is not as expected')
return False
LOG.info('Successfully authenticated using old settings')
app.ACCOUNT.set_authenticated()
return True
# Could not use settings - try to get Plex user list from plex.tv
if app.ACCOUNT.plex_token:
LOG.info("Trying to connect to plex.tv to get a user list")
user, _ = userselect.start()
if not user:
LOG.info('No user received')
app.CONN.pms_status = 'Stop'
return False
username = user.title
user_id = user.id
token = user.authToken
else:
LOG.info("Trying to authenticate without a token")
username = ''
user_id = ''
token = ''
res = PF.check_connection(app.CONN.server,
token=token,
verifySSL=app.CONN.verify_ssl_cert)
if res is False:
LOG.error('Something went wrong while checking connection')
return False
elif res == 401:
LOG.error('Token not valid')
return False
elif res >= 400:
LOG.error('Answer from PMS is not as expected')
return False
LOG.info('Successfully authenticated')
# Got new values that need to be saved
utils.settings('username', value=username)
utils.settings('userid', value=user_id)
utils.settings('accessToken', value=token)
app.ACCOUNT.load()
app.ACCOUNT.set_authenticated()
return True
class Service():
server_online = True
warn_auth = True
user = None
ws = None
sync = None
plexcompanion = None
user_running = False
ws_running = False
alexa_running = False
sync_running = False
@ -67,28 +136,110 @@ class Service():
utils.settings('syncSpecificPlexPlaylistsPrefix'))
LOG.info('XML decoding being used: %s', utils.ETREE)
LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion'))
# Reset window props
for prop in WINDOW_PROPERTIES:
utils.window(prop, clear=True)
# To detect Kodi profile switches
utils.window('plex_kodiProfile',
value=utils.try_decode(xbmc.translatePath("special://profile")))
self.monitor = xbmc.Monitor()
# Load/Reset PKC entirely - important for user/Kodi profile switch
initialsetup.reload_pkc()
# Clear video nodes properties
from .library_sync import videonodes
videonodes.VideoNodes().clearProperties()
clientinfo.getDeviceId()
# Init time-offset between Kodi and Plex
timing.KODI_PLEX_TIME_OFFSET = utils.settings('kodiplextimeoffset') or 0.0
def _stop_pkc(self):
return xbmc.abortRequested or state.STOP_PKC
def isCanceled(self):
return xbmc.abortRequested or app.APP.stop_pkc
def log_out(self):
"""
Ensures that lib sync threads are suspended; signs out user
"""
LOG.info('Log-out requested')
app.SYNC.suspend_library_thread = True
i = 0
while app.SYNC.db_scan:
i += 1
xbmc.sleep(50)
if i > 100:
LOG.error('Could not stop library sync, aborting log-out')
# Failed to reset PMS and plex.tv connects. Try to restart Kodi
utils.messageDialog(utils.lang(29999), utils.lang(39208))
# Resuming threads, just in case
app.SYNC.suspend_library_thread = False
return False
LOG.info('Successfully stopped library sync')
app.ACCOUNT.clear()
LOG.info('User has been logged out')
return True
def choose_pms_server(self, manual=False):
LOG.info("Choosing PMS server requested, starting")
if manual:
if not self.setup.enter_new_pms_address():
return False
else:
server = self.setup.pick_pms(showDialog=True)
if server is None:
LOG.info('We did not connect to a new PMS, aborting')
return False
LOG.info("User chose server %s", server['name'])
if server['baseURL'] == app.CONN.server:
LOG.info('User chose old PMS to connect to')
return False
self.setup.write_pms_to_settings(server)
if not self.log_out():
return False
# Wipe Kodi and Plex database as well as playlists and video nodes
utils.wipe_database()
app.CONN.load()
LOG.info("Choosing new PMS complete")
return True
def switch_plex_user(self):
if not self.log_out():
return False
# First remove playlists of old user
utils.delete_playlists()
# Remove video nodes
utils.delete_nodes()
return True
def toggle_plex_tv(self):
if utils.settings('plexToken'):
LOG.info('Reseting plex.tv credentials in settings')
app.ACCOUNT.clear()
return True
else:
LOG.info('Login to plex.tv')
return self.setup.plex_tv_sign_in()
def ServiceEntryPoint(self):
# Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once.
_stop_pkc = self._stop_pkc
monitor = self.monitor
app.init()
# Some plumbing
artwork.IMAGE_CACHING_SUSPENDS = [
app.SYNC.suspend_library_thread,
app.SYNC.stop_sync,
app.SYNC.db_scan
]
if not utils.settings('imageSyncDuringPlayback') == 'true':
artwork.IMAGE_CACHING_SUSPENDS.append(app.SYNC.suspend_sync)
# Initialize the PKC playqueues
PQ.init_playqueues()
# Server auto-detect
initialsetup.InitialSetup().setup()
self.setup = initialsetup.InitialSetup()
self.setup.setup()
# Detect playback start early on
self.command_pipeline = command_pipeline.Monitor_Window()
self.command_pipeline.start()
# Initialize important threads, handing over self for callback purposes
self.user = userclient.UserClient()
# Initialize important threads
self.ws = websocket_client.PMS_Websocket()
self.alexa = websocket_client.Alexa_Websocket()
self.sync = sync.Sync()
@ -97,9 +248,10 @@ class Service():
self.playback_starter = playback_starter.PlaybackStarter()
self.playqueue = playqueue.PlayqueueMonitor()
server_online = True
welcome_msg = True
counter = 0
while not _stop_pkc():
while not self.isCanceled():
if utils.window('plex_kodiProfile') != v.KODI_PROFILE:
# Profile change happened, terminate this thread and others
@ -108,149 +260,167 @@ class Service():
v.KODI_PROFILE, utils.window('plex_kodiProfile'))
break
plex_command = utils.window('plex_command')
if plex_command:
# Commands/user interaction received from other PKC Python
# instances (default.py and context.py instead of service.py)
utils.window('plex_command', clear=True)
if plex_command.startswith('PLAY-'):
# Add-on path playback!
app.APP.command_pipeline_queue.put(
plex_command.replace('PLAY-', ''))
elif plex_command.startswith('NAVIGATE-'):
app.APP.command_pipeline_queue.put(
plex_command.replace('NAVIGATE-', ''))
elif plex_command.startswith('CONTEXT_menu?'):
app.APP.command_pipeline_queue.put(
'dummy?mode=context_menu&%s'
% plex_command.replace('CONTEXT_menu?', ''))
elif plex_command == 'choose_pms_server':
if self.choose_pms_server():
utils.window('plex_online', clear=True)
app.ACCOUNT.set_unauthenticated()
server_online = False
welcome_msg = False
elif plex_command == 'switch_plex_user':
if self.switch_plex_user():
app.ACCOUNT.set_unauthenticated()
elif plex_command == 'enter_new_pms_address':
if self.setup.enter_new_pms_address():
if self.log_out():
utils.window('plex_online', clear=True)
app.ACCOUNT.set_unauthenticated()
server_online = False
welcome_msg = False
elif plex_command == 'toggle_plex_tv_sign_in':
if self.toggle_plex_tv():
app.ACCOUNT.set_unauthenticated()
elif plex_command == 'repair-scan':
app.SYNC.run_lib_scan = 'repair'
elif plex_command == 'full-scan':
app.SYNC.run_lib_scan = 'full'
elif plex_command == 'fanart-scan':
app.SYNC.run_lib_scan = 'fanart'
elif plex_command == 'textures-scan':
app.SYNC.run_lib_scan = 'textures'
continue
# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
# 3. User has access to the server
if utils.window('plex_online') == "true":
# Plex server is online
# Verify if user is set and has access to the server
if (self.user.user is not None) and self.user.has_access:
if app.CONN.pms_status == 'Stop':
xbmc.sleep(500)
continue
elif app.CONN.pms_status == '401':
# Unauthorized access, revoke token
LOG.info('401 received - revoking token')
app.ACCOUNT.clear()
app.CONN.pms_status = 'Auth'
utils.window('plex_serverStatus', value='Auth')
continue
if not app.ACCOUNT.authenticated:
LOG.info('Not yet authenticated')
# Do authentication
if not authenticate():
continue
# Start up events
if welcome_msg is True:
# Reset authentication warnings
welcome_msg = False
utils.dialog('notification',
utils.lang(29999),
"%s %s" % (utils.lang(33000),
app.ACCOUNT.plex_username),
icon='{plex}',
time=2000,
sound=False)
# Start monitoring kodi events
if not self.kodimonitor_running:
# Start up events
self.warn_auth = True
if welcome_msg is True:
# Reset authentication warnings
welcome_msg = False
utils.dialog('notification',
utils.lang(29999),
"%s %s" % (utils.lang(33000),
self.user.user),
icon='{plex}',
time=2000,
sound=False)
# Start monitoring kodi events
self.kodimonitor_running = kodimonitor.KodiMonitor()
self.specialmonitor.start()
# Start the Websocket Client
if not self.ws_running:
self.ws_running = True
self.ws.start()
# Start the Alexa thread
if (not self.alexa_running and
utils.settings('enable_alexa') == 'true'):
self.alexa_running = True
self.alexa.start()
# Start the syncing thread
if not self.sync_running:
self.sync_running = True
self.sync.start()
# Start the Plex Companion thread
if not self.plexcompanion_running:
self.plexcompanion_running = True
self.plexcompanion.start()
if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
# Start the Websocket Client
if not self.ws_running:
self.ws_running = True
self.ws.start()
# Start the Alexa thread
if (not self.alexa_running and
utils.settings('enable_alexa') == 'true'):
self.alexa_running = True
self.alexa.start()
# Start the syncing thread
if not self.sync_running:
self.sync_running = True
self.sync.start()
# Start the Plex Companion thread
if not self.plexcompanion_running:
self.plexcompanion_running = True
self.plexcompanion.start()
if not self.playback_starter_running:
self.playback_starter_running = True
self.playback_starter.start()
self.playqueue.start()
else:
if (self.user.user is None) and self.warn_auth:
# Alert user is not authenticated and suppress future
# warning
self.warn_auth = False
LOG.warn("Not authenticated yet.")
# User access is restricted.
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
while self.user.has_access is False:
# Verify access with an API call
self.user.check_access()
if utils.window('plex_online') != "true":
# Server went offline
break
if monitor.waitForAbort(3):
# Abort was requested while waiting. We should exit
break
else:
# Wait until Plex server is online
# or Kodi is shut down.
while not self._stop_pkc():
server = self.user.get_server()
if server is False:
# No server info set in add-on settings
pass
elif PF.check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online:
self.server_online = False
utils.window('plex_online', value="false")
# Suspend threads
state.SUSPEND_LIBRARY_THREAD = True
LOG.error("Plex Media Server went offline")
if utils.settings('show_pms_offline') == 'true':
utils.dialog('notification',
utils.lang(33001),
"%s %s" % (utils.lang(29999),
utils.lang(33002)),
icon='{plex}',
sound=False)
counter += 1
# Periodically check if the IP changed, e.g. per minute
if counter > 20:
counter = 0
setup = initialsetup.InitialSetup()
tmp = setup.pick_pms()
if tmp is not None:
setup.write_pms_to_settings(tmp)
else:
# Server is online
server = app.CONN.server
if not server:
# No server info set in add-on settings
pass
elif PF.check_connection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if server_online:
server_online = False
utils.window('plex_online', value="false")
# Suspend threads
app.SYNC.suspend_library_thread = True
LOG.warn("Plex Media Server went offline")
if utils.settings('show_pms_offline') == 'true':
utils.dialog('notification',
utils.lang(33001),
"%s %s" % (utils.lang(29999),
utils.lang(33002)),
icon='{plex}',
sound=False)
counter += 1
# Periodically check if the IP changed, e.g. per minute
if counter > 20:
counter = 0
if not self.server_online:
# Server was offline when Kodi started.
# Wait for server to be fully established.
if monitor.waitForAbort(5):
# Abort was requested while waiting.
break
self.server_online = True
# Alert the user that server is online.
if (welcome_msg is False and
utils.settings('show_pms_offline') == 'true'):
utils.dialog('notification',
utils.lang(29999),
utils.lang(33003),
icon='{plex}',
time=5000,
sound=False)
LOG.info("Server %s is online and ready.", server)
utils.window('plex_online', value="true")
if state.AUTHENTICATED:
# Server got offline when we were authenticated.
# Hence resume threads
state.SUSPEND_LIBRARY_THREAD = False
setup = initialsetup.InitialSetup()
tmp = setup.pick_pms()
if tmp:
setup.write_pms_to_settings(tmp)
app.CONN.load()
else:
# Server is online
counter = 0
if not server_online:
# Server was offline when Kodi started.
server_online = True
# Alert the user that server is online.
if (welcome_msg is False and
utils.settings('show_pms_offline') == 'true'):
utils.dialog('notification',
utils.lang(29999),
utils.lang(33003),
icon='{plex}',
time=5000,
sound=False)
LOG.info("Server %s is online and ready.", server)
utils.window('plex_online', value="true")
if app.ACCOUNT.authenticated:
# Server got offline when we were authenticated.
# Hence resume threads
app.SYNC.suspend_library_thread = False
# Start the userclient thread
if not self.user_running:
self.user_running = True
self.user.start()
break
if monitor.waitForAbort(3):
# Abort was requested while waiting.
break
if monitor.waitForAbort(0.05):
if self.monitor.waitForAbort(0.05):
# Abort was requested while waiting. We should exit
break
# Terminating PlexKodiConnect
# Tell all threads to terminate (e.g. several lib sync threads)
state.STOP_PKC = True
app.APP.stop_pkc = True
utils.window('plex_service_started', clear=True)
LOG.info("======== STOP %s ========", v.ADDON_NAME)

View file

@ -1,190 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
THREAD SAFE
"""
from __future__ import absolute_import, division, unicode_literals
from threading import Lock, RLock
# LOCKS
####################
# Need to lock all methods and functions messing with Plex Companion subscribers
LOCK_SUBSCRIBER = RLock()
# Need to lock everything messing with Kodi/PKC playqueues
LOCK_PLAYQUEUES = RLock()
# Necessary to temporarily hold back librarysync/websocket listener when doing
# a full sync
LOCK_PLAYLISTS = Lock()
# Quit PKC
STOP_PKC = False
# URL of our current PMS
PMS_SERVER = None
# Usually triggered by another Python instance - will have to be set (by
# polling window) through e.g. librarysync thread
SUSPEND_LIBRARY_THREAD = False
# Set if user decided to cancel sync
STOP_SYNC = False
# Set during media playback if PKC should not do any syncs. Will NOT
# suspend synching of playstate progress
SUSPEND_SYNC = False
# Could we access the paths?
PATH_VERIFIED = False
# Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true'
DB_SCAN = False
# Plex Media Server Status - along with window('plex_serverStatus')
PMS_STATUS = False
# When the userclient needs to wait
SUSPEND_USER_CLIENT = False
# Plex home user? Then "False". Along with window('plex_restricteduser')
RESTRICTED_USER = False
# Direct Paths (True) or Addon Paths (False)? Along with
# window('useDirectPaths')
DIRECT_PATHS = False
# Shall we replace custom user ratings with the number of versions available?
INDICATE_MEDIA_VERSIONS = False
# Will sync movie trailer differently: either play trailer directly or show
# all the Plex extras for the user to choose
SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER = False
# Do we need to run a special library scan?
RUN_LIB_SCAN = None
# Number of items to fetch and display in widgets
FETCH_PMS_ITEM_NUMBER = None
# Hack to force Kodi widget for "in progress" to show up if it was empty before
FORCE_RELOAD_SKIN = True
# Stemming from the PKC settings.xml
# Shall we show Kodi dialogs when synching?
SYNC_DIALOG = True
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left to sync)
IMAGE_SYNC_NOTIFICATIONS = True
# Only sync specific Plex playlists to Kodi?
SYNC_SPECIFIC_PLEX_PLAYLISTS = False
# Only sync specific Kodi playlists to Plex?
SYNC_SPECIFIC_KODI_PLAYLISTS = False
# Is synching of Plex music enabled?
ENABLE_MUSIC = True
# How often shall we sync?
FULL_SYNC_INTERVALL = 0
# Background Sync disabled?
BACKGROUND_SYNC_DISABLED = False
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
BACKGROUNDSYNC_SAFTYMARGIN = 0
# How many threads to download Plex metadata on sync?
SYNC_THREAD_NUMBER = 0
# What's the time offset between the PMS and Kodi?
KODI_PLEX_TIME_OFFSET = 0.0
# Path remapping mechanism (e.g. smb paths)
# Do we replace \\myserver\path to smb://myserver/path?
REPLACE_SMB_PATH = False
# Do we generally remap?
REMAP_PATH = False
# Mappings for REMAP_PATH:
remapSMBmovieOrg = None
remapSMBmovieNew = None
remapSMBtvOrg = None
remapSMBtvNew = None
remapSMBmusicOrg = None
remapSMBmusicNew = None
remapSMBphotoOrg = None
remapSMBphotoNew = None
# Shall we verify SSL certificates?
VERIFY_SSL_CERT = False
# Do we have an ssl certificate for PKC we need to use?
SSL_CERT_PATH = None
# Along with window('plex_authenticated')
AUTHENTICATED = False
# plex.tv username
PLEX_USERNAME = None
# Token for that user for plex.tv
PLEX_TOKEN = None
# Plex token for the active PMS for the active user
# (might be diffent to PLEX_TOKEN)
PMS_TOKEN = None
# Plex ID of that user (e.g. for plex.tv) as a STRING
PLEX_USER_ID = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
PLEX_TRANSIENT_TOKEN = None
# Plex Companion Queue()
COMPANION_QUEUE = None
# Command Pipeline Queue()
COMMAND_PIPELINE_QUEUE = None
# Websocket_client queue to communicate with librarysync
WEBSOCKET_QUEUE = None
# Which Kodi player is/has been active? (either int 1, 2 or 3)
ACTIVE_PLAYERS = set()
# Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set
# up our own playlist from the very beginning
PKC_CAUSED_STOP = False
# Flag if the 0 length PKC video has already failed so we can start resolving
# playback (set in player.py)
PKC_CAUSED_STOP_DONE = True
# Kodi player states - here, initial values are set
PLAYER_STATES = {
0: {},
1: {},
2: {}
}
# The LAST playstate once playback is finished
OLD_PLAYER_STATES = {
0: {},
1: {},
2: {}
}
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
PLAYSTATE = {
'type': None,
'time': {
'hours': 0,
'minutes': 0,
'seconds': 0,
'milliseconds': 0},
'totaltime': {
'hours': 0,
'minutes': 0,
'seconds': 0,
'milliseconds': 0},
'speed': 0,
'shuffled': False,
'repeat': 'off',
'position': None,
'playlistid': None,
'currentvideostream': -1,
'currentaudiostream': -1,
'subtitleenabled': False,
'currentsubtitle': -1,
'file': None,
'kodi_id': None,
'kodi_type': None,
'plex_id': None,
'plex_type': None,
'container_key': None,
'volume': 100,
'muted': False,
'playmethod': None,
'playcount': None
}
PLAYED_INFO = {}
# Set by SpecialMonitor - did user choose to resume playback or start from the
# beginning?
RESUME_PLAYBACK = False
# Was the playback initiated by the user using the Kodi context menu?
CONTEXT_MENU_PLAY = False
# Set by context menu - shall we force-transcode the next playing item?
FORCE_TRANSCODE = False
# Kodi webserver details
WEBSERVER_PORT = 8080
WEBSERVER_USERNAME = 'kodi'
WEBSERVER_PASSWORD = ''
WEBSERVER_HOST = 'localhost'

View file

@ -5,8 +5,8 @@ from logging import getLogger
import xbmc
from .downloadutils import DownloadUtils as DU
from . import library_sync
from . import backgroundthread, utils, path_ops, artwork, variables as v, state
from . import library_sync, timing
from . import backgroundthread, utils, path_ops, artwork, variables as v, app
from . import plex_db, kodi_db
LOG = getLogger('PLEX.sync')
@ -18,10 +18,10 @@ def set_library_scan_toggle(boolean=True):
"""
if not boolean:
# Deactivate
state.DB_SCAN = False
app.SYNC.db_scan = False
utils.window('plex_dbScan', clear=True)
else:
state.DB_SCAN = True
app.SYNC.db_scan = True
utils.window('plex_dbScan', value="true")
@ -40,11 +40,8 @@ class Sync(backgroundthread.KillableThread):
# self.lock = backgroundthread.threading.Lock()
super(Sync, self).__init__()
def isCanceled(self):
return state.STOP_PKC
def isSuspended(self):
return state.SUSPEND_LIBRARY_THREAD
return self._suspended or app.SYNC.suspend_library_thread
def show_kodi_note(self, message, icon="plex", force=False):
"""
@ -54,7 +51,7 @@ class Sync(backgroundthread.KillableThread):
icon: "plex": shows Plex icon
"error": shows Kodi error icon
"""
if not force and state.SYNC_DIALOG is not True and self.force_dialog is not True:
if not force and app.SYNC.sync_dialog is not True and self.force_dialog is not True:
return
if icon == "plex":
utils.dialog('notification',
@ -70,14 +67,14 @@ class Sync(backgroundthread.KillableThread):
def triage_lib_scans(self):
"""
Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually
Decides what to do if app.SYNC.run_lib_scan has been set. E.g. manually
triggered full or repair syncs
"""
if state.RUN_LIB_SCAN in ("full", "repair"):
if app.SYNC.run_lib_scan in ("full", "repair"):
set_library_scan_toggle()
LOG.info('Full library scan requested, starting')
self.start_library_sync(show_dialog=True,
repair=state.RUN_LIB_SCAN == 'repair',
repair=app.SYNC.run_lib_scan == 'repair',
block=True)
if self.sync_successful:
# Full library sync finished
@ -85,7 +82,7 @@ class Sync(backgroundthread.KillableThread):
elif not self.isSuspended() and not self.isCanceled():
# ERROR in library sync
self.show_kodi_note(utils.lang(39410), icon='error')
elif state.RUN_LIB_SCAN == 'fanart':
elif app.SYNC.run_lib_scan == 'fanart':
# Only look for missing fanart (No) or refresh all fanart (Yes)
from .windows import optionsdialog
refresh = optionsdialog.show(utils.lang(29999),
@ -99,7 +96,7 @@ class Sync(backgroundthread.KillableThread):
message=utils.lang(30015),
icon='{plex}',
sound=False)
elif state.RUN_LIB_SCAN == 'textures':
elif app.SYNC.run_lib_scan == 'textures':
LOG.info("Caching of images requested")
if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)):
return
@ -113,7 +110,7 @@ class Sync(backgroundthread.KillableThread):
Hit this after the full sync has finished
"""
self.sync_successful = successful
self.last_full_sync = utils.unix_timestamp()
self.last_full_sync = timing.unix_timestamp()
set_library_scan_toggle(boolean=False)
if successful:
self.show_kodi_note(utils.lang(39407))
@ -129,7 +126,7 @@ class Sync(backgroundthread.KillableThread):
def start_library_sync(self, show_dialog=None, repair=False, block=False):
set_library_scan_toggle(boolean=True)
show_dialog = show_dialog if show_dialog is not None else state.SYNC_DIALOG
show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog
library_sync.start(show_dialog, repair, self.on_library_scan_finished)
# if block:
# self.lock.acquire()
@ -153,7 +150,7 @@ class Sync(backgroundthread.KillableThread):
def on_fanart_download_finished(self, successful):
# FanartTV lookup completed
if successful and state.SYNC_DIALOG:
if successful and app.SYNC.sync_dialog:
utils.dialog('notification',
heading='{plex}',
message=utils.lang(30019),
@ -174,7 +171,7 @@ class Sync(backgroundthread.KillableThread):
try:
self._run_internal()
except:
state.DB_SCAN = False
app.SYNC.db_scan = False
utils.window('plex_dbScan', clear=True)
utils.ERROR(txt='sync.py crashed', notify=True)
raise
@ -188,12 +185,12 @@ class Sync(backgroundthread.KillableThread):
last_time_sync = 0
one_day_in_seconds = 60 * 60 * 24
# Link to Websocket queue
queue = state.WEBSOCKET_QUEUE
queue = app.APP.websocket_queue
# Kodi Version supported by PKC?
if (not path_ops.exists(v.DB_VIDEO_PATH) or
not path_ops.exists(v.DB_TEXTURE_PATH) or
(state.ENABLE_MUSIC and not path_ops.exists(v.DB_MUSIC_PATH))):
(app.SYNC.enable_music and not path_ops.exists(v.DB_MUSIC_PATH))):
# Database does not exists
LOG.error('The current Kodi version is incompatible')
LOG.error('Current Kodi version: %s', utils.try_decode(
@ -242,7 +239,7 @@ class Sync(backgroundthread.KillableThread):
self.force_dialog = True
# Initialize time offset Kodi - PMS
library_sync.sync_pms_time()
last_time_sync = utils.unix_timestamp()
last_time_sync = timing.unix_timestamp()
LOG.info('Initial start-up full sync starting')
xbmc.executebuiltin('InhibitIdleShutdown(true)')
# This call will block until scan is completed
@ -268,9 +265,9 @@ class Sync(backgroundthread.KillableThread):
# First sync upon PKC restart. Skipped if very first sync upon
# PKC installation has been completed
LOG.info('Doing initial sync on Kodi startup')
if state.SUSPEND_SYNC:
if app.SYNC.suspend_sync:
LOG.warning('Forcing startup sync even if Kodi is playing')
state.SUSPEND_SYNC = False
app.SYNC.suspend_sync = False
self.start_library_sync(block=True)
if self.sync_successful:
initial_sync_done = True
@ -285,27 +282,27 @@ class Sync(backgroundthread.KillableThread):
xbmc.sleep(1000)
# Currently no db scan, so we could start a new scan
elif state.DB_SCAN is False:
# Full scan was requested from somewhere else, e.g. userclient
if state.RUN_LIB_SCAN is not None:
elif app.SYNC.db_scan is False:
# Full scan was requested from somewhere else
if app.SYNC.run_lib_scan is not None:
# Force-show dialogs since they are user-initiated
self.force_dialog = True
self.triage_lib_scans()
self.force_dialog = False
# Reset the flag
state.RUN_LIB_SCAN = None
app.SYNC.run_lib_scan = None
continue
# Standard syncs - don't force-show dialogs
now = utils.unix_timestamp()
if (now - self.last_full_sync > state.FULL_SYNC_INTERVALL):
now = timing.unix_timestamp()
if (now - self.last_full_sync > app.SYNC.full_sync_intervall):
LOG.info('Doing scheduled full library scan')
self.start_library_sync()
elif now - last_time_sync > one_day_in_seconds:
LOG.info('Starting daily time sync')
library_sync.sync_pms_time()
last_time_sync = now
elif not state.BACKGROUND_SYNC_DISABLED:
elif not app.SYNC.background_sync_disabled:
# Check back whether we should process something Only do
# this once a while (otherwise, potentially many screen
# refreshes lead to flickering)

94
resources/lib/timing.py Normal file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from datetime import datetime, timedelta
from time import localtime, strftime
EPOCH = datetime.utcfromtimestamp(0)
# What's the time offset between the PMS and Kodi?
KODI_PLEX_TIME_OFFSET = 0.0
def unix_timestamp(seconds_into_the_future=None):
"""
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
an integer.
Optionally, pass seconds_into_the_future: positive int's will result in a
future timestamp, negative the past
"""
if seconds_into_the_future:
future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future)
else:
future = datetime.utcnow()
return int((future - EPOCH).total_seconds())
def unix_date_to_kodi(unix_kodi_time):
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
"""
return strftime('%Y-%m-%d %H:%M:%S', localtime(float(unix_kodi_time)))
def plex_date_to_kodi(plex_timestamp):
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
"""
return strftime('%Y-%m-%d %H:%M:%S',
localtime(float(plex_timestamp) + KODI_PLEX_TIME_OFFSET))
def kodi_timestamp(plex_timestamp):
return unix_date_to_kodi(plex_timestamp)
def kodi_now():
return unix_date_to_kodi(unix_timestamp())
def millis_to_kodi_time(milliseconds):
"""
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
Pass in the time in milliseconds as an int
"""
seconds = int(milliseconds / 1000)
minutes = int(seconds / 60)
seconds = seconds % 60
hours = int(minutes / 60)
minutes = minutes % 60
milliseconds = milliseconds % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': milliseconds}
def kodi_time_to_millis(time):
"""
Converts the Kodi time dict
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
to milliseconds [int]. Will not return negative results but 0!
"""
ret = (time['hours'] * 3600 +
time['minutes'] * 60 +
time['seconds']) * 1000 + time['milliseconds']
return 0 if ret < 0 else ret

View file

@ -1,356 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from threading import Thread
from xbmc import sleep, executebuiltin
from .windows import userselect
from .downloadutils import DownloadUtils as DU
from . import utils
from . import path_ops
from . import plex_functions as PF
from . import variables as v
from . import state
###############################################################################
LOG = getLogger('PLEX.userclient')
###############################################################################
@utils.thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
class UserClient(Thread):
"""
Manage Plex users
"""
# Borg - multiple instances, shared state
__shared_state = {}
def __init__(self):
self.__dict__ = self.__shared_state
self.auth = True
self.retry = 0
self.aborted = False
self.user = None
self.has_access = True
self.server = None
self.server_name = None
self.machine_identifier = None
self.token = None
self.ssl = None
self.sslcert = None
self.do_utils = None
Thread.__init__(self)
def get_server(self):
"""
Get the current PMS' URL
"""
# Original host
self.server_name = utils.settings('plex_servername')
https = utils.settings('https') == "true"
host = utils.settings('ipaddress')
port = utils.settings('port')
self.machine_identifier = utils.settings('plex_machineIdentifier')
if not host:
LOG.debug("No server information saved.")
return False
server = host + ":" + port
# If https is true
if https:
server = "https://%s" % server
# If https is false
else:
server = "http://%s" % server
# User entered IP; we need to get the machineIdentifier
if not self.machine_identifier:
self.machine_identifier = PF.GetMachineIdentifier(server)
if not self.machine_identifier:
self.machine_identifier = ''
utils.settings('plex_machineIdentifier',
value=self.machine_identifier)
LOG.debug('Returning active server: %s', server)
return server
@staticmethod
def get_ssl_verify():
"""
Do we need to verify the SSL certificate? Return None if that is the
case, else False
"""
return None if utils.settings('sslverify') == 'true' else False
@staticmethod
def get_ssl_certificate():
"""
Client side certificate
"""
return None if utils.settings('sslcert') == 'None' \
else utils.settings('sslcert')
def set_user_prefs(self):
"""
Load a user's profile picture
"""
LOG.debug('Setting user preferences')
# Only try to get user avatar if there is a token
if self.token:
url = PF.GetUserArtworkURL(self.user)
if url:
utils.window('PlexUserImage', value=url)
@staticmethod
def check_access():
# Plex: always return True for now
return True
def load_user(self, username, user_id, usertoken, authenticated=False):
"""
Load the current user's details for PKC
"""
LOG.debug('Loading current user')
self.token = usertoken
self.server = self.get_server()
self.ssl = self.get_ssl_verify()
self.sslcert = self.get_ssl_certificate()
if authenticated is False:
if self.server is None:
return False
LOG.debug('Testing validity of current token')
res = PF.check_connection(self.server,
token=self.token,
verifySSL=self.ssl)
if res is False:
# PMS probably offline
return False
elif res == 401:
LOG.error('Token is no longer valid')
return 401
elif res >= 400:
LOG.error('Answer from PMS is not as expected. Retrying')
return False
# Set to windows property
state.PLEX_USER_ID = user_id or None
state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '')
utils.window('pms_token', value=usertoken)
state.PMS_TOKEN = usertoken
# This is the token for plex.tv for the current user
# Is only '' if user is not signed in to plex.tv
utils.window('plex_token', value=utils.settings('plexToken'))
state.PLEX_TOKEN = utils.settings('plexToken') or None
utils.window('plex_restricteduser',
value=utils.settings('plex_restricteduser'))
state.RESTRICTED_USER = True \
if utils.settings('plex_restricteduser') == 'true' else False
utils.window('pms_server', value=self.server)
state.PMS_SERVER = self.server
utils.window('plex_machineIdentifier', value=self.machine_identifier)
utils.window('plex_servername', value=self.server_name)
utils.window('plex_authenticated', value='true')
state.AUTHENTICATED = True
utils.window('useDirectPaths',
value='true' if utils.settings('useDirectPaths') == "1"
else 'false')
state.DIRECT_PATHS = True if utils.settings('useDirectPaths') == "1" \
else False
state.INDICATE_MEDIA_VERSIONS = True \
if utils.settings('indicate_media_versions') == "true" else False
utils.window('plex_force_transcode_pix',
value='true' if utils.settings('force_transcode_pix') == "1"
else 'false')
# Start DownloadUtils session
self.do_utils = DU()
self.do_utils.startSession(reset=True)
# Set user preferences in settings
self.user = username
self.set_user_prefs()
# Writing values to settings file
utils.settings('username', value=username)
utils.settings('userid', value=user_id)
utils.settings('accessToken', value=usertoken)
return True
def authenticate(self):
"""
Authenticate the current user
"""
LOG.debug('Authenticating user')
# Give attempts at entering password / selecting user
if self.retry > 0:
if not self.aborted:
LOG.error("Too many retries to login.")
state.PMS_STATUS = 'Stop'
# Failed to authenticate. Did you login to plex.tv?
utils.messageDialog(utils.lang(29999),utils.lang(39023))
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
return False
# If there's no settings.xml
if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE):
LOG.error("Error, no settings.xml found.")
self.auth = False
return False
server = self.get_server()
# If there is no server we can connect to
if not server:
LOG.info("Missing server information.")
self.auth = False
return False
# If there is a username in the settings, try authenticating
username = utils.settings('username')
userId = utils.settings('userid')
usertoken = utils.settings('accessToken')
enforceLogin = utils.settings('enforceUserLogin')
# Found a user in the settings, try to authenticate
if username and enforceLogin == 'false':
LOG.debug('Trying to authenticate with old settings')
answ = self.load_user(username,
userId,
usertoken,
authenticated=False)
if answ is True:
# SUCCESS: loaded a user from the settings
return True
elif answ == 401:
LOG.error("User token no longer valid. Sign user out")
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('accessToken', value='')
else:
LOG.debug("Could not yet authenticate user")
return False
# Could not use settings - try to get Plex user list from plex.tv
plextoken = utils.settings('plexToken')
if plextoken:
LOG.info("Trying to connect to plex.tv to get a user list")
user, self.aborted = userselect.start()
if not user:
# FAILURE: Something went wrong, try again
self.auth = True
self.retry += 1
return False
username = user.title
user_id = user.id
usertoken = user.authToken
else:
LOG.info("Trying to authenticate without a token")
username = ''
user_id = ''
usertoken = ''
if self.load_user(username, user_id, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings
return True
# Something went wrong, try again
self.auth = True
self.retry += 1
return False
def reset_client(self):
"""
Reset all user settings
"""
LOG.debug("Reset UserClient authentication.")
try:
self.do_utils.stopSession()
except AttributeError:
pass
utils.window('plex_authenticated', clear=True)
state.AUTHENTICATED = False
utils.window('pms_token', clear=True)
state.PLEX_TOKEN = None
state.PLEX_TRANSIENT_TOKEN = None
state.PMS_TOKEN = None
utils.window('plex_token', clear=True)
utils.window('pms_server', clear=True)
state.PMS_SERVER = None
utils.window('plex_machineIdentifier', clear=True)
utils.window('plex_servername', clear=True)
state.PLEX_USER_ID = None
state.PLEX_USERNAME = None
utils.window('plex_restricteduser', clear=True)
state.RESTRICTED_USER = False
utils.settings('username', value='')
utils.settings('userid', value='')
utils.settings('accessToken', value='')
self.token = None
self.auth = True
self.user = None
self.retry = 0
def run(self):
"""
Do the work
"""
LOG.info("----===## Starting UserClient ##===----")
stopped = self.stopped
suspended = self.suspended
while not stopped():
while suspended():
if stopped():
break
sleep(1000)
if state.PMS_STATUS == "Stop":
sleep(500)
continue
elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token
state.PMS_STATUS = 'Auth'
utils.window('plex_serverStatus', value='Auth')
self.reset_client()
sleep(3000)
if self.auth and (self.user is None):
# Try to authenticate user
if not state.PMS_STATUS or state.PMS_STATUS == "Auth":
# Set auth flag because we no longer need
# to authenticate the user
self.auth = False
if self.authenticate():
# Successfully authenticated and loaded a user
LOG.info("Successfully authenticated!")
LOG.info("Current user: %s", self.user)
LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False
utils.window('plex_serverStatus', clear=True)
state.PMS_STATUS = False
if not self.auth and (self.user is None):
# Loop if no server found
server = self.get_server()
# The status Stop is for when user cancelled password dialog.
# Or retried too many times
if server and state.PMS_STATUS != "Stop":
# Only if there's information found to login
LOG.debug("Server found: %s", server)
self.auth = True
# Minimize CPU load
sleep(100)
LOG.info("##===---- UserClient Stopped ----===##")

View file

@ -6,8 +6,7 @@ Various functions and decorators for PKC
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from sqlite3 import connect, OperationalError
from datetime import datetime, timedelta
from time import localtime, strftime
from datetime import datetime
from unicodedata import normalize
try:
import xml.etree.cElementTree as etree
@ -19,7 +18,7 @@ except ImportError:
import defusedxml.ElementTree as defused_etree # etree parse unsafe
from xml.etree.ElementTree import ParseError
ETREE = 'ElementTree'
from functools import wraps, partial
from functools import wraps
from urllib import quote_plus
import hashlib
import re
@ -28,7 +27,7 @@ import xbmc
import xbmcaddon
import xbmcgui
from . import path_ops, variables as v, state
from . import path_ops, variables as v
###############################################################################
@ -36,7 +35,6 @@ LOG = getLogger('PLEX.utils')
WINDOW = xbmcgui.Window(10000)
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
EPOCH = datetime.utcfromtimestamp(0)
# Grab Plex id from '...plex_id=XXXX....'
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
@ -107,17 +105,14 @@ def window(prop, value=None, clear=False, windowid=10000):
return try_decode(win.getProperty(prop))
def plex_command(key, value):
def plex_command(value):
"""
Used to funnel states between different Python instances. NOT really thread
safe - let's hope the Kodi user can't click fast enough
key: state.py variable
value: either 'True' or 'False'
"""
while window('plex_command'):
xbmc.sleep(20)
window('plex_command', value='%s-%s' % (key, value))
window('plex_command', value=value)
def settings(setting, value=None):
@ -238,7 +233,8 @@ def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False):
short = str(sys.exc_info()[1])
LOG.error('Error encountered: %s - %s', txt, short)
if cancel_sync:
state.STOP_SYNC = True
import app
app.SYNC.stop_sync = True
if hide_tb:
return short
@ -275,47 +271,6 @@ class AttributeDict(dict):
return self.__unicode__().encode('utf-8')
def millis_to_kodi_time(milliseconds):
"""
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
Pass in the time in milliseconds as an int
"""
seconds = int(milliseconds / 1000)
minutes = int(seconds / 60)
seconds = seconds % 60
hours = int(minutes / 60)
minutes = minutes % 60
milliseconds = milliseconds % 1000
return {'hours': hours,
'minutes': minutes,
'seconds': seconds,
'milliseconds': milliseconds}
def kodi_time_to_millis(time):
"""
Converts the Kodi time dict
{
'hours': [int],
'minutes': [int],
'seconds'[int],
'milliseconds': [int]
}
to milliseconds [int]. Will not return negative results but 0!
"""
ret = (time['hours'] * 3600 +
time['minutes'] * 60 +
time['seconds']) * 1000 + time['milliseconds']
ret = 0 if ret < 0 else ret
return ret
def cast(func, value):
"""
Cast the specified value to the specified type (returned by func). Currently this
@ -434,47 +389,6 @@ def escape_html(string):
return string
def unix_date_to_kodi(stamp):
"""
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
propper, human-readable time stamp used by Kodi
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
None if an error was encountered
"""
try:
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
date_time = localtime(stamp)
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
except:
localdate = None
return localdate
def kodi_time_to_plex(stamp):
"""
Returns a Kodi timestamp (int/float) in Plex time (subtracting the
KODI_PLEX_TIME_OFFSET)
"""
return stamp - state.KODI_PLEX_TIME_OFFSET
def unix_timestamp(seconds_into_the_future=None):
"""
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
an integer.
Optionally, pass seconds_into_the_future: positive int's will result in a
future timestamp, negative the past
"""
if seconds_into_the_future:
future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future)
else:
future = datetime.utcnow()
return int((future - EPOCH).total_seconds())
def kodi_sql(media_type=None):
"""
Open a connection to the Kodi database.
@ -1135,95 +1049,3 @@ def log_time(func):
elapsedtotal, func.__name__)
return result
return wrapper
def thread_methods(cls=None, add_stops=None, add_suspends=None):
"""
Decorator to add the following methods to a threading class:
suspend(): pauses the thread
resume(): resumes the thread
stop(): stopps/kills the thread
suspended(): returns True if thread is suspended
stopped(): returns True if thread is stopped (or should stop ;-))
ALSO returns True if PKC should exit
Also adds the following class attributes:
thread_stopped
thread_suspended
stops
suspends
invoke with either
@thread_methods
class MyClass():
or
@thread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'],
add_suspends=['DB_SCAN', 'WHATEVER'])
class MyClass():
"""
# So we don't need to invoke with ()
if cls is None:
return partial(thread_methods,
add_stops=add_stops,
add_suspends=add_suspends)
# Because we need a reference, not a copy of the immutable objects in
# state, we need to look up state every time explicitly
cls.stops = ['STOP_PKC']
if add_stops is not None:
cls.stops.extend(add_stops)
cls.suspends = add_suspends or []
# Attach new attributes to class
cls.thread_stopped = False
cls.thread_suspended = False
# Define new class methods and attach them to class
def stop(self):
"""
Call to stop this thread
"""
self.thread_stopped = True
cls.stop = stop
def suspend(self):
"""
Call to suspend this thread
"""
self.thread_suspended = True
cls.suspend = suspend
def resume(self):
"""
Call to revive a suspended thread back to life
"""
self.thread_suspended = False
cls.resume = resume
def suspended(self):
"""
Returns True if the thread is suspended
"""
if self.thread_suspended is True:
return True
for suspend in self.suspends:
if getattr(state, suspend):
return True
return False
cls.suspended = suspended
def stopped(self):
"""
Returns True if the thread is stopped
"""
if self.thread_stopped is True:
return True
for stop in self.stops:
if getattr(state, stop):
return True
return False
cls.stopped = stopped
# Return class to render this a decorator
return cls

View file

@ -2,11 +2,10 @@
# -*- coding: utf-8 -*-
from logging import getLogger
from json import loads
from threading import Thread
from ssl import CERT_NONE
from xbmc import sleep
from . import websocket, utils, companion, state, variables as v
from . import backgroundthread, websocket, utils, companion, app, variables as v
###############################################################################
@ -15,7 +14,7 @@ LOG = getLogger('PLEX.websocket_client')
###############################################################################
class WebSocket(Thread):
class WebSocket(backgroundthread.KillableThread):
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
def __init__(self):
@ -48,18 +47,15 @@ class WebSocket(Thread):
def run(self):
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
counter = 0
stopped = self.stopped
suspended = self.suspended
while not stopped():
while not self.isCanceled():
# In the event the server goes offline
while suspended():
while self.isSuspended():
# Set in service.py
if self.ws is not None:
self.ws.close()
self.ws = None
if stopped():
if self.isCanceled():
# Abort was requested while waiting. We should exit
LOG.info("##===---- %s Stopped ----===##",
self.__class__.__name__)
@ -133,14 +129,20 @@ class WebSocket(Thread):
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'BACKGROUND_SYNC_DISABLED'])
class PMS_Websocket(WebSocket):
"""
Websocket connection with the PMS for Plex Companion
"""
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
return (self._suspended or
app.SYNC.suspend_library_thread or
app.SYNC.background_sync_disabled)
def getUri(self):
server = utils.window('pms_server')
server = app.CONN.server
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
@ -148,8 +150,8 @@ class PMS_Websocket(WebSocket):
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
# Need to use plex.tv token, if any. NOT user token
if state.PLEX_TOKEN:
uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN
if app.ACCOUNT.plex_token:
uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token
sslopt = {}
if utils.settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
@ -185,30 +187,33 @@ class PMS_Websocket(WebSocket):
# Drop everything we're not interested in
if typus not in ('playing', 'timeline', 'activity'):
return
elif typus == 'activity' and state.DB_SCAN is True:
elif typus == 'activity' and app.SYNC.db_scan is True:
# Only add to processing if PKC is NOT doing a lib scan (and thus
# possibly causing these reprocessing messages en mass)
LOG.debug('%s: Dropping message as PKC is currently synching',
self.__class__.__name__)
else:
# Put PMS message on queue and let libsync take care of it
state.WEBSOCKET_QUEUE.put(message)
app.APP.websocket_queue.put(message)
class Alexa_Websocket(WebSocket):
"""
Websocket connection to talk to Amazon Alexa.
Can't use utils.thread_methods!
"""
thread_stopped = False
thread_suspended = False
def isSuspended(self):
"""
Overwrite method since we need to check for plex token
"""
return (self._suspended or
not app.ACCOUNT.plex_token or
app.ACCOUNT.restricted_user)
def getUri(self):
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (state.PLEX_USER_ID,
% (app.ACCOUNT.plex_user_id,
v.PKC_MACHINE_IDENTIFIER,
state.PLEX_TOKEN))
app.ACCOUNT.plex_token))
sslopt = {}
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)
@ -238,33 +243,3 @@ class Alexa_Websocket(WebSocket):
self.__class__.__name__)
return
companion.process_command(message.attrib['path'][1:], message.attrib)
# Path in utils.thread_methods
def stop(self):
self.thread_stopped = True
def suspend(self):
self.thread_suspended = True
def resume(self):
self.thread_suspended = False
def stopped(self):
if self.thread_stopped is True:
return True
if state.STOP_PKC:
return True
return False
# The culprit
def suspended(self):
"""
Overwrite method since we need to check for plex token
"""
if self.thread_suspended is True:
return True
if not state.PLEX_TOKEN:
return True
if state.RESTRICTED_USER:
return True
return False

View file

@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
import xbmcgui
from . import kodigui
from .. import utils, variables as v
from .. import variables as v
class Background(kodigui.BaseWindow):

View file

@ -16,7 +16,6 @@
<setting id="enforceUserLogin" label="30536" type="bool" default="false" />
<setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials -->
<setting label="30505" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" /><!-- reset connection attempts -->
<setting id="accessToken" type="text" visible="false" default="" />
</category>