Skip to content

Add image tags and movie/show logos #1462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading