From 7dbc02514cb8352702841121499941dcd463c08f Mon Sep 17 00:00:00 2001 From: Josh Wood Date: Sun, 11 Nov 2018 10:32:15 -0600 Subject: [PATCH 01/24] (feat): allow MusicSection search by track.userRating and sort by userRating --- plexapi/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index ae3e38e6..84063e97 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -776,8 +776,8 @@ class MusicSection(LibrarySection): TAG (str): 'Directory' TYPE (str): 'artist' """ - ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood') - ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort') + ALLOWED_FILTERS = ('genre', 'country', 'collection', 'mood', 'track.userRating') + ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort', 'userRating') TAG = 'Directory' TYPE = 'artist' From e6dbf833d4d6b3db7042c7620837ca79e9a25a80 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Fri, 16 Nov 2018 23:47:49 +0100 Subject: [PATCH 02/24] inital smart playlist --- plexapi/playlist.py | 63 +++++++++++++++++++++++++++++++++++++++++++-- plexapi/server.py | 4 +-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 8c74147c..f79a1028 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -2,6 +2,7 @@ from plexapi import utils from plexapi.base import PlexPartialObject, Playable from plexapi.exceptions import BadRequest, Unsupported +from plexapi.library import LibrarySection from plexapi.playqueue import PlayQueue from plexapi.utils import cast, toDatetime from plexapi.compat import quote_plus @@ -127,9 +128,9 @@ class Playlist(PlexPartialObject, Playable): return PlayQueue.create(self._server, self, *args, **kwargs) @classmethod - def create(cls, server, title, items): + def _create(cls, server, title, items): """ Create a playlist. """ - if not isinstance(items, (list, tuple)): + if items and not isinstance(items, (list, tuple)): items = [items] ratingKeys = [] for item in items: @@ -147,6 +148,64 @@ class Playlist(PlexPartialObject, Playable): data = server.query(key, method=server._session.post)[0] return cls(server, data, initpath=key) + @classmethod + def create(cls, server, title, items=None, section=None, limit=None, smart=False, **kwargs): + """Create a playlist. + + Parameters: + server (:class:`~plexapi.server.PlexServer`): Server your connected to + title (str): Title of the playlist. + items (Iterable): Iterable of objects that should be in the playlist + section (:class:`~plexapi.library.LibrarySection, str): + limit (int): default None + smart (bool): default False + + **kwargs dict: + is passed to the filters. For a example see the search method. + + returns: + class:`~plexapi.playlist.Playlist + + + """ + if smart: + return cls._createSmart(server, title, section, limit, **kwargs) + + else: + return cls._create(server, title, items) + + @classmethod + def _createSmart(cls, server, title, section, limit=None, **kwargs): + """ Create a Smart playlist. """ + + if not isinstance(section, LibrarySection): + section = server.library.section(section) + + sectionType = utils.searchType(section.type) + sectionId = section.key + uuid = section.uuid + uri = 'library://%s/directory//library/sections/%s/all?type=%s' % (uuid, + sectionId, + sectionType) + if limit: + uri = uri + '&limit=%s' % str(limit) + + for category, value in kwargs.items(): + sectionChoices = section.listChoices(category) + for choice in sectionChoices: + if choice.title == value or choice.title.lower() == value.lower(): + uri = uri + '&%s=%s' % (category.lower(), str(choice.key)) + + uri = uri + '&sourceType=%s' % sectionType + key = '/playlists%s' % utils.joinArgs({ + 'uri': uri, + 'type': section.CONTENT_TYPE, + 'title': title, + 'smart': 1, + }) + data = server.query(key, method=server._session.post)[0] + return cls(server, data, initpath=key) + def copyToUser(self, user): """ Copy playlist to another user account. """ from plexapi.server import PlexServer diff --git a/plexapi/server.py b/plexapi/server.py index 8915c7ae..ca160e3c 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -240,14 +240,14 @@ class PlexServer(PlexObject): raise NotFound('Unknown client name: %s' % name) - def createPlaylist(self, title, items): + def createPlaylist(self, title, items=None, section=None, limit=None, smart=None, **kwargs): """ Creates and returns a new :class:`~plexapi.playlist.Playlist`. Parameters: title (str): Title of the playlist to be created. items (list): List of media items to include in the playlist. """ - return Playlist.create(self, title, items) + return Playlist.create(self, title, items=items, limit=limit, section=section, smart=smart, **kwargs) def createPlayQueue(self, item, **kwargs): """ Creates and returns a new :class:`~plexapi.playqueue.PlayQueue`. From a1292173c8bda358884d4848ef64d3e0ecde5ba0 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 00:06:33 +0100 Subject: [PATCH 03/24] docs. --- plexapi/playlist.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index f79a1028..11bd1394 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -153,20 +153,18 @@ class Playlist(PlexPartialObject, Playable): """Create a playlist. Parameters: - server (:class:`~plexapi.server.PlexServer`): Server your connected to + server (:class:`~plexapi.server.PlexServer`): Server your connected to. title (str): Title of the playlist. - items (Iterable): Iterable of objects that should be in the playlist + items (Iterable): Iterable of objects that should be in the playlist. section (:class:`~plexapi.library.LibrarySection, str): - limit (int): default None - smart (bool): default False + limit (int): default None. + smart (bool): default False. **kwargs dict: is passed to the filters. For a example see the search method. returns: class:`~plexapi.playlist.Playlist - - """ if smart: return cls._createSmart(server, title, section, limit, **kwargs) From e0796e2d9e5d08069ea3dd48f60a2dbc11bd26f9 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 00:20:21 +0100 Subject: [PATCH 04/24] add test for smart playlist --- tests/test_playlist.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 7ecfbaa7..23e7318f 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -109,3 +109,9 @@ def test_copyToUser(plex, show, fresh_plex, shared_username): assert playlist.title in [p.title for p in user_plex.playlists()] finally: playlist.delete() + + +def test_smart_playlist(plex, movies): + pl = plex.createPlaylist(title='smart_playlist', limit=1, section=movies, year=2008) + assert len(pl.items()) == 1 + assert pl.smart From c5083c311c85ba5ef944ca1380ee3d5b9b1483ba Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 00:33:56 +0100 Subject: [PATCH 05/24] fix test.. --- plexapi/playlist.py | 5 ++--- tests/test_playlist.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 11bd1394..481b3e4b 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -160,10 +160,9 @@ class Playlist(PlexPartialObject, Playable): limit (int): default None. smart (bool): default False. - **kwargs dict: - is passed to the filters. For a example see the search method. + **kwargs (dict): is passed to the filters. For a example see the search method. - returns: + Returns: class:`~plexapi.playlist.Playlist """ if smart: diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 23e7318f..1d2b7891 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -112,6 +112,6 @@ def test_copyToUser(plex, show, fresh_plex, shared_username): def test_smart_playlist(plex, movies): - pl = plex.createPlaylist(title='smart_playlist', limit=1, section=movies, year=2008) + pl = plex.createPlaylist(title='smart_playlist', smart=True, limit=1, section=movies, year=2008) assert len(pl.items()) == 1 assert pl.smart From d9c87d8423c3d1f92e2e3e9279a96c8f8e958925 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 01:46:42 +0100 Subject: [PATCH 06/24] inital pmp workaround. --- plexapi/client.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/plexapi/client.py b/plexapi/client.py index 3ff99f0c..3695b57b 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import time import requests from requests.status_codes import _codes as codes @@ -70,6 +70,7 @@ class PlexClient(PlexObject): self._session = session or server_session or requests.Session() self._proxyThroughServer = False self._commandId = 0 + self._last_call = 0 if not any([data, initpath, baseurl, token]): self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433') self._token = logfilter.add_secret(CONFIG.get('auth.client_token')) @@ -181,14 +182,26 @@ class PlexClient(PlexObject): """ command = command.strip('/') controller = command.split('/')[0] + headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} if controller not in self.protocolCapabilities: log.debug('Client %s doesnt support %s controller.' 'What your trying might not work' % (self.title, controller)) + # Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244 + t = time.time() + if t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): + url = '/player/timeline/poll?wait=0&commandID=%s' % self._nextCommandId() + if proxy: + self._server.query(url, headers=headers) + else: + self.query(url, headers=headers) + self._last_call = t + params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) - headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} + proxy = self._proxyThroughServer if proxy is None else proxy + if proxy: return self._server.query(key, headers=headers) return self.query(key, headers=headers) From e5df09ca6cb5f4bdf311766de5fd39606ee33070 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 20:42:03 +0100 Subject: [PATCH 07/24] fix settings for py 2 --- plexapi/settings.py | 7 ++++--- tests/test_settings.py | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plexapi/settings.py b/plexapi/settings.py index 94824b6d..0bbc70c8 100644 --- a/plexapi/settings.py +++ b/plexapi/settings.py @@ -101,11 +101,12 @@ class Setting(PlexObject): """ _bool_cast = lambda x: True if x == 'true' or x == '1' else False _bool_str = lambda x: str(x).lower() + _str = lambda x: str(x).encode('utf-8') TYPES = { 'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str}, - 'double': {'type': float, 'cast': float, 'tostr': string_type}, - 'int': {'type': int, 'cast': int, 'tostr': string_type}, - 'text': {'type': string_type, 'cast': string_type, 'tostr': string_type}, + 'double': {'type': float, 'cast': float, 'tostr': _str}, + 'int': {'type': int, 'cast': int, 'tostr': _str}, + 'text': {'type': string_type, 'cast': _str, 'tostr': _str}, } def _loadData(self, data): diff --git a/tests/test_settings.py b/tests/test_settings.py index 78309c80..b28a4a71 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -17,3 +17,12 @@ def test_settings_set(plex): plex.settings.save() plex._settings = None assert plex.settings.get('autoEmptyTrash').value == new_value + + +def test_settings_set_str(plex): + cd = plex.settings.get('OnDeckWindow') + new_value = 99 + cd.set(new_value) + plex.settings.save() + plex._settings = None + assert plex.settings.get('autoEmptyTrash').value == 99 From e6a6a1f7cc51ceb58637456f33e889874e4be9b5 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 17 Nov 2018 20:53:32 +0100 Subject: [PATCH 08/24] oops --- tests/test_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_settings.py b/tests/test_settings.py index b28a4a71..7f5aea17 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -25,4 +25,4 @@ def test_settings_set_str(plex): cd.set(new_value) plex.settings.save() plex._settings = None - assert plex.settings.get('autoEmptyTrash').value == 99 + assert plex.settings.get('OnDeckWindow').value == 99 From 7ef2e406073702c0b3792a92fb3ba6d2f7d8831d Mon Sep 17 00:00:00 2001 From: Michael Shepanski Date: Tue, 4 Dec 2018 15:45:14 -0500 Subject: [PATCH 09/24] Add Not Rated content rating --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 10af6298..fc5a15a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ AUDIOCHANNELS = {2, 6} AUDIOLAYOUTS = {'5.1', '5.1(side)', 'stereo'} CODECS = {'aac', 'ac3', 'dca', 'h264', 'mp3', 'mpeg4'} CONTAINERS = {'avi', 'mp4', 'mkv'} -CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR'} +CONTENTRATINGS = {'TV-14', 'TV-MA', 'G', 'NR', 'Not Rated'} FRAMERATES = {'24p', 'PAL', 'NTSC'} PROFILES = {'advanced simple', 'main', 'constrained baseline'} RESOLUTIONS = {'sd', '480', '576', '720', '1080'} From 5553b87539f6978f56c9f3a8efd7e66af25055b1 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Tue, 4 Dec 2018 22:00:58 +0100 Subject: [PATCH 10/24] Update playlist.py Docs stuff. --- plexapi/playlist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 481b3e4b..a40665d8 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -156,14 +156,14 @@ class Playlist(PlexPartialObject, Playable): server (:class:`~plexapi.server.PlexServer`): Server your connected to. title (str): Title of the playlist. items (Iterable): Iterable of objects that should be in the playlist. - section (:class:`~plexapi.library.LibrarySection, str): + section (:class:`~plexapi.library.LibrarySection`, str): limit (int): default None. smart (bool): default False. **kwargs (dict): is passed to the filters. For a example see the search method. Returns: - class:`~plexapi.playlist.Playlist + :class:`plexapi.playlist.Playlist`: an instance of created Playlist. """ if smart: return cls._createSmart(server, title, section, limit, **kwargs) @@ -190,7 +190,7 @@ class Playlist(PlexPartialObject, Playable): for category, value in kwargs.items(): sectionChoices = section.listChoices(category) for choice in sectionChoices: - if choice.title == value or choice.title.lower() == value.lower(): + if str(choice.title).lower() == str(value).lower(): uri = uri + '&%s=%s' % (category.lower(), str(choice.key)) uri = uri + '&sourceType=%s' % sectionType From abac9173f838c306497f3b69899af0b3ae3f0eb5 Mon Sep 17 00:00:00 2001 From: tijder Date: Fri, 21 Dec 2018 20:51:39 +0100 Subject: [PATCH 11/24] Function updated section all with sort --- plexapi/library.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plexapi/library.py b/plexapi/library.py index 84063e97..4b061682 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -377,9 +377,17 @@ class LibrarySection(PlexObject): key = '/library/sections/%s/all' % self.key return self.fetchItem(key, title__iexact=title) - def all(self, **kwargs): - """ Returns a list of media from this library section. """ - key = '/library/sections/%s/all' % self.key + def all(self, sort=None, **kwargs): + """ Returns a list of media from this library section. + + Parameters: + sort (string): The sort string + """ + sortStr = '' + if sort != None: + sortStr = '?sort=' + sort + + key = '/library/sections/%s/all%s' % (self.key, sortStr) return self.fetchItems(key, **kwargs) def onDeck(self): From 3719d4b5991a19d7980a6d16bbe6216918fa8f58 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Mon, 7 Jan 2019 08:04:53 -0500 Subject: [PATCH 12/24] Fix spelling of "original" --- plexapi/audio.py | 12 ++++++------ plexapi/base.py | 6 +++--- plexapi/video.py | 18 +++++++++--------- tests/test_video.py | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 53f9d608..d6826831 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -168,12 +168,12 @@ class Artist(Audio): """ Alias of :func:`~plexapi.audio.Artist.track`. """ return self.track(title) - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Downloads all tracks for this artist to the specified location. Parameters: savepath (str): Title of the track to return. - keep_orginal_name (bool): Set True to keep the original filename as stored in + keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will @@ -184,7 +184,7 @@ class Artist(Audio): filepaths = [] for album in self.albums(): for track in album.tracks(): - filepaths += track.download(savepath, keep_orginal_name, **kwargs) + filepaths += track.download(savepath, keep_original_name, **kwargs) return filepaths @@ -251,12 +251,12 @@ class Album(Audio): """ Return :func:`~plexapi.audio.Artist` of this album. """ return self.fetchItem(self.parentKey) - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Downloads all tracks for this artist to the specified location. Parameters: savepath (str): Title of the track to return. - keep_orginal_name (bool): Set True to keep the original filename as stored in + keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will @@ -266,7 +266,7 @@ class Album(Audio): """ filepaths = [] for track in self.tracks(): - filepaths += track.download(savepath, keep_orginal_name, **kwargs) + filepaths += track.download(savepath, keep_original_name, **kwargs) return filepaths def _defaultSyncTitle(self): diff --git a/plexapi/base.py b/plexapi/base.py index 3ff9eadc..5800011a 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -519,13 +519,13 @@ class Playable(object): """ client.playMedia(self) - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Downloads this items media to the specified location. Returns a list of filepaths that have been saved to disk. Parameters: savepath (str): Title of the track to return. - keep_orginal_name (bool): Set True to keep the original filename as stored in + keep_original_name (bool): Set True to keep the original filename as stored in the Plex server. False will create a new filename with the format " - ". kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will @@ -537,7 +537,7 @@ class Playable(object): locations = [i for i in self.iterParts() if i] for location in locations: filename = location.file - if keep_orginal_name is False: + if keep_original_name is False: filename = '%s.%s' % (self._prettyfilename(), location.container) # So this seems to be a alot slower but allows transcode. if kwargs: diff --git a/plexapi/video.py b/plexapi/video.py index abd1cde8..4c7b5eb6 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -224,12 +224,12 @@ class Movie(Playable, Video): # This is just for compat. return self.title - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Download video files to specified directory. Parameters: savepath (str): Defaults to current working dir. - keep_orginal_name (bool): True to keep the original file name otherwise + keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. """ @@ -237,7 +237,7 @@ class Movie(Playable, Video): locations = [i for i in self.iterParts() if i] for location in locations: name = location.file - if not keep_orginal_name: + if not keep_original_name: title = self.title.replace(' ', '.') name = '%s.%s' % (title, location.container) if kwargs is not None: @@ -376,18 +376,18 @@ class Show(Video): """ Alias to :func:`~plexapi.video.Show.episode()`. """ return self.episode(title, season, episode) - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Download video files to specified directory. Parameters: savepath (str): Defaults to current working dir. - keep_orginal_name (bool): True to keep the original file name otherwise + keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. """ filepaths = [] for episode in self.episodes(): - filepaths += episode.download(savepath, keep_orginal_name, **kwargs) + filepaths += episode.download(savepath, keep_original_name, **kwargs) return filepaths @@ -477,18 +477,18 @@ class Season(Video): """ Returns list of unwatched :class:`~plexapi.video.Episode` objects. """ return self.episodes(watched=False) - def download(self, savepath=None, keep_orginal_name=False, **kwargs): + def download(self, savepath=None, keep_original_name=False, **kwargs): """ Download video files to specified directory. Parameters: savepath (str): Defaults to current working dir. - keep_orginal_name (bool): True to keep the original file name otherwise + keep_original_name (bool): True to keep the original file name otherwise a friendlier is generated. **kwargs: Additional options passed into :func:`~plexapi.base.PlexObject.getStreamURL()`. """ filepaths = [] for episode in self.episodes(): - filepaths += episode.download(savepath, keep_orginal_name, **kwargs) + filepaths += episode.download(savepath, keep_original_name, **kwargs) return filepaths def _defaultSyncTitle(self): diff --git a/tests/test_video.py b/tests/test_video.py index 0268b791..5d9c114d 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -91,7 +91,7 @@ def test_video_Movie_attrs(movies): assert utils.is_metadata(movie.art) assert movie.artUrl assert movie.audienceRating == 8.5 - # Disabled this since it failed on the last run, wasnt in the orginal xml result. + # Disabled this since it failed on the last run, wasnt in the original xml result. #assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright' movie.reload() # RELOAD assert movie.chapterSource is None From 22d1c2011ce6d904eedd7f5d1b18440aa03c4ac2 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 19 Jan 2019 23:05:12 +0100 Subject: [PATCH 13/24] rstrip baseurl for plex servers, new users seems add / to the end. --- plexapi/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plexapi/server.py b/plexapi/server.py index ca160e3c..c05c8f3a 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -93,6 +93,7 @@ class PlexServer(PlexObject): def __init__(self, baseurl=None, token=None, session=None, timeout=None): self._baseurl = baseurl or CONFIG.get('auth.server_baseurl', 'http://localhost:32400') + self._baseurl = self._baseurl.rstrip('/') self._token = logfilter.add_secret(token or CONFIG.get('auth.server_token')) self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true' self._session = session or requests.Session() From bd8949eeeb7203e5d1006c6682809bd6949806b6 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Sat, 19 Jan 2019 23:23:42 +0100 Subject: [PATCH 14/24] fix datetime issue on windows. --- plexapi/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plexapi/utils.py b/plexapi/utils.py index 76b1f812..8368e2ec 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -178,7 +178,9 @@ def toDatetime(value, format=None): if format: value = datetime.strptime(value, format) else: - value = datetime.fromtimestamp(int(value)) + # https://bugs.python.org/issue30684 + # And platform support for before epoch seems to be flaky. + value = datetime.datetime(1970, 1, 1) + datetime.fromtimestamp(milliseconds=value) return value From 99a803795990b4a1cf0caf32380a6502b44287e3 Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Mon, 21 Jan 2019 21:50:42 +0100 Subject: [PATCH 15/24] Update utils.py Ooobs --- plexapi/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexapi/utils.py b/plexapi/utils.py index 8368e2ec..0bbad4a7 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -5,7 +5,7 @@ import re import requests import time import zipfile -from datetime import datetime +from datetime import datetime, timedelta from getpass import getpass from threading import Thread, Event from tqdm import tqdm @@ -180,7 +180,7 @@ def toDatetime(value, format=None): else: # https://bugs.python.org/issue30684 # And platform support for before epoch seems to be flaky. - value = datetime.datetime(1970, 1, 1) + datetime.fromtimestamp(milliseconds=value) + value = datetime(1970, 1, 1) + timedelta(milliseconds=int(value)) return value From d22743705408cb2ab38fdf254cefca323ecb9ebb Mon Sep 17 00:00:00 2001 From: Hellowlol Date: Mon, 21 Jan 2019 22:14:53 +0100 Subject: [PATCH 16/24] Update utils.py add a proper fix for the issue, TODO make it more fault tolerant --- plexapi/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plexapi/utils.py b/plexapi/utils.py index 0bbad4a7..31a44514 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -180,7 +180,10 @@ def toDatetime(value, format=None): else: # https://bugs.python.org/issue30684 # And platform support for before epoch seems to be flaky. - value = datetime(1970, 1, 1) + timedelta(milliseconds=int(value)) + # TODO check for others errors too. + if int(value) == 0: + value = 86400 + value = datetime.fromtimestamp(int(value)) return value From 14fdc51b37f95341452be996aec5d2797c93072b Mon Sep 17 00:00:00 2001 From: Gabriel Stackhouse Date: Mon, 4 Feb 2019 13:15:05 -0600 Subject: [PATCH 17/24] Add 'forced' var to subtitleStream --- plexapi/media.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plexapi/media.py b/plexapi/media.py index e0519ef1..85732270 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -256,6 +256,7 @@ class SubtitleStream(MediaPartStream): Attributes: TAG (str): 'Stream' STREAMTYPE (int): 3 + forced (bool): True if this is a forced subtitle format (str): Subtitle format (ex: srt). key (str): Key of this subtitle stream (ex: /library/streams/212284). title (str): Title of this subtitle stream. @@ -266,6 +267,7 @@ class SubtitleStream(MediaPartStream): def _loadData(self, data): """ Load attribute values from Plex XML response. """ super(SubtitleStream, self)._loadData(data) + self.forced = cast(bool, data.attrib.get('forced', '0')) self.format = data.attrib.get('format') self.key = data.attrib.get('key') self.title = data.attrib.get('title') From d2c7feeaac3142a1c94fbac4eda2fe0b2cec3d39 Mon Sep 17 00:00:00 2001 From: Gabriel Stackhouse Date: Mon, 4 Feb 2019 13:45:49 -0600 Subject: [PATCH 18/24] Closes #334 - set default audio & subtitle stream --- plexapi/media.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plexapi/media.py b/plexapi/media.py index e0519ef1..394f091f 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -124,6 +124,25 @@ class MediaPart(PlexObject): def subtitleStreams(self): """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """ return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE] + + def setDefaultAudioStream(self, id): + """ Set the default :class:`~plexapi.media.AudioStream` for this MediaPart. + + Parameters: + id (int): ID of the AudioStream to set + """ + key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, id) + self._server.query(key, method=self._server._session.put) + + def setDefaultSubtitleStream(self, id): + """ Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart. + + Parameters: + id (int): ID of the SubtitleStream to set (0 for no subtitles) + """ + key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, id) + self._server.query(key, method=self._server._session.put) + class MediaPartStream(PlexObject): From a554aec87248b74d4b43f1ebbaf4c3bebb6d0991 Mon Sep 17 00:00:00 2001 From: Gabriel Stackhouse Date: Mon, 4 Feb 2019 13:52:16 -0600 Subject: [PATCH 19/24] Removed a space --- plexapi/media.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plexapi/media.py b/plexapi/media.py index 394f091f..e4af9a11 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -144,7 +144,6 @@ class MediaPart(PlexObject): self._server.query(key, method=self._server._session.put) - class MediaPartStream(PlexObject): """ Base class for media streams. These consist of video, audio and subtitles. From 1c95e7165c50b848b8253cb1feacbf2f28373d4a Mon Sep 17 00:00:00 2001 From: gstacks13 Date: Mon, 4 Feb 2019 16:28:30 -0600 Subject: [PATCH 20/24] Pass either stream or stream.id --- plexapi/media.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index e4af9a11..420c7949 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -125,22 +125,42 @@ class MediaPart(PlexObject): """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """ return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE] - def setDefaultAudioStream(self, id): + def setDefaultAudioStream(self, stream=None, streamID=None): """ Set the default :class:`~plexapi.media.AudioStream` for this MediaPart. Parameters: - id (int): ID of the AudioStream to set + stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default + (default:None; required if streamID is not specified). + streamID (int): ID of the AudioStream to set + (default:None; required if stream is not specified). + + Raises: + :class:`plexapi.exceptions.BadRequest`: If both stream and streamID are missing. """ - key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, id) + if stream: + key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id) + elif streamID: + key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, streamID) + else: + raise BadRequest('Missing argument: stream or streamID is required') self._server.query(key, method=self._server._session.put) - - def setDefaultSubtitleStream(self, id): + + def setDefaultSubtitleStream(self, stream=None, streamID=None): """ Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart. + (Note: pass no parameters to disable subtitles) Parameters: - id (int): ID of the SubtitleStream to set (0 for no subtitles) + stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default + (default:None). + streamID (int): ID of the AudioStream to set + (default:None). """ - key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, id) + if stream: + key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id) + elif streamID: + key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, streamID) + else: + key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, 0) self._server.query(key, method=self._server._session.put) From 5270395b3a9281b30e518f5e3de7430255193854 Mon Sep 17 00:00:00 2001 From: gstacks13 Date: Mon, 4 Feb 2019 20:07:22 -0600 Subject: [PATCH 21/24] Only pass stream object to function --- plexapi/media.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index 420c7949..7f0789fe 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -125,44 +125,40 @@ class MediaPart(PlexObject): """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects in this MediaPart. """ return [stream for stream in self.streams if stream.streamType == SubtitleStream.STREAMTYPE] - def setDefaultAudioStream(self, stream=None, streamID=None): + def setDefaultAudioStream(self, stream): """ Set the default :class:`~plexapi.media.AudioStream` for this MediaPart. Parameters: stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default - (default:None; required if streamID is not specified). - streamID (int): ID of the AudioStream to set - (default:None; required if stream is not specified). Raises: - :class:`plexapi.exceptions.BadRequest`: If both stream and streamID are missing. + :class:`plexapi.exceptions.BadRequest`: If stream is not an AudioStream. """ - if stream: + if type(stream) == AudioStream: key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id) - elif streamID: - key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, streamID) + self._server.query(key, method=self._server._session.put) else: - raise BadRequest('Missing argument: stream or streamID is required') - self._server.query(key, method=self._server._session.put) + raise BadRequest("Object 'stream' is not an AudioStream.") - def setDefaultSubtitleStream(self, stream=None, streamID=None): + def setDefaultSubtitleStream(self, stream): """ Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart. - (Note: pass no parameters to disable subtitles) - + Parameters: - stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default - (default:None). - streamID (int): ID of the AudioStream to set - (default:None). + stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default. + + Raises: + :class:`plexapi.exceptions.BadRequest`: If stream is not a SubtitleStream. """ - if stream: + if type(stream) == SubtitleStream: key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id) - elif streamID: - key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, streamID) + self._server.query(key, method=self._server._session.put) else: - key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, 0) - self._server.query(key, method=self._server._session.put) + raise BadRequest("Object 'stream' is not a SubtitleStream.") + def resetSubtitles(self): + """ Set default subtitle of this MediaPart to 'none'. """ + key = "/library/parts/%d?subtitleStreamID=0&allParts=1" % (self.id) + self._server.query(key, method=self._server._session.put) class MediaPartStream(PlexObject): """ Base class for media streams. These consist of video, audio and subtitles. From e830f74436bc3d535c43a4f07e028700c39b8e89 Mon Sep 17 00:00:00 2001 From: gstacks13 Date: Wed, 6 Feb 2019 17:22:28 -0600 Subject: [PATCH 22/24] Tidying up, as requested. --- plexapi/media.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index 7f0789fe..eecef1fd 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -130,32 +130,26 @@ class MediaPart(PlexObject): Parameters: stream (:class:`~plexapi.media.AudioStream`): AudioStream to set as default - - Raises: - :class:`plexapi.exceptions.BadRequest`: If stream is not an AudioStream. """ - if type(stream) == AudioStream: + if isinstance(stream, AudioStream): key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream.id) - self._server.query(key, method=self._server._session.put) else: - raise BadRequest("Object 'stream' is not an AudioStream.") + key = "/library/parts/%d?audioStreamID=%d&allParts=1" % (self.id, stream) + self._server.query(key, method=self._server._session.put) def setDefaultSubtitleStream(self, stream): """ Set the default :class:`~plexapi.media.SubtitleStream` for this MediaPart. Parameters: stream (:class:`~plexapi.media.SubtitleStream`): SubtitleStream to set as default. - - Raises: - :class:`plexapi.exceptions.BadRequest`: If stream is not a SubtitleStream. """ - if type(stream) == SubtitleStream: + if isinstance(stream, SubtitleStream): key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream.id) - self._server.query(key, method=self._server._session.put) else: - raise BadRequest("Object 'stream' is not a SubtitleStream.") + key = "/library/parts/%d?subtitleStreamID=%d&allParts=1" % (self.id, stream) + self._server.query(key, method=self._server._session.put) - def resetSubtitles(self): + def resetDefaultSubtitleStream(self): """ Set default subtitle of this MediaPart to 'none'. """ key = "/library/parts/%d?subtitleStreamID=0&allParts=1" % (self.id) self._server.query(key, method=self._server._session.put) From 6d135a7848929fe8e6387ce9bbe4251ed8b54b81 Mon Sep 17 00:00:00 2001 From: Michael Shepanski Date: Wed, 6 Feb 2019 18:47:35 -0500 Subject: [PATCH 23/24] Remove unused import. Flake8 caught this one. --- plexapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/utils.py b/plexapi/utils.py index 31a44514..4af0227d 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -5,7 +5,7 @@ import re import requests import time import zipfile -from datetime import datetime, timedelta +from datetime import datetime from getpass import getpass from threading import Thread, Event from tqdm import tqdm From 5980abe956fc54a78392daa0e34a56c188f4b4a4 Mon Sep 17 00:00:00 2001 From: Michael Shepanski Date: Wed, 6 Feb 2019 19:09:35 -0500 Subject: [PATCH 24/24] Triggering a build. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c0862dac..43dea560 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Plex Web Client. A few of the many features we currently support are: * Perform library actions such as scan, analyze, empty trash. * Remote control and play media on connected clients. * Listen in on all Plex Server notifications. - + Installation & Documentation ----------------------------