|
| 1 | +""" |
| 2 | +Support for discovery using GDM (Good Day Mate), multicast protocol by Plex. |
| 3 | +
|
| 4 | +# Licensed Apache 2.0 |
| 5 | +# From https://github.com/home-assistant/netdisco/netdisco/gdm.py |
| 6 | +
|
| 7 | +Inspired by |
| 8 | + hippojay's plexGDM: |
| 9 | + https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py |
| 10 | + iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py |
| 11 | +""" |
| 12 | +import socket |
| 13 | +import struct |
| 14 | +from typing import Any, Dict, List # noqa: F401 |
| 15 | + |
| 16 | + |
| 17 | +class GDM: |
| 18 | + """Base class to discover GDM services.""" |
| 19 | + |
| 20 | + def __init__(self): |
| 21 | + self.entries = [] # type: List[Dict[str, Any]] |
| 22 | + self.last_scan = None |
| 23 | + |
| 24 | + def scan(self): |
| 25 | + """Scan the network.""" |
| 26 | + self.update() |
| 27 | + |
| 28 | + def all(self): |
| 29 | + """Return all found entries. |
| 30 | +
|
| 31 | + Will scan for entries if not scanned recently. |
| 32 | + """ |
| 33 | + self.scan() |
| 34 | + return list(self.entries) |
| 35 | + |
| 36 | + def find_by_content_type(self, value): |
| 37 | + """Return a list of entries that match the content_type.""" |
| 38 | + self.scan() |
| 39 | + return [entry for entry in self.entries |
| 40 | + if value in entry['data']['Content_Type']] |
| 41 | + |
| 42 | + def find_by_data(self, values): |
| 43 | + """Return a list of entries that match the search parameters.""" |
| 44 | + self.scan() |
| 45 | + return [entry for entry in self.entries |
| 46 | + if all(item in entry['data'].items() |
| 47 | + for item in values.items())] |
| 48 | + |
| 49 | + def update(self): |
| 50 | + """Scan for new GDM services. |
| 51 | +
|
| 52 | + Example of the dict list assigned to self.entries by this function: |
| 53 | + [{'data': { |
| 54 | + 'Content-Type': 'plex/media-server', |
| 55 | + 'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct', |
| 56 | + 'Name': 'myfirstplexserver', |
| 57 | + 'Port': '32400', |
| 58 | + 'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7', |
| 59 | + 'Updated-At': '1444852697', |
| 60 | + 'Version': '0.9.12.13.1464-4ccd2ca', |
| 61 | + }, |
| 62 | + 'from': ('10.10.10.100', 32414)}] |
| 63 | + """ |
| 64 | + |
| 65 | + gdm_ip = '239.0.0.250' # multicast to PMS |
| 66 | + gdm_port = 32414 |
| 67 | + gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii') |
| 68 | + gdm_timeout = 1 |
| 69 | + |
| 70 | + self.entries = [] |
| 71 | + |
| 72 | + # setup socket for discovery -> multicast message |
| 73 | + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 74 | + sock.settimeout(gdm_timeout) |
| 75 | + |
| 76 | + # Set the time-to-live for messages for local network |
| 77 | + sock.setsockopt(socket.IPPROTO_IP, |
| 78 | + socket.IP_MULTICAST_TTL, |
| 79 | + struct.pack("B", gdm_timeout)) |
| 80 | + |
| 81 | + try: |
| 82 | + # Send data to the multicast group |
| 83 | + sock.sendto(gdm_msg, (gdm_ip, gdm_port)) |
| 84 | + |
| 85 | + # Look for responses from all recipients |
| 86 | + while True: |
| 87 | + try: |
| 88 | + bdata, server = sock.recvfrom(1024) |
| 89 | + data = bdata.decode('utf-8') |
| 90 | + if '200 OK' in data.splitlines()[0]: |
| 91 | + ddata = {k: v.strip() for (k, v) in ( |
| 92 | + line.split(':') for line in |
| 93 | + data.splitlines() if ':' in line)} |
| 94 | + self.entries.append({'data': ddata, |
| 95 | + 'from': server}) |
| 96 | + except socket.timeout: |
| 97 | + break |
| 98 | + finally: |
| 99 | + sock.close() |
| 100 | + |
| 101 | + |
| 102 | +def main(): |
| 103 | + """Test GDM discovery.""" |
| 104 | + from pprint import pprint |
| 105 | + |
| 106 | + gdm = GDM() |
| 107 | + |
| 108 | + pprint("Scanning GDM...") |
| 109 | + gdm.update() |
| 110 | + pprint(gdm.entries) |
| 111 | + |
| 112 | + |
| 113 | +if __name__ == "__main__": |
| 114 | + main() |
0 commit comments