Add image tags and movie/show logos (#1462)

* Add image tags

* Test image tags

* Add logo resources

* Ignore flake8 C901 for movie attr test
This commit is contained in:
JonnyWong16 2024-11-16 14:52:49 -08:00 committed by GitHub
parent d6c5f09567
commit 88309546b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 110 additions and 5 deletions

View file

@ -33,6 +33,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
distance (float): Sonic Distance of the item from the seed item. distance (float): Sonic Distance of the item from the seed item.
fields (List<:class:`~plexapi.media.Field`>): List of field objects. fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c). 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). index (int): Plex index number (often the track number).
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated. lastRatedAt (datetime): Datetime the item was last rated.
@ -65,6 +66,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):
self.distance = utils.cast(float, data.attrib.get('distance')) self.distance = utils.cast(float, data.attrib.get('distance'))
self.fields = self.findItems(data, media.Field) self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '') self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

View file

@ -71,7 +71,7 @@ class PlexObject:
self._details_key = self._buildDetailsKey() self._details_key = self._buildDetailsKey()
def __repr__(self): 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')) 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])}>" return f"<{':'.join([p for p in [self.__class__.__name__, uid, name] if p])}>"

View file

@ -39,6 +39,7 @@ class Collection(
contentRating (str) Content rating (PG-13; NR; TV-G). contentRating (str) Content rating (PG-13; NR; TV-G).
fields (List<:class:`~plexapi.media.Field`>): List of field objects. fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). 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. index (int): Plex index number for the collection.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects. labels (List<:class:`~plexapi.media.Label`>): List of label objects.
@ -82,6 +83,7 @@ class Collection(
self.contentRating = data.attrib.get('contentRating') self.contentRating = data.attrib.get('contentRating')
self.fields = self.findItems(data, media.Field) self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50 self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label) self.labels = self.findItems(data, media.Label)

View file

@ -958,6 +958,26 @@ class Guid(PlexObject):
self.id = data.attrib.get('id') 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 @utils.registerPlexObject
class Rating(PlexObject): class Rating(PlexObject):
""" Represents a single Rating media tag. """ Represents a single Rating media tag.
@ -1078,6 +1098,11 @@ class Art(BaseResource):
TAG = 'Photo' TAG = 'Photo'
class Logo(BaseResource):
""" Represents a single Logo object. """
TAG = 'Photo'
class Poster(BaseResource): class Poster(BaseResource):
""" Represents a single Poster object. """ """ Represents a single Poster object. """
TAG = 'Photo' TAG = 'Photo'

View file

@ -403,6 +403,63 @@ class ArtMixin(ArtUrlMixin, ArtLockMixin):
return self 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: class PosterUrlMixin:
""" Mixin for Plex objects that can have a poster url. """ """ Mixin for Plex objects that can have a poster url. """
@ -513,6 +570,11 @@ class ThemeMixin(ThemeUrlMixin, ThemeLockMixin):
return self return self
def setTheme(self, theme): 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( raise NotImplementedError(
'Themes cannot be set through the API. ' 'Themes cannot be set through the API. '
'Re-upload the theme using "uploadTheme" to set it.' 'Re-upload the theme using "uploadTheme" to set it.'

View file

@ -30,6 +30,7 @@ class Photoalbum(
composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>) composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)
fields (List<:class:`~plexapi.media.Field`>): List of field objects. fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo album (local://229674). 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. index (sting): Plex index number for the photo album.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo album was last rated. lastRatedAt (datetime): Datetime the photo album was last rated.
@ -57,6 +58,7 @@ class Photoalbum(
self.composite = data.attrib.get('composite') self.composite = data.attrib.get('composite')
self.fields = self.findItems(data, media.Field) self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50 self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
@ -164,6 +166,7 @@ class Photo(
createdAtTZOffset (int): Unknown (-25200). createdAtTZOffset (int): Unknown (-25200).
fields (List<:class:`~plexapi.media.Field`>): List of field objects. fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn). 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. index (sting): Plex index number for the photo.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo was last rated. lastRatedAt (datetime): Datetime the photo was last rated.
@ -204,6 +207,7 @@ class Photo(
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset')) self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
self.fields = self.findItems(data, media.Field) self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '') self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))

View file

@ -90,6 +90,8 @@ TAGTYPES = {
'theme': 317, 'theme': 317,
'studio': 318, 'studio': 318,
'network': 319, 'network': 319,
'showOrdering': 322,
'clearLogo': 323,
'place': 400, 'place': 400,
} }
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()} REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}

View file

@ -9,7 +9,7 @@ from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin, ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins, MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
WatchlistMixin WatchlistMixin
) )
@ -26,6 +26,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
artBlurHash (str): BlurHash string for artwork image. artBlurHash (str): BlurHash string for artwork image.
fields (List<:class:`~plexapi.media.Field`>): List of field objects. 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). 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>). key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the item was last rated. lastRatedAt (datetime): Datetime the item was last rated.
lastViewedAt (datetime): Datetime the item was last played. lastViewedAt (datetime): Datetime the item was last played.
@ -53,6 +54,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
self.artBlurHash = data.attrib.get('artBlurHash') self.artBlurHash = data.attrib.get('artBlurHash')
self.fields = self.findItems(data, media.Field) self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.images = self.findItems(data, media.Image)
self.key = data.attrib.get('key', '') self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt')) self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
@ -332,7 +334,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):
class Movie( class Movie(
Video, Playable, Video, Playable,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
MovieEditMixins, MovieEditMixins,
WatchlistMixin WatchlistMixin
): ):
@ -494,7 +496,7 @@ class Movie(
class Show( class Show(
Video, Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
ShowEditMixins, ShowEditMixins,
WatchlistMixin WatchlistMixin
): ):

View file

@ -24,6 +24,8 @@ def test_audio_Artist_attr(artist):
# assert "Electronic" in [i.tag for i in artist.genres] # assert "Electronic" in [i.tag for i in artist.genres]
assert artist.guid in artist_guids assert artist.guid in artist_guids
assert artist_guids[0] in [i.id for i 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 artist.index == 1
assert utils.is_metadata(artist._initpath) assert utils.is_metadata(artist._initpath)
assert utils.is_metadata(artist.key) assert utils.is_metadata(artist.key)

View file

@ -47,6 +47,8 @@ def test_Collection_attrs(collection):
assert collection.isVideo is True assert collection.isVideo is True
assert collection.isAudio is False assert collection.isAudio is False
assert collection.isPhoto 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): def test_Collection_section(collection, movies):

View file

@ -41,7 +41,7 @@ def test_video_Movie_merge(movie, patched_http_call):
movie.merge(1337) movie.merge(1337)
def test_video_Movie_attrs(movies): def test_video_Movie_attrs(movies): # noqa: C901
movie = movies.get("Sita Sings the Blues") movie = movies.get("Sita Sings the Blues")
assert len(movie.locations) == 1 assert len(movie.locations) == 1
assert len(movie.locations[0]) >= 10 assert len(movie.locations[0]) >= 10
@ -54,6 +54,8 @@ def test_video_Movie_attrs(movies):
assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright' assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright'
if movie.ratings: if movie.ratings:
assert "imdb://image.rating" in [i.image for i in 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 movie.reload() # RELOAD
assert movie.chapterSource is None assert movie.chapterSource is None
assert not movie.collections assert not movie.collections