Skip to content

Commit 839a9da

Browse files
authored
Merge branch 'master' into conversion_actions
2 parents 0edca72 + 807aaea commit 839a9da

File tree

6 files changed

+210
-5
lines changed

6 files changed

+210
-5
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ services:
1010
- docker
1111

1212
python:
13-
- 2.7
14-
- 3.4
1513
- 3.6
1614

1715
env:

plexapi/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,13 @@ def sendCommand(self, command, proxy=None, **params):
204204
return query(key, headers=headers)
205205
except ElementTree.ParseError:
206206
# Workaround for players which don't return valid XML on successful commands
207-
# - Plexamp: `b'OK'`
207+
# - Plexamp, Plex for Android: `b'OK'`
208+
# - Plex for Samsung: `b'<?xml version="1.0"?><Response code="200" status="OK">'`
208209
if self.product in (
209210
'Plexamp',
210211
'Plex for Android (TV)',
212+
'Plex for Android (Mobile)',
213+
'Plex for Samsung',
211214
):
212215
return
213216
raise

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()

plexapi/server.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,25 @@ def refreshSync(self):
512512
self.refreshSynclist()
513513
self.refreshContent()
514514

515+
def _allowMediaDeletion(self, toggle=False):
516+
""" Toggle allowMediaDeletion.
517+
Parameters:
518+
toggle (bool): True enables Media Deletion
519+
False or None disable Media Deletion (Default)
520+
"""
521+
if self.allowMediaDeletion and toggle is False:
522+
log.debug('Plex is currently allowed to delete media. Toggling off.')
523+
elif self.allowMediaDeletion and toggle is True:
524+
log.debug('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
525+
raise BadRequest('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
526+
elif self.allowMediaDeletion is None and toggle is True:
527+
log.debug('Plex is currently not allowed to delete media. Toggle set to allow.')
528+
else:
529+
log.debug('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
530+
raise BadRequest('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
531+
value = 1 if toggle is True else 0
532+
return self.query('/:/prefs?allowMediaDeletion=%s' % value, self._session.put)
533+
515534

516535
class Account(PlexObject):
517536
""" Contains the locally cached MyPlex account information. The properties provided don't

plexapi/video.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ def get(self, title=None, episode=None):
581581

582582
def show(self):
583583
""" Return this seasons :func:`~plexapi.video.Show`.. """
584-
return self.fetchItem(self.parentKey)
584+
return self.fetchItem(int(self.parentRatingKey))
585585

586586
def watched(self):
587587
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
@@ -722,7 +722,7 @@ def season(self):
722722

723723
def show(self):
724724
"""" Return this episodes :func:`~plexapi.video.Show`.. """
725-
return self.fetchItem(self.grandparentKey)
725+
return self.fetchItem(int(self.grandparentRatingKey))
726726

727727
def _defaultSyncTitle(self):
728728
""" Returns str, default title for a new syncItem. """

tests/test_server.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,41 @@ def test_server_downloadLogs(tmpdir, plex):
269269
def test_server_downloadDatabases(tmpdir, plex):
270270
plex.downloadDatabases(savepath=str(tmpdir), unpack=True)
271271
assert len(tmpdir.listdir()) > 1
272+
273+
def test_server_allowMediaDeletion(account):
274+
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
275+
# Check server current allowMediaDeletion setting
276+
if plex.allowMediaDeletion:
277+
# If allowed then test disallowed
278+
plex._allowMediaDeletion(False)
279+
time.sleep(1)
280+
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
281+
assert plex.allowMediaDeletion is None
282+
# Test redundant toggle
283+
with pytest.raises(BadRequest):
284+
plex._allowMediaDeletion(False)
285+
286+
plex._allowMediaDeletion(True)
287+
time.sleep(1)
288+
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
289+
assert plex.allowMediaDeletion is True
290+
# Test redundant toggle
291+
with pytest.raises(BadRequest):
292+
plex._allowMediaDeletion(True)
293+
else:
294+
# If disallowed then test allowed
295+
plex._allowMediaDeletion(True)
296+
time.sleep(1)
297+
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
298+
assert plex.allowMediaDeletion is True
299+
# Test redundant toggle
300+
with pytest.raises(BadRequest):
301+
plex._allowMediaDeletion(True)
302+
303+
plex._allowMediaDeletion(False)
304+
time.sleep(1)
305+
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
306+
assert plex.allowMediaDeletion is None
307+
# Test redundant toggle
308+
with pytest.raises(BadRequest):
309+
plex._allowMediaDeletion(False)

0 commit comments

Comments
 (0)