Skip to content

Commit bf57116

Browse files
committed
Merge branch 'master' of github.com:pkkid/python-plexapi
2 parents 574d023 + 4f5ce34 commit bf57116

36 files changed

+2184
-222
lines changed

.travis.yml

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,80 @@
1-
language:
2-
- python
1+
language: python
2+
3+
stages:
4+
- test
5+
- name: deploy
6+
if: tag = 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+
- '[ -z "${PLEXAPI_AUTH_MYPLEX_USERNAME}" ] && PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py
30+
--destination plex --advertise-ip=127.0.0.1 --bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG --unclaimed ||
31+
PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-bootstraptest.py --destination plex --advertise-ip=127.0.0.1
32+
--bootstrap-timeout 540 --docker-tag $PLEX_CONTAINER_TAG'
33+
1334
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
35+
- py.test tests -rxXs --ignore=tests/test_sync.py --tb=native --verbose --cov-config .coveragerc --cov=plexapi
36+
- PLEXAPI_HEADER_PROVIDES='controller,sync-target' PLEXAPI_HEADER_PLATFORM=iOS PLEXAPI_HEADER_PLATFORM_VERSION=11.4.1
37+
PLEXAPI_HEADER_DEVICE=iPhone py.test tests/test_sync.py -rxXs --tb=native --verbose --cov-config .coveragerc
38+
--cov=plexapi --cov-append
39+
1740
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
41+
- COVERALLS_PARALLEL=true coveralls
42+
43+
after_script:
44+
- '[ -z "${PLEXAPI_AUTH_MYPLEX_USERNAME}" ] || PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py'
45+
46+
jobs:
47+
include:
48+
- python: 3.6
49+
name: "Flake8"
50+
install:
51+
- pip install -r requirements_dev.txt
52+
script: flake8 plexapi --exclude=compat.py --max-line-length=120 --ignore=E128,E701,E702,E731,W293,W605
53+
after_success: skip
54+
env:
55+
- PLEX_CONTAINER_TAG=latest
56+
- stage: test
57+
python: 3.6
58+
env:
59+
- PLEX_CONTAINER_TAG=1.3.2.3112-1751929
60+
- TEST_ACCOUNT_ONCE=1
61+
- stage: test
62+
python: 3.6
63+
if: type != 'pull_request' # pull requests always run over unclaimed server
64+
after_success: skip
65+
env:
66+
- PLEX_CONTAINER_TAG=latest PLEXAPI_AUTH_MYPLEX_USERNAME=
67+
- stage: deploy
68+
name: "Deploy to PyPi"
69+
python: 3.6
70+
install: true
71+
script: true
72+
env:
73+
- PLEX_CONTAINER_TAG=latest
74+
deploy:
75+
provider: pypi
76+
user: mjs7231
77+
password:
78+
secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c=
79+
on:
80+
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

docs/configuration.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ Section [plexapi] Options
5555
Timeout in seconds to use when making requests to the Plex Media Server or Plex Client
5656
resources (default: 30).
5757

58+
**enable_fast_connect**
59+
By default Plex will be trying to connect with all available connection methods simultaneously,
60+
combining local and remote addresses, http and https, and be waiting for all connection to
61+
establish (or fail due to timeout / any other error), this can take long time when you're trying
62+
to connect to your Plex Server outside of your home network.
63+
64+
When the options is set to `true` the connection procedure will be aborted with first successfully
65+
established connection.
66+
5867

5968
Section [auth] Options
6069
----------------------

plexapi/__init__.py

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

1515
# PlexAPI Settings
1616
PROJECT = 'PlexAPI'
17-
VERSION = '3.0.6'
17+
VERSION = '3.1.0'
1818
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
1919
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
20+
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
2021

2122
# Plex Header Configuation
2223
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
23-
X_PLEX_PLATFORM = CONFIG.get('header.platorm', uname()[0])
24+
X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platorm', uname()[0]))
2425
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
2526
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
2627
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)

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/audio.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from plexapi import media, utils
33
from plexapi.base import Playable, PlexPartialObject
4+
from plexapi.compat import quote_plus
45

56

67
class Audio(PlexPartialObject):
@@ -23,6 +24,9 @@ class Audio(PlexPartialObject):
2324
updatedAt (datatime): Datetime this item was updated.
2425
viewCount (int): Count of times this item was accessed.
2526
"""
27+
28+
METADATA_TYPE = 'track'
29+
2630
def _loadData(self, data):
2731
""" Load attribute values from Plex XML response. """
2832
self._data = data
@@ -57,6 +61,46 @@ def url(self, part):
5761
""" Returns the full URL for this audio item. Typically used for getting a specific track. """
5862
return self._server.url(part, includeToken=True) if part else None
5963

64+
def _defaultSyncTitle(self):
65+
""" Returns str, default title for a new syncItem. """
66+
return self.title
67+
68+
def sync(self, bitrate, client=None, clientId=None, limit=None, title=None):
69+
""" Add current audio (artist, album or track) as sync item for specified device.
70+
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
71+
72+
Parameters:
73+
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
74+
module :mod:`plexapi.sync`.
75+
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
76+
:func:`plexapi.myplex.MyPlexAccount.sync`.
77+
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
78+
limit (int): maximum count of items to sync, unlimited if `None`.
79+
title (str): descriptive title for the new :class:`plexapi.sync.SyncItem`, if empty the value would be
80+
generated from metadata of current media.
81+
82+
Returns:
83+
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
84+
"""
85+
86+
from plexapi.sync import SyncItem, Policy, MediaSettings
87+
88+
myplex = self._server.myPlexAccount()
89+
sync_item = SyncItem(self._server, None)
90+
sync_item.title = title if title else self._defaultSyncTitle()
91+
sync_item.rootTitle = self.title
92+
sync_item.contentType = self.listType
93+
sync_item.metadataType = self.METADATA_TYPE
94+
sync_item.machineIdentifier = self._server.machineIdentifier
95+
96+
section = self._server.library.sectionByID(self.librarySectionID)
97+
98+
sync_item.location = 'library://%s/item/%s' % (section.uuid, quote_plus(self.key))
99+
sync_item.policy = Policy.create(limit)
100+
sync_item.mediaSettings = MediaSettings.createMusic(bitrate)
101+
102+
return myplex.sync(sync_item, client=client, clientId=clientId)
103+
60104

61105
@utils.registerPlexObject
62106
class Artist(Audio):
@@ -225,6 +269,10 @@ def download(self, savepath=None, keep_orginal_name=False, **kwargs):
225269
filepaths += track.download(savepath, keep_orginal_name, **kwargs)
226270
return filepaths
227271

272+
def _defaultSyncTitle(self):
273+
""" Returns str, default title for a new syncItem. """
274+
return '%s - %s' % (self.parentTitle, self.title)
275+
228276

229277
@utils.registerPlexObject
230278
class Track(Audio, Playable):
@@ -302,3 +350,7 @@ def album(self):
302350
def artist(self):
303351
""" Return this track's :class:`~plexapi.audio.Artist`. """
304352
return self.fetchItem(self.grandparentKey)
353+
354+
def _defaultSyncTitle(self):
355+
""" Returns str, default title for a new syncItem. """
356+
return '%s - %s - %s' % (self.grandparentTitle, self.parentTitle, self.title)

plexapi/base.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,12 @@ def fetchItems(self, ekey, cls=None, **kwargs):
145145
on how this is used.
146146
"""
147147
data = self._server.query(ekey)
148-
return self.findItems(data, cls, ekey, **kwargs)
148+
items = self.findItems(data, cls, ekey, **kwargs)
149+
librarySectionID = data.attrib.get('librarySectionID')
150+
if librarySectionID:
151+
for item in items:
152+
item.librarySectionID = librarySectionID
153+
return items
149154

150155
def findItems(self, data, cls=None, initpath=None, **kwargs):
151156
""" Load the specified data to find and build all items with the specified tag
@@ -466,7 +471,7 @@ def getStreamURL(self, **params):
466471
offset, copyts, protocol, mediaIndex, platform.
467472
468473
Raises:
469-
Unsupported: When the item doesn't support fetching a stream URL.
474+
:class:`plexapi.exceptions.Unsupported`: When the item doesn't support fetching a stream URL.
470475
"""
471476
if self.TYPE not in ('movie', 'episode', 'track'):
472477
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
@@ -565,6 +570,24 @@ def updateProgress(self, time, state='stopped'):
565570
time, state)
566571
self._server.query(key)
567572
self.reload()
573+
574+
def updateTimeline(self, time, state='stopped', duration=None):
575+
""" Set the timeline progress for this video.
576+
577+
Parameters:
578+
time (int): milliseconds watched
579+
state (string): state of the video, default 'stopped'
580+
duration (int): duration of the item
581+
"""
582+
durationStr = '&duration='
583+
if duration is not None:
584+
durationStr = durationStr + str(duration)
585+
else:
586+
durationStr = durationStr + str(self.duration)
587+
key = '/:/timeline?ratingKey=%s&key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s%s'
588+
key %= (self.ratingKey, self.key, time, state, durationStr)
589+
self._server.query(key)
590+
self.reload()
568591

569592

570593
@utils.registerPlexObject

0 commit comments

Comments
 (0)