diff --git a/README.rst b/README.rst index 9237fca0..fafcf48e 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ the top left above your available libraries. plex = account.resource('').connect() # returns a PlexServer instance If you want to avoid logging into MyPlex and you already know your auth token -string, you can use the PlexServer object directly as above, but passing in +string, you can use the PlexServer object directly as above, by passing in the baseurl and auth token directly. .. code-block:: python diff --git a/plexapi/audio.py b/plexapi/audio.py index b48dc8c5..d6ff5943 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -11,6 +11,7 @@ class Audio(PlexPartialObject): Attributes: addedAt (datetime): Datetime this item was added to the library. + fields (list): List of :class:`~plexapi.media.Field`. index (sting): Index Number (often the track number). key (str): API URL (/library/metadata/). lastViewedAt (datetime): Datetime item was last accessed. @@ -33,6 +34,7 @@ class Audio(PlexPartialObject): self._data = data self.listType = 'audio' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) + self.fields = self.findItems(data, etag='Field') self.index = data.attrib.get('index') self.key = data.attrib.get('key') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) diff --git a/plexapi/client.py b/plexapi/client.py index 34b9ac48..9e720d62 100644 --- a/plexapi/client.py +++ b/plexapi/client.py @@ -69,7 +69,7 @@ class PlexClient(PlexObject): self._proxyThroughServer = False self._commandId = 0 self._last_call = 0 - if not any([data, initpath, baseurl, token]): + 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')) if connect and self._baseurl: diff --git a/plexapi/library.py b/plexapi/library.py index 397f5c2e..b0fafa38 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -2,7 +2,7 @@ from urllib.parse import quote, quote_plus, unquote, urlencode from plexapi import X_PLEX_CONTAINER_SIZE, log, utils -from plexapi.base import PlexObject +from plexapi.base import PlexObject, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound from plexapi.media import MediaTag from plexapi.settings import Setting @@ -657,6 +657,11 @@ class LibrarySection(PlexObject): raise BadRequest('Unknown sort dir: %s' % sdir) return '%s:%s' % (lookup[scol], sdir) + def _locations(self): + """ Returns a list of :class:`~plexapi.library.Location` objects + """ + return self.findItems(self._data, etag='Location') + def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None, **kwargs): """ Add current library section as sync item for specified device. @@ -1054,6 +1059,23 @@ class FilterChoice(PlexObject): self.title = data.attrib.get('title') self.type = data.attrib.get('type') +@utils.registerPlexObject +class Location(PlexObject): + """ Represents a single library Location. + + Attributes: + TAG (str): 'Location' + id (int): Location path ID. + path (str): Path used for library.. + """ + TAG = 'Location' + + def _loadData(self, data): + """ Load attribute values from Plex XML response. """ + self._data = data + self.id = utils.cast(int, data.attrib.get('id')) + self.path = data.attrib.get('path') + @utils.registerPlexObject class Hub(PlexObject): @@ -1084,7 +1106,38 @@ class Hub(PlexObject): @utils.registerPlexObject -class Collections(PlexObject): +class Collections(PlexPartialObject): + """ Represents a single Collection. + + Attributes: + TAG (str): 'Directory' + TYPE (str): 'collection' + + ratingKey (int): Unique key identifying this item. + addedAt (datetime): Datetime this item was added to the library. + childCount (int): Count of child object(s) + collectionMode (str): How the items in the collection are displayed. + collectionSort (str): How to sort the items in the collection. + contentRating (str) Content rating (PG-13; NR; TV-G). + fields (list): List of :class:`~plexapi.media.Field`. + guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). + index (int): Unknown + key (str): API URL (/library/metadata/). + labels (List<:class:`~plexapi.media.Label`>): List of field objects. + librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. + librarySectionKey (str): API URL (/library/sections/). + librarySectionTitle (str): Section Title + maxYear (int): Year + minYear (int): Year + subtype (str): Media type + summary (str): Summary of the collection + thumb (str): URL to thumbnail image. + title (str): Collection Title + titleSort (str): Title to use when sorting (defaults to title). + type (str): Hardcoded 'collection' + updatedAt (datatime): Datetime this item was updated. + + """ TAG = 'Directory' TYPE = 'collection' @@ -1093,20 +1146,29 @@ class Collections(PlexObject): def _loadData(self, data): self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self._details_key = "/library/metadata/%s%s" % (self.ratingKey, self._include) - self.key = data.attrib.get('key') - self.type = data.attrib.get('type') - self.title = data.attrib.get('title') - self.subtype = data.attrib.get('subtype') - self.summary = data.attrib.get('summary') - self.index = utils.cast(int, data.attrib.get('index')) - self.thumb = data.attrib.get('thumb') self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) - self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.art = data.attrib.get('art') self.childCount = utils.cast(int, data.attrib.get('childCount')) - self.minYear = utils.cast(int, data.attrib.get('minYear')) - self.maxYear = utils.cast(int, data.attrib.get('maxYear')) self.collectionMode = data.attrib.get('collectionMode') self.collectionSort = data.attrib.get('collectionSort') + self.contentRating = data.attrib.get('contentRating') + self.fields = self.findItems(data, etag='Field') + self.guid = data.attrib.get('guid') + self.index = utils.cast(int, data.attrib.get('index')) + self.key = data.attrib.get('key') + self.labels = self.findItems(data, etag='Label') + self.librarySectionID = data.attrib.get('librarySectionID') + self.librarySectionKey = data.attrib.get('librarySectionKey') + self.librarySectionTitle = data.attrib.get('librarySectionTitle') + self.maxYear = utils.cast(int, data.attrib.get('maxYear')) + self.minYear = utils.cast(int, data.attrib.get('minYear')) + self.subtype = data.attrib.get('subtype') + self.summary = data.attrib.get('summary') + self.thumb = data.attrib.get('thumb') + self.title = data.attrib.get('title') + self.titleSort = data.attrib.get('titleSort') + self.type = data.attrib.get('type') + self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) @property def children(self): diff --git a/plexapi/myplex.py b/plexapi/myplex.py index 5585c885..8806fdb5 100644 --- a/plexapi/myplex.py +++ b/plexapi/myplex.py @@ -139,7 +139,7 @@ class MyPlexAccount(PlexObject): roles = data.find('roles') self.roles = [] - if roles: + if roles is not None: for role in roles.iter('role'): self.roles.append(role.attrib.get('id')) diff --git a/plexapi/photo.py b/plexapi/photo.py index 52bd8bf7..a8fc1c70 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -16,6 +16,7 @@ class Photoalbum(PlexPartialObject): addedAt (datetime): Datetime this item was added to the library. art (str): Photo art (/library/metadata//art/) composite (str): Unknown + fields (list): List of :class:`~plexapi.media.Field`. guid (str): Unknown (unique ID) index (sting): Index number of this album. key (str): API URL (/library/metadata/). @@ -37,6 +38,7 @@ class Photoalbum(PlexPartialObject): self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.composite = data.attrib.get('composite') + self.fields = self.findItems(data, etag='Field') self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') @@ -81,6 +83,7 @@ class Photo(PlexPartialObject): TAG (str): 'Photo' TYPE (str): 'photo' addedAt (datetime): Datetime this item was added to the library. + fields (list): List of :class:`~plexapi.media.Field`. index (sting): Index number of this photo. key (str): API URL (/library/metadata/). listType (str): Hardcoded as 'photo' (useful for search filters). @@ -104,6 +107,7 @@ class Photo(PlexPartialObject): """ Load attribute values from Plex XML response. """ self.listType = 'photo' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) + self.fields = self.findItems(data, etag='Field') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key') self.originallyAvailableAt = utils.toDatetime( diff --git a/plexapi/utils.py b/plexapi/utils.py index 01e91830..58e9be0b 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -110,7 +110,7 @@ def lowerFirst(s): def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover """ Returns the value at the specified attrstr location within a nexted tree of - dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley + dicts, lists, tuples, functions, classes, etc. The lookup is done recursively for each key in attrstr (split by by the delimiter) This function is heavily influenced by the lookups used in Django templates. diff --git a/plexapi/video.py b/plexapi/video.py index 5396d87f..526bfcce 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -14,6 +14,7 @@ class Video(PlexPartialObject): Attributes: addedAt (datetime): Datetime this item was added to the library. + fields (list): List of :class:`~plexapi.media.Field`. key (str): API URL (/library/metadata/). lastViewedAt (datetime): Datetime item was last accessed. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. @@ -33,6 +34,8 @@ class Video(PlexPartialObject): self._data = data self.listType = 'video' self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) + self.art = data.attrib.get('art') + self.fields = self.findItems(data, etag='Field') self.key = data.attrib.get('key', '') self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.librarySectionID = data.attrib.get('librarySectionID') @@ -126,8 +129,9 @@ class Video(PlexPartialObject): policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None): """ Optimize item - locationID (int): -1 in folder with orginal items - 2 library path + locationID (int): -1 in folder with original items + 2 library path id + library path id is found in library.locations[i].id target (str): custom quality name. if none provided use "Custom: {deviceProfile}" @@ -157,6 +161,13 @@ class Video(PlexPartialObject): if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None): raise BadRequest('Unexpected or missing quality profile.') + libraryLocationIDs = [location.id for location in self.section()._locations()] + libraryLocationIDs.append(-1) + + if locationID not in libraryLocationIDs: + raise BadRequest('Unexpected library path ID. %s not in %s' % + (locationID, libraryLocationIDs)) + if isinstance(targetTagID, str): tagIndex = tagKeys.index(targetTagID) targetTagID = tagValues[tagIndex] diff --git a/setup.py b/setup.py index 03bb9613..9bdb3527 100644 --- a/setup.py +++ b/setup.py @@ -41,5 +41,6 @@ setup( classifiers=[ 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: BSD License', ] ) diff --git a/tests/test_library.py b/tests/test_library.py index 17f8f54a..0fc29837 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -238,6 +238,18 @@ def test_library_Colletion_sortRelease(collection): assert collection.collectionSort == "0" +def test_library_Colletion_edit(collection): + edits = {'titleSort.value': 'New Title Sort', 'titleSort.locked': 1} + collectionTitleSort = collection.titleSort + collection.edit(**edits) + collection.reload() + for field in collection.fields: + if field.name == 'titleSort': + assert collection.titleSort == 'New Title Sort' + assert field.locked == True + collection.edit(**{'titleSort.value': collectionTitleSort, 'titleSort.locked': 0}) + + def test_search_with_weird_a(plex): ep_title = "Coup de GrĂ¢ce" result_root = plex.search(ep_title) diff --git a/tests/test_video.py b/tests/test_video.py index 139031e0..c1ae4eca 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -880,6 +880,27 @@ def test_video_exists_accessible(movie, episode): assert episode.media[0].parts[0].accessible is True +def test_video_edits_locked(movie, episode): + edits = {'titleSort.value':'New Title Sort', 'titleSort.locked': 1} + movieTitleSort = movie.titleSort + movie.edit(**edits) + movie.reload() + for field in movie.fields: + if field.name == 'titleSort': + assert movie.titleSort == 'New Title Sort' + assert field.locked == True + movie.edit(**{'titleSort.value': movieTitleSort, 'titleSort.locked': 0}) + + episodeTitleSort = episode.titleSort + episode.edit(**edits) + episode.reload() + for field in episode.fields: + if field.name == 'titleSort': + assert episode.titleSort == 'New Title Sort' + assert field.locked == True + episode.edit(**{'titleSort.value': episodeTitleSort, 'titleSort.locked': 0}) + + @pytest.mark.skip( reason="broken? assert len(plex.conversions()) == 1 may fail on some builds" )