Skip to content

Commit 68fc970

Browse files
andrey-yantsenHellowlol
authored andcommitted
Improvements in tests process (#297)
* lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c9. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
1 parent afd4e24 commit 68fc970

23 files changed

+607
-344
lines changed

.travis.yml

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,72 @@
1-
language:
2-
- python
1+
language: python
2+
3+
stages:
4+
- test
5+
- name: deploy
6+
if: tag IS present
7+
8+
sudo: required
9+
services:
10+
- docker
11+
312
python:
4-
- '2.7'
5-
- '3.4'
6-
- '3.6'
13+
- 2.7
14+
- 3.4
15+
- 3.6
16+
17+
env:
18+
global:
19+
- PLEXAPI_AUTH_SERVER_BASEURL=http://127.0.0.1:32400
20+
matrix:
21+
- PLEX_CONTAINER_TAG=latest
22+
723
before_install:
8-
- pip install --upgrade pip
9-
- pip install --upgrade setuptools
10-
- pip install --upgrade pytest pytest-cov coveralls
24+
- pip install --upgrade pip
25+
- pip install --upgrade setuptools
26+
- pip install --upgrade pytest pytest-cov coveralls
1127
install:
12-
- pip install -r requirements_dev.txt
28+
- pip install -r requirements_dev.txt
29+
- PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1
30+
--bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG
31+
1332
script:
14-
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then py.test tests --tb=native --verbose
15-
--cov-config .coveragerc --cov=plexapi; fi
16-
- flake8 plexapi --exclude=compat.py --max-line-length=120 --ignore=E128,E701,E702,E731,W293
33+
- py.test tests -rxXs --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi
34+
- PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1
35+
PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXs --tb=native --verbose --cov-config .coveragerc
36+
--cov=plexapi --cov-append
37+
1738
after_success:
18-
- coveralls
19-
matrix:
20-
fast_finish: true
21-
deploy:
22-
provider: pypi
23-
user: mjs7231
24-
password:
25-
secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c=
26-
on:
27-
tags: true
39+
- coveralls
40+
41+
after_script:
42+
- PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py
43+
44+
jobs:
45+
include:
46+
- python: 3.6
47+
env:
48+
- PLEX_CONTAINER_TAG=1.3.2.3112-1751929
49+
- TEST_ACCOUNT_ONCE=1
50+
- python: 3.6
51+
name: "Flake8"
52+
install:
53+
- pip install -r requirements_dev.txt
54+
script: flake8 plexapi --exclude=compat.py --max-line-length=120 --ignore=E128,E701,E702,E731,W293
55+
after_success: true
56+
after_script: true
57+
env:
58+
- PLEX_CONTAINER_TAG=latest
59+
- stage: deploy
60+
name: "Deploy to PyPi"
61+
python: 3.6
62+
install: true
63+
script: true
64+
env:
65+
- PLEX_CONTAINER_TAG=latest
66+
deploy:
67+
provider: pypi
68+
user: mjs7231
69+
password:
70+
secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c=
71+
on:
72+
tags: true

README.rst

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,68 @@ Usage Examples
131131
print(playlist.title)
132132
133133
134+
Running tests over PlexAPI
135+
--------------------------
136+
137+
In order to test the PlexAPI library you have to prepare a Plex Server instance with following libraries:
138+
139+
1. Movies section (agent `com.plexapp.agents.imdb`) containing both movies:
140+
* Sintel - https://durian.blender.org/
141+
* Elephants Dream - https://orange.blender.org/
142+
* Sita Sings the Blues - http://www.sitasingstheblues.com/
143+
* Big Buck Bunny - https://peach.blender.org/
144+
2. TV Show section (agent `com.plexapp.agents.thetvdb`) containing the shows:
145+
* Game of Thrones (Season 1 and 2)
146+
* The 100 (Seasons 1 and 2)
147+
* (or symlink the above movies with proper names)
148+
3. Music section (agent `com.plexapp.agents.lastfm`) containing the albums:
149+
* Infinite State - Unmastered Impulses - https://github.com/kennethreitz/unmastered-impulses
150+
* Broke For Free - Layers - http://freemusicarchive.org/music/broke_for_free/Layers/
151+
4. A Photos section (any agent) containing the photoalbums (photoalbum is just a folder on your disk):
152+
* `Cats`
153+
* Within `Cats` album you need to place 3 photos (cute cat photos, of course)
154+
* Within `Cats` album you should place 3 more photoalbums (one of them should be named `Cats in bed`,
155+
names of others doesn't matter)
156+
* Within `Cats in bed` you need to place 7 photos
157+
* Within other 2 albums you should place 1 photo in each
158+
159+
Instead of manual creation of the library you could use a script `tools/plex-boostraptest.py` with appropriate
160+
arguments and add this new server to a shared user which username is defined in environment veriable `SHARED_USERNAME`.
161+
It uses `official docker image`_ to create a proper instance.
162+
163+
Also in order to run most of the tests you have to provide some environment variables:
164+
165+
* `PLEXAPI_AUTH_SERVER_BASEURL` containing an URL to your Plex instance, e.g. `http://127.0.0.1:32400` (without trailing
166+
slash)
167+
* `PLEXAPI_AUTH_MYPLEX_USERNAME` and `PLEXAPI_AUTH_MYPLEX_PASSWORD` with your MyPlex username and password accordingly
168+
169+
After this step you can run tests with following command:
170+
171+
.. code-block:: bash
172+
173+
py.test tests -rxXs --ignore=tests/test_sync.py
174+
175+
Some of the tests in main test-suite require a shared user in your account (e.g. `test_myplex_users`,
176+
`test_myplex_updateFriend`, etc.), you need to provide a valid shared user's username to get them running you need to
177+
provide the username of the shared user as an environment variable `SHARED_USERNAME`. You can enable a Guest account and
178+
simply pass `Guest` as `SHARED_USERNAME` (or just create a user like `plexapitest` and play with it).
179+
180+
To be able to run tests over Mobile Sync api you have to some some more environment variables, to following values
181+
exactly:
182+
183+
* PLEXAPI_HEADER_PROVIDES='controller,sync-target'
184+
* PLEXAPI_HEADER_PLATFORM=iOS
185+
* PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1
186+
* PLEXAPI_HEADER_DEVICE=iPhone
187+
188+
And finally run the sync-related tests:
189+
190+
.. code-block:: bash
191+
192+
py.test tests/test_sync.py -rxXs
193+
194+
.. _official docker image: https://hub.docker.com/r/plexinc/pms-docker/
195+
134196
Common Questions
135197
----------------
136198

plexapi/alert.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ class AlertListener(threading.Thread):
1212
alerts you must call .start() on the object once it's created. When calling
1313
`PlexServer.startAlertListener()`, the thread will be started for you.
1414
15+
Known `state`-values for timeline entries, with identifier=`com.plexapp.plugins.library`:
16+
17+
:0: The item was created
18+
:1: Reporting progress on item processing
19+
:2: Matching the item
20+
:3: Downloading the metadata
21+
:4: Processing downloaded metadata
22+
:5: The item processed
23+
:9: The item deleted
24+
25+
When metadata agent is not set for the library processing ends with state=1.
26+
1527
Parameters:
1628
server (:class:`~plexapi.server.PlexServer`): PlexServer this listener is connected to.
1729
callback (func): Callback function to call on recieved messages. The callback function

plexapi/library.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,8 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg
470470
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
471471
album, track; optional).
472472
**kwargs (dict): Any of the available filters for the current library section. Partial string
473-
matches allowed. Multiple matches OR together. All inputs will be compared with the
474-
available options and a warning logged if the option does not appear valid.
473+
matches allowed. Multiple matches OR together. Negative filtering also possible, just add an
474+
exclamation mark to the end of filter name, e.g. `resolution!=1x1`.
475475
476476
* unwatched: Display or hide unwatched content (True, False). [all]
477477
* 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
486486
* resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
487487
* studio: List of studios to search within ([studio_or_key, ...]). [music]
488488
* year: List of years to search within ([yyyy, ...]). [all]
489+
490+
Raises:
491+
:class:`plexapi.exceptions.BadRequest`: when applying unknown filter
489492
"""
490493
# cleanup the core arguments
491494
args = {}
@@ -510,7 +513,10 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg
510513

511514
def _cleanSearchFilter(self, category, value, libtype=None):
512515
# check a few things before we begin
513-
if category not in self.ALLOWED_FILTERS:
516+
if category.endswith('!'):
517+
if category[:-1] not in self.ALLOWED_FILTERS:
518+
raise BadRequest('Unknown filter category: %s' % category[:-1])
519+
elif category not in self.ALLOWED_FILTERS:
514520
raise BadRequest('Unknown filter category: %s' % category)
515521
if category in self.BOOLEAN_FILTERS:
516522
return '1' if value else '0'
@@ -839,12 +845,12 @@ class PhotoSection(LibrarySection):
839845
840846
Attributes:
841847
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('all', 'iso',
842-
'make', 'lens', 'aperture', 'exposure')
848+
'make', 'lens', 'aperture', 'exposure', 'device', 'resolution')
843849
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt')
844850
TAG (str): 'Directory'
845851
TYPE (str): 'photo'
846852
"""
847-
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
853+
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure', 'device', 'resolution')
848854
ALLOWED_SORT = ('addedAt',)
849855
TAG = 'Directory'
850856
TYPE = 'photo'
@@ -853,13 +859,11 @@ class PhotoSection(LibrarySection):
853859

854860
def searchAlbums(self, title, **kwargs):
855861
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
856-
key = '/library/sections/%s/all?type=14' % self.key
857-
return self.fetchItems(key, title=title)
862+
return self.search(libtype='photoalbum', title=title, **kwargs)
858863

859864
def searchPhotos(self, title, **kwargs):
860865
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
861-
key = '/library/sections/%s/all?type=13' % self.key
862-
return self.fetchItems(key, title=title)
866+
return self.search(libtype='photo', title=title, **kwargs)
863867

864868
def sync(self, resolution, limit=None, **kwargs):
865869
""" Add current Music library section as sync item for specified device.

plexapi/myplex.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def updateFriend(self, user, server, sections=None, removeSections=False, allowS
263263
# Update friend servers
264264
response_filters = ''
265265
response_servers = ''
266-
user = self.user(user.username if isinstance(user, MyPlexUser) else user)
266+
user = user if isinstance(user, MyPlexUser) else self.user(user)
267267
machineId = server.machineIdentifier if isinstance(server, PlexServer) else server
268268
sectionIds = self._getSectionIds(machineId, sections)
269269
headers = {'Content-Type': 'application/json'}
@@ -278,7 +278,7 @@ def updateFriend(self, user, server, sections=None, removeSections=False, allowS
278278
"invited_id": user.id}}
279279
url = self.FRIENDINVITE.format(machineId=machineId)
280280
# Remove share sections, add shares to user without shares, or update shares
281-
if sectionIds:
281+
if not user_servers or sectionIds:
282282
if removeSections is True:
283283
response_servers = self.query(url, self._session.delete, json=params, headers=headers)
284284
elif 'invited_id' in params.get('shared_server', ''):
@@ -376,7 +376,8 @@ def deleteWebhook(self, url):
376376

377377
def setWebhooks(self, urls):
378378
log.info('Setting webhooks: %s' % urls)
379-
data = self.query(self.WEBHOOKS, self._session.post, data={'urls[]': urls})
379+
data = {'urls[]': urls} if len(urls) else {'urls': ''}
380+
data = self.query(self.WEBHOOKS, self._session.post, data=data)
380381
self._webhooks = self.listAttrs(data, 'url', etag='webhook')
381382
return self._webhooks
382383

@@ -395,7 +396,7 @@ def optOut(self, playback=None, library=None):
395396
if library is not None:
396397
params['optOutLibraryStats'] = int(library)
397398
url = 'https://plex.tv/api/v2/user/privacy'
398-
return self.query(url, method=self._session.put, params=params)
399+
return self.query(url, method=self._session.put, data=params)
399400

400401
def syncItems(self, client=None, clientId=None):
401402
""" Returns an instance of :class:`plexapi.sync.SyncList` for specified client.

plexapi/photo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
154154
sync_item.metadataType = self.METADATA_TYPE
155155
sync_item.machineIdentifier = self._server.machineIdentifier
156156

157-
section = self._server.library.sectionByID(self.librarySectionID)
157+
section = self.section()
158158

159159
sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key))
160160
sync_item.policy = Policy.create(limit)

plexapi/playlist.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, clien
168168
:mod:`plexapi.sync` module. Used only when playlist contains video.
169169
photoResolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in
170170
the module :mod:`plexapi.sync`. Used only when playlist contains photos.
171-
audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
172-
module :mod:`plexapi.sync`. Used only when playlist contains audio.
171+
audioBitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values
172+
from the module :mod:`plexapi.sync`. Used only when playlist contains audio.
173173
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
174174
:func:`plexapi.myplex.MyPlexAccount.sync`.
175175
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.

plexapi/server.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,14 @@ def check_for_update(self, force=True, download=False):
290290
part = '/updater/check?download=%s' % (1 if download else 0)
291291
if force:
292292
self.query(part, method=self._session.put)
293-
return self.fetchItem('/updater/status')
293+
releases = self.fetchItems('/updater/status')
294+
if len(releases):
295+
return releases[0]
294296

295297
def isLatest(self):
296298
""" Check if the installed version of PMS is the latest. """
297299
release = self.check_for_update(force=True)
298-
return bool(release.version == self.version)
300+
return release is None
299301

300302
def installUpdate(self):
301303
""" Install the newest version of Plex Media Server. """

plexapi/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class Setting(PlexObject):
9999
group (str): Group name this setting is categorized as.
100100
enumValues (list,dict): List or dictionary of valis values for this setting.
101101
"""
102-
_bool_cast = lambda x: True if x == 'true' else False
102+
_bool_cast = lambda x: True if x == 'true' or x == '1' else False
103103
_bool_str = lambda x: str(x).lower()
104104
TYPES = {
105105
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},

plexapi/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
# Search Types - Plex uses these to filter specific media types when searching.
1616
# Library Types - Populated at runtime
17-
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4,
18-
'artist': 8, 'album': 9, 'track': 10, 'photo': 14,
19-
'collection': 18}
17+
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'trailer': 5, 'comic': 6, 'person': 7,
18+
'artist': 8, 'album': 9, 'track': 10, 'picture': 11, 'clip': 12, 'photo': 13, 'photoalbum': 14,
19+
'playlist': 15, 'playlistFolder': 16, 'collection': 18, 'userPlaylistItem': 1001}
2020
PLEXOBJECTS = {}
2121

2222

0 commit comments

Comments
 (0)