Skip to content

Commit 9b8c7d5

Browse files
authored
Properly close file object in mixin uploads (#1074)
* Properly close file object in mixin uploads * Also support a file-like object as the upload `filepath` parameter * Update image upload tests for file-like object
1 parent e1e9a69 commit 9b8c7d5

File tree

3 files changed

+39
-9
lines changed

3 files changed

+39
-9
lines changed

plexapi/mixins.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from plexapi import media, settings, utils
77
from plexapi.exceptions import BadRequest, NotFound
8-
from plexapi.utils import deprecated
8+
from plexapi.utils import deprecated, openOrRead
99

1010

1111
class AdvancedSettingsMixin:
@@ -341,14 +341,14 @@ def uploadArt(self, url=None, filepath=None):
341341
342342
Parameters:
343343
url (str): The full URL to the image to upload.
344-
filepath (str): The full file path the the image to upload.
344+
filepath (str): The full file path the the image to upload or file-like object.
345345
"""
346346
if url:
347347
key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}'
348348
self._server.query(key, method=self._server._session.post)
349349
elif filepath:
350350
key = f'/library/metadata/{self.ratingKey}/arts'
351-
data = open(filepath, 'rb').read()
351+
data = openOrRead(filepath)
352352
self._server.query(key, method=self._server._session.post, data=data)
353353
return self
354354

@@ -392,14 +392,14 @@ def uploadBanner(self, url=None, filepath=None):
392392
393393
Parameters:
394394
url (str): The full URL to the image to upload.
395-
filepath (str): The full file path the the image to upload.
395+
filepath (str): The full file path the the image to upload or file-like object.
396396
"""
397397
if url:
398398
key = f'/library/metadata/{self.ratingKey}/banners?url={quote_plus(url)}'
399399
self._server.query(key, method=self._server._session.post)
400400
elif filepath:
401401
key = f'/library/metadata/{self.ratingKey}/banners'
402-
data = open(filepath, 'rb').read()
402+
data = openOrRead(filepath)
403403
self._server.query(key, method=self._server._session.post, data=data)
404404
return self
405405

@@ -448,14 +448,14 @@ def uploadPoster(self, url=None, filepath=None):
448448
449449
Parameters:
450450
url (str): The full URL to the image to upload.
451-
filepath (str): The full file path the the image to upload.
451+
filepath (str): The full file path the the image to upload or file-like object.
452452
"""
453453
if url:
454454
key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}'
455455
self._server.query(key, method=self._server._session.post)
456456
elif filepath:
457457
key = f'/library/metadata/{self.ratingKey}/posters'
458-
data = open(filepath, 'rb').read()
458+
data = openOrRead(filepath)
459459
self._server.query(key, method=self._server._session.post, data=data)
460460
return self
461461

@@ -501,7 +501,7 @@ def uploadTheme(self, url=None, filepath=None, timeout=None):
501501
502502
Parameters:
503503
url (str): The full URL to the theme to upload.
504-
filepath (str): The full file path to the theme to upload.
504+
filepath (str): The full file path to the theme to upload or file-like object.
505505
timeout (int, optional): Timeout, in seconds, to use when uploading themes to the server.
506506
(default config.TIMEOUT).
507507
"""
@@ -510,7 +510,7 @@ def uploadTheme(self, url=None, filepath=None, timeout=None):
510510
self._server.query(key, method=self._server._session.post, timeout=timeout)
511511
elif filepath:
512512
key = f'/library/metadata/{self.ratingKey}/themes'
513-
data = open(filepath, 'rb').read()
513+
data = openOrRead(filepath)
514514
self._server.query(key, method=self._server._session.post, data=data, timeout=timeout)
515515
return self
516516

plexapi/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,3 +623,10 @@ def serialize(obj):
623623
return obj.isoformat()
624624
return {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
625625
return json.dumps(obj, default=serialize, **kwargs)
626+
627+
628+
def openOrRead(file):
629+
if hasattr(file, 'read'):
630+
return file.read()
631+
with open(file, 'rb') as f:
632+
return f.read()

tests/test_mixins.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def _test_mixins_field(obj, attr, field_method):
1717
edit_field_method = getattr(obj, "edit" + field_method)
1818
_value = lambda: getattr(obj, attr)
1919
_fields = lambda: [f for f in obj.fields if f.name == attr]
20+
2021
# Check field does not match to begin with
2122
default_value = _value()
2223
if isinstance(default_value, datetime):
@@ -26,13 +27,15 @@ def _test_mixins_field(obj, attr, field_method):
2627
else:
2728
test_value = TEST_MIXIN_FIELD
2829
assert default_value != test_value
30+
2931
# Edit and lock the field
3032
edit_field_method(test_value)
3133
obj.reload()
3234
value = _value()
3335
fields = _fields()
3436
assert value == test_value
3537
assert fields and fields[0].locked
38+
3639
# Reset and unlock the field to restore the clean state
3740
edit_field_method(default_value, locked=False)
3841
obj.reload()
@@ -229,6 +232,7 @@ def _test_mixins_edit_image(obj, attr):
229232
assert images[1].selected is True
230233
else:
231234
default_image = None
235+
232236
# Test upload image from file
233237
upload_img_method(filepath=utils.STUB_IMAGE_PATH)
234238
images = get_img_method()
@@ -237,9 +241,25 @@ def _test_mixins_edit_image(obj, attr):
237241
if i.ratingKey.startswith("upload://") and i.ratingKey.endswith(CUTE_CAT_SHA1)
238242
]
239243
assert file_image
244+
240245
# Reset to default image
241246
if default_image:
242247
set_img_method(default_image)
248+
249+
# Test upload image from file-like ojbect
250+
with open(utils.STUB_IMAGE_PATH, "rb") as f:
251+
upload_img_method(filepath=f)
252+
images = get_img_method()
253+
file_image = [
254+
i for i in images
255+
if i.ratingKey.startswith("upload://") and i.ratingKey.endswith(CUTE_CAT_SHA1)
256+
]
257+
assert file_image
258+
259+
# Reset to default image
260+
if default_image:
261+
set_img_method(default_image)
262+
243263
# Unlock the image
244264
unlock_img_method = getattr(obj, "unlock" + cap_attr)
245265
unlock_img_method()
@@ -283,6 +303,7 @@ def attr_posterUrl(obj):
283303

284304
def _test_mixins_edit_theme(obj):
285305
_fields = lambda: [f.name for f in obj.fields]
306+
286307
# Test upload theme from file
287308
obj.uploadTheme(filepath=utils.STUB_MP3_PATH)
288309
themes = obj.themes()
@@ -293,10 +314,12 @@ def _test_mixins_edit_theme(obj):
293314
assert file_theme
294315
obj.reload()
295316
assert "theme" in _fields()
317+
296318
# Unlock the theme
297319
obj.unlockTheme()
298320
obj.reload()
299321
assert "theme" not in _fields()
322+
300323
# Lock the theme
301324
obj.lockTheme()
302325
obj.reload()

0 commit comments

Comments
 (0)