From 5a04ce4f71751290691cf983c519c639a677ebc7 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sat, 8 Sep 2018 23:09:22 +0100 Subject: [PATCH 01/54] lets begin --- .travis.yml | 87 +++++++++++++++++++++++++++---------- plexapi/playlist.py | 4 +- requirements_dev.txt | 2 +- tests/conftest.py | 29 +++++++------ tests/test_library.py | 10 ++++- tests/test_server.py | 8 ++-- tests/test_settings.py | 9 ++-- tests/test_video.py | 18 ++++---- tools/plex-bootstraptest.py | 45 ++++++++++--------- tools/plex-teardowntest.py | 25 +++++++++++ 10 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 tools/plex-teardowntest.py diff --git a/.travis.yml b/.travis.yml index 22a3e70e8..c4a2ecd3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,70 @@ -language: -- python +language: python + +stages: + - test + - name: deploy + if: tag IS present + +cache: pip +sudo: required +services: + - docker + python: -- '2.7' -- '3.4' -- '3.6' + - 2.7 + - 3.4 + - 3.6 + +env: + global: + - PLEXAPI_AUTH_SERVER_BASEURL=http://127.0.0.1:32400 + - PLEXAPI_PLEXAPI_TIMEOUT=10 + matrix: + - PLEX_CONTAINER_TAG=latest + before_install: -- pip install --upgrade pip -- pip install --upgrade setuptools -- pip install --upgrade pytest pytest-cov coveralls + - pip install --upgrade pip + - pip install --upgrade setuptools + - pip install --upgrade pytest pytest-cov coveralls install: -- pip install -r requirements_dev.txt + - pip install -r requirements_dev.txt + - PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1 + --without-album --bootstrap-timeout 300 --docker-tag $PLEX_CONTAINER_TAG --accept-eula + script: -- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then py.test tests --tb=native --verbose - --cov-config .coveragerc --cov=plexapi; fi -- flake8 plexapi --exclude=compat.py --max-line-length=120 --ignore=E128,E701,E702,E731,W293 + - py.test tests --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi + - PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1 + PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi + --cov-append + after_success: -- coveralls -matrix: - fast_finish: true -deploy: - provider: pypi - user: mjs7231 - password: - secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c= - on: - tags: true \ No newline at end of file + - coveralls + +after_script: + - PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py + +jobs: + include: + - python: 3.6 + name: "Flake8" + install: + - pip install -r requirements_dev.txt + script: flake8 plexapi --exclude=compat.py --max-line-length=120 --ignore=E128,E701,E702,E731,W293 + after_success: true + after_script: true + env: + - PLEX_CONTAINER_TAG=latest + - stage: deploy + name: "Deploy to PyPi" + python: 3.6 + install: true + script: true + env: + - PLEX_CONTAINER_TAG=latest + deploy: + provider: pypi + user: mjs7231 + password: + secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c= + on: + tags: true diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 9a1dbcf80..8c74147cf 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -168,8 +168,8 @@ def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, clien :mod:`plexapi.sync` module. Used only when playlist contains video. photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the module :mod:`plexapi.sync`. Used only when playlist contains photos. - audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the - module :mod:`plexapi.sync`. Used only when playlist contains audio. + audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values + from the module :mod:`plexapi.sync`. Used only when playlist contains audio. client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`. diff --git a/requirements_dev.txt b/requirements_dev.txt index 8007a1ca0..3fb657cc3 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -15,4 +15,4 @@ sphinx sphinx-rtd-theme sphinxcontrib-napoleon tqdm -websocket-client +websocket-client==0.48.0 diff --git a/tests/conftest.py b/tests/conftest.py index 841ecf0d2..98672d447 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,8 @@ import pytest import requests +from plexapi.myplex import MyPlexAccount + try: from unittest.mock import patch, MagicMock except ImportError: @@ -34,7 +36,8 @@ SERVER_BASEURL = plexapi.CONFIG.get('auth.server_baseurl') -SERVER_TOKEN = plexapi.CONFIG.get('auth.server_token') +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') @@ -47,7 +50,7 @@ CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'} CONTAINERS = {'avi', 'mp4', 'mkv'} CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR'} -FRAMERATES = {'24p', 'PAL'} +FRAMERATES = {'24p', 'PAL', 'NTSC'} PROFILES = {'advanced simple', 'main', 'constrained baseline'} RESOLUTIONS = {'sd', '480', '576', '720', '1080'} @@ -65,31 +68,29 @@ def pytest_runtest_setup(item): # Fixtures # --------------------------------- -@pytest.fixture() +@pytest.fixture(scope='session') def account(): - return plex().myPlexAccount() - # assert MYPLEX_USERNAME, 'Required MYPLEX_USERNAME not specified.' - # assert MYPLEX_PASSWORD, 'Required MYPLEX_PASSWORD not specified.' - # return MyPlexAccount(MYPLEX_USERNAME, MYPLEX_PASSWORD) + assert MYPLEX_USERNAME, 'Required MYPLEX_USERNAME not specified.' + assert MYPLEX_PASSWORD, 'Required MYPLEX_PASSWORD not specified.' + return MyPlexAccount() -@pytest.fixture() -def account_synctarget(): +@pytest.fixture(scope='session') +def account_synctarget(account): 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_PLATORM=iOS' + 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 plex().myPlexAccount() + return account @pytest.fixture(scope='session') -def plex(): +def plex(account): assert SERVER_BASEURL, 'Required SERVER_BASEURL not specified.' - assert SERVER_TOKEN, 'Requred SERVER_TOKEN not specified.' session = requests.Session() - return PlexServer(SERVER_BASEURL, SERVER_TOKEN, session=session) + return PlexServer(SERVER_BASEURL, account.authenticationToken, session=session) @pytest.fixture() diff --git a/tests/test_library.py b/tests/test_library.py index d2bb4b582..e7d8e3e65 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -56,8 +56,10 @@ def test_library_fetchItem(plex, movie): assert item1 == item2 == movie -def test_library_onDeck(plex): +def test_library_onDeck(plex, movie): + movie.updateProgress(5000) assert len(list(plex.library.onDeck())) + movie.markUnwatched() def test_library_recentlyAdded(plex): @@ -140,8 +142,12 @@ def test_librarty_deleteMediaPreviews(movies): movies.deleteMediaPreviews() -def test_library_MovieSection_onDeck(movies, tvshows): +def test_library_MovieSection_onDeck(movie, movies, tvshows, episode): + movie.updateProgress(5000) + episode.markWatched() assert len(movies.onDeck()) + len(tvshows.onDeck()) + episode.markUnwatched() + movie.markUnwatched() def test_library_MovieSection_recentlyAdded(movies): diff --git a/tests/test_server.py b/tests/test_server.py index d28fe162c..b023f0bbb 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -8,7 +8,7 @@ from . import conftest as utils -def test_server_attr(plex): +def test_server_attr(plex, account): assert plex._baseurl == utils.SERVER_BASEURL assert len(plex.friendlyName) >= 1 assert len(plex.machineIdentifier) == 40 @@ -19,7 +19,7 @@ def test_server_attr(plex): assert re.match(utils.REGEX_EMAIL, plex.myPlexUsername) assert plex.platform in ('Linux', 'Windows') assert len(plex.platformVersion) >= 5 - assert plex._token == utils.SERVER_TOKEN + assert plex._token == account.authenticationToken assert utils.is_int(plex.transcoderActiveVideoSessions, gte=0) assert utils.is_datetime(plex.updatedAt) assert len(plex.version) >= 5 @@ -131,14 +131,14 @@ def test_server_Server_query(plex): PlexServer(utils.SERVER_BASEURL, '1234') -def test_server_Server_session(): +def test_server_Server_session(account): # Mock Sesstion class MySession(Session): def __init__(self): super(self.__class__, self).__init__() self.plexapi_session_test = True # Test Code - plex = PlexServer(utils.SERVER_BASEURL, utils.SERVER_TOKEN, session=MySession()) + plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken, session=MySession()) assert hasattr(plex._session, 'plexapi_session_test') diff --git a/tests/test_settings.py b/tests/test_settings.py index 0ed38e179..c6417c87c 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -9,9 +9,10 @@ def test_settings_get(plex): assert plex.settings.get('FriendlyName').value == '' -def test_settings_get(plex): +def test_settings_set(plex): cd = plex.settings.get('collectUsageData') - cd.set(False) - # Save works but since we reload asap the data isnt changed. - # or it might be our caching that does this. ## TODO + old_value = cd.value + cd.set(not old_value) plex.settings.save() + delattr(plex, '_settings') + assert plex.settings.get('collectUsageData').value == (not old_value) diff --git a/tests/test_video.py b/tests/test_video.py index 75e551708..6428f7c06 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -14,7 +14,7 @@ def test_video_Movie_attributeerror(movie): movie.asshat def test_video_ne(movies): - assert len(movies.fetchItems('/library/sections/7/all', title__ne='Sintel')) == 3 + assert len(movies.fetchItems('/library/sections/1/all', title__ne='Sintel')) == 3 def test_video_Movie_delete(movie, patched_http_call): @@ -33,10 +33,10 @@ def test_video_Movie_addCollection(movie): assert labelname not in [tag.tag for tag in movie.collections if tag] -def test_video_Movie_getStreamURL(movie): +def test_video_Movie_getStreamURL(movie, account): key = movie.ratingKey - assert movie.getStreamURL() == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, utils.SERVER_TOKEN) # noqa - assert movie.getStreamURL(videoResolution='800x600') == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, utils.SERVER_TOKEN) # noqa + assert movie.getStreamURL() == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, account.authenticationToken) # noqa + assert movie.getStreamURL(videoResolution='800x600') == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, account.authenticationToken) # noqa def test_video_Movie_isFullObject_and_reload(plex): @@ -118,7 +118,7 @@ def test_video_Movie_attrs(movies): assert float(movie.rating) >= 6.4 #assert movie.ratingImage == 'rottentomatoes://image.rating.ripe' assert movie.ratingKey >= 1 - assert sorted([i.tag for i in movie.roles])[:4] == ['Aladdin Ullah', 'Annette Hanshaw', 'Aseem Chhabra', 'Debargo Sanyal'] # noqa + assert sorted([i.tag for i in movie.roles])[:4] == ['Aladdin Ullah', 'Annette Hanshaw', 'Aseem Chhabra', 'Bhavana Nagulapally'] # noqa assert movie._server._baseurl == utils.SERVER_BASEURL assert movie.sessionKey is None assert movie.studio == 'Nina Paley' @@ -301,7 +301,7 @@ def test_video_Show_attrs(show): assert utils.is_int(show.duration, gte=1600000) assert utils.is_section(show._initpath) # Check reloading the show loads the full list of genres - assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama', 'Fantasy'] + assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama'] show.reload() assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama', 'Fantasy'] # So the initkey should have changed because of the reload @@ -316,8 +316,8 @@ def test_video_Show_attrs(show): assert show.originallyAvailableAt.strftime('%Y-%m-%d') == '2011-04-17' assert show.rating >= 8.0 assert utils.is_int(show.ratingKey) - assert sorted([i.tag for i in show.roles])[:4] == ['Aidan Gillen', 'Alexander Siddig', 'Alfie Allen', 'Art Parkinson'] - assert sorted([i.tag for i in show.actors])[:4] == ['Aidan Gillen', 'Alexander Siddig', 'Alfie Allen', 'Art Parkinson'] + assert sorted([i.tag for i in show.roles])[:4] == ['Aidan Gillen', 'Aimee Richardson', 'Alexander Siddig', 'Alfie Allen'] # noqa + assert sorted([i.tag for i in show.actors])[:4] == ['Aidan Gillen', 'Aimee Richardson', 'Alexander Siddig', 'Alfie Allen'] # noqa assert show._server._baseurl == utils.SERVER_BASEURL assert show.studio == 'HBO' assert utils.is_string(show.summary, gte=100) @@ -503,7 +503,7 @@ def test_video_Episode_attrs(episode): assert utils.is_metadata(part._initpath) assert len(part.key) >= 10 assert part._server._baseurl == utils.SERVER_BASEURL - assert utils.is_int(part.size, gte=30000000) + assert part.size == 18184197 def test_video_Season(show): diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index d45911acc..c38bcc017 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -1,4 +1,5 @@ import argparse +import logging import os import platform from glob import glob @@ -12,6 +13,8 @@ import plexapi from plexapi.compat import which, makedirs from plexapi.exceptions import BadRequest +from plexapi.myplex import MyPlexAccount +from plexapi.server import PlexServer from plexapi.utils import download, SEARCHTYPES DOCKER_CMD = [ @@ -61,14 +64,6 @@ def get_ips(): if i[4][0] not in ('127.0.0.1', '::1') and not i[4][0].startswith('fe80:')])) -# Unfortunately plex ignore hardlinks created on OS X host machine, so we have to copy here -def cp(src, dst): - if platform.system() == 'Darwin': - copyfile(src, dst) - else: - os.link(src, dst) - - if __name__ == '__main__': if which('docker') is None: print('Docker is required to be available') @@ -82,6 +77,7 @@ def cp(src, dst): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--username', help='Your Plex username') parser.add_argument('--password', help='Your Plex password') + parser.add_argument('--token', help='Plex.tv authentication token', default=plexapi.CONFIG.get('auth.server_token')) parser.add_argument('--timezone', help='Timezone to set inside plex', default='UTC') parser.add_argument('--destination', help='Local path where to store all the media', default=os.path.join(os.getcwd(), 'plex')) @@ -103,14 +99,20 @@ def cp(src, dst): action='store_false') parser.add_argument('--without-album', help='Do not create Photo Album', default=True, dest='with_photo_album', action='store_false') + parser.add_argument('--show-token', help='Display access token after bootstrap', default=False, action='store_true') opts = parser.parse_args() print('I`m going to create a plex instance named %s with advertised ip "%s", be prepared!' % (opts.server_name, opts.advertise_ip)) if call(['docker', 'pull', 'plexinc/pms-docker:%s' % opts.docker_tag]) != 0: print('Got an error when executing docker pull!') exit(1) - account = plexapi.utils.getMyPlexAccount(opts) + + if opts.token: + account = MyPlexAccount(token=opts.token) + else: + account = plexapi.utils.getMyPlexAccount(opts) path = os.path.realpath(os.path.expanduser(opts.destination)) + makedirs(os.path.join(path, 'media'), exist_ok=True) arg_bindings = { 'destination': path, 'hostname': opts.server_name, @@ -155,14 +157,14 @@ def get_tvshow_path(name, season, episode): def get_movie_path(name, year): return os.path.join(movies_path, '%s (%d).mp4' % (name, year)) - media_stub_path = os.path.join(opts.destination, 'media', 'video_stub.mp4') + media_stub_path = os.path.join(path, 'media', 'video_stub.mp4') if not os.path.isfile(media_stub_path): download('http://www.mytvtestpatterns.com/mytvtestpatterns/Default/GetFile?p=PhilipsCircleMP4', '', - filename='video_stub.mp4', savepath=os.path.join(opts.destination, 'media'), showstatus=True) + filename='video_stub.mp4', savepath=os.path.join(path, 'media'), showstatus=True) sections = [] if opts.with_movies: - movies_path = os.path.join(opts.destination, 'media', 'Movies') + movies_path = os.path.join(path, 'media', 'Movies') makedirs(movies_path, exist_ok=True) required_movies = { @@ -175,14 +177,14 @@ def get_movie_path(name, year): for name, year in required_movies.items(): expected_media_count += 1 if not os.path.isfile(get_movie_path(name, year)): - cp(media_stub_path, get_movie_path(name, year)) + copyfile(media_stub_path, get_movie_path(name, year)) print('Finished with movies...') sections.append(dict(name='Movies', type='movie', location='/data/Movies', agent='com.plexapp.agents.imdb', scanner='Plex Movie Scanner')) if opts.with_shows: - tvshows_path = os.path.join(opts.destination, 'media', 'TV-Shows') + tvshows_path = os.path.join(path, 'media', 'TV-Shows') makedirs(os.path.join(tvshows_path, 'Game of Thrones'), exist_ok=True) makedirs(os.path.join(tvshows_path, 'The 100'), exist_ok=True) @@ -203,14 +205,14 @@ def get_movie_path(name, year): expected_media_count += 1 episode_path = get_tvshow_path(show_name, season_id, episode_id) if not os.path.isfile(episode_path): - cp(get_movie_path('Sintel', 2010), episode_path) + copyfile(get_movie_path('Sintel', 2010), episode_path) print('Finished with TV Shows...') sections.append(dict(name='TV Shows', type='show', location='/data/TV-Shows', agent='com.plexapp.agents.thetvdb', scanner='Plex Series Scanner')) if opts.with_music: - music_path = os.path.join(opts.destination, 'media', 'Music') + music_path = os.path.join(path, 'media', 'Music') makedirs(music_path, exist_ok=True) artist_dst = os.path.join(music_path, 'Infinite State') @@ -234,7 +236,7 @@ def get_movie_path(name, year): if not os.path.isdir(dest_path): zip_path = os.path.join(artist_dst, 'Layers.zip') if not os.path.isfile(zip_path): - download('https://freemusicarchive.org/music/zip/2803d3e9c9510c17d180b821b43b248e9db83487', '', + download('https://archive.org/compress/Layers-11520/formats=VBR%20MP3&file=/Layers-11520.zip', '', filename='Layers.zip', savepath=artist_dst, showstatus=True) makedirs(dest_path, exist_ok=True) import zipfile @@ -248,7 +250,7 @@ def get_movie_path(name, year): scanner='Plex Music Scanner')) if opts.with_photos: - photos_path = os.path.join(opts.destination, 'media', 'Photos') + photos_path = os.path.join(path, 'media', 'Photos') makedirs(photos_path, exist_ok=True) has_photos = len(glob(os.path.join(photos_path, '*.jpg'))) @@ -279,8 +281,8 @@ def alert_callback(data): SEARCHTYPES['photo']): processed_media += 1 - if processed_media == expected_media_count: - finished = True + if processed_media == expected_media_count: + finished = True notifier = server.startAlertListener(alert_callback) @@ -324,6 +326,7 @@ def alert_callback(data): sleep(3) print('Base URL is %s' % server.url('', False)) - print('Auth token is %s' % account.authenticationToken) + if opts.show_token: + print('Auth token is %s' % account.authenticationToken) print('Server %s is ready to use!' % opts.server_name) diff --git a/tools/plex-teardowntest.py b/tools/plex-teardowntest.py new file mode 100644 index 000000000..e63c40610 --- /dev/null +++ b/tools/plex-teardowntest.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Listen to plex alerts and print them to the console. +Because we're using print as a function, example only works in Python3. +""" +from plexapi.myplex import MyPlexAccount +from plexapi.server import PlexServer +from plexapi import X_PLEX_IDENTIFIER + + +if __name__ == '__main__': + myplex = MyPlexAccount() + plex = PlexServer(token=myplex.authenticationToken) + for device in plex.myPlexAccount().devices(): + if device.clientIdentifier == plex.machineIdentifier: + print('Removing device "%s", with id "%s"' % (device.name, device. clientIdentifier)) + device.delete() + + # If we suddenly remove the client first we wouldn't be able to authenticate to delete the server + for device in plex.myPlexAccount().devices(): + if device.clientIdentifier == X_PLEX_IDENTIFIER: + print('Removing device "%s", with id "%s"' % (device.name, device. clientIdentifier)) + device.delete() + break From 0c5574f506070b17604c993c3742295d57232054 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 11:19:49 +0100 Subject: [PATCH 02/54] skip plexpass tests if there is not plexpass on account --- tests/conftest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 98672d447..5e3e22429 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,14 +76,21 @@ def account(): @pytest.fixture(scope='session') -def account_synctarget(account): +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 + return account_plexpass @pytest.fixture(scope='session') From 470aca4d3d04e5f7b0565a6c1645d17a034cee0b Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 12:27:06 +0100 Subject: [PATCH 03/54] test new myplex attrubutes --- tests/test_myplex.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_myplex.py b/tests/test_myplex.py index d4419ac9a..09fe44663 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -152,3 +152,12 @@ def test_myplex_updateFriend(account, plex, mocker): allowCameraUpload=True, allowChannels=False, filterMovies=vid_filter, filterTelevision=vid_filter, filterMusic={'label': ['foo']}) + +def test_myplex_plexpass_attributes(account_plexpass): + assert account_plexpass.subscriptionActive + assert account_plexpass.subscriptionStatus == 'Active' + assert account_plexpass.subscriptionPlan + assert 'sync' in account_plexpass.subscriptionFeatures + assert 'premium_music_metadata' in account_plexpass.subscriptionFeatures + assert 'plexpass' in account_plexpass.roles + assert 'all' in account_plexpass.entitlements From 2924495e069f8e4c58a87fd0df80caf4663a2e75 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 12:27:27 +0100 Subject: [PATCH 04/54] bootstrap: proper photos organisation --- tools/plex-bootstraptest.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index c38bcc017..6e5650e3a 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -253,13 +253,27 @@ def get_movie_path(name, year): photos_path = os.path.join(path, 'media', 'Photos') makedirs(photos_path, exist_ok=True) - has_photos = len(glob(os.path.join(photos_path, '*.jpg'))) - while has_photos < 10: - has_photos += 1 - download('https://picsum.photos/800/600/?random', '', - filename='photo%d.jpg' % has_photos, savepath=photos_path) + folders = { + ('Cats', ): 3, + ('Cats', 'Cats in bed'): 7, + ('Cats', 'Cats not in bed'): 1, + ('Cats', 'Not cats in bed'): 1, + } - print('Photos collected, but we need to create an album later...') + has_photos = 0 + for folder_path, required_cnt in folders.items(): + folder_path = os.path.join(photos_path, *folder_path) + photos_in_folder = len(glob(os.path.join(folder_path, '*.jpg'))) + while photos_in_folder < required_cnt: + photos_in_folder += 1 + download('https://picsum.photos/800/600/?random', '', + filename='photo%d.jpg' % photos_in_folder, savepath=folder_path) + has_photos += photos_in_folder + + if opts.with_photo_album: + print('Photos collected, but we need to create an album later...') + else: + print('Photos collected...') sections.append(dict(name='Photos', type='photo', location='/data/Photos', agent='com.plexapp.agents.none', scanner='Plex Photo Scanner')) From 7d8b61a42c6972e5cb593d32788596ddb22f66de Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 12:55:05 +0100 Subject: [PATCH 05/54] fix rest of photos tests --- plexapi/library.py | 6 ++---- plexapi/utils.py | 6 +++--- tests/test_photo.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index 71380d015..0349667bf 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -853,13 +853,11 @@ class PhotoSection(LibrarySection): def searchAlbums(self, title, **kwargs): """ Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ - key = '/library/sections/%s/all?type=14' % self.key - return self.fetchItems(key, title=title) + return self.search(libtype='photoalbum', title=title, **kwargs) def searchPhotos(self, title, **kwargs): """ Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """ - key = '/library/sections/%s/all?type=13' % self.key - return self.fetchItems(key, title=title) + return self.search(libtype='photo', title=title, **kwargs) def sync(self, resolution, limit=None, **kwargs): """ Add current Music library section as sync item for specified device. diff --git a/plexapi/utils.py b/plexapi/utils.py index f83fa858a..3c64d450f 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -14,9 +14,9 @@ # Search Types - Plex uses these to filter specific media types when searching. # Library Types - Populated at runtime -SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, - 'artist': 8, 'album': 9, 'track': 10, 'photo': 14, - 'collection': 18} +SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'trailer': 5, 'comic': 6, 'person': 7, + 'artist': 8, 'album': 9, 'track': 10, 'picture': 11, 'clip': 12, 'photo': 13, 'photoalbum': 14, + 'playlist': 15, 'playlistFolder': 16, 'collection': 18, 'userPlaylistItem': 1001} PLEXOBJECTS = {} diff --git a/tests/test_photo.py b/tests/test_photo.py index 0a7a6c3fb..efde68197 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -5,5 +5,5 @@ def test_photo_Photoalbum(photoalbum): assert len(photoalbum.photos()) == 3 cats_in_bed = photoalbum.album('Cats in bed') assert len(cats_in_bed.photos()) == 7 - a_pic = cats_in_bed.photo('maxresdefault') + a_pic = cats_in_bed.photo('photo7') assert a_pic From 90f786715aa6007e76e39ebe3d2e08136e7bf670 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 13:06:48 +0100 Subject: [PATCH 06/54] fix myplex new attributes test --- tests/conftest.py | 1 + tests/test_myplex.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5e3e22429..0b6988b26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,6 +53,7 @@ FRAMERATES = {'24p', 'PAL', 'NTSC'} PROFILES = {'advanced simple', 'main', 'constrained baseline'} RESOLUTIONS = {'sd', '480', '576', '720', '1080'} +ENTITLEMENTS = {'ios', 'cpms', 'roku', 'android', 'xbox_one', 'xbox_360', 'windows', 'windows_phone'} def pytest_addoption(parser): diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 09fe44663..a40213b3d 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -160,4 +160,4 @@ def test_myplex_plexpass_attributes(account_plexpass): assert 'sync' in account_plexpass.subscriptionFeatures assert 'premium_music_metadata' in account_plexpass.subscriptionFeatures assert 'plexpass' in account_plexpass.roles - assert 'all' in account_plexpass.entitlements + assert set(account_plexpass.entitlements) == utils.ENTITLEMENTS From ab2e856d240c5559116a5530d838915c19e8c98d Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 13:07:05 +0100 Subject: [PATCH 07/54] fix music bootstrap by setting agent to lastfm --- tools/plex-bootstraptest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 6e5650e3a..cd0efd312 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -246,7 +246,7 @@ def get_movie_path(name, year): expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) print('Finished with Music...') - sections.append(dict(name='Music', type='artist', location='/data/Music', agent='com.plexapp.agents.none', + sections.append(dict(name='Music', type='artist', location='/data/Music', agent='com.plexapp.agents.lastfm', scanner='Plex Music Scanner')) if opts.with_photos: From cdac7b6553104fa6d3d3343185906fbb75737e66 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 13:33:39 +0100 Subject: [PATCH 08/54] fix sync tests --- plexapi/library.py | 2 +- plexapi/photo.py | 2 +- tests/test_sync.py | 14 +++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index 0349667bf..a5ab3de56 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -844,7 +844,7 @@ class PhotoSection(LibrarySection): TAG (str): 'Directory' TYPE (str): 'photo' """ - ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure') + ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device') ALLOWED_SORT = ('addedAt',) TAG = 'Directory' TYPE = 'photo' diff --git a/plexapi/photo.py b/plexapi/photo.py index 9563b9bf0..4d03212df 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -154,7 +154,7 @@ def sync(self, resolution, client=None, clientId=None, limit=None, title=None): sync_item.metadataType = self.METADATA_TYPE sync_item.machineIdentifier = self._server.machineIdentifier - section = self._server.library.sectionByID(self.librarySectionID) + section = self.section() sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key)) sync_item.policy = Policy.create(limit) diff --git a/tests/test_sync.py b/tests/test_sync.py index a81398d88..2271a3a47 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -183,10 +183,8 @@ def test_add_music_track_to_sync(clear_sync_device, track): assert track.ratingKey == media_list[0].ratingKey -def test_add_photo_to_sync(clear_sync_device, photos): - photo = photos.all()[0] - if not hasattr(photo, 'librarySectionID'): - pytest.skip('Photos are not ready for individual synchronization yet') +def test_add_photo_to_sync(clear_sync_device, photoalbum): + photo = photoalbum.photo('photo1') new_item = photo.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photo._server.refreshSync() item = ensure_sync_item(clear_sync_device, new_item) @@ -229,7 +227,7 @@ def test_sync_entire_library_photos(clear_sync_device, photos): new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photos._server.refreshSync() item = ensure_sync_item(clear_sync_device, new_item) - section_content = photos.all() + section_content = photos.search(libtype='photo') media_list = item.getMedia() assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] @@ -283,10 +281,8 @@ def test_playlist_music_sync(plex, clear_sync_device, artist): playlist.delete() -def test_playlist_photos_sync(plex, clear_sync_device, photos): - items = photos.all() - if not hasattr(items[0], 'librarySectionID'): - pytest.skip('Photos are not ready for individual synchronization yet') +def test_playlist_photos_sync(plex, clear_sync_device, photoalbum): + items = photoalbum.photos() playlist = plex.createPlaylist('Sync: Photos', items) new_item = playlist.sync(photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device) playlist._server.refreshSync() From a923db6a5532941f5f2a5c2f04dbda77df7baa15 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 13:48:30 +0100 Subject: [PATCH 09/54] increase bootstrap timeout --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c4a2ecd3e..f16cd7f5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ before_install: install: - pip install -r requirements_dev.txt - PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1 - --without-album --bootstrap-timeout 300 --docker-tag $PLEX_CONTAINER_TAG --accept-eula + --without-album --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG --accept-eula script: - py.test tests --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi From 31a9f533392093fc7c6763fc5c6fdb34d21d63a2 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 14:44:41 +0100 Subject: [PATCH 10/54] remove timeout from .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f16cd7f5f..dc71f4690 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ python: env: global: - PLEXAPI_AUTH_SERVER_BASEURL=http://127.0.0.1:32400 - - PLEXAPI_PLEXAPI_TIMEOUT=10 matrix: - PLEX_CONTAINER_TAG=latest From 8acc1251d669671e76b0e72341ff5a7da810ccdb Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 14:45:29 +0100 Subject: [PATCH 11/54] do not create playlist-style photoalbums in plex-bootstraptest.py --- .travis.yml | 2 +- tools/plex-bootstraptest.py | 27 +++++---------------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc71f4690..578a72f9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ before_install: install: - pip install -r requirements_dev.txt - PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1 - --without-album --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG --accept-eula + --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG --accept-eula script: - py.test tests --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index cd0efd312..974537ec2 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -1,7 +1,8 @@ +""" The script is used to create bootstrap a docker container with Plex and with all the libraries required for testing. +""" + import argparse -import logging import os -import platform from glob import glob from shutil import copyfile, rmtree from subprocess import call @@ -14,7 +15,6 @@ from plexapi.compat import which, makedirs from plexapi.exceptions import BadRequest from plexapi.myplex import MyPlexAccount -from plexapi.server import PlexServer from plexapi.utils import download, SEARCHTYPES DOCKER_CMD = [ @@ -97,8 +97,6 @@ def get_ips(): action='store_false') parser.add_argument('--without-photos', help='Do not create Photos section', default=True, dest='with_photos', action='store_false') - parser.add_argument('--without-album', help='Do not create Photo Album', default=True, dest='with_photo_album', - action='store_false') parser.add_argument('--show-token', help='Display access token after bootstrap', default=False, action='store_true') opts = parser.parse_args() print('I`m going to create a plex instance named %s with advertised ip "%s", be prepared!' % (opts.server_name, @@ -270,10 +268,7 @@ def get_movie_path(name, year): filename='photo%d.jpg' % photos_in_folder, savepath=folder_path) has_photos += photos_in_folder - if opts.with_photo_album: - print('Photos collected, but we need to create an album later...') - else: - print('Photos collected...') + print('Photos collected...') sections.append(dict(name='Photos', type='photo', location='/data/Photos', agent='com.plexapp.agents.none', scanner='Plex Photo Scanner')) @@ -282,8 +277,6 @@ def get_movie_path(name, year): library = server.library - finished = expected_media_count == 0 - processed_media = 0 def alert_callback(data): @@ -295,9 +288,6 @@ def alert_callback(data): SEARCHTYPES['photo']): processed_media += 1 - if processed_media == expected_media_count: - finished = True - notifier = server.startAlertListener(alert_callback) first_section = sections.pop(0) @@ -325,18 +315,11 @@ def alert_callback(data): print('Sections created, almost done! Please wait while metadata will be collected, it may take a couple ' 'minutes...') - album_created = False start_time = time() - while not finished and not (album_created and opts.with_photos and opts.with_photo_album): + while processed_media < expected_media_count: if time() - start_time >= opts.bootstrap_timeout: print('Metadata scan takes too long, probably something went really wrong') exit(1) - if not album_created and opts.with_photos and opts.with_photo_album: - photos = library.section('Photos').all() - if len(photos) == has_photos: - server.createPlaylist('photo_album1', photos) - album_created = True - print('Photo album created') sleep(3) print('Base URL is %s' % server.url('', False)) From 92d5b96d97d8e6cef96bafe0b485dac81c60e01b Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:11:00 +0100 Subject: [PATCH 12/54] allow negative filtering in LibrarySection.search() --- plexapi/library.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index a5ab3de56..793e60fc3 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -470,8 +470,8 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist, album, track; optional). **kwargs (dict): Any of the available filters for the current library section. Partial string - matches allowed. Multiple matches OR together. All inputs will be compared with the - available options and a warning logged if the option does not appear valid. + matches allowed. Multiple matches OR together. Negative filtering also possible, just add an + exclamation mark to the end of filter name, e.g. `resolution!=1x1`. * unwatched: Display or hide unwatched content (True, False). [all] * duplicate: Display or hide duplicate items (True, False). [movie] @@ -486,6 +486,9 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg * resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie] * studio: List of studios to search within ([studio_or_key, ...]). [music] * year: List of years to search within ([yyyy, ...]). [all] + + Raises: + :class:`plexapi.exceptions.BadRequest`: when applying unknown filter """ # cleanup the core arguments args = {} @@ -510,7 +513,10 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg def _cleanSearchFilter(self, category, value, libtype=None): # check a few things before we begin - if category not in self.ALLOWED_FILTERS: + if category.endswith('!'): + if category[:-1] not in self.ALLOWED_FILTERS: + raise BadRequest('Unknown filter category: %s' % category[:-1]) + elif category not in self.ALLOWED_FILTERS: raise BadRequest('Unknown filter category: %s' % category) if category in self.BOOLEAN_FILTERS: return '1' if value else '0' @@ -839,12 +845,12 @@ class PhotoSection(LibrarySection): Attributes: ALLOWED_FILTERS (list): List of allowed search filters. ('all', 'iso', - 'make', 'lens', 'aperture', 'exposure') + 'make', 'lens', 'aperture', 'exposure', 'device', 'resolution') ALLOWED_SORT (list): List of allowed sorting keys. ('addedAt') TAG (str): 'Directory' TYPE (str): 'photo' """ - ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device') + ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device', 'resolution') ALLOWED_SORT = ('addedAt',) TAG = 'Directory' TYPE = 'photo' From c1abf50093e20149ab9d7d3fec80b1a2e41ecdb4 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:11:14 +0100 Subject: [PATCH 13/54] fix sync tests once again --- tests/test_sync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index 2271a3a47..7ea5dc148 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -227,7 +227,8 @@ def test_sync_entire_library_photos(clear_sync_device, photos): new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photos._server.refreshSync() item = ensure_sync_item(clear_sync_device, new_item) - section_content = photos.search(libtype='photo') + # It's not that easy, to just get all the photos within the library, so let`s query for photos with resolution!=0x0 + section_content = photos.search(libtype='photo', **{'resolution!': '0x0'}) media_list = item.getMedia() assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] From faeaaba3d56cc70160ae2aecaacba5d376276972 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:12:01 +0100 Subject: [PATCH 14/54] use sendCrashReports in test_settings --- tests/test_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_settings.py b/tests/test_settings.py index c6417c87c..cd4908f17 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -10,9 +10,9 @@ def test_settings_get(plex): def test_settings_set(plex): - cd = plex.settings.get('collectUsageData') + cd = plex.settings.get('sendCrashReports') old_value = cd.value cd.set(not old_value) plex.settings.save() delattr(plex, '_settings') - assert plex.settings.get('collectUsageData').value == (not old_value) + assert plex.settings.get('sendCrashReports').value == (not old_value) From 6410fe0fb4858a4d531a55054fec1a337ef30272 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:21:52 +0100 Subject: [PATCH 15/54] fix test_settings --- plexapi/settings.py | 2 +- tests/test_settings.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plexapi/settings.py b/plexapi/settings.py index 9f85ebdc2..94824b6d4 100644 --- a/plexapi/settings.py +++ b/plexapi/settings.py @@ -99,7 +99,7 @@ class Setting(PlexObject): group (str): Group name this setting is categorized as. enumValues (list,dict): List or dictionary of valis values for this setting. """ - _bool_cast = lambda x: True if x == 'true' else False + _bool_cast = lambda x: True if x == 'true' or x == '1' else False _bool_str = lambda x: str(x).lower() TYPES = { 'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str}, diff --git a/tests/test_settings.py b/tests/test_settings.py index cd4908f17..e385fc16b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -12,7 +12,8 @@ def test_settings_get(plex): def test_settings_set(plex): cd = plex.settings.get('sendCrashReports') old_value = cd.value - cd.set(not old_value) + new_value = not old_value + cd.set(new_value) plex.settings.save() - delattr(plex, '_settings') - assert plex.settings.get('sendCrashReports').value == (not old_value) + plex._settings = None + assert plex.settings.get('sendCrashReports').value == new_value From c582a8018fe94a9ad052fe2d3626cb90fa0f4e72 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:28:29 +0100 Subject: [PATCH 16/54] fix test_video --- plexapi/video.py | 2 +- tests/test_video.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plexapi/video.py b/plexapi/video.py index 728c8bd00..df5393fab 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -323,7 +323,7 @@ def isWatched(self): def seasons(self, **kwargs): """ Returns a list of :class:`~plexapi.video.Season` objects. """ - key = '/library/metadata/%s/children' % self.ratingKey + key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey return self.fetchItems(key, **kwargs) def season(self, title=None): diff --git a/tests/test_video.py b/tests/test_video.py index 6428f7c06..1550bab67 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -151,9 +151,9 @@ def test_video_Movie_attrs(movies): assert audio.id >= 1 assert audio.index == 1 assert utils.is_metadata(audio._initpath) - assert audio.language == 'English' - assert audio.languageCode == 'eng' - assert audio.samplingRate == 48000 + assert audio.language is None + assert audio.languageCode is None + assert audio.samplingRate == 44100 assert audio.selected is True assert audio._server._baseurl == utils.SERVER_BASEURL assert audio.streamType == 2 @@ -184,7 +184,7 @@ def test_video_Movie_attrs(movies): assert video.chromaSubsampling == '4:2:0' assert video.codec in utils.CODECS assert video.codecID is None - assert video.colorSpace == 'bt709' + assert video.colorSpace is None assert video.duration is None assert utils.is_float(video.frameRate, gte=20.0) assert video.frameRateMode is None @@ -193,8 +193,8 @@ def test_video_Movie_attrs(movies): assert utils.is_int(video.id) assert utils.is_int(video.index, gte=0) assert utils.is_metadata(video._initpath) - assert video.language == 'English' - assert video.languageCode == 'eng' + assert video.language is None + assert video.languageCode is None assert utils.is_int(video.level) assert video.profile in utils.PROFILES assert utils.is_int(video.refFrames) @@ -223,7 +223,7 @@ def test_video_Movie_attrs(movies): assert stream1.chromaSubsampling == '4:2:0' assert stream1.codec in utils.CODECS assert stream1.codecID is None - assert stream1.colorSpace == 'bt709' + assert stream1.colorSpace is None assert stream1.duration is None assert utils.is_float(stream1.frameRate, gte=20.0) assert stream1.frameRateMode is None @@ -232,8 +232,8 @@ def test_video_Movie_attrs(movies): assert utils.is_int(stream1.id) assert utils.is_int(stream1.index, gte=0) assert utils.is_metadata(stream1._initpath) - assert stream1.language == 'English' - assert stream1.languageCode == 'eng' + assert stream1.language is None + assert stream1.languageCode is None assert utils.is_int(stream1.level) assert stream1.profile in utils.PROFILES assert utils.is_int(stream1.refFrames) @@ -259,8 +259,8 @@ def test_video_Movie_attrs(movies): assert utils.is_int(stream2.id) assert utils.is_int(stream2.index) assert utils.is_metadata(stream2._initpath) - assert stream2.language == 'English' - assert stream2.languageCode == 'eng' + assert stream2.language is None + assert stream2.languageCode is None assert utils.is_int(stream2.samplingRate) assert stream2.selected is True assert stream2._server._baseurl == utils.SERVER_BASEURL @@ -508,7 +508,7 @@ def test_video_Episode_attrs(episode): def test_video_Season(show): seasons = show.seasons() - assert len(seasons) >= 1 + assert len(seasons) == 2 assert ['Season 1', 'Season 2'] == [s.title for s in seasons[:2]] assert show.season('Season 1') == seasons[0] From 28e0883c520d04df2dd5f4d7ec5ab8d7d55053a3 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:42:43 +0100 Subject: [PATCH 17/54] do not accept eula in bootstrap --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 578a72f9b..a6285448a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ before_install: install: - pip install -r requirements_dev.txt - PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1 - --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG --accept-eula + --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG script: - py.test tests --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi From fee3c8163b5a552b18dd5919164f1a27c30807d7 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:43:11 +0100 Subject: [PATCH 18/54] fix PlexServer.isLatest() --- plexapi/server.py | 9 +++++++-- tests/test_server.py | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plexapi/server.py b/plexapi/server.py index bf1438eb0..7be355dd8 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -290,12 +290,17 @@ def check_for_update(self, force=True, download=False): part = '/updater/check?download=%s' % (1 if download else 0) if force: self.query(part, method=self._session.put) - return self.fetchItem('/updater/status') + releases = self.fetchItems('/updater/status') + if len(releases): + return releases[0] + else: + return None def isLatest(self): """ Check if the installed version of PMS is the latest. """ release = self.check_for_update(force=True) - return bool(release.version == self.version) + print(release) + return release is None def installUpdate(self): """ Install the newest version of Plex Media Server. """ diff --git a/tests/test_server.py b/tests/test_server.py index b023f0bbb..28d40240f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -165,7 +165,12 @@ def test_server_sessions(plex): def test_server_isLatest(plex, mocker): - plex.isLatest() + from os import environ + is_latest = plex.isLatest() + if environ.get('PLEX_CONTAINER_TAG') and environ['PLEX_CONTAINER_TAG'] != 'latest': + assert not is_latest + else: + return pytest.skip('Do not forget to run with PLEX_CONTAINER_TAG != latest to ensure that update is available') def test_server_installUpdate(plex, mocker): From 5043d07730c5718b830bbcfca405a2fdd5ce1b86 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:43:27 +0100 Subject: [PATCH 19/54] add test against old version of PlexServer --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index a6285448a..9803ff1c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,9 @@ after_script: jobs: include: + - python: 3.6 + env: + - PLEX_CONTAINER_TAG=1.3.2.3112-1751929 - python: 3.6 name: "Flake8" install: From b3a6658cb706df18781a345fe11b3ad8e501ab4b Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 15:54:23 +0100 Subject: [PATCH 20/54] fix MyPlexAccount.OutOut --- plexapi/myplex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index e9d1fe495..cb1aef084 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -395,7 +395,7 @@ def optOut(self, playback=None, library=None): if library is not None: params['optOutLibraryStats'] = int(library) url = 'https://plex.tv/api/v2/user/privacy' - return self.query(url, method=self._session.put, params=params) + return self.query(url, method=self._session.put, data=params) def syncItems(self, client=None, clientId=None): """ Returns an instance of :class:`plexapi.sync.SyncList` for specified client. From aa7c0524859165df0bae24be1bae8c0d4d4e6e74 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 16:00:28 +0100 Subject: [PATCH 21/54] add flag for one-time testing in Travis --- .travis.yml | 1 + tests/conftest.py | 8 ++++++++ tests/test_myplex.py | 8 ++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9803ff1c5..bf31061a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,6 +56,7 @@ jobs: after_script: true env: - PLEX_CONTAINER_TAG=latest + - TEST_ACCOUNT_ONCE=1 - stage: deploy name: "Deploy to PyPi" python: 3.6 diff --git a/tests/conftest.py b/tests/conftest.py index 0b6988b26..7c1f098bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,6 +76,14 @@ def account(): return MyPlexAccount() +@pytest.fixture(scope='session') +def account_once(account): + from os import environ + if environ.get('TEST_ACCOUNT_ONCE') != '1': + 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: diff --git a/tests/test_myplex.py b/tests/test_myplex.py index a40213b3d..61d90745a 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -98,9 +98,9 @@ def test_myplex_deletewebhooks(account): account.deleteWebhook('http://site.com') -def test_myplex_optout(account): +def test_myplex_optout(account_once): def enabled(): - ele = account.query('https://plex.tv/api/v2/user/privacy') + ele = account_once.query('https://plex.tv/api/v2/user/privacy') lib = ele.attrib.get('optOutLibraryStats') play = ele.attrib.get('optOutPlayback') return bool(int(lib)), bool(int(play)) @@ -108,11 +108,11 @@ def enabled(): # This should be False False library_enabled, playback_enabled = enabled() - account.optOut(library=True, playback=True) + account_once.optOut(library=True, playback=True) assert all(enabled()) - account.optOut(library=False, playback=False) + account_once.optOut(library=False, playback=False) assert not all(enabled()) From fe3972d027ee99189887560de6e3b20c71353b6e Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 16:02:58 +0100 Subject: [PATCH 22/54] fix test_library onDeck tests --- tests/test_library.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_library.py b/tests/test_library.py index e7d8e3e65..ae3b4e36a 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -57,7 +57,7 @@ def test_library_fetchItem(plex, movie): def test_library_onDeck(plex, movie): - movie.updateProgress(5000) + movie.updateProgress(movie.duration * 1000 / 10) # set progress to 10% assert len(list(plex.library.onDeck())) movie.markUnwatched() @@ -143,11 +143,12 @@ def test_librarty_deleteMediaPreviews(movies): def test_library_MovieSection_onDeck(movie, movies, tvshows, episode): - movie.updateProgress(5000) + movie.updateProgress(movie.duration * 1000 / 10) # set progress to 10% + assert movies.onDeck() + movie.markUnwatched() episode.markWatched() - assert len(movies.onDeck()) + len(tvshows.onDeck()) + assert tvshows.onDeck() episode.markUnwatched() - movie.markUnwatched() def test_library_MovieSection_recentlyAdded(movies): From 4bdf372591513602ec0cee1696bf2d602a18bd84 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 17:07:05 +0100 Subject: [PATCH 23/54] fix more tests --- plexapi/myplex.py | 3 ++- tests/conftest.py | 13 +++++++++-- tests/test_myplex.py | 43 ++++++++++++++++++++++--------------- tests/test_playlist.py | 6 +++--- tests/test_server.py | 21 ++++++++++++------ tools/plex-bootstraptest.py | 6 ++++-- 6 files changed, 61 insertions(+), 31 deletions(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index cb1aef084..b6ec6577c 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -376,7 +376,8 @@ def deleteWebhook(self, url): def setWebhooks(self, urls): log.info('Setting webhooks: %s' % urls) - data = self.query(self.WEBHOOKS, self._session.post, data={'urls[]': urls}) + data = {'urls[]': urls} if len(urls) else {'urls': ''} + data = self.query(self.WEBHOOKS, self._session.post, data=data) self._webhooks = self.listAttrs(data, 'url', etag='webhook') return self._webhooks diff --git a/tests/conftest.py b/tests/conftest.py index 7c1f098bc..d4159dce2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ # Cats (with cute cat photos inside) from datetime import datetime from functools import partial +from os import environ import pytest import requests @@ -78,8 +79,7 @@ def account(): @pytest.fixture(scope='session') def account_once(account): - from os import environ - if environ.get('TEST_ACCOUNT_ONCE') != '1': + 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 @@ -203,6 +203,15 @@ def photoalbum(photos): return photos.get('photo_album1') +@pytest.fixture() +def shared_username(account): + username = environ.get('SHARED_USERNAME', 'PKKid') + for user in account.users(): + if user.username == username: + return username + pytest.skip('Shared user wasn`t found') + + @pytest.fixture() def monkeydownload(request, monkeypatch): monkeypatch.setattr('plexapi.utils.download', partial(plexapi.utils.download, mocked=True)) diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 61d90745a..fe9435c5a 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -54,8 +54,10 @@ def test_myplex_devices(account): assert devices, 'No devices found for account: %s' % account.name -def test_myplex_device(account): - assert account.device('pkkid-plexapi') +def test_myplex_device(account, plex): + from plexapi import X_PLEX_DEVICE_NAME + assert account.device(plex.friendlyName) + assert account.device(X_PLEX_DEVICE_NAME) def _test_myplex_connect_to_device(account): @@ -78,24 +80,32 @@ def test_myplex_users(account): assert len(users[0].servers[0].sections()) == 10, "Could'nt info about the shared libraries" -def test_myplex_resource(account): - assert account.resource('pkkid-plexapi') +def test_myplex_resource(account, plex): + assert account.resource(plex.friendlyName) def test_myplex_webhooks(account): - # Webhooks are a plex pass feature to this will fail - with pytest.raises(BadRequest): - account.webhooks() + if account.subscriptionActive: + assert not account.webhooks() + else: + with pytest.raises(BadRequest): + account.webhooks() def test_myplex_addwebhooks(account): - with pytest.raises(BadRequest): - account.addWebhook('http://site.com') + if account.subscriptionActive: + assert len(account.addWebhook('http://example.com')) == 1 + else: + with pytest.raises(BadRequest): + account.addWebhook('http://example.com') def test_myplex_deletewebhooks(account): - with pytest.raises(BadRequest): - account.deleteWebhook('http://site.com') + if account.subscriptionActive: + assert not account.deleteWebhook('http://example.com') + else: + with pytest.raises(BadRequest): + account.deleteWebhook('http://example.com') def test_myplex_optout(account_once): @@ -137,20 +147,19 @@ def test_myplex_inviteFriend_remove(account, plex, mocker): account.removeFriend(inv_user) -def test_myplex_updateFriend(account, plex, mocker): - edit_user = 'PKKid' +def test_myplex_updateFriend(account, plex, mocker, shared_username): vid_filter = {'contentRating': ['G'], 'label': ['foo']} secs = plex.library.sections() - user = account.user(edit_user) + user = account.user(shared_username) ids = account._getSectionIds(plex.machineIdentifier, secs) with mocker.patch.object(account, '_getSectionIds', return_value=ids): with mocker.patch.object(account, 'user', return_value=user): with utils.callable_http_patch(): - account.updateFriend(edit_user, plex, secs, allowSync=True, removeSections=True, - allowCameraUpload=True, allowChannels=False, filterMovies=vid_filter, - filterTelevision=vid_filter, filterMusic={'label': ['foo']}) + account.updateFriend(shared_username, plex, secs, allowSync=True, removeSections=True, + allowCameraUpload=True, allowChannels=False, filterMovies=vid_filter, + filterTelevision=vid_filter, filterMusic={'label': ['foo']}) def test_myplex_plexpass_attributes(account_plexpass): diff --git a/tests/test_playlist.py b/tests/test_playlist.py index c52c73c46..7ecfbaa7c 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -99,12 +99,12 @@ def test_playqueues(plex): assert playqueue.playQueueID, 'Play queue ID not set.' -def test_copyToUser(plex, show, fresh_plex): +def test_copyToUser(plex, show, fresh_plex, shared_username): episodes = show.episodes() playlist = plex.createPlaylist('shared_from_test_plexapi', episodes) try: - playlist.copyToUser('PKKid') - user = plex.myPlexAccount().user('PKKid') + playlist.copyToUser(shared_username) + user = plex.myPlexAccount().user(shared_username) user_plex = fresh_plex(plex._baseurl, user.get_token(plex.machineIdentifier)) assert playlist.title in [p.title for p in user_plex.playlists()] finally: diff --git a/tests/test_server.py b/tests/test_server.py index 28d40240f..94a62fcb9 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -116,9 +116,11 @@ def test_server_playlists(plex, show): playlist.delete() -def test_server_history(plex): +def test_server_history(plex, movie): + movie.markWatched() history = plex.history() assert len(history) + movie.markUnwatched() def test_server_Server_query(plex): @@ -230,13 +232,20 @@ def test_server_account(plex): # assert account.mappingError == 'publisherror' assert account.mappingErrorMessage is None assert account.mappingState == 'mapped' - assert re.match(utils.REGEX_IPADDR, account.privateAddress) - assert int(account.privatePort) >= 1000 - assert re.match(utils.REGEX_IPADDR, account.publicAddress) - assert int(account.publicPort) >= 1000 + if account.mappingError != 'unreachable': + assert re.match(utils.REGEX_IPADDR, account.privateAddress) + assert int(account.privatePort) >= 1000 + assert re.match(utils.REGEX_IPADDR, account.publicAddress) + assert int(account.publicPort) >= 1000 + else: + assert account.privateAddress == '' + assert int(account.privatePort) == 0 + assert account.publicAddress == '' + assert int(account.publicPort) == 0 assert account.signInState == 'ok' assert isinstance(account.subscriptionActive, bool) - if account.subscriptionActive: assert len(account.subscriptionFeatures) + if account.subscriptionActive: + assert len(account.subscriptionFeatures) # Below check keeps failing.. it should go away. # else: assert sorted(account.subscriptionFeatures) == ['adaptive_bitrate', # 'download_certificates', 'federated-auth', 'news'] diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 974537ec2..da6a5bf0d 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -268,7 +268,7 @@ def get_movie_path(name, year): filename='photo%d.jpg' % photos_in_folder, savepath=folder_path) has_photos += photos_in_folder - print('Photos collected...') + print('Finished with photos...') sections.append(dict(name='Photos', type='photo', location='/data/Photos', agent='com.plexapp.agents.none', scanner='Plex Photo Scanner')) @@ -278,15 +278,17 @@ def get_movie_path(name, year): library = server.library processed_media = 0 + print('Expected media count: %d' % expected_media_count) def alert_callback(data): - global finished, processed_media + global processed_media if data['type'] == 'timeline': for entry in data['TimelineEntry']: if entry['identifier'] == 'com.plexapp.plugins.library' and entry['state'] == 5 \ and entry['type'] in (SEARCHTYPES['movie'], SEARCHTYPES['episode'], SEARCHTYPES['track'], SEARCHTYPES['photo']): processed_media += 1 + print('Processed media count: %d' % processed_media) notifier = server.startAlertListener(alert_callback) From 440b8baaf5c211c1e4fd247c8a3d0eb33f7db952 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 17:17:16 +0100 Subject: [PATCH 24/54] use tqdm in plex-bootstraptest for media scanning progress --- tools/plex-bootstraptest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index da6a5bf0d..5753d7798 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -10,6 +10,7 @@ from uuid import uuid4 from requests import codes +from tqdm import tqdm import plexapi from plexapi.compat import which, makedirs @@ -277,18 +278,15 @@ def get_movie_path(name, year): library = server.library - processed_media = 0 - print('Expected media count: %d' % expected_media_count) + bar = tqdm(desc='Scanning libraries', total=expected_media_count) def alert_callback(data): - global processed_media if data['type'] == 'timeline': for entry in data['TimelineEntry']: if entry['identifier'] == 'com.plexapp.plugins.library' and entry['state'] == 5 \ and entry['type'] in (SEARCHTYPES['movie'], SEARCHTYPES['episode'], SEARCHTYPES['track'], SEARCHTYPES['photo']): - processed_media += 1 - print('Processed media count: %d' % processed_media) + bar.update() notifier = server.startAlertListener(alert_callback) @@ -318,11 +316,12 @@ def alert_callback(data): 'minutes...') start_time = time() - while processed_media < expected_media_count: + while bar.n < bar.total: if time() - start_time >= opts.bootstrap_timeout: print('Metadata scan takes too long, probably something went really wrong') exit(1) sleep(3) + bar.close() print('Base URL is %s' % server.url('', False)) if opts.show_token: From ef9ec7baa3c5459277d8b2fcfe9f82425ef88e6a Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 18:10:13 +0100 Subject: [PATCH 25/54] create sections one-by-one --- tools/plex-bootstraptest.py | 117 ++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 5753d7798..90a818ae5 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -65,6 +65,63 @@ def get_ips(): if i[4][0] not in ('127.0.0.1', '::1') and not i[4][0].startswith('fe80:')])) +def create_section(server, section): + processed_media = 0 + expected_media_count = section.pop('expected_media_count', 0) + + bar = tqdm(desc='Scanning section ' + section['name'], total=expected_media_count) + + expected_media_type = section['type'] + if section['type'] == 'show': + expected_media_type = 'episode' + elif section['type'] == 'artist': + expected_media_type = 'track' + + def alert_callback(data): + global processed_media + if data['type'] == 'timeline': + for entry in data['TimelineEntry']: + if entry['identifier'] == 'com.plexapp.plugins.library': + # Missed mediaState means that media was processed (analyzed & thumbnailed) + if 'mediaState' not in entry and entry['type'] == SEARCHTYPES[expected_media_type]: + # state=5 means record processed, applicable only when metadata source was set + if entry['state'] == 5: + bar.update() + + # state=1 means record processed, when no metadata source was set + elif entry['state'] == 1 and entry['type'] == SEARCHTYPES['photo']: + bar.update() + + notifier = server.startAlertListener(alert_callback) + + # I don't know how to determinate of plex successfully started, so let's do it in creepy way + success = False + start_time = time() + while not success and (time() - start_time < opts.bootstrap_timeout): + try: + server.library.add(**section) + success = True + except BadRequest as e: + if 'the server is still starting up. Please retry later' in str(e): + sleep(1) + else: + raise + + if not success: + print('Something went wrong :(') + exit(1) + + while bar.n < bar.total: + if time() - start_time >= opts.bootstrap_timeout: + print('Metadata scan takes too long, probably something went really wrong') + exit(1) + sleep(3) + + bar.close() + + notifier.stop() + + if __name__ == '__main__': if which('docker') is None: print('Docker is required to be available') @@ -147,8 +204,6 @@ def get_ips(): print('Ok, I got the server instance, let`s download what you`re missing') - expected_media_count = 0 - def get_tvshow_path(name, season, episode): return os.path.join(tvshows_path, name, 'S%02dE%02d.mp4' % (season, episode)) @@ -173,6 +228,7 @@ def get_movie_path(name, year): 'Sintel': 2010, } + expected_media_count = 0 for name, year in required_movies.items(): expected_media_count += 1 if not os.path.isfile(get_movie_path(name, year)): @@ -180,7 +236,7 @@ def get_movie_path(name, year): print('Finished with movies...') sections.append(dict(name='Movies', type='movie', location='/data/Movies', agent='com.plexapp.agents.imdb', - scanner='Plex Movie Scanner')) + scanner='Plex Movie Scanner', expected_media_count=expected_media_count)) if opts.with_shows: tvshows_path = os.path.join(path, 'media', 'TV-Shows') @@ -198,6 +254,7 @@ def get_movie_path(name, year): ] } + expected_media_count = 0 for show_name, seasons in required_tv_shows.items(): for season_id, episodes in enumerate(seasons, start=1): for episode_id in episodes: @@ -208,11 +265,12 @@ def get_movie_path(name, year): print('Finished with TV Shows...') sections.append(dict(name='TV Shows', type='show', location='/data/TV-Shows', agent='com.plexapp.agents.thetvdb', - scanner='Plex Series Scanner')) + scanner='Plex Series Scanner', expected_media_count=expected_media_count)) if opts.with_music: music_path = os.path.join(path, 'media', 'Music') makedirs(music_path, exist_ok=True) + expected_media_count = 0 artist_dst = os.path.join(music_path, 'Infinite State') dest_path = os.path.join(artist_dst, 'Unmastered Impulses') @@ -246,11 +304,12 @@ def get_movie_path(name, year): print('Finished with Music...') sections.append(dict(name='Music', type='artist', location='/data/Music', agent='com.plexapp.agents.lastfm', - scanner='Plex Music Scanner')) + scanner='Plex Music Scanner', expected_media_count=expected_media_count)) if opts.with_photos: photos_path = os.path.join(path, 'media', 'Photos') makedirs(photos_path, exist_ok=True) + expected_photo_count = 0 folders = { ('Cats', ): 3, @@ -271,57 +330,13 @@ def get_movie_path(name, year): print('Finished with photos...') sections.append(dict(name='Photos', type='photo', location='/data/Photos', agent='com.plexapp.agents.none', - scanner='Plex Photo Scanner')) + scanner='Plex Photo Scanner', expected_media_count=has_photos)) if sections: print('Ok, got the media, it`s time to create a library for you!') - library = server.library - - bar = tqdm(desc='Scanning libraries', total=expected_media_count) - - def alert_callback(data): - if data['type'] == 'timeline': - for entry in data['TimelineEntry']: - if entry['identifier'] == 'com.plexapp.plugins.library' and entry['state'] == 5 \ - and entry['type'] in (SEARCHTYPES['movie'], SEARCHTYPES['episode'], SEARCHTYPES['track'], - SEARCHTYPES['photo']): - bar.update() - - notifier = server.startAlertListener(alert_callback) - - first_section = sections.pop(0) - - # I don't know how to determinate of plex successfully started, so let's do it in creepy way - success = False - start_time = time() - while not success and (time() - start_time < opts.bootstrap_timeout): - try: - library.add(**first_section) - success = True - except BadRequest as e: - if 'the server is still starting up. Please retry later' in str(e): - sleep(1) - else: - raise - - if not success: - print('Something went wrong :(') - exit(1) - for section in sections: - library.add(**section) - - print('Sections created, almost done! Please wait while metadata will be collected, it may take a couple ' - 'minutes...') - - start_time = time() - while bar.n < bar.total: - if time() - start_time >= opts.bootstrap_timeout: - print('Metadata scan takes too long, probably something went really wrong') - exit(1) - sleep(3) - bar.close() + create_section(server, section) print('Base URL is %s' % server.url('', False)) if opts.show_token: From df874b0d5b47cf5dc7db628c3fdac506c498d058 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 18:10:42 +0100 Subject: [PATCH 26/54] update docs on AlertListener for timeline entries --- plexapi/alert.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plexapi/alert.py b/plexapi/alert.py index dc1c76e15..2a19c6d88 100644 --- a/plexapi/alert.py +++ b/plexapi/alert.py @@ -12,6 +12,18 @@ class AlertListener(threading.Thread): alerts you must call .start() on the object once it's created. When calling `PlexServer.startAlertListener()`, the thread will be started for you. + Known `state`-values for timeline entries, with identifier=`com.plexapp.plugins.library`: + + :0: The item was created + :1: Reporting progress on item processing + :2: Matching the item + :3: Downloading the metadata + :4: Processing downloaded metadata + :5: The item processed + :9: The item deleted + + When metadata agent is not set for the library processing ends with state=1. + Parameters: server (:class:`~plexapi.server.PlexServer`): PlexServer this listener is connected to. callback (func): Callback function to call on recieved messages. The callback function From 7ccca8e8c721c315551ff3a369a54285a54b1d84 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 18:47:58 +0100 Subject: [PATCH 27/54] fix plex-bootstraptest for server version 1.3.2 --- tools/plex-bootstraptest.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 90a818ae5..0a83bc85e 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -72,21 +72,23 @@ def create_section(server, section): bar = tqdm(desc='Scanning section ' + section['name'], total=expected_media_count) expected_media_type = section['type'] - if section['type'] == 'show': - expected_media_type = 'episode' - elif section['type'] == 'artist': + if expected_media_type == 'artist': expected_media_type = 'track' def alert_callback(data): global processed_media if data['type'] == 'timeline': for entry in data['TimelineEntry']: - if entry['identifier'] == 'com.plexapp.plugins.library': + if entry.get('identifier', 'com.plexapp.plugins.library') == 'com.plexapp.plugins.library': # Missed mediaState means that media was processed (analyzed & thumbnailed) if 'mediaState' not in entry and entry['type'] == SEARCHTYPES[expected_media_type]: # state=5 means record processed, applicable only when metadata source was set if entry['state'] == 5: - bar.update() + cnt = 1 + if expected_media_type == 'show': + show = server.library.sectionByID(str(entry['sectionID'])).get(entry['title']) + cnt = show.leafCount + bar.update(cnt) # state=1 means record processed, when no metadata source was set elif entry['state'] == 1 and entry['type'] == SEARCHTYPES['photo']: From 04befe2d858669840694ed1bfc20b352b105b908 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 19:09:17 +0100 Subject: [PATCH 28/54] display skip/xpass/xfail reasons --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index bf31061a2..89f6914bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,10 +31,10 @@ install: --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG script: - - py.test tests --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi + - py.test tests -rxXs --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi - PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1 - PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi - --cov-append + PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXs --tb=native --verbose --cov-config .coveragerc + --cov=plexapi --cov-append after_success: - coveralls From 34a7c5e2b7acbde6efa8606bdb772462c43a894e Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 19:09:34 +0100 Subject: [PATCH 29/54] fix tests on 1.3 --- tests/test_settings.py | 4 ++-- tests/test_sync.py | 2 +- tests/test_video.py | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_settings.py b/tests/test_settings.py index e385fc16b..78309c803 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -10,10 +10,10 @@ def test_settings_get(plex): def test_settings_set(plex): - cd = plex.settings.get('sendCrashReports') + cd = plex.settings.get('autoEmptyTrash') old_value = cd.value new_value = not old_value cd.set(new_value) plex.settings.save() plex._settings = None - assert plex.settings.get('sendCrashReports').value == new_value + assert plex.settings.get('autoEmptyTrash').value == new_value diff --git a/tests/test_sync.py b/tests/test_sync.py index 7ea5dc148..76cf1f44e 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -228,7 +228,7 @@ def test_sync_entire_library_photos(clear_sync_device, photos): photos._server.refreshSync() item = ensure_sync_item(clear_sync_device, new_item) # It's not that easy, to just get all the photos within the library, so let`s query for photos with resolution!=0x0 - section_content = photos.search(libtype='photo', **{'resolution!': '0x0'}) + section_content = photos.search(libtype='photo', **{'device!': '0x0'}) media_list = item.getMedia() assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] diff --git a/tests/test_video.py b/tests/test_video.py index 1550bab67..eb4b630ed 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -178,10 +178,10 @@ def test_video_Movie_attrs(movies): assert utils.is_int(media.width, gte=200) # Video video = movie.media[0].parts[0].videoStreams()[0] - assert video.bitDepth == 8 + assert video.bitDepth in (8, None) # Different versions of Plex Server return different values assert utils.is_int(video.bitrate) assert video.cabac is None - assert video.chromaSubsampling == '4:2:0' + assert video.chromaSubsampling in ('4:2:0', None) assert video.codec in utils.CODECS assert video.codecID is None assert video.colorSpace is None @@ -198,7 +198,7 @@ def test_video_Movie_attrs(movies): assert utils.is_int(video.level) assert video.profile in utils.PROFILES assert utils.is_int(video.refFrames) - assert video.scanType is None + assert video.scanType in ('progressive', None) assert video.selected is False assert video._server._baseurl == utils.SERVER_BASEURL assert utils.is_int(video.streamType) @@ -217,10 +217,10 @@ def test_video_Movie_attrs(movies): assert utils.is_int(part.size, gte=1000000) # Stream 1 stream1 = part.streams[0] - assert stream1.bitDepth == 8 + assert stream1.bitDepth in (8, None) assert utils.is_int(stream1.bitrate) assert stream1.cabac is None - assert stream1.chromaSubsampling == '4:2:0' + assert stream1.chromaSubsampling in ('4:2:0', None) assert stream1.codec in utils.CODECS assert stream1.codecID is None assert stream1.colorSpace is None @@ -237,7 +237,7 @@ def test_video_Movie_attrs(movies): assert utils.is_int(stream1.level) assert stream1.profile in utils.PROFILES assert utils.is_int(stream1.refFrames) - assert stream1.scanType is None + assert stream1.scanType in ('progressive', None) assert stream1.selected is False assert stream1._server._baseurl == utils.SERVER_BASEURL assert utils.is_int(stream1.streamType) @@ -301,7 +301,7 @@ def test_video_Show_attrs(show): assert utils.is_int(show.duration, gte=1600000) assert utils.is_section(show._initpath) # Check reloading the show loads the full list of genres - assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama'] + assert not {'Adventure', 'Drama'} - {i.tag for i in show.genres} show.reload() assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama', 'Fantasy'] # So the initkey should have changed because of the reload From 0902567e5c5ac2f173a12813dc4e0e86942407fd Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 19:45:31 +0100 Subject: [PATCH 30/54] wait for music to be fully processed in plex-bootstraptest --- tools/plex-bootstraptest.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 0a83bc85e..3637cf237 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -69,11 +69,11 @@ def create_section(server, section): processed_media = 0 expected_media_count = section.pop('expected_media_count', 0) - bar = tqdm(desc='Scanning section ' + section['name'], total=expected_media_count) + expected_media_type = (section['type'], ) + if section['type'] == 'artist': + expected_media_type = ('artist', 'album', 'track') - expected_media_type = section['type'] - if expected_media_type == 'artist': - expected_media_type = 'track' + expected_media_type = tuple(SEARCHTYPES[t] for t in expected_media_type) def alert_callback(data): global processed_media @@ -81,11 +81,13 @@ def alert_callback(data): for entry in data['TimelineEntry']: if entry.get('identifier', 'com.plexapp.plugins.library') == 'com.plexapp.plugins.library': # Missed mediaState means that media was processed (analyzed & thumbnailed) - if 'mediaState' not in entry and entry['type'] == SEARCHTYPES[expected_media_type]: + if 'mediaState' not in entry and entry['type'] in expected_media_type: # state=5 means record processed, applicable only when metadata source was set if entry['state'] == 5: cnt = 1 - if expected_media_type == 'show': + + # Workaround for old Plex versions which not reports individual episodes' progress + if entry['type'] == SEARCHTYPES['show']: show = server.library.sectionByID(str(entry['sectionID'])).get(entry['title']) cnt = show.leafCount bar.update(cnt) @@ -94,6 +96,7 @@ def alert_callback(data): elif entry['state'] == 1 and entry['type'] == SEARCHTYPES['photo']: bar.update() + bar = tqdm(desc='Scanning section ' + section['name'], total=expected_media_count) notifier = server.startAlertListener(alert_callback) # I don't know how to determinate of plex successfully started, so let's do it in creepy way @@ -288,7 +291,7 @@ def get_movie_path(name, year): os.rename(os.path.join(artist_dst, 'unmastered-impulses-master', 'mp3'), dest_path) rmtree(os.path.join(artist_dst, 'unmastered-impulses-master')) - expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + 2 # wait for artist & album artist_dst = os.path.join(music_path, 'Broke For Free') dest_path = os.path.join(artist_dst, 'Layers') @@ -302,7 +305,7 @@ def get_movie_path(name, year): with zipfile.ZipFile(zip_path, 'r') as handle: handle.extractall(dest_path) - expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + 2 # wait for artist & album print('Finished with Music...') sections.append(dict(name='Music', type='artist', location='/data/Music', agent='com.plexapp.agents.lastfm', From 1e294bf0da928f1533901369b04b585dae64e45c Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 20:03:29 +0100 Subject: [PATCH 31/54] fix misplaced TEST_ACCOUNT_ONCE --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 89f6914bb..f64742f41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,7 @@ jobs: - python: 3.6 env: - PLEX_CONTAINER_TAG=1.3.2.3112-1751929 + - TEST_ACCOUNT_ONCE=1 - python: 3.6 name: "Flake8" install: @@ -56,7 +57,6 @@ jobs: after_script: true env: - PLEX_CONTAINER_TAG=latest - - TEST_ACCOUNT_ONCE=1 - stage: deploy name: "Deploy to PyPi" python: 3.6 From 604325d28e12a4b51e8a5daa51fdd67b3409d25e Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 20:25:36 +0100 Subject: [PATCH 32/54] fix test_myplex_users, not sure if in proper-way --- tests/test_myplex.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_myplex.py b/tests/test_myplex.py index fe9435c5a..9140eca5b 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -71,13 +71,14 @@ def _test_myplex_connect_to_device(account): def test_myplex_users(account): users = account.users() - assert users, 'Found no users on account: %s' % account.name + if not len(users): + return pytest.skip('You have to add a shared account into your MyPlex') print('Found %s users.' % len(users)) user = account.user(users[0].title) print('Found user: %s' % user) assert user, 'Could not find user %s' % users[0].title - assert len(users[0].servers[0].sections()) == 10, "Could'nt info about the shared libraries" + assert len(users[0].servers[0].sections()) > 0, "Couldn't info about the shared libraries" def test_myplex_resource(account, plex): From 580e4c95a758c92329d757eb2f3fc3bf44b26f09 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 23:27:25 +0100 Subject: [PATCH 33/54] add pytest-rerunfailures; mark test_myplex_optout as flaky --- .travis.yml | 4 ++-- requirements_dev.txt | 1 + tests/test_myplex.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f64742f41..be3d25ab2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,9 +31,9 @@ install: --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG script: - - py.test tests -rxXs --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi + - py.test tests -rxXsaR --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi - PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1 - PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXs --tb=native --verbose --cov-config .coveragerc + PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXsaR --tb=native --verbose --cov-config .coveragerc --cov=plexapi --cov-append after_success: diff --git a/requirements_dev.txt b/requirements_dev.txt index 3fb657cc3..e2ebd4e20 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,6 +9,7 @@ pytest pytest-cache pytest-cov pytest-mock +pytest-rerunfailures recommonmark requests sphinx diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 9140eca5b..4e88643ff 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -109,6 +109,7 @@ def test_myplex_deletewebhooks(account): account.deleteWebhook('http://example.com') +@pytest.mark.flaky(reruns=5, reruns_delay=2) def test_myplex_optout(account_once): def enabled(): ele = account_once.query('https://plex.tv/api/v2/user/privacy') From 18a3cb18b8c417c779133f2eececb2d97c2a3f69 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Sun, 9 Sep 2018 23:27:37 +0100 Subject: [PATCH 34/54] fix comment --- tests/test_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sync.py b/tests/test_sync.py index 76cf1f44e..c39ad00a0 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -227,7 +227,7 @@ def test_sync_entire_library_photos(clear_sync_device, photos): new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photos._server.refreshSync() item = ensure_sync_item(clear_sync_device, new_item) - # It's not that easy, to just get all the photos within the library, so let`s query for photos with resolution!=0x0 + # It's not that easy, to just get all the photos within the library, so let`s query for photos with device!=0x0 section_content = photos.search(libtype='photo', **{'device!': '0x0'}) media_list = item.getMedia() assert len(section_content) == len(media_list) From 18e31cd75ef33f765fc980c2fbf35e4ceff50a4a Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 21:17:40 +0100 Subject: [PATCH 35/54] Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. --- .travis.yml | 4 ++-- requirements_dev.txt | 1 - tests/test_myplex.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index be3d25ab2..f64742f41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,9 +31,9 @@ install: --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG script: - - py.test tests -rxXsaR --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi + - py.test tests -rxXs --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi - PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1 - PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXsaR --tb=native --verbose --cov-config .coveragerc + PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXs --tb=native --verbose --cov-config .coveragerc --cov=plexapi --cov-append after_success: diff --git a/requirements_dev.txt b/requirements_dev.txt index e2ebd4e20..3fb657cc3 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,7 +9,6 @@ pytest pytest-cache pytest-cov pytest-mock -pytest-rerunfailures recommonmark requests sphinx diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 4e88643ff..9140eca5b 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -109,7 +109,6 @@ def test_myplex_deletewebhooks(account): account.deleteWebhook('http://example.com') -@pytest.mark.flaky(reruns=5, reruns_delay=2) def test_myplex_optout(account_once): def enabled(): ele = account_once.query('https://plex.tv/api/v2/user/privacy') From 987401b300582ae500cf31f953aeaab1ae668956 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 21:17:56 +0100 Subject: [PATCH 36/54] restart plex container on failure --- tools/plex-bootstraptest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 3637cf237..4a4c683ea 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -21,6 +21,7 @@ DOCKER_CMD = [ 'docker', 'run', '-d', '--name', 'plex-test-%(image_tag)s', + '--restart', 'on-failure', '-p', '32400:32400/tcp', '-p', '3005:3005/tcp', '-p', '8324:8324/tcp', From 00877648494395d3f30710ac441f1bb376fbe0a2 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 21:39:36 +0100 Subject: [PATCH 37/54] add conftest.wait_until() and used where some retries are required --- tests/conftest.py | 13 ++++++ tests/test_myplex.py | 10 +---- tests/test_server.py | 5 +-- tests/test_sync.py | 104 ++++++++++++++++++++++--------------------- 4 files changed, 69 insertions(+), 63 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d4159dce2..57b87f7a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ # Broke For Free - Layers - http://freemusicarchive.org/music/broke_for_free/Layers/ # 4. A Photos section containing the photoalbums: # Cats (with cute cat photos inside) +import time from datetime import datetime from functools import partial from os import environ @@ -280,3 +281,15 @@ def is_string(value, gte=1): 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) + while not ready and time.time() - start < timeout: + time.sleep(delay) + ready = condition_function(*args, **kwargs) + + assert ready, 'Wait timeout' + + return ready diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 9140eca5b..6cf890794 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -116,16 +116,10 @@ def enabled(): play = ele.attrib.get('optOutPlayback') return bool(int(lib)), bool(int(play)) - # This should be False False - library_enabled, playback_enabled = enabled() - account_once.optOut(library=True, playback=True) - - assert all(enabled()) - + utils.wait_until(lambda: enabled() == (True, True)) account_once.optOut(library=False, playback=False) - - assert not all(enabled()) + utils.wait_until(lambda: enabled() == (False, False)) def test_myplex_inviteFriend_remove(account, plex, mocker): diff --git a/tests/test_server.py b/tests/test_server.py index 94a62fcb9..38a28212e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -30,10 +30,7 @@ def test_server_alert_listener(plex, movies): messages = [] listener = plex.startAlertListener(messages.append) movies.refresh() - starttime, runtime = time.time(), 0 - while len(messages) < 3 and runtime <= 30: - time.sleep(1) - runtime = int(time.time() - starttime) + utils.wait_until(lambda: len(messages) >= 3, delay=1, timeout=30) assert len(messages) >= 3 finally: listener.stop() diff --git a/tests/test_sync.py b/tests/test_sync.py index c39ad00a0..ad3672235 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,37 +1,17 @@ -from time import sleep, time - -import pytest +from . import conftest as utils from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p, AUDIO_BITRATE_192_KBPS, PHOTO_QUALITY_MEDIUM -def ensure_sync_item(device, sync_item, timeout=3): - start = time() - while time() - start < timeout: - sync_list = device.syncItems() - for item in sync_list.items: - if item.id == sync_item.id: - return item - sleep(0.5) - - assert False, 'Failed to ensure that required sync_item is exist' - - -def ensure_sync_item_missing(device, sync_item, timeout=3): - start = time() - ret = None - while time() - start < timeout: - sync_list = device.syncItems() - for item in sync_list.items: - if item.id == sync_item.id: - ret = item +def get_sync_item_from_server(device, sync_item): + sync_list = device.syncItems() + for item in sync_list.items: + if item.id == sync_item.id: + return item - if ret: - sleep(0.5) - else: - break - assert not ret, 'Failed to ensure that required sync_item is missing' +def is_sync_item_missing(device, sync_item): + return not get_sync_item_from_server(device, sync_item) def test_current_device_got_sync_target(clear_sync_device): @@ -41,7 +21,8 @@ def test_current_device_got_sync_target(clear_sync_device): def test_add_movie_to_sync(clear_sync_device, movie): new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) movie._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(media_list) == 1 assert media_list[0].ratingKey == movie.ratingKey @@ -50,17 +31,19 @@ def test_add_movie_to_sync(clear_sync_device, movie): def test_delete_sync_item(clear_sync_device, movie): new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) movie._server.refreshSync() - new_item_in_myplex = ensure_sync_item(clear_sync_device, new_item) + new_item_in_myplex = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) sync_items = clear_sync_device.syncItems() for item in sync_items.items: item.delete() - ensure_sync_item_missing(clear_sync_device, new_item_in_myplex) + utils.wait_until(is_sync_item_missing, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item_in_myplex) def test_add_show_to_sync(clear_sync_device, show): new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) show._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = show.episodes() media_list = item.getMedia() assert len(episodes) == len(media_list) @@ -71,7 +54,8 @@ def test_add_season_to_sync(clear_sync_device, show): season = show.season('Season 1') new_item = season.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) season._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = season.episodes() media_list = item.getMedia() assert len(episodes) == len(media_list) @@ -81,7 +65,8 @@ def test_add_season_to_sync(clear_sync_device, show): def test_add_episode_to_sync(clear_sync_device, episode): new_item = episode.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) episode._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert 1 == len(media_list) assert episode.ratingKey == media_list[0].ratingKey @@ -91,7 +76,8 @@ def test_limited_watched(clear_sync_device, show): show.markUnwatched() new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=False) show._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = show.episodes()[:5] media_list = item.getMedia() assert 5 == len(media_list) @@ -107,7 +93,8 @@ def test_limited_unwatched(clear_sync_device, show): show.markUnwatched() new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=True) show._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = show.episodes(viewCount=0)[:5] media_list = item.getMedia() assert len(episodes) == len(media_list) @@ -124,7 +111,8 @@ def test_unlimited_and_watched(clear_sync_device, show): show.markUnwatched() new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=False) show._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = show.episodes() media_list = item.getMedia() assert len(episodes) == len(media_list) @@ -141,7 +129,8 @@ def test_unlimited_and_unwatched(clear_sync_device, show): show.markUnwatched() new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=True) show._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) episodes = show.episodes(viewCount=0) media_list = item.getMedia() assert len(episodes) == len(media_list) @@ -157,7 +146,8 @@ def test_unlimited_and_unwatched(clear_sync_device, show): def test_add_music_artist_to_sync(clear_sync_device, artist): new_item = artist.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) artist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) tracks = artist.tracks() media_list = item.getMedia() assert len(tracks) == len(media_list) @@ -167,7 +157,8 @@ def test_add_music_artist_to_sync(clear_sync_device, artist): def test_add_music_album_to_sync(clear_sync_device, album): new_item = album.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) album._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) tracks = album.tracks() media_list = item.getMedia() assert len(tracks) == len(media_list) @@ -177,7 +168,8 @@ def test_add_music_album_to_sync(clear_sync_device, album): def test_add_music_track_to_sync(clear_sync_device, track): new_item = track.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) track._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert 1 == len(media_list) assert track.ratingKey == media_list[0].ratingKey @@ -187,7 +179,8 @@ def test_add_photo_to_sync(clear_sync_device, photoalbum): photo = photoalbum.photo('photo1') new_item = photo.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photo._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert 1 == len(media_list) assert photo.ratingKey == media_list[0].ratingKey @@ -196,7 +189,8 @@ def test_add_photo_to_sync(clear_sync_device, photoalbum): def test_sync_entire_library_movies(clear_sync_device, movies): new_item = movies.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) movies._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) section_content = movies.all() media_list = item.getMedia() assert len(section_content) == len(media_list) @@ -206,7 +200,8 @@ def test_sync_entire_library_movies(clear_sync_device, movies): def test_sync_entire_library_tvshows(clear_sync_device, tvshows): new_item = tvshows.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) tvshows._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) section_content = tvshows.searchEpisodes() media_list = item.getMedia() assert len(section_content) == len(media_list) @@ -216,7 +211,8 @@ def test_sync_entire_library_tvshows(clear_sync_device, tvshows): def test_sync_entire_library_music(clear_sync_device, music): new_item = music.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) music._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) section_content = music.searchTracks() media_list = item.getMedia() assert len(section_content) == len(media_list) @@ -226,7 +222,8 @@ def test_sync_entire_library_music(clear_sync_device, music): def test_sync_entire_library_photos(clear_sync_device, photos): new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) photos._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) # It's not that easy, to just get all the photos within the library, so let`s query for photos with device!=0x0 section_content = photos.search(libtype='photo', **{'device!': '0x0'}) media_list = item.getMedia() @@ -239,7 +236,8 @@ def test_playlist_movie_sync(plex, clear_sync_device, movies): playlist = plex.createPlaylist('Sync: Movies', items) new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) playlist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] @@ -251,7 +249,8 @@ def test_playlist_tvshow_sync(plex, clear_sync_device, show): playlist = plex.createPlaylist('Sync: TV Show', items) new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) playlist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] @@ -263,7 +262,8 @@ def test_playlist_mixed_sync(plex, clear_sync_device, movie, episode): playlist = plex.createPlaylist('Sync: Mixed', items) new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) playlist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] @@ -275,7 +275,8 @@ def test_playlist_music_sync(plex, clear_sync_device, artist): playlist = plex.createPlaylist('Sync: Music', items) new_item = playlist.sync(audioBitrate=AUDIO_BITRATE_192_KBPS, client=clear_sync_device) playlist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] @@ -287,7 +288,8 @@ def test_playlist_photos_sync(plex, clear_sync_device, photoalbum): playlist = plex.createPlaylist('Sync: Photos', items) new_item = playlist.sync(photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device) playlist._server.refreshSync() - item = ensure_sync_item(clear_sync_device, new_item) + item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, + sync_item=new_item) media_list = item.getMedia() assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] From e3d270752b014874d49e0141369ca9a3c2c27123 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 22:12:00 +0100 Subject: [PATCH 38/54] add more wait_until() usage in test_sync --- tests/conftest.py | 4 ++- tests/test_sync.py | 62 +++++++++++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 57b87f7a4..a9c432362 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -286,10 +286,12 @@ def is_thumb(key): 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' + assert ready, 'Wait timeout after %d retries, %.2f seconds' % (retries, time.time() - start) return ready diff --git a/tests/test_sync.py b/tests/test_sync.py index ad3672235..ec9d7f8b2 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,3 +1,4 @@ +from plexapi.exceptions import BadRequest from . import conftest as utils from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p, AUDIO_BITRATE_192_KBPS, PHOTO_QUALITY_MEDIUM @@ -18,12 +19,23 @@ def test_current_device_got_sync_target(clear_sync_device): assert 'sync-target' in clear_sync_device.provides +def get_media(item, server): + try: + return item.getMedia() + except BadRequest as e: + if 'not_found' in str(e): + server.refreshSync() + return None + else: + raise + + def test_add_movie_to_sync(clear_sync_device, movie): new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) movie._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=movie._server) assert len(media_list) == 1 assert media_list[0].ratingKey == movie.ratingKey @@ -45,7 +57,7 @@ def test_add_show_to_sync(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = show.episodes() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -57,7 +69,7 @@ def test_add_season_to_sync(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = season.episodes() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=season._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -67,7 +79,7 @@ def test_add_episode_to_sync(clear_sync_device, episode): episode._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=episode._server) assert 1 == len(media_list) assert episode.ratingKey == media_list[0].ratingKey @@ -79,12 +91,12 @@ def test_limited_watched(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = show.episodes()[:5] - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert 5 == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] episodes[0].markWatched() show._server.refreshSync() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert 5 == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -96,13 +108,13 @@ def test_limited_unwatched(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = show.episodes(viewCount=0)[:5] - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] episodes[0].markWatched() show._server.refreshSync() episodes = show.episodes(viewCount=0)[:5] - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -114,13 +126,13 @@ def test_unlimited_and_watched(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = show.episodes() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] episodes[0].markWatched() show._server.refreshSync() episodes = show.episodes() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -132,13 +144,13 @@ def test_unlimited_and_unwatched(clear_sync_device, show): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) episodes = show.episodes(viewCount=0) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] episodes[0].markWatched() show._server.refreshSync() episodes = show.episodes(viewCount=0) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] @@ -149,7 +161,7 @@ def test_add_music_artist_to_sync(clear_sync_device, artist): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) tracks = artist.tracks() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=artist._server) assert len(tracks) == len(media_list) assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list] @@ -160,7 +172,7 @@ def test_add_music_album_to_sync(clear_sync_device, album): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) tracks = album.tracks() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=album._server) assert len(tracks) == len(media_list) assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list] @@ -170,7 +182,7 @@ def test_add_music_track_to_sync(clear_sync_device, track): track._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=track._server) assert 1 == len(media_list) assert track.ratingKey == media_list[0].ratingKey @@ -181,7 +193,7 @@ def test_add_photo_to_sync(clear_sync_device, photoalbum): photo._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=photo._server) assert 1 == len(media_list) assert photo.ratingKey == media_list[0].ratingKey @@ -192,7 +204,7 @@ def test_sync_entire_library_movies(clear_sync_device, movies): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) section_content = movies.all() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=movies._server) assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] @@ -203,7 +215,7 @@ def test_sync_entire_library_tvshows(clear_sync_device, tvshows): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) section_content = tvshows.searchEpisodes() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=tvshows._server) assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] @@ -214,7 +226,7 @@ def test_sync_entire_library_music(clear_sync_device, music): item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) section_content = music.searchTracks() - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=music._server) assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] @@ -226,7 +238,7 @@ def test_sync_entire_library_photos(clear_sync_device, photos): sync_item=new_item) # It's not that easy, to just get all the photos within the library, so let`s query for photos with device!=0x0 section_content = photos.search(libtype='photo', **{'device!': '0x0'}) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=photos._server) assert len(section_content) == len(media_list) assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] @@ -238,7 +250,7 @@ def test_playlist_movie_sync(plex, clear_sync_device, movies): playlist._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] playlist.delete() @@ -251,7 +263,7 @@ def test_playlist_tvshow_sync(plex, clear_sync_device, show): playlist._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] playlist.delete() @@ -264,7 +276,7 @@ def test_playlist_mixed_sync(plex, clear_sync_device, movie, episode): playlist._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] playlist.delete() @@ -277,7 +289,7 @@ def test_playlist_music_sync(plex, clear_sync_device, artist): playlist._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] playlist.delete() @@ -290,7 +302,7 @@ def test_playlist_photos_sync(plex, clear_sync_device, photoalbum): playlist._server.refreshSync() item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item) - media_list = item.getMedia() + media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) assert len(items) == len(media_list) assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] playlist.delete() From 7d2f2b0afdf42267de074674db4c3d872d8184ec Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 22:58:15 +0100 Subject: [PATCH 39/54] fix managed user search --- tests/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a9c432362..0b509db6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -208,9 +208,12 @@ def photoalbum(photos): def shared_username(account): username = environ.get('SHARED_USERNAME', 'PKKid') for user in account.users(): - if user.username == username: + if user.title.lower() == username.lower(): return username - pytest.skip('Shared user wasn`t found') + 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() From 90b5477afecf2ed6eadec79b275e4e183baedab0 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:31:09 +0100 Subject: [PATCH 40/54] fix updating managed users in myplex --- plexapi/myplex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index b6ec6577c..9689d24fa 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -263,7 +263,7 @@ def updateFriend(self, user, server, sections=None, removeSections=False, allowS # Update friend servers response_filters = '' response_servers = '' - user = self.user(user.username if isinstance(user, MyPlexUser) else user) + user = user if isinstance(user, MyPlexUser) else self.user(user) machineId = server.machineIdentifier if isinstance(server, PlexServer) else server sectionIds = self._getSectionIds(machineId, sections) headers = {'Content-Type': 'application/json'} From 78aa09fd1a2972316d903538a8f506cbbabb29f9 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:31:39 +0100 Subject: [PATCH 41/54] allow to add new servers to existent users --- plexapi/myplex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 9689d24fa..0e2d377fd 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -278,7 +278,7 @@ def updateFriend(self, user, server, sections=None, removeSections=False, allowS "invited_id": user.id}} url = self.FRIENDINVITE.format(machineId=machineId) # Remove share sections, add shares to user without shares, or update shares - if sectionIds: + if not user_servers or sectionIds: if removeSections is True: response_servers = self.query(url, self._session.delete, json=params, headers=headers) elif 'invited_id' in params.get('shared_server', ''): From 20c4eff730bbac1422d82a40bfc2a0ca5a139bed Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:32:50 +0100 Subject: [PATCH 42/54] add new server to a shared user while bootstrapping --- tools/plex-bootstraptest.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 4a4c683ea..5f09603cb 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -1,4 +1,4 @@ -""" The script is used to create bootstrap a docker container with Plex and with all the libraries required for testing. +""" The script is used to bootstrap a docker container with Plex and with all the libraries required for testing. """ import argparse @@ -14,7 +14,7 @@ import plexapi from plexapi.compat import which, makedirs -from plexapi.exceptions import BadRequest +from plexapi.exceptions import BadRequest, NotFound from plexapi.myplex import MyPlexAccount from plexapi.utils import download, SEARCHTYPES @@ -344,6 +344,16 @@ def get_movie_path(name, year): for section in sections: create_section(server, section) + import logging + logging.basicConfig(level=logging.INFO) + shared_username = os.environ.get('SHARED_USERNAME', 'PKKid') + try: + user = account.user(shared_username) + account.updateFriend(user, server) + print('The server was shared with user "%s"' % shared_username) + except NotFound: + pass + print('Base URL is %s' % server.url('', False)) if opts.show_token: print('Auth token is %s' % account.authenticationToken) From 24ceba72c7912a421fb6b5195e8e97e43cb39c11 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:34:17 +0100 Subject: [PATCH 43/54] add some docs on testing process --- README.rst | 62 +++++++++++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 15 ------------ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 1ffddd624..c0862dac0 100644 --- a/README.rst +++ b/README.rst @@ -131,6 +131,68 @@ Usage Examples print(playlist.title) +Running tests over PlexAPI +-------------------------- + +In order to test the PlexAPI library you have to prepare a Plex Server instance with following libraries: + +1. Movies section (agent `com.plexapp.agents.imdb`) containing both movies: + * Sintel - https://durian.blender.org/ + * Elephants Dream - https://orange.blender.org/ + * Sita Sings the Blues - http://www.sitasingstheblues.com/ + * Big Buck Bunny - https://peach.blender.org/ +2. TV Show section (agent `com.plexapp.agents.thetvdb`) containing the shows: + * Game of Thrones (Season 1 and 2) + * The 100 (Seasons 1 and 2) + * (or symlink the above movies with proper names) +3. Music section (agent `com.plexapp.agents.lastfm`) containing the albums: + * Infinite State - Unmastered Impulses - https://github.com/kennethreitz/unmastered-impulses + * Broke For Free - Layers - http://freemusicarchive.org/music/broke_for_free/Layers/ +4. A Photos section (any agent) containing the photoalbums (photoalbum is just a folder on your disk): + * `Cats` + * Within `Cats` album you need to place 3 photos (cute cat photos, of course) + * Within `Cats` album you should place 3 more photoalbums (one of them should be named `Cats in bed`, + names of others doesn't matter) + * Within `Cats in bed` you need to place 7 photos + * Within other 2 albums you should place 1 photo in each + +Instead of manual creation of the library you could use a script `tools/plex-boostraptest.py` with appropriate +arguments and add this new server to a shared user which username is defined in environment veriable `SHARED_USERNAME`. +It uses `official docker image`_ to create a proper instance. + +Also in order to run most of the tests you have to provide some environment variables: + +* `PLEXAPI_AUTH_SERVER_BASEURL` containing an URL to your Plex instance, e.g. `http://127.0.0.1:32400` (without trailing + slash) +* `PLEXAPI_AUTH_MYPLEX_USERNAME` and `PLEXAPI_AUTH_MYPLEX_PASSWORD` with your MyPlex username and password accordingly + +After this step you can run tests with following command: + +.. code-block:: bash + + py.test tests -rxXs --ignore=tests/test_sync.py + +Some of the tests in main test-suite require a shared user in your account (e.g. `test_myplex_users`, +`test_myplex_updateFriend`, etc.), you need to provide a valid shared user's username to get them running you need to +provide the username of the shared user as an environment variable `SHARED_USERNAME`. You can enable a Guest account and +simply pass `Guest` as `SHARED_USERNAME` (or just create a user like `plexapitest` and play with it). + +To be able to run tests over Mobile Sync api you have to some some more environment variables, to following values +exactly: + +* PLEXAPI_HEADER_PROVIDES='controller,sync-target' +* PLEXAPI_HEADER_PLATFORM=iOS +* PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1 +* PLEXAPI_HEADER_DEVICE=iPhone + +And finally run the sync-related tests: + +.. code-block:: bash + + py.test tests/test_sync.py -rxXs + +.. _official docker image: https://hub.docker.com/r/plexinc/pms-docker/ + Common Questions ---------------- diff --git a/tests/conftest.py b/tests/conftest.py index 0b509db6b..e4f57f818 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,4 @@ # -*- coding: utf-8 -*- -# Running these tests requires a few things in your Plex Library. -# 1. Movies section containing both movies: -# * Sintel - https://durian.blender.org/ -# * Elephants Dream - https://orange.blender.org/ -# * Sita Sings the Blues - http://www.sitasingstheblues.com/ -# * Big Buck Bunny - https://peach.blender.org/ -# 2. TV Show section containing the shows: -# * Game of Thrones (Season 1 and 2) -# * The 100 (Seasons 1 and 2) -# * (or symlink the above movies with proper names) -# 3. Music section containing the albums: -# Infinite State - Unmastered Impulses - https://github.com/kennethreitz/unmastered-impulses -# Broke For Free - Layers - http://freemusicarchive.org/music/broke_for_free/Layers/ -# 4. A Photos section containing the photoalbums: -# Cats (with cute cat photos inside) import time from datetime import datetime from functools import partial From fcce22b60532db38493a716157775b8f01dde72a Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:42:12 +0100 Subject: [PATCH 44/54] perform few attemps when unable to get the claim token --- tools/plex-bootstraptest.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tools/plex-bootstraptest.py b/tools/plex-bootstraptest.py index 5f09603cb..0a654e581 100644 --- a/tools/plex-bootstraptest.py +++ b/tools/plex-bootstraptest.py @@ -51,12 +51,19 @@ def get_claim_token(myplex): Arguments: myplex (:class:`~plexapi.myplex.MyPlexAccount`) """ - response = myplex._session.get('https://plex.tv/api/claim/token.json', headers=myplex._headers(), - timeout=plexapi.TIMEOUT) - if response.status_code not in (200, 201, 204): - codename = codes.get(response.status_code)[0] + retry = 0 + status_code = None + while retry < 3 and status_code not in (200, 201, 204): + if retry > 0: + sleep(2) + response = myplex._session.get('https://plex.tv/api/claim/token.json', headers=myplex._headers(), + timeout=plexapi.TIMEOUT) + status_code = response.status_code + retry += 1 + + if status_code not in (200, 201, 204): errtext = response.text.replace('\n', ' ') - raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) + raise BadRequest('(%s) unable to get status code %s; %s' % (response.status_code, response.url, errtext)) return response.json()['token'] From 0d536bd06dbdc4a4b869a1686f8cd008898859fe Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:45:20 +0100 Subject: [PATCH 45/54] unlock websocket-client in requirements_dev --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 3fb657cc3..8007a1ca0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -15,4 +15,4 @@ sphinx sphinx-rtd-theme sphinxcontrib-napoleon tqdm -websocket-client==0.48.0 +websocket-client From 99a3bb1ceef4b90e6bf19f6146db26c53d384dfb Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:51:33 +0100 Subject: [PATCH 46/54] fix docblock in tools/plex-teardowntest --- tools/plex-teardowntest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/plex-teardowntest.py b/tools/plex-teardowntest.py index e63c40610..a1e383f95 100644 --- a/tools/plex-teardowntest.py +++ b/tools/plex-teardowntest.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Listen to plex alerts and print them to the console. -Because we're using print as a function, example only works in Python3. +Remove current Plex Server and a Client from MyPlex account. Useful when running tests in CI. """ from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer From ae930936eb6e70787ebddddcb5c1f54e0f444282 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:53:48 +0100 Subject: [PATCH 47/54] do not hardcode mediapart size in test_video --- tests/test_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_video.py b/tests/test_video.py index eb4b630ed..0268b791a 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -503,7 +503,7 @@ def test_video_Episode_attrs(episode): assert utils.is_metadata(part._initpath) assert len(part.key) >= 10 assert part._server._baseurl == utils.SERVER_BASEURL - assert part.size == 18184197 + assert utils.is_int(part.size, gte=18184197) def test_video_Season(show): From 1df20d2450fad8287c0413d718d1f4cdf0a7f929 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 10 Sep 2018 23:59:29 +0100 Subject: [PATCH 48/54] remove cache:pip from travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f64742f41..46e1caab0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ stages: - name: deploy if: tag IS present -cache: pip sudo: required services: - docker From bde68d05726ead4227465abfc5fc5c92f2e8cbc1 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 00:06:41 +0100 Subject: [PATCH 49/54] Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 8007a1ca0..3fb657cc3 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -15,4 +15,4 @@ sphinx sphinx-rtd-theme sphinxcontrib-napoleon tqdm -websocket-client +websocket-client==0.48.0 From ada447aa5f38c703fb6aabde97ab39987494a4ca Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 00:08:37 +0100 Subject: [PATCH 50/54] remove debug from server.py --- plexapi/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plexapi/server.py b/plexapi/server.py index 7be355dd8..93510394e 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -299,7 +299,6 @@ def check_for_update(self, force=True, download=False): def isLatest(self): """ Check if the installed version of PMS is the latest. """ release = self.check_for_update(force=True) - print(release) return release is None def installUpdate(self): From af069e50487f7ad629afaa0ccd2a5487fc7c7513 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 07:33:04 +0100 Subject: [PATCH 51/54] improve webhook tests --- tests/test_myplex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 6cf890794..6e0fc7cd8 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -87,7 +87,7 @@ def test_myplex_resource(account, plex): def test_myplex_webhooks(account): if account.subscriptionActive: - assert not account.webhooks() + assert type(account.webhooks()) is list else: with pytest.raises(BadRequest): account.webhooks() @@ -95,7 +95,7 @@ def test_myplex_webhooks(account): def test_myplex_addwebhooks(account): if account.subscriptionActive: - assert len(account.addWebhook('http://example.com')) == 1 + assert 'http://example.com' in account.addWebhook('http://example.com') else: with pytest.raises(BadRequest): account.addWebhook('http://example.com') @@ -103,7 +103,7 @@ def test_myplex_addwebhooks(account): def test_myplex_deletewebhooks(account): if account.subscriptionActive: - assert not account.deleteWebhook('http://example.com') + assert 'http://example.com' not in account.deleteWebhook('http://example.com') else: with pytest.raises(BadRequest): account.deleteWebhook('http://example.com') From 8b0430aa0e22a20dd3fabbac883c0bb137a14e9d Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 07:52:48 +0100 Subject: [PATCH 52/54] fix type() check to isinstance() --- tests/test_myplex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_myplex.py b/tests/test_myplex.py index 6e0fc7cd8..7bf34f13a 100644 --- a/tests/test_myplex.py +++ b/tests/test_myplex.py @@ -87,7 +87,7 @@ def test_myplex_resource(account, plex): def test_myplex_webhooks(account): if account.subscriptionActive: - assert type(account.webhooks()) is list + assert isinstance(account.webhooks(), list) else: with pytest.raises(BadRequest): account.webhooks() From 6a9584557e0960a7b593f2dc612c6840fd69c007 Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 08:54:05 +0100 Subject: [PATCH 53/54] remove excessive `else` branch due to Hellowlol advice --- plexapi/server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plexapi/server.py b/plexapi/server.py index 93510394e..ee4ea37f9 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -293,8 +293,6 @@ def check_for_update(self, force=True, download=False): releases = self.fetchItems('/updater/status') if len(releases): return releases[0] - else: - return None def isLatest(self): """ Check if the installed version of PMS is the latest. """ From 415e3a47f5969a3b2c7753bb3aea23c828cf821d Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Tue, 11 Sep 2018 09:09:59 +0100 Subject: [PATCH 54/54] add `unknown` as allowed `myPlexMappingState` in test_server --- tests/test_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 38a28212e..5ac4cf03f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -13,7 +13,8 @@ def test_server_attr(plex, account): assert len(plex.friendlyName) >= 1 assert len(plex.machineIdentifier) == 40 assert plex.myPlex is True - assert plex.myPlexMappingState == 'mapped' + # if you run the tests very shortly after server creation the state in rare cases may be `unknown` + assert plex.myPlexMappingState in ('mapped', 'unknown') assert plex.myPlexSigninState == 'ok' assert utils.is_int(plex.myPlexSubscription, gte=0) assert re.match(utils.REGEX_EMAIL, plex.myPlexUsername)