Update PlexAPI to 4.6.3

This commit is contained in:
JonnyWong16 2021-08-15 15:23:53 -07:00
parent 4ec8ef3ab3
commit 3fe77932a0
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
9 changed files with 265 additions and 70 deletions

View file

@ -6,13 +6,13 @@ from plexapi.base import PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin
from plexapi.mixins import LabelMixin from plexapi.mixins import LabelMixin, SmartFilterMixin
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import deprecated from plexapi.utils import deprecated
@utils.registerPlexObject @utils.registerPlexObject
class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin): class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin, SmartFilterMixin):
""" Represents a single Collection. """ Represents a single Collection.
Attributes: Attributes:
@ -90,6 +90,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
self.userRating = utils.cast(float, data.attrib.get('userRating')) self.userRating = utils.cast(float, data.attrib.get('userRating'))
self._items = None # cache for self.items self._items = None # cache for self.items
self._section = None # cache for self.section self._section = None # cache for self.section
self._filters = None # cache for self.filters
def __len__(self): # pragma: no cover def __len__(self): # pragma: no cover
return len(self.items()) return len(self.items())
@ -141,6 +142,15 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
def children(self): def children(self):
return self.items() return self.items()
def filters(self):
""" Returns the search filter dict for smart collection.
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
to get the list of items.
"""
if self.smart and self._filters is None:
self._filters = self._parseFilters(self.content)
return self._filters
def section(self): def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this collection belongs to. """ Returns the :class:`~plexapi.library.LibrarySection` this collection belongs to.
""" """
@ -277,6 +287,28 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
key = '%s/items/%s' % (self.key, item.ratingKey) key = '%s/items/%s' % (self.key, item.ratingKey)
self._server.query(key, method=self._server._session.delete) self._server.query(key, method=self._server._session.delete)
def moveItem(self, item, after=None):
""" Move an item to a new position in the collection.
Parameters:
items (obj): :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,
or :class:`~plexapi.photo.Photo` objects to be moved in the collection.
after (obj): :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,
or :class:`~plexapi.photo.Photo` objects to move the item after in the collection.
Raises:
:class:`plexapi.exceptions.BadRequest`: When trying to move items in a smart collection.
"""
if self.smart:
raise BadRequest('Cannot move items in a smart collection.')
key = '%s/items/%s/move' % (self.key, item.ratingKey)
if after:
key += '?after=%s' % after.ratingKey
self._server.query(key, method=self._server._session.put)
def updateFilters(self, libtype=None, limit=None, sort=None, filters=None, **kwargs): def updateFilters(self, libtype=None, limit=None, sort=None, filters=None, **kwargs):
""" Update the filters for a smart collection. """ Update the filters for a smart collection.

View file

@ -847,12 +847,6 @@ class LibrarySection(PlexObject):
values = [values] values = [values]
fieldType = self.getFieldType(filterField.type) fieldType = self.getFieldType(filterField.type)
choiceTypes = {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'}
if fieldType.type in choiceTypes:
filterChoices = self.listFilterChoices(filterField.key, libtype)
else:
filterChoices = []
results = [] results = []
try: try:
@ -865,11 +859,8 @@ class LibrarySection(PlexObject):
value = float(value) if '.' in str(value) else int(value) value = float(value) if '.' in str(value) else int(value)
elif fieldType.type == 'string': elif fieldType.type == 'string':
value = str(value) value = str(value)
elif fieldType.type in choiceTypes: elif fieldType.type in {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'}:
value = str((value.id or value.tag) if isinstance(value, media.MediaTag) else value) value = self._validateFieldValueTag(value, filterField, libtype)
matchValue = value.lower()
value = next((f.key for f in filterChoices
if matchValue in {f.key.lower(), f.title.lower()}), value)
results.append(str(value)) results.append(str(value))
except (ValueError, AttributeError): except (ValueError, AttributeError):
raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s' raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s'
@ -888,24 +879,47 @@ class LibrarySection(PlexObject):
else: else:
return int(utils.toDatetime(value, '%Y-%m-%d').timestamp()) return int(utils.toDatetime(value, '%Y-%m-%d').timestamp())
def _validateFieldValueTag(self, value, filterField, libtype):
""" Validates a filter tag value. A filter tag value can be a :class:`~plexapi.library.FilterChoice` object,
a :class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*),
or the exact id :attr:`MediaTag.id` (*int*).
"""
if isinstance(value, FilterChoice):
return value.key
if isinstance(value, media.MediaTag):
value = str(value.id or value.tag)
else:
value = str(value)
filterChoices = self.listFilterChoices(filterField.key, libtype)
matchValue = value.lower()
return next((f.key for f in filterChoices if matchValue in {f.key.lower(), f.title.lower()}), value)
def _validateSortFields(self, sort, libtype=None): def _validateSortFields(self, sort, libtype=None):
""" Validates a list of filter sort fields is available for the library. """ Validates a list of filter sort fields is available for the library. Sort fields can be a
list of :class:`~plexapi.library.FilteringSort` objects, or a comma separated string.
Returns the validated comma separated sort fields string. Returns the validated comma separated sort fields string.
""" """
if isinstance(sort, str): if isinstance(sort, str):
sort = sort.split(',') sort = sort.split(',')
if not isinstance(sort, (list, tuple)):
sort = [sort]
validatedSorts = [] validatedSorts = []
for _sort in sort: for _sort in sort:
validatedSorts.append(self._validateSortField(_sort.strip(), libtype)) validatedSorts.append(self._validateSortField(_sort, libtype))
return ','.join(validatedSorts) return ','.join(validatedSorts)
def _validateSortField(self, sort, libtype=None): def _validateSortField(self, sort, libtype=None):
""" Validates a filter sort field is available for the library. """ Validates a filter sort field is available for the library. A sort field can be a
:class:`~plexapi.library.FilteringSort` object, or a string.
Returns the validated sort field string. Returns the validated sort field string.
""" """
match = re.match(r'(?:([a-zA-Z]*)\.)?([a-zA-Z]+):?([a-zA-Z]*)', sort) if isinstance(sort, FilteringSort):
return '%s.%s:%s' % (libtype or self.TYPE, sort.key, sort.defaultDirection)
match = re.match(r'(?:([a-zA-Z]*)\.)?([a-zA-Z]+):?([a-zA-Z]*)', sort.strip())
if not match: if not match:
raise BadRequest('Invalid filter sort: %s' % sort) raise BadRequest('Invalid filter sort: %s' % sort)
_libtype, sortField, sortDir = match.groups() _libtype, sortField, sortDir = match.groups()
@ -921,16 +935,13 @@ class LibrarySection(PlexObject):
sortField = libtype + '.' + filterSort.key sortField = libtype + '.' + filterSort.key
if not sortDir: availableDirections = ['', 'asc', 'desc', 'nullsLast']
sortDir = filterSort.defaultDirection
availableDirections = ['asc', 'desc', 'nullsLast']
if sortDir not in availableDirections: if sortDir not in availableDirections:
raise NotFound('Unknown sort direction "%s". ' raise NotFound('Unknown sort direction "%s". '
'Available sort directions: %s' 'Available sort directions: %s'
% (sortDir, availableDirections)) % (sortDir, availableDirections))
return '%s:%s' % (sortField, sortDir) return '%s:%s' % (sortField, sortDir) if sortDir else sortField
def _validateAdvancedSearch(self, filters, libtype): def _validateAdvancedSearch(self, filters, libtype):
""" Validates an advanced search filter dictionary. """ Validates an advanced search filter dictionary.
@ -1009,9 +1020,8 @@ class LibrarySection(PlexObject):
Parameters: Parameters:
title (str, optional): General string query to search for. Partial string matches are allowed. title (str, optional): General string query to search for. Partial string matches are allowed.
sort (str or list, optional): A string of comma separated sort fields or a list of sort fields sort (:class:`~plexapi.library.FilteringSort` or str or list, optional): A field to sort the results.
in the format ``column:dir``. See the details below for more info.
See :func:`~plexapi.library.LibrarySection.listSorts` to get a list of available sort fields.
maxresults (int, optional): Only return the specified number of results. maxresults (int, optional): Only return the specified number of results.
libtype (str, optional): Return results of a specific type (movie, show, season, episode, libtype (str, optional): Return results of a specific type (movie, show, season, episode,
artist, album, track, photoalbum, photo, collection) (e.g. ``libtype='episode'`` will only artist, album, track, photoalbum, photo, collection) (e.g. ``libtype='episode'`` will only
@ -1027,6 +1037,28 @@ class LibrarySection(PlexObject):
:exc:`~plexapi.exceptions.BadRequest`: When the sort or filter is invalid. :exc:`~plexapi.exceptions.BadRequest`: When the sort or filter is invalid.
:exc:`~plexapi.exceptions.NotFound`: When applying an unknown sort or filter. :exc:`~plexapi.exceptions.NotFound`: When applying an unknown sort or filter.
**Sorting Results**
The search results can be sorted by including the ``sort`` parameter.
* See :func:`~plexapi.library.LibrarySection.listSorts` to get a list of available sort fields.
The ``sort`` parameter can be a :class:`~plexapi.library.FilteringSort` object or a sort string in the
format ``field:dir``. The sort direction ``dir`` can be ``asc``, ``desc``, or ``nullsLast``. Omitting the
sort direction or using a :class:`~plexapi.library.FilteringSort` object will sort the results in the default
direction of the field. Multi-sorting on multiple fields can be achieved by using a comma separated list of
sort strings, or a list of :class:`~plexapi.library.FilteringSort` object or strings.
Examples:
.. code-block:: python
library.search(sort="titleSort:desc") # Sort title in descending order
library.search(sort="titleSort") # Sort title in the default order
# Multi-sort by year in descending order, then by audience rating in descending order
library.search(sort="year:desc,audienceRating:desc")
library.search(sort=["year:desc", "audienceRating:desc"])
**Using Plex Filters** **Using Plex Filters**
Any of the available custom filters can be applied to the search results Any of the available custom filters can be applied to the search results
@ -1065,8 +1097,9 @@ class LibrarySection(PlexObject):
* **writer** (:class:`~plexapi.media.MediaTag`): Search for the name of a writer. * **writer** (:class:`~plexapi.media.MediaTag`): Search for the name of a writer.
* **year** (*int*): Search for a specific year. * **year** (*int*): Search for a specific year.
Tag type filter values can be a :class:`~plexapi.media.MediaTag` object, the exact name Tag type filter values can be a :class:`~plexapi.library.FilterChoice` object,
:attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*). :class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*),
or the exact id :attr:`MediaTag.id` (*int*).
Date type filter values can be a ``datetime`` object, a relative date using a one of the Date type filter values can be a ``datetime`` object, a relative date using a one of the
available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format. available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format.
@ -2014,7 +2047,6 @@ class FilteringType(PlexObject):
('guid', 'asc', 'Guid'), ('guid', 'asc', 'Guid'),
('id', 'asc', 'Rating Key'), ('id', 'asc', 'Rating Key'),
('index', 'asc', '%s Number' % self.type.capitalize()), ('index', 'asc', '%s Number' % self.type.capitalize()),
('random', 'asc', 'Random'),
('summary', 'asc', 'Summary'), ('summary', 'asc', 'Summary'),
('tagline', 'asc', 'Tagline'), ('tagline', 'asc', 'Tagline'),
('updatedAt', 'asc', 'Date Updated') ('updatedAt', 'asc', 'Date Updated')
@ -2029,7 +2061,11 @@ class FilteringType(PlexObject):
additionalSorts.extend([ additionalSorts.extend([
('absoluteIndex', 'asc', 'Absolute Index') ('absoluteIndex', 'asc', 'Absolute Index')
]) ])
if self.type == 'collection': elif self.type == 'photo':
additionalSorts.extend([
('viewUpdatedAt', 'desc', 'View Updated At')
])
elif self.type == 'collection':
additionalSorts.extend([ additionalSorts.extend([
('addedAt', 'asc', 'Date Added') ('addedAt', 'asc', 'Date Added')
]) ])
@ -2081,10 +2117,6 @@ class FilteringType(PlexObject):
('rating', 'integer', 'Critic Rating'), ('rating', 'integer', 'Critic Rating'),
('viewOffset', 'integer', 'View Offset') ('viewOffset', 'integer', 'View Offset')
]) ])
elif self.type == 'artist':
additionalFields.extend([
('lastViewedAt', 'date', 'Artist Last Played')
])
elif self.type == 'track': elif self.type == 'track':
additionalFields.extend([ additionalFields.extend([
('duration', 'integer', 'Duration'), ('duration', 'integer', 'Duration'),

View file

@ -964,10 +964,12 @@ class Marker(PlexObject):
name = self._clean(self.firstAttr('type')) name = self._clean(self.firstAttr('type'))
start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start'))) start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end'))) end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end')))
return '<%s:%s %s - %s>' % (self.__class__.__name__, name, start, end) offsets = '%s-%s' % (start, end)
return '<%s>' % ':'.join([self.__class__.__name__, name, offsets])
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.id = utils.cast(int, data.attrib.get('id'))
self.type = data.attrib.get('type') self.type = data.attrib.get('type')
self.start = utils.cast(int, data.attrib.get('startTimeOffset')) self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
self.end = utils.cast(int, data.attrib.get('endTimeOffset')) self.end = utils.cast(int, data.attrib.get('endTimeOffset'))

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import quote_plus, urlencode from urllib.parse import parse_qsl, quote_plus, unquote, urlencode, urlsplit
from plexapi import media, settings, utils from plexapi import media, settings, utils
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
@ -559,3 +559,61 @@ class WriterMixin(object):
locked (bool): True (default) to lock the field, False to unlock the field. locked (bool): True (default) to lock the field, False to unlock the field.
""" """
self._edit_tags('writer', writers, locked=locked, remove=True) self._edit_tags('writer', writers, locked=locked, remove=True)
class SmartFilterMixin(object):
""" Mixing for Plex objects that can have smart filters. """
def _parseFilters(self, content):
""" Parse the content string and returns the filter dict. """
content = urlsplit(unquote(content))
filters = {}
filterOp = 'and'
filterGroups = [[]]
for key, value in parse_qsl(content.query):
# Move = sign to key when operator is ==
if value.startswith('='):
key += '='
value = value[1:]
if key == 'type':
filters['libtype'] = utils.reverseSearchType(value)
elif key == 'sort':
filters['sort'] = value.split(',')
elif key == 'limit':
filters['limit'] = int(value)
elif key == 'push':
filterGroups[-1].append([])
filterGroups.append(filterGroups[-1][-1])
elif key == 'and':
filterOp = 'and'
elif key == 'or':
filterOp = 'or'
elif key == 'pop':
filterGroups[-1].insert(0, filterOp)
filterGroups.pop()
else:
filterGroups[-1].append({key: value})
if filterGroups:
filters['filters'] = self._formatFilterGroups(filterGroups.pop())
return filters
def _formatFilterGroups(self, groups):
""" Formats the filter groups into the advanced search rules. """
if len(groups) == 1 and isinstance(groups[0], list):
groups = groups.pop()
filterOp = 'and'
rules = []
for g in groups:
if isinstance(g, list):
rules.append(self._formatFilterGroups(g))
elif isinstance(g, dict):
rules.append(g)
elif g in {'and', 'or'}:
filterOp = g
return {filterOp: rules}

View file

@ -623,9 +623,7 @@ class MyPlexAccount(PlexObject):
} }
url = SyncList.key.format(clientId=client.clientIdentifier) url = SyncList.key.format(clientId=client.clientIdentifier)
data = self.query(url, method=self._session.post, headers={ data = self.query(url, method=self._session.post, params=params)
'Content-type': 'x-www-form-urlencoded',
}, params=params)
return SyncItem(self, data, None, clientIdentifier=client.clientIdentifier) return SyncItem(self, data, None, clientIdentifier=client.clientIdentifier)
@ -925,6 +923,10 @@ class MyPlexResource(PlexObject):
TAG = 'Device' TAG = 'Device'
key = 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1' key = 'https://plex.tv/api/resources?includeHttps=1&includeRelay=1'
# Default order to prioritize available resource connections
DEFAULT_LOCATION_ORDER = ['local', 'remote', 'relay']
DEFAULT_SCHEME_ORDER = ['https', 'http']
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.name = data.attrib.get('name') self.name = data.attrib.get('name')
@ -949,10 +951,51 @@ class MyPlexResource(PlexObject):
self.ownerid = utils.cast(int, data.attrib.get('ownerId', 0)) self.ownerid = utils.cast(int, data.attrib.get('ownerId', 0))
self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username. self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username.
def connect(self, ssl=None, timeout=None): def preferred_connections(
""" Returns a new :class:`~plexapi.server.PlexServer` or :class:`~plexapi.client.PlexClient` object. self,
ssl=None,
timeout=None,
locations=DEFAULT_LOCATION_ORDER,
schemes=DEFAULT_SCHEME_ORDER,
):
""" Returns a sorted list of the available connection addresses for this resource.
Often times there is more than one address specified for a server or client. Often times there is more than one address specified for a server or client.
This function will prioritize local connections before remote or relay and HTTPS before HTTP. Default behavior will prioritize local connections before remote or relay and HTTPS before HTTP.
Parameters:
ssl (bool, optional): Set True to only connect to HTTPS connections. Set False to
only connect to HTTP connections. Set None (default) to connect to any
HTTP or HTTPS connection.
timeout (int, optional): The timeout in seconds to attempt each connection.
"""
connections_dict = {location: {scheme: [] for scheme in schemes} for location in locations}
for connection in self.connections:
# Only check non-local connections unless we own the resource
if self.owned or (not self.owned and not connection.local):
location = 'relay' if connection.relay else ('local' if connection.local else 'remote')
if location not in locations:
continue
if 'http' in schemes:
connections_dict[location]['http'].append(connection.httpuri)
if 'https' in schemes:
connections_dict[location]['https'].append(connection.uri)
if ssl is True: schemes.remove('http')
elif ssl is False: schemes.remove('https')
connections = []
for location in locations:
for scheme in schemes:
connections.extend(connections_dict[location][scheme])
return connections
def connect(
self,
ssl=None,
timeout=None,
locations=DEFAULT_LOCATION_ORDER,
schemes=DEFAULT_SCHEME_ORDER,
):
""" Returns a new :class:`~plexapi.server.PlexServer` or :class:`~plexapi.client.PlexClient` object.
Uses `MyPlexResource.preferred_connections()` to generate the priority order of connection addresses.
After trying to connect to all available addresses for this resource and After trying to connect to all available addresses for this resource and
assuming at least one connection was successful, the PlexServer object is built and returned. assuming at least one connection was successful, the PlexServer object is built and returned.
@ -965,22 +1008,7 @@ class MyPlexResource(PlexObject):
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. :exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
""" """
# Keys in the order we want the connections to be sorted connections = self.preferred_connections(ssl, timeout, locations, schemes)
locations = ['local', 'remote', 'relay']
schemes = ['https', 'http']
connections_dict = {location: {scheme: [] for scheme in schemes} for location in locations}
for connection in self.connections:
# Only check non-local connections unless we own the resource
if self.owned or (not self.owned and not connection.local):
location = 'relay' if connection.relay else ('local' if connection.local else 'remote')
connections_dict[location]['http'].append(connection.httpuri)
connections_dict[location]['https'].append(connection.uri)
if ssl is True: schemes.remove('http')
elif ssl is False: schemes.remove('https')
connections = []
for location in locations:
for scheme in schemes:
connections.extend(connections_dict[location][scheme])
# Try connecting to all known resource connections in parellel, but # Try connecting to all known resource connections in parellel, but
# only return the first server (in order) that provides a response. # only return the first server (in order) that provides a response.
cls = PlexServer if 'server' in self.provides else PlexClient cls = PlexServer if 'server' in self.provides else PlexClient

View file

@ -6,13 +6,13 @@ from plexapi import utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.mixins import ArtMixin, PosterMixin from plexapi.mixins import ArtMixin, PosterMixin, SmartFilterMixin
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import deprecated from plexapi.utils import deprecated
@utils.registerPlexObject @utils.registerPlexObject
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin): class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMixin):
""" Represents a single Playlist. """ Represents a single Playlist.
Attributes: Attributes:
@ -61,6 +61,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self._items = None # cache for self.items self._items = None # cache for self.items
self._section = None # cache for self.section self._section = None # cache for self.section
self._filters = None # cache for self.filters
def __len__(self): # pragma: no cover def __len__(self): # pragma: no cover
return len(self.items()) return len(self.items())
@ -107,6 +108,22 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
""" Returns True if this is a photo playlist. """ """ Returns True if this is a photo playlist. """
return self.playlistType == 'photo' return self.playlistType == 'photo'
def _getPlaylistItemID(self, item):
""" Match an item to a playlist item and return the item playlistItemID. """
for _item in self.items():
if _item.ratingKey == item.ratingKey:
return _item.playlistItemID
raise NotFound('Item with title "%s" not found in the playlist' % item.title)
def filters(self):
""" Returns the search filter dict for smart playlist.
The filter dict be passed back into :func:`~plexapi.library.LibrarySection.search`
to get the list of items.
"""
if self.smart and self._filters is None:
self._filters = self._parseFilters(self.content)
return self._filters
def section(self): def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` this smart playlist belongs to. """ Returns the :class:`~plexapi.library.LibrarySection` this smart playlist belongs to.
@ -160,13 +177,6 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
""" Alias to :func:`~plexapi.playlist.Playlist.item`. """ """ Alias to :func:`~plexapi.playlist.Playlist.item`. """
return self.item(title) return self.item(title)
def _getPlaylistItemID(self, item):
""" Match an item to a playlist item and return the item playlistItemID. """
for _item in self.items():
if _item.ratingKey == item.ratingKey:
return _item.playlistItemID
raise NotFound('Item with title "%s" not found in the playlist' % item.title)
def addItems(self, items): def addItems(self, items):
""" Add items to the playlist. """ Add items to the playlist.
@ -225,7 +235,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
self._server.query(key, method=self._server._session.delete) self._server.query(key, method=self._server._session.delete)
def moveItem(self, item, after=None): def moveItem(self, item, after=None):
""" Move an item to a new position in playlist. """ Move an item to a new position in the playlist.
Parameters: Parameters:
items (obj): :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`, items (obj): :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,

View file

@ -178,19 +178,19 @@ class MediaSettings(object):
photoQuality (int): photo quality on scale 0 to 100. photoQuality (int): photo quality on scale 0 to 100.
photoResolution (str): maximum photo resolution, formatted as WxH (e.g. `1920x1080`). photoResolution (str): maximum photo resolution, formatted as WxH (e.g. `1920x1080`).
videoResolution (str): maximum video resolution, formatted as WxH (e.g. `1280x720`, may be empty). videoResolution (str): maximum video resolution, formatted as WxH (e.g. `1280x720`, may be empty).
subtitleSize (int|str): unknown, usually equals to 0, may be empty string. subtitleSize (int): subtitle size on scale 0 to 100.
videoQuality (int): video quality on scale 0 to 100. videoQuality (int): video quality on scale 0 to 100.
""" """
def __init__(self, maxVideoBitrate=4000, videoQuality=100, videoResolution='1280x720', audioBoost=100, def __init__(self, maxVideoBitrate=4000, videoQuality=100, videoResolution='1280x720', audioBoost=100,
musicBitrate=192, photoQuality=74, photoResolution='1920x1080', subtitleSize=''): musicBitrate=192, photoQuality=74, photoResolution='1920x1080', subtitleSize=100):
self.audioBoost = plexapi.utils.cast(int, audioBoost) self.audioBoost = plexapi.utils.cast(int, audioBoost)
self.maxVideoBitrate = plexapi.utils.cast(int, maxVideoBitrate) if maxVideoBitrate != '' else '' self.maxVideoBitrate = plexapi.utils.cast(int, maxVideoBitrate) if maxVideoBitrate != '' else ''
self.musicBitrate = plexapi.utils.cast(int, musicBitrate) if musicBitrate != '' else '' self.musicBitrate = plexapi.utils.cast(int, musicBitrate) if musicBitrate != '' else ''
self.photoQuality = plexapi.utils.cast(int, photoQuality) if photoQuality != '' else '' self.photoQuality = plexapi.utils.cast(int, photoQuality) if photoQuality != '' else ''
self.photoResolution = photoResolution self.photoResolution = photoResolution
self.videoResolution = videoResolution self.videoResolution = videoResolution
self.subtitleSize = subtitleSize self.subtitleSize = plexapi.utils.cast(int, subtitleSize) if subtitleSize != '' else ''
self.videoQuality = plexapi.utils.cast(int, videoQuality) if videoQuality != '' else '' self.videoQuality = plexapi.utils.cast(int, videoQuality) if videoQuality != '' else ''
@staticmethod @staticmethod

View file

@ -148,6 +148,7 @@ def searchType(libtype):
Parameters: Parameters:
libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track, libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track,
collection) collection)
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype :exc:`~plexapi.exceptions.NotFound`: Unknown libtype
""" """
@ -159,6 +160,24 @@ def searchType(libtype):
raise NotFound('Unknown libtype: %s' % libtype) raise NotFound('Unknown libtype: %s' % libtype)
def reverseSearchType(libtype):
""" Returns the string value of the library type.
Parameters:
libtype (int): Integer value of the library type.
Raises:
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype
"""
if libtype in SEARCHTYPES:
return libtype
libtype = int(libtype)
for k, v in SEARCHTYPES.items():
if libtype == v:
return k
raise NotFound('Unknown libtype: %s' % libtype)
def threaded(callback, listargs): def threaded(callback, listargs):
""" Returns the result of <callback> for each set of `*args` in listargs. Each call """ Returns the result of <callback> for each set of `*args` in listargs. Each call
to <callback> is called concurrently in their own separate threads. to <callback> is called concurrently in their own separate threads.

View file

@ -767,7 +767,9 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>). parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the season for the episode. parentTitle (str): Name of the season for the episode.
parentYear (int): Year the season was released. parentYear (int): Year the season was released.
producers (List<:class:`~plexapi.media.Producer`>): List of producers objects.
rating (float): Episode rating (7.9; 9.8; 8.1). rating (float): Episode rating (7.9; 9.8; 8.1).
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
skipParent (bool): True if the show's seasons are set to hidden. skipParent (bool): True if the show's seasons are set to hidden.
viewOffset (int): View offset in milliseconds. viewOffset (int): View offset in milliseconds.
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
@ -809,7 +811,9 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
self.parentThumb = data.attrib.get('parentThumb') self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle') self.parentTitle = data.attrib.get('parentTitle')
self.parentYear = utils.cast(int, data.attrib.get('parentYear')) self.parentYear = utils.cast(int, data.attrib.get('parentYear'))
self.producers = self.findItems(data, media.Producer)
self.rating = utils.cast(float, data.attrib.get('rating')) self.rating = utils.cast(float, data.attrib.get('rating'))
self.roles = self.findItems(data, media.Role)
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0')) self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.writers = self.findItems(data, media.Writer) self.writers = self.findItems(data, media.Writer)
@ -838,6 +842,11 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
""" Returns a human friendly filename. """ """ Returns a human friendly filename. """
return '%s.%s' % (self.grandparentTitle.replace(' ', '.'), self.seasonEpisode) return '%s.%s' % (self.grandparentTitle.replace(' ', '.'), self.seasonEpisode)
@property
def actors(self):
""" Alias to self.roles. """
return self.roles
@property @property
def locations(self): def locations(self):
""" This does not exist in plex xml response but is added to have a common """ This does not exist in plex xml response but is added to have a common
@ -865,6 +874,11 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
""" Returns the s00e00 string containing the season and episode numbers. """ """ Returns the s00e00 string containing the season and episode numbers. """
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.episodeNumber).zfill(2)) return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.episodeNumber).zfill(2))
@property
def hasCommercialMarker(self):
""" Returns True if the episode has a commercial marker in the xml. """
return any(marker.type == 'commercial' for marker in self.markers)
@property @property
def hasIntroMarker(self): def hasIntroMarker(self):
""" Returns True if the episode has an intro marker in the xml. """ """ Returns True if the episode has an intro marker in the xml. """