mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 19:53:17 +00:00
Add support for SSL
This commit is contained in:
parent
527b0ee323
commit
433e0a18b4
11 changed files with 131 additions and 175 deletions
|
@ -69,7 +69,7 @@ def example_008_get_stream_url(plex):
|
|||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Run PlexAPI examples.')
|
||||
parser.add_argument('-s', '--server', help='Name of the Plex server (requires user/pass).')
|
||||
parser.add_argument('-r', '--resource', help='Name of the Plex resource (requires user/pass).')
|
||||
parser.add_argument('-u', '--username', help='Username for the Plex server.')
|
||||
parser.add_argument('-p', '--password', help='Password for the Plex server.')
|
||||
parser.add_argument('-n', '--name', help='Only run tests containing this string. Leave blank to run all examples.')
|
||||
|
|
|
@ -147,13 +147,19 @@ def test_011_play_media(plex, user=None):
|
|||
# Make sure the client is turned on!
|
||||
episode = plex.library.get(SHOW_TITLE).get(SHOW_EPISODE)
|
||||
client = plex.client(PLEX_CLIENT)
|
||||
client.playMedia(episode); time.sleep(10)
|
||||
client.pause(); time.sleep(3)
|
||||
client.stepForward(); time.sleep(3)
|
||||
client.play(); time.sleep(3)
|
||||
client.stop(); time.sleep(3)
|
||||
client.playMedia(episode)
|
||||
time.sleep(10)
|
||||
client.pause()
|
||||
time.sleep(3)
|
||||
client.stepForward()
|
||||
time.sleep(3)
|
||||
client.play()
|
||||
time.sleep(3)
|
||||
client.stop()
|
||||
time.sleep(3)
|
||||
movie = plex.library.get(MOVIE_TITLE)
|
||||
movie.play(client); time.sleep(10)
|
||||
movie.play(client)
|
||||
time.sleep(10)
|
||||
client.stop()
|
||||
|
||||
|
||||
|
@ -226,7 +232,7 @@ def test_015_list_devices(plex, user=None):
|
|||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Run PlexAPI tests.')
|
||||
parser.add_argument('-s', '--server', help='Name of the Plex server (requires user/pass).')
|
||||
parser.add_argument('-r', '--resource', help='Name of the Plex resource (requires user/pass).')
|
||||
parser.add_argument('-u', '--username', help='Username for the Plex server.')
|
||||
parser.add_argument('-p', '--password', help='Password for the Plex server.')
|
||||
parser.add_argument('-n', '--name', help='Only run tests containing this string. Leave blank to run all tests.')
|
||||
|
|
|
@ -13,9 +13,9 @@ def log(indent, message):
|
|||
|
||||
|
||||
def fetch_server(args):
|
||||
if args.server:
|
||||
if args.resource:
|
||||
user = MyPlexUser.signin(args.username, args.password)
|
||||
return user.getServer(args.server).connect(), user
|
||||
return user.getResource(args.resource).connect(), user
|
||||
return server.PlexServer(), None
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from plexapi.config import PlexConfig
|
|||
from uuid import getnode
|
||||
|
||||
PROJECT = 'PlexAPI'
|
||||
VERSION = '0.9.5'
|
||||
VERSION = '0.9.6'
|
||||
|
||||
# Load User Defined Config
|
||||
CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
|
||||
|
|
|
@ -32,13 +32,6 @@ class Client(object):
|
|||
self.state = data.attrib.get('state')
|
||||
self._sendCommandsTo = SERVER
|
||||
|
||||
|
||||
# machineIdentifier = data.attrib.get('audioCodec') "f9b12e31-9604-485e-a2a1-8cfe7dd8de0d"
|
||||
# platform="Chrome"
|
||||
# product="Plex Web"
|
||||
# state="paused"
|
||||
# title="Plex Web (Chrome)
|
||||
|
||||
def sendCommandsTo(self, value):
|
||||
self._sendCommandsTo = value
|
||||
|
||||
|
@ -66,29 +59,29 @@ class Client(object):
|
|||
return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/'))
|
||||
|
||||
# Navigation Commands
|
||||
def moveUp(self): self.sendCommand('navigation/moveUp')
|
||||
def moveDown(self): self.sendCommand('navigation/moveDown')
|
||||
def moveLeft(self): self.sendCommand('navigation/moveLeft')
|
||||
def moveRight(self): self.sendCommand('navigation/moveRight')
|
||||
def pageUp(self): self.sendCommand('navigation/pageUp')
|
||||
def pageDown(self): self.sendCommand('navigation/pageDown')
|
||||
def nextLetter(self): self.sendCommand('navigation/nextLetter')
|
||||
def previousLetter(self): self.sendCommand('navigation/previousLetter')
|
||||
def select(self): self.sendCommand('navigation/select')
|
||||
def back(self): self.sendCommand('navigation/back')
|
||||
def contextMenu(self): self.sendCommand('navigation/contextMenu')
|
||||
def toggleOSD(self): self.sendCommand('navigation/toggleOSD')
|
||||
def moveUp(self): self.sendCommand('navigation/moveUp') # noqa
|
||||
def moveDown(self): self.sendCommand('navigation/moveDown') # noqa
|
||||
def moveLeft(self): self.sendCommand('navigation/moveLeft') # noqa
|
||||
def moveRight(self): self.sendCommand('navigation/moveRight') # noqa
|
||||
def pageUp(self): self.sendCommand('navigation/pageUp') # noqa
|
||||
def pageDown(self): self.sendCommand('navigation/pageDown') # noqa
|
||||
def nextLetter(self): self.sendCommand('navigation/nextLetter') # noqa
|
||||
def previousLetter(self): self.sendCommand('navigation/previousLetter') # noqa
|
||||
def select(self): self.sendCommand('navigation/select') # noqa
|
||||
def back(self): self.sendCommand('navigation/back') # noqa
|
||||
def contextMenu(self): self.sendCommand('navigation/contextMenu') # noqa
|
||||
def toggleOSD(self): self.sendCommand('navigation/toggleOSD') # noqa
|
||||
|
||||
# Playback Commands
|
||||
def play(self): self.sendCommand('playback/play')
|
||||
def pause(self): self.sendCommand('playback/pause')
|
||||
def stop(self): self.sendCommand('playback/stop')
|
||||
def stepForward(self): self.sendCommand('playback/stepForward')
|
||||
def bigStepForward(self): self.sendCommand('playback/bigStepForward')
|
||||
def stepBack(self): self.sendCommand('playback/stepBack')
|
||||
def bigStepBack(self): self.sendCommand('playback/bigStepBack')
|
||||
def skipNext(self): self.sendCommand('playback/skipNext')
|
||||
def skipPrevious(self): self.sendCommand('playback/skipPrevious')
|
||||
def play(self): self.sendCommand('playback/play') # noqa
|
||||
def pause(self): self.sendCommand('playback/pause') # noqa
|
||||
def stop(self): self.sendCommand('playback/stop') # noqa
|
||||
def stepForward(self): self.sendCommand('playback/stepForward') # noqa
|
||||
def bigStepForward(self): self.sendCommand('playback/bigStepForward') # noqa
|
||||
def stepBack(self): self.sendCommand('playback/stepBack') # noqa
|
||||
def bigStepBack(self): self.sendCommand('playback/bigStepBack') # noqa
|
||||
def skipNext(self): self.sendCommand('playback/skipNext') # noqa
|
||||
def skipPrevious(self): self.sendCommand('playback/skipPrevious') # noqa
|
||||
|
||||
def playMedia(self, video, viewOffset=0):
|
||||
playqueue = self.server.createPlayQueue(video)
|
||||
|
@ -100,36 +93,14 @@ class Client(object):
|
|||
})
|
||||
|
||||
def timeline(self):
|
||||
"""
|
||||
Returns an XML ElementTree object corresponding to the timeline for
|
||||
this client. Holds the information about what media is playing on this
|
||||
client.
|
||||
"""
|
||||
|
||||
url = self.url('timeline/poll')
|
||||
params = {
|
||||
'wait': 1,
|
||||
'commandID': 4,
|
||||
}
|
||||
params = {'wait':1, 'commandID':4}
|
||||
xml_text = requests.get(url, params=params, headers=BASE_HEADERS).text
|
||||
return ElementTree.fromstring(xml_text)
|
||||
|
||||
def isPlayingMedia(self):
|
||||
"""
|
||||
Returns True if any of the media types for this client have the status
|
||||
of "playing", False otherwise. Also returns True if media is paused.
|
||||
"""
|
||||
|
||||
timeline = self.timeline()
|
||||
for media_type in timeline:
|
||||
if media_type.get('state') == 'playing':
|
||||
return True
|
||||
return False
|
||||
|
||||
# def rewind(self): self.sendCommand('playback/rewind')
|
||||
# def fastForward(self): self.sendCommand('playback/fastForward')
|
||||
# def playFile(self): pass
|
||||
# def screenshot(self): pass
|
||||
# def sendString(self): pass
|
||||
# def sendKey(self): pass
|
||||
# def sendVirtualKey(self): pass
|
||||
|
|
|
@ -159,12 +159,12 @@ class VideoTag(object):
|
|||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
|
||||
|
||||
def related(self, vtype=None):
|
||||
return self.server.library.search(None, **{self.FILTER:self})
|
||||
return self.server.search(None, **{self.FILTER:self})
|
||||
|
||||
|
||||
class Country(VideoTag): TYPE='Country'; FILTER='country'
|
||||
class Director(VideoTag): TYPE = 'Director'; FILTER='director'
|
||||
class Genre(VideoTag): TYPE='Genre'; FILTER='genre'
|
||||
class Producer(VideoTag): TYPE = 'Producer'; FILTER='producer'
|
||||
class Actor(VideoTag): TYPE = 'Role'; FILTER='actor'
|
||||
class Writer(VideoTag): TYPE = 'Writer'; FILTER='writer'
|
||||
class Country(VideoTag): TYPE='Country'; FILTER='country' # noqa
|
||||
class Director(VideoTag): TYPE = 'Director'; FILTER='director' # noqa
|
||||
class Genre(VideoTag): TYPE='Genre'; FILTER='genre' # noqa
|
||||
class Producer(VideoTag): TYPE = 'Producer'; FILTER='producer' # noqa
|
||||
class Actor(VideoTag): TYPE = 'Role'; FILTER='actor' # noqa
|
||||
class Writer(VideoTag): TYPE = 'Writer'; FILTER='writer' # noqa
|
||||
|
|
|
@ -4,7 +4,7 @@ PlexAPI MyPlex
|
|||
import plexapi, requests
|
||||
from plexapi import TIMEOUT, log
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.utils import addrToIP, cast, toDatetime
|
||||
from plexapi.utils import cast, toDatetime
|
||||
from requests.status_codes import _codes as codes
|
||||
from threading import Thread
|
||||
from xml.etree import ElementTree
|
||||
|
@ -28,14 +28,14 @@ class MyPlexUser:
|
|||
self.queueEmail = data.attrib.get('queueEmail')
|
||||
self.queueUid = data.attrib.get('queueUid')
|
||||
|
||||
def servers(self):
|
||||
return MyPlexServer.fetch_servers(self.authenticationToken)
|
||||
def resources(self):
|
||||
return MyPlexResource.fetch_resources(self.authenticationToken)
|
||||
|
||||
def getServer(self, search, port=32400):
|
||||
def getResource(self, search, port=32400):
|
||||
""" Searches server.name, server.sourceTitle and server.host:server.port
|
||||
from the list of available for this PlexUser.
|
||||
"""
|
||||
return _findServer(self.servers(), search, port)
|
||||
return _findResource(self.resources(), search, port)
|
||||
|
||||
@classmethod
|
||||
def signin(cls, username, password):
|
||||
|
@ -71,87 +71,98 @@ class MyPlexAccount:
|
|||
self.subscriptionActive = data.attrib.get('subscriptionActive')
|
||||
self.subscriptionState = data.attrib.get('subscriptionState')
|
||||
|
||||
def servers(self):
|
||||
return MyPlexServer.fetch_servers(self.authToken)
|
||||
def resources(self):
|
||||
return MyPlexResource.fetch_resources(self.authToken)
|
||||
|
||||
def getServer(self, search, port=32400):
|
||||
def getResource(self, search, port=32400):
|
||||
""" Searches server.name, server.sourceTitle and server.host:server.port
|
||||
from the list of available for this PlexAccount.
|
||||
"""
|
||||
return _findServer(self.servers(), search, port)
|
||||
return _findResource(self.resources(), search, port)
|
||||
|
||||
|
||||
class MyPlexServer:
|
||||
SERVERS = 'https://plex.tv/pms/servers.xml?includeLite=1'
|
||||
class MyPlexResource:
|
||||
RESOURCES = 'https://plex.tv/api/resources?includeHttps=1'
|
||||
|
||||
def __init__(self, data):
|
||||
self.accessToken = data.attrib.get('accessToken')
|
||||
self.name = data.attrib.get('name')
|
||||
self.address = data.attrib.get('address')
|
||||
self.port = cast(int, data.attrib.get('port'))
|
||||
self.version = data.attrib.get('version')
|
||||
self.scheme = data.attrib.get('scheme')
|
||||
self.host = data.attrib.get('host')
|
||||
self.localAddresses = data.attrib.get('localAddresses', '').split(',')
|
||||
self.machineIdentifier = data.attrib.get('machineIdentifier')
|
||||
self.accessToken = data.attrib.get('accessToken')
|
||||
self.product = data.attrib.get('product')
|
||||
self.productVersion = data.attrib.get('productVersion')
|
||||
self.platform = data.attrib.get('platform')
|
||||
self.platformVersion = data.attrib.get('platformVersion')
|
||||
self.device = data.attrib.get('device')
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.createdAt = toDatetime(data.attrib.get('createdAt'))
|
||||
self.updatedAt = toDatetime(data.attrib.get('updatedAt'))
|
||||
self.lastSeenAt = toDatetime(data.attrib.get('lastSeenAt'))
|
||||
self.provides = data.attrib.get('provides')
|
||||
self.owned = cast(bool, data.attrib.get('owned'))
|
||||
self.home = cast(bool, data.attrib.get('home'))
|
||||
self.synced = cast(bool, data.attrib.get('synced'))
|
||||
self.sourceTitle = data.attrib.get('sourceTitle', '')
|
||||
self.ownerId = cast(int, data.attrib.get('ownerId'))
|
||||
self.home = data.attrib.get('home')
|
||||
self.presence = cast(bool, data.attrib.get('presence'))
|
||||
self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
|
||||
|
||||
def connect(self):
|
||||
# Try connecting to all known addresses in parellel to save time, but
|
||||
# Only check non-local connections unless we own the resource
|
||||
connections = sorted(self.connections, key=lambda c:c.local, reverse=True)
|
||||
if not self.owned:
|
||||
connections = [c for c in connections if c.local is False]
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
addresses = [self.address]
|
||||
if self.owned:
|
||||
addresses = self.localAddresses + [self.address]
|
||||
threads = [None] * len(addresses)
|
||||
results = [None] * len(addresses)
|
||||
for i in range(len(addresses)):
|
||||
args = (addresses[i], results, i)
|
||||
threads = [None] * len(connections)
|
||||
results = [None] * len(connections)
|
||||
for i in range(len(connections)):
|
||||
args = (connections[i].uri, results, i)
|
||||
threads[i] = Thread(target=self._connect, args=args)
|
||||
threads[i].start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
results = list(filter(None, results))
|
||||
if results:
|
||||
results = [r for r in results if r]
|
||||
if not results:
|
||||
raise NotFound('Unable to connect to resource: %s' % self.name)
|
||||
log.info('Connecting to server: %s', results[0])
|
||||
return results[0]
|
||||
raise NotFound('Unable to connect to server: %s' % self.name)
|
||||
|
||||
def _connect(self, address, results, i):
|
||||
from plexapi.server import PlexServer
|
||||
def _connect(self, uri, results, i):
|
||||
try:
|
||||
results[i] = PlexServer(address, self.port, self.accessToken)
|
||||
from plexapi.server import PlexServer
|
||||
results[i] = PlexServer(uri, self.accessToken)
|
||||
except NotFound:
|
||||
results[i] = None
|
||||
|
||||
@classmethod
|
||||
def fetch_servers(cls, token):
|
||||
def fetch_resources(cls, token):
|
||||
headers = plexapi.BASE_HEADERS
|
||||
headers['X-Plex-Token'] = token
|
||||
log.info('GET %s?X-Plex-Token=%s', cls.SERVERS, token)
|
||||
response = requests.get(cls.SERVERS, headers=headers, timeout=TIMEOUT)
|
||||
log.info('GET %s?X-Plex-Token=%s', cls.RESOURCES, token)
|
||||
response = requests.get(cls.RESOURCES, headers=headers, timeout=TIMEOUT)
|
||||
data = ElementTree.fromstring(response.text.encode('utf8'))
|
||||
return [MyPlexServer(elem) for elem in data]
|
||||
return [MyPlexResource(elem) for elem in data]
|
||||
|
||||
|
||||
def _findServer(servers, search, port=32400):
|
||||
""" Searches server.name, server.sourceTitle and server.host:server.port """
|
||||
class ResourceConnection:
|
||||
|
||||
def __init__(self, data):
|
||||
self.protocol = data.attrib.get('protocol')
|
||||
self.address = data.attrib.get('address')
|
||||
self.port = cast(int, data.attrib.get('port'))
|
||||
self.uri = data.attrib.get('uri')
|
||||
self.local = cast(bool, data.attrib.get('local'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
|
||||
|
||||
|
||||
def _findResource(resources, search, port=32400):
|
||||
""" Searches server.name """
|
||||
search = search.lower()
|
||||
ipaddr = addrToIP(search)
|
||||
log.info('Looking for server: %s (host: %s:%s)', search, ipaddr, port)
|
||||
for server in servers:
|
||||
serverName = server.name.lower() if server.name else 'NA'
|
||||
sourceTitle = server.sourceTitle.lower() if server.sourceTitle else 'NA'
|
||||
if (search in [serverName, sourceTitle]) or (server.host == ipaddr and server.port == port):
|
||||
log.info('Looking for server: %s', search)
|
||||
for server in resources:
|
||||
if search == server.name.lower():
|
||||
log.info('Server found: %s', server)
|
||||
return server
|
||||
log.info('Unable to find server: %s (host: %s:%s)', search, ipaddr, port)
|
||||
raise NotFound('Unable to find server: %s (host: %s:%s)' % (search, ipaddr, port))
|
||||
log.info('Unable to find server: %s', search)
|
||||
raise NotFound('Unable to find server: %s' % search)
|
||||
|
|
|
@ -13,13 +13,13 @@ from plexapi.playqueue import PlayQueue
|
|||
from xml.etree import ElementTree
|
||||
|
||||
TOTAL_QUERIES = 0
|
||||
DEFAULT_BASEURI = 'http://localhost:32400'
|
||||
|
||||
|
||||
class PlexServer(object):
|
||||
|
||||
def __init__(self, address='localhost', port=32400, token=None):
|
||||
self.address = self._cleanAddress(address)
|
||||
self.port = port
|
||||
def __init__(self, baseuri=None, token=None):
|
||||
self.baseuri = baseuri or DEFAULT_BASEURI
|
||||
self.token = token
|
||||
data = self._connect()
|
||||
self.friendlyName = data.attrib.get('friendlyName')
|
||||
|
@ -36,20 +36,14 @@ class PlexServer(object):
|
|||
self.version = data.attrib.get('version')
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.address, self.port)
|
||||
|
||||
def _cleanAddress(self, address):
|
||||
address = address.lower().strip('/')
|
||||
if address.startswith('http://'):
|
||||
address = address[8:]
|
||||
return address
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.baseuri)
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
return self.query('/')
|
||||
except Exception as err:
|
||||
log.error('%s:%s: %s', self.address, self.port, err)
|
||||
raise NotFound('No server found at: %s:%s' % (self.address, self.port))
|
||||
log.error('%s: %s', self.baseuri, err)
|
||||
raise NotFound('No server found at: %s' % self.baseuri)
|
||||
|
||||
@property
|
||||
def library(self):
|
||||
|
@ -103,6 +97,7 @@ class PlexServer(object):
|
|||
return video.list_items(self, '/status/sessions')
|
||||
|
||||
def url(self, path):
|
||||
url = 'http://%s:%s/%s' % (self.address, self.port, path.lstrip('/'))
|
||||
if self.token: url += '?X-Plex-Token=%s' % self.token
|
||||
return url
|
||||
if self.token:
|
||||
delim = '&' if '?' in path else '?'
|
||||
return '%s%s%sX-Plex-Token=%s' % (self.baseuri, path, delim, self.token)
|
||||
return '%s%s' % (self.baseuri, path)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
PlexAPI Utils
|
||||
"""
|
||||
import socket, urllib
|
||||
import urllib
|
||||
from datetime import datetime
|
||||
|
||||
NA = '__NA__' # Value not available
|
||||
|
@ -43,21 +43,6 @@ class PlexPartialObject(object):
|
|||
self._loadData(data[0])
|
||||
|
||||
|
||||
def addrToIP(addr):
|
||||
# Check it's already a valid IP
|
||||
try:
|
||||
socket.inet_aton(addr)
|
||||
return addr
|
||||
except socket.error:
|
||||
pass
|
||||
# Try getting the IP
|
||||
try:
|
||||
addr = addr.replace('http://', '')
|
||||
return str(socket.gethostbyname(addr))
|
||||
except socket.error:
|
||||
return addr
|
||||
|
||||
|
||||
def cast(func, value):
|
||||
if value not in [None, NA]:
|
||||
if func == bool:
|
||||
|
@ -80,15 +65,3 @@ def toDatetime(value, format=None):
|
|||
if format: value = datetime.strptime(value, format)
|
||||
else: value = datetime.fromtimestamp(int(value))
|
||||
return value
|
||||
|
||||
|
||||
def lazyproperty(func):
|
||||
""" Decorator: Memoize method result. """
|
||||
attr = '_lazy_%s' % func.__name__
|
||||
|
||||
@property
|
||||
def wrapper(self):
|
||||
if not hasattr(self, attr):
|
||||
setattr(self, attr, func(self))
|
||||
return getattr(self, attr)
|
||||
return wrapper
|
||||
|
|
|
@ -77,7 +77,7 @@ class Video(PlexPartialObject):
|
|||
params: Dict of additional parameters to include in URL.
|
||||
"""
|
||||
params = {}
|
||||
params['path'] = 'http://127.0.0.1:32400%s' % self.key
|
||||
params['path'] = self.key
|
||||
params['offset'] = offset
|
||||
params['copyts'] = kwargs.get('copyts', 1)
|
||||
params['mediaIndex'] = kwargs.get('mediaIndex', 0)
|
||||
|
|
Loading…
Reference in a new issue