Merge branch '3.0.0' into pkkid/opt

This commit is contained in:
Michael Shepanski 2017-02-19 22:36:53 -05:00 committed by GitHub
commit 601471c304
12 changed files with 223 additions and 63 deletions

View file

@ -1,4 +1,169 @@
Configuration
=============
dasfasd
.. |br| raw:: html
<br />
The default configuration file path is :samp:`~/.config/plexapi/config.ini`
which can be overridden by setting the environment variable
:samp:`PLEXAPI_CONFIG_PATH` with the filepath you desire. All configuration
variables in this file are optional. An example config.ini file may look like
the following with all possible value specified.
.. code-block:: ini
# ~/.config/plexapi/config.ini
[plexapi]
container_size = 50
timeout = 30
[auth]
myplex_username = johndoe
myplex_password = kodi-stinks
server_baseurl = http://127.0.0.1:32400
server_token = XBHSMSJSDJ763JSm
[headers]
identifier = 0x485b314307f3L
platorm = Linux
platform_version = 4.4.0-62-generic
product = PlexAPI
version = 3.0.0
[log]
backup_count = 3
format = %(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s
level = INFO
path = ~/.config/plexapi/plexapi.log
rotate_bytes = 512000
secrets = false
Environment Variables
---------------------
All configuration values can be set or overridden via environment variables. The
environment variable names are in all upper case and follow the format
:samp:`PLEXAPI_<SECTION>_<NAME>`. For example, if you wish to set the log path via an
environment variable, you may specify: `PLEXAPI_LOG_PATH="/tmp/plexapi.log"`
Section [plexapi] Options
-------------------------
**container_size**
Default max results to return in on single search page. Looping through
result pages is done internall by the API. Therfore, tuning this setting
will not affect usage of plexapi. However, it help improve performance for
large media collections (default: 50).
**timeout**
Timeout in seconds to use when making requests to the Plex Media Server
or Plex Client resources (default: 30).
Section [auth] Options
----------------------
**myplex_username**
Default MyPlex (plex.tv) username to use when creating a new
:any:`MyPlexAccount` object. Specifying this along with :samp:`auth.myplex_password`
allow you to more easily connect to your account and remove the need to hard
code the username and password in any supplemental scripts you may write. To
create an account object using these values you may simply specify
:samp:`account = MyPlexAccount()` without any arguments (default: None).
**myplex_password**
Default MyPlex (plex.tv) password to use when creating a new :any:`MyPlexAccount`
object. See `auth.myplex_password` for more information and example usage
(default: None).
WARNING: When specifying a password or token in the configuration file, be
sure lock it down (persmission 600) to ensure no other users on the system
can read them. Or better yet, only specify sensitive values as a local
environment variables.
**server_baseurl**
Default baseurl to use when creating a new :any:`PlexServer` object.
Specifying this along with :samp:`auth.server_token` allow you to more easily
connect to a server and remove the need to hard code the baseurl and token
in any supplemental scripts you may write. To create a server object using
these values you may simply specify :samp:`plex = PlexServer()` without any
arguments (default: None).
**server_token**
Default token to use when creating a new :any:`PlexServer` object.
See `auth.server_baseurl` for more information and example usage (default:
None).
WARNING: When specifying a password or token in the configuration file, be
sure lock it down (persmission 600) to ensure no other users on the system
can read them. Or better yet, only specify sensitive values as a local
environment variables.
Section [header] Options
------------------------
**device**
Header value used for X_PLEX_DEVICE to all Plex server and Plex client
requests. Example devices include: iPhone, FireTV, Linux (default:
`result of platform.uname()[0]`).
**device_name**
Header value used for X_PLEX_DEVICE_NAME to all Plex server and Plex client
requests. Example device names include: hostname or phone name
(default: `result of platform.uname()[1]`).
**identifier**
Header value used for X_PLEX_IDENTIFIER to all Plex server and Plex client
requests. This is generally a UUID, serial number, or other number unique
id for the device (default: `result of hex(uuid.getnode())`).
**platorm**
Header value used for X_PLEX_PLATFORM to all Plex server and Plex client
requests. Example platforms include: iOS, MacOSX, Android, LG (default:
`result of platform.uname()[0]`).
**platform_version**
Header value used for X_PLEX_PLATFORM_VERSION to all Plex server
and Plex client requests. This is genrally the server or client operating
system version: 4.3.1, 10.6.7, 3.2 (default: `result of platform.uname()[2]`).
**product**
Header value used for X_PLEX_PRODUCT to all Plex server and Plex client
requests. This is the Plex application name: Laika, Plex Media Server,
Media Link (default: PlexAPI).
**provides**
Header value used for X_PLEX_PROVIDES to all Plex server and Plex client
requests This is generally one or more of: controller, player, server
(default: PlexAPI).
**version**
Header value used for X_PLEX_VERSION to all Plex server and Plex client
requests. This is the Plex application version (default: plexapi.VERSION).
Section [log] Options
---------------------
**backup_count**
Number backup log files to keep before rotating out old logs (default 3).
**format**
Log file format to use for plexapi logging. (default:
'%(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s').
Ref: https://docs.python.org/2/library/logging.html#logrecord-attributes
**level**
Log level to use when for plexapi logging (default: INFO).
**path**
Filepath to save plexapi logs to. If not specified, plexapi will not save
logs to an output file (default: None).
**rotate_bytes**
Max size of the log file before rotating logs to a backup file
(default: 512000 equals 0.5MB).
**secrets**
By default Plex will hide all passwords and token values when logging. Set
this to 'true' to enable logging these secrets. This should only be done on
a private server and only enabled when needed (default: false).

View file

@ -8,39 +8,39 @@ from uuid import getnode
# Load User Defined Config
DEFAULT_CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
CONFIG_PATH = os.environ.get('PLEX_CONFIG_PATH', DEFAULT_CONFIG_PATH)
CONFIG_PATH = os.environ.get('PLEXAPI_CONFIG_PATH', DEFAULT_CONFIG_PATH)
CONFIG = PlexConfig(CONFIG_PATH)
# Core Settings
PROJECT = 'PlexAPI' # name provided to plex server
VERSION = '2.9.0' # version of this api
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int) # request timeout
X_PLEX_CONTAINER_SIZE = 50 # max results to return in a single search page
# PlexAPI Settings
PROJECT = 'PlexAPI'
VERSION = '2.9.0'
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 50, int)
# Plex Header Configuation
X_PLEX_PROVIDES = 'controller' # one or more of [player, controller, server]
X_PLEX_PLATFORM = CONFIG.get('headers.platorm', uname()[0]) # Platform name, eg iOS, MacOSX, Android, LG, etc
X_PLEX_PLATFORM_VERSION = CONFIG.get('headers.platform_version', uname()[2]) # Operating system version, eg 4.3.1, 10.6.7, 3.2
X_PLEX_PRODUCT = CONFIG.get('headers.product', PROJECT) # Plex application name, eg Laika, Plex Media Server, Media Link
X_PLEX_VERSION = CONFIG.get('headers.version', VERSION) # Plex application version number
X_PLEX_DEVICE = CONFIG.get('headers.platform', X_PLEX_PLATFORM) # Device make, eg iPhone, FiteTV, Linux, etc.
X_PLEX_DEVICE_NAME = uname()[1] # Device name, hostname or phone name, etc.
X_PLEX_IDENTIFIER = CONFIG.get('headers.identifier', str(hex(getnode()))) # UUID, serial number, or other number unique per device
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
X_PLEX_PLATFORM = CONFIG.get('header.platorm', uname()[0])
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)
X_PLEX_DEVICE = CONFIG.get('header.device', X_PLEX_PLATFORM)
X_PLEX_DEVICE_NAME = CONFIG.get('header.device_name', uname()[1])
X_PLEX_IDENTIFIER = CONFIG.get('header.identifier', str(hex(getnode())))
BASE_HEADERS = reset_base_headers()
# Logging Configuration
log = logging.getLogger('plexapi')
logfile = CONFIG.get('logging.path')
logformat = CONFIG.get('logging.format', '%(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s')
loglevel = CONFIG.get('logging.level', 'INFO').upper()
logfile = CONFIG.get('log.path')
logformat = CONFIG.get('log.format', '%(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s')
loglevel = CONFIG.get('log.level', 'INFO').upper()
loghandler = logging.NullHandler()
if logfile:
logbackups = CONFIG.get('logging.backup_count', 3, int)
logbytes = CONFIG.get('logging.rotate_bytes', 512000, int)
logbackups = CONFIG.get('log.backup_count', 3, int)
logbytes = CONFIG.get('log.rotate_bytes', 512000, int)
loghandler = RotatingFileHandler(os.path.expanduser(logfile), 'a', logbytes, logbackups)
loghandler.setFormatter(logging.Formatter(logformat))
log.addHandler(loghandler)
log.setLevel(loglevel)
logfilter = SecretsFilter()
if CONFIG.get('logging.show_secrets') != 'true':
if CONFIG.get('log.secrets', '').lower() != 'true':
log.addFilter(logfilter)

View file

@ -135,7 +135,7 @@ class PlexClient(PlexObject):
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s' % (response.status_code, codename))
data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data else None

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from collections import defaultdict
from plexapi.compat import ConfigParser
@ -17,14 +18,6 @@ class PlexConfig(ConfigParser):
self.read(path)
self.data = self._asDict()
def __getattr__(self, attr):
if attr not in ('get', '_asDict', 'data'):
for section in self._sections:
for name, value in self._sections[section].items():
if name == attr:
return value
raise Exception('Config attr not found: %s' % attr)
def get(self, key, default=None, cast=None):
""" Returns the specified configuration value or <default> if not found.
@ -34,8 +27,13 @@ class PlexConfig(ConfigParser):
cast (func): Cast the value to the specified type before returning.
"""
try:
section, name = key.split('.')
value = self.data.get(section.lower(), {}).get(name.lower(), default)
# First: check environment variable is set
envkey = 'PLEXAPI_%s' % key.upper().replace('.', '_')
value = os.environ.get(envkey)
if value is None:
# Second: check the config file has attr
section, name = key.lower().split('.')
value = self.data.get(section, {}).get(name, default)
return cast(value) if cast else value
except:
return default

View file

@ -52,8 +52,8 @@ class MyPlexAccount(PlexObject):
def __init__(self, username=None, password=None, session=None):
self._session = session or requests.Session()
self._token = None
username = username or CONFIG.get('authentication.myplex_username')
password = password or CONFIG.get('authentication.myplex_password')
username = username or CONFIG.get('auth.myplex_username')
password = password or CONFIG.get('auth.myplex_password')
data = self.query(self.SIGNIN, method=self._session.post, auth=(username, password))
super(MyPlexAccount, self).__init__(self, data, self.SIGNIN)
@ -116,7 +116,7 @@ class MyPlexAccount(PlexObject):
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s' % (response.status_code, codename))
text = response.text.encode('utf8')
return ElementTree.fromstring(text) if text else None

View file

@ -90,8 +90,8 @@ class PlexServer(PlexObject):
key = '/'
def __init__(self, baseurl='http://localhost:32400', token=None, session=None):
self._baseurl = baseurl or CONFIG.get('authentication.server_baseurl')
self._token = logfilter.add_secret(token or CONFIG.get('authentication.server_token'))
self._baseurl = baseurl or CONFIG.get('auth.server_baseurl')
self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token'))
self._session = session or requests.Session()
self._library = None # cached library
super(PlexServer, self).__init__(self, self.query(self.key), self.key)
@ -239,7 +239,7 @@ class PlexServer(PlexObject):
if response.status_code not in (200, 201):
codename = codes.get(response.status_code)[0]
log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
raise BadRequest('(%s) %s' % (response.status_code, codename))
data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data else None

View file

@ -15,9 +15,9 @@ from utils import log, itertests
def runtests(args):
# Get username and password from environment
username = args.username or CONFIG.get('authentication.myplex_username')
password = args.password or CONFIG.get('authentication.myplex_password')
resource = args.resource or CONFIG.get('authentication.server_resource')
username = args.username or CONFIG.get('auth.myplex_username')
password = args.password or CONFIG.get('auth.myplex_password')
resource = args.resource
# Register known tests
for loader, name, ispkg in pkgutil.iter_modules([dirname(abspath(__file__))]):
if name.startswith('test_'):

View file

@ -4,10 +4,10 @@ import pytest, requests
from betamax_serializers import pretty_json
from functools import partial
token = os.environ.get('PLEX_TOKEN')
test_token = os.environ.get('PLEX_TEST_TOKEN')
test_username = os.environ.get('PLEX_TEST_USERNAME')
test_password = os.environ.get('PLEX_TEST_PASSWORD')
test_baseurl = plexapi.CONFIG.get('auth.server_baseurl')
test_token = plexapi.CONFIG.get('auth.server_token')
test_username = plexapi.CONFIG.get('auth.myplex_username')
test_password = plexapi.CONFIG.get('auth.myplex_password')
@pytest.fixture(scope='session')
@ -22,10 +22,9 @@ def pms(request):
# recorder = betamax.Betamax(sess, cassette_library_dir=CASSETTE_LIBRARY_DIR)
# recorder.use_cassette('http_responses', serialize_with='prettyjson') # record='new_episodes'
# recorder.start()
url = 'http://138.68.157.5:32400'
assert test_baseurl
assert test_token
assert url
pms = PlexServer(url, test_token, session=sess)
pms = PlexServer(test_baseurl, test_token, session=sess)
#request.addfinalizer(recorder.stop)
return pms
@ -34,10 +33,9 @@ def pms(request):
def freshpms():
from plexapi.server import PlexServer
sess = requests.Session()
url = 'http://138.68.157.5:32400'
assert test_baseurl
assert test_token
assert url
pms = PlexServer(url, test_token, session=sess)
pms = PlexServer(test_baseurl, test_token, session=sess)
return pms

View file

@ -11,10 +11,8 @@ import pytest
@pytest.mark.skipif(os.name == 'nt',
reason='Skipping this test for windows as there there is no make.bat.')
def test_build_documentation():
docroot = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'docs')
cmd = shlex.split('/usr/bin/make html')
docroot = join(dirname(dirname(abspath(__file__))), 'docs')
cmd = shlex.split('/usr/bin/make html --warn-undefined-variables')
proc = subprocess.Popen(cmd, cwd=docroot)
status = proc.wait()
assert status == 0

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import pytest
from datetime import datetime
from plexapi.exceptions import NotFound
@ -26,7 +27,7 @@ def test_library_sectionByID_with_attrs(pms):
assert m.allowSync is False
assert m.art == '/:/resources/movie-fanart.jpg'
assert '/library/sections/1/composite/' in m.composite
#assert str(m.createdAt.date()) == '2017-01-17' # FIXME
assert m.createdAt > datetime(2017, 1, 16)
assert m.filters == '1'
assert m._initpath == '/library/sections'
assert m.key == '1'
@ -38,7 +39,7 @@ def test_library_sectionByID_with_attrs(pms):
assert m.thumb == '/:/resources/movie.png'
assert m.title == 'Movies'
assert m.type == 'movie'
#assert str(m.updatedAt.date()) == '2017-01-17' # fixme
assert m.updatedAt > datetime(2017, 1, 16)
assert m.uuid == '2b72d593-3881-43f4-a8b8-db541bd3535a'

View file

@ -17,7 +17,7 @@ def test_server_attr(pms):
assert pms.platform == 'Linux'
assert pms.platformVersion == '4.4.0-59-generic (#80-Ubuntu SMP Fri Jan 6 17:47:47 UTC 2017)'
#assert pms.session == <requests.sessions.Session object at 0x029A5E10>
assert pms._token == os.environ.get('PLEX_TEST_TOKEN') or CONFIG.get('authentication.server_token')
assert pms._token == CONFIG.get('auth.server_token')
assert pms.transcoderActiveVideoSessions == 0
#assert str(pms.updatedAt.date()) == '2017-01-20'
assert pms.version == '1.3.3.3148-b38628e'
@ -129,8 +129,7 @@ def test_server_Server_session():
self.plexapi_session_test = True
plex = PlexServer('http://138.68.157.5:32400',
os.environ.get('PLEX_TEST_TOKEN'),
session=MySession())
CONFIG.get('auth.server_token'), session=MySession())
assert hasattr(plex._session, 'plexapi_session_test')
pl = plex.playlists()
assert hasattr(pl[0]._server._session, 'plexapi_session_test')

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import os, pytest
import plexapi, pytest
from datetime import datetime
from plexapi.exceptions import BadRequest, NotFound
@ -20,8 +20,9 @@ def test_video_Movie_delete(monkeypatch, pms):
def test_video_Movie_getStreamURL(a_movie):
assert a_movie.getStreamURL() == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&X-Plex-Token={0}".format(os.environ.get('PLEX_TEST_TOKEN'))
assert a_movie.getStreamURL(videoResolution='800x600') == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&videoResolution=800x600&X-Plex-Token={0}".format(os.environ.get('PLEX_TEST_TOKEN'))
server_token = plexapi.CONFIG.get('auth.server_token')
assert a_movie.getStreamURL() == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&X-Plex-Token={0}".format(server_token)
assert a_movie.getStreamURL(videoResolution='800x600') == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&videoResolution=800x600&X-Plex-Token={0}".format(server_token)
def test_video_Movie_isFullObject_and_reload(pms):