diff --git a/docs/configuration.rst b/docs/configuration.rst
index 80c1f451..054192d0 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -1,4 +1,169 @@
Configuration
=============
-dasfasd
\ No newline at end of file
+.. |br| raw:: html
+
+
+
+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__`. 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).
diff --git a/plexapi/__init__.py b/plexapi/__init__.py
index b50c7418..c9a14a41 100644
--- a/plexapi/__init__.py
+++ b/plexapi/__init__.py
@@ -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)
diff --git a/plexapi/client.py b/plexapi/client.py
index 6a239197..433cc7f4 100644
--- a/plexapi/client.py
+++ b/plexapi/client.py
@@ -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
diff --git a/plexapi/config.py b/plexapi/config.py
index 754f1e84..3b1ccfd2 100644
--- a/plexapi/config.py
+++ b/plexapi/config.py
@@ -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 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
diff --git a/plexapi/myplex.py b/plexapi/myplex.py
index c6e566d2..721d2071 100644
--- a/plexapi/myplex.py
+++ b/plexapi/myplex.py
@@ -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
diff --git a/plexapi/server.py b/plexapi/server.py
index ae828215..cf9a22d0 100644
--- a/plexapi/server.py
+++ b/plexapi/server.py
@@ -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
diff --git a/tests-old/runtests.py b/tests-old/runtests.py
index eea38805..589cdb6d 100755
--- a/tests-old/runtests.py
+++ b/tests-old/runtests.py
@@ -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_'):
diff --git a/tests/conftest.py b/tests/conftest.py
index f2c827b6..e3e60179 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/test_docs.py b/tests/test_docs.py
index 695e34c7..d9cb7907 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -4,8 +4,9 @@ from os.path import abspath, dirname, join
def test_build_documentation():
+ # TODO: assert no WARNING messages in sphinx output
docroot = join(dirname(dirname(abspath(__file__))), 'docs')
- cmd = shlex.split('/usr/bin/make html')
+ cmd = shlex.split('/usr/bin/make html --warn-undefined-variables')
proc = subprocess.Popen(cmd, cwd=docroot)
status = proc.wait()
assert status == 0
diff --git a/tests/test_library.py b/tests/test_library.py
index 0d541fb0..def8b31d 100644
--- a/tests/test_library.py
+++ b/tests/test_library.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import pytest
+from datetime import datetime
from plexapi.exceptions import NotFound
@@ -25,8 +26,8 @@ def test_library_sectionByID_with_attrs(pms):
assert m.agent == 'com.plexapp.agents.imdb'
assert m.allowSync is False
assert m.art == '/:/resources/movie-fanart.jpg'
- assert m.composite == '/library/sections/1/composite/1484690696'
- assert str(m.createdAt.date()) == '2017-01-17'
+ assert '/library/sections/1/composite/' in m.composite
+ 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'
+ assert m.updatedAt > datetime(2017, 1, 16)
assert m.uuid == '2b72d593-3881-43f4-a8b8-db541bd3535a'
diff --git a/tests/test_server.py b/tests/test_server.py
index de834030..177e6df0 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -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 ==
- 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')
diff --git a/tests/test_video.py b/tests/test_video.py
index db0ba5bb..72b21608 100644
--- a/tests/test_video.py
+++ b/tests/test_video.py
@@ -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©ts=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©ts=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©ts=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©ts=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):