Reprogram part 1
This commit is contained in:
parent
5cdda0e334
commit
dbe0339b71
8 changed files with 1170 additions and 22 deletions
105
resources/lib/kodijsonrpc.py
Normal file
105
resources/lib/kodijsonrpc.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
import xbmc
|
||||
import json
|
||||
|
||||
|
||||
class JSONRPCMethod:
|
||||
|
||||
class Exception(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.family = None
|
||||
|
||||
def __getattr__(self, method):
|
||||
def handler(**kwargs):
|
||||
command = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 1,
|
||||
'method': '{0}.{1}'.format(self.family, method)
|
||||
}
|
||||
|
||||
if kwargs:
|
||||
command['params'] = kwargs
|
||||
|
||||
# xbmc.log(json.dumps(command))
|
||||
ret = json.loads(xbmc.executeJSONRPC(json.dumps(command)))
|
||||
|
||||
if ret:
|
||||
if 'error' in ret:
|
||||
raise self.Exception(ret['error'])
|
||||
else:
|
||||
return ret['result']
|
||||
else:
|
||||
return None
|
||||
|
||||
return handler
|
||||
|
||||
def __call__(self, family):
|
||||
self.family = family
|
||||
return self
|
||||
|
||||
|
||||
class KodiJSONRPC:
|
||||
def __init__(self):
|
||||
self.methodHandler = JSONRPCMethod()
|
||||
|
||||
def __getattr__(self, family):
|
||||
return self.methodHandler(family)
|
||||
|
||||
|
||||
rpc = KodiJSONRPC()
|
||||
|
||||
|
||||
class BuiltInMethod:
|
||||
|
||||
class Exception(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.module = None
|
||||
|
||||
def __getattr__(self, method):
|
||||
def handler(*args, **kwargs):
|
||||
args = [str(a).replace(',', '\,') for a in args]
|
||||
for k, v in kwargs.items():
|
||||
args.append('{0}={v}'.format(k, str(v).replace(',', '\,')))
|
||||
|
||||
if args:
|
||||
command = '{0}.{1}({2})'.format(self.module, method, ','.join(args))
|
||||
else:
|
||||
command = '{0}.{1}'.format(self.module, method)
|
||||
|
||||
xbmc.log(command, xbmc.LOGNOTICE)
|
||||
|
||||
xbmc.executebuiltin(command)
|
||||
|
||||
return handler
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
args = [str(a).replace(',', '\,') for a in args]
|
||||
for k, v in kwargs.items():
|
||||
args.append('{0}={v}'.format(k, str(v).replace(',', '\,')))
|
||||
|
||||
if args:
|
||||
command = '{0}({1})'.format(self.module, ','.join(args))
|
||||
else:
|
||||
command = '{0}'.format(self.module)
|
||||
|
||||
xbmc.log(command, xbmc.LOGNOTICE)
|
||||
|
||||
xbmc.executebuiltin(command)
|
||||
|
||||
def initModule(self, module):
|
||||
self.module = module
|
||||
return self
|
||||
|
||||
|
||||
class KodiBuiltin:
|
||||
def __init__(self):
|
||||
self.methodHandler = BuiltInMethod()
|
||||
|
||||
def __getattr__(self, module):
|
||||
return self.methodHandler.initModule(module)
|
||||
|
||||
|
||||
builtin = KodiBuiltin()
|
|
@ -3,8 +3,14 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import gc
|
||||
|
||||
import xbmc
|
||||
|
||||
from . import plex, util, backgroundthread
|
||||
from .plexnet import plexapp, threadutils
|
||||
|
||||
from . import utils
|
||||
from . import userclient
|
||||
from . import initialsetup
|
||||
|
@ -23,7 +29,7 @@ from . import loghandler
|
|||
|
||||
###############################################################################
|
||||
loghandler.config()
|
||||
LOG = logging.getLogger("PLEX.service_entry")
|
||||
LOG = logging.getLogger("PLEX.main")
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -264,22 +270,104 @@ class Service():
|
|||
LOG.info("======== STOP %s ========", v.ADDON_NAME)
|
||||
|
||||
|
||||
def start():
|
||||
# Safety net - Kody starts PKC twice upon first installation!
|
||||
if utils.window('plex_service_started') == 'true':
|
||||
EXIT = True
|
||||
else:
|
||||
utils.window('plex_service_started', value='true')
|
||||
EXIT = False
|
||||
def waitForThreads():
|
||||
LOG.debug('Checking for any remaining threads')
|
||||
while len(threading.enumerate()) > 1:
|
||||
for t in threading.enumerate():
|
||||
if t != threading.currentThread():
|
||||
if t.isAlive():
|
||||
LOG.debug('Waiting on thread: %s', t.name)
|
||||
if isinstance(t, threading._Timer):
|
||||
t.cancel()
|
||||
t.join()
|
||||
elif isinstance(t, threadutils.KillableThread):
|
||||
t.kill(force_and_wait=True)
|
||||
else:
|
||||
t.join()
|
||||
LOG.debug('All threads done')
|
||||
|
||||
# Delay option
|
||||
DELAY = int(utils.settings('startupDelay'))
|
||||
|
||||
LOG.info("Delaying Plex startup by: %s sec...", DELAY)
|
||||
if EXIT:
|
||||
LOG.error('PKC service.py already started - exiting this instance')
|
||||
elif DELAY and xbmc.Monitor().waitForAbort(DELAY):
|
||||
# Start the service
|
||||
LOG.info("Abort requested while waiting. PKC not started.")
|
||||
else:
|
||||
Service().ServiceEntryPoint()
|
||||
def signout():
|
||||
util.setSetting('auth.token', '')
|
||||
LOG.info('Signing out...')
|
||||
plexapp.ACCOUNT.signOut()
|
||||
|
||||
|
||||
def main():
|
||||
LOG.info('Starting %s', util.ADDON.getAddonInfo('version'))
|
||||
LOG.info('User-agent: %s', plex.defaultUserAgent())
|
||||
|
||||
try:
|
||||
while not xbmc.abortRequested:
|
||||
if plex.init():
|
||||
while not xbmc.abortRequested:
|
||||
if (
|
||||
not plexapp.ACCOUNT.isOffline and not
|
||||
plexapp.ACCOUNT.isAuthenticated and
|
||||
(len(plexapp.ACCOUNT.homeUsers) > 1 or plexapp.ACCOUNT.isProtected)
|
||||
|
||||
):
|
||||
result = userselect.start()
|
||||
if not result:
|
||||
return
|
||||
elif result == 'signout':
|
||||
signout()
|
||||
break
|
||||
elif result == 'signin':
|
||||
break
|
||||
LOG.info('User selected')
|
||||
|
||||
try:
|
||||
selectedServer = plexapp.SERVERMANAGER.selectedServer
|
||||
|
||||
if not selectedServer:
|
||||
LOG.debug('Waiting for selected server...')
|
||||
for timeout, skip_preferred, skip_owned in ((10, True, False), (10, True, True)):
|
||||
plex.CallbackEvent(plexapp.APP, 'change:selectedServer', timeout=timeout).wait()
|
||||
|
||||
selectedServer = plexapp.SERVERMANAGER.checkSelectedServerSearch(skip_preferred=skip_preferred, skip_owned=skip_owned)
|
||||
if selectedServer:
|
||||
break
|
||||
else:
|
||||
LOG.debug('Finished waiting for selected server...')
|
||||
|
||||
LOG.info('Starting with server: %s', selectedServer)
|
||||
|
||||
windowutils.HOME = home.HomeWindow.open()
|
||||
util.CRON.cancelReceiver(windowutils.HOME)
|
||||
|
||||
if not windowutils.HOME.closeOption:
|
||||
return
|
||||
|
||||
closeOption = windowutils.HOME.closeOption
|
||||
|
||||
windowutils.shutdownHome()
|
||||
|
||||
if closeOption == 'signout':
|
||||
signout()
|
||||
break
|
||||
elif closeOption == 'switch':
|
||||
plexapp.ACCOUNT.isAuthenticated = False
|
||||
finally:
|
||||
windowutils.shutdownHome()
|
||||
gc.collect(2)
|
||||
|
||||
else:
|
||||
break
|
||||
except:
|
||||
util.ERROR()
|
||||
finally:
|
||||
LOG.info('SHUTTING DOWN...')
|
||||
# player.shutdown()
|
||||
plexapp.APP.preShutdown()
|
||||
if util.CRON:
|
||||
util.CRON.stop()
|
||||
backgroundthread.BGThreader.shutdown()
|
||||
plexapp.APP.shutdown()
|
||||
waitForThreads()
|
||||
LOG.info('SHUTDOWN FINISHED')
|
||||
|
||||
from .windows import kodigui
|
||||
kodigui.MONITOR = None
|
||||
util.shutdown()
|
||||
gc.collect(2)
|
390
resources/lib/plex.py
Normal file
390
resources/lib/plex.py
Normal file
|
@ -0,0 +1,390 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import logging
|
||||
import sys
|
||||
import platform
|
||||
import uuid
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
|
||||
import xbmc
|
||||
|
||||
from .plexnet import plexapp, myplex
|
||||
from . import util
|
||||
|
||||
LOG = logging.getLogger('PLEX.plex')
|
||||
|
||||
|
||||
class PlexTimer(plexapp.Timer):
|
||||
def shouldAbort(self):
|
||||
return xbmc.abortRequested
|
||||
|
||||
|
||||
def abortFlag():
|
||||
return util.MONITOR.abortRequested()
|
||||
|
||||
|
||||
plexapp.setTimer(PlexTimer)
|
||||
plexapp.setAbortFlagFunction(abortFlag)
|
||||
|
||||
maxVideoRes = plexapp.Res((3840, 2160)) # INTERFACE.globals["supports4k"] and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080))
|
||||
|
||||
CLIENT_ID = util.getSetting('client.ID')
|
||||
if not CLIENT_ID:
|
||||
CLIENT_ID = str(uuid.uuid4())
|
||||
util.setSetting('client.ID', CLIENT_ID)
|
||||
|
||||
|
||||
def defaultUserAgent():
|
||||
"""Return a string representing the default user agent."""
|
||||
_implementation = platform.python_implementation()
|
||||
|
||||
if _implementation == 'CPython':
|
||||
_implementation_version = platform.python_version()
|
||||
elif _implementation == 'PyPy':
|
||||
_implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major,
|
||||
sys.pypy_version_info.minor,
|
||||
sys.pypy_version_info.micro)
|
||||
if sys.pypy_version_info.releaselevel != 'final':
|
||||
_implementation_version = ''.join([_implementation_version, sys.pypy_version_info.releaselevel])
|
||||
elif _implementation == 'Jython':
|
||||
_implementation_version = platform.python_version() # Complete Guess
|
||||
elif _implementation == 'IronPython':
|
||||
_implementation_version = platform.python_version() # Complete Guess
|
||||
else:
|
||||
_implementation_version = 'Unknown'
|
||||
|
||||
try:
|
||||
p_system = platform.system()
|
||||
p_release = platform.release()
|
||||
except IOError:
|
||||
p_system = 'Unknown'
|
||||
p_release = 'Unknown'
|
||||
|
||||
return " ".join(['%s/%s' % ('Plex-for-Kodi', util.ADDON.getAddonInfo('version')),
|
||||
'%s/%s' % ('Kodi', xbmc.getInfoLabel('System.BuildVersion').replace(' ', '-')),
|
||||
'%s/%s' % (_implementation, _implementation_version),
|
||||
'%s/%s' % (p_system, p_release)])
|
||||
|
||||
class PlexInterface(plexapp.AppInterface):
|
||||
_regs = {
|
||||
None: {},
|
||||
}
|
||||
_globals = {
|
||||
'platform': 'Kodi',
|
||||
'appVersionStr': util.ADDON.getAddonInfo('version'),
|
||||
'clientIdentifier': CLIENT_ID,
|
||||
'platformVersion': xbmc.getInfoLabel('System.BuildVersion'),
|
||||
'product': 'Plex for Kodi',
|
||||
'provides': 'player',
|
||||
'device': util.getPlatform() or plexapp.PLATFORM,
|
||||
'model': 'Unknown',
|
||||
'friendlyName': 'Kodi Add-on ({0})'.format(platform.node()),
|
||||
'supports1080p60': True,
|
||||
'vp9Support': True,
|
||||
'transcodeVideoQualities': [
|
||||
"10", "20", "30", "30", "40", "60", "60", "75", "100", "60", "75", "90", "100", "100"
|
||||
],
|
||||
'transcodeVideoResolutions': [
|
||||
plexapp.Res((220, 180)),
|
||||
plexapp.Res((220, 128)),
|
||||
plexapp.Res((284, 160)),
|
||||
plexapp.Res((420, 240)),
|
||||
plexapp.Res((576, 320)),
|
||||
plexapp.Res((720, 480)),
|
||||
plexapp.Res((1024, 768)),
|
||||
plexapp.Res((1280, 720)),
|
||||
plexapp.Res((1280, 720)),
|
||||
maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes, maxVideoRes
|
||||
],
|
||||
'transcodeVideoBitrates': [
|
||||
"64", "96", "208", "320", "720", "1500", "2000", "3000", "4000", "8000", "10000", "12000", "20000", "200000"
|
||||
],
|
||||
'deviceInfo': plexapp.DeviceInfo()
|
||||
}
|
||||
|
||||
def getPreference(self, pref, default=None):
|
||||
if pref == 'manual_connections':
|
||||
return self.getManualConnections()
|
||||
else:
|
||||
return util.getSetting(pref, default)
|
||||
|
||||
def getManualConnections(self):
|
||||
conns = []
|
||||
for i in range(2):
|
||||
ip = util.getSetting('manual_ip_{0}'.format(i))
|
||||
if not ip:
|
||||
continue
|
||||
port = util.getSetting('manual_port_{0}'.format(i), 32400)
|
||||
conns.append({'connection': ip, 'port': port})
|
||||
return json.dumps(conns)
|
||||
|
||||
def setPreference(self, pref, value):
|
||||
util.setSetting(pref, value)
|
||||
|
||||
def getRegistry(self, reg, default=None, sec=None):
|
||||
if sec == 'myplex' and reg == 'MyPlexAccount':
|
||||
ret = util.getSetting('{0}.{1}'.format(sec, reg), default)
|
||||
if ret:
|
||||
return ret
|
||||
return json.dumps({'authToken': util.getSetting('auth.token')})
|
||||
else:
|
||||
return util.getSetting('{0}.{1}'.format(sec, reg), default)
|
||||
|
||||
def setRegistry(self, reg, value, sec=None):
|
||||
util.setSetting('{0}.{1}'.format(sec, reg), value)
|
||||
|
||||
def clearRegistry(self, reg, sec=None):
|
||||
util.setSetting('{0}.{1}'.format(sec, reg), '')
|
||||
|
||||
def addInitializer(self, sec):
|
||||
pass
|
||||
|
||||
def clearInitializer(self, sec):
|
||||
pass
|
||||
|
||||
def getGlobal(self, glbl, default=None):
|
||||
if glbl == 'transcodeVideoResolutions':
|
||||
maxres = self.getPreference('allow_4k', True) and plexapp.Res((3840, 2160)) or plexapp.Res((1920, 1080))
|
||||
self._globals['transcodeVideoResolutions'][-5:] = [maxres] * 5
|
||||
return self._globals.get(glbl, default)
|
||||
|
||||
def getCapabilities(self):
|
||||
return ''
|
||||
|
||||
def LOG(self, msg):
|
||||
LOG.debug('API: %s', msg)
|
||||
|
||||
def DEBUG_LOG(self, msg):
|
||||
LOG.debug('API: %s', msg)
|
||||
|
||||
def WARN_LOG(self, msg):
|
||||
LOG.warn('API: %s', msg)
|
||||
|
||||
def ERROR_LOG(self, msg):
|
||||
LOG.error('API: %s', msg)
|
||||
|
||||
def ERROR(self, msg=None, err=None):
|
||||
if err:
|
||||
LOG.error('%s - %s', msg, err.message)
|
||||
else:
|
||||
util.ERROR()
|
||||
|
||||
def supportsAudioStream(self, codec, channels):
|
||||
return True
|
||||
# if codec = invalid then return true
|
||||
|
||||
# canDownmix = (m.globals["audioDownmix"][codec] <> invalid)
|
||||
# supportsSurroundSound = m.SupportsSurroundSound()
|
||||
|
||||
# if not supportsSurroundSound and canDownmix then
|
||||
# maxChannels = m.globals["audioDownmix"][codec]
|
||||
# else
|
||||
# maxChannels = firstOf(m.globals["audioDecoders"][codec], 0)
|
||||
# end if
|
||||
|
||||
# if maxChannels > 2 and not canDownmix and not supportsSurroundSound then
|
||||
# ' It's a surround sound codec and we can't do surround sound
|
||||
# supported = false
|
||||
# else if maxChannels = 0 or maxChannels < channels then
|
||||
# ' The codec is either unsupported or can't handle the requested channels
|
||||
# supported = false
|
||||
# else
|
||||
# supported = true
|
||||
|
||||
# return supported
|
||||
|
||||
def supportsSurroundSound(self):
|
||||
return True
|
||||
|
||||
def getQualityIndex(self, qualityType):
|
||||
if qualityType == self.QUALITY_LOCAL:
|
||||
return self.getPreference("local_quality", 13)
|
||||
elif qualityType == self.QUALITY_ONLINE:
|
||||
return self.getPreference("online_quality", 8)
|
||||
else:
|
||||
return self.getPreference("remote_quality", 13)
|
||||
|
||||
def getMaxResolution(self, quality_type, allow4k=False):
|
||||
qualityIndex = self.getQualityIndex(quality_type)
|
||||
|
||||
if qualityIndex >= 9:
|
||||
if self.getPreference('allow_4k', True):
|
||||
return allow4k and 2160 or 1088
|
||||
else:
|
||||
return 1088
|
||||
elif qualityIndex >= 6:
|
||||
return 720
|
||||
elif qualityIndex >= 5:
|
||||
return 480
|
||||
else:
|
||||
return 360
|
||||
|
||||
|
||||
plexapp.setInterface(PlexInterface())
|
||||
plexapp.setUserAgent(defaultUserAgent())
|
||||
|
||||
|
||||
class CallbackEvent(plexapp.CompatEvent):
|
||||
def __init__(self, context, signal, timeout=15, *args, **kwargs):
|
||||
threading._Event.__init__(self, *args, **kwargs)
|
||||
self.start = time.time()
|
||||
self.context = context
|
||||
self.signal = signal
|
||||
self.timeout = timeout
|
||||
self.context.on(self.signal, self.set)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.wait()
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0}:{1}>'.format(self.__class__.__name__, self.signal)
|
||||
|
||||
def set(self, **kwargs):
|
||||
threading._Event.set(self)
|
||||
|
||||
def wait(self):
|
||||
if not threading._Event.wait(self, self.timeout):
|
||||
LOG.debug('%s: TIMED-OUT', self)
|
||||
self.close()
|
||||
|
||||
def triggeredOrTimedOut(self, timeout=None):
|
||||
try:
|
||||
if time.time() - self.start() > self.timeout:
|
||||
LOG.debug('%s: TIMED-OUT', self)
|
||||
return True
|
||||
|
||||
if timeout:
|
||||
threading._Event.wait(self, timeout)
|
||||
finally:
|
||||
return self.isSet()
|
||||
|
||||
def close(self):
|
||||
self.set()
|
||||
self.context.off(self.signal, self.set)
|
||||
|
||||
|
||||
def init():
|
||||
LOG.info('Initializing')
|
||||
|
||||
with CallbackEvent(plexapp.APP, 'init'):
|
||||
plexapp.init()
|
||||
LOG.info('Waiting for account initialization...')
|
||||
|
||||
retry = True
|
||||
|
||||
while retry:
|
||||
retry = False
|
||||
if not plexapp.ACCOUNT.authToken:
|
||||
token = authorize()
|
||||
|
||||
if not token:
|
||||
LOG.info('FAILED TO AUTHORIZE')
|
||||
return False
|
||||
|
||||
with CallbackEvent(plexapp.APP, 'account:response'):
|
||||
plexapp.ACCOUNT.validateToken(token)
|
||||
LOG.info('Waiting for account initialization')
|
||||
|
||||
# if not PLEX:
|
||||
# util.messageDialog('Connection Error', u'Unable to connect to any servers')
|
||||
# util.DEBUG_LOG('SIGN IN: Failed to connect to any servers')
|
||||
# return False
|
||||
|
||||
# util.DEBUG_LOG('SIGN IN: Connected to server: {0} - {1}'.format(PLEX.friendlyName, PLEX.baseuri))
|
||||
success = requirePlexPass()
|
||||
if success == 'RETRY':
|
||||
retry = True
|
||||
continue
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def requirePlexPass():
|
||||
return True
|
||||
# if not plexapp.ACCOUNT.hasPlexPass():
|
||||
# from windows import signin, background
|
||||
# background.setSplash(False)
|
||||
# w = signin.SignInPlexPass.open()
|
||||
# retry = w.retry
|
||||
# del w
|
||||
# util.DEBUG_LOG('PlexPass required. Signing out...')
|
||||
# plexapp.ACCOUNT.signOut()
|
||||
# plexapp.SERVERMANAGER.clearState()
|
||||
# if retry:
|
||||
# return 'RETRY'
|
||||
# else:
|
||||
# return False
|
||||
|
||||
# return True
|
||||
|
||||
|
||||
def authorize():
|
||||
from .windows import background
|
||||
with background.BackgroundContext(function=_authorize) as win:
|
||||
return win.result
|
||||
|
||||
|
||||
def _authorize():
|
||||
from .windows import signin, background
|
||||
|
||||
background.setSplash(False)
|
||||
|
||||
back = signin.Background.create()
|
||||
|
||||
pre = signin.PreSignInWindow.open()
|
||||
try:
|
||||
if not pre.doSignin:
|
||||
return None
|
||||
finally:
|
||||
del pre
|
||||
|
||||
try:
|
||||
while True:
|
||||
pinLoginWindow = signin.PinLoginWindow.create()
|
||||
try:
|
||||
pl = myplex.PinLogin()
|
||||
except requests.ConnectionError:
|
||||
util.ERROR()
|
||||
util.messageDialog(util.T(32427, 'Failed'), util.T(32449, 'Sign-in failed. Cound not connect to plex.tv'))
|
||||
return
|
||||
|
||||
pinLoginWindow.setPin(pl.pin)
|
||||
|
||||
try:
|
||||
pl.startTokenPolling()
|
||||
while not pl.finished():
|
||||
if pinLoginWindow.abort:
|
||||
util.DEBUG_LOG('SIGN IN: Pin login aborted')
|
||||
pl.abort()
|
||||
return None
|
||||
xbmc.sleep(100)
|
||||
else:
|
||||
if not pl.expired():
|
||||
if pl.authenticationToken:
|
||||
pinLoginWindow.setLinking()
|
||||
return pl.authenticationToken
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
pinLoginWindow.doClose()
|
||||
del pinLoginWindow
|
||||
|
||||
if pl.expired():
|
||||
util.DEBUG_LOG('SIGN IN: Pin expired')
|
||||
expiredWindow = signin.ExpiredWindow.open()
|
||||
try:
|
||||
if not expiredWindow.refresh:
|
||||
util.DEBUG_LOG('SIGN IN: Pin refresh aborted')
|
||||
return None
|
||||
finally:
|
||||
del expiredWindow
|
||||
finally:
|
||||
back.doClose()
|
||||
del back
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
|
@ -11,6 +12,7 @@ import asyncadapter
|
|||
|
||||
import util
|
||||
|
||||
LOG = logging.getLogger('PLEX.myplexaccount')
|
||||
ACCOUNT = None
|
||||
|
||||
|
||||
|
@ -71,7 +73,7 @@ class MyPlexAccount(object):
|
|||
def loadState(self):
|
||||
# Look for the new JSON serialization. If it's not there, look for the
|
||||
# old token and Plex Pass values.
|
||||
|
||||
LOG.debug('Loading State')
|
||||
plexapp.APP.addInitializer("myplex")
|
||||
|
||||
jstring = plexapp.INTERFACE.getRegistry("MyPlexAccount", None, "myplex")
|
||||
|
|
9
resources/lib/user.py
Normal file
9
resources/lib/user.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from . import kodigui
|
||||
from .. import backgroundthread, utils, plex_tv, variables as v
|
500
resources/lib/util.py
Normal file
500
resources/lib/util.py
Normal file
|
@ -0,0 +1,500 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import gc
|
||||
import sys
|
||||
import re
|
||||
import binascii
|
||||
import json
|
||||
import threading
|
||||
import math
|
||||
import time
|
||||
import datetime
|
||||
import contextlib
|
||||
import urllib
|
||||
|
||||
from .kodijsonrpc import rpc
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from .plexnet import signalsmixin
|
||||
|
||||
DEBUG = True
|
||||
_SHUTDOWN = False
|
||||
|
||||
ADDON = xbmcaddon.Addon()
|
||||
|
||||
PROFILE = xbmc.translatePath(ADDON.getAddonInfo('profile')).decode('utf-8')
|
||||
|
||||
SETTINGS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class UtilityMonitor(xbmc.Monitor, signalsmixin.SignalsMixin):
|
||||
def watchStatusChanged(self):
|
||||
self.trigger('changed.watchstatus')
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
if (sender == 'plugin.video.plexkodiconnect' and
|
||||
method.endswith('RESTORE')):
|
||||
from .windows import kodigui
|
||||
xbmc.executebuiltin('ActivateWindow({0})'.format(kodigui.BaseFunctions.lastWinID))
|
||||
|
||||
|
||||
MONITOR = UtilityMonitor()
|
||||
|
||||
|
||||
def T(ID, eng=''):
|
||||
return ADDON.getLocalizedString(ID)
|
||||
|
||||
|
||||
def LOG(msg, level=xbmc.LOGNOTICE):
|
||||
xbmc.log('PLEX: {0}'.format(msg), level)
|
||||
|
||||
|
||||
def DEBUG_LOG(msg):
|
||||
if _SHUTDOWN:
|
||||
return
|
||||
|
||||
if not getSetting('debug', False) and not xbmc.getCondVisibility('System.GetBool(debug.showloginfo)'):
|
||||
return
|
||||
|
||||
LOG(msg)
|
||||
|
||||
|
||||
def ERROR(txt='', hide_tb=False, notify=False):
|
||||
if isinstance(txt, str):
|
||||
txt = txt.decode("utf-8")
|
||||
short = str(sys.exc_info()[1])
|
||||
if hide_tb:
|
||||
xbmc.log('PLEX: {0} - {1}'.format(txt, short), xbmc.LOGERROR)
|
||||
return short
|
||||
|
||||
import traceback
|
||||
tb = traceback.format_exc()
|
||||
xbmc.log("_________________________________________________________________________________", xbmc.LOGERROR)
|
||||
xbmc.log('PLEX: ' + txt, xbmc.LOGERROR)
|
||||
for l in tb.splitlines():
|
||||
xbmc.log(' ' + l, xbmc.LOGERROR)
|
||||
xbmc.log("_________________________________________________________________________________", xbmc.LOGERROR)
|
||||
xbmc.log("`", xbmc.LOGERROR)
|
||||
if notify:
|
||||
showNotification('ERROR: {0}'.format(short))
|
||||
return short
|
||||
|
||||
|
||||
def TEST(msg):
|
||||
xbmc.log('---TEST: {0}'.format(msg), xbmc.LOGNOTICE)
|
||||
|
||||
|
||||
def getSetting(key, default=None):
|
||||
with SETTINGS_LOCK:
|
||||
setting = ADDON.getSetting(key)
|
||||
return _processSetting(setting, default)
|
||||
|
||||
|
||||
def _processSetting(setting, default):
|
||||
if not setting:
|
||||
return default
|
||||
if isinstance(default, bool):
|
||||
return setting.lower() == 'true'
|
||||
elif isinstance(default, float):
|
||||
return float(setting)
|
||||
elif isinstance(default, int):
|
||||
return int(float(setting or 0))
|
||||
elif isinstance(default, list):
|
||||
if setting:
|
||||
return json.loads(binascii.unhexlify(setting))
|
||||
else:
|
||||
return default
|
||||
|
||||
return setting
|
||||
|
||||
|
||||
def setSetting(key, value):
|
||||
with SETTINGS_LOCK:
|
||||
value = _processSettingForWrite(value)
|
||||
ADDON.setSetting(key, value)
|
||||
|
||||
|
||||
def _processSettingForWrite(value):
|
||||
if isinstance(value, list):
|
||||
value = binascii.hexlify(json.dumps(value))
|
||||
elif isinstance(value, bool):
|
||||
value = value and 'true' or 'false'
|
||||
return str(value)
|
||||
|
||||
|
||||
def setGlobalProperty(key, val):
|
||||
xbmcgui.Window(10000).setProperty(
|
||||
'plugin.video.plexkodiconnect.{0}'.format(key), val)
|
||||
|
||||
|
||||
def setGlobalBoolProperty(key, boolean):
|
||||
xbmcgui.Window(10000).setProperty(
|
||||
'plugin.video.plexkodiconnect.{0}'.format(key), boolean and '1' or '')
|
||||
|
||||
|
||||
def getGlobalProperty(key):
|
||||
return xbmc.getInfoLabel(
|
||||
'Window(10000).Property(plugin.video.plexkodiconnect.{0})'.format(key))
|
||||
|
||||
|
||||
def showNotification(message, time_ms=3000, icon_path=None, header=ADDON.getAddonInfo('name')):
|
||||
try:
|
||||
icon_path = icon_path or xbmc.translatePath(ADDON.getAddonInfo('icon')).decode('utf-8')
|
||||
xbmc.executebuiltin('Notification({0},{1},{2},{3})'.format(header, message, time_ms, icon_path))
|
||||
except RuntimeError: # Happens when disabling the addon
|
||||
LOG(message)
|
||||
|
||||
|
||||
def videoIsPlaying():
|
||||
return xbmc.getCondVisibility('Player.HasVideo')
|
||||
|
||||
|
||||
def messageDialog(heading='Message', msg=''):
|
||||
from .windows import optionsdialog
|
||||
optionsdialog.show(heading, msg, 'OK')
|
||||
|
||||
|
||||
def showTextDialog(heading, text):
|
||||
t = TextBox()
|
||||
t.setControls(heading, text)
|
||||
|
||||
|
||||
def sortTitle(title):
|
||||
return title.startswith('The ') and title[4:] or title
|
||||
|
||||
|
||||
def durationToText(seconds):
|
||||
"""
|
||||
Converts seconds to a short user friendly string
|
||||
Example: 143 -> 2m 23s
|
||||
"""
|
||||
days = int(seconds / 86400000)
|
||||
if days:
|
||||
return '{0} day{1}'.format(days, days > 1 and 's' or '')
|
||||
left = seconds % 86400000
|
||||
hours = int(left / 3600000)
|
||||
if hours:
|
||||
hours = '{0} hr{1} '.format(hours, hours > 1 and 's' or '')
|
||||
else:
|
||||
hours = ''
|
||||
left = left % 3600000
|
||||
mins = int(left / 60000)
|
||||
if mins:
|
||||
return hours + '{0} min{1}'.format(mins, mins > 1 and 's' or '')
|
||||
elif hours:
|
||||
return hours.rstrip()
|
||||
secs = int(left % 60000)
|
||||
if secs:
|
||||
secs /= 1000
|
||||
return '{0} sec{1}'.format(secs, secs > 1 and 's' or '')
|
||||
return '0 seconds'
|
||||
|
||||
|
||||
def durationToShortText(seconds):
|
||||
"""
|
||||
Converts seconds to a short user friendly string
|
||||
Example: 143 -> 2m 23s
|
||||
"""
|
||||
days = int(seconds / 86400000)
|
||||
if days:
|
||||
return '{0} d'.format(days)
|
||||
left = seconds % 86400000
|
||||
hours = int(left / 3600000)
|
||||
if hours:
|
||||
hours = '{0} h '.format(hours)
|
||||
else:
|
||||
hours = ''
|
||||
left = left % 3600000
|
||||
mins = int(left / 60000)
|
||||
if mins:
|
||||
return hours + '{0} m'.format(mins)
|
||||
elif hours:
|
||||
return hours.rstrip()
|
||||
secs = int(left % 60000)
|
||||
if secs:
|
||||
secs /= 1000
|
||||
return '{0} s'.format(secs)
|
||||
return '0 s'
|
||||
|
||||
|
||||
def cleanLeadingZeros(text):
|
||||
if not text:
|
||||
return ''
|
||||
return re.sub('(?<= )0(\d)', r'\1', text)
|
||||
|
||||
|
||||
def removeDups(dlist):
|
||||
return [ii for n, ii in enumerate(dlist) if ii not in dlist[:n]]
|
||||
|
||||
|
||||
SIZE_NAMES = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
|
||||
|
||||
def simpleSize(size):
|
||||
"""
|
||||
Converts bytes to a short user friendly string
|
||||
Example: 12345 -> 12.06 KB
|
||||
"""
|
||||
s = 0
|
||||
if size > 0:
|
||||
i = int(math.floor(math.log(size, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size / p, 2)
|
||||
if (s > 0):
|
||||
return '%s %s' % (s, SIZE_NAMES[i])
|
||||
else:
|
||||
return '0B'
|
||||
|
||||
|
||||
def timeDisplay(ms):
|
||||
h = ms / 3600000
|
||||
m = (ms % 3600000) / 60000
|
||||
s = (ms % 60000) / 1000
|
||||
return '{0:0>2}:{1:0>2}:{2:0>2}'.format(h, m, s)
|
||||
|
||||
|
||||
def simplifiedTimeDisplay(ms):
|
||||
left, right = timeDisplay(ms).rsplit(':', 1)
|
||||
left = left.lstrip('0:') or '0'
|
||||
return left + ':' + right
|
||||
|
||||
|
||||
def shortenText(text, size):
|
||||
if len(text) < size:
|
||||
return text
|
||||
|
||||
return u'{0}\u2026'.format(text[:size - 1])
|
||||
|
||||
|
||||
class TextBox:
|
||||
# constants
|
||||
WINDOW = 10147
|
||||
CONTROL_LABEL = 1
|
||||
CONTROL_TEXTBOX = 5
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# activate the text viewer window
|
||||
xbmc.executebuiltin("ActivateWindow(%d)" % (self.WINDOW, ))
|
||||
# get window
|
||||
self.win = xbmcgui.Window(self.WINDOW)
|
||||
# give window time to initialize
|
||||
xbmc.sleep(1000)
|
||||
|
||||
def setControls(self, heading, text):
|
||||
# set heading
|
||||
self.win.getControl(self.CONTROL_LABEL).setLabel(heading)
|
||||
# set text
|
||||
self.win.getControl(self.CONTROL_TEXTBOX).setText(text)
|
||||
|
||||
|
||||
class SettingControl:
|
||||
def __init__(self, setting, log_display, disable_value=''):
|
||||
self.setting = setting
|
||||
self.logDisplay = log_display
|
||||
self.disableValue = disable_value
|
||||
self._originalMode = None
|
||||
self.store()
|
||||
|
||||
def disable(self):
|
||||
rpc.Settings.SetSettingValue(setting=self.setting, value=self.disableValue)
|
||||
DEBUG_LOG('{0}: DISABLED'.format(self.logDisplay))
|
||||
|
||||
def set(self, value):
|
||||
rpc.Settings.SetSettingValue(setting=self.setting, value=value)
|
||||
DEBUG_LOG('{0}: SET={1}'.format(self.logDisplay, value))
|
||||
|
||||
def store(self):
|
||||
try:
|
||||
self._originalMode = rpc.Settings.GetSettingValue(setting=self.setting).get('value')
|
||||
DEBUG_LOG('{0}: Mode stored ({1})'.format(self.logDisplay, self._originalMode))
|
||||
except:
|
||||
ERROR()
|
||||
|
||||
def restore(self):
|
||||
if self._originalMode is None:
|
||||
return
|
||||
rpc.Settings.SetSettingValue(setting=self.setting, value=self._originalMode)
|
||||
DEBUG_LOG('{0}: RESTORED'.format(self.logDisplay))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suspend(self):
|
||||
self.disable()
|
||||
yield
|
||||
self.restore()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def save(self):
|
||||
yield
|
||||
self.restore()
|
||||
|
||||
|
||||
def timeInDayLocalSeconds():
|
||||
now = datetime.datetime.now()
|
||||
sod = datetime.datetime(year=now.year, month=now.month, day=now.day)
|
||||
sod = int(time.mktime(sod.timetuple()))
|
||||
return int(time.time() - sod)
|
||||
|
||||
|
||||
CRON = None
|
||||
|
||||
|
||||
class CronReceiver():
|
||||
def tick(self):
|
||||
pass
|
||||
|
||||
def halfHour(self):
|
||||
pass
|
||||
|
||||
def day(self):
|
||||
pass
|
||||
|
||||
|
||||
class Cron(threading.Thread):
|
||||
def __init__(self, interval):
|
||||
threading.Thread.__init__(self, name='CRON')
|
||||
self.stopped = threading.Event()
|
||||
self.force = threading.Event()
|
||||
self.interval = interval
|
||||
self._lastHalfHour = self._getHalfHour()
|
||||
self._receivers = []
|
||||
|
||||
global CRON
|
||||
|
||||
CRON = self
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
DEBUG_LOG('Cron started')
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.stop()
|
||||
self.join()
|
||||
|
||||
def _wait(self):
|
||||
ct = 0
|
||||
while ct < self.interval:
|
||||
xbmc.sleep(100)
|
||||
ct += 0.1
|
||||
if self.force.isSet():
|
||||
self.force.clear()
|
||||
return True
|
||||
if xbmc.abortRequested or self.stopped.isSet():
|
||||
return False
|
||||
return True
|
||||
|
||||
def forceTick(self):
|
||||
self.force.set()
|
||||
|
||||
def stop(self):
|
||||
self.stopped.set()
|
||||
|
||||
def run(self):
|
||||
while self._wait():
|
||||
self._tick()
|
||||
DEBUG_LOG('Cron stopped')
|
||||
|
||||
def _getHalfHour(self):
|
||||
tid = timeInDayLocalSeconds() / 60
|
||||
return tid - (tid % 30)
|
||||
|
||||
def _tick(self):
|
||||
receivers = list(self._receivers)
|
||||
receivers = self._halfHour(receivers)
|
||||
for r in receivers:
|
||||
try:
|
||||
r.tick()
|
||||
except:
|
||||
ERROR()
|
||||
|
||||
def _halfHour(self, receivers):
|
||||
hh = self._getHalfHour()
|
||||
if hh == self._lastHalfHour:
|
||||
return receivers
|
||||
try:
|
||||
receivers = self._day(receivers, hh)
|
||||
ret = []
|
||||
for r in receivers:
|
||||
try:
|
||||
if not r.halfHour():
|
||||
ret.append(r)
|
||||
except:
|
||||
ret.append(r)
|
||||
ERROR()
|
||||
return ret
|
||||
finally:
|
||||
self._lastHalfHour = hh
|
||||
|
||||
def _day(self, receivers, hh):
|
||||
if hh >= self._lastHalfHour:
|
||||
return receivers
|
||||
ret = []
|
||||
for r in receivers:
|
||||
try:
|
||||
if not r.day():
|
||||
ret.append(r)
|
||||
except:
|
||||
ret.append(r)
|
||||
ERROR()
|
||||
return ret
|
||||
|
||||
def registerReceiver(self, receiver):
|
||||
if receiver not in self._receivers:
|
||||
DEBUG_LOG('Cron: Receiver added: {0}'.format(receiver))
|
||||
self._receivers.append(receiver)
|
||||
|
||||
def cancelReceiver(self, receiver):
|
||||
if receiver in self._receivers:
|
||||
DEBUG_LOG('Cron: Receiver canceled: {0}'.format(receiver))
|
||||
self._receivers.pop(self._receivers.index(receiver))
|
||||
|
||||
|
||||
def getPlatform():
|
||||
for key in [
|
||||
'System.Platform.Android',
|
||||
'System.Platform.Linux.RaspberryPi',
|
||||
'System.Platform.Linux',
|
||||
'System.Platform.Windows',
|
||||
'System.Platform.OSX',
|
||||
'System.Platform.IOS',
|
||||
'System.Platform.Darwin',
|
||||
'System.Platform.ATV2'
|
||||
]:
|
||||
if xbmc.getCondVisibility(key):
|
||||
return key.rsplit('.', 1)[-1]
|
||||
|
||||
|
||||
def getProgressImage(obj):
|
||||
if not obj.get('viewOffset'):
|
||||
return ''
|
||||
pct = int((obj.viewOffset.asInt() / obj.duration.asFloat()) * 100)
|
||||
pct = pct - pct % 2 # Round to even number - we have even numbered progress only
|
||||
return 'plugin.video.plexkodiconnect/progress/{0}.png'.format(pct)
|
||||
|
||||
|
||||
def trackIsPlaying(track):
|
||||
return xbmc.getCondVisibility('String.StartsWith(MusicPlayer.Comment,{0})'.format('PLEX-{0}:'.format(track.ratingKey)))
|
||||
|
||||
|
||||
def addURLParams(url, params):
|
||||
if '?' in url:
|
||||
url += '&'
|
||||
else:
|
||||
url += '?'
|
||||
url += urllib.urlencode(params)
|
||||
return url
|
||||
|
||||
|
||||
def garbageCollect():
|
||||
gc.collect(2)
|
||||
|
||||
|
||||
def shutdown():
|
||||
global MONITOR, ADDON, T, _SHUTDOWN
|
||||
_SHUTDOWN = True
|
||||
del MONITOR
|
||||
del T
|
||||
del ADDON
|
|
@ -42,3 +42,27 @@ def setSplash(on=True):
|
|||
|
||||
def setShutdown(on=True):
|
||||
utils.setGlobalProperty('background.shutdown', on and '1' or '')
|
||||
|
||||
|
||||
class BackgroundContext(object):
|
||||
"""
|
||||
Context Manager to open a Plex background window - in the background. This
|
||||
will e.g. ensure that you can capture key-presses
|
||||
Use like this:
|
||||
with BackgroundContext(function) as win:
|
||||
<now function will be executed immediately. Get its results:>
|
||||
result = win.result
|
||||
"""
|
||||
def __init__(self, function=None):
|
||||
self.window = None
|
||||
self.result = None
|
||||
self.function = function
|
||||
|
||||
def __enter__(self):
|
||||
self.window = BackgroundWindow.create(function=self.function)
|
||||
self.window.modal()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.result = self.window.result
|
||||
del self.window
|
||||
|
|
36
service.py
36
service.py
|
@ -1,8 +1,38 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from resources.lib import service_entry
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
service_entry.start()
|
||||
def start():
|
||||
# Safety net - Kodi starts PKC twice upon first installation!
|
||||
if xbmc.getInfoLabel(
|
||||
'Window(10000).Property(plugin.video.plexkodiconnect.running)').decode('utf-8') == '1':
|
||||
xbmc.log('PLEX: PlexKodiConnect is already running',
|
||||
level=xbmc.LOGWARNING)
|
||||
return
|
||||
else:
|
||||
xbmcgui.Window(10000).setProperty(
|
||||
'plugin.video.plexkodiconnect.running', '1')
|
||||
try:
|
||||
# We might have to wait a bit before starting PKC
|
||||
delay = int(xbmcaddon.Addon(
|
||||
id='plugin.video.plexkodiconnect').getSetting('startupDelay'))
|
||||
xbmc.log('PLEX: Delaying PKC startup by: %s seconds'.format(delay),
|
||||
level=xbmc.LOGNOTICE)
|
||||
if delay and xbmc.Monitor().waitForAbort(delay):
|
||||
xbmc.log('PLEX: Kodi shutdown while waiting for PKC startup',
|
||||
level=xbmc.LOGWARNING)
|
||||
return
|
||||
from resources.lib import main
|
||||
main.main()
|
||||
finally:
|
||||
xbmcgui.Window(10000).setProperty(
|
||||
'plugin.video.plexkodiconnect.running', '')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start()
|
||||
|
|
Loading…
Reference in a new issue