Skip to content

Commit 2af8974

Browse files
committed
Add GDM implementation from netdisco
1 parent 7ed812b commit 2af8974

File tree

1 file changed

+114
-0
lines changed

1 file changed

+114
-0
lines changed

plexapi/gdm.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)