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__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run PlexAPI examples.') 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('-u', '--username', help='Username for the Plex server.')
parser.add_argument('-p', '--password', help='Password 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.') parser.add_argument('-n', '--name', help='Only run tests containing this string. Leave blank to run all examples.')

View file

@ -1,7 +1,7 @@
""" """
Test Library Functions Test Library Functions
As of Plex version 0.9.11 I noticed that you must be logged in As of Plex version 0.9.11 I noticed that you must be logged in
to browse even the plex server locatewd at localhost. You can to browse even the plex server locatewd at localhost. You can
run this test suite with the following command: run this test suite with the following command:
@ -52,7 +52,7 @@ def test_003_search_show(plex, user=None):
assert result_server, 'Show not found.' assert result_server, 'Show not found.'
assert result_server == result_library == result_shows, 'Show searches not consistent.' assert result_server == result_library == result_shows, 'Show searches not consistent.'
assert not result_movies, 'Movie search returned show title.' assert not result_movies, 'Movie search returned show title.'
def test_004_search_movie(plex, user=None): def test_004_search_movie(plex, user=None):
result_server = plex.search(MOVIE_TITLE) result_server = plex.search(MOVIE_TITLE)
@ -124,7 +124,7 @@ def test_008_mark_movie_watched(plex, user=None):
log(2, 'View count: %s' % movie.viewCount) log(2, 'View count: %s' % movie.viewCount)
movie.markWatched() movie.markWatched()
log(2, 'View count: %s' % movie.viewCount) log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 1, 'View count 0 after watched.' assert movie.viewCount == 1, 'View count 0 after watched.'
movie.markUnwatched() movie.markUnwatched()
log(2, 'View count: %s' % movie.viewCount) log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 0, 'View count 1 after unwatched.' assert movie.viewCount == 0, 'View count 1 after unwatched.'
@ -147,13 +147,19 @@ def test_011_play_media(plex, user=None):
# Make sure the client is turned on! # Make sure the client is turned on!
episode = plex.library.get(SHOW_TITLE).get(SHOW_EPISODE) episode = plex.library.get(SHOW_TITLE).get(SHOW_EPISODE)
client = plex.client(PLEX_CLIENT) client = plex.client(PLEX_CLIENT)
client.playMedia(episode); time.sleep(10) client.playMedia(episode)
client.pause(); time.sleep(3) time.sleep(10)
client.stepForward(); time.sleep(3) client.pause()
client.play(); time.sleep(3) time.sleep(3)
client.stop(); 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 = plex.library.get(MOVIE_TITLE)
movie.play(client); time.sleep(10) movie.play(client)
time.sleep(10)
client.stop() client.stop()
@ -226,7 +232,7 @@ def test_015_list_devices(plex, user=None):
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run PlexAPI tests.') 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('-u', '--username', help='Username for the Plex server.')
parser.add_argument('-p', '--password', help='Password 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.') 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): def fetch_server(args):
if args.server: if args.resource:
user = MyPlexUser.signin(args.username, args.password) 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 return server.PlexServer(), None

View file

@ -7,7 +7,7 @@ from plexapi.config import PlexConfig
from uuid import getnode from uuid import getnode
PROJECT = 'PlexAPI' PROJECT = 'PlexAPI'
VERSION = '0.9.5' VERSION = '0.9.6'
# Load User Defined Config # Load User Defined Config
CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini') 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.state = data.attrib.get('state')
self._sendCommandsTo = SERVER 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): def sendCommandsTo(self, value):
self._sendCommandsTo = value self._sendCommandsTo = value
@ -66,29 +59,29 @@ class Client(object):
return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/')) return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/'))
# Navigation Commands # Navigation Commands
def moveUp(self): self.sendCommand('navigation/moveUp') def moveUp(self): self.sendCommand('navigation/moveUp') # noqa
def moveDown(self): self.sendCommand('navigation/moveDown') def moveDown(self): self.sendCommand('navigation/moveDown') # noqa
def moveLeft(self): self.sendCommand('navigation/moveLeft') def moveLeft(self): self.sendCommand('navigation/moveLeft') # noqa
def moveRight(self): self.sendCommand('navigation/moveRight') def moveRight(self): self.sendCommand('navigation/moveRight') # noqa
def pageUp(self): self.sendCommand('navigation/pageUp') def pageUp(self): self.sendCommand('navigation/pageUp') # noqa
def pageDown(self): self.sendCommand('navigation/pageDown') def pageDown(self): self.sendCommand('navigation/pageDown') # noqa
def nextLetter(self): self.sendCommand('navigation/nextLetter') def nextLetter(self): self.sendCommand('navigation/nextLetter') # noqa
def previousLetter(self): self.sendCommand('navigation/previousLetter') def previousLetter(self): self.sendCommand('navigation/previousLetter') # noqa
def select(self): self.sendCommand('navigation/select') def select(self): self.sendCommand('navigation/select') # noqa
def back(self): self.sendCommand('navigation/back') def back(self): self.sendCommand('navigation/back') # noqa
def contextMenu(self): self.sendCommand('navigation/contextMenu') def contextMenu(self): self.sendCommand('navigation/contextMenu') # noqa
def toggleOSD(self): self.sendCommand('navigation/toggleOSD') def toggleOSD(self): self.sendCommand('navigation/toggleOSD') # noqa
# Playback Commands # Playback Commands
def play(self): self.sendCommand('playback/play') def play(self): self.sendCommand('playback/play') # noqa
def pause(self): self.sendCommand('playback/pause') def pause(self): self.sendCommand('playback/pause') # noqa
def stop(self): self.sendCommand('playback/stop') def stop(self): self.sendCommand('playback/stop') # noqa
def stepForward(self): self.sendCommand('playback/stepForward') def stepForward(self): self.sendCommand('playback/stepForward') # noqa
def bigStepForward(self): self.sendCommand('playback/bigStepForward') def bigStepForward(self): self.sendCommand('playback/bigStepForward') # noqa
def stepBack(self): self.sendCommand('playback/stepBack') def stepBack(self): self.sendCommand('playback/stepBack') # noqa
def bigStepBack(self): self.sendCommand('playback/bigStepBack') def bigStepBack(self): self.sendCommand('playback/bigStepBack') # noqa
def skipNext(self): self.sendCommand('playback/skipNext') def skipNext(self): self.sendCommand('playback/skipNext') # noqa
def skipPrevious(self): self.sendCommand('playback/skipPrevious') def skipPrevious(self): self.sendCommand('playback/skipPrevious') # noqa
def playMedia(self, video, viewOffset=0): def playMedia(self, video, viewOffset=0):
playqueue = self.server.createPlayQueue(video) playqueue = self.server.createPlayQueue(video)
@ -100,36 +93,14 @@ class Client(object):
}) })
def timeline(self): 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') url = self.url('timeline/poll')
params = { params = {'wait':1, 'commandID':4}
'wait': 1,
'commandID': 4,
}
xml_text = requests.get(url, params=params, headers=BASE_HEADERS).text xml_text = requests.get(url, params=params, headers=BASE_HEADERS).text
return ElementTree.fromstring(xml_text) return ElementTree.fromstring(xml_text)
def isPlayingMedia(self): 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() timeline = self.timeline()
for media_type in timeline: for media_type in timeline:
if media_type.get('state') == 'playing': if media_type.get('state') == 'playing':
return True return True
return False 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

@ -6,7 +6,7 @@ from plexapi.exceptions import NotFound
class Library(object): class Library(object):
def __init__(self, server, data): def __init__(self, server, data):
self.server = server self.server = server
self.identifier = data.attrib.get('identifier') self.identifier = data.attrib.get('identifier')
@ -122,7 +122,7 @@ class LibrarySection(object):
def contentRating(self, input=None): def contentRating(self, input=None):
return self._secondary_list('contentRating', input) return self._secondary_list('contentRating', input)
def firstCharacter(self, input=None): def firstCharacter(self, input=None):
return self._secondary_list('firstCharacter', input) return self._secondary_list('firstCharacter', input)
@ -169,7 +169,7 @@ class MovieSection(LibrarySection):
def country(self, input=None): def country(self, input=None):
return self._secondary_list('country', input) return self._secondary_list('country', input)
def decade(self, input=None): def decade(self, input=None):
return self._secondary_list('decade', input) return self._secondary_list('decade', input)
@ -188,7 +188,7 @@ class MovieSection(LibrarySection):
class ShowSection(LibrarySection): class ShowSection(LibrarySection):
TYPE = 'show' TYPE = 'show'
def recentlyViewedShows(self): def recentlyViewedShows(self):
return self._primary_list('recentlyViewedShows') return self._primary_list('recentlyViewedShows')

View file

@ -6,7 +6,7 @@ from plexapi.utils import cast
class Media(object): class Media(object):
TYPE = 'Media' TYPE = 'Media'
def __init__(self, server, data, initpath, video): def __init__(self, server, data, initpath, video):
self.server = server self.server = server
self.initpath = initpath self.initpath = initpath
@ -159,12 +159,12 @@ class VideoTag(object):
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag) return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
def related(self, vtype=None): 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 Country(VideoTag): TYPE='Country'; FILTER='country' # noqa
class Director(VideoTag): TYPE = 'Director'; FILTER='director' class Director(VideoTag): TYPE = 'Director'; FILTER='director' # noqa
class Genre(VideoTag): TYPE='Genre'; FILTER='genre' class Genre(VideoTag): TYPE='Genre'; FILTER='genre' # noqa
class Producer(VideoTag): TYPE = 'Producer'; FILTER='producer' class Producer(VideoTag): TYPE = 'Producer'; FILTER='producer' # noqa
class Actor(VideoTag): TYPE = 'Role'; FILTER='actor' class Actor(VideoTag): TYPE = 'Role'; FILTER='actor' # noqa
class Writer(VideoTag): TYPE = 'Writer'; FILTER='writer' class Writer(VideoTag): TYPE = 'Writer'; FILTER='writer' # noqa

View file

@ -4,7 +4,7 @@ PlexAPI MyPlex
import plexapi, requests import plexapi, requests
from plexapi import TIMEOUT, log from plexapi import TIMEOUT, log
from plexapi.exceptions import BadRequest, NotFound, Unauthorized 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 requests.status_codes import _codes as codes
from threading import Thread from threading import Thread
from xml.etree import ElementTree from xml.etree import ElementTree
@ -28,14 +28,14 @@ class MyPlexUser:
self.queueEmail = data.attrib.get('queueEmail') self.queueEmail = data.attrib.get('queueEmail')
self.queueUid = data.attrib.get('queueUid') self.queueUid = data.attrib.get('queueUid')
def servers(self): def resources(self):
return MyPlexServer.fetch_servers(self.authenticationToken) 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 """ Searches server.name, server.sourceTitle and server.host:server.port
from the list of available for this PlexUser. from the list of available for this PlexUser.
""" """
return _findServer(self.servers(), search, port) return _findResource(self.resources(), search, port)
@classmethod @classmethod
def signin(cls, username, password): def signin(cls, username, password):
@ -71,87 +71,98 @@ class MyPlexAccount:
self.subscriptionActive = data.attrib.get('subscriptionActive') self.subscriptionActive = data.attrib.get('subscriptionActive')
self.subscriptionState = data.attrib.get('subscriptionState') self.subscriptionState = data.attrib.get('subscriptionState')
def servers(self): def resources(self):
return MyPlexServer.fetch_servers(self.authToken) 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 """ Searches server.name, server.sourceTitle and server.host:server.port
from the list of available for this PlexAccount. from the list of available for this PlexAccount.
""" """
return _findServer(self.servers(), search, port) return _findResource(self.resources(), search, port)
class MyPlexServer: class MyPlexResource:
SERVERS = 'https://plex.tv/pms/servers.xml?includeLite=1' RESOURCES = 'https://plex.tv/api/resources?includeHttps=1'
def __init__(self, data): def __init__(self, data):
self.accessToken = data.attrib.get('accessToken')
self.name = data.attrib.get('name') self.name = data.attrib.get('name')
self.address = data.attrib.get('address') self.accessToken = data.attrib.get('accessToken')
self.port = cast(int, data.attrib.get('port')) self.product = data.attrib.get('product')
self.version = data.attrib.get('version') self.productVersion = data.attrib.get('productVersion')
self.scheme = data.attrib.get('scheme') self.platform = data.attrib.get('platform')
self.host = data.attrib.get('host') self.platformVersion = data.attrib.get('platformVersion')
self.localAddresses = data.attrib.get('localAddresses', '').split(',') self.device = data.attrib.get('device')
self.machineIdentifier = data.attrib.get('machineIdentifier') self.clientIdentifier = data.attrib.get('clientIdentifier')
self.createdAt = toDatetime(data.attrib.get('createdAt')) 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.owned = cast(bool, data.attrib.get('owned'))
self.home = cast(bool, data.attrib.get('home'))
self.synced = cast(bool, data.attrib.get('synced')) self.synced = cast(bool, data.attrib.get('synced'))
self.sourceTitle = data.attrib.get('sourceTitle', '') self.presence = cast(bool, data.attrib.get('presence'))
self.ownerId = cast(int, data.attrib.get('ownerId')) self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
self.home = data.attrib.get('home')
def __repr__(self): def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8')) return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
def connect(self): 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. # only return the first server (in order) that provides a response.
addresses = [self.address] threads = [None] * len(connections)
if self.owned: results = [None] * len(connections)
addresses = self.localAddresses + [self.address] for i in range(len(connections)):
threads = [None] * len(addresses) args = (connections[i].uri, results, i)
results = [None] * len(addresses)
for i in range(len(addresses)):
args = (addresses[i], results, i)
threads[i] = Thread(target=self._connect, args=args) threads[i] = Thread(target=self._connect, args=args)
threads[i].start() threads[i].start()
for thread in threads: for thread in threads:
thread.join() thread.join()
results = list(filter(None, results)) results = [r for r in results if r]
if results: if not results:
log.info('Connecting to server: %s', results[0]) raise NotFound('Unable to connect to resource: %s' % self.name)
return results[0] log.info('Connecting to server: %s', results[0])
raise NotFound('Unable to connect to server: %s' % self.name) return results[0]
def _connect(self, address, results, i): def _connect(self, uri, results, i):
from plexapi.server import PlexServer
try: try:
results[i] = PlexServer(address, self.port, self.accessToken) from plexapi.server import PlexServer
results[i] = PlexServer(uri, self.accessToken)
except NotFound: except NotFound:
results[i] = None results[i] = None
@classmethod @classmethod
def fetch_servers(cls, token): def fetch_resources(cls, token):
headers = plexapi.BASE_HEADERS headers = plexapi.BASE_HEADERS
headers['X-Plex-Token'] = token headers['X-Plex-Token'] = token
log.info('GET %s?X-Plex-Token=%s', cls.SERVERS, token) log.info('GET %s?X-Plex-Token=%s', cls.RESOURCES, token)
response = requests.get(cls.SERVERS, headers=headers, timeout=TIMEOUT) response = requests.get(cls.RESOURCES, headers=headers, timeout=TIMEOUT)
data = ElementTree.fromstring(response.text.encode('utf8')) 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): class ResourceConnection:
""" Searches server.name, server.sourceTitle and server.host:server.port """
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() search = search.lower()
ipaddr = addrToIP(search) log.info('Looking for server: %s', search)
log.info('Looking for server: %s (host: %s:%s)', search, ipaddr, port) for server in resources:
for server in servers: if search == server.name.lower():
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('Server found: %s', server) log.info('Server found: %s', server)
return server return server
log.info('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 (host: %s:%s)' % (search, ipaddr, port)) raise NotFound('Unable to find server: %s' % search)

View file

@ -13,13 +13,13 @@ from plexapi.playqueue import PlayQueue
from xml.etree import ElementTree from xml.etree import ElementTree
TOTAL_QUERIES = 0 TOTAL_QUERIES = 0
DEFAULT_BASEURI = 'http://localhost:32400'
class PlexServer(object): class PlexServer(object):
def __init__(self, address='localhost', port=32400, token=None): def __init__(self, baseuri=None, token=None):
self.address = self._cleanAddress(address) self.baseuri = baseuri or DEFAULT_BASEURI
self.port = port
self.token = token self.token = token
data = self._connect() data = self._connect()
self.friendlyName = data.attrib.get('friendlyName') self.friendlyName = data.attrib.get('friendlyName')
@ -36,20 +36,14 @@ class PlexServer(object):
self.version = data.attrib.get('version') self.version = data.attrib.get('version')
def __repr__(self): def __repr__(self):
return '<%s:%s:%s>' % (self.__class__.__name__, self.address, self.port) return '<%s:%s>' % (self.__class__.__name__, self.baseuri)
def _cleanAddress(self, address):
address = address.lower().strip('/')
if address.startswith('http://'):
address = address[8:]
return address
def _connect(self): def _connect(self):
try: try:
return self.query('/') return self.query('/')
except Exception as err: except Exception as err:
log.error('%s:%s: %s', self.address, self.port, err) log.error('%s: %s', self.baseuri, err)
raise NotFound('No server found at: %s:%s' % (self.address, self.port)) raise NotFound('No server found at: %s' % self.baseuri)
@property @property
def library(self): def library(self):
@ -103,6 +97,7 @@ class PlexServer(object):
return video.list_items(self, '/status/sessions') return video.list_items(self, '/status/sessions')
def url(self, path): def url(self, path):
url = 'http://%s:%s/%s' % (self.address, self.port, path.lstrip('/')) if self.token:
if self.token: url += '?X-Plex-Token=%s' % self.token delim = '&' if '?' in path else '?'
return url 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 PlexAPI Utils
""" """
import socket, urllib import urllib
from datetime import datetime from datetime import datetime
NA = '__NA__' # Value not available NA = '__NA__' # Value not available
@ -43,21 +43,6 @@ class PlexPartialObject(object):
self._loadData(data[0]) 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): def cast(func, value):
if value not in [None, NA]: if value not in [None, NA]:
if func == bool: if func == bool:
@ -80,15 +65,3 @@ def toDatetime(value, format=None):
if format: value = datetime.strptime(value, format) if format: value = datetime.strptime(value, format)
else: value = datetime.fromtimestamp(int(value)) else: value = datetime.fromtimestamp(int(value))
return 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: Dict of additional parameters to include in URL.
""" """
params = {} params = {}
params['path'] = 'http://127.0.0.1:32400%s' % self.key params['path'] = self.key
params['offset'] = offset params['offset'] = offset
params['copyts'] = kwargs.get('copyts', 1) params['copyts'] = kwargs.get('copyts', 1)
params['mediaIndex'] = kwargs.get('mediaIndex', 0) params['mediaIndex'] = kwargs.get('mediaIndex', 0)