214 lines
7.4 KiB
Python
214 lines
7.4 KiB
Python
|
import random
|
||
|
|
||
|
import http
|
||
|
import plexapp
|
||
|
import callback
|
||
|
import util
|
||
|
|
||
|
|
||
|
class ConnectionSource(int):
|
||
|
def init(self, name):
|
||
|
self.name = name
|
||
|
return self
|
||
|
|
||
|
def __repr__(self):
|
||
|
return self.name
|
||
|
|
||
|
|
||
|
class PlexConnection(object):
|
||
|
# Constants
|
||
|
STATE_UNKNOWN = "unknown"
|
||
|
STATE_UNREACHABLE = "unreachable"
|
||
|
STATE_REACHABLE = "reachable"
|
||
|
STATE_UNAUTHORIZED = "unauthorized"
|
||
|
STATE_INSECURE = "insecure_untested"
|
||
|
|
||
|
SOURCE_MANUAL = ConnectionSource(1).init('MANUAL')
|
||
|
SOURCE_DISCOVERED = ConnectionSource(2).init('DISCOVERED')
|
||
|
SOURCE_MANUAL_AND_DISCOVERED = ConnectionSource(3).init('MANUAL, DISCOVERED')
|
||
|
SOURCE_MYPLEX = ConnectionSource(4).init('MYPLEX')
|
||
|
SOURCE_MANUAL_AND_MYPLEX = ConnectionSource(5).init('MANUAL, MYPLEX')
|
||
|
SOURCE_DISCOVERED_AND_MYPLEX = ConnectionSource(6).init('DISCOVERED, MYPLEX')
|
||
|
SOURCE_ALL = ConnectionSource(7).init('ALL')
|
||
|
|
||
|
SCORE_REACHABLE = 4
|
||
|
SCORE_LOCAL = 2
|
||
|
SCORE_SECURE = 1
|
||
|
|
||
|
SOURCE_BY_VAL = {
|
||
|
1: SOURCE_MANUAL,
|
||
|
2: SOURCE_DISCOVERED,
|
||
|
3: SOURCE_MANUAL_AND_DISCOVERED,
|
||
|
4: SOURCE_MYPLEX,
|
||
|
5: SOURCE_MANUAL_AND_MYPLEX,
|
||
|
6: SOURCE_DISCOVERED_AND_MYPLEX,
|
||
|
7: SOURCE_ALL
|
||
|
}
|
||
|
|
||
|
def __init__(self, source, address, isLocal, token, isFallback=False):
|
||
|
self.state = self.STATE_UNKNOWN
|
||
|
self.sources = source
|
||
|
self.address = address
|
||
|
self.isLocal = isLocal
|
||
|
self.isSecure = address[:5] == 'https'
|
||
|
self.isFallback = isFallback
|
||
|
self.token = token
|
||
|
self.refreshed = True
|
||
|
self.score = 0
|
||
|
self.request = None
|
||
|
|
||
|
self.lastTestedAt = 0
|
||
|
self.hasPendingRequest = False
|
||
|
|
||
|
self.getScore(True)
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not other:
|
||
|
return False
|
||
|
if self.__class__ != other.__class__:
|
||
|
return False
|
||
|
return self.address == other.address
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __str__(self):
|
||
|
return "Connection: {0} local: {1} token: {2} sources: {3} state: {4}".format(
|
||
|
self.address,
|
||
|
self.isLocal,
|
||
|
util.hideToken(self.token),
|
||
|
repr(self.sources),
|
||
|
self.state
|
||
|
)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return self.__str__()
|
||
|
|
||
|
def merge(self, other):
|
||
|
# plex.tv trumps all, otherwise assume newer is better
|
||
|
# ROKU: if (other.sources and self.SOURCE_MYPLEX) <> 0 then
|
||
|
if other.sources == self.SOURCE_MYPLEX:
|
||
|
self.token = other.token
|
||
|
else:
|
||
|
self.token = self.token or other.token
|
||
|
|
||
|
self.address = other.address
|
||
|
self.sources = self.SOURCE_BY_VAL[self.sources | other.sources]
|
||
|
self.isLocal = self.isLocal | other.isLocal
|
||
|
self.isSecure = other.isSecure
|
||
|
self.isFallback = self.isFallback or other.isFallback
|
||
|
self.refreshed = True
|
||
|
|
||
|
self.getScore(True)
|
||
|
|
||
|
def testReachability(self, server, allowFallback=False):
|
||
|
# Check if we will allow the connection test. If this is a fallback connection,
|
||
|
# then we will defer it until we "allowFallback" (test insecure connections
|
||
|
# after secure tests have completed and failed). Insecure connections will be
|
||
|
# tested if the policy "always" allows them, or if set to "same_network" and
|
||
|
# the current connection is local and server has (publicAddressMatches=1).
|
||
|
|
||
|
allowConnectionTest = not self.isFallback
|
||
|
if not allowConnectionTest:
|
||
|
insecurePolicy = plexapp.INTERFACE.getPreference("allow_insecure")
|
||
|
if insecurePolicy == "always" or (insecurePolicy == "same_network" and server.sameNetwork and self.isLocal):
|
||
|
allowConnectionTest = allowFallback
|
||
|
server.hasFallback = not allowConnectionTest
|
||
|
util.LOG(
|
||
|
'{0} for {1}'.format(
|
||
|
allowConnectionTest and "Continuing with insecure connection testing" or "Insecure connection testing is deferred", server
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
util.LOG("Insecure connections not allowed. Ignore insecure connection test for {0}".format(server))
|
||
|
self.state = self.STATE_INSECURE
|
||
|
callable = callback.Callable(server.onReachabilityResult, [self], random.randint(0, 256))
|
||
|
callable.deferCall()
|
||
|
return True
|
||
|
|
||
|
if allowConnectionTest:
|
||
|
if not self.isSecure and (
|
||
|
not allowFallback and
|
||
|
server.hasSecureConnections() or
|
||
|
server.activeConnection and
|
||
|
server.activeConnection.state != self.STATE_REACHABLE and
|
||
|
server.activeConnection.isSecure
|
||
|
):
|
||
|
util.DEBUG_LOG("Invalid insecure connection test in progress")
|
||
|
self.request = http.HttpRequest(self.buildUrl(server, "/"))
|
||
|
context = self.request.createRequestContext("reachability", callback.Callable(self.onReachabilityResponse))
|
||
|
context.server = server
|
||
|
util.addPlexHeaders(self.request, server.getToken())
|
||
|
self.hasPendingRequest = plexapp.APP.startRequest(self.request, context)
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def cancelReachability(self):
|
||
|
if self.request:
|
||
|
self.request.ignoreResponse = True
|
||
|
self.request.cancel()
|
||
|
|
||
|
def onReachabilityResponse(self, request, response, context):
|
||
|
self.hasPendingRequest = False
|
||
|
# It's possible we may have a result pending before we were able
|
||
|
# to cancel it, so we'll just ignore it.
|
||
|
|
||
|
# if request.ignoreResponse:
|
||
|
# return
|
||
|
|
||
|
if response.isSuccess():
|
||
|
data = response.getBodyXml()
|
||
|
if data is not None and context.server.collectDataFromRoot(data):
|
||
|
self.state = self.STATE_REACHABLE
|
||
|
else:
|
||
|
# This is unexpected, but treat it as unreachable
|
||
|
util.ERROR_LOG("Unable to parse root response from {0}".format(context.server))
|
||
|
self.state = self.STATE_UNREACHABLE
|
||
|
elif response.getStatus() == 401:
|
||
|
self.state = self.STATE_UNAUTHORIZED
|
||
|
else:
|
||
|
self.state = self.STATE_UNREACHABLE
|
||
|
|
||
|
self.getScore(True)
|
||
|
|
||
|
context.server.onReachabilityResult(self)
|
||
|
|
||
|
def buildUrl(self, server, path, includeToken=False):
|
||
|
if '://' in path:
|
||
|
url = path
|
||
|
else:
|
||
|
url = self.address + path
|
||
|
|
||
|
if includeToken:
|
||
|
# If we have a token, use it. Otherwise see if any other connections
|
||
|
# for this server have one. That will let us use a plex.tv token for
|
||
|
# something like a manually configured connection.
|
||
|
|
||
|
token = self.token or server.getToken()
|
||
|
|
||
|
if token:
|
||
|
url = http.addUrlParam(url, "X-Plex-Token=" + token)
|
||
|
|
||
|
return url
|
||
|
|
||
|
def simpleBuildUrl(self, server, path):
|
||
|
token = (self.token or server.getToken())
|
||
|
param = ''
|
||
|
if token:
|
||
|
param = '&X-Plex-Token={0}'.format(token)
|
||
|
|
||
|
return '{0}{1}{2}'.format(self.address, path, param)
|
||
|
|
||
|
def getScore(self, recalc=False):
|
||
|
if recalc:
|
||
|
self.score = 0
|
||
|
if self.state == self.STATE_REACHABLE:
|
||
|
self.score += self.SCORE_REACHABLE
|
||
|
if self.isSecure:
|
||
|
self.score += self.SCORE_SECURE
|
||
|
if self.isLocal:
|
||
|
self.score += self.SCORE_LOCAL
|
||
|
|
||
|
return self.score
|