mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 03:33:08 +00:00
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:
parent
d6c5f09567
commit
88309546b1
11 changed files with 110 additions and 5 deletions
|
@ -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'))
|
||||||
|
|
|
@ -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])}>"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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.'
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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
|
||||||
):
|
):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue