Add support for SSL

This commit is contained in:
Michael Shepanski 2015-06-08 12:41:47 -04:00
parent 527b0ee323
commit 433e0a18b4
11 changed files with 131 additions and 175 deletions

View file

@ -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.')

View file

@ -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.')

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

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