Skip to content

Commit 807aaea

Browse files
authored
Merge pull request #445 from jjlawren/add_gdm_scanning
GDM server and client discovery
2 parents 7ed812b + ee5983f commit 807aaea

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed

plexapi/gdm.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
15+
16+
class GDM:
17+
"""Base class to discover GDM services."""
18+
19+
def __init__(self):
20+
self.entries = []
21+
self.last_scan = None
22+
23+
def scan(self, scan_for_clients=False):
24+
"""Scan the network."""
25+
self.update(scan_for_clients)
26+
27+
def all(self):
28+
"""Return all found entries.
29+
30+
Will scan for entries if not scanned recently.
31+
"""
32+
self.scan()
33+
return list(self.entries)
34+
35+
def find_by_content_type(self, value):
36+
"""Return a list of entries that match the content_type."""
37+
self.scan()
38+
return [entry for entry in self.entries
39+
if value in entry['data']['Content_Type']]
40+
41+
def find_by_data(self, values):
42+
"""Return a list of entries that match the search parameters."""
43+
self.scan()
44+
return [entry for entry in self.entries
45+
if all(item in entry['data'].items()
46+
for item in values.items())]
47+
48+
def update(self, scan_for_clients):
49+
"""Scan for new GDM services.
50+
51+
Examples of the dict list assigned to self.entries by this function:
52+
53+
Server:
54+
[{'data': {
55+
'Content-Type': 'plex/media-server',
56+
'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct',
57+
'Name': 'myfirstplexserver',
58+
'Port': '32400',
59+
'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7',
60+
'Updated-At': '1585769946',
61+
'Version': '1.18.8.2527-740d4c206',
62+
},
63+
'from': ('10.10.10.100', 32414)}]
64+
65+
Clients:
66+
[{'data': {'Content-Type': 'plex/media-player',
67+
'Device-Class': 'stb',
68+
'Name': 'plexamp',
69+
'Port': '36000',
70+
'Product': 'Plexamp',
71+
'Protocol': 'plex',
72+
'Protocol-Capabilities': 'timeline,playback,playqueues,playqueues-creation',
73+
'Protocol-Version': '1',
74+
'Resource-Identifier': 'b6e57a3f-e0f8-494f-8884-f4b58501467e',
75+
'Version': '1.1.0',
76+
},
77+
'from': ('10.10.10.101', 32412)}]
78+
"""
79+
80+
gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii')
81+
gdm_timeout = 1
82+
83+
self.entries = []
84+
known_responses = []
85+
86+
# setup socket for discovery -> multicast message
87+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
88+
sock.settimeout(gdm_timeout)
89+
90+
# Set the time-to-live for messages for local network
91+
sock.setsockopt(socket.IPPROTO_IP,
92+
socket.IP_MULTICAST_TTL,
93+
struct.pack("B", gdm_timeout))
94+
95+
if scan_for_clients:
96+
# setup socket for broadcast to Plex clients
97+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
98+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
99+
gdm_ip = '255.255.255.255'
100+
gdm_port = 32412
101+
else:
102+
# setup socket for multicast to Plex server(s)
103+
gdm_ip = '239.0.0.250'
104+
gdm_port = 32414
105+
106+
try:
107+
# Send data to the multicast group
108+
sock.sendto(gdm_msg, (gdm_ip, gdm_port))
109+
110+
# Look for responses from all recipients
111+
while True:
112+
try:
113+
bdata, host = sock.recvfrom(1024)
114+
data = bdata.decode('utf-8')
115+
if '200 OK' in data.splitlines()[0]:
116+
ddata = {k: v.strip() for (k, v) in (
117+
line.split(':') for line in
118+
data.splitlines() if ':' in line)}
119+
identifier = ddata.get('Resource-Identifier')
120+
if identifier and identifier in known_responses:
121+
continue
122+
known_responses.append(identifier)
123+
self.entries.append({'data': ddata,
124+
'from': host})
125+
except socket.timeout:
126+
break
127+
finally:
128+
sock.close()
129+
130+
131+
def main():
132+
"""Test GDM discovery."""
133+
from pprint import pprint
134+
135+
gdm = GDM()
136+
137+
pprint("Scanning GDM for servers...")
138+
gdm.scan()
139+
pprint(gdm.entries)
140+
141+
pprint("Scanning GDM for clients...")
142+
gdm.scan(scan_for_clients=True)
143+
pprint(gdm.entries)
144+
145+
146+
if __name__ == "__main__":
147+
main()

0 commit comments

Comments
 (0)