Merge branch 'master' into conversion_actions

This commit is contained in:
blacktwin 2020-04-07 09:21:32 -04:00 committed by GitHub
commit 839a9da41d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 5 deletions

View file

@ -10,8 +10,6 @@ services:
- docker
python:
- 2.7
- 3.4
- 3.6
env:

View file

@ -204,10 +204,13 @@ class PlexClient(PlexObject):
return query(key, headers=headers)
except ElementTree.ParseError:
# Workaround for players which don't return valid XML on successful commands
# - Plexamp: `b'OK'`
# - Plexamp, Plex for Android: `b'OK'`
# - Plex for Samsung: `b'<?xml version="1.0"?><Response code="200" status="OK">'`
if self.product in (
'Plexamp',
'Plex for Android (TV)',
'Plex for Android (Mobile)',
'Plex for Samsung',
):
return
raise

147
plexapi/gdm.py Normal file
View file

@ -0,0 +1,147 @@
"""
Support for discovery using GDM (Good Day Mate), multicast protocol by Plex.
# Licensed Apache 2.0
# From https://github.com/home-assistant/netdisco/netdisco/gdm.py
Inspired by
hippojay's plexGDM:
https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py
"""
import socket
import struct
class GDM:
"""Base class to discover GDM services."""
def __init__(self):
self.entries = []
self.last_scan = None
def scan(self, scan_for_clients=False):
"""Scan the network."""
self.update(scan_for_clients)
def all(self):
"""Return all found entries.
Will scan for entries if not scanned recently.
"""
self.scan()
return list(self.entries)
def find_by_content_type(self, value):
"""Return a list of entries that match the content_type."""
self.scan()
return [entry for entry in self.entries
if value in entry['data']['Content_Type']]
def find_by_data(self, values):
"""Return a list of entries that match the search parameters."""
self.scan()
return [entry for entry in self.entries
if all(item in entry['data'].items()
for item in values.items())]
def update(self, scan_for_clients):
"""Scan for new GDM services.
Examples of the dict list assigned to self.entries by this function:
Server:
[{'data': {
'Content-Type': 'plex/media-server',
'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct',
'Name': 'myfirstplexserver',
'Port': '32400',
'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7',
'Updated-At': '1585769946',
'Version': '1.18.8.2527-740d4c206',
},
'from': ('10.10.10.100', 32414)}]
Clients:
[{'data': {'Content-Type': 'plex/media-player',
'Device-Class': 'stb',
'Name': 'plexamp',
'Port': '36000',
'Product': 'Plexamp',
'Protocol': 'plex',
'Protocol-Capabilities': 'timeline,playback,playqueues,playqueues-creation',
'Protocol-Version': '1',
'Resource-Identifier': 'b6e57a3f-e0f8-494f-8884-f4b58501467e',
'Version': '1.1.0',
},
'from': ('10.10.10.101', 32412)}]
"""
gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii')
gdm_timeout = 1
self.entries = []
known_responses = []
# setup socket for discovery -> multicast message
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(gdm_timeout)
# Set the time-to-live for messages for local network
sock.setsockopt(socket.IPPROTO_IP,
socket.IP_MULTICAST_TTL,
struct.pack("B", gdm_timeout))
if scan_for_clients:
# setup socket for broadcast to Plex clients
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
gdm_ip = '255.255.255.255'
gdm_port = 32412
else:
# setup socket for multicast to Plex server(s)
gdm_ip = '239.0.0.250'
gdm_port = 32414
try:
# Send data to the multicast group
sock.sendto(gdm_msg, (gdm_ip, gdm_port))
# Look for responses from all recipients
while True:
try:
bdata, host = sock.recvfrom(1024)
data = bdata.decode('utf-8')
if '200 OK' in data.splitlines()[0]:
ddata = {k: v.strip() for (k, v) in (
line.split(':') for line in
data.splitlines() if ':' in line)}
identifier = ddata.get('Resource-Identifier')
if identifier and identifier in known_responses:
continue
known_responses.append(identifier)
self.entries.append({'data': ddata,
'from': host})
except socket.timeout:
break
finally:
sock.close()
def main():
"""Test GDM discovery."""
from pprint import pprint
gdm = GDM()
pprint("Scanning GDM for servers...")
gdm.scan()
pprint(gdm.entries)
pprint("Scanning GDM for clients...")
gdm.scan(scan_for_clients=True)
pprint(gdm.entries)
if __name__ == "__main__":
main()

View file

@ -512,6 +512,25 @@ class PlexServer(PlexObject):
self.refreshSynclist()
self.refreshContent()
def _allowMediaDeletion(self, toggle=False):
""" Toggle allowMediaDeletion.
Parameters:
toggle (bool): True enables Media Deletion
False or None disable Media Deletion (Default)
"""
if self.allowMediaDeletion and toggle is False:
log.debug('Plex is currently allowed to delete media. Toggling off.')
elif self.allowMediaDeletion and toggle is True:
log.debug('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
raise BadRequest('Plex is currently allowed to delete media. Toggle set to allow, exiting.')
elif self.allowMediaDeletion is None and toggle is True:
log.debug('Plex is currently not allowed to delete media. Toggle set to allow.')
else:
log.debug('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
raise BadRequest('Plex is currently not allowed to delete media. Toggle set to not allow, exiting.')
value = 1 if toggle is True else 0
return self.query('/:/prefs?allowMediaDeletion=%s' % value, self._session.put)
class Account(PlexObject):
""" Contains the locally cached MyPlex account information. The properties provided don't

View file

@ -581,7 +581,7 @@ class Season(Video):
def show(self):
""" Return this seasons :func:`~plexapi.video.Show`.. """
return self.fetchItem(self.parentKey)
return self.fetchItem(int(self.parentRatingKey))
def watched(self):
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
@ -722,7 +722,7 @@ class Episode(Playable, Video):
def show(self):
"""" Return this episodes :func:`~plexapi.video.Show`.. """
return self.fetchItem(self.grandparentKey)
return self.fetchItem(int(self.grandparentRatingKey))
def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """

View file

@ -269,3 +269,41 @@ def test_server_downloadLogs(tmpdir, plex):
def test_server_downloadDatabases(tmpdir, plex):
plex.downloadDatabases(savepath=str(tmpdir), unpack=True)
assert len(tmpdir.listdir()) > 1
def test_server_allowMediaDeletion(account):
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
# Check server current allowMediaDeletion setting
if plex.allowMediaDeletion:
# If allowed then test disallowed
plex._allowMediaDeletion(False)
time.sleep(1)
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
assert plex.allowMediaDeletion is None
# Test redundant toggle
with pytest.raises(BadRequest):
plex._allowMediaDeletion(False)
plex._allowMediaDeletion(True)
time.sleep(1)
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
assert plex.allowMediaDeletion is True
# Test redundant toggle
with pytest.raises(BadRequest):
plex._allowMediaDeletion(True)
else:
# If disallowed then test allowed
plex._allowMediaDeletion(True)
time.sleep(1)
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
assert plex.allowMediaDeletion is True
# Test redundant toggle
with pytest.raises(BadRequest):
plex._allowMediaDeletion(True)
plex._allowMediaDeletion(False)
time.sleep(1)
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
assert plex.allowMediaDeletion is None
# Test redundant toggle
with pytest.raises(BadRequest):
plex._allowMediaDeletion(False)