PlexKodiConnect/resources/lib/plexbmchelper/plexgdm.py

394 lines
14 KiB
Python
Raw Normal View History

2016-01-15 22:12:52 +11:00
"""
PlexGDM.py - Version 0.2
This class implements the Plex GDM (G'Day Mate) protocol to discover
local Plex Media Servers. Also allow client registration into all local
media servers.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301, USA.
"""
__author__ = 'DHJ (hippojay) <plex@h-jay.com>'
import socket
import struct
import threading
import time
from xbmc import sleep
import downloadutils
from PlexFunctions import PMSHttpsEnabled
from utils import window, logging
2016-01-15 22:12:52 +11:00
@logging
2016-01-15 22:12:52 +11:00
class plexgdm:
def __init__(self):
2016-01-15 22:12:52 +11:00
self.discover_message = 'M-SEARCH * HTTP/1.0'
self.client_header = '* HTTP/1.0'
self.client_data = None
self.client_id = None
2016-01-15 22:12:52 +11:00
self._multicast_address = '239.0.0.250'
self.discover_group = (self._multicast_address, 32414)
self.client_register_group = (self._multicast_address, 32413)
self.client_update_port = 32412
self.server_list = []
self.discovery_interval = 120
2016-01-15 22:12:52 +11:00
self._discovery_is_running = False
self._registration_is_running = False
self.discovery_complete = False
self.client_registered = False
self.download = downloadutils.DownloadUtils().downloadUrl
2016-01-15 22:12:52 +11:00
def clientDetails(self, settings):
self.client_data = (
"Content-Type: plex/media-player\r\n"
"Resource-Identifier: %s\r\n"
"Name: %s\r\n"
"Port: %s\r\n"
"Product: %s\r\n"
"Version: %s\r\n"
"Protocol: plex\r\n"
"Protocol-Version: 1\r\n"
"Protocol-Capabilities: timeline,playback,navigation,"
"mirror,playqueues\r\n"
"Device-Class: HTPC"
) % (
settings['uuid'],
settings['client_name'],
settings['myport'],
settings['addonName'],
settings['version']
)
self.client_id = settings['uuid']
2016-01-15 22:12:52 +11:00
def getClientDetails(self):
if not self.client_data:
self.logMsg("Client data has not been initialised. Please use "
"PlexGDM.clientDetails()", -1)
2016-01-15 22:12:52 +11:00
return self.client_data
def client_update(self):
update_sock = socket.socket(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
# Set socket reuse, may not work on all OSs.
2016-01-15 22:12:52 +11:00
try:
update_sock.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1)
2016-01-15 22:12:52 +11:00
except:
pass
# Attempt to bind to the socket to recieve and send data. If we cant
# do this, then we cannot send registration
2016-01-15 22:12:52 +11:00
try:
update_sock.bind(('0.0.0.0', self.client_update_port))
2016-01-15 22:12:52 +11:00
except:
self.logMsg("Unable to bind to port [%s] - client will not be "
"registered" % self.client_update_port, -1)
return
update_sock.setsockopt(socket.IPPROTO_IP,
socket.IP_MULTICAST_TTL,
255)
update_sock.setsockopt(socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(
self._multicast_address) +
socket.inet_aton('0.0.0.0'))
2016-01-15 22:12:52 +11:00
update_sock.setblocking(0)
self.logMsg("Sending registration data: HELLO %s\r\n%s"
% (self.client_header, self.client_data), 2)
# Send initial client registration
2016-01-15 22:12:52 +11:00
try:
update_sock.sendto("HELLO %s\r\n%s"
% (self.client_header, self.client_data),
self.client_register_group)
2016-01-15 22:12:52 +11:00
except:
self.logMsg("Unable to send registration message", -1)
# Now, listen for client discovery reguests and respond.
2016-01-15 22:12:52 +11:00
while self._registration_is_running:
try:
data, addr = update_sock.recvfrom(1024)
self.logMsg("Recieved UDP packet from [%s] containing [%s]"
% (addr, data.strip()), 2)
except socket.error:
2016-01-15 22:12:52 +11:00
pass
else:
if "M-SEARCH * HTTP/1." in data:
self.logMsg("Detected client discovery request from %s. "
" Replying" % addr, 2)
2016-01-15 22:12:52 +11:00
try:
update_sock.sendto("HTTP/1.0 200 OK\r\n%s"
% self.client_data,
addr)
2016-01-15 22:12:52 +11:00
except:
self.logMsg("Unable to send client update message", -1)
self.logMsg("Sending registration data: HTTP/1.0 200 OK"
"\r\n%s" % self.client_data, 2)
2016-01-15 22:12:52 +11:00
self.client_registered = True
sleep(500)
self.logMsg("Client Update loop stopped", 1)
2016-01-15 22:12:52 +11:00
# When we are finished, then send a final goodbye message to
# deregister cleanly.
self.logMsg("Sending registration data: BYE %s\r\n%s"
% (self.client_header, self.client_data), 2)
2016-01-15 22:12:52 +11:00
try:
update_sock.sendto("BYE %s\r\n%s"
% (self.client_header, self.client_data),
self.client_register_group)
2016-01-15 22:12:52 +11:00
except:
self.logMsg("Unable to send client update message", -1)
2016-01-15 22:12:52 +11:00
self.client_registered = False
2016-01-15 22:12:52 +11:00
def check_client_registration(self):
2016-01-15 22:12:52 +11:00
if self.client_registered and self.discovery_complete:
2016-01-15 22:12:52 +11:00
if not self.server_list:
self.logMsg("Server list is empty. Unable to check", 1)
2016-01-15 22:12:52 +11:00
return False
try:
for server in self.server_list:
if server['uuid'] == window('plex_machineIdentifier'):
media_server = server['server']
media_port = server['port']
scheme = server['protocol']
break
else:
self.logMsg("Did not find our server!", 0)
return False
2016-01-15 22:12:52 +11:00
self.logMsg("Checking server [%s] on port [%s]"
% (media_server, media_port), 2)
client_result = self.download(
'%s://%s:%s/clients' % (scheme, media_server, media_port))
registered = False
for client in client_result:
if (client.attrib.get('machineIdentifier') ==
self.client_id):
registered = True
if registered:
self.logMsg("Client registration successful", 1)
self.logMsg("Client data is: %s" % client_result, 2)
2016-01-15 22:12:52 +11:00
return True
else:
self.logMsg("Client registration not found", 1)
self.logMsg("Client data is: %s" % client_result, 1)
2016-01-15 22:12:52 +11:00
except:
self.logMsg("Unable to check status", 0)
2016-01-15 22:12:52 +11:00
pass
2016-01-15 22:12:52 +11:00
return False
def getServerList(self):
2016-01-15 22:12:52 +11:00
return self.server_list
2016-01-15 22:12:52 +11:00
def discover(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set a timeout so the socket does not block indefinitely
sock.settimeout(0.6)
# Set the time-to-live for messages to 1 for local network
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
self.logMsg("Sending discovery messages: %s"
% self.discover_message, 2)
sock.sendto(self.discover_message, self.discover_group)
2016-01-15 22:12:52 +11:00
# Look for responses from all recipients
while True:
try:
data, server = sock.recvfrom(1024)
self.logMsg("Received data from %s, %s" % server, 2)
self.logMsg("Data received is:\r\n %s" % data, 2)
returnData.append({'from': server,
'data': data})
2016-01-15 22:12:52 +11:00
except socket.timeout:
break
except:
# if we can't send our discovery query, just abort and try again
# on the next loop
return
finally:
sock.close()
self.discovery_complete = True
discovered_servers = []
if returnData:
for response in returnData:
update = {'server': response.get('from')[0]}
2016-01-15 22:12:52 +11:00
# Check if we had a positive HTTP reponse
2016-01-15 22:12:52 +11:00
if "200 OK" in response.get('data'):
for each in response.get('data').split('\r\n'):
update['discovery'] = "auto"
update['owned'] = '1'
update['master'] = 1
update['role'] = 'master'
update['class'] = None
2016-01-15 22:12:52 +11:00
if "Content-Type:" in each:
update['content-type'] = each.split(':')[1].strip()
elif "Resource-Identifier:" in each:
update['uuid'] = each.split(':')[1].strip()
elif "Name:" in each:
update['serverName'] = each.split(':')[1].strip()
elif "Port:" in each:
update['port'] = each.split(':')[1].strip()
elif "Updated-At:" in each:
update['updated'] = each.split(':')[1].strip()
elif "Version:" in each:
update['version'] = each.split(':')[1].strip()
elif "Server-Class:" in each:
update['class'] = each.split(':')[1].strip()
# Quickly test if we need https
https = PMSHttpsEnabled(
'%s:%s' % (update['server'], update['port']))
if https is None:
# Error contacting server
continue
elif https:
update['protocol'] = 'https'
else:
update['protocol'] = 'http'
discovered_servers.append(update)
2016-01-15 22:12:52 +11:00
2016-03-23 00:40:38 +11:00
# Append REMOTE PMS that we haven't found yet; if necessary
currServer = window('pms_server')
if currServer:
currServerProt, currServerIP, currServerPort = \
currServer.split(':')
currServerIP = currServerIP.replace('/', '')
for server in discovered_servers:
if server['server'] == currServerIP:
break
else:
# Currently active server was not discovered via GDM; ADD
update = {
'port': currServerPort,
'protocol': currServerProt,
'class': None,
'content-type': 'plex/media-server',
'discovery': 'auto',
'master': 1,
'owned': '1',
'role': 'master',
'server': currServerIP,
'serverName': window('plex_servername'),
'updated': int(time.time()),
'uuid': window('plex_machineIdentifier'),
'version': 'irrelevant'
}
discovered_servers.append(update)
2016-01-15 22:12:52 +11:00
self.server_list = discovered_servers
2016-03-23 00:40:38 +11:00
2016-01-15 22:12:52 +11:00
if not self.server_list:
self.logMsg("No servers have been discovered", 0)
2016-01-15 22:12:52 +11:00
else:
self.logMsg("Number of servers Discovered: %s"
% len(self.server_list), 2)
2016-01-15 22:12:52 +11:00
for items in self.server_list:
self.logMsg("Server Discovered: %s" % items, 2)
2016-01-15 22:12:52 +11:00
def setInterval(self, interval):
self.discovery_interval = interval
def stop_all(self):
self.stop_discovery()
self.stop_registration()
def stop_discovery(self):
if self._discovery_is_running:
self.logMsg("Discovery shutting down", 0)
2016-01-15 22:12:52 +11:00
self._discovery_is_running = False
self.discover_t.join()
del self.discover_t
else:
self.logMsg("Discovery not running", 0)
2016-01-15 22:12:52 +11:00
def stop_registration(self):
if self._registration_is_running:
self.logMsg("Registration shutting down", 0)
2016-01-15 22:12:52 +11:00
self._registration_is_running = False
self.register_t.join()
del self.register_t
else:
self.logMsg("Registration not running", 0)
2016-01-15 22:12:52 +11:00
def run_discovery_loop(self):
# Run initial discovery
2016-01-15 22:12:52 +11:00
self.discover()
discovery_count = 0
2016-01-15 22:12:52 +11:00
while self._discovery_is_running:
discovery_count += 1
2016-01-15 22:12:52 +11:00
if discovery_count > self.discovery_interval:
self.discover()
discovery_count = 0
sleep(500)
2016-01-15 22:12:52 +11:00
def start_discovery(self, daemon=False):
2016-01-15 22:12:52 +11:00
if not self._discovery_is_running:
self.logMsg("Discovery starting up", 0)
2016-01-15 22:12:52 +11:00
self._discovery_is_running = True
self.discover_t = threading.Thread(target=self.run_discovery_loop)
self.discover_t.setDaemon(daemon)
self.discover_t.start()
else:
self.logMsg("Discovery already running", 0)
def start_registration(self, daemon=False):
2016-01-15 22:12:52 +11:00
if not self._registration_is_running:
self.logMsg("Registration starting up", 0)
2016-01-15 22:12:52 +11:00
self._registration_is_running = True
self.register_t = threading.Thread(target=self.client_update)
self.register_t.setDaemon(daemon)
self.register_t.start()
else:
self.logMsg("Registration already running", 0)
def start_all(self, daemon=False):
2016-01-15 22:12:52 +11:00
self.start_discovery(daemon)
self.start_registration(daemon)