2017-02-02 04:47:22 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2019-10-03 00:58:00 +00:00
|
|
|
import plexapi
|
|
|
|
import pytest
|
|
|
|
import requests
|
2018-09-14 18:03:23 +00:00
|
|
|
import time
|
2017-10-28 20:58:47 +00:00
|
|
|
from datetime import datetime
|
|
|
|
from functools import partial
|
2018-09-14 18:03:23 +00:00
|
|
|
from os import environ
|
|
|
|
from plexapi.myplex import MyPlexAccount
|
|
|
|
|
2017-10-28 20:58:47 +00:00
|
|
|
try:
|
2019-09-24 18:35:29 +00:00
|
|
|
from unittest.mock import patch, MagicMock, mock_open
|
2017-10-28 20:58:47 +00:00
|
|
|
except ImportError:
|
2019-09-24 18:35:29 +00:00
|
|
|
from mock import patch, MagicMock, mock_open
|
2017-10-28 20:58:47 +00:00
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
from plexapi import compat
|
2019-10-03 00:58:00 +00:00
|
|
|
from plexapi.compat import patch, MagicMock
|
2017-04-26 03:09:37 +00:00
|
|
|
from plexapi.client import PlexClient
|
2017-04-15 00:47:59 +00:00
|
|
|
from plexapi.server import PlexServer
|
2017-10-28 20:58:47 +00:00
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
SERVER_BASEURL = plexapi.CONFIG.get('auth.server_baseurl')
|
2018-09-14 18:03:23 +00:00
|
|
|
MYPLEX_USERNAME = plexapi.CONFIG.get('auth.myplex_username')
|
|
|
|
MYPLEX_PASSWORD = plexapi.CONFIG.get('auth.myplex_password')
|
2017-04-26 03:23:57 +00:00
|
|
|
CLIENT_BASEURL = plexapi.CONFIG.get('auth.client_baseurl')
|
|
|
|
CLIENT_TOKEN = plexapi.CONFIG.get('auth.client_token')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-05-13 03:25:57 +00:00
|
|
|
MIN_DATETIME = datetime(1999, 1, 1)
|
2017-04-23 05:18:53 +00:00
|
|
|
REGEX_EMAIL = r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)'
|
|
|
|
REGEX_IPADDR = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
|
|
|
|
|
2017-04-29 05:47:21 +00:00
|
|
|
AUDIOCHANNELS = {2, 6}
|
|
|
|
AUDIOLAYOUTS = {'5.1', '5.1(side)', 'stereo'}
|
|
|
|
CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'}
|
|
|
|
CONTAINERS = {'avi', 'mp4', 'mkv'}
|
2018-12-04 20:45:14 +00:00
|
|
|
CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR', 'Not Rated'}
|
2018-09-14 18:03:23 +00:00
|
|
|
FRAMERATES = {'24p', 'PAL', 'NTSC'}
|
2017-05-02 03:26:27 +00:00
|
|
|
PROFILES = {'advanced simple', 'main', 'constrained baseline'}
|
|
|
|
RESOLUTIONS = {'sd', '480', '576', '720', '1080'}
|
2019-02-07 00:09:04 +00:00
|
|
|
ENTITLEMENTS = {'ios', 'roku', 'android', 'xbox_one', 'xbox_360', 'windows', 'windows_phone'}
|
2017-04-23 05:18:53 +00:00
|
|
|
|
2018-09-15 08:42:42 +00:00
|
|
|
TEST_AUTHENTICATED = 'authenticated'
|
|
|
|
TEST_ANONYMOUSLY = 'anonymously'
|
|
|
|
ANON_PARAM = pytest.param(TEST_ANONYMOUSLY, marks=pytest.mark.anonymous)
|
|
|
|
AUTH_PARAM = pytest.param(TEST_AUTHENTICATED, marks=pytest.mark.authenticated)
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
def pytest_addoption(parser):
|
2017-04-26 03:23:57 +00:00
|
|
|
parser.addoption('--client', action='store_true', default=False, help='Run client tests.')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2018-09-15 08:42:42 +00:00
|
|
|
def pytest_generate_tests(metafunc):
|
|
|
|
if 'plex' in metafunc.fixturenames:
|
|
|
|
if 'account' in metafunc.fixturenames or TEST_AUTHENTICATED in metafunc.definition.keywords:
|
|
|
|
metafunc.parametrize('plex', [AUTH_PARAM], indirect=True)
|
|
|
|
else:
|
|
|
|
metafunc.parametrize('plex', [ANON_PARAM, AUTH_PARAM], indirect=True)
|
|
|
|
elif 'account' in metafunc.fixturenames:
|
|
|
|
metafunc.parametrize('account', [AUTH_PARAM], indirect=True)
|
|
|
|
|
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
def pytest_runtest_setup(item):
|
2017-04-26 03:09:37 +00:00
|
|
|
if 'client' in item.keywords and not item.config.getvalue('client'):
|
|
|
|
return pytest.skip('Need --client option to run.')
|
2018-09-15 08:42:42 +00:00
|
|
|
if TEST_AUTHENTICATED in item.keywords and not (MYPLEX_USERNAME and MYPLEX_PASSWORD):
|
|
|
|
return pytest.skip('You have to specify MYPLEX_USERNAME and MYPLEX_PASSWORD to run authenticated tests')
|
|
|
|
if TEST_ANONYMOUSLY in item.keywords and MYPLEX_USERNAME and MYPLEX_PASSWORD:
|
|
|
|
return pytest.skip('Anonymous tests should be ran on unclaimed server, without providing MYPLEX_USERNAME and '
|
|
|
|
'MYPLEX_PASSWORD')
|
2017-01-31 00:02:22 +00:00
|
|
|
|
|
|
|
|
2017-06-06 01:40:52 +00:00
|
|
|
# ---------------------------------
|
|
|
|
# Fixtures
|
|
|
|
# ---------------------------------
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2018-09-15 08:42:42 +00:00
|
|
|
|
|
|
|
def get_account():
|
|
|
|
return MyPlexAccount()
|
|
|
|
|
|
|
|
|
2018-09-14 18:03:23 +00:00
|
|
|
@pytest.fixture(scope='session')
|
2017-04-15 00:47:59 +00:00
|
|
|
def account():
|
2018-09-14 18:03:23 +00:00
|
|
|
assert MYPLEX_USERNAME, 'Required MYPLEX_USERNAME not specified.'
|
|
|
|
assert MYPLEX_PASSWORD, 'Required MYPLEX_PASSWORD not specified.'
|
2018-09-15 08:42:42 +00:00
|
|
|
return get_account()
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2018-09-14 18:03:23 +00:00
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
def account_once(account):
|
|
|
|
if environ.get('TEST_ACCOUNT_ONCE') != '1' and environ.get('CI') == 'true':
|
|
|
|
pytest.skip('Do not forget to test this by providing TEST_ACCOUNT_ONCE=1')
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
def account_plexpass(account):
|
|
|
|
if not account.subscriptionActive:
|
|
|
|
pytest.skip('PlexPass subscription is not active, unable to test sync-stuff, be careful!')
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
|
|
def account_synctarget(account_plexpass):
|
2018-09-08 15:25:16 +00:00
|
|
|
assert 'sync-target' in plexapi.X_PLEX_PROVIDES, 'You have to set env var ' \
|
|
|
|
'PLEXAPI_HEADER_PROVIDES=sync-target,controller'
|
|
|
|
assert 'sync-target' in plexapi.BASE_HEADERS['X-Plex-Provides']
|
2018-09-14 18:03:23 +00:00
|
|
|
assert 'iOS' == plexapi.X_PLEX_PLATFORM, 'You have to set env var PLEXAPI_HEADER_PLATFORM=iOS'
|
2018-09-08 15:25:16 +00:00
|
|
|
assert '11.4.1' == plexapi.X_PLEX_PLATFORM_VERSION, 'You have to set env var PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1'
|
|
|
|
assert 'iPhone' == plexapi.X_PLEX_DEVICE, 'You have to set env var PLEXAPI_HEADER_DEVICE=iPhone'
|
2018-09-14 18:03:23 +00:00
|
|
|
return account_plexpass
|
2018-09-08 15:25:16 +00:00
|
|
|
|
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
@pytest.fixture(scope='session')
|
2018-09-15 08:42:42 +00:00
|
|
|
def plex(request):
|
2017-04-15 00:47:59 +00:00
|
|
|
assert SERVER_BASEURL, 'Required SERVER_BASEURL not specified.'
|
|
|
|
session = requests.Session()
|
2018-09-15 08:42:42 +00:00
|
|
|
if request.param == TEST_AUTHENTICATED:
|
|
|
|
token = get_account().authenticationToken
|
|
|
|
else:
|
|
|
|
token = None
|
|
|
|
return PlexServer(SERVER_BASEURL, token, session=session)
|
2017-01-09 14:21:54 +00:00
|
|
|
|
2017-02-27 05:36:20 +00:00
|
|
|
|
2018-09-08 15:25:16 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def device(account):
|
|
|
|
d = None
|
|
|
|
for device in account.devices():
|
|
|
|
if device.clientIdentifier == plexapi.X_PLEX_IDENTIFIER:
|
|
|
|
d = device
|
|
|
|
break
|
|
|
|
|
|
|
|
assert d
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def clear_sync_device(device, account_synctarget, plex):
|
|
|
|
sync_items = account_synctarget.syncItems(clientId=device.clientIdentifier)
|
|
|
|
for item in sync_items.items:
|
|
|
|
item.delete()
|
|
|
|
plex.refreshSync()
|
|
|
|
return device
|
|
|
|
|
|
|
|
|
2017-07-17 14:11:03 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def fresh_plex():
|
|
|
|
return PlexServer
|
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def plex2():
|
|
|
|
return plex()
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2017-04-26 03:09:37 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def client(request):
|
2017-05-13 03:25:57 +00:00
|
|
|
return PlexClient(plex(), baseurl=CLIENT_BASEURL, token=CLIENT_TOKEN)
|
2017-04-26 03:09:37 +00:00
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def tvshows(plex):
|
|
|
|
return plex.library.section('TV Shows')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def movies(plex):
|
|
|
|
return plex.library.section('Movies')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def music(plex):
|
|
|
|
return plex.library.section('Music')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def photos(plex):
|
|
|
|
return plex.library.section('Photos')
|
2017-01-31 00:02:22 +00:00
|
|
|
|
2017-02-27 05:36:20 +00:00
|
|
|
|
2017-01-31 00:02:22 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def movie(movies):
|
2017-05-02 03:26:27 +00:00
|
|
|
return movies.get('Elephants Dream')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2019-07-31 18:53:06 +00:00
|
|
|
@pytest.fixture()
|
2019-09-21 20:54:50 +00:00
|
|
|
def collection(plex, movie):
|
|
|
|
|
|
|
|
try:
|
|
|
|
plex.library.section('Movies').collection()[0]
|
|
|
|
except IndexError:
|
|
|
|
movie.addCollection(["marvel"])
|
|
|
|
|
2019-09-21 21:17:35 +00:00
|
|
|
sec = plex.library.section('Movies').reload()
|
|
|
|
|
|
|
|
return sec.collection()[0]
|
2019-07-31 18:53:06 +00:00
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def artist(music):
|
|
|
|
return music.get('Infinite State')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def album(artist):
|
|
|
|
return artist.album('Unmastered Impulses')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def track(album):
|
|
|
|
return album.track('Holy Moment')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def show(tvshows):
|
2017-05-02 03:26:27 +00:00
|
|
|
return tvshows.get('Game of Thrones')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def episode(show):
|
2017-05-02 03:26:27 +00:00
|
|
|
return show.get('Winter Is Coming')
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2017-04-15 00:47:59 +00:00
|
|
|
def photoalbum(photos):
|
2017-04-23 05:54:53 +00:00
|
|
|
try:
|
|
|
|
return photos.get('Cats')
|
2019-10-03 00:58:00 +00:00
|
|
|
except Exception:
|
2017-04-23 05:54:53 +00:00
|
|
|
return photos.get('photo_album1')
|
2019-09-24 03:13:17 +00:00
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def subtitle():
|
|
|
|
mopen = mock_open()
|
|
|
|
with patch('__main__.open', mopen):
|
|
|
|
with open('subtitle.srt', 'w') as handler:
|
|
|
|
handler.write('test')
|
|
|
|
return handler
|
2017-01-09 14:21:54 +00:00
|
|
|
|
|
|
|
|
2018-09-14 18:03:23 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def shared_username(account):
|
|
|
|
username = environ.get('SHARED_USERNAME', 'PKKid')
|
|
|
|
for user in account.users():
|
|
|
|
if user.title.lower() == username.lower():
|
|
|
|
return username
|
|
|
|
elif (user.username and user.email and user.id and username.lower() in
|
|
|
|
(user.username.lower(), user.email.lower(), str(user.id))):
|
|
|
|
return username
|
|
|
|
pytest.skip('Shared user %s wasn`t found in your MyPlex account' % username)
|
|
|
|
|
|
|
|
|
2017-01-09 14:21:54 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
def monkeydownload(request, monkeypatch):
|
|
|
|
monkeypatch.setattr('plexapi.utils.download', partial(plexapi.utils.download, mocked=True))
|
|
|
|
yield
|
|
|
|
monkeypatch.undo()
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
2017-10-28 20:58:47 +00:00
|
|
|
def callable_http_patch():
|
2017-11-01 22:18:18 +00:00
|
|
|
"""This intented to stop some http requests inside some tests."""
|
2017-10-28 20:58:47 +00:00
|
|
|
return patch('plexapi.server.requests.sessions.Session.send',
|
|
|
|
return_value=MagicMock(status_code=200,
|
|
|
|
text='<xml><child></child></xml>'))
|
|
|
|
|
2017-10-26 21:55:59 +00:00
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
def empty_response(mocker):
|
|
|
|
response = mocker.MagicMock(status_code=200, text='<xml><child></child></xml>')
|
|
|
|
return response
|
|
|
|
|
2017-10-26 23:09:17 +00:00
|
|
|
|
2017-10-26 21:55:59 +00:00
|
|
|
@pytest.fixture()
|
2017-10-28 22:07:06 +00:00
|
|
|
def patched_http_call(mocker):
|
2017-11-01 22:18:18 +00:00
|
|
|
"""This will stop any http calls inside any test."""
|
2017-10-28 22:07:06 +00:00
|
|
|
return mocker.patch('plexapi.server.requests.sessions.Session.send',
|
|
|
|
return_value=MagicMock(status_code=200,
|
|
|
|
text='<xml><child></child></xml>')
|
|
|
|
)
|
2017-10-26 21:55:59 +00:00
|
|
|
|
|
|
|
|
2017-06-06 01:40:52 +00:00
|
|
|
# ---------------------------------
|
|
|
|
# Utility Functions
|
|
|
|
# ---------------------------------
|
2017-04-15 00:47:59 +00:00
|
|
|
def is_datetime(value):
|
|
|
|
return value > MIN_DATETIME
|
|
|
|
|
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
def is_int(value, gte=1):
|
|
|
|
return int(value) >= gte
|
|
|
|
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
def is_float(value, gte=1.0):
|
|
|
|
return float(value) >= gte
|
2017-04-15 00:47:59 +00:00
|
|
|
|
2017-04-23 05:18:53 +00:00
|
|
|
|
|
|
|
def is_metadata(key, prefix='/library/metadata/', contains='', suffix=''):
|
|
|
|
try:
|
|
|
|
assert key.startswith(prefix)
|
|
|
|
assert contains in key
|
|
|
|
assert key.endswith(suffix)
|
|
|
|
return True
|
|
|
|
except AssertionError:
|
|
|
|
return False
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_part(key):
|
2017-04-23 05:18:53 +00:00
|
|
|
return is_metadata(key, prefix='/library/parts/')
|
|
|
|
|
|
|
|
|
|
|
|
def is_section(key):
|
|
|
|
return is_metadata(key, prefix='/library/sections/')
|
|
|
|
|
|
|
|
|
|
|
|
def is_string(value, gte=1):
|
|
|
|
return isinstance(value, compat.string_type) and len(value) >= gte
|
2017-04-15 00:47:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_thumb(key):
|
2017-04-23 05:18:53 +00:00
|
|
|
return is_metadata(key, contains='/thumb/')
|
2018-09-14 18:03:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def wait_until(condition_function, delay=0.25, timeout=1, *args, **kwargs):
|
|
|
|
start = time.time()
|
|
|
|
ready = condition_function(*args, **kwargs)
|
|
|
|
retries = 1
|
|
|
|
while not ready and time.time() - start < timeout:
|
|
|
|
retries += 1
|
|
|
|
time.sleep(delay)
|
|
|
|
ready = condition_function(*args, **kwargs)
|
|
|
|
|
|
|
|
assert ready, 'Wait timeout after %d retries, %.2f seconds' % (retries, time.time() - start)
|
|
|
|
|
|
|
|
return ready
|