mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-21 19:23:05 +00:00
Merge branch 'master' into conversion_actions
This commit is contained in:
commit
839a9da41d
6 changed files with 210 additions and 5 deletions
|
@ -10,8 +10,6 @@ services:
|
|||
- docker
|
||||
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.6
|
||||
|
||||
env:
|
||||
|
|
|
@ -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
147
plexapi/gdm.py
Normal 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()
|
|
@ -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
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue