diff --git a/plexapi/audio.py b/plexapi/audio.py index 33716589..dda6d534 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -5,9 +5,11 @@ from urllib.parse import quote_plus from plexapi import library, media, utils from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest -from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin +from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, \ + ThemeUrlMixin from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin -from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin +from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, \ + StyleMixin from plexapi.playlist import Playlist @@ -125,8 +127,9 @@ class Audio(PlexPartialObject): @utils.registerPlexObject -class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, - CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin): +class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, ThemeMixin, RatingMixin, SplitMergeMixin, + UnmatchMatchMixin, CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, + SimilarArtistMixin, StyleMixin): """ Represents a single Artist. Attributes: @@ -142,6 +145,7 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S locations (List): List of folder paths where the artist is found on disk. similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. + theme (str): URL to theme resource (/library/metadata//theme/). """ TAG = 'Directory' TYPE = 'artist' @@ -158,6 +162,7 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S self.locations = self.listAttrs(data, 'path', etag='Location') self.similar = self.findItems(data, media.Similar) self.styles = self.findItems(data, media.Style) + self.theme = data.attrib.get('theme') def __iter__(self): for album in self.albums(): @@ -232,8 +237,8 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S @utils.registerPlexObject -class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, - CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): +class Album(Audio, ArtMixin, PosterMixin, ThemeUrlMixin, RatingMixin, UnmatchMatchMixin, + CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): """ Represents a single Album. Attributes: @@ -250,6 +255,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). parentKey (str): API URL of the album artist (/library/metadata/). parentRatingKey (int): Unique key identifying the album artist. + parentTheme (str): URL to artist theme resource (/library/metadata//theme/). parentThumb (str): URL to album artist thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the album artist. rating (float): Album rating (7.9; 9.8; 8.1). @@ -276,6 +282,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, self.parentGuid = data.attrib.get('parentGuid') self.parentKey = data.attrib.get('parentKey') self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) + self.parentTheme = data.attrib.get('parentTheme') self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.rating = utils.cast(float, data.attrib.get('rating')) @@ -339,7 +346,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, @utils.registerPlexObject -class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, +class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin, RatingMixin, CollectionMixin, LabelMixin, MoodMixin): """ Represents a single Track. @@ -353,6 +360,8 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). grandparentKey (str): API URL of the album artist (/library/metadata/). grandparentRatingKey (int): Unique key identifying the album artist. + grandparentTheme (str): URL to artist theme resource (/library/metadata//theme/). + (/library/metadata//theme/). grandparentThumb (str): URL to album artist thumbnail image (/library/metadata//thumb/). grandparentTitle (str): Name of the album artist for the track. @@ -384,6 +393,7 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, self.grandparentGuid = data.attrib.get('grandparentGuid') self.grandparentKey = data.attrib.get('grandparentKey') self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey')) + self.grandparentTheme = data.attrib.get('grandparentTheme') self.grandparentThumb = data.attrib.get('grandparentThumb') self.grandparentTitle = data.attrib.get('grandparentTitle') self.labels = self.findItems(data, media.Label) diff --git a/plexapi/collection.py b/plexapi/collection.py index cd9e52c1..10870589 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -5,14 +5,15 @@ from plexapi import media, utils from plexapi.base import PlexPartialObject from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.library import LibrarySection -from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin +from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, ThemeMixin, RatingMixin from plexapi.mixins import LabelMixin, SmartFilterMixin from plexapi.playqueue import PlayQueue from plexapi.utils import deprecated @utils.registerPlexObject -class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin, SmartFilterMixin): +class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, ThemeMixin, RatingMixin, + LabelMixin, SmartFilterMixin): """ Represents a single Collection. Attributes: @@ -43,6 +44,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin smart (bool): True if the collection is a smart collection. subtype (str): Media type of the items in the collection (movie, show, artist, or album). summary (str): Summary of the collection. + theme (str): URL to theme resource (/library/metadata//theme/). thumb (str): URL to thumbnail image (/library/metadata//thumb/). thumbBlurHash (str): BlurHash string for thumbnail image. title (str): Name of the collection. @@ -81,6 +83,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin self.smart = utils.cast(bool, data.attrib.get('smart', '0')) self.subtype = data.attrib.get('subtype') self.summary = data.attrib.get('summary') + self.theme = data.attrib.get('theme') self.thumb = data.attrib.get('thumb') self.thumbBlurHash = data.attrib.get('thumbBlurHash') self.title = data.attrib.get('title') diff --git a/plexapi/media.py b/plexapi/media.py index 87c33383..ecf65fbb 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -917,19 +917,17 @@ class Review(PlexObject): self.text = data.attrib.get('text') -class BaseImage(PlexObject): - """ Base class for all Art, Banner, and Poster objects. +class BaseResource(PlexObject): + """ Base class for all Art, Banner, Poster, and Theme objects. Attributes: - TAG (str): 'Photo' + TAG (str): 'Photo' or 'Track' key (str): API URL (/library/metadata/). - provider (str): The source of the poster or art. - ratingKey (str): Unique key identifying the poster or art. - selected (bool): True if the poster or art is currently selected. - thumb (str): The URL to retrieve the poster or art thumbnail. + provider (str): The source of the art or poster, None for Theme objects. + ratingKey (str): Unique key identifying the resource. + selected (bool): True if the resource is currently selected. + thumb (str): The URL to retrieve the resource thumbnail. """ - TAG = 'Photo' - def _loadData(self, data): self._data = data self.key = data.attrib.get('key') @@ -947,16 +945,24 @@ class BaseImage(PlexObject): pass -class Art(BaseImage): +class Art(BaseResource): """ Represents a single Art object. """ + TAG = 'Photo' -class Banner(BaseImage): +class Banner(BaseResource): """ Represents a single Banner object. """ + TAG = 'Photo' -class Poster(BaseImage): +class Poster(BaseResource): """ Represents a single Poster object. """ + TAG = 'Photo' + + +class Theme(BaseResource): + """ Represents a single Theme object. """ + TAG = 'Track' @utils.registerPlexObject diff --git a/plexapi/mixins.py b/plexapi/mixins.py index af5d1da5..95783305 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -161,7 +161,7 @@ class PosterUrlMixin(object): @property def thumbUrl(self): """ Return the thumb url for the Plex object. """ - thumb = self.firstAttr('thumb', 'parentThumb', 'granparentThumb') + thumb = self.firstAttr('thumb', 'parentThumb', 'grandparentThumb') return self._server.url(thumb, includeToken=True) if thumb else None @property @@ -209,6 +209,49 @@ class PosterMixin(PosterUrlMixin): self._edit(**{'thumb.locked': 0}) +class ThemeUrlMixin(object): + """ Mixin for Plex objects that can have a theme url. """ + + @property + def themeUrl(self): + """ Return the theme url for the Plex object. """ + theme = self.firstAttr('theme', 'parentTheme', 'grandparentTheme') + return self._server.url(theme, includeToken=True) if theme else None + + +class ThemeMixin(ThemeUrlMixin): + """ Mixin for Plex objects that can have themes. """ + + def themes(self): + """ Returns list of available :class:`~plexapi.media.Theme` objects. """ + return self.fetchItems('/library/metadata/%s/themes' % self.ratingKey, cls=media.Theme) + + def uploadTheme(self, url=None, filepath=None): + """ Upload a theme from url or filepath. + + Warning: Themes cannot be deleted using PlexAPI! + + Parameters: + url (str): The full URL to the theme to upload. + filepath (str): The full file path to the theme to upload. + """ + if url: + key = '/library/metadata/%s/themes?url=%s' % (self.ratingKey, quote_plus(url)) + self._server.query(key, method=self._server._session.post) + elif filepath: + key = '/library/metadata/%s/themes?' % self.ratingKey + data = open(filepath, 'rb').read() + self._server.query(key, method=self._server._session.post, data=data) + + def setTheme(self, theme): + """ Set the theme for a Plex object. + + Parameters: + theme (:class:`~plexapi.media.Theme`): The theme object to select. + """ + theme.select() + + class RatingMixin(object): """ Mixin for Plex objects that can have user star ratings. """ diff --git a/plexapi/video.py b/plexapi/video.py index 7459221c..b25e9c82 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -5,9 +5,11 @@ from urllib.parse import quote_plus, urlencode from plexapi import library, media, utils from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest -from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin +from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, \ + ThemeUrlMixin, ThemeMixin from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin -from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin +from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, \ + WriterMixin class Video(PlexPartialObject): @@ -261,8 +263,9 @@ class Video(PlexPartialObject): @utils.registerPlexObject -class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, - CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin): +class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, ThemeMixin, RatingMixin, SplitMergeMixin, + UnmatchMatchMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, + WriterMixin): """ Represents a single Movie. Attributes: @@ -293,6 +296,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment). tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). + theme (str): URL to theme resource (/library/metadata//theme/). useOriginalTitle (int): Setting that indicates if the original title is used for the movie (-1 = Library default, 0 = No, 1 = Yes). viewOffset (int): View offset in milliseconds. @@ -331,6 +335,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin self.similar = self.findItems(data, media.Similar) self.studio = data.attrib.get('studio') self.tagline = data.attrib.get('tagline') + self.theme = data.attrib.get('theme') self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.writers = self.findItems(data, media.Writer) @@ -377,8 +382,8 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin @utils.registerPlexObject -class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, - CollectionMixin, GenreMixin, LabelMixin): +class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, ThemeMixin, RatingMixin, SplitMergeMixin, + UnmatchMatchMixin, CollectionMixin, GenreMixin, LabelMixin): """ Represents a single Show (including all seasons and episodes). Attributes: @@ -574,7 +579,7 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat @utils.registerPlexObject -class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin, LabelMixin): +class Season(Video, ArtMixin, PosterMixin, ThemeUrlMixin, RatingMixin, CollectionMixin, LabelMixin): """ Represents a single Show Season (including all episodes). Attributes: @@ -711,8 +716,8 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin, LabelMi @utils.registerPlexObject -class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin, - CollectionMixin, DirectorMixin, LabelMixin, WriterMixin): +class Episode(Video, Playable, ArtMixin, PosterMixin, ThemeUrlMixin, RatingMixin, + CollectionMixin, DirectorMixin, LabelMixin, WriterMixin): """ Represents a single Shows Episode. Attributes: