python-plexapi/plexapi/mixins.py

1226 lines
44 KiB
Python
Raw Normal View History

2021-01-24 22:35:13 +00:00
# -*- coding: utf-8 -*-
from collections import deque
from datetime import datetime
from typing import Tuple
from urllib.parse import parse_qsl, quote, quote_plus, unquote, urlencode, urlsplit
2021-01-24 23:29:20 +00:00
from plexapi import media, settings, utils
2021-05-30 22:37:44 +00:00
from plexapi.exceptions import BadRequest, NotFound
from plexapi.utils import deprecated, openOrRead
2021-01-24 23:29:20 +00:00
class AdvancedSettingsMixin:
""" Mixin for Plex objects that can have advanced settings. """
def preferences(self):
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
data = self._server.query(self._details_key)
2021-05-25 00:28:11 +00:00
return self.findItems(data, 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():
2021-04-07 03:44:28 +00:00
try:
pref = preferences[settingID]
2021-04-07 03:44:28 +00:00
except KeyError:
raise NotFound(f'{value} not found in {list(preferences.keys())}')
2023-08-29 03:29:39 +00:00
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: "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"}
reserved_keys = special_keys | integer_keys
while feed:
key, value = feed.popleft()
if key in integer_keys:
filtersDict[key] = int(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()
2023-08-29 03:29:39 +00:00
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.
2023-08-29 03:29:39 +00:00
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)
data = self._server.query(key, method=self._server._session.get)
return self.findItems(data, initpath=key)
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:
2022-02-27 06:04:18 +00:00
""" 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
data = self._server.query(self._details_key)
return self.findItems(data, Extra, rtag='Extras')
class HubsMixin:
2022-02-27 06:04:18 +00:00
""" 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
2022-07-21 02:49:11 +00:00
key = f'{self.key}/related'
data = self._server.query(key)
return self.findItems(data, Hub)
2022-02-27 06:04:18 +00:00
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
@deprecated('use "isPlayed" instead', stacklevel=3)
def isWatched(self):
""" Returns True if the show is watched. """
return self.isPlayed
@deprecated('use "markPlayed" instead')
def markWatched(self):
""" Mark the video as played. """
self.markPlayed()
@deprecated('use "markUnplayed" instead')
def markUnwatched(self):
""" Mark the video as unplayed. """
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:
2021-02-15 04:06:49 +00:00
""" Mixin for Plex objects that can have a background artwork url. """
2023-08-29 03:29:39 +00:00
@property
def artUrl(self):
2021-02-15 04:06:49 +00:00
""" Return the art url for the Plex object. """
art = self.firstAttr('art', 'grandparentArt')
return self._server.url(art, includeToken=True) if art else None
2021-02-15 03:38:09 +00:00
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):
2021-02-15 04:06:49 +00:00
""" Mixin for Plex objects that can have background artwork. """
2021-02-15 03:38:09 +00:00
2021-02-07 04:19:11 +00:00
def arts(self):
""" Returns list of available :class:`~plexapi.media.Art` objects. """
return self.fetchItems(f'/library/metadata/{self.ratingKey}/arts', cls=media.Art)
2021-01-24 23:29:20 +00:00
2021-02-07 04:19:11 +00:00
def uploadArt(self, url=None, filepath=None):
2021-02-15 00:11:50 +00:00
""" Upload a background artwork from a url or filepath.
2023-08-29 03:29:39 +00:00
2021-01-24 23:29:20 +00:00
Parameters:
url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload or file-like object.
2021-01-24 23:29:20 +00:00
"""
if url:
key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}'
2021-01-24 23:29:20 +00:00
self._server.query(key, method=self._server._session.post)
elif filepath:
key = f'/library/metadata/{self.ratingKey}/arts'
data = openOrRead(filepath)
2021-01-24 23:29:20 +00:00
self._server.query(key, method=self._server._session.post, data=data)
return self
2021-01-24 23:29:20 +00:00
2021-02-07 04:19:11 +00:00
def setArt(self, art):
2021-02-15 00:11:50 +00:00
""" Set the background artwork for a Plex object.
2023-08-29 03:29:39 +00:00
2021-01-24 23:29:20 +00:00
Parameters:
2021-02-07 04:19:11 +00:00
art (:class:`~plexapi.media.Art`): The art object to select.
2021-01-24 23:29:20 +00:00
"""
2021-02-07 04:19:11 +00:00
art.select()
return self
2021-01-24 23:29:20 +00:00
2021-02-15 00:11:50 +00:00
class PosterUrlMixin:
2021-02-15 04:06:49 +00:00
""" Mixin for Plex objects that can have a poster url. """
2021-02-07 04:19:11 +00:00
@property
def thumbUrl(self):
2021-02-15 04:06:49 +00:00
""" 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
2021-02-15 03:58:18 +00:00
@property
def posterUrl(self):
2021-02-15 04:06:49 +00:00
""" Alias to self.thumbUrl. """
2021-02-15 03:58:18 +00:00
return self.thumbUrl
2021-02-15 03:38:09 +00:00
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):
2021-02-15 04:06:49 +00:00
""" Mixin for Plex objects that can have posters. """
2021-02-15 03:38:09 +00:00
2021-02-07 04:19:11 +00:00
def posters(self):
""" Returns list of available :class:`~plexapi.media.Poster` objects. """
return self.fetchItems(f'/library/metadata/{self.ratingKey}/posters', cls=media.Poster)
2021-02-07 04:19:11 +00:00
def uploadPoster(self, url=None, filepath=None):
2021-02-15 00:11:50 +00:00
""" Upload a poster from a url or filepath.
2021-02-07 04:19:11 +00:00
2021-01-24 23:29:20 +00:00
Parameters:
url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload or file-like object.
2021-01-24 23:29:20 +00:00
"""
if url:
key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}'
2021-01-24 23:29:20 +00:00
self._server.query(key, method=self._server._session.post)
elif filepath:
key = f'/library/metadata/{self.ratingKey}/posters'
data = openOrRead(filepath)
2021-01-24 23:29:20 +00:00
self._server.query(key, method=self._server._session.post, data=data)
return self
2021-01-24 23:29:20 +00:00
2021-02-07 04:19:11 +00:00
def setPoster(self, poster):
""" Set the poster for a Plex object.
2023-08-29 03:29:39 +00:00
2021-01-24 23:29:20 +00:00
Parameters:
2021-02-07 04:19:11 +00:00
poster (:class:`~plexapi.media.Poster`): The poster object to select.
2021-01-24 23:29:20 +00:00
"""
2021-02-07 04:19:11 +00:00
poster.select()
return self
2021-01-24 22:35:13 +00:00
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):
raise NotImplementedError(
'Themes cannot be set through the API. '
'Re-upload the theme using "uploadTheme" to set it.'
)
class EditFieldMixin:
""" Mixin for editing Plex object fields. """
2023-08-29 03:29:39 +00:00
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.
2023-08-29 03:29:39 +00:00
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)
2021-05-30 22:37:44 +00:00
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 ContentRatingMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a content rating. """
def editContentRating(self, contentRating, locked=True):
""" Edit the content rating.
2021-05-30 22:37:44 +00:00
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)
2021-05-30 22:37:44 +00:00
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.
2021-05-30 22:37:44 +00:00
"""
if isinstance(originallyAvailable, datetime):
originallyAvailable = originallyAvailable.strftime('%Y-%m-%d')
return self.editField('originallyAvailableAt', originallyAvailable, locked=locked)
2021-05-30 22:37:44 +00:00
class OriginalTitleMixin(EditFieldMixin):
""" Mixin for Plex objects that can have an original title. """
2021-01-04 20:30:42 +00:00
def editOriginalTitle(self, originalTitle, locked=True):
""" Edit the original title.
2021-01-04 20:30:42 +00:00
2021-01-24 23:13:22 +00:00
Parameters:
originalTitle (str): The new value.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 23:13:22 +00:00
"""
return self.editField('originalTitle', originalTitle, locked=locked)
2021-01-04 20:30:42 +00:00
2021-01-24 23:18:28 +00:00
class SortTitleMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a sort title. """
2021-01-24 23:18:28 +00:00
def editSortTitle(self, sortTitle, locked=True):
""" Edit the sort title.
2021-01-24 23:18:28 +00:00
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)
2021-01-24 23:18:28 +00:00
class StudioMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a studio. """
2021-01-24 23:18:28 +00:00
def editStudio(self, studio, locked=True):
""" Edit the studio.
2021-01-24 23:18:28 +00:00
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)
2021-01-24 23:18:28 +00:00
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.
2021-01-24 23:18:28 +00:00
"""
return self.editField('summary', summary, locked=locked)
2021-01-24 23:18:28 +00:00
class TaglineMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a tagline. """
2021-01-24 23:18:28 +00:00
def editTagline(self, tagline, locked=True):
""" Edit the tagline.
2021-01-24 23:18:28 +00:00
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)
2021-01-24 23:18:28 +00:00
class TitleMixin(EditFieldMixin):
""" Mixin for Plex objects that can have a title. """
def editTitle(self, title, locked=True):
""" Edit the title.
2021-01-24 23:18:28 +00:00
Parameters:
title (str): The new value.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 23:18:28 +00:00
"""
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)
2021-01-24 23:18:28 +00:00
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)
2021-01-24 22:35:13 +00:00
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 (int): 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<str> 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), [])
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):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have collections. """
def addCollection(self, collections, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a collection tag(s).
Parameters:
collections (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('collection', collections, locked=locked)
2021-01-24 22:35:13 +00:00
def removeCollection(self, collections, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a collection tag(s).
Parameters:
collections (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('collection', collections, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class CountryMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have countries. """
def addCountry(self, countries, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a country tag(s).
Parameters:
countries (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('country', countries, locked=locked)
2021-01-24 22:35:13 +00:00
def removeCountry(self, countries, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a country tag(s).
Parameters:
countries (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('country', countries, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class DirectorMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have directors. """
def addDirector(self, directors, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a director tag(s).
Parameters:
directors (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('director', directors, locked=locked)
2021-01-24 22:35:13 +00:00
def removeDirector(self, directors, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a director tag(s).
Parameters:
directors (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('director', directors, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class GenreMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have genres. """
def addGenre(self, genres, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a genre tag(s).
Parameters:
genres (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('genre', genres, locked=locked)
2021-01-24 22:35:13 +00:00
def removeGenre(self, genres, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a genre tag(s).
Parameters:
genres (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('genre', genres, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class LabelMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have labels. """
def addLabel(self, labels, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a label tag(s).
Parameters:
labels (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('label', labels, locked=locked)
2021-01-24 22:35:13 +00:00
def removeLabel(self, labels, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a label tag(s).
Parameters:
labels (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('label', labels, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class MoodMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have moods. """
def addMood(self, moods, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a mood tag(s).
Parameters:
moods (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('mood', moods, locked=locked)
2021-01-24 22:35:13 +00:00
def removeMood(self, moods, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a mood tag(s).
Parameters:
moods (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('mood', moods, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class ProducerMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have producers. """
def addProducer(self, producers, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a producer tag(s).
Parameters:
producers (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('producer', producers, locked=locked)
2021-01-24 22:35:13 +00:00
def removeProducer(self, producers, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a producer tag(s).
Parameters:
producers (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('producer', producers, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class SimilarArtistMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have similar artists. """
def addSimilarArtist(self, artists, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a similar artist tag(s).
Parameters:
artists (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('similar', artists, locked=locked)
2021-01-24 22:35:13 +00:00
def removeSimilarArtist(self, artists, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a similar artist tag(s).
Parameters:
artists (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('similar', artists, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class StyleMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have styles. """
def addStyle(self, styles, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a style tag(s).
Parameters:
styles (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('style', styles, locked=locked)
2021-01-24 22:35:13 +00:00
def removeStyle(self, styles, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a style tag(s).
Parameters:
styles (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('style', styles, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class TagMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have tags. """
def addTag(self, tags, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a tag(s).
Parameters:
tags (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('tag', tags, locked=locked)
2021-01-24 22:35:13 +00:00
def removeTag(self, tags, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a tag(s).
Parameters:
tags (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('tag', tags, locked=locked, remove=True)
2021-01-24 22:35:13 +00:00
class WriterMixin(EditTagsMixin):
2021-01-24 22:35:13 +00:00
""" Mixin for Plex objects that can have writers. """
def addWriter(self, writers, locked=True):
2021-01-24 22:35:13 +00:00
""" Add a writer tag(s).
Parameters:
writers (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
return self.editTags('writer', writers, locked=locked)
2021-01-24 22:35:13 +00:00
def removeWriter(self, writers, locked=True):
2021-01-24 22:35:13 +00:00
""" Remove a writer tag(s).
Parameters:
writers (List<str> or List<:class:`~plexapi.media.MediaTag`>): List of tags.
locked (bool): True (default) to lock the field, False to unlock the field.
2021-01-24 22:35:13 +00:00
"""
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, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin,
StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
):
pass
class ShowEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin,
CollectionMixin, GenreMixin, LabelMixin,
):
pass
class SeasonEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, SummaryMixin, TitleMixin, UserRatingMixin,
CollectionMixin, LabelMixin
):
pass
class EpisodeEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin,
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
):
pass
class ArtistEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
):
pass
class AlbumEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin,
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
):
pass
class TrackEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin,
CollectionMixin, 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, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin,
LabelMixin
):
pass