Skip to content

Commit bd9e1db

Browse files
authored
Merge branch 'master' into patch-8
2 parents 4db275d + 4906c1c commit bd9e1db

File tree

8 files changed

+208
-167
lines changed

8 files changed

+208
-167
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ include/
2424
lib/
2525
pip-selfcheck.json
2626
pyvenv.cfg
27+
MANIFEST

plexapi/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
77
from plexapi.utils import tag_helper
88

9+
DONT_RELOAD_FOR_KEYS = ['key', 'session']
910
OPERATORS = {
1011
'exact': lambda v, q: v == q,
1112
'iexact': lambda v, q: v.lower() == q.lower(),
@@ -278,7 +279,8 @@ def __getattribute__(self, attr):
278279
# Dragons inside.. :-/
279280
value = super(PlexPartialObject, self).__getattribute__(attr)
280281
# Check a few cases where we dont want to reload
281-
if attr == 'key' or attr.startswith('_'): return value
282+
if attr in DONT_RELOAD_FOR_KEYS: return value
283+
if attr.startswith('_'): return value
282284
if value not in (None, []): return value
283285
if self.isFullObject(): return value
284286
# Log the reload.

plexapi/client.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,24 +187,30 @@ def sendCommand(self, command, proxy=None, **params):
187187
log.debug('Client %s doesnt support %s controller.'
188188
'What your trying might not work' % (self.title, controller))
189189

190+
proxy = self._proxyThroughServer if proxy is None else proxy
191+
query = self._server.query if proxy else self.query
192+
190193
# Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244
191194
t = time.time()
192195
if t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'):
193196
url = '/player/timeline/poll?wait=0&commandID=%s' % self._nextCommandId()
194-
if proxy:
195-
self._server.query(url, headers=headers)
196-
else:
197-
self.query(url, headers=headers)
197+
query(url, headers=headers)
198198
self._last_call = t
199199

200200
params['commandID'] = self._nextCommandId()
201201
key = '/player/%s%s' % (command, utils.joinArgs(params))
202202

203-
proxy = self._proxyThroughServer if proxy is None else proxy
204-
205-
if proxy:
206-
return self._server.query(key, headers=headers)
207-
return self.query(key, headers=headers)
203+
try:
204+
return query(key, headers=headers)
205+
except ElementTree.ParseError:
206+
# Workaround for players which don't return valid XML on successful commands
207+
# - Plexamp: `b'OK'`
208+
if self.product in (
209+
'Plexamp',
210+
'Plex for Android (TV)',
211+
):
212+
return
213+
raise
208214

209215
def url(self, key, includeToken=False):
210216
""" Build a URL string with proper token argument. Token will be appended to the URL

plexapi/media.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class MediaPart(PlexObject):
8585
key (str): Key used to access this media part (ex: /library/parts/46618/1389985872/file.avi).
8686
size (int): Size of this file in bytes (ex: 733884416).
8787
streams (list<:class:`~plexapi.media.MediaPartStream`>): List of streams in this media part.
88+
exists (bool): Determine if file exists
89+
accessible (bool): Determine if file is accessible
8890
"""
8991
TAG = 'Part'
9092

@@ -104,6 +106,8 @@ def _loadData(self, data):
104106
self.syncState = data.attrib.get('syncState')
105107
self.videoProfile = data.attrib.get('videoProfile')
106108
self.streams = self._buildStreams(data)
109+
self.exists = cast(bool, data.attrib.get('exists'))
110+
self.accessible = cast(bool, data.attrib.get('accessible'))
107111

108112
def _buildStreams(self, data):
109113
streams = []
@@ -463,6 +467,23 @@ class Mood(MediaTag):
463467
FILTER = 'mood'
464468

465469

470+
@utils.registerPlexObject
471+
class Poster(PlexObject):
472+
""" Represents a Poster.
473+
474+
Attributes:
475+
TAG (str): 'Photo'
476+
"""
477+
TAG = 'Photo'
478+
479+
def _loadData(self, data):
480+
self._data = data
481+
self.key = data.attrib.get('key')
482+
self.ratingKey = data.attrib.get('ratingKey')
483+
self.selected = data.attrib.get('selected')
484+
self.thumb = data.attrib.get('thumb')
485+
486+
466487
@utils.registerPlexObject
467488
class Producer(MediaTag):
468489
""" Represents a single Producer media tag.

plexapi/video.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ def removeSubtitles(self, streamID=None, streamTitle=None):
123123
self._server.query(stream.key, self._server._session.delete)
124124
self.reload()
125125

126+
def posters(self):
127+
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`:"""
128+
129+
return self.fetchItems('%s/posters' % self.key, cls=media.Poster)
130+
126131
def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=False, title=None):
127132
""" Add current video (movie, tv-show, season or episode) as sync item for specified device.
128133
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.

tests/test_audio.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def test_audio_Artist_attr(artist):
77
artist.reload()
88
assert utils.is_datetime(artist.addedAt)
99
assert artist.countries == []
10-
assert [i.tag for i in artist.genres] == ['Electronic']
10+
assert [i.tag for i in artist.genres] in [[], ['Electronic']]
1111
assert utils.is_string(artist.guid, gte=5)
1212
assert artist.index == '1'
1313
assert utils.is_metadata(artist._initpath)
@@ -79,6 +79,7 @@ def test_audio_Album_attrs(album):
7979
assert album.year == 2016
8080
assert album.artUrl is None
8181

82+
8283
def test_audio_Album_tracks(album):
8384
tracks = album.tracks()
8485
track = tracks[0]
@@ -96,7 +97,7 @@ def test_audio_Album_tracks(album):
9697
assert utils.is_int(track.parentRatingKey)
9798
assert utils.is_metadata(track.parentThumb, contains='/thumb/')
9899
assert track.parentTitle == 'Unmastered Impulses'
99-
#assert track.ratingCount == 9 # Flaky
100+
# assert track.ratingCount == 9 # Flaky
100101
assert utils.is_int(track.ratingKey)
101102
assert track._server._baseurl == utils.SERVER_BASEURL
102103
assert track.summary == ""
@@ -114,7 +115,7 @@ def test_audio_Album_track(album, track=None):
114115
# this is not reloaded. its not that much info missing.
115116
track = track or album.track('Holy Moment')
116117
assert utils.is_datetime(track.addedAt)
117-
assert track.duration == 298606
118+
assert track.duration in [298605, 298606]
118119
assert utils.is_metadata(track.grandparentKey)
119120
assert utils.is_int(track.grandparentRatingKey)
120121
assert track.grandparentTitle == 'Infinite State'
@@ -147,7 +148,7 @@ def test_audio_Album_track(album, track=None):
147148
assert media.audioCodec == 'mp3'
148149
assert media.bitrate == 385
149150
assert media.container == 'mp3'
150-
assert media.duration == 298606
151+
assert media.duration in [298605, 298606]
151152
assert media.height is None
152153
assert utils.is_int(media.id, gte=1)
153154
assert utils.is_metadata(media._initpath)
@@ -160,7 +161,7 @@ def test_audio_Album_track(album, track=None):
160161
assert media.videoResolution is None
161162
assert media.width is None
162163
assert part.container == 'mp3'
163-
assert part.duration == 298606
164+
assert part.duration in [298605, 298606]
164165
assert part.file.endswith('.mp3')
165166
assert utils.is_int(part.id)
166167
assert utils.is_metadata(part._initpath)
@@ -186,7 +187,7 @@ def test_audio_Track_attrs(album):
186187
assert utils.is_datetime(track.addedAt)
187188
assert track.art is None
188189
assert track.chapterSource is None
189-
assert track.duration == 298606
190+
assert track.duration in [298605, 298606]
190191
assert track.grandparentArt is None
191192
assert utils.is_metadata(track.grandparentKey)
192193
assert utils.is_int(track.grandparentRatingKey)
@@ -232,7 +233,7 @@ def test_audio_Track_attrs(album):
232233
assert media.audioCodec == 'mp3'
233234
assert media.bitrate == 385
234235
assert media.container == 'mp3'
235-
assert media.duration == 298606
236+
assert media.duration in [298605, 298606]
236237
assert media.height is None
237238
assert utils.is_int(media.id, gte=1)
238239
assert utils.is_metadata(media._initpath)
@@ -245,12 +246,12 @@ def test_audio_Track_attrs(album):
245246
assert media.videoResolution is None
246247
assert media.width is None
247248
assert part.container == 'mp3'
248-
assert part.duration == 298606
249+
assert part.duration in [298605, 298606]
249250
assert part.file.endswith('.mp3')
250251
assert utils.is_int(part.id)
251252
assert utils.is_metadata(part._initpath)
252253
assert utils.is_part(part.key)
253-
#assert part.media == <Media:Holy.Moment>
254+
# assert part.media == <Media:Holy.Moment>
254255
assert part._server._baseurl == utils.SERVER_BASEURL
255256
assert part.size == 14360402
256257
# Assign 0 part.streams
@@ -269,7 +270,7 @@ def test_audio_Track_attrs(album):
269270
assert utils.is_metadata(stream._initpath)
270271
assert stream.language is None
271272
assert stream.languageCode is None
272-
#assert stream.part == <MediaPart:22>
273+
# assert stream.part == <MediaPart:22>
273274
assert stream.samplingRate == 44100
274275
assert stream.selected is True
275276
assert stream._server._baseurl == utils.SERVER_BASEURL

tests/test_video.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def test_video_Movie_attrs(movies):
125125
assert [i.tag for i in movie.directors] == ['Nina Paley']
126126
assert movie.duration >= 160000
127127
assert movie.fields == []
128+
assert movie.posters()
128129
assert sorted([i.tag for i in movie.genres]) == ['Animation', 'Comedy', 'Fantasy', 'Musical', 'Romance']
129130
assert movie.guid == 'com.plexapp.agents.imdb://tt1172203?lang=en'
130131
assert utils.is_metadata(movie._initpath)
@@ -239,6 +240,8 @@ def test_video_Movie_attrs(movies):
239240
assert len(part.key) >= 10
240241
assert part._server._baseurl == utils.SERVER_BASEURL
241242
assert utils.is_int(part.size, gte=1000000)
243+
assert part.exists
244+
assert part.accessible
242245
# Stream 1
243246
stream1 = part.streams[0]
244247
assert stream1.bitDepth in (8, None)
@@ -530,6 +533,8 @@ def test_video_Episode_attrs(episode):
530533
assert len(part.key) >= 10
531534
assert part._server._baseurl == utils.SERVER_BASEURL
532535
assert utils.is_int(part.size, gte=18184197)
536+
assert part.exists
537+
assert part.accessible
533538

534539

535540
def test_video_Season(show):
@@ -638,3 +643,17 @@ def test_that_reload_return_the_same_object(plex):
638643
episode_section_get_key = episode_section_get.key
639644
assert episode_library_search_key == episode_library_search.reload().key == episode_search_key == episode_search.reload().key == episode_section_get_key == episode_section_get.reload().key # noqa
640645

646+
647+
def test_video_exists_accessible(movie, episode):
648+
assert movie.media[0].parts[0].exists is None
649+
assert movie.media[0].parts[0].accessible is None
650+
movie.reload()
651+
assert movie.media[0].parts[0].exists is True
652+
assert movie.media[0].parts[0].accessible is True
653+
654+
assert episode.media[0].parts[0].exists is None
655+
assert episode.media[0].parts[0].accessible is None
656+
episode.reload()
657+
assert episode.media[0].parts[0].exists is True
658+
assert episode.media[0].parts[0].accessible is True
659+

0 commit comments

Comments
 (0)