Skip to content

Commit 8830954

Browse files
authored
Add image tags and movie/show logos (#1462)
* Add image tags * Test image tags * Add logo resources * Ignore flake8 C901 for movie attr test
1 parent d6c5f09 commit 8830954

File tree

11 files changed

+110
-5
lines changed

11 files changed

+110
-5
lines changed

plexapi/audio.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
3333
distance (float): Sonic Distance of the item from the seed item.
3434
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
3535
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).
36+
images (List<:class:`~plexapi.media.Image`>): List of image objects.
3637
index (int): Plex index number (often the track number).
3738
key (str): API URL (/library/metadata/<ratingkey>).
3839
lastRatedAt (datetime): Datetime the item was last rated.
@@ -65,6 +66,7 @@ def _loadData(self, data):
6566
self.distance = utils.cast(float, data.attrib.get('distance'))
6667
self.fields = self.findItems(data, media.Field)
6768
self.guid = data.attrib.get('guid')
69+
self.images = self.findItems(data, media.Image)
6870
self.index = utils.cast(int, data.attrib.get('index'))
6971
self.key = data.attrib.get('key', '')
7072
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

plexapi/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(self, server, data, initpath=None, parent=None):
7171
self._details_key = self._buildDetailsKey()
7272

7373
def __repr__(self):
74-
uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri'))
74+
uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri', 'type'))
7575
name = self._clean(self.firstAttr('title', 'name', 'username', 'product', 'tag', 'value'))
7676
return f"<{':'.join([p for p in [self.__class__.__name__, uid, name] if p])}>"
7777

plexapi/collection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Collection(
3939
contentRating (str) Content rating (PG-13; NR; TV-G).
4040
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
4141
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
42+
images (List<:class:`~plexapi.media.Image`>): List of image objects.
4243
index (int): Plex index number for the collection.
4344
key (str): API URL (/library/metadata/<ratingkey>).
4445
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
@@ -82,6 +83,7 @@ def _loadData(self, data):
8283
self.contentRating = data.attrib.get('contentRating')
8384
self.fields = self.findItems(data, media.Field)
8485
self.guid = data.attrib.get('guid')
86+
self.images = self.findItems(data, media.Image)
8587
self.index = utils.cast(int, data.attrib.get('index'))
8688
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
8789
self.labels = self.findItems(data, media.Label)

plexapi/media.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,26 @@ def _loadData(self, data):
958958
self.id = data.attrib.get('id')
959959

960960

961+
@utils.registerPlexObject
962+
class Image(PlexObject):
963+
""" Represents a single Image media tag.
964+
965+
Attributes:
966+
TAG (str): 'Image'
967+
alt (str): The alt text for the image.
968+
type (str): The type of image (e.g. coverPoster, background, snapshot).
969+
url (str): The API URL (/library/metadata/<ratingKey>/thumb/<thumbid>).
970+
"""
971+
TAG = 'Image'
972+
973+
def _loadData(self, data):
974+
""" Load attribute values from Plex XML response. """
975+
self._data = data
976+
self.alt = data.attrib.get('alt')
977+
self.type = data.attrib.get('type')
978+
self.url = data.attrib.get('url')
979+
980+
961981
@utils.registerPlexObject
962982
class Rating(PlexObject):
963983
""" Represents a single Rating media tag.
@@ -1078,6 +1098,11 @@ class Art(BaseResource):
10781098
TAG = 'Photo'
10791099

10801100

1101+
class Logo(BaseResource):
1102+
""" Represents a single Logo object. """
1103+
TAG = 'Photo'
1104+
1105+
10811106
class Poster(BaseResource):
10821107
""" Represents a single Poster object. """
10831108
TAG = 'Photo'

plexapi/mixins.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,63 @@ def setArt(self, art):
403403
return self
404404

405405

406+
class LogoUrlMixin:
407+
""" Mixin for Plex objects that can have a logo url. """
408+
409+
@property
410+
def logoUrl(self):
411+
""" Return the logo url for the Plex object. """
412+
image = next((i for i in self.images if i.type == 'clearLogo'), None)
413+
return self._server.url(image.url, includeToken=True) if image else None
414+
415+
416+
class LogoLockMixin:
417+
""" Mixin for Plex objects that can have a locked logo. """
418+
419+
def lockLogo(self):
420+
""" Lock the logo for a Plex object. """
421+
raise NotImplementedError('Logo cannot be locked through the API.')
422+
423+
def unlockLogo(self):
424+
""" Unlock the logo for a Plex object. """
425+
raise NotImplementedError('Logo cannot be unlocked through the API.')
426+
427+
428+
class LogoMixin(LogoUrlMixin, LogoLockMixin):
429+
""" Mixin for Plex objects that can have logos. """
430+
431+
def logos(self):
432+
""" Returns list of available :class:`~plexapi.media.Logo` objects. """
433+
return self.fetchItems(f'/library/metadata/{self.ratingKey}/clearLogos', cls=media.Logo)
434+
435+
def uploadLogo(self, url=None, filepath=None):
436+
""" Upload a logo from a url or filepath.
437+
438+
Parameters:
439+
url (str): The full URL to the image to upload.
440+
filepath (str): The full file path the the image to upload or file-like object.
441+
"""
442+
if url:
443+
key = f'/library/metadata/{self.ratingKey}/clearLogos?url={quote_plus(url)}'
444+
self._server.query(key, method=self._server._session.post)
445+
elif filepath:
446+
key = f'/library/metadata/{self.ratingKey}/clearLogos'
447+
data = openOrRead(filepath)
448+
self._server.query(key, method=self._server._session.post, data=data)
449+
return self
450+
451+
def setLogo(self, logo):
452+
""" Set the logo for a Plex object.
453+
454+
Raises:
455+
:exc:`~plexapi.exceptions.NotImplementedError`: Logo cannot be set through the API.
456+
"""
457+
raise NotImplementedError(
458+
'Logo cannot be set through the API. '
459+
'Re-upload the logo using "uploadLogo" to set it.'
460+
)
461+
462+
406463
class PosterUrlMixin:
407464
""" Mixin for Plex objects that can have a poster url. """
408465

@@ -513,6 +570,11 @@ def uploadTheme(self, url=None, filepath=None, timeout=None):
513570
return self
514571

515572
def setTheme(self, theme):
573+
""" Set the theme for a Plex object.
574+
575+
Raises:
576+
:exc:`~plexapi.exceptions.NotImplementedError`: Themes cannot be set through the API.
577+
"""
516578
raise NotImplementedError(
517579
'Themes cannot be set through the API. '
518580
'Re-upload the theme using "uploadTheme" to set it.'

plexapi/photo.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Photoalbum(
3030
composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)
3131
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
3232
guid (str): Plex GUID for the photo album (local://229674).
33+
images (List<:class:`~plexapi.media.Image`>): List of image objects.
3334
index (sting): Plex index number for the photo album.
3435
key (str): API URL (/library/metadata/<ratingkey>).
3536
lastRatedAt (datetime): Datetime the photo album was last rated.
@@ -57,6 +58,7 @@ def _loadData(self, data):
5758
self.composite = data.attrib.get('composite')
5859
self.fields = self.findItems(data, media.Field)
5960
self.guid = data.attrib.get('guid')
61+
self.images = self.findItems(data, media.Image)
6062
self.index = utils.cast(int, data.attrib.get('index'))
6163
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
6264
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
@@ -164,6 +166,7 @@ class Photo(
164166
createdAtTZOffset (int): Unknown (-25200).
165167
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
166168
guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn).
169+
images (List<:class:`~plexapi.media.Image`>): List of image objects.
167170
index (sting): Plex index number for the photo.
168171
key (str): API URL (/library/metadata/<ratingkey>).
169172
lastRatedAt (datetime): Datetime the photo was last rated.
@@ -204,6 +207,7 @@ def _loadData(self, data):
204207
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
205208
self.fields = self.findItems(data, media.Field)
206209
self.guid = data.attrib.get('guid')
210+
self.images = self.findItems(data, media.Image)
207211
self.index = utils.cast(int, data.attrib.get('index'))
208212
self.key = data.attrib.get('key', '')
209213
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

plexapi/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@
9090
'theme': 317,
9191
'studio': 318,
9292
'network': 319,
93+
'showOrdering': 322,
94+
'clearLogo': 323,
9395
'place': 400,
9496
}
9597
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}

plexapi/video.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from plexapi.exceptions import BadRequest
1010
from plexapi.mixins import (
1111
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
12-
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
12+
ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
1313
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
1414
WatchlistMixin
1515
)
@@ -26,6 +26,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
2626
artBlurHash (str): BlurHash string for artwork image.
2727
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
2828
guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8).
29+
images (List<:class:`~plexapi.media.Image`>): List of image objects.
2930
key (str): API URL (/library/metadata/<ratingkey>).
3031
lastRatedAt (datetime): Datetime the item was last rated.
3132
lastViewedAt (datetime): Datetime the item was last played.
@@ -53,6 +54,7 @@ def _loadData(self, data):
5354
self.artBlurHash = data.attrib.get('artBlurHash')
5455
self.fields = self.findItems(data, media.Field)
5556
self.guid = data.attrib.get('guid')
57+
self.images = self.findItems(data, media.Image)
5658
self.key = data.attrib.get('key', '')
5759
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
5860
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
@@ -332,7 +334,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F
332334
class Movie(
333335
Video, Playable,
334336
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
335-
ArtMixin, PosterMixin, ThemeMixin,
337+
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
336338
MovieEditMixins,
337339
WatchlistMixin
338340
):
@@ -494,7 +496,7 @@ def metadataDirectory(self):
494496
class Show(
495497
Video,
496498
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
497-
ArtMixin, PosterMixin, ThemeMixin,
499+
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
498500
ShowEditMixins,
499501
WatchlistMixin
500502
):

tests/test_audio.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def test_audio_Artist_attr(artist):
2424
# assert "Electronic" in [i.tag for i in artist.genres]
2525
assert artist.guid in artist_guids
2626
assert artist_guids[0] in [i.id for i in artist.guids]
27+
if artist.images:
28+
assert any("coverPoster" in i.type for i in artist.images)
2729
assert artist.index == 1
2830
assert utils.is_metadata(artist._initpath)
2931
assert utils.is_metadata(artist.key)

tests/test_collection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def test_Collection_attrs(collection):
4747
assert collection.isVideo is True
4848
assert collection.isAudio is False
4949
assert collection.isPhoto is False
50+
if collection.images:
51+
assert any("coverPoster" in i.type for i in collection.images)
5052

5153

5254
def test_Collection_section(collection, movies):

0 commit comments

Comments
 (0)