# -*- coding: utf-8 -*-
import time
from datetime import datetime
from functools import partial
from os import environ
import plexapi
import pytest
import requests
from plexapi.client import PlexClient
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
from .payloads import ACCOUNT_XML
try:
from unittest.mock import patch, MagicMock, mock_open
except ImportError:
from mock import patch, MagicMock, mock_open
SERVER_BASEURL = plexapi.CONFIG.get("auth.server_baseurl")
MYPLEX_USERNAME = plexapi.CONFIG.get("auth.myplex_username")
MYPLEX_PASSWORD = plexapi.CONFIG.get("auth.myplex_password")
CLIENT_BASEURL = plexapi.CONFIG.get("auth.client_baseurl")
CLIENT_TOKEN = plexapi.CONFIG.get("auth.client_token")
MIN_DATETIME = datetime(1999, 1, 1)
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}$"
AUDIOCHANNELS = {2, 6}
AUDIOLAYOUTS = {"5.1", "5.1(side)", "stereo"}
CODECS = {"aac", "ac3", "dca", "h264", "mp3", "mpeg4"}
CONTAINERS = {"avi", "mp4", "mkv"}
CONTENTRATINGS = {"TV-14", "TV-MA", "G", "NR", "Not Rated"}
FRAMERATES = {"24p", "PAL", "NTSC"}
PROFILES = {"advanced simple", "main", "constrained baseline"}
RESOLUTIONS = {"sd", "480", "576", "720", "1080"}
ENTITLEMENTS = {
"ios",
"roku",
"android",
"xbox_one",
"xbox_360",
"windows",
"windows_phone",
}
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)
def pytest_addoption(parser):
parser.addoption(
"--client", action="store_true", default=False, help="Run client tests."
)
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)
def pytest_runtest_setup(item):
if "client" in item.keywords and not item.config.getvalue("client"):
return pytest.skip("Need --client option to run.")
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"
)
# ---------------------------------
# Fixtures
# ---------------------------------
def get_account():
return MyPlexAccount()
@pytest.fixture(scope="session")
def account():
assert MYPLEX_USERNAME, "Required MYPLEX_USERNAME not specified."
assert MYPLEX_PASSWORD, "Required MYPLEX_PASSWORD not specified."
return get_account()
@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):
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"]
assert (
"iOS" == plexapi.X_PLEX_PLATFORM
), "You have to set env var PLEXAPI_HEADER_PLATFORM=iOS"
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"
return account_plexpass
@pytest.fixture()
def mocked_account(requests_mock):
requests_mock.get("https://plex.tv/users/account", text=ACCOUNT_XML)
return MyPlexAccount(token="faketoken")
@pytest.fixture(scope="session")
def plex(request):
assert SERVER_BASEURL, "Required SERVER_BASEURL not specified."
session = requests.Session()
if request.param == TEST_AUTHENTICATED:
token = get_account().authenticationToken
else:
token = None
return PlexServer(SERVER_BASEURL, token, session=session)
@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
@pytest.fixture
def fresh_plex():
return PlexServer
@pytest.fixture()
def plex2(plex):
return plex()
@pytest.fixture()
def client(request, plex):
return PlexClient(plex, baseurl=CLIENT_BASEURL, token=CLIENT_TOKEN)
@pytest.fixture()
def tvshows(plex):
return plex.library.section("TV Shows")
@pytest.fixture()
def movies(plex):
return plex.library.section("Movies")
@pytest.fixture()
def music(plex):
return plex.library.section("Music")
@pytest.fixture()
def photos(plex):
return plex.library.section("Photos")
@pytest.fixture()
def movie(movies):
return movies.get("Elephants Dream")
@pytest.fixture()
def collection(plex):
try:
return plex.library.section("Movies").collection()[0]
except IndexError:
movie = plex.library.section("Movies").get("Elephants Dream")
movie.addCollection(["marvel"])
n = plex.library.section("Movies").reload()
return n.collection()[0]
@pytest.fixture()
def artist(music):
return music.get("Broke For Free")
@pytest.fixture()
def album(artist):
return artist.album("Layers")
@pytest.fixture()
def track(album):
return album.track("As Colourful as Ever")
@pytest.fixture()
def show(tvshows):
return tvshows.get("Game of Thrones")
@pytest.fixture()
def episode(show):
return show.get("Winter Is Coming")
@pytest.fixture()
def photoalbum(photos):
try:
return photos.get("Cats")
except Exception:
return photos.get("photo_album1")
@pytest.fixture()
def subtitle():
mopen = mock_open()
with patch("__main__.open", mopen):
with open("subtitle.srt", "w") as handler:
handler.write("test")
return handler
@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)
@pytest.fixture()
def monkeydownload(request, monkeypatch):
monkeypatch.setattr(
"plexapi.utils.download", partial(plexapi.utils.download, mocked=True)
)
yield
monkeypatch.undo()
def callable_http_patch():
"""This intented to stop some http requests inside some tests."""
return patch(
"plexapi.server.requests.sessions.Session.send",
return_value=MagicMock(status_code=200, text=""),
)
@pytest.fixture()
def empty_response(mocker):
response = mocker.MagicMock(status_code=200, text="")
return response
@pytest.fixture()
def patched_http_call(mocker):
"""This will stop any http calls inside any test."""
return mocker.patch(
"plexapi.server.requests.sessions.Session.send",
return_value=MagicMock(status_code=200, text=""),
)
# ---------------------------------
# Utility Functions
# ---------------------------------
def is_datetime(value):
if value is None:
return True
return value > MIN_DATETIME
def is_int(value, gte=1):
return int(value) >= gte
def is_float(value, gte=1.0):
return float(value) >= gte
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
def is_part(key):
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, str) and len(value) >= gte
def is_thumb(key):
return is_metadata(key, contains="/thumb/")
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