diff --git a/.travis.yml b/.travis.yml index 81d31131..0dd425ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,6 @@ services: - docker python: - - 2.7 - - 3.4 - 3.6 env: diff --git a/plexapi/client.py b/plexapi/client.py index 36cf8d0e..b41177f3 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -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''` if self.product in ( 'Plexamp', 'Plex for Android (TV)', + 'Plex for Android (Mobile)', + 'Plex for Samsung', ): return raise diff --git a/plexapi/gdm.py b/plexapi/gdm.py new file mode 100644 index 00000000..fa40bf14 --- /dev/null +++ b/plexapi/gdm.py @@ -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() diff --git a/plexapi/server.py b/plexapi/server.py index bd80baa1..b42a53d4 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -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 diff --git a/plexapi/video.py b/plexapi/video.py index 41fa384a..b5b8a039 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -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. """ diff --git a/tests/test_server.py b/tests/test_server.py index eb4f96a5..eb9ec721 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -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)