347 lines
11 KiB
Python
347 lines
11 KiB
Python
|
import threading
|
||
|
import socket
|
||
|
import traceback
|
||
|
import time
|
||
|
import util
|
||
|
import netif
|
||
|
|
||
|
import plexconnection
|
||
|
|
||
|
DISCOVERY_PORT = 32414
|
||
|
WIN_NL = chr(13) + chr(10)
|
||
|
|
||
|
|
||
|
class GDMDiscovery(object):
|
||
|
def __init__(self):
|
||
|
self._close = False
|
||
|
self.thread = None
|
||
|
|
||
|
# def isActive(self):
|
||
|
# util.LOG('GDMDiscovery().isActive() - NOT IMPLEMENTED')
|
||
|
# return False
|
||
|
|
||
|
# def discover(self):
|
||
|
# util.LOG('GDMDiscovery().discover() - NOT IMPLEMENTED')
|
||
|
|
||
|
def isActive(self):
|
||
|
import plexapp
|
||
|
return plexapp.INTERFACE.getPreference("gdm_discovery", True) and self.thread and self.thread.isAlive()
|
||
|
|
||
|
'''
|
||
|
def discover(self):
|
||
|
# Only allow discovery if enabled and not currently running
|
||
|
self._close = False
|
||
|
import plexapp
|
||
|
if not plexapp.INTERFACE.getPreference("gdm_discovery", True) or self.isActive():
|
||
|
return
|
||
|
|
||
|
ifaces = netif.getInterfaces()
|
||
|
|
||
|
message = "M-SEARCH * HTTP/1.1" + WIN_NL + WIN_NL
|
||
|
|
||
|
# Broadcasting to 255.255.255.255 only works on some Rokus, but we
|
||
|
# can't reliably determine the broadcast address for our current
|
||
|
# interface. Try assuming a /24 network, and then fall back to the
|
||
|
# multicast address if that doesn't work.
|
||
|
|
||
|
multicast = "239.0.0.250"
|
||
|
ip = multicast
|
||
|
subnetRegex = re.compile("((\d+)\.(\d+)\.(\d+)\.)(\d+)")
|
||
|
addr = getFirstIPAddress() # TODO:: -------------------------------------------------------------------------------------------------------- HANDLE
|
||
|
if addr:
|
||
|
match = subnetRegex.search(addr)
|
||
|
if match:
|
||
|
ip = match.group(1) + "255"
|
||
|
util.DEBUG_LOG("Using broadcast address {0}".format())
|
||
|
|
||
|
# Socket things sometimes fail for no good reason, so try a few times.
|
||
|
attempt = 0
|
||
|
success = False
|
||
|
|
||
|
while attempt < 5 and not success:
|
||
|
udp = CreateObject("roDatagramSocket")
|
||
|
udp.setMessagePort(Application().port)
|
||
|
udp.setBroadcast(true)
|
||
|
|
||
|
# More things that have been observed to be flaky.
|
||
|
for i in range(5):
|
||
|
addr = CreateObject("roSocketAddress")
|
||
|
addr.setHostName(ip)
|
||
|
addr.setPort(32414)
|
||
|
udp.setSendToAddress(addr)
|
||
|
|
||
|
sendTo = udp.getSendToAddress()
|
||
|
if sendTo:
|
||
|
sendToStr = str(sendTo.getAddress())
|
||
|
addrStr = str(addr.getAddress())
|
||
|
util.DEBUG_LOG("GDM sendto address: " + sendToStr + " / " + addrStr)
|
||
|
if sendToStr == addrStr:
|
||
|
break
|
||
|
|
||
|
util.ERROR_LOG("Failed to set GDM sendto address")
|
||
|
|
||
|
udp.notifyReadable(true)
|
||
|
bytesSent = udp.sendStr(message)
|
||
|
util.DEBUG_LOG("Sent " + str(bytesSent) + " bytes")
|
||
|
if bytesSent > 0:
|
||
|
success = udp.eOK()
|
||
|
else:
|
||
|
success = False
|
||
|
if bytesSent == 0 and ip != multicast:
|
||
|
util.LOG("Falling back to multicast address")
|
||
|
ip = multicast
|
||
|
attempt = 0
|
||
|
|
||
|
if success:
|
||
|
break
|
||
|
elif attempt == 4 and ip != multicast:
|
||
|
util.LOG("Falling back to multicast address")
|
||
|
ip = multicast
|
||
|
attempt = 0
|
||
|
else:
|
||
|
time.sleep(500)
|
||
|
util.WARN_LOG("Retrying GDM, errno=" + str(udp.status()))
|
||
|
attempt += 1
|
||
|
|
||
|
if success:
|
||
|
util.DEBUG_LOG("Successfully sent GDM discovery message, waiting for servers")
|
||
|
self.servers = []
|
||
|
self.timer = plexapp.createTimer(5000, self.onTimer)
|
||
|
self.socket = udp
|
||
|
Application().AddSocketCallback(udp, createCallable("OnSocketEvent", m))
|
||
|
plexapp.APP.addTimer(self.timer)
|
||
|
else:
|
||
|
util.ERROR_LOG("Failed to send GDM discovery message")
|
||
|
import plexapp
|
||
|
import plexresource
|
||
|
plexapp.SERVERMANAGER.UpdateFromConnectionType([], plexresource.ResourceConnection.SOURCE_DISCOVERED)
|
||
|
self.socket = None
|
||
|
self.timer = None
|
||
|
'''
|
||
|
|
||
|
def discover(self):
|
||
|
import plexapp
|
||
|
if not plexapp.INTERFACE.getPreference("gdm_discovery", True) or self.isActive():
|
||
|
return
|
||
|
|
||
|
self.thread = threading.Thread(target=self._discover)
|
||
|
self.thread.start()
|
||
|
|
||
|
def _discover(self):
|
||
|
ifaces = netif.getInterfaces()
|
||
|
sockets = []
|
||
|
self.servers = []
|
||
|
|
||
|
packet = "M-SEARCH * HTTP/1.1" + WIN_NL + WIN_NL
|
||
|
|
||
|
for i in ifaces:
|
||
|
if not i.broadcast:
|
||
|
continue
|
||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
|
s.settimeout(0.01) # 10ms
|
||
|
s.bind((i.ip, 0))
|
||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||
|
sockets.append((s, i))
|
||
|
|
||
|
success = False
|
||
|
|
||
|
for attempt in (0, 1):
|
||
|
for s, i in sockets:
|
||
|
if self._close:
|
||
|
return
|
||
|
util.DEBUG_LOG(' o-> Broadcasting to {0}: {1}'.format(i.name, i.broadcast))
|
||
|
try:
|
||
|
s.sendto(packet, (i.broadcast, DISCOVERY_PORT))
|
||
|
success = True
|
||
|
except:
|
||
|
util.ERROR()
|
||
|
|
||
|
if success:
|
||
|
break
|
||
|
|
||
|
end = time.time() + 5
|
||
|
|
||
|
while time.time() < end:
|
||
|
for s, i in sockets:
|
||
|
if self._close:
|
||
|
return
|
||
|
try:
|
||
|
message, address = s.recvfrom(4096)
|
||
|
self.onSocketEvent(message, address)
|
||
|
except socket.timeout:
|
||
|
pass
|
||
|
except:
|
||
|
traceback.print_exc()
|
||
|
|
||
|
self.discoveryFinished()
|
||
|
|
||
|
def onSocketEvent(self, message, addr):
|
||
|
util.DEBUG_LOG('Received GDM message:\n' + str(message))
|
||
|
|
||
|
hostname = addr[0] # socket.gethostbyaddr(addr[0])[0]
|
||
|
|
||
|
name = parseFieldValue(message, "Name: ")
|
||
|
port = parseFieldValue(message, "Port: ") or "32400"
|
||
|
machineID = parseFieldValue(message, "Resource-Identifier: ")
|
||
|
secureHost = parseFieldValue(message, "Host: ")
|
||
|
|
||
|
util.DEBUG_LOG("Received GDM response for " + repr(name) + " at http://" + hostname + ":" + port)
|
||
|
|
||
|
if not name or not machineID:
|
||
|
return
|
||
|
|
||
|
import plexserver
|
||
|
conn = plexconnection.PlexConnection(plexconnection.PlexConnection.SOURCE_DISCOVERED, "http://" + hostname + ":" + port, True, None, bool(secureHost))
|
||
|
server = plexserver.createPlexServerForConnection(conn)
|
||
|
server.uuid = machineID
|
||
|
server.name = name
|
||
|
server.sameNetwork = True
|
||
|
|
||
|
# If the server advertised a secure hostname, add a secure connection as well, and
|
||
|
# set the http connection as a fallback.
|
||
|
#
|
||
|
if secureHost:
|
||
|
server.connections.insert(
|
||
|
0,
|
||
|
plexconnection.PlexConnection(
|
||
|
plexconnection.PlexConnection.SOURCE_DISCOVERED, "https://" + hostname.replace(".", "-") + "." + secureHost + ":" + port, True, None
|
||
|
)
|
||
|
)
|
||
|
|
||
|
self.servers.append(server)
|
||
|
|
||
|
def discoveryFinished(self, *args, **kwargs):
|
||
|
# Time's up, report whatever we found
|
||
|
self.close()
|
||
|
|
||
|
if self.servers:
|
||
|
util.LOG("Finished GDM discovery, found {0} server(s)".format(len(self.servers)))
|
||
|
import plexapp
|
||
|
plexapp.SERVERMANAGER.updateFromConnectionType(self.servers, plexconnection.PlexConnection.SOURCE_DISCOVERED)
|
||
|
self.servers = None
|
||
|
|
||
|
def close(self):
|
||
|
self._close = True
|
||
|
|
||
|
|
||
|
def parseFieldValue(message, label):
|
||
|
if label not in message:
|
||
|
return None
|
||
|
|
||
|
return message.split(label, 1)[-1].split(chr(13))[0]
|
||
|
|
||
|
|
||
|
DISCOVERY = GDMDiscovery()
|
||
|
|
||
|
'''
|
||
|
# GDM Advertising
|
||
|
|
||
|
class GDMAdvertiser(object):
|
||
|
|
||
|
def __init__(self):
|
||
|
self.responseString = None
|
||
|
|
||
|
def createSocket()
|
||
|
listenAddr = CreateObject("roSocketAddress")
|
||
|
listenAddr.setPort(32412)
|
||
|
listenAddr.setAddress("0.0.0.0")
|
||
|
|
||
|
udp = CreateObject("roDatagramSocket")
|
||
|
|
||
|
if not udp.setAddress(listenAddr) then
|
||
|
Error("Failed to set address on GDM advertiser socket")
|
||
|
return
|
||
|
end if
|
||
|
|
||
|
if not udp.setBroadcast(true) then
|
||
|
Error("Failed to set broadcast on GDM advertiser socket")
|
||
|
return
|
||
|
end if
|
||
|
|
||
|
udp.notifyReadable(true)
|
||
|
udp.setMessagePort(Application().port)
|
||
|
|
||
|
m.socket = udp
|
||
|
|
||
|
Application().AddSocketCallback(udp, createCallable("OnSocketEvent", m))
|
||
|
|
||
|
Debug("Created GDM player advertiser")
|
||
|
|
||
|
|
||
|
def refresh()
|
||
|
# Always regenerate our response, even if it might not have changed, it's
|
||
|
# just not that expensive.
|
||
|
m.responseString = invalid
|
||
|
|
||
|
enabled = AppSettings().GetBoolPreference("remotecontrol")
|
||
|
if enabled AND m.socket = invalid then
|
||
|
m.CreateSocket()
|
||
|
else if not enabled AND m.socket <> invalid then
|
||
|
m.Close()
|
||
|
end if
|
||
|
|
||
|
|
||
|
def cleanup()
|
||
|
m.Close()
|
||
|
fn = function() :m.GDMAdvertiser = invalid :
|
||
|
fn()
|
||
|
|
||
|
|
||
|
def onSocketEvent(msg as object)
|
||
|
# PMS polls every five seconds, so this is chatty when not debugging.
|
||
|
# Debug("Got a GDM advertiser socket event, is readable: " + tostr(m.socket.isReadable()))
|
||
|
|
||
|
if m.socket.isReadable() then
|
||
|
message = m.socket.receiveStr(4096)
|
||
|
endIndex = instr(1, message, chr(13)) - 1
|
||
|
if endIndex <= 0 then endIndex = message.Len()
|
||
|
line = Mid(message, 1, endIndex)
|
||
|
|
||
|
if line = "M-SEARCH * HTTP/1.1" then
|
||
|
response = m.GetResponseString()
|
||
|
|
||
|
# Respond directly to whoever sent the search message.
|
||
|
sock = CreateObject("roDatagramSocket")
|
||
|
sock.setSendToAddress(m.socket.getReceivedFromAddress())
|
||
|
bytesSent = sock.sendStr(response)
|
||
|
sock.Close()
|
||
|
if bytesSent <> Len(response) then
|
||
|
Error("GDM player response only sent " + tostr(bytesSent) + " bytes out of " + tostr(Len(response)))
|
||
|
end if
|
||
|
else
|
||
|
Error("Received unexpected message on GDM advertiser socket: " + tostr(line) + ";")
|
||
|
end if
|
||
|
end if
|
||
|
|
||
|
|
||
|
def getResponseString() as string
|
||
|
if m.responseString = invalid then
|
||
|
buf = box("HTTP/1.0 200 OK" + WinNL())
|
||
|
|
||
|
settings = AppSettings()
|
||
|
|
||
|
appendNameValue(buf, "Name", settings.GetGlobal("friendlyName"))
|
||
|
appendNameValue(buf, "Port", WebServer().port.tostr())
|
||
|
appendNameValue(buf, "Product", "Plex for Roku")
|
||
|
appendNameValue(buf, "Content-Type", "plex/media-player")
|
||
|
appendNameValue(buf, "Protocol", "plex")
|
||
|
appendNameValue(buf, "Protocol-Version", "1")
|
||
|
appendNameValue(buf, "Protocol-Capabilities", "timeline,playback,navigation,playqueues")
|
||
|
appendNameValue(buf, "Version", settings.GetGlobal("appVersionStr"))
|
||
|
appendNameValue(buf, "Resource-Identifier", settings.GetGlobal("clientIdentifier"))
|
||
|
appendNameValue(buf, "Device-Class", "stb")
|
||
|
|
||
|
m.responseString = buf
|
||
|
|
||
|
Debug("Built GDM player response:" + m.responseString)
|
||
|
end if
|
||
|
|
||
|
return m.responseString
|
||
|
|
||
|
|
||
|
sub appendNameValue(buf, name, value)
|
||
|
line = name + ": " + value + WinNL()
|
||
|
buf.AppendString(line, Len(line))
|
||
|
|
||
|
'''
|