diff --git a/plexapi/audio.py b/plexapi/audio.py index 7686a37e4..f1a4987b4 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -10,12 +10,7 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession, cached_data_property from plexapi.exceptions import BadRequest -from plexapi.mixins import ( - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, - ArtUrlMixin, ArtMixin, LogoMixin, LogoUrlMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, - ThemeMixin, ThemeUrlMixin, - ArtistEditMixins, AlbumEditMixins, TrackEditMixins -) +from plexapi.mixins import ArtistMixins, AlbumMixins, TrackMixins, PlayedUnplayedMixin from plexapi.playlist import Playlist @@ -180,10 +175,7 @@ def sonicallySimilar( @utils.registerPlexObject class Artist( - Audio, - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, - ArtistEditMixins + Audio, ArtistMixins ): """ Represents a single Artist. @@ -350,10 +342,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Album( - Audio, - SplitMergeMixin, UnmatchMatchMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, - AlbumEditMixins + Audio, AlbumMixins ): """ Represents a single Album. @@ -503,10 +492,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Track( - Audio, Playable, - ExtrasMixin, RatingMixin, - ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin, ThemeUrlMixin, - TrackEditMixins + Audio, Playable, TrackMixins ): """ Represents a single Track. diff --git a/plexapi/collection.py b/plexapi/collection.py index 75d76445d..6ff75dafd 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -6,20 +6,13 @@ from plexapi.base import PlexPartialObject, cached_data_property from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.library import LibrarySection, ManagedHub -from plexapi.mixins import ( - AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, - CollectionEditMixins -) +from plexapi.mixins import CollectionMixins from plexapi.utils import deprecated @utils.registerPlexObject class Collection( - PlexPartialObject, - AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, - CollectionEditMixins + PlexPartialObject, CollectionMixins ): """ Represents a single Collection. diff --git a/plexapi/mixins.py b/plexapi/mixins.py deleted file mode 100644 index f27ab71ec..000000000 --- a/plexapi/mixins.py +++ /dev/null @@ -1,1416 +0,0 @@ -# -*- coding: utf-8 -*- -from collections import deque -from datetime import datetime -from typing import Deque, Set, Tuple, Union -from urllib.parse import parse_qsl, quote, quote_plus, unquote, urlencode, urlsplit - -from plexapi import media, settings, utils -from plexapi.exceptions import BadRequest, NotFound -from plexapi.utils import deprecated, openOrRead - - -class AdvancedSettingsMixin: - """ Mixin for Plex objects that can have advanced settings. """ - - def preferences(self): - """ Returns a list of :class:`~plexapi.settings.Preferences` objects. """ - key = f'{self.key}?includePreferences=1' - return self.fetchItems(key, cls=settings.Preferences, rtag='Preferences') - - def preference(self, pref): - """ Returns a :class:`~plexapi.settings.Preferences` object for the specified pref. - - Parameters: - pref (str): The id of the preference to return. - """ - prefs = self.preferences() - try: - return next(p for p in prefs if p.id == pref) - except StopIteration: - availablePrefs = [p.id for p in prefs] - raise NotFound(f'Unknown preference "{pref}" for {self.TYPE}. ' - f'Available preferences: {availablePrefs}') from None - - def editAdvanced(self, **kwargs): - """ Edit a Plex object's advanced settings. """ - data = {} - key = f'{self.key}/prefs?' - preferences = {pref.id: pref for pref in self.preferences() if pref.enumValues} - for settingID, value in kwargs.items(): - try: - pref = preferences[settingID] - except KeyError: - raise NotFound(f'{value} not found in {list(preferences.keys())}') - - enumValues = pref.enumValues - if enumValues.get(value, enumValues.get(str(value))): - data[settingID] = value - else: - raise NotFound(f'{value} not found in {list(enumValues)}') - url = key + urlencode(data) - self._server.query(url, method=self._server._session.put) - return self - - def defaultAdvanced(self): - """ Edit all of a Plex object's advanced settings to default. """ - data = {} - key = f'{self.key}/prefs?' - for preference in self.preferences(): - data[preference.id] = preference.default - url = key + urlencode(data) - self._server.query(url, method=self._server._session.put) - return self - - -class SmartFilterMixin: - """ Mixin for Plex objects that can have smart filters. """ - - def _parseFilterGroups(self, feed: Deque[Tuple[str, str]], returnOn: Union[Set[str], None] = None) -> dict: - """ Parse filter groups from input lines between push and pop. """ - currentFiltersStack: list[dict] = [] - operatorForStack = None - if returnOn is None: - returnOn = set("pop") - else: - returnOn.add("pop") - allowedLogicalOperators = ["and", "or"] # first is the default - - while feed: - key, value = feed.popleft() # consume the first item - if key == "push": - # recurse and add the result to the current stack - currentFiltersStack.append( - self._parseFilterGroups(feed, returnOn) - ) - elif key in returnOn: - # stop iterating and return the current stack - if not key == "pop": - feed.appendleft((key, value)) # put the item back - break - - elif key in allowedLogicalOperators: - # set the operator - if operatorForStack and not operatorForStack == key: - raise ValueError( - "cannot have different logical operators for the same" - " filter group" - ) - operatorForStack = key - - else: - # add the key value pair to the current filter - currentFiltersStack.append({key: value}) - - if not operatorForStack and len(currentFiltersStack) > 1: - # consider 'and' as the default operator - operatorForStack = allowedLogicalOperators[0] - - if operatorForStack: - return {operatorForStack: currentFiltersStack} - return currentFiltersStack.pop() - - def _parseQueryFeed(self, feed: "deque[Tuple[str, str]]") -> dict: - """ Parse the query string into a dict. """ - filtersDict = {} - special_keys = {"type", "sort"} - integer_keys = {"includeGuids", "limit"} - as_is_keys = {"group", "having"} - reserved_keys = special_keys | integer_keys | as_is_keys - while feed: - key, value = feed.popleft() - if key in integer_keys: - filtersDict[key] = int(value) - elif key in as_is_keys: - filtersDict[key] = value - elif key == "type": - filtersDict["libtype"] = utils.reverseSearchType(value) - elif key == "sort": - filtersDict["sort"] = value.split(",") - else: - feed.appendleft((key, value)) # put the item back - filter_group = self._parseFilterGroups( - feed, returnOn=reserved_keys - ) - if "filters" in filtersDict: - filtersDict["filters"] = { - "and": [filtersDict["filters"], filter_group] - } - else: - filtersDict["filters"] = filter_group - - return filtersDict - - def _parseFilters(self, content): - """ Parse the content string and returns the filter dict. """ - content = urlsplit(unquote(content)) - feed = deque() - - for key, value in parse_qsl(content.query): - # Move = sign to key when operator is == - if value.startswith("="): - key, value = f"{key}=", value[1:] - - feed.append((key, value)) - - return self._parseQueryFeed(feed) - - -class SplitMergeMixin: - """ Mixin for Plex objects that can be split and merged. """ - - def split(self): - """ Split duplicated Plex object into separate objects. """ - key = f'{self.key}/split' - self._server.query(key, method=self._server._session.put) - return self - - def merge(self, ratingKeys): - """ Merge other Plex objects into the current object. - - Parameters: - ratingKeys (list): A list of rating keys to merge. - """ - if not isinstance(ratingKeys, list): - ratingKeys = str(ratingKeys).split(',') - - key = f"{self.key}/merge?ids={','.join(str(r) for r in ratingKeys)}" - self._server.query(key, method=self._server._session.put) - return self - - -class UnmatchMatchMixin: - """ Mixin for Plex objects that can be unmatched and matched. """ - - def unmatch(self): - """ Unmatches metadata match from object. """ - key = f'{self.key}/unmatch' - self._server.query(key, method=self._server._session.put) - - def matches(self, agent=None, title=None, year=None, language=None): - """ Return list of (:class:`~plexapi.media.SearchResult`) metadata matches. - - Parameters: - agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) - title (str): Title of item to search for - year (str): Year of item to search in - language (str) : Language of item to search in - - Examples: - 1. video.matches() - 2. video.matches(title="something", year=2020) - 3. video.matches(title="something") - 4. video.matches(year=2020) - 5. video.matches(title="something", year="") - 6. video.matches(title="", year=2020) - 7. video.matches(title="", year="") - - 1. The default behaviour in Plex Web = no params in plexapi - 2. Both title and year specified by user - 3. Year automatically filled in - 4. Title automatically filled in - 5. Explicitly searches for title with blank year - 6. Explicitly searches for blank title with year - 7. I don't know what the user is thinking... return the same result as 1 - - For 2 to 7, the agent and language is automatically filled in - """ - key = f'{self.key}/matches' - params = {'manual': 1} - - if agent and not any([title, year, language]): - params['language'] = self.section().language - params['agent'] = utils.getAgentIdentifier(self.section(), agent) - else: - if any(x is not None for x in [agent, title, year, language]): - if title is None: - params['title'] = self.title - else: - params['title'] = title - - if year is None: - params['year'] = getattr(self, 'year', '') - else: - params['year'] = year - - params['language'] = language or self.section().language - - if agent is None: - params['agent'] = self.section().agent - else: - params['agent'] = utils.getAgentIdentifier(self.section(), agent) - - key = key + '?' + urlencode(params) - return self.fetchItems(key, cls=media.SearchResult) - - def fixMatch(self, searchResult=None, auto=False, agent=None): - """ Use match result to update show metadata. - - Parameters: - auto (bool): True uses first match from matches - False allows user to provide the match - searchResult (:class:`~plexapi.media.SearchResult`): Search result from - ~plexapi.base.matches() - agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) - """ - key = f'{self.key}/match' - if auto: - autoMatch = self.matches(agent=agent) - if autoMatch: - searchResult = autoMatch[0] - else: - raise NotFound(f'No matches found using this agent: ({agent}:{autoMatch})') - elif not searchResult: - raise NotFound('fixMatch() requires either auto=True or ' - 'searchResult=:class:`~plexapi.media.SearchResult`.') - - params = {'guid': searchResult.guid, - 'name': searchResult.name} - - data = key + '?' + urlencode(params) - self._server.query(data, method=self._server._session.put) - return self - - -class ExtrasMixin: - """ Mixin for Plex objects that can have extras. """ - - def extras(self): - """ Returns a list of :class:`~plexapi.video.Extra` objects. """ - from plexapi.video import Extra - key = f'{self.key}/extras' - return self.fetchItems(key, cls=Extra) - - -class HubsMixin: - """ Mixin for Plex objects that can have related hubs. """ - - def hubs(self): - """ Returns a list of :class:`~plexapi.library.Hub` objects. """ - from plexapi.library import Hub - key = f'{self.key}/related' - return self.fetchItems(key, cls=Hub) - - -class PlayedUnplayedMixin: - """ Mixin for Plex objects that can be marked played and unplayed. """ - - @property - def isPlayed(self): - """ Returns True if this video is played. """ - return bool(self.viewCount > 0) if self.viewCount else False - - def markPlayed(self): - """ Mark the Plex object as played. """ - key = '/:/scrobble' - params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} - self._server.query(key, params=params) - return self - - def markUnplayed(self): - """ Mark the Plex object as unplayed. """ - key = '/:/unscrobble' - params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} - self._server.query(key, params=params) - return self - - @property - def isWatched(self): - """ Alias to self.isPlayed. """ - return self.isPlayed - - def markWatched(self): - """ Alias to :func:`~plexapi.mixins.PlayedUnplayedMixin.markPlayed`. """ - self.markPlayed() - - def markUnwatched(self): - """ Alias to :func:`~plexapi.mixins.PlayedUnplayedMixin.markUnplayed`. """ - self.markUnplayed() - - -class RatingMixin: - """ Mixin for Plex objects that can have user star ratings. """ - - def rate(self, rating=None): - """ Rate the Plex object. Note: Plex ratings are displayed out of 5 stars (e.g. rating 7.0 = 3.5 stars). - - Parameters: - rating (float, optional): Rating from 0 to 10. Exclude to reset the rating. - - Raises: - :exc:`~plexapi.exceptions.BadRequest`: If the rating is invalid. - """ - if rating is None: - rating = -1 - elif not isinstance(rating, (int, float)) or rating < 0 or rating > 10: - raise BadRequest('Rating must be between 0 to 10.') - key = f'/:/rate?key={self.ratingKey}&identifier=com.plexapp.plugins.library&rating={rating}' - self._server.query(key, method=self._server._session.put) - return self - - -class ArtUrlMixin: - """ Mixin for Plex objects that can have a background artwork url. """ - - @property - def artUrl(self): - """ Return the art url for the Plex object. """ - art = self.firstAttr('art', 'grandparentArt') - return self._server.url(art, includeToken=True) if art else None - - -class ArtLockMixin: - """ Mixin for Plex objects that can have a locked background artwork. """ - - def lockArt(self): - """ Lock the background artwork for a Plex object. """ - return self._edit(**{'art.locked': 1}) - - def unlockArt(self): - """ Unlock the background artwork for a Plex object. """ - return self._edit(**{'art.locked': 0}) - - -class ArtMixin(ArtUrlMixin, ArtLockMixin): - """ Mixin for Plex objects that can have background artwork. """ - - def arts(self): - """ Returns list of available :class:`~plexapi.media.Art` objects. """ - return self.fetchItems(f'/library/metadata/{self.ratingKey}/arts', cls=media.Art) - - def uploadArt(self, url=None, filepath=None): - """ Upload a background artwork from a url or filepath. - - Parameters: - url (str): The full URL to the image to upload. - filepath (str): The full file path to the image to upload or file-like object. - """ - if url: - key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}' - self._server.query(key, method=self._server._session.post) - elif filepath: - key = f'/library/metadata/{self.ratingKey}/arts' - data = openOrRead(filepath) - self._server.query(key, method=self._server._session.post, data=data) - return self - - def setArt(self, art): - """ Set the background artwork for a Plex object. - - Parameters: - art (:class:`~plexapi.media.Art`): The art object to select. - """ - art.select() - return self - - def deleteArt(self): - """ Delete the art from a Plex object. """ - key = f'/library/metadata/{self.ratingKey}/art' - self._server.query(key, method=self._server._session.delete) - return self - - -class LogoUrlMixin: - """ Mixin for Plex objects that can have a logo url. """ - - @property - def logo(self): - """ Return the API path to the logo image. """ - return next((i.url for i in self.images if i.type == 'clearLogo'), None) - - @property - def logoUrl(self): - """ Return the logo url for the Plex object. """ - return self._server.url(self.logo, includeToken=True) if self.logo else None - - -class LogoLockMixin: - """ Mixin for Plex objects that can have a locked logo. """ - - def lockLogo(self): - """ Lock the logo for a Plex object. """ - return self._edit(**{'clearLogo.locked': 1}) - - def unlockLogo(self): - """ Unlock the logo for a Plex object. """ - return self._edit(**{'clearLogo.locked': 0}) - - -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 to 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. - - Parameters: - logo (:class:`~plexapi.media.Logo`): The logo object to select. - """ - logo.select() - return self - - def deleteLogo(self): - """ Delete the logo from a Plex object. """ - key = f'/library/metadata/{self.ratingKey}/clearLogo' - self._server.query(key, method=self._server._session.delete) - return self - - -class PosterUrlMixin: - """ Mixin for Plex objects that can have a poster url. """ - - @property - def thumbUrl(self): - """ Return the thumb url for the Plex object. """ - thumb = self.firstAttr('thumb', 'parentThumb', 'grandparentThumb') - return self._server.url(thumb, includeToken=True) if thumb else None - - @property - def posterUrl(self): - """ Alias to self.thumbUrl. """ - return self.thumbUrl - - -class PosterLockMixin: - """ Mixin for Plex objects that can have a locked poster. """ - - def lockPoster(self): - """ Lock the poster for a Plex object. """ - return self._edit(**{'thumb.locked': 1}) - - def unlockPoster(self): - """ Unlock the poster for a Plex object. """ - return self._edit(**{'thumb.locked': 0}) - - -class PosterMixin(PosterUrlMixin, PosterLockMixin): - """ Mixin for Plex objects that can have posters. """ - - def posters(self): - """ Returns list of available :class:`~plexapi.media.Poster` objects. """ - return self.fetchItems(f'/library/metadata/{self.ratingKey}/posters', cls=media.Poster) - - def uploadPoster(self, url=None, filepath=None): - """ Upload a poster from a url or filepath. - - Parameters: - url (str): The full URL to the image to upload. - filepath (str): The full file path to the image to upload or file-like object. - """ - if url: - key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}' - self._server.query(key, method=self._server._session.post) - elif filepath: - key = f'/library/metadata/{self.ratingKey}/posters' - data = openOrRead(filepath) - self._server.query(key, method=self._server._session.post, data=data) - return self - - def setPoster(self, poster): - """ Set the poster for a Plex object. - - Parameters: - poster (:class:`~plexapi.media.Poster`): The poster object to select. - """ - poster.select() - return self - - def deletePoster(self): - """ Delete the poster from a Plex object. """ - key = f'/library/metadata/{self.ratingKey}/thumb' - self._server.query(key, method=self._server._session.delete) - return self - - -class SquareArtUrlMixin: - """ Mixin for Plex objects that can have a square art url. """ - - @property - def squareArt(self): - """ Return the API path to the square art image. """ - return next((i.url for i in self.images if i.type == 'backgroundSquare'), None) - - @property - def squareArtUrl(self): - """ Return the square art url for the Plex object. """ - return self._server.url(self.squareArt, includeToken=True) if self.squareArt else None - - -class SquareArtLockMixin: - """ Mixin for Plex objects that can have a locked square art. """ - - def lockSquareArt(self): - """ Lock the square art for a Plex object. """ - return self._edit(**{'squareArt.locked': 1}) - - def unlockSquareArt(self): - """ Unlock the square art for a Plex object. """ - return self._edit(**{'squareArt.locked': 0}) - - -class SquareArtMixin(SquareArtUrlMixin, SquareArtLockMixin): - """ Mixin for Plex objects that can have square art. """ - - def squareArts(self): - """ Returns list of available :class:`~plexapi.media.SquareArt` objects. """ - return self.fetchItems(f'/library/metadata/{self.ratingKey}/squareArts', cls=media.SquareArt) - - def uploadSquareArt(self, url=None, filepath=None): - """ Upload a square art from a url or filepath. - - Parameters: - url (str): The full URL to the image to upload. - filepath (str): The full file path to the image to upload or file-like object. - """ - if url: - key = f'/library/metadata/{self.ratingKey}/squareArts?url={quote_plus(url)}' - self._server.query(key, method=self._server._session.post) - elif filepath: - key = f'/library/metadata/{self.ratingKey}/squareArts' - data = openOrRead(filepath) - self._server.query(key, method=self._server._session.post, data=data) - return self - - def setSquareArt(self, squareArt): - """ Set the square art for a Plex object. - - Parameters: - squareArt (:class:`~plexapi.media.SquareArt`): The square art object to select. - """ - squareArt.select() - return self - - def deleteSquareArt(self): - """ Delete the square art from a Plex object. """ - key = f'/library/metadata/{self.ratingKey}/squareArt' - self._server.query(key, method=self._server._session.delete) - return self - - -class ThemeUrlMixin: - """ 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 ThemeLockMixin: - """ Mixin for Plex objects that can have a locked theme. """ - - def lockTheme(self): - """ Lock the theme for a Plex object. """ - return self._edit(**{'theme.locked': 1}) - - def unlockTheme(self): - """ Unlock the theme for a Plex object. """ - return self._edit(**{'theme.locked': 0}) - - -class ThemeMixin(ThemeUrlMixin, ThemeLockMixin): - """ Mixin for Plex objects that can have themes. """ - - def themes(self): - """ Returns list of available :class:`~plexapi.media.Theme` objects. """ - return self.fetchItems(f'/library/metadata/{self.ratingKey}/themes', cls=media.Theme) - - def uploadTheme(self, url=None, filepath=None, timeout=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 or file-like object. - timeout (int, optional): Timeout, in seconds, to use when uploading themes to the server. - (default config.TIMEOUT). - """ - if url: - key = f'/library/metadata/{self.ratingKey}/themes?url={quote_plus(url)}' - self._server.query(key, method=self._server._session.post, timeout=timeout) - elif filepath: - key = f'/library/metadata/{self.ratingKey}/themes' - data = openOrRead(filepath) - self._server.query(key, method=self._server._session.post, data=data, timeout=timeout) - 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.' - ) - - def deleteTheme(self): - """ Delete the theme from a Plex object. """ - key = f'/library/metadata/{self.ratingKey}/theme' - self._server.query(key, method=self._server._session.delete) - return self - - -class EditFieldMixin: - """ Mixin for editing Plex object fields. """ - - def editField(self, field, value, locked=True, **kwargs): - """ Edit the field of a Plex object. All field editing methods can be chained together. - Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing fields. - - Parameters: - field (str): The name of the field to edit. - value (str): The value to edit the field to. - locked (bool): True (default) to lock the field, False to unlock the field. - - Example: - - .. code-block:: python - - # Chaining multiple field edits with reloading - Movie.editTitle('A New Title').editSummary('A new summary').editTagline('A new tagline').reload() - - """ - edits = { - f'{field}.value': value or '', - f'{field}.locked': 1 if locked else 0 - } - edits.update(kwargs) - return self._edit(**edits) - - -class AddedAtMixin(EditFieldMixin): - """ Mixin for Plex objects that can have an added at date. """ - - def editAddedAt(self, addedAt, locked=True): - """ Edit the added at date. - - Parameters: - addedAt (int or str or datetime): The new value as a unix timestamp (int), - "YYYY-MM-DD" (str), or datetime object. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - if isinstance(addedAt, str): - addedAt = int(round(datetime.strptime(addedAt, '%Y-%m-%d').timestamp())) - elif isinstance(addedAt, datetime): - addedAt = int(round(addedAt.timestamp())) - return self.editField('addedAt', addedAt, locked=locked) - - -class AudienceRatingMixin(EditFieldMixin): - """ Mixin for Plex objects that can have an audience rating. """ - - def editAudienceRating(self, audienceRating, locked=True): - """ Edit the audience rating. - - Parameters: - audienceRating (float): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('audienceRating', audienceRating, locked=locked) - - -class ContentRatingMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a content rating. """ - - def editContentRating(self, contentRating, locked=True): - """ Edit the content rating. - - Parameters: - contentRating (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('contentRating', contentRating, locked=locked) - - -class CriticRatingMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a critic rating. """ - - def editCriticRating(self, criticRating, locked=True): - """ Edit the critic rating. - - Parameters: - criticRating (float): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('rating', criticRating, locked=locked) - - -class EditionTitleMixin(EditFieldMixin): - """ Mixin for Plex objects that can have an edition title. """ - - def editEditionTitle(self, editionTitle, locked=True): - """ Edit the edition title. Plex Pass is required to edit this field. - - Parameters: - editionTitle (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('editionTitle', editionTitle, locked=locked) - - -class OriginallyAvailableMixin(EditFieldMixin): - """ Mixin for Plex objects that can have an originally available date. """ - - def editOriginallyAvailable(self, originallyAvailable, locked=True): - """ Edit the originally available date. - - Parameters: - originallyAvailable (str or datetime): The new value "YYYY-MM-DD (str) or datetime object. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - if isinstance(originallyAvailable, datetime): - originallyAvailable = originallyAvailable.strftime('%Y-%m-%d') - return self.editField('originallyAvailableAt', originallyAvailable, locked=locked) - - -class OriginalTitleMixin(EditFieldMixin): - """ Mixin for Plex objects that can have an original title. """ - - def editOriginalTitle(self, originalTitle, locked=True): - """ Edit the original title. - - Parameters: - originalTitle (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('originalTitle', originalTitle, locked=locked) - - -class SortTitleMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a sort title. """ - - def editSortTitle(self, sortTitle, locked=True): - """ Edit the sort title. - - Parameters: - sortTitle (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('titleSort', sortTitle, locked=locked) - - -class StudioMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a studio. """ - - def editStudio(self, studio, locked=True): - """ Edit the studio. - - Parameters: - studio (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('studio', studio, locked=locked) - - -class SummaryMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a summary. """ - - def editSummary(self, summary, locked=True): - """ Edit the summary. - - Parameters: - summary (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('summary', summary, locked=locked) - - -class TaglineMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a tagline. """ - - def editTagline(self, tagline, locked=True): - """ Edit the tagline. - - Parameters: - tagline (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('tagline', tagline, locked=locked) - - -class TitleMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a title. """ - - def editTitle(self, title, locked=True): - """ Edit the title. - - Parameters: - title (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - kwargs = {} - if self.TYPE == 'album': - # Editing album title also requires the artist ratingKey - kwargs['artist.id.value'] = self.parentRatingKey - return self.editField('title', title, locked=locked, **kwargs) - - -class TrackArtistMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a track artist. """ - - def editTrackArtist(self, trackArtist, locked=True): - """ Edit the track artist. - - Parameters: - trackArtist (str): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('originalTitle', trackArtist, locked=locked) - - -class TrackNumberMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a track number. """ - - def editTrackNumber(self, trackNumber, locked=True): - """ Edit the track number. - - Parameters: - trackNumber (int): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('index', trackNumber, locked=locked) - - -class TrackDiscNumberMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a track disc number. """ - - def editDiscNumber(self, discNumber, locked=True): - """ Edit the track disc number. - - Parameters: - discNumber (int): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('parentIndex', discNumber, locked=locked) - - -class PhotoCapturedTimeMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a captured time. """ - - def editCapturedTime(self, capturedTime, locked=True): - """ Edit the photo captured time. - - Parameters: - capturedTime (str or datetime): The new value "YYYY-MM-DD hh:mm:ss" (str) or datetime object. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - if isinstance(capturedTime, datetime): - capturedTime = capturedTime.strftime('%Y-%m-%d %H:%M:%S') - return self.editField('originallyAvailableAt', capturedTime, locked=locked) - - -class UserRatingMixin(EditFieldMixin): - """ Mixin for Plex objects that can have a user rating. """ - - def editUserRating(self, userRating, locked=True): - """ Edit the user rating. - - Parameters: - userRating (float): The new value. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editField('userRating', userRating, locked=locked) - - -class EditTagsMixin: - """ Mixin for editing Plex object tags. """ - - @deprecated('use "editTags" instead') - def _edit_tags(self, tag, items, locked=True, remove=False): - return self.editTags(tag, items, locked, remove) - - def editTags(self, tag, items, locked=True, remove=False, **kwargs): - """ Edit the tags of a Plex object. All tag editing methods can be chained together. - Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing tags. - - Parameters: - tag (str): Name of the tag to edit. - items (List or List<:class:`~plexapi.media.MediaTag`>): List of tags to add or remove. - locked (bool): True (default) to lock the tags, False to unlock the tags. - remove (bool): True to remove the tags in items. - - Example: - - .. code-block:: python - - # Chaining multiple tag edits with reloading - Show.addCollection('New Collection').removeGenre('Action').addLabel('Favorite').reload() - - """ - if not isinstance(items, list): - items = [items] - - if not remove: - tags = getattr(self, self._tagPlural(tag), []) - if isinstance(tags, list): - items = tags + items - - edits = self._tagHelper(self._tagSingular(tag), items, locked, remove) - edits.update(kwargs) - return self._edit(**edits) - - @staticmethod - def _tagSingular(tag): - """ Return the singular name of a tag. """ - if tag == 'countries': - return 'country' - elif tag == 'similar': - return 'similar' - elif tag[-1] == 's': - return tag[:-1] - return tag - - @staticmethod - def _tagPlural(tag): - """ Return the plural name of a tag. """ - if tag == 'country': - return 'countries' - elif tag == 'similar': - return 'similar' - elif tag[-1] != 's': - return tag + 's' - return tag - - @staticmethod - def _tagHelper(tag, items, locked=True, remove=False): - """ Return a dict of the query parameters for editing a tag. """ - if not isinstance(items, list): - items = [items] - - data = { - f'{tag}.locked': 1 if locked else 0 - } - - if remove: - tagname = f'{tag}[].tag.tag-' - data[tagname] = ','.join(quote(str(t)) for t in items) - else: - for i, item in enumerate(items): - tagname = f'{str(tag)}[{i}].tag.tag' - data[tagname] = item - - return data - - -class CollectionMixin(EditTagsMixin): - """ Mixin for Plex objects that can have collections. """ - - def addCollection(self, collections, locked=True): - """ Add a collection tag(s). - - Parameters: - collections (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('collection', collections, locked=locked) - - def removeCollection(self, collections, locked=True): - """ Remove a collection tag(s). - - Parameters: - collections (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('collection', collections, locked=locked, remove=True) - - -class CountryMixin(EditTagsMixin): - """ Mixin for Plex objects that can have countries. """ - - def addCountry(self, countries, locked=True): - """ Add a country tag(s). - - Parameters: - countries (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('country', countries, locked=locked) - - def removeCountry(self, countries, locked=True): - """ Remove a country tag(s). - - Parameters: - countries (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('country', countries, locked=locked, remove=True) - - -class DirectorMixin(EditTagsMixin): - """ Mixin for Plex objects that can have directors. """ - - def addDirector(self, directors, locked=True): - """ Add a director tag(s). - - Parameters: - directors (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('director', directors, locked=locked) - - def removeDirector(self, directors, locked=True): - """ Remove a director tag(s). - - Parameters: - directors (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('director', directors, locked=locked, remove=True) - - -class GenreMixin(EditTagsMixin): - """ Mixin for Plex objects that can have genres. """ - - def addGenre(self, genres, locked=True): - """ Add a genre tag(s). - - Parameters: - genres (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('genre', genres, locked=locked) - - def removeGenre(self, genres, locked=True): - """ Remove a genre tag(s). - - Parameters: - genres (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('genre', genres, locked=locked, remove=True) - - -class LabelMixin(EditTagsMixin): - """ Mixin for Plex objects that can have labels. """ - - def addLabel(self, labels, locked=True): - """ Add a label tag(s). - - Parameters: - labels (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('label', labels, locked=locked) - - def removeLabel(self, labels, locked=True): - """ Remove a label tag(s). - - Parameters: - labels (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('label', labels, locked=locked, remove=True) - - -class MoodMixin(EditTagsMixin): - """ Mixin for Plex objects that can have moods. """ - - def addMood(self, moods, locked=True): - """ Add a mood tag(s). - - Parameters: - moods (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('mood', moods, locked=locked) - - def removeMood(self, moods, locked=True): - """ Remove a mood tag(s). - - Parameters: - moods (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('mood', moods, locked=locked, remove=True) - - -class ProducerMixin(EditTagsMixin): - """ Mixin for Plex objects that can have producers. """ - - def addProducer(self, producers, locked=True): - """ Add a producer tag(s). - - Parameters: - producers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('producer', producers, locked=locked) - - def removeProducer(self, producers, locked=True): - """ Remove a producer tag(s). - - Parameters: - producers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('producer', producers, locked=locked, remove=True) - - -class SimilarArtistMixin(EditTagsMixin): - """ Mixin for Plex objects that can have similar artists. """ - - def addSimilarArtist(self, artists, locked=True): - """ Add a similar artist tag(s). - - Parameters: - artists (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('similar', artists, locked=locked) - - def removeSimilarArtist(self, artists, locked=True): - """ Remove a similar artist tag(s). - - Parameters: - artists (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('similar', artists, locked=locked, remove=True) - - -class StyleMixin(EditTagsMixin): - """ Mixin for Plex objects that can have styles. """ - - def addStyle(self, styles, locked=True): - """ Add a style tag(s). - - Parameters: - styles (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('style', styles, locked=locked) - - def removeStyle(self, styles, locked=True): - """ Remove a style tag(s). - - Parameters: - styles (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('style', styles, locked=locked, remove=True) - - -class TagMixin(EditTagsMixin): - """ Mixin for Plex objects that can have tags. """ - - def addTag(self, tags, locked=True): - """ Add a tag(s). - - Parameters: - tags (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('tag', tags, locked=locked) - - def removeTag(self, tags, locked=True): - """ Remove a tag(s). - - Parameters: - tags (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('tag', tags, locked=locked, remove=True) - - -class WriterMixin(EditTagsMixin): - """ Mixin for Plex objects that can have writers. """ - - def addWriter(self, writers, locked=True): - """ Add a writer tag(s). - - Parameters: - writers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('writer', writers, locked=locked) - - def removeWriter(self, writers, locked=True): - """ Remove a writer tag(s). - - Parameters: - writers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. - locked (bool): True (default) to lock the field, False to unlock the field. - """ - return self.editTags('writer', writers, locked=locked, remove=True) - - -class WatchlistMixin: - """ Mixin for Plex objects that can be added to a user's watchlist. """ - - def onWatchlist(self, account=None): - """ Returns True if the item is on the user's watchlist. - Also see :func:`~plexapi.myplex.MyPlexAccount.onWatchlist`. - - Parameters: - account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to check item on the watchlist. - Note: This is required if you are not connected to a Plex server instance using the admin account. - """ - try: - account = account or self._server.myPlexAccount() - except AttributeError: - account = self._server - return account.onWatchlist(self) - - def addToWatchlist(self, account=None): - """ Add this item to the specified user's watchlist. - Also see :func:`~plexapi.myplex.MyPlexAccount.addToWatchlist`. - - Parameters: - account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to add item to the watchlist. - Note: This is required if you are not connected to a Plex server instance using the admin account. - """ - try: - account = account or self._server.myPlexAccount() - except AttributeError: - account = self._server - account.addToWatchlist(self) - return self - - def removeFromWatchlist(self, account=None): - """ Remove this item from the specified user's watchlist. - Also see :func:`~plexapi.myplex.MyPlexAccount.removeFromWatchlist`. - - Parameters: - account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to remove item from the watchlist. - Note: This is required if you are not connected to a Plex server instance using the admin account. - """ - try: - account = account or self._server.myPlexAccount() - except AttributeError: - account = self._server - account.removeFromWatchlist(self) - return self - - def streamingServices(self, account=None): - """ Return a list of :class:`~plexapi.media.Availability` - objects for the available streaming services for this item. - - Parameters: - account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account used to retrieve availability. - Note: This is required if you are not connected to a Plex server instance using the admin account. - """ - try: - account = account or self._server.myPlexAccount() - except AttributeError: - account = self._server - ratingKey = self.guid.rsplit('/', 1)[-1] - data = account.query(f"{account.METADATA}/library/metadata/{ratingKey}/availabilities") - return self.findItems(data) - - -class MovieEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, EditionTitleMixin, - OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, - StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, - CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin -): - pass - - -class ShowEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, - OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, - SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, - CollectionMixin, GenreMixin, LabelMixin, -): - pass - - -class SeasonEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, - SummaryMixin, TitleMixin, UserRatingMixin, - CollectionMixin, LabelMixin -): - pass - - -class EpisodeEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, - OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, - CollectionMixin, DirectorMixin, LabelMixin, WriterMixin -): - pass - - -class ArtistEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, - SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, - CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin -): - pass - - -class AlbumEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, - OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, - CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin -): - pass - - -class TrackEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, - TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, - CollectionMixin, GenreMixin, LabelMixin, MoodMixin -): - pass - - -class PhotoalbumEditMixins( - ArtLockMixin, PosterLockMixin, - AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin -): - pass - - -class PhotoEditMixins( - ArtLockMixin, PosterLockMixin, - AddedAtMixin, PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, - TagMixin -): - pass - - -class CollectionEditMixins( - ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, - SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, - LabelMixin -): - pass - - -class PlaylistEditMixins( - ArtLockMixin, PosterLockMixin, - SortTitleMixin, SummaryMixin, TitleMixin -): - pass diff --git a/plexapi/mixins/__init__.py b/plexapi/mixins/__init__.py new file mode 100644 index 000000000..c5e01b872 --- /dev/null +++ b/plexapi/mixins/__init__.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +""" +PlexAPI Mixins Module + +This module contains mixins for Plex objects. +""" + +from .advanced_settings import AdvancedSettingsMixin +from .edit import ( + AddedAtMixin, AudienceRatingMixin, CollectionMixin, ContentRatingMixin, + CountryMixin, CriticRatingMixin, DirectorMixin, EditionTitleMixin, + EditFieldMixin, EditTagsMixin, GenreMixin, LabelMixin, MoodMixin, + OriginallyAvailableMixin, OriginalTitleMixin, PhotoCapturedTimeMixin, + ProducerMixin, SimilarArtistMixin, SortTitleMixin, StudioMixin, + StyleMixin, SummaryMixin, TaglineMixin, TagMixin, TitleMixin, + TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin, + UserRatingMixin, WriterMixin +) +from .objects import ExtrasMixin, HubsMixin +from .played_unplayed import PlayedUnplayedMixin +from .rating import RatingMixin +from .resources import ( + ArtLockMixin, ArtMixin, ArtUrlMixin, + LogoLockMixin, LogoMixin, LogoUrlMixin, + PosterLockMixin, PosterMixin, PosterUrlMixin, + SquareArtLockMixin, SquareArtMixin, SquareArtUrlMixin, + ThemeLockMixin, ThemeMixin, ThemeUrlMixin +) +from .smart_filter import SmartFilterMixin +from .split_merge import SplitMergeMixin +from .unmatch_match import UnmatchMatchMixin +from .watchlist import WatchlistMixin + + +class MovieEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, EditionTitleMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, + StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, + CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin +): + pass + + +class ShowEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, + SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, + CollectionMixin, GenreMixin, LabelMixin, +): + pass + + +class SeasonEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SummaryMixin, TitleMixin, UserRatingMixin, + CollectionMixin, LabelMixin +): + pass + + +class EpisodeEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + CollectionMixin, DirectorMixin, LabelMixin, WriterMixin +): + pass + + +class ArtistEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin +): + pass + + +class AlbumEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, + CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin +): + pass + + +class TrackEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, + CollectionMixin, GenreMixin, LabelMixin, MoodMixin +): + pass + + +class PhotoalbumEditMixins( + ArtLockMixin, PosterLockMixin, + AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin +): + pass + + +class PhotoEditMixins( + ArtLockMixin, PosterLockMixin, + AddedAtMixin, PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + TagMixin +): + pass + + +class CollectionEditMixins( + ArtLockMixin, PosterLockMixin, ThemeLockMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + LabelMixin +): + pass + + +class PlaylistEditMixins( + ArtLockMixin, PosterLockMixin, + SortTitleMixin, SummaryMixin, TitleMixin +): + pass + + +class MovieMixins( + AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, + MovieEditMixins, + WatchlistMixin +): + pass + + +class ShowMixins( + AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, + ShowEditMixins, + WatchlistMixin +): + pass + + +class SeasonMixins( + AdvancedSettingsMixin, ExtrasMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, + SeasonEditMixins +): + pass + + +class EpisodeMixins( + ExtrasMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, + EpisodeEditMixins +): + pass + + +class ClipMixins( + ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin +): + pass + + +class ArtistMixins( + AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, + ArtistEditMixins +): + pass + + +class AlbumMixins( + SplitMergeMixin, UnmatchMatchMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, + AlbumEditMixins +): + pass + + +class TrackMixins( + ExtrasMixin, RatingMixin, + ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin, ThemeUrlMixin, + TrackEditMixins +): + pass + + +class PhotoalbumMixins( + RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, + PhotoalbumEditMixins +): + pass + + +class PhotoMixins( + RatingMixin, + ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin, + PhotoEditMixins +): + pass + + +class CollectionMixins( + AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, + CollectionEditMixins +): + pass + + +class PlaylistMixins( + SmartFilterMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, + PlaylistEditMixins +): + pass + + +__all__ = [ + # Advanced settings + 'AdvancedSettingsMixin', + # Edit mixins + 'AddedAtMixin', 'AudienceRatingMixin', 'CollectionMixin', 'ContentRatingMixin', + 'CountryMixin', 'CriticRatingMixin', 'DirectorMixin', 'EditionTitleMixin', + 'EditFieldMixin', 'EditTagsMixin', 'GenreMixin', 'LabelMixin', 'MoodMixin', + 'OriginallyAvailableMixin', 'OriginalTitleMixin', 'PhotoCapturedTimeMixin', + 'ProducerMixin', 'SimilarArtistMixin', 'SortTitleMixin', 'StudioMixin', + 'StyleMixin', 'SummaryMixin', 'TaglineMixin', 'TagMixin', 'TitleMixin', + 'TrackArtistMixin', 'TrackDiscNumberMixin', 'TrackNumberMixin', + 'UserRatingMixin', 'WriterMixin', + # Objects + 'ExtrasMixin', 'HubsMixin', + # Played/Unplayed + 'PlayedUnplayedMixin', + # Rating + 'RatingMixin', + # Resource mixins + 'ArtLockMixin', 'ArtMixin', 'ArtUrlMixin', + 'LogoLockMixin', 'LogoMixin', 'LogoUrlMixin', + 'PosterLockMixin', 'PosterMixin', 'PosterUrlMixin', + 'SquareArtLockMixin', 'SquareArtMixin', 'SquareArtUrlMixin', + 'ThemeLockMixin', 'ThemeMixin', 'ThemeUrlMixin', + # Smart Filter + 'SmartFilterMixin', + # Split/Merge + 'SplitMergeMixin', + # Unmatch/Match + 'UnmatchMatchMixin', + # Watchlist + 'WatchlistMixin', + # Composite Edit Mixins + 'AlbumEditMixins', 'ArtistEditMixins', 'CollectionEditMixins', 'EpisodeEditMixins', + 'MovieEditMixins', 'PhotoEditMixins', 'PhotoalbumEditMixins', 'PlaylistEditMixins', + 'SeasonEditMixins', 'ShowEditMixins', 'TrackEditMixins', + # Composite Mixins + 'AlbumMixins', 'ArtistMixins', 'ClipMixins', 'CollectionMixins', 'EpisodeMixins', + 'MovieMixins', 'PhotoMixins', 'PhotoalbumMixins', 'PlaylistMixins', + 'SeasonMixins', 'ShowMixins', 'TrackMixins', +] diff --git a/plexapi/mixins/advanced_settings.py b/plexapi/mixins/advanced_settings.py new file mode 100644 index 000000000..bb74eb5e3 --- /dev/null +++ b/plexapi/mixins/advanced_settings.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from urllib.parse import urlencode + +from plexapi import settings +from plexapi.exceptions import NotFound + + +class AdvancedSettingsMixin: + """ Mixin for Plex objects that can have advanced settings. """ + + def preferences(self): + """ Returns a list of :class:`~plexapi.settings.Preferences` objects. """ + key = f'{self.key}?includePreferences=1' + return self.fetchItems(key, cls=settings.Preferences, rtag='Preferences') + + def preference(self, pref): + """ Returns a :class:`~plexapi.settings.Preferences` object for the specified pref. + + Parameters: + pref (str): The id of the preference to return. + """ + prefs = self.preferences() + try: + return next(p for p in prefs if p.id == pref) + except StopIteration: + availablePrefs = [p.id for p in prefs] + raise NotFound(f'Unknown preference "{pref}" for {self.TYPE}. ' + f'Available preferences: {availablePrefs}') from None + + def editAdvanced(self, **kwargs): + """ Edit a Plex object's advanced settings. """ + data = {} + key = f'{self.key}/prefs?' + preferences = {pref.id: pref for pref in self.preferences() if pref.enumValues} + for settingID, value in kwargs.items(): + try: + pref = preferences[settingID] + except KeyError: + raise NotFound(f'{value} not found in {list(preferences.keys())}') + + enumValues = pref.enumValues + if enumValues.get(value, enumValues.get(str(value))): + data[settingID] = value + else: + raise NotFound(f'{value} not found in {list(enumValues)}') + url = key + urlencode(data) + self._server.query(url, method=self._server._session.put) + return self + + def defaultAdvanced(self): + """ Edit all of a Plex object's advanced settings to default. """ + data = {} + key = f'{self.key}/prefs?' + for preference in self.preferences(): + data[preference.id] = preference.default + url = key + urlencode(data) + self._server.query(url, method=self._server._session.put) + return self diff --git a/plexapi/mixins/edit.py b/plexapi/mixins/edit.py new file mode 100644 index 000000000..182417ab4 --- /dev/null +++ b/plexapi/mixins/edit.py @@ -0,0 +1,583 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from urllib.parse import quote + + +class EditFieldMixin: + """ Mixin for editing Plex object fields. """ + + def editField(self, field, value, locked=True, **kwargs): + """ Edit the field of a Plex object. All field editing methods can be chained together. + Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing fields. + + Parameters: + field (str): The name of the field to edit. + value (str): The value to edit the field to. + locked (bool): True (default) to lock the field, False to unlock the field. + + Example: + + .. code-block:: python + + # Chaining multiple field edits with reloading + Movie.editTitle('A New Title').editSummary('A new summary').editTagline('A new tagline').reload() + + """ + edits = { + f'{field}.value': value or '', + f'{field}.locked': 1 if locked else 0 + } + edits.update(kwargs) + return self._edit(**edits) + + +class AddedAtMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an added at date. """ + + def editAddedAt(self, addedAt, locked=True): + """ Edit the added at date. + + Parameters: + addedAt (int or str or datetime): The new value as a unix timestamp (int), + "YYYY-MM-DD" (str), or datetime object. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + if isinstance(addedAt, str): + addedAt = int(round(datetime.strptime(addedAt, '%Y-%m-%d').timestamp())) + elif isinstance(addedAt, datetime): + addedAt = int(round(addedAt.timestamp())) + return self.editField('addedAt', addedAt, locked=locked) + + +class AudienceRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an audience rating. """ + + def editAudienceRating(self, audienceRating, locked=True): + """ Edit the audience rating. + + Parameters: + audienceRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('audienceRating', audienceRating, locked=locked) + + +class ContentRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a content rating. """ + + def editContentRating(self, contentRating, locked=True): + """ Edit the content rating. + + Parameters: + contentRating (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('contentRating', contentRating, locked=locked) + + +class CriticRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a critic rating. """ + + def editCriticRating(self, criticRating, locked=True): + """ Edit the critic rating. + + Parameters: + criticRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('rating', criticRating, locked=locked) + + +class EditionTitleMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an edition title. """ + + def editEditionTitle(self, editionTitle, locked=True): + """ Edit the edition title. Plex Pass is required to edit this field. + + Parameters: + editionTitle (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('editionTitle', editionTitle, locked=locked) + + +class OriginallyAvailableMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an originally available date. """ + + def editOriginallyAvailable(self, originallyAvailable, locked=True): + """ Edit the originally available date. + + Parameters: + originallyAvailable (str or datetime): The new value "YYYY-MM-DD (str) or datetime object. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + if isinstance(originallyAvailable, datetime): + originallyAvailable = originallyAvailable.strftime('%Y-%m-%d') + return self.editField('originallyAvailableAt', originallyAvailable, locked=locked) + + +class OriginalTitleMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an original title. """ + + def editOriginalTitle(self, originalTitle, locked=True): + """ Edit the original title. + + Parameters: + originalTitle (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('originalTitle', originalTitle, locked=locked) + + +class SortTitleMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a sort title. """ + + def editSortTitle(self, sortTitle, locked=True): + """ Edit the sort title. + + Parameters: + sortTitle (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('titleSort', sortTitle, locked=locked) + + +class StudioMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a studio. """ + + def editStudio(self, studio, locked=True): + """ Edit the studio. + + Parameters: + studio (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('studio', studio, locked=locked) + + +class SummaryMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a summary. """ + + def editSummary(self, summary, locked=True): + """ Edit the summary. + + Parameters: + summary (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('summary', summary, locked=locked) + + +class TaglineMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a tagline. """ + + def editTagline(self, tagline, locked=True): + """ Edit the tagline. + + Parameters: + tagline (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('tagline', tagline, locked=locked) + + +class TitleMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a title. """ + + def editTitle(self, title, locked=True): + """ Edit the title. + + Parameters: + title (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + kwargs = {} + if self.TYPE == 'album': + # Editing album title also requires the artist ratingKey + kwargs['artist.id.value'] = self.parentRatingKey + return self.editField('title', title, locked=locked, **kwargs) + + +class TrackArtistMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a track artist. """ + + def editTrackArtist(self, trackArtist, locked=True): + """ Edit the track artist. + + Parameters: + trackArtist (str): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('originalTitle', trackArtist, locked=locked) + + +class TrackNumberMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a track number. """ + + def editTrackNumber(self, trackNumber, locked=True): + """ Edit the track number. + + Parameters: + trackNumber (int): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('index', trackNumber, locked=locked) + + +class TrackDiscNumberMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a track disc number. """ + + def editDiscNumber(self, discNumber, locked=True): + """ Edit the track disc number. + + Parameters: + discNumber (int): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('parentIndex', discNumber, locked=locked) + + +class PhotoCapturedTimeMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a captured time. """ + + def editCapturedTime(self, capturedTime, locked=True): + """ Edit the photo captured time. + + Parameters: + capturedTime (str or datetime): The new value "YYYY-MM-DD hh:mm:ss" (str) or datetime object. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + if isinstance(capturedTime, datetime): + capturedTime = capturedTime.strftime('%Y-%m-%d %H:%M:%S') + return self.editField('originallyAvailableAt', capturedTime, locked=locked) + + +class UserRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a user rating. """ + + def editUserRating(self, userRating, locked=True): + """ Edit the user rating. + + Parameters: + userRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('userRating', userRating, locked=locked) + + +class EditTagsMixin: + """ Mixin for editing Plex object tags. """ + + def editTags(self, tag, items, locked=True, remove=False, **kwargs): + """ Edit the tags of a Plex object. All tag editing methods can be chained together. + Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing tags. + + Parameters: + tag (str): Name of the tag to edit. + items (List or List<:class:`~plexapi.media.MediaTag`>): List of tags to add or remove. + locked (bool): True (default) to lock the tags, False to unlock the tags. + remove (bool): True to remove the tags in items. + + Example: + + .. code-block:: python + + # Chaining multiple tag edits with reloading + Show.addCollection('New Collection').removeGenre('Action').addLabel('Favorite').reload() + + """ + if not isinstance(items, list): + items = [items] + + if not remove: + tags = getattr(self, self._tagPlural(tag), []) + if isinstance(tags, list): + items = tags + items + + edits = self._tagHelper(self._tagSingular(tag), items, locked, remove) + edits.update(kwargs) + return self._edit(**edits) + + @staticmethod + def _tagSingular(tag): + """ Return the singular name of a tag. """ + if tag == 'countries': + return 'country' + elif tag == 'similar': + return 'similar' + elif tag[-1] == 's': + return tag[:-1] + return tag + + @staticmethod + def _tagPlural(tag): + """ Return the plural name of a tag. """ + if tag == 'country': + return 'countries' + elif tag == 'similar': + return 'similar' + elif tag[-1] != 's': + return tag + 's' + return tag + + @staticmethod + def _tagHelper(tag, items, locked=True, remove=False): + """ Return a dict of the query parameters for editing a tag. """ + if not isinstance(items, list): + items = [items] + + data = { + f'{tag}.locked': 1 if locked else 0 + } + + if remove: + tagname = f'{tag}[].tag.tag-' + data[tagname] = ','.join(quote(str(t)) for t in items) + else: + for i, item in enumerate(items): + tagname = f'{str(tag)}[{i}].tag.tag' + data[tagname] = item + + return data + + +class CollectionMixin(EditTagsMixin): + """ Mixin for Plex objects that can have collections. """ + + def addCollection(self, collections, locked=True): + """ Add a collection tag(s). + + Parameters: + collections (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('collection', collections, locked=locked) + + def removeCollection(self, collections, locked=True): + """ Remove a collection tag(s). + + Parameters: + collections (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('collection', collections, locked=locked, remove=True) + + +class CountryMixin(EditTagsMixin): + """ Mixin for Plex objects that can have countries. """ + + def addCountry(self, countries, locked=True): + """ Add a country tag(s). + + Parameters: + countries (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('country', countries, locked=locked) + + def removeCountry(self, countries, locked=True): + """ Remove a country tag(s). + + Parameters: + countries (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('country', countries, locked=locked, remove=True) + + +class DirectorMixin(EditTagsMixin): + """ Mixin for Plex objects that can have directors. """ + + def addDirector(self, directors, locked=True): + """ Add a director tag(s). + + Parameters: + directors (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('director', directors, locked=locked) + + def removeDirector(self, directors, locked=True): + """ Remove a director tag(s). + + Parameters: + directors (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('director', directors, locked=locked, remove=True) + + +class GenreMixin(EditTagsMixin): + """ Mixin for Plex objects that can have genres. """ + + def addGenre(self, genres, locked=True): + """ Add a genre tag(s). + + Parameters: + genres (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('genre', genres, locked=locked) + + def removeGenre(self, genres, locked=True): + """ Remove a genre tag(s). + + Parameters: + genres (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('genre', genres, locked=locked, remove=True) + + +class LabelMixin(EditTagsMixin): + """ Mixin for Plex objects that can have labels. """ + + def addLabel(self, labels, locked=True): + """ Add a label tag(s). + + Parameters: + labels (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('label', labels, locked=locked) + + def removeLabel(self, labels, locked=True): + """ Remove a label tag(s). + + Parameters: + labels (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('label', labels, locked=locked, remove=True) + + +class MoodMixin(EditTagsMixin): + """ Mixin for Plex objects that can have moods. """ + + def addMood(self, moods, locked=True): + """ Add a mood tag(s). + + Parameters: + moods (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('mood', moods, locked=locked) + + def removeMood(self, moods, locked=True): + """ Remove a mood tag(s). + + Parameters: + moods (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('mood', moods, locked=locked, remove=True) + + +class ProducerMixin(EditTagsMixin): + """ Mixin for Plex objects that can have producers. """ + + def addProducer(self, producers, locked=True): + """ Add a producer tag(s). + + Parameters: + producers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('producer', producers, locked=locked) + + def removeProducer(self, producers, locked=True): + """ Remove a producer tag(s). + + Parameters: + producers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('producer', producers, locked=locked, remove=True) + + +class SimilarArtistMixin(EditTagsMixin): + """ Mixin for Plex objects that can have similar artists. """ + + def addSimilarArtist(self, artists, locked=True): + """ Add a similar artist tag(s). + + Parameters: + artists (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('similar', artists, locked=locked) + + def removeSimilarArtist(self, artists, locked=True): + """ Remove a similar artist tag(s). + + Parameters: + artists (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('similar', artists, locked=locked, remove=True) + + +class StyleMixin(EditTagsMixin): + """ Mixin for Plex objects that can have styles. """ + + def addStyle(self, styles, locked=True): + """ Add a style tag(s). + + Parameters: + styles (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('style', styles, locked=locked) + + def removeStyle(self, styles, locked=True): + """ Remove a style tag(s). + + Parameters: + styles (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('style', styles, locked=locked, remove=True) + + +class TagMixin(EditTagsMixin): + """ Mixin for Plex objects that can have tags. """ + + def addTag(self, tags, locked=True): + """ Add a tag(s). + + Parameters: + tags (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('tag', tags, locked=locked) + + def removeTag(self, tags, locked=True): + """ Remove a tag(s). + + Parameters: + tags (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('tag', tags, locked=locked, remove=True) + + +class WriterMixin(EditTagsMixin): + """ Mixin for Plex objects that can have writers. """ + + def addWriter(self, writers, locked=True): + """ Add a writer tag(s). + + Parameters: + writers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('writer', writers, locked=locked) + + def removeWriter(self, writers, locked=True): + """ Remove a writer tag(s). + + Parameters: + writers (List or List<:class:`~plexapi.media.MediaTag`>): List of tags. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editTags('writer', writers, locked=locked, remove=True) diff --git a/plexapi/mixins/objects.py b/plexapi/mixins/objects.py new file mode 100644 index 000000000..8cd4c3c6a --- /dev/null +++ b/plexapi/mixins/objects.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + + +class ExtrasMixin: + """ Mixin for Plex objects that can have extras. """ + + def extras(self): + """ Returns a list of :class:`~plexapi.video.Extra` objects. """ + from plexapi.video import Extra + key = f'{self.key}/extras' + return self.fetchItems(key, cls=Extra) + + +class HubsMixin: + """ Mixin for Plex objects that can have related hubs. """ + + def hubs(self): + """ Returns a list of :class:`~plexapi.library.Hub` objects. """ + from plexapi.library import Hub + key = f'{self.key}/related' + return self.fetchItems(key, cls=Hub) diff --git a/plexapi/mixins/played_unplayed.py b/plexapi/mixins/played_unplayed.py new file mode 100644 index 000000000..6e6165559 --- /dev/null +++ b/plexapi/mixins/played_unplayed.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + + +class PlayedUnplayedMixin: + """ Mixin for Plex objects that can be marked played and unplayed. """ + + @property + def isPlayed(self): + """ Returns True if this video is played. """ + return bool(self.viewCount > 0) if self.viewCount else False + + def markPlayed(self): + """ Mark the Plex object as played. """ + key = '/:/scrobble' + params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} + self._server.query(key, params=params) + return self + + def markUnplayed(self): + """ Mark the Plex object as unplayed. """ + key = '/:/unscrobble' + params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} + self._server.query(key, params=params) + return self + + @property + def isWatched(self): + """ Alias to self.isPlayed. """ + return self.isPlayed + + def markWatched(self): + """ Alias to :func:`~plexapi.mixins.PlayedUnplayedMixin.markPlayed`. """ + self.markPlayed() + + def markUnwatched(self): + """ Alias to :func:`~plexapi.mixins.PlayedUnplayedMixin.markUnplayed`. """ + self.markUnplayed() diff --git a/plexapi/mixins/rating.py b/plexapi/mixins/rating.py new file mode 100644 index 000000000..055c9c1d1 --- /dev/null +++ b/plexapi/mixins/rating.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from plexapi.exceptions import BadRequest + + +class RatingMixin: + """ Mixin for Plex objects that can have user star ratings. """ + + def rate(self, rating=None): + """ Rate the Plex object. Note: Plex ratings are displayed out of 5 stars (e.g. rating 7.0 = 3.5 stars). + + Parameters: + rating (float, optional): Rating from 0 to 10. Exclude to reset the rating. + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If the rating is invalid. + """ + if rating is None: + rating = -1 + elif not isinstance(rating, (int, float)) or rating < 0 or rating > 10: + raise BadRequest('Rating must be between 0 to 10.') + key = f'/:/rate?key={self.ratingKey}&identifier=com.plexapp.plugins.library&rating={rating}' + self._server.query(key, method=self._server._session.put) + return self diff --git a/plexapi/mixins/resources.py b/plexapi/mixins/resources.py new file mode 100644 index 000000000..33cd02123 --- /dev/null +++ b/plexapi/mixins/resources.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- +from urllib.parse import quote_plus + +from plexapi import media +from plexapi.utils import openOrRead + + +class ArtUrlMixin: + """ Mixin for Plex objects that can have a background artwork url. """ + + @property + def artUrl(self): + """ Return the art url for the Plex object. """ + art = self.firstAttr('art', 'grandparentArt') + return self._server.url(art, includeToken=True) if art else None + + +class ArtLockMixin: + """ Mixin for Plex objects that can have a locked background artwork. """ + + def lockArt(self): + """ Lock the background artwork for a Plex object. """ + return self._edit(**{'art.locked': 1}) + + def unlockArt(self): + """ Unlock the background artwork for a Plex object. """ + return self._edit(**{'art.locked': 0}) + + +class ArtMixin(ArtUrlMixin, ArtLockMixin): + """ Mixin for Plex objects that can have background artwork. """ + + def arts(self): + """ Returns list of available :class:`~plexapi.media.Art` objects. """ + return self.fetchItems(f'/library/metadata/{self.ratingKey}/arts', cls=media.Art) + + def uploadArt(self, url=None, filepath=None): + """ Upload a background artwork from a url or filepath. + + Parameters: + url (str): The full URL to the image to upload. + filepath (str): The full file path to the image to upload or file-like object. + """ + if url: + key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}' + self._server.query(key, method=self._server._session.post) + elif filepath: + key = f'/library/metadata/{self.ratingKey}/arts' + data = openOrRead(filepath) + self._server.query(key, method=self._server._session.post, data=data) + return self + + def setArt(self, art): + """ Set the background artwork for a Plex object. + + Parameters: + art (:class:`~plexapi.media.Art`): The art object to select. + """ + art.select() + return self + + def deleteArt(self): + """ Delete the art from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/art' + self._server.query(key, method=self._server._session.delete) + return self + + +class LogoUrlMixin: + """ Mixin for Plex objects that can have a logo url. """ + + @property + def logo(self): + """ Return the API path to the logo image. """ + return next((i.url for i in self.images if i.type == 'clearLogo'), None) + + @property + def logoUrl(self): + """ Return the logo url for the Plex object. """ + return self._server.url(self.logo, includeToken=True) if self.logo else None + + +class LogoLockMixin: + """ Mixin for Plex objects that can have a locked logo. """ + + def lockLogo(self): + """ Lock the logo for a Plex object. """ + return self._edit(**{'clearLogo.locked': 1}) + + def unlockLogo(self): + """ Unlock the logo for a Plex object. """ + return self._edit(**{'clearLogo.locked': 0}) + + +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 to 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. + + Parameters: + logo (:class:`~plexapi.media.Logo`): The logo object to select. + """ + logo.select() + return self + + def deleteLogo(self): + """ Delete the logo from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/clearLogo' + self._server.query(key, method=self._server._session.delete) + return self + + +class PosterUrlMixin: + """ Mixin for Plex objects that can have a poster url. """ + + @property + def thumbUrl(self): + """ Return the thumb url for the Plex object. """ + thumb = self.firstAttr('thumb', 'parentThumb', 'grandparentThumb') + return self._server.url(thumb, includeToken=True) if thumb else None + + @property + def posterUrl(self): + """ Alias to self.thumbUrl. """ + return self.thumbUrl + + +class PosterLockMixin: + """ Mixin for Plex objects that can have a locked poster. """ + + def lockPoster(self): + """ Lock the poster for a Plex object. """ + return self._edit(**{'thumb.locked': 1}) + + def unlockPoster(self): + """ Unlock the poster for a Plex object. """ + return self._edit(**{'thumb.locked': 0}) + + +class PosterMixin(PosterUrlMixin, PosterLockMixin): + """ Mixin for Plex objects that can have posters. """ + + def posters(self): + """ Returns list of available :class:`~plexapi.media.Poster` objects. """ + return self.fetchItems(f'/library/metadata/{self.ratingKey}/posters', cls=media.Poster) + + def uploadPoster(self, url=None, filepath=None): + """ Upload a poster from a url or filepath. + + Parameters: + url (str): The full URL to the image to upload. + filepath (str): The full file path to the image to upload or file-like object. + """ + if url: + key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}' + self._server.query(key, method=self._server._session.post) + elif filepath: + key = f'/library/metadata/{self.ratingKey}/posters' + data = openOrRead(filepath) + self._server.query(key, method=self._server._session.post, data=data) + return self + + def setPoster(self, poster): + """ Set the poster for a Plex object. + + Parameters: + poster (:class:`~plexapi.media.Poster`): The poster object to select. + """ + poster.select() + return self + + def deletePoster(self): + """ Delete the poster from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/thumb' + self._server.query(key, method=self._server._session.delete) + return self + + +class SquareArtUrlMixin: + """ Mixin for Plex objects that can have a square art url. """ + + @property + def squareArt(self): + """ Return the API path to the square art image. """ + return next((i.url for i in self.images if i.type == 'backgroundSquare'), None) + + @property + def squareArtUrl(self): + """ Return the square art url for the Plex object. """ + return self._server.url(self.squareArt, includeToken=True) if self.squareArt else None + + +class SquareArtLockMixin: + """ Mixin for Plex objects that can have a locked square art. """ + + def lockSquareArt(self): + """ Lock the square art for a Plex object. """ + return self._edit(**{'squareArt.locked': 1}) + + def unlockSquareArt(self): + """ Unlock the square art for a Plex object. """ + return self._edit(**{'squareArt.locked': 0}) + + +class SquareArtMixin(SquareArtUrlMixin, SquareArtLockMixin): + """ Mixin for Plex objects that can have square art. """ + + def squareArts(self): + """ Returns list of available :class:`~plexapi.media.SquareArt` objects. """ + return self.fetchItems(f'/library/metadata/{self.ratingKey}/squareArts', cls=media.SquareArt) + + def uploadSquareArt(self, url=None, filepath=None): + """ Upload a square art from a url or filepath. + + Parameters: + url (str): The full URL to the image to upload. + filepath (str): The full file path to the image to upload or file-like object. + """ + if url: + key = f'/library/metadata/{self.ratingKey}/squareArts?url={quote_plus(url)}' + self._server.query(key, method=self._server._session.post) + elif filepath: + key = f'/library/metadata/{self.ratingKey}/squareArts' + data = openOrRead(filepath) + self._server.query(key, method=self._server._session.post, data=data) + return self + + def setSquareArt(self, squareArt): + """ Set the square art for a Plex object. + + Parameters: + squareArt (:class:`~plexapi.media.SquareArt`): The square art object to select. + """ + squareArt.select() + return self + + def deleteSquareArt(self): + """ Delete the square art from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/squareArt' + self._server.query(key, method=self._server._session.delete) + return self + + +class ThemeUrlMixin: + """ 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 ThemeLockMixin: + """ Mixin for Plex objects that can have a locked theme. """ + + def lockTheme(self): + """ Lock the theme for a Plex object. """ + return self._edit(**{'theme.locked': 1}) + + def unlockTheme(self): + """ Unlock the theme for a Plex object. """ + return self._edit(**{'theme.locked': 0}) + + +class ThemeMixin(ThemeUrlMixin, ThemeLockMixin): + """ Mixin for Plex objects that can have themes. """ + + def themes(self): + """ Returns list of available :class:`~plexapi.media.Theme` objects. """ + return self.fetchItems(f'/library/metadata/{self.ratingKey}/themes', cls=media.Theme) + + def uploadTheme(self, url=None, filepath=None, timeout=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 or file-like object. + timeout (int, optional): Timeout, in seconds, to use when uploading themes to the server. + (default config.TIMEOUT). + """ + if url: + key = f'/library/metadata/{self.ratingKey}/themes?url={quote_plus(url)}' + self._server.query(key, method=self._server._session.post, timeout=timeout) + elif filepath: + key = f'/library/metadata/{self.ratingKey}/themes' + data = openOrRead(filepath) + self._server.query(key, method=self._server._session.post, data=data, timeout=timeout) + 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.' + ) + + def deleteTheme(self): + """ Delete the theme from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/theme' + self._server.query(key, method=self._server._session.delete) + return self diff --git a/plexapi/mixins/smart_filter.py b/plexapi/mixins/smart_filter.py new file mode 100644 index 000000000..3507aa71a --- /dev/null +++ b/plexapi/mixins/smart_filter.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from collections import deque +from typing import Deque, Set, Tuple, Union +from urllib.parse import parse_qsl, unquote, urlsplit + +from plexapi import utils + + +class SmartFilterMixin: + """ Mixin for Plex objects that can have smart filters. """ + + def _parseFilterGroups(self, feed: Deque[Tuple[str, str]], returnOn: Union[Set[str], None] = None) -> dict: + """ Parse filter groups from input lines between push and pop. """ + currentFiltersStack: list[dict] = [] + operatorForStack = None + if returnOn is None: + returnOn = set("pop") + else: + returnOn.add("pop") + allowedLogicalOperators = ["and", "or"] # first is the default + + while feed: + key, value = feed.popleft() # consume the first item + if key == "push": + # recurse and add the result to the current stack + currentFiltersStack.append( + self._parseFilterGroups(feed, returnOn) + ) + elif key in returnOn: + # stop iterating and return the current stack + if not key == "pop": + feed.appendleft((key, value)) # put the item back + break + + elif key in allowedLogicalOperators: + # set the operator + if operatorForStack and not operatorForStack == key: + raise ValueError( + "cannot have different logical operators for the same" + " filter group" + ) + operatorForStack = key + + else: + # add the key value pair to the current filter + currentFiltersStack.append({key: value}) + + if not operatorForStack and len(currentFiltersStack) > 1: + # consider 'and' as the default operator + operatorForStack = allowedLogicalOperators[0] + + if operatorForStack: + return {operatorForStack: currentFiltersStack} + return currentFiltersStack.pop() + + def _parseQueryFeed(self, feed: "deque[Tuple[str, str]]") -> dict: + """ Parse the query string into a dict. """ + filtersDict: dict[str, Union[str, int, list, dict]] = {} + special_keys = {"type", "sort"} + integer_keys = {"includeGuids", "limit"} + as_is_keys = {"group", "having"} + reserved_keys = special_keys | integer_keys | as_is_keys + while feed: + key, value = feed.popleft() + if key in integer_keys: + filtersDict[key] = int(value) + elif key in as_is_keys: + filtersDict[key] = value + elif key == "type": + filtersDict["libtype"] = utils.reverseSearchType(value) + elif key == "sort": + filtersDict["sort"] = value.split(",") + else: + feed.appendleft((key, value)) # put the item back + filter_group = self._parseFilterGroups( + feed, returnOn=reserved_keys + ) + if "filters" in filtersDict: + filtersDict["filters"] = { + "and": [filtersDict["filters"], filter_group] + } + else: + filtersDict["filters"] = filter_group + + return filtersDict + + def _parseFilters(self, content): + """ Parse the content string and returns the filter dict. """ + content = urlsplit(unquote(content)) + feed = deque() + + for key, value in parse_qsl(content.query): + # Move = sign to key when operator is == + if value.startswith("="): + key, value = f"{key}=", value[1:] + + feed.append((key, value)) + + return self._parseQueryFeed(feed) diff --git a/plexapi/mixins/split_merge.py b/plexapi/mixins/split_merge.py new file mode 100644 index 000000000..9d3487efa --- /dev/null +++ b/plexapi/mixins/split_merge.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + + +class SplitMergeMixin: + """ Mixin for Plex objects that can be split and merged. """ + + def split(self): + """ Split duplicated Plex object into separate objects. """ + key = f'{self.key}/split' + self._server.query(key, method=self._server._session.put) + return self + + def merge(self, ratingKeys): + """ Merge other Plex objects into the current object. + + Parameters: + ratingKeys (list): A list of rating keys to merge. + """ + if not isinstance(ratingKeys, list): + ratingKeys = str(ratingKeys).split(',') + + key = f"{self.key}/merge?ids={','.join(str(r) for r in ratingKeys)}" + self._server.query(key, method=self._server._session.put) + return self diff --git a/plexapi/mixins/unmatch_match.py b/plexapi/mixins/unmatch_match.py new file mode 100644 index 000000000..7e72d3d37 --- /dev/null +++ b/plexapi/mixins/unmatch_match.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from urllib.parse import urlencode + +from plexapi import media, utils +from plexapi.exceptions import NotFound + + +class UnmatchMatchMixin: + """ Mixin for Plex objects that can be unmatched and matched. """ + + def unmatch(self): + """ Unmatches metadata match from object. """ + key = f'{self.key}/unmatch' + self._server.query(key, method=self._server._session.put) + + def matches(self, agent=None, title=None, year=None, language=None): + """ Return list of (:class:`~plexapi.media.SearchResult`) metadata matches. + + Parameters: + agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) + title (str): Title of item to search for + year (str): Year of item to search in + language (str) : Language of item to search in + + Examples: + 1. video.matches() + 2. video.matches(title="something", year=2020) + 3. video.matches(title="something") + 4. video.matches(year=2020) + 5. video.matches(title="something", year="") + 6. video.matches(title="", year=2020) + 7. video.matches(title="", year="") + + 1. The default behaviour in Plex Web = no params in plexapi + 2. Both title and year specified by user + 3. Year automatically filled in + 4. Title automatically filled in + 5. Explicitly searches for title with blank year + 6. Explicitly searches for blank title with year + 7. I don't know what the user is thinking... return the same result as 1 + + For 2 to 7, the agent and language is automatically filled in + """ + key = f'{self.key}/matches' + params = {'manual': 1} + + if agent and not any([title, year, language]): + params['language'] = self.section().language + params['agent'] = utils.getAgentIdentifier(self.section(), agent) + else: + if any(x is not None for x in [agent, title, year, language]): + if title is None: + params['title'] = self.title + else: + params['title'] = title + + if year is None: + params['year'] = getattr(self, 'year', '') + else: + params['year'] = year + + params['language'] = language or self.section().language + + if agent is None: + params['agent'] = self.section().agent + else: + params['agent'] = utils.getAgentIdentifier(self.section(), agent) + + key = key + '?' + urlencode(params) + return self.fetchItems(key, cls=media.SearchResult) + + def fixMatch(self, searchResult=None, auto=False, agent=None): + """ Use match result to update show metadata. + + Parameters: + auto (bool): True uses first match from matches + False allows user to provide the match + searchResult (:class:`~plexapi.media.SearchResult`): Search result from + ~plexapi.base.matches() + agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) + """ + key = f'{self.key}/match' + if auto: + autoMatch = self.matches(agent=agent) + if autoMatch: + searchResult = autoMatch[0] + else: + raise NotFound(f'No matches found using this agent: ({agent}:{autoMatch})') + elif not searchResult: + raise NotFound('fixMatch() requires either auto=True or ' + 'searchResult=:class:`~plexapi.media.SearchResult`.') + + params = {'guid': searchResult.guid, + 'name': searchResult.name} + + data = key + '?' + urlencode(params) + self._server.query(data, method=self._server._session.put) + return self diff --git a/plexapi/mixins/watchlist.py b/plexapi/mixins/watchlist.py new file mode 100644 index 000000000..146b4b527 --- /dev/null +++ b/plexapi/mixins/watchlist.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + + +class WatchlistMixin: + """ Mixin for Plex objects that can be added to a user's watchlist. """ + + def onWatchlist(self, account=None): + """ Returns True if the item is on the user's watchlist. + Also see :func:`~plexapi.myplex.MyPlexAccount.onWatchlist`. + + Parameters: + account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to check item on the watchlist. + Note: This is required if you are not connected to a Plex server instance using the admin account. + """ + try: + account = account or self._server.myPlexAccount() + except AttributeError: + account = self._server + return account.onWatchlist(self) + + def addToWatchlist(self, account=None): + """ Add this item to the specified user's watchlist. + Also see :func:`~plexapi.myplex.MyPlexAccount.addToWatchlist`. + + Parameters: + account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to add item to the watchlist. + Note: This is required if you are not connected to a Plex server instance using the admin account. + """ + try: + account = account or self._server.myPlexAccount() + except AttributeError: + account = self._server + account.addToWatchlist(self) + return self + + def removeFromWatchlist(self, account=None): + """ Remove this item from the specified user's watchlist. + Also see :func:`~plexapi.myplex.MyPlexAccount.removeFromWatchlist`. + + Parameters: + account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account to remove item from the watchlist. + Note: This is required if you are not connected to a Plex server instance using the admin account. + """ + try: + account = account or self._server.myPlexAccount() + except AttributeError: + account = self._server + account.removeFromWatchlist(self) + return self + + def streamingServices(self, account=None): + """ Return a list of :class:`~plexapi.media.Availability` + objects for the available streaming services for this item. + + Parameters: + account (:class:`~plexapi.myplex.MyPlexAccount`, optional): Account used to retrieve availability. + Note: This is required if you are not connected to a Plex server instance using the admin account. + """ + try: + account = account or self._server.myPlexAccount() + except AttributeError: + account = self._server + ratingKey = self.guid.rsplit('/', 1)[-1] + data = account.query(f"{account.METADATA}/library/metadata/{ratingKey}/availabilities") + return self.findItems(data) diff --git a/plexapi/photo.py b/plexapi/photo.py index adb44d5eb..67446d321 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -6,19 +6,12 @@ from plexapi import media, utils, video from plexapi.base import Playable, PlexPartialObject, PlexSession, cached_data_property from plexapi.exceptions import BadRequest -from plexapi.mixins import ( - RatingMixin, - ArtUrlMixin, ArtMixin, LogoMixin, LogoUrlMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, - PhotoalbumEditMixins, PhotoEditMixins -) +from plexapi.mixins import PhotoalbumMixins, PhotoMixins @utils.registerPlexObject class Photoalbum( - PlexPartialObject, - RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, - PhotoalbumEditMixins + PlexPartialObject, PhotoalbumMixins ): """ Represents a single Photoalbum (collection of photos). @@ -157,10 +150,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Photo( - PlexPartialObject, Playable, - RatingMixin, - ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin, - PhotoEditMixins + PlexPartialObject, Playable, PhotoMixins ): """ Represents a single Photo. diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 8fe0625c6..4fdc24e16 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -8,16 +8,13 @@ from plexapi.base import Playable, PlexPartialObject, cached_data_property from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.library import LibrarySection, MusicSection -from plexapi.mixins import SmartFilterMixin, ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, PlaylistEditMixins +from plexapi.mixins import PlaylistMixins from plexapi.utils import deprecated @utils.registerPlexObject class Playlist( - PlexPartialObject, Playable, - SmartFilterMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, - PlaylistEditMixins + PlexPartialObject, Playable, PlaylistMixins ): """ Represents a single Playlist. diff --git a/plexapi/video.py b/plexapi/video.py index 7f23912fd..8c4ec9165 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -6,13 +6,7 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession, cached_data_property from plexapi.exceptions import BadRequest -from plexapi.mixins import ( - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, - ArtUrlMixin, ArtMixin, LogoMixin, LogoUrlMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, - ThemeUrlMixin, ThemeMixin, - MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins, - WatchlistMixin -) +from plexapi.mixins import MovieMixins, ShowMixins, SeasonMixins, EpisodeMixins, ClipMixins, PlayedUnplayedMixin class Video(PlexPartialObject, PlayedUnplayedMixin): @@ -337,11 +331,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F @utils.registerPlexObject class Movie( - Video, Playable, - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, - MovieEditMixins, - WatchlistMixin + Video, Playable, MovieMixins ): """ Represents a single Movie. @@ -549,11 +539,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Show( - Video, - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, - ShowEditMixins, - WatchlistMixin + Video, ShowMixins ): """ Represents a single Show (including all seasons and episodes). @@ -801,10 +787,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Season( - Video, - AdvancedSettingsMixin, ExtrasMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, - SeasonEditMixins + Video, SeasonMixins ): """ Represents a single Season. @@ -983,10 +966,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Episode( - Video, Playable, - ExtrasMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, - EpisodeEditMixins + Video, Playable, EpisodeMixins ): """ Represents a single Episode. @@ -1260,8 +1240,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Clip( - Video, Playable, - ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin + Video, Playable, ClipMixins ): """ Represents a single Clip.