Skip to content

Commit

Permalink
Add image tags and movie/show logos (#1462)
Browse files Browse the repository at this point in the history
* Add image tags

* Test image tags

* Add logo resources

* Ignore flake8 C901 for movie attr test
  • Loading branch information
JonnyWong16 authored Nov 16, 2024
1 parent d6c5f09 commit 8830954
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 5 deletions.
2 changes: 2 additions & 0 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
distance (float): Sonic Distance of the item from the seed item.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (int): Plex index number (often the track number).
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated.
Expand Down Expand Up @@ -65,6 +66,7 @@ def _loadData(self, data):
self.distance = utils.cast(float, data.attrib.get('distance'))
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
Expand Down
2 changes: 1 addition & 1 deletion plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self, server, data, initpath=None, parent=None):
self._details_key = self._buildDetailsKey()

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

Expand Down
2 changes: 2 additions & 0 deletions plexapi/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Collection(
contentRating (str) Content rating (PG-13; NR; TV-G).
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (int): Plex index number for the collection.
key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
Expand Down Expand Up @@ -82,6 +83,7 @@ def _loadData(self, data):
self.contentRating = data.attrib.get('contentRating')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label)
Expand Down
25 changes: 25 additions & 0 deletions plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,26 @@ def _loadData(self, data):
self.id = data.attrib.get('id')


@utils.registerPlexObject
class Image(PlexObject):
""" Represents a single Image media tag.
Attributes:
TAG (str): 'Image'
alt (str): The alt text for the image.
type (str): The type of image (e.g. coverPoster, background, snapshot).
url (str): The API URL (/library/metadata/<ratingKey>/thumb/<thumbid>).
"""
TAG = 'Image'

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.alt = data.attrib.get('alt')
self.type = data.attrib.get('type')
self.url = data.attrib.get('url')


@utils.registerPlexObject
class Rating(PlexObject):
""" Represents a single Rating media tag.
Expand Down Expand Up @@ -1078,6 +1098,11 @@ class Art(BaseResource):
TAG = 'Photo'


class Logo(BaseResource):
""" Represents a single Logo object. """
TAG = 'Photo'


class Poster(BaseResource):
""" Represents a single Poster object. """
TAG = 'Photo'
Expand Down
62 changes: 62 additions & 0 deletions plexapi/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,63 @@ def setArt(self, art):
return self


class LogoUrlMixin:
""" Mixin for Plex objects that can have a logo url. """

@property
def logoUrl(self):
""" Return the logo url for the Plex object. """
image = next((i for i in self.images if i.type == 'clearLogo'), None)
return self._server.url(image.url, includeToken=True) if image else None


class LogoLockMixin:
""" Mixin for Plex objects that can have a locked logo. """

def lockLogo(self):
""" Lock the logo for a Plex object. """
raise NotImplementedError('Logo cannot be locked through the API.')

def unlockLogo(self):
""" Unlock the logo for a Plex object. """
raise NotImplementedError('Logo cannot be unlocked through the API.')


class LogoMixin(LogoUrlMixin, LogoLockMixin):
""" Mixin for Plex objects that can have logos. """

def logos(self):
""" Returns list of available :class:`~plexapi.media.Logo` objects. """
return self.fetchItems(f'/library/metadata/{self.ratingKey}/clearLogos', cls=media.Logo)

def uploadLogo(self, url=None, filepath=None):
""" Upload a logo from a url or filepath.
Parameters:
url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload or file-like object.
"""
if url:
key = f'/library/metadata/{self.ratingKey}/clearLogos?url={quote_plus(url)}'
self._server.query(key, method=self._server._session.post)
elif filepath:
key = f'/library/metadata/{self.ratingKey}/clearLogos'
data = openOrRead(filepath)
self._server.query(key, method=self._server._session.post, data=data)
return self

def setLogo(self, logo):
""" Set the logo for a Plex object.
Raises:
:exc:`~plexapi.exceptions.NotImplementedError`: Logo cannot be set through the API.
"""
raise NotImplementedError(
'Logo cannot be set through the API. '
'Re-upload the logo using "uploadLogo" to set it.'
)


class PosterUrlMixin:
""" Mixin for Plex objects that can have a poster url. """

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

def setTheme(self, theme):
""" Set the theme for a Plex object.
Raises:
:exc:`~plexapi.exceptions.NotImplementedError`: Themes cannot be set through the API.
"""
raise NotImplementedError(
'Themes cannot be set through the API. '
'Re-upload the theme using "uploadTheme" to set it.'
Expand Down
4 changes: 4 additions & 0 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Photoalbum(
composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo album (local://229674).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (sting): Plex index number for the photo album.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo album was last rated.
Expand Down Expand Up @@ -57,6 +58,7 @@ def _loadData(self, data):
self.composite = data.attrib.get('composite')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
Expand Down Expand Up @@ -164,6 +166,7 @@ class Photo(
createdAtTZOffset (int): Unknown (-25200).
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
index (sting): Plex index number for the photo.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo was last rated.
Expand Down Expand Up @@ -204,6 +207,7 @@ def _loadData(self, data):
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
Expand Down
2 changes: 2 additions & 0 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
'theme': 317,
'studio': 318,
'network': 319,
'showOrdering': 322,
'clearLogo': 323,
'place': 400,
}
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}
Expand Down
8 changes: 5 additions & 3 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from plexapi.exceptions import BadRequest
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
WatchlistMixin
)
Expand All @@ -26,6 +26,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
artBlurHash (str): BlurHash string for artwork image.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8).
images (List<:class:`~plexapi.media.Image`>): List of image objects.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated.
lastViewedAt (datetime): Datetime the item was last played.
Expand Down Expand Up @@ -53,6 +54,7 @@ def _loadData(self, data):
self.artBlurHash = data.attrib.get('artBlurHash')
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
Expand Down Expand Up @@ -332,7 +334,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F
class Movie(
Video, Playable,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
MovieEditMixins,
WatchlistMixin
):
Expand Down Expand Up @@ -494,7 +496,7 @@ def metadataDirectory(self):
class Show(
Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
ShowEditMixins,
WatchlistMixin
):
Expand Down
2 changes: 2 additions & 0 deletions tests/test_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def test_audio_Artist_attr(artist):
# assert "Electronic" in [i.tag for i in artist.genres]
assert artist.guid in artist_guids
assert artist_guids[0] in [i.id for i in artist.guids]
if artist.images:
assert any("coverPoster" in i.type for i in artist.images)
assert artist.index == 1
assert utils.is_metadata(artist._initpath)
assert utils.is_metadata(artist.key)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def test_Collection_attrs(collection):
assert collection.isVideo is True
assert collection.isAudio is False
assert collection.isPhoto is False
if collection.images:
assert any("coverPoster" in i.type for i in collection.images)


def test_Collection_section(collection, movies):
Expand Down
4 changes: 3 additions & 1 deletion tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_video_Movie_merge(movie, patched_http_call):
movie.merge(1337)


def test_video_Movie_attrs(movies):
def test_video_Movie_attrs(movies): # noqa: C901
movie = movies.get("Sita Sings the Blues")
assert len(movie.locations) == 1
assert len(movie.locations[0]) >= 10
Expand All @@ -54,6 +54,8 @@ def test_video_Movie_attrs(movies):
assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright'
if movie.ratings:
assert "imdb://image.rating" in [i.image for i in movie.ratings]
if movie.images:
assert any("coverPoster" in i.type for i in movie.images)
movie.reload() # RELOAD
assert movie.chapterSource is None
assert not movie.collections
Expand Down

0 comments on commit 8830954

Please sign in to comment.