From 9bc7868d6a5467de7472023a7b338624d2569e4a Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Wed, 16 Sep 2020 14:16:03 -0400 Subject: [PATCH 01/12] timeline() now returns ClientTimeline objects * timeline() creates and returns ClientTimeline objects with associated attributes and caching * Refactor isPlayingMedia to use the new attributes and fix it's default value --- plexapi/client.py | 66 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 9e720d62c..fed9debdf 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -69,6 +69,8 @@ def __init__(self, server=None, data=None, initpath=None, baseurl=None, self._proxyThroughServer = False self._commandId = 0 self._last_call = 0 + self._timeline_cache = [] + self._timeline_cache_timestamp = 0 if not any([data is not None, initpath, baseurl, token]): self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433') self._token = logfilter.add_secret(CONFIG.get('auth.client_token')) @@ -540,20 +542,62 @@ def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=No # ------------------- # Timeline Commands - def timeline(self, wait=1): - """ Poll the current timeline and return the XML response. """ - return self.sendCommand('timeline/poll', wait=wait) + def timelines(self, wait=0): + """ Poll the current timeline and return timeline objects. """ + t = time.time() + if t - self._timeline_cache_timestamp > 1: + self._timeline_cache_timestamp = t + timelines = self.sendCommand(ClientTimeline.key, wait=wait) + self._timeline_cache = [ClientTimeline(self, data) for data in timelines] + + return self._timeline_cache + + @property + def timeline(self): + """ Returns active client attributes. """ + return next((x for x in self.timelines() if x.state != 'stopped'), None) - def isPlayingMedia(self, includePaused=False): + def isPlayingMedia(self, includePaused=True): """ Returns True if any media is currently playing. Parameters: includePaused (bool): Set True to treat currently paused items - as playing (optional; default True). + as playing (optional; default True). """ - for mediatype in self.timeline(wait=0): - if mediatype.get('state') == 'playing': - return True - if includePaused and mediatype.get('state') == 'paused': - return True - return False + state = getattr(self.timeline, "state", None) + return bool(state == 'playing' or (includePaused and state == 'paused')) + + +class ClientTimeline(PlexObject): + """ Get the attributes of an active client. """ + + key = 'timeline/poll' + + def _loadData(self, data): + self._data = data + self.address = data.attrib.get('address') + self.autoPlay = utils.cast(int, data.attrib.get('autoPlay')) + self.containerKey = data.attrib.get('containerKey') + self.controllable = data.attrib.get('controllable') + self.duration = utils.cast(int, data.attrib.get('duration')) + self.itemType = data.attrib.get('itemType') + self.key = data.attrib.get('key') + self.location = data.attrib.get('location') + self.machineIdentifier = data.attrib.get('machineIdentifier') + self.playQueueID = data.attrib.get('playQueueID') + self.playQueueItemID = data.attrib.get('playQueueItemID') + self.playQueueVersion = utils.cast(int, data.attrib.get('playQueueVersion')) + self.port = utils.cast(int, data.attrib.get('port')) + self.protocol = data.attrib.get('protocol') + self.providerIdentifier = data.attrib.get('providerIdentifier') + self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) + self.repeat = utils.cast(int, data.attrib.get('repeat')) + self.seekRange = data.attrib.get('seekRange') + self.shuffle = utils.cast(int, data.attrib.get('shuffle')) + self.state = data.attrib.get('state') + self.subtitleColor = data.attrib.get('subtitleColor') + self.subtitlePosition = data.attrib.get('subtitlePosition') + self.subtitleSize = utils.cast(int, data.attrib.get('subtitleSize')) + self.time = utils.cast(int, data.attrib.get('time')) + self.type = data.attrib.get('type') + self.volume = utils.cast(int, data.attrib.get('volume')) From 7ae3fe2e4eefa4c050e1db51e17210e9cfc9bcce Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Thu, 17 Sep 2020 07:35:40 -0400 Subject: [PATCH 02/12] Clarify docstrings --- plexapi/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index fed9debdf..81dd7199e 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -543,7 +543,7 @@ def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=No # ------------------- # Timeline Commands def timelines(self, wait=0): - """ Poll the current timeline and return timeline objects. """ + """Poll the client's timelines, create, and return timeline objects.""" t = time.time() if t - self._timeline_cache_timestamp > 1: self._timeline_cache_timestamp = t @@ -554,11 +554,11 @@ def timelines(self, wait=0): @property def timeline(self): - """ Returns active client attributes. """ + """Returns the active timeline object.""" return next((x for x in self.timelines() if x.state != 'stopped'), None) def isPlayingMedia(self, includePaused=True): - """ Returns True if any media is currently playing. + """Returns True if any media is currently playing. Parameters: includePaused (bool): Set True to treat currently paused items @@ -569,7 +569,7 @@ def isPlayingMedia(self, includePaused=True): class ClientTimeline(PlexObject): - """ Get the attributes of an active client. """ + """Get the timeline attributes the client.""" key = 'timeline/poll' From adc8e7fbe63612a405230e160b48557f350177cf Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Thu, 17 Sep 2020 08:06:08 -0400 Subject: [PATCH 03/12] Remove default param in timeline call & fix docstring typo --- plexapi/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 81dd7199e..a11d25704 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -201,7 +201,7 @@ def sendCommand(self, command, proxy=None, **params): self._last_call = t elif t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): self._last_call = t - self.timeline(wait=0) + self.timeline() params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) @@ -569,7 +569,7 @@ def isPlayingMedia(self, includePaused=True): class ClientTimeline(PlexObject): - """Get the timeline attributes the client.""" + """Get the timeline's attributes.""" key = 'timeline/poll' From 0c50c2f94d21e19bd51deda9f6b4e711b77c4685 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Fri, 18 Sep 2020 18:58:45 -0400 Subject: [PATCH 04/12] return empty list if `timelines()` comes back empty Web clients can occasionally return no timelines if no media has been played on them or if nothing has played for a while, this prevents errors in those cases. --- plexapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/client.py b/plexapi/client.py index a11d25704..3462080a2 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -550,7 +550,7 @@ def timelines(self, wait=0): timelines = self.sendCommand(ClientTimeline.key, wait=wait) self._timeline_cache = [ClientTimeline(self, data) for data in timelines] - return self._timeline_cache + return self._timeline_cache or [] @property def timeline(self): From 2b95127307a4d9b2aafda2e3d50db82844922f1c Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Sun, 20 Sep 2020 14:37:48 -0400 Subject: [PATCH 05/12] typo --- plexapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/client.py b/plexapi/client.py index 3462080a2..4a887dc71 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -201,7 +201,7 @@ def sendCommand(self, command, proxy=None, **params): self._last_call = t elif t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): self._last_call = t - self.timeline() + self.timelines() params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) From 50c84e4fc9c6133ea5d9bffd0349b7ff77c772cc Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Fri, 25 Sep 2020 10:46:20 -0400 Subject: [PATCH 06/12] Workaround for unresponsive clients --- plexapi/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 4a887dc71..228dc81ea 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -544,13 +544,14 @@ def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=No # Timeline Commands def timelines(self, wait=0): """Poll the client's timelines, create, and return timeline objects.""" + self.sendCommand(ClientTimeline.key, wait=1) t = time.time() if t - self._timeline_cache_timestamp > 1: self._timeline_cache_timestamp = t - timelines = self.sendCommand(ClientTimeline.key, wait=wait) + timelines = self.sendCommand(ClientTimeline.key, wait=wait) or [] self._timeline_cache = [ClientTimeline(self, data) for data in timelines] - return self._timeline_cache or [] + return self._timeline_cache @property def timeline(self): From 9a69a9646d32f3e186028d62d4e177d8a48620d9 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Fri, 25 Sep 2020 13:39:32 -0400 Subject: [PATCH 07/12] Use sendCommand rather than timelines() for PTP workaround --- plexapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/client.py b/plexapi/client.py index 228dc81ea..5a47b9435 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -201,7 +201,7 @@ def sendCommand(self, command, proxy=None, **params): self._last_call = t elif t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): self._last_call = t - self.timelines() + self.sendCommand('timeline/poll', wait=0) params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) From c526a1d60b95670b2764ba7208165a6f80adbdab Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Fri, 25 Sep 2020 14:42:06 -0400 Subject: [PATCH 08/12] Remove workaround, set timeline's wait default to 1 --- plexapi/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 5a47b9435..cea8f8b70 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -542,9 +542,8 @@ def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=No # ------------------- # Timeline Commands - def timelines(self, wait=0): + def timelines(self, wait=1): """Poll the client's timelines, create, and return timeline objects.""" - self.sendCommand(ClientTimeline.key, wait=1) t = time.time() if t - self._timeline_cache_timestamp > 1: self._timeline_cache_timestamp = t From cda34c3e4d591db5097050fb136f819cf67fd319 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Sat, 26 Sep 2020 09:29:25 -0400 Subject: [PATCH 09/12] set timelines() wait default to 0, document buggy behavior --- plexapi/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index cea8f8b70..950efc3a5 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -542,8 +542,11 @@ def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=No # ------------------- # Timeline Commands - def timelines(self, wait=1): - """Poll the client's timelines, create, and return timeline objects.""" + def timelines(self, wait=0): + """Poll the client's timelines, create, and return timeline objects. + Some clients may not always respond to timeline requests, believe this + to be a Plex bug. + """ t = time.time() if t - self._timeline_cache_timestamp > 1: self._timeline_cache_timestamp = t From 83116d45708588472e8593d0bd45200c81349848 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Tue, 29 Sep 2020 15:05:27 -0400 Subject: [PATCH 10/12] Use ClientTimeline.key for consistency Co-authored-by: jjlawren --- plexapi/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/client.py b/plexapi/client.py index 950efc3a5..87b028c9e 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -201,7 +201,7 @@ def sendCommand(self, command, proxy=None, **params): self._last_call = t elif t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): self._last_call = t - self.sendCommand('timeline/poll', wait=0) + self.sendCommand(ClientTimeline.key, wait=0) params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) From 97729d2fca36b00d9eb53aa5d1289c555e47ad2d Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Tue, 29 Sep 2020 15:06:48 -0400 Subject: [PATCH 11/12] cast playQueue's IDs as int Co-authored-by: jjlawren --- plexapi/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 87b028c9e..8ec968646 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -587,8 +587,8 @@ def _loadData(self, data): self.key = data.attrib.get('key') self.location = data.attrib.get('location') self.machineIdentifier = data.attrib.get('machineIdentifier') - self.playQueueID = data.attrib.get('playQueueID') - self.playQueueItemID = data.attrib.get('playQueueItemID') + self.playQueueID = utils.cast(int, data.attrib.get('playQueueID')) + self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) self.playQueueVersion = utils.cast(int, data.attrib.get('playQueueVersion')) self.port = utils.cast(int, data.attrib.get('port')) self.protocol = data.attrib.get('protocol') From 567debd03f1eaa591c23dfe5002b7e74cc6c1ec7 Mon Sep 17 00:00:00 2001 From: Ryan Meek <25127328+maykar@users.noreply.github.com> Date: Tue, 29 Sep 2020 15:11:55 -0400 Subject: [PATCH 12/12] Add audio attribs & make casts bool from int where it makes sense. --- plexapi/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 8ec968646..4405c230e 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -579,7 +579,8 @@ class ClientTimeline(PlexObject): def _loadData(self, data): self._data = data self.address = data.attrib.get('address') - self.autoPlay = utils.cast(int, data.attrib.get('autoPlay')) + self.audioStreamId = utils.cast(int, data.attrib.get('audioStreamId')) + self.autoPlay = utils.cast(bool, data.attrib.get('autoPlay')) self.containerKey = data.attrib.get('containerKey') self.controllable = data.attrib.get('controllable') self.duration = utils.cast(int, data.attrib.get('duration')) @@ -587,6 +588,8 @@ def _loadData(self, data): self.key = data.attrib.get('key') self.location = data.attrib.get('location') self.machineIdentifier = data.attrib.get('machineIdentifier') + self.partCount = utils.cast(int, data.attrib.get('partCount')) + self.partIndex = utils.cast(int, data.attrib.get('partIndex')) self.playQueueID = utils.cast(int, data.attrib.get('playQueueID')) self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) self.playQueueVersion = utils.cast(int, data.attrib.get('playQueueVersion')) @@ -594,9 +597,9 @@ def _loadData(self, data): self.protocol = data.attrib.get('protocol') self.providerIdentifier = data.attrib.get('providerIdentifier') self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) - self.repeat = utils.cast(int, data.attrib.get('repeat')) + self.repeat = utils.cast(bool, data.attrib.get('repeat')) self.seekRange = data.attrib.get('seekRange') - self.shuffle = utils.cast(int, data.attrib.get('shuffle')) + self.shuffle = utils.cast(bool, data.attrib.get('shuffle')) self.state = data.attrib.get('state') self.subtitleColor = data.attrib.get('subtitleColor') self.subtitlePosition = data.attrib.get('subtitlePosition')