474 lines
13 KiB
Python
474 lines
13 KiB
Python
|
import threading
|
||
|
import platform
|
||
|
import uuid
|
||
|
import sys
|
||
|
import callback
|
||
|
|
||
|
import signalsmixin
|
||
|
import simpleobjects
|
||
|
import nowplayingmanager
|
||
|
import util
|
||
|
|
||
|
Res = simpleobjects.Res
|
||
|
|
||
|
APP = None
|
||
|
INTERFACE = None
|
||
|
|
||
|
MANAGER = None
|
||
|
SERVERMANAGER = None
|
||
|
ACCOUNT = None
|
||
|
|
||
|
PLATFORM = util.X_PLEX_DEVICE
|
||
|
|
||
|
def init():
|
||
|
global MANAGER, SERVERMANAGER, ACCOUNT
|
||
|
import myplexaccount
|
||
|
ACCOUNT = myplexaccount.ACCOUNT
|
||
|
import plexservermanager
|
||
|
SERVERMANAGER = plexservermanager.MANAGER
|
||
|
import myplexmanager
|
||
|
MANAGER = myplexmanager.MANAGER
|
||
|
ACCOUNT.init()
|
||
|
|
||
|
|
||
|
class App(signalsmixin.SignalsMixin):
|
||
|
def __init__(self):
|
||
|
signalsmixin.SignalsMixin.__init__(self)
|
||
|
self.pendingRequests = {}
|
||
|
self.initializers = {}
|
||
|
self.timers = []
|
||
|
self.nowplayingmanager = nowplayingmanager.NowPlayingManager()
|
||
|
|
||
|
def addTimer(self, timer):
|
||
|
self.timers.append(timer)
|
||
|
|
||
|
def startRequest(self, request, context, body=None, contentType=None):
|
||
|
context.request = request
|
||
|
|
||
|
started = request.startAsync(body=body, contentType=contentType, context=context)
|
||
|
|
||
|
if started:
|
||
|
requestID = context.request.getIdentity()
|
||
|
self.pendingRequests[requestID] = context
|
||
|
elif context.callback:
|
||
|
context.callback(None, context)
|
||
|
|
||
|
return started
|
||
|
|
||
|
def onRequestTimeout(self, context):
|
||
|
requestID = context.request.getIdentity()
|
||
|
|
||
|
if requestID not in self.pendingRequests:
|
||
|
return
|
||
|
|
||
|
context.request.cancel()
|
||
|
|
||
|
util.WARN_LOG("Request to {0} timed out after {1} sec".format(util.cleanToken(context.request.url), context.timeout))
|
||
|
|
||
|
if context.callback:
|
||
|
context.callback(None, context)
|
||
|
|
||
|
def delRequest(self, request):
|
||
|
requestID = request.getIdentity()
|
||
|
if requestID not in self.pendingRequests:
|
||
|
return
|
||
|
|
||
|
del self.pendingRequests[requestID]
|
||
|
|
||
|
def addInitializer(self, name):
|
||
|
self.initializers[name] = True
|
||
|
|
||
|
def clearInitializer(self, name):
|
||
|
if name in self.initializers:
|
||
|
del self.initializers[name]
|
||
|
if self.isInitialized():
|
||
|
self.onInitialized()
|
||
|
|
||
|
def isInitialized(self):
|
||
|
return not self.initializers
|
||
|
|
||
|
def onInitialized(self):
|
||
|
# Wire up a few of our own listeners
|
||
|
# PlexServerManager()
|
||
|
# self.on("change:user", callback.Callable(self.onAccountChange))
|
||
|
|
||
|
self.trigger('init')
|
||
|
|
||
|
def cancelAllTimers(self):
|
||
|
for timer in self.timers:
|
||
|
timer.cancel()
|
||
|
|
||
|
def preShutdown(self):
|
||
|
import http
|
||
|
http.HttpRequest._cancel = True
|
||
|
if self.pendingRequests:
|
||
|
util.DEBUG_LOG('Closing down {0} App() requests...'.format(len(self.pendingRequests)))
|
||
|
for p in self.pendingRequests.values():
|
||
|
if p:
|
||
|
p.request.cancel()
|
||
|
|
||
|
if self.timers:
|
||
|
util.DEBUG_LOG('Canceling App() timers...')
|
||
|
self.cancelAllTimers()
|
||
|
|
||
|
if SERVERMANAGER.selectedServer:
|
||
|
util.DEBUG_LOG('Closing server...')
|
||
|
SERVERMANAGER.selectedServer.close()
|
||
|
|
||
|
def shutdown(self):
|
||
|
if self.timers:
|
||
|
util.DEBUG_LOG('Waiting for {0} App() timers: Started'.format(len(self.timers)))
|
||
|
|
||
|
self.cancelAllTimers()
|
||
|
|
||
|
for timer in self.timers:
|
||
|
timer.join()
|
||
|
|
||
|
util.DEBUG_LOG('Waiting for App() timers: Finished')
|
||
|
|
||
|
|
||
|
class DeviceInfo(object):
|
||
|
def getCaptionsOption(self, key):
|
||
|
return None
|
||
|
|
||
|
|
||
|
class AppInterface(object):
|
||
|
QUALITY_LOCAL = 0
|
||
|
QUALITY_REMOTE = 1
|
||
|
QUALITY_ONLINE = 2
|
||
|
|
||
|
_globals = {}
|
||
|
|
||
|
def __init__(self):
|
||
|
self.setQualities()
|
||
|
|
||
|
def setQualities(self):
|
||
|
# Calculate the max quality based on 4k support
|
||
|
if self._globals.get("supports4k"):
|
||
|
maxQuality = simpleobjects.AttributeDict({
|
||
|
'height': 2160,
|
||
|
'maxHeight': 2160,
|
||
|
'origHeight': 1080
|
||
|
})
|
||
|
maxResolution = self._globals.get("Is4k") and "4k" or "1080p"
|
||
|
else:
|
||
|
maxQuality = simpleobjects.AttributeDict({
|
||
|
'height': 1080,
|
||
|
'maxHeight': 1088
|
||
|
})
|
||
|
maxResolution = "1080p"
|
||
|
|
||
|
self._globals['qualities'] = [
|
||
|
simpleobjects.AttributeDict({'title': "Original", 'index': 13, 'maxBitrate': 200000}),
|
||
|
simpleobjects.AttributeDict({'title': "20 Mbps " + maxResolution, 'index': 12, 'maxBitrate': 20000}),
|
||
|
simpleobjects.AttributeDict({'title': "12 Mbps " + maxResolution, 'index': 11, 'maxBitrate': 12000}),
|
||
|
simpleobjects.AttributeDict({'title': "10 Mbps " + maxResolution, 'index': 10, 'maxBitrate': 10000}),
|
||
|
simpleobjects.AttributeDict({'title': "8 Mbps " + maxResolution, 'index': 9, 'maxBitrate': 8000}),
|
||
|
simpleobjects.AttributeDict({'title': "4 Mbps 720p", 'index': 8, 'maxBitrate': 4000, 'maxHeight': 720}),
|
||
|
simpleobjects.AttributeDict({'title': "3 Mbps 720p", 'index': 7, 'maxBitrate': 3000, 'maxHeight': 720}),
|
||
|
simpleobjects.AttributeDict({'title': "2 Mbps 720p", 'index': 6, 'maxBitrate': 2000, 'maxHeight': 720}),
|
||
|
simpleobjects.AttributeDict({'title': "1.5 Mbps 480p", 'index': 5, 'maxBitrate': 1500, 'maxHeight': 480}),
|
||
|
simpleobjects.AttributeDict({'title': "720 Kbps", 'index': 4, 'maxBitrate': 720, 'maxHeight': 360}),
|
||
|
simpleobjects.AttributeDict({'title': "320 Kbps", 'index': 3, 'maxBitrate': 320, 'maxHeight': 360}),
|
||
|
maxQuality
|
||
|
]
|
||
|
|
||
|
for quality in self._globals['qualities']:
|
||
|
if quality.index >= 9:
|
||
|
quality.update(maxQuality)
|
||
|
|
||
|
def getPreference(self, pref, default=None):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def setPreference(self, pref, value):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def clearRegistry(self, reg, sec=None):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def getRegistry(self, reg, default=None, sec=None):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def setRegistry(self, reg, value, sec=None):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def getGlobal(self, glbl, default=None):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def getCapabilities(self):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def LOG(self, msg):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def DEBUG_LOG(self, msg):
|
||
|
self.LOG(msg)
|
||
|
|
||
|
def WARN_LOG(self, msg):
|
||
|
self.LOG(msg)
|
||
|
|
||
|
def ERROR_LOG(self, msg):
|
||
|
self.LOG(msg)
|
||
|
|
||
|
def ERROR(self, msg=None, err=None):
|
||
|
self.LOG(msg)
|
||
|
|
||
|
def FATAL(self, msg=None):
|
||
|
self.ERROR_LOG('FATAL: {0}'.format(msg))
|
||
|
|
||
|
def supportsAudioStream(self, codec, channels):
|
||
|
return False
|
||
|
|
||
|
def supportsSurroundSound(self):
|
||
|
return False
|
||
|
|
||
|
def getMaxResolution(self, quality_type, allow4k=False):
|
||
|
return 480
|
||
|
|
||
|
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 settingsGetMaxResolution(self, qualityType, allow4k):
|
||
|
qualityIndex = self.getQualityIndex(qualityType)
|
||
|
|
||
|
if qualityIndex >= 9:
|
||
|
return allow4k and 2160 or 1088
|
||
|
elif qualityIndex >= 6:
|
||
|
return 720
|
||
|
elif qualityIndex >= 5:
|
||
|
return 480
|
||
|
else:
|
||
|
return 360
|
||
|
|
||
|
def getMaxBitrate(self, qualityType):
|
||
|
qualityIndex = self.getQualityIndex(qualityType)
|
||
|
|
||
|
qualities = self.getGlobal("qualities", [])
|
||
|
for quality in qualities:
|
||
|
if quality.index == qualityIndex:
|
||
|
return util.validInt(quality.maxBitrate)
|
||
|
|
||
|
return 0
|
||
|
|
||
|
|
||
|
class PlayerSettingsInterface(object):
|
||
|
def __init__(self):
|
||
|
self.prefOverrides = {}
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
return getattr(INTERFACE, name)
|
||
|
|
||
|
def setPrefOverride(self, key, val):
|
||
|
self.prefOverrides[key] = val
|
||
|
|
||
|
def getPrefOverride(self, key, default=None):
|
||
|
return self.prefOverrides.get(key, default)
|
||
|
|
||
|
def getQualityIndex(self, qualityType):
|
||
|
if qualityType == INTERFACE.QUALITY_LOCAL:
|
||
|
return self.getPreference("local_quality", 13)
|
||
|
elif qualityType == INTERFACE.QUALITY_ONLINE:
|
||
|
return self.getPreference("online_quality", 8)
|
||
|
else:
|
||
|
return self.getPreference("remote_quality", 13)
|
||
|
|
||
|
def getPreference(self, key, default=None):
|
||
|
if key in self.prefOverrides:
|
||
|
return self.prefOverrides[key]
|
||
|
else:
|
||
|
return INTERFACE.getPreference(key, default)
|
||
|
|
||
|
def getMaxResolution(self, quality_type, allow4k=False):
|
||
|
qualityIndex = self.getQualityIndex(quality_type)
|
||
|
|
||
|
if qualityIndex >= 9:
|
||
|
return allow4k and 2160 or 1088
|
||
|
elif qualityIndex >= 6:
|
||
|
return 720
|
||
|
elif qualityIndex >= 5:
|
||
|
return 480
|
||
|
else:
|
||
|
return 360
|
||
|
|
||
|
|
||
|
class DumbInterface(AppInterface):
|
||
|
_prefs = {}
|
||
|
_regs = {
|
||
|
None: {}
|
||
|
}
|
||
|
_globals = {
|
||
|
'platform': platform.uname()[0],
|
||
|
'appVersionStr': '0.0.0a1',
|
||
|
'clientIdentifier': str(hex(uuid.getnode())),
|
||
|
'platformVersion': platform.uname()[2],
|
||
|
'product': 'PlexNet.API',
|
||
|
'provides': 'player',
|
||
|
'device': platform.uname()[0],
|
||
|
'model': 'Unknown',
|
||
|
'friendlyName': 'PlexNet.API',
|
||
|
'deviceInfo': DeviceInfo()
|
||
|
}
|
||
|
|
||
|
def getPreference(self, pref, default=None):
|
||
|
return self._prefs.get(pref, default)
|
||
|
|
||
|
def setPreference(self, pref, value):
|
||
|
self._prefs[pref] = value
|
||
|
|
||
|
def getRegistry(self, reg, default=None, sec=None):
|
||
|
section = self._regs.get(sec)
|
||
|
if section:
|
||
|
return section.get(reg, default)
|
||
|
|
||
|
return default
|
||
|
|
||
|
def setRegistry(self, reg, value, sec=None):
|
||
|
if sec and sec not in self._regs:
|
||
|
self._regs[sec] = {}
|
||
|
self._regs[sec][reg] = value
|
||
|
|
||
|
def clearRegistry(self, reg, sec=None):
|
||
|
del self._regs[sec][reg]
|
||
|
|
||
|
def getGlobal(self, glbl, default=None):
|
||
|
return self._globals.get(glbl, default)
|
||
|
|
||
|
def getCapabilities(self):
|
||
|
return ''
|
||
|
|
||
|
def LOG(self, msg):
|
||
|
print 'PlexNet.API: {0}'.format(msg)
|
||
|
|
||
|
def DEBUG_LOG(self, msg):
|
||
|
self.LOG('DEBUG: {0}'.format(msg))
|
||
|
|
||
|
def WARN_LOG(self, msg):
|
||
|
self.LOG('WARNING: {0}'.format(msg))
|
||
|
|
||
|
def ERROR_LOG(self, msg):
|
||
|
self.LOG('ERROR: {0}'.format(msg))
|
||
|
|
||
|
def ERROR(self, msg=None, err=None):
|
||
|
if err:
|
||
|
self.LOG('ERROR: {0} - {1}'.format(msg, err.message))
|
||
|
else:
|
||
|
import traceback
|
||
|
traceback.print_exc()
|
||
|
|
||
|
|
||
|
class CompatEvent(threading._Event):
|
||
|
def wait(self, timeout):
|
||
|
threading._Event.wait(self, timeout)
|
||
|
return self.isSet()
|
||
|
|
||
|
|
||
|
class Timer(object):
|
||
|
def __init__(self, timeout, function, repeat=False, *args, **kwargs):
|
||
|
self.function = function
|
||
|
self.timeout = timeout
|
||
|
self.repeat = repeat
|
||
|
self.args = args
|
||
|
self.kwargs = kwargs
|
||
|
self._reset = False
|
||
|
self.event = CompatEvent()
|
||
|
self.start()
|
||
|
|
||
|
def start(self):
|
||
|
self.event.clear()
|
||
|
self.thread = threading.Thread(target=self.run, name='TIMER:{0}'.format(self.function), *self.args, **self.kwargs)
|
||
|
self.thread.start()
|
||
|
|
||
|
def run(self):
|
||
|
util.DEBUG_LOG('Timer {0}: {1}'.format(repr(self.function), self._reset and 'RESET'or 'STARTED'))
|
||
|
try:
|
||
|
while not self.event.isSet() and not self.shouldAbort():
|
||
|
while not self.event.wait(self.timeout) and not self.shouldAbort():
|
||
|
if self._reset:
|
||
|
return
|
||
|
|
||
|
self.function(*self.args, **self.kwargs)
|
||
|
if not self.repeat:
|
||
|
return
|
||
|
finally:
|
||
|
if not self._reset:
|
||
|
if self in APP.timers:
|
||
|
APP.timers.remove(self)
|
||
|
|
||
|
util.DEBUG_LOG('Timer {0}: FINISHED'.format(repr(self.function)))
|
||
|
|
||
|
self._reset = False
|
||
|
|
||
|
def cancel(self):
|
||
|
self.event.set()
|
||
|
|
||
|
def reset(self):
|
||
|
self._reset = True
|
||
|
self.cancel()
|
||
|
if self.thread and self.thread.isAlive():
|
||
|
self.thread.join()
|
||
|
self.start()
|
||
|
|
||
|
def shouldAbort(self):
|
||
|
return False
|
||
|
|
||
|
def join(self):
|
||
|
if self.thread.isAlive():
|
||
|
self.thread.join()
|
||
|
|
||
|
def isExpired(self):
|
||
|
return self.event.isSet()
|
||
|
|
||
|
|
||
|
TIMER = Timer
|
||
|
|
||
|
|
||
|
def createTimer(timeout, function, repeat=False, *args, **kwargs):
|
||
|
if isinstance(function, basestring):
|
||
|
def dummy(*args, **kwargs):
|
||
|
pass
|
||
|
dummy.__name__ = function
|
||
|
function = dummy
|
||
|
timer = TIMER(timeout / 1000.0, function, repeat=repeat, *args, **kwargs)
|
||
|
return timer
|
||
|
|
||
|
|
||
|
def setTimer(timer):
|
||
|
global TIMER
|
||
|
TIMER = timer
|
||
|
|
||
|
|
||
|
def setInterface(interface):
|
||
|
global INTERFACE
|
||
|
INTERFACE = interface
|
||
|
|
||
|
|
||
|
def setApp(app):
|
||
|
global APP
|
||
|
APP = app
|
||
|
|
||
|
|
||
|
def setUserAgent(agent):
|
||
|
util.USER_AGENT = agent
|
||
|
util.BASE_HEADERS = util.resetBaseHeaders()
|
||
|
|
||
|
|
||
|
def setAbortFlagFunction(func):
|
||
|
import asyncadapter
|
||
|
asyncadapter.ABORT_FLAG_FUNCTION = func
|
||
|
|
||
|
|
||
|
def refreshResources(force=False):
|
||
|
import gdm
|
||
|
gdm.DISCOVERY.discover()
|
||
|
MANAGER.refreshResources(force)
|
||
|
SERVERMANAGER.refreshManualConnections()
|
||
|
|
||
|
|
||
|
setApp(App())
|
||
|
setInterface(DumbInterface())
|