PlexKodiConnect/resources/lib/plex_companion/plexgdm.py

210 lines
7.9 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
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.
"""
import logging
import socket
from ..downloadutils import DownloadUtils as DU
from .. import backgroundthread
from .. import utils, app, variables as v
log = logging.getLogger('PLEX.plexgdm')
class plexgdm(backgroundthread.KillableThread):
daemon = True
def __init__(self):
client = (
'Content-Type: plex/media-player\n'
f'Resource-Identifier: {v.PKC_MACHINE_IDENTIFIER}\n'
f'Name: {v.DEVICENAME}\n'
f'Port: {v.COMPANION_PORT}\n'
f'Product: {v.ADDON_NAME}\n'
f'Version: {v.ADDON_VERSION}\n'
'Protocol: plex\n'
'Protocol-Version: 3\n'
'Protocol-Capabilities: timeline,playback,navigation,playqueues\n'
'Device-Class: pc\n'
)
self.hello_msg = f'HELLO * HTTP/1.0\n{client}'.encode()
self.ok_msg = f'HTTP/1.0 200 OK\n{client}'.encode()
self.bye_msg = f'BYE * HTTP/1.0\n{client}'.encode()
self.socket = None
self.port = int(utils.settings('companionUpdatePort'))
self.multicast_address = '239.0.0.250'
self.client_register_group = (self.multicast_address, 32413)
super().__init__()
def on_bind_error(self):
self.socket = None
log.error('Unable to bind to port [%s] - Plex Companion will not '
'be registered. Change the Plex Companion update port!'
% self.port)
if utils.settings('companion_show_gdm_port_warning') == 'true':
from ..windows import optionsdialog
# Plex Companion could not open the GDM port. Please change it
# in the PKC settings.
if optionsdialog.show(utils.lang(29999),
'Port %s\n%s' % (self.port,
utils.lang(39079)),
utils.lang(30013), # Never show again
utils.lang(186)) == 0:
utils.settings('companion_show_gdm_port_warning',
value='false')
from xbmc import executebuiltin
executebuiltin(
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
def register_as_client(self):
'''
Registers PKC's Plex Companion to the PMS
'''
log.debug('Sending registration data: HELLO')
try:
self.socket.sendto(self.hello_msg, self.client_register_group)
except Exception as exc:
log.error('Unable to send registration message. Error: %s', exc)
def check_client_registration(self):
"""
Checks whetere we are registered as a Plex Companion casting target
(using the old "GDM method") on our PMS. If not, registers
"""
if self.socket is None:
return
log.debug('Checking whether we are still listed as GDM Plex Companion'
'client on our PMS')
xml = DU().downloadUrl('{server}/clients')
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
log.error('Could not download GDM Plex Companion clients')
return False
for client in xml:
if (client.attrib.get('machineIdentifier') == v.PKC_MACHINE_IDENTIFIER):
break
else:
log.info('PKC not registered as a GDM Plex Companion client')
self.register_as_client()
def setup_socket(self):
self.socket = socket.socket(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
# Set socket reuse, may not work on all OSs.
try:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception:
pass
# Attempt to bind to the socket to recieve and send data. If we cant
# do this, then we cannot send registration
try:
self.socket.bind(('0.0.0.0', self.port))
except Exception:
self.on_bind_error()
return False
self.socket.setsockopt(socket.IPPROTO_IP,
socket.IP_MULTICAST_TTL,
255)
self.socket.setsockopt(socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton(self.multicast_address) + socket.inet_aton('0.0.0.0'))
self.socket.setblocking(0)
return True
def teardown_socket(self):
'''
When we are finished, then send a final goodbye message to deregister
cleanly.
'''
if self.socket is None:
return
log.debug('Sending goodbye: BYE')
try:
self.socket.sendto(self.bye_msg, self.client_register_group)
except Exception:
log.error('Unable to send client goodbye message')
try:
self.socket.shutdown(socket.SHUT_RDWR)
except OSError:
# The server might already have closed the connection. On Windows,
# this may result in WSAEINVAL (error 10022): An invalid operation
# was attempted.
pass
finally:
self.socket.close()
self.socket = None
def reply(self, addr):
log.debug('Detected client discovery request from %s. Replying', addr)
try:
self.socket.sendto(self.ok_msg, addr)
except Exception as error:
log.error('Unable to send client update message to %s', addr)
log.error('Error encountered: %s: %s', type(error), error)
def wait_while_suspended(self):
should_shutdown = super().wait_while_suspended()
if not should_shutdown and not self.setup_socket():
raise RuntimeError('Could not bind socket to port %s' % self.port)
return should_shutdown
def run(self):
if not utils.settings('plexCompanion') == 'true':
return
log.info('----===## Starting PlexGDM client ##===----')
app.APP.register_thread(self)
try:
self._run()
finally:
self.teardown_socket()
app.APP.deregister_thread(self)
log.info('----===## Stopping PlexGDM client ##===----')
def _run(self):
if not self.setup_socket():
return
# Send initial client registration
self.register_as_client()
# Listen for Plex Companion client discovery reguests and respond
while not self.should_cancel():
if self.should_suspend():
self.teardown_socket()
if self.wait_while_suspended():
break
try:
data, addr = self.socket.recvfrom(1024)
except socket.error:
pass
else:
data = data.decode()
log.debug('Received UDP packet from [%s] containing [%s]'
% (addr, data.strip()))
if 'M-SEARCH * HTTP/1.' in data:
self.reply(addr)
self.sleep(0.5)