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