Skip to content

Commit 74dff27

Browse files
authored
Merge branch 'master' into andy/add-python-versions
2 parents ad18a45 + 07674a2 commit 74dff27

File tree

11 files changed

+130
-17
lines changed

11 files changed

+130
-17
lines changed

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ the top left above your available libraries.
5353
plex = account.resource('<SERVERNAME>').connect() # returns a PlexServer instance
5454
5555
If you want to avoid logging into MyPlex and you already know your auth token
56-
string, you can use the PlexServer object directly as above, but passing in
56+
string, you can use the PlexServer object directly as above, by passing in
5757
the baseurl and auth token directly.
5858

5959
.. code-block:: python

plexapi/audio.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Audio(PlexPartialObject):
1111
1212
Attributes:
1313
addedAt (datetime): Datetime this item was added to the library.
14+
fields (list): List of :class:`~plexapi.media.Field`.
1415
index (sting): Index Number (often the track number).
1516
key (str): API URL (/library/metadata/<ratingkey>).
1617
lastViewedAt (datetime): Datetime item was last accessed.
@@ -33,6 +34,7 @@ def _loadData(self, data):
3334
self._data = data
3435
self.listType = 'audio'
3536
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
37+
self.fields = self.findItems(data, etag='Field')
3638
self.index = data.attrib.get('index')
3739
self.key = data.attrib.get('key')
3840
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))

plexapi/client.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def __init__(self, server=None, data=None, initpath=None, baseurl=None,
6969
self._proxyThroughServer = False
7070
self._commandId = 0
7171
self._last_call = 0
72-
if not any([data, initpath, baseurl, token]):
72+
if not any([data is not None, initpath, baseurl, token]):
7373
self._baseurl = CONFIG.get('auth.client_baseurl', 'http://localhost:32433')
7474
self._token = logfilter.add_secret(CONFIG.get('auth.client_token'))
7575
if connect and self._baseurl:

plexapi/library.py

+73-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from urllib.parse import quote, quote_plus, unquote, urlencode
33

44
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
5-
from plexapi.base import PlexObject
5+
from plexapi.base import PlexObject, PlexPartialObject
66
from plexapi.exceptions import BadRequest, NotFound
77
from plexapi.media import MediaTag
88
from plexapi.settings import Setting
@@ -657,6 +657,11 @@ def _cleanSearchSort(self, sort):
657657
raise BadRequest('Unknown sort dir: %s' % sdir)
658658
return '%s:%s' % (lookup[scol], sdir)
659659

660+
def _locations(self):
661+
""" Returns a list of :class:`~plexapi.library.Location` objects
662+
"""
663+
return self.findItems(self._data, etag='Location')
664+
660665
def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
661666
**kwargs):
662667
""" Add current library section as sync item for specified device.
@@ -1054,6 +1059,23 @@ def _loadData(self, data):
10541059
self.title = data.attrib.get('title')
10551060
self.type = data.attrib.get('type')
10561061

1062+
@utils.registerPlexObject
1063+
class Location(PlexObject):
1064+
""" Represents a single library Location.
1065+
1066+
Attributes:
1067+
TAG (str): 'Location'
1068+
id (int): Location path ID.
1069+
path (str): Path used for library..
1070+
"""
1071+
TAG = 'Location'
1072+
1073+
def _loadData(self, data):
1074+
""" Load attribute values from Plex XML response. """
1075+
self._data = data
1076+
self.id = utils.cast(int, data.attrib.get('id'))
1077+
self.path = data.attrib.get('path')
1078+
10571079

10581080
@utils.registerPlexObject
10591081
class Hub(PlexObject):
@@ -1084,7 +1106,38 @@ def __len__(self):
10841106

10851107

10861108
@utils.registerPlexObject
1087-
class Collections(PlexObject):
1109+
class Collections(PlexPartialObject):
1110+
""" Represents a single Collection.
1111+
1112+
Attributes:
1113+
TAG (str): 'Directory'
1114+
TYPE (str): 'collection'
1115+
1116+
ratingKey (int): Unique key identifying this item.
1117+
addedAt (datetime): Datetime this item was added to the library.
1118+
childCount (int): Count of child object(s)
1119+
collectionMode (str): How the items in the collection are displayed.
1120+
collectionSort (str): How to sort the items in the collection.
1121+
contentRating (str) Content rating (PG-13; NR; TV-G).
1122+
fields (list): List of :class:`~plexapi.media.Field`.
1123+
guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
1124+
index (int): Unknown
1125+
key (str): API URL (/library/metadata/<ratingkey>).
1126+
labels (List<:class:`~plexapi.media.Label`>): List of field objects.
1127+
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
1128+
librarySectionKey (str): API URL (/library/sections/<sectionkey>).
1129+
librarySectionTitle (str): Section Title
1130+
maxYear (int): Year
1131+
minYear (int): Year
1132+
subtype (str): Media type
1133+
summary (str): Summary of the collection
1134+
thumb (str): URL to thumbnail image.
1135+
title (str): Collection Title
1136+
titleSort (str): Title to use when sorting (defaults to title).
1137+
type (str): Hardcoded 'collection'
1138+
updatedAt (datatime): Datetime this item was updated.
1139+
1140+
"""
10881141

10891142
TAG = 'Directory'
10901143
TYPE = 'collection'
@@ -1093,20 +1146,29 @@ class Collections(PlexObject):
10931146
def _loadData(self, data):
10941147
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
10951148
self._details_key = "/library/metadata/%s%s" % (self.ratingKey, self._include)
1149+
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
1150+
self.art = data.attrib.get('art')
1151+
self.childCount = utils.cast(int, data.attrib.get('childCount'))
1152+
self.collectionMode = data.attrib.get('collectionMode')
1153+
self.collectionSort = data.attrib.get('collectionSort')
1154+
self.contentRating = data.attrib.get('contentRating')
1155+
self.fields = self.findItems(data, etag='Field')
1156+
self.guid = data.attrib.get('guid')
1157+
self.index = utils.cast(int, data.attrib.get('index'))
10961158
self.key = data.attrib.get('key')
1097-
self.type = data.attrib.get('type')
1098-
self.title = data.attrib.get('title')
1159+
self.labels = self.findItems(data, etag='Label')
1160+
self.librarySectionID = data.attrib.get('librarySectionID')
1161+
self.librarySectionKey = data.attrib.get('librarySectionKey')
1162+
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
1163+
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
1164+
self.minYear = utils.cast(int, data.attrib.get('minYear'))
10991165
self.subtype = data.attrib.get('subtype')
11001166
self.summary = data.attrib.get('summary')
1101-
self.index = utils.cast(int, data.attrib.get('index'))
11021167
self.thumb = data.attrib.get('thumb')
1103-
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
1168+
self.title = data.attrib.get('title')
1169+
self.titleSort = data.attrib.get('titleSort')
1170+
self.type = data.attrib.get('type')
11041171
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
1105-
self.childCount = utils.cast(int, data.attrib.get('childCount'))
1106-
self.minYear = utils.cast(int, data.attrib.get('minYear'))
1107-
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
1108-
self.collectionMode = data.attrib.get('collectionMode')
1109-
self.collectionSort = data.attrib.get('collectionSort')
11101172

11111173
@property
11121174
def children(self):

plexapi/myplex.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def _loadData(self, data):
139139

140140
roles = data.find('roles')
141141
self.roles = []
142-
if roles:
142+
if roles is not None:
143143
for role in roles.iter('role'):
144144
self.roles.append(role.attrib.get('id'))
145145

plexapi/photo.py

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Photoalbum(PlexPartialObject):
1616
addedAt (datetime): Datetime this item was added to the library.
1717
art (str): Photo art (/library/metadata/<ratingkey>/art/<artid>)
1818
composite (str): Unknown
19+
fields (list): List of :class:`~plexapi.media.Field`.
1920
guid (str): Unknown (unique ID)
2021
index (sting): Index number of this album.
2122
key (str): API URL (/library/metadata/<ratingkey>).
@@ -37,6 +38,7 @@ def _loadData(self, data):
3738
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
3839
self.art = data.attrib.get('art')
3940
self.composite = data.attrib.get('composite')
41+
self.fields = self.findItems(data, etag='Field')
4042
self.guid = data.attrib.get('guid')
4143
self.index = utils.cast(int, data.attrib.get('index'))
4244
self.key = data.attrib.get('key')
@@ -81,6 +83,7 @@ class Photo(PlexPartialObject):
8183
TAG (str): 'Photo'
8284
TYPE (str): 'photo'
8385
addedAt (datetime): Datetime this item was added to the library.
86+
fields (list): List of :class:`~plexapi.media.Field`.
8487
index (sting): Index number of this photo.
8588
key (str): API URL (/library/metadata/<ratingkey>).
8689
listType (str): Hardcoded as 'photo' (useful for search filters).
@@ -104,6 +107,7 @@ def _loadData(self, data):
104107
""" Load attribute values from Plex XML response. """
105108
self.listType = 'photo'
106109
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
110+
self.fields = self.findItems(data, etag='Field')
107111
self.index = utils.cast(int, data.attrib.get('index'))
108112
self.key = data.attrib.get('key')
109113
self.originallyAvailableAt = utils.toDatetime(

plexapi/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def lowerFirst(s):
110110

111111
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
112112
""" Returns the value at the specified attrstr location within a nexted tree of
113-
dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley
113+
dicts, lists, tuples, functions, classes, etc. The lookup is done recursively
114114
for each key in attrstr (split by by the delimiter) This function is heavily
115115
influenced by the lookups used in Django templates.
116116

plexapi/video.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Video(PlexPartialObject):
1414
1515
Attributes:
1616
addedAt (datetime): Datetime this item was added to the library.
17+
fields (list): List of :class:`~plexapi.media.Field`.
1718
key (str): API URL (/library/metadata/<ratingkey>).
1819
lastViewedAt (datetime): Datetime item was last accessed.
1920
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
@@ -33,6 +34,8 @@ def _loadData(self, data):
3334
self._data = data
3435
self.listType = 'video'
3536
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
37+
self.art = data.attrib.get('art')
38+
self.fields = self.findItems(data, etag='Field')
3639
self.key = data.attrib.get('key', '')
3740
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
3841
self.librarySectionID = data.attrib.get('librarySectionID')
@@ -126,8 +129,9 @@ def optimize(self, title=None, target="", targetTagID=None, locationID=-1, polic
126129
policyValue="", policyUnwatched=0, videoQuality=None, deviceProfile=None):
127130
""" Optimize item
128131
129-
locationID (int): -1 in folder with orginal items
130-
2 library path
132+
locationID (int): -1 in folder with original items
133+
2 library path id
134+
library path id is found in library.locations[i].id
131135
132136
target (str): custom quality name.
133137
if none provided use "Custom: {deviceProfile}"
@@ -157,6 +161,13 @@ def optimize(self, title=None, target="", targetTagID=None, locationID=-1, polic
157161
if targetTagID not in tagIDs and (deviceProfile is None or videoQuality is None):
158162
raise BadRequest('Unexpected or missing quality profile.')
159163

164+
libraryLocationIDs = [location.id for location in self.section()._locations()]
165+
libraryLocationIDs.append(-1)
166+
167+
if locationID not in libraryLocationIDs:
168+
raise BadRequest('Unexpected library path ID. %s not in %s' %
169+
(locationID, libraryLocationIDs))
170+
160171
if isinstance(targetTagID, str):
161172
tagIndex = tagKeys.index(targetTagID)
162173
targetTagID = tagValues[tagIndex]

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@
4141
classifiers=[
4242
'Operating System :: OS Independent',
4343
'Programming Language :: Python :: 3',
44+
'License :: OSI Approved :: BSD License',
4445
]
4546
)

tests/test_library.py

+12
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,18 @@ def test_library_Colletion_sortRelease(collection):
238238
assert collection.collectionSort == "0"
239239

240240

241+
def test_library_Colletion_edit(collection):
242+
edits = {'titleSort.value': 'New Title Sort', 'titleSort.locked': 1}
243+
collectionTitleSort = collection.titleSort
244+
collection.edit(**edits)
245+
collection.reload()
246+
for field in collection.fields:
247+
if field.name == 'titleSort':
248+
assert collection.titleSort == 'New Title Sort'
249+
assert field.locked == True
250+
collection.edit(**{'titleSort.value': collectionTitleSort, 'titleSort.locked': 0})
251+
252+
241253
def test_search_with_weird_a(plex):
242254
ep_title = "Coup de Grâce"
243255
result_root = plex.search(ep_title)

tests/test_video.py

+21
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,27 @@ def test_video_exists_accessible(movie, episode):
880880
assert episode.media[0].parts[0].accessible is True
881881

882882

883+
def test_video_edits_locked(movie, episode):
884+
edits = {'titleSort.value':'New Title Sort', 'titleSort.locked': 1}
885+
movieTitleSort = movie.titleSort
886+
movie.edit(**edits)
887+
movie.reload()
888+
for field in movie.fields:
889+
if field.name == 'titleSort':
890+
assert movie.titleSort == 'New Title Sort'
891+
assert field.locked == True
892+
movie.edit(**{'titleSort.value': movieTitleSort, 'titleSort.locked': 0})
893+
894+
episodeTitleSort = episode.titleSort
895+
episode.edit(**edits)
896+
episode.reload()
897+
for field in episode.fields:
898+
if field.name == 'titleSort':
899+
assert episode.titleSort == 'New Title Sort'
900+
assert field.locked == True
901+
episode.edit(**{'titleSort.value': episodeTitleSort, 'titleSort.locked': 0})
902+
903+
883904
@pytest.mark.skip(
884905
reason="broken? assert len(plex.conversions()) == 1 may fail on some builds"
885906
)

0 commit comments

Comments
 (0)