mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 15:56:07 -07:00
Update PlexAPI to 4.6.3
This commit is contained in:
parent
4ec8ef3ab3
commit
3fe77932a0
9 changed files with 265 additions and 70 deletions
|
@ -6,13 +6,13 @@ from plexapi.base import PlexPartialObject
|
|||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
from plexapi.library import LibrarySection
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin
|
||||
from plexapi.mixins import LabelMixin
|
||||
from plexapi.mixins import LabelMixin, SmartFilterMixin
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin):
|
||||
class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin, SmartFilterMixin):
|
||||
""" Represents a single Collection.
|
||||
|
||||
Attributes:
|
||||
|
@ -90,6 +90,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
self.userRating = utils.cast(float, data.attrib.get('userRating'))
|
||||
self._items = None # cache for self.items
|
||||
self._section = None # cache for self.section
|
||||
self._filters = None # cache for self.filters
|
||||
|
||||
def __len__(self): # pragma: no cover
|
||||
return len(self.items())
|
||||
|
@ -141,6 +142,15 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
def children(self):
|
||||
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):
|
||||
""" 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)
|
||||
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):
|
||||
""" Update the filters for a smart collection.
|
||||
|
||||
|
|
|
@ -847,12 +847,6 @@ class LibrarySection(PlexObject):
|
|||
values = [values]
|
||||
|
||||
fieldType = self.getFieldType(filterField.type)
|
||||
choiceTypes = {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'}
|
||||
if fieldType.type in choiceTypes:
|
||||
filterChoices = self.listFilterChoices(filterField.key, libtype)
|
||||
else:
|
||||
filterChoices = []
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
|
@ -865,11 +859,8 @@ class LibrarySection(PlexObject):
|
|||
value = float(value) if '.' in str(value) else int(value)
|
||||
elif fieldType.type == 'string':
|
||||
value = str(value)
|
||||
elif fieldType.type in choiceTypes:
|
||||
value = str((value.id or value.tag) if isinstance(value, media.MediaTag) else value)
|
||||
matchValue = value.lower()
|
||||
value = next((f.key for f in filterChoices
|
||||
if matchValue in {f.key.lower(), f.title.lower()}), value)
|
||||
elif fieldType.type in {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'}:
|
||||
value = self._validateFieldValueTag(value, filterField, libtype)
|
||||
results.append(str(value))
|
||||
except (ValueError, AttributeError):
|
||||
raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s'
|
||||
|
@ -888,24 +879,47 @@ class LibrarySection(PlexObject):
|
|||
else:
|
||||
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):
|
||||
""" 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.
|
||||
"""
|
||||
if isinstance(sort, str):
|
||||
sort = sort.split(',')
|
||||
|
||||
if not isinstance(sort, (list, tuple)):
|
||||
sort = [sort]
|
||||
|
||||
validatedSorts = []
|
||||
for _sort in sort:
|
||||
validatedSorts.append(self._validateSortField(_sort.strip(), libtype))
|
||||
validatedSorts.append(self._validateSortField(_sort, libtype))
|
||||
|
||||
return ','.join(validatedSorts)
|
||||
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
raise BadRequest('Invalid filter sort: %s' % sort)
|
||||
_libtype, sortField, sortDir = match.groups()
|
||||
|
@ -921,16 +935,13 @@ class LibrarySection(PlexObject):
|
|||
|
||||
sortField = libtype + '.' + filterSort.key
|
||||
|
||||
if not sortDir:
|
||||
sortDir = filterSort.defaultDirection
|
||||
|
||||
availableDirections = ['asc', 'desc', 'nullsLast']
|
||||
availableDirections = ['', 'asc', 'desc', 'nullsLast']
|
||||
if sortDir not in availableDirections:
|
||||
raise NotFound('Unknown sort direction "%s". '
|
||||
'Available sort directions: %s'
|
||||
% (sortDir, availableDirections))
|
||||
|
||||
return '%s:%s' % (sortField, sortDir)
|
||||
return '%s:%s' % (sortField, sortDir) if sortDir else sortField
|
||||
|
||||
def _validateAdvancedSearch(self, filters, libtype):
|
||||
""" Validates an advanced search filter dictionary.
|
||||
|
@ -1009,9 +1020,8 @@ class LibrarySection(PlexObject):
|
|||
|
||||
Parameters:
|
||||
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
|
||||
in the format ``column:dir``.
|
||||
See :func:`~plexapi.library.LibrarySection.listSorts` to get a list of available sort fields.
|
||||
sort (:class:`~plexapi.library.FilteringSort` or str or list, optional): A field to sort the results.
|
||||
See the details below for more info.
|
||||
maxresults (int, optional): Only return the specified number of results.
|
||||
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
|
||||
|
@ -1027,6 +1037,28 @@ class LibrarySection(PlexObject):
|
|||
:exc:`~plexapi.exceptions.BadRequest`: When the sort or filter is invalid.
|
||||
: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**
|
||||
|
||||
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.
|
||||
* **year** (*int*): Search for a specific year.
|
||||
|
||||
Tag type filter values can be a :class:`~plexapi.media.MediaTag` object, the exact name
|
||||
:attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*).
|
||||
Tag type filter values can be a :class:`~plexapi.library.FilterChoice` object,
|
||||
: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
|
||||
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'),
|
||||
('id', 'asc', 'Rating Key'),
|
||||
('index', 'asc', '%s Number' % self.type.capitalize()),
|
||||
('random', 'asc', 'Random'),
|
||||
('summary', 'asc', 'Summary'),
|
||||
('tagline', 'asc', 'Tagline'),
|
||||
('updatedAt', 'asc', 'Date Updated')
|
||||
|
@ -2029,7 +2061,11 @@ class FilteringType(PlexObject):
|
|||
additionalSorts.extend([
|
||||
('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([
|
||||
('addedAt', 'asc', 'Date Added')
|
||||
])
|
||||
|
@ -2081,10 +2117,6 @@ class FilteringType(PlexObject):
|
|||
('rating', 'integer', 'Critic Rating'),
|
||||
('viewOffset', 'integer', 'View Offset')
|
||||
])
|
||||
elif self.type == 'artist':
|
||||
additionalFields.extend([
|
||||
('lastViewedAt', 'date', 'Artist Last Played')
|
||||
])
|
||||
elif self.type == 'track':
|
||||
additionalFields.extend([
|
||||
('duration', 'integer', 'Duration'),
|
||||
|
|
|
@ -964,10 +964,12 @@ class Marker(PlexObject):
|
|||
name = self._clean(self.firstAttr('type'))
|
||||
start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
|
||||
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):
|
||||
self._data = data
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.type = data.attrib.get('type')
|
||||
self.start = utils.cast(int, data.attrib.get('startTimeOffset'))
|
||||
self.end = utils.cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- 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.exceptions import BadRequest, NotFound
|
||||
|
@ -559,3 +559,61 @@ class WriterMixin(object):
|
|||
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||
"""
|
||||
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}
|
||||
|
|
|
@ -623,9 +623,7 @@ class MyPlexAccount(PlexObject):
|
|||
}
|
||||
|
||||
url = SyncList.key.format(clientId=client.clientIdentifier)
|
||||
data = self.query(url, method=self._session.post, headers={
|
||||
'Content-type': 'x-www-form-urlencoded',
|
||||
}, params=params)
|
||||
data = self.query(url, method=self._session.post, params=params)
|
||||
|
||||
return SyncItem(self, data, None, clientIdentifier=client.clientIdentifier)
|
||||
|
||||
|
@ -925,6 +923,10 @@ class MyPlexResource(PlexObject):
|
|||
TAG = 'Device'
|
||||
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):
|
||||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
|
@ -949,10 +951,51 @@ class MyPlexResource(PlexObject):
|
|||
self.ownerid = utils.cast(int, data.attrib.get('ownerId', 0))
|
||||
self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username.
|
||||
|
||||
def connect(self, ssl=None, timeout=None):
|
||||
""" Returns a new :class:`~plexapi.server.PlexServer` or :class:`~plexapi.client.PlexClient` object.
|
||||
def preferred_connections(
|
||||
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.
|
||||
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
|
||||
assuming at least one connection was successful, the PlexServer object is built and returned.
|
||||
|
||||
|
@ -965,22 +1008,7 @@ class MyPlexResource(PlexObject):
|
|||
Raises:
|
||||
: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
|
||||
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])
|
||||
connections = self.preferred_connections(ssl, timeout, locations, schemes)
|
||||
# Try connecting to all known resource connections in parellel, but
|
||||
# only return the first server (in order) that provides a response.
|
||||
cls = PlexServer if 'server' in self.provides else PlexClient
|
||||
|
|
|
@ -6,13 +6,13 @@ from plexapi import utils
|
|||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
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.utils import deprecated
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
||||
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMixin):
|
||||
""" Represents a single Playlist.
|
||||
|
||||
Attributes:
|
||||
|
@ -61,6 +61,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self._items = None # cache for self.items
|
||||
self._section = None # cache for self.section
|
||||
self._filters = None # cache for self.filters
|
||||
|
||||
def __len__(self): # pragma: no cover
|
||||
return len(self.items())
|
||||
|
@ -107,6 +108,22 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
""" Returns True if this is a photo playlist. """
|
||||
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):
|
||||
""" 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`. """
|
||||
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):
|
||||
""" Add items to the playlist.
|
||||
|
||||
|
@ -225,7 +235,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
self._server.query(key, method=self._server._session.delete)
|
||||
|
||||
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:
|
||||
items (obj): :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,
|
||||
|
|
|
@ -178,19 +178,19 @@ class MediaSettings(object):
|
|||
photoQuality (int): photo quality on scale 0 to 100.
|
||||
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).
|
||||
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.
|
||||
"""
|
||||
|
||||
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.maxVideoBitrate = plexapi.utils.cast(int, maxVideoBitrate) if maxVideoBitrate != '' else ''
|
||||
self.musicBitrate = plexapi.utils.cast(int, musicBitrate) if musicBitrate != '' else ''
|
||||
self.photoQuality = plexapi.utils.cast(int, photoQuality) if photoQuality != '' else ''
|
||||
self.photoResolution = photoResolution
|
||||
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 ''
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -148,6 +148,7 @@ def searchType(libtype):
|
|||
Parameters:
|
||||
libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track,
|
||||
collection)
|
||||
|
||||
Raises:
|
||||
:exc:`~plexapi.exceptions.NotFound`: Unknown libtype
|
||||
"""
|
||||
|
@ -159,6 +160,24 @@ def searchType(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):
|
||||
""" Returns the result of <callback> for each set of `*args` in listargs. Each call
|
||||
to <callback> is called concurrently in their own separate threads.
|
||||
|
|
|
@ -767,7 +767,9 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
|||
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||
parentTitle (str): Name of the season for the episode.
|
||||
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).
|
||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||
skipParent (bool): True if the show's seasons are set to hidden.
|
||||
viewOffset (int): View offset in milliseconds.
|
||||
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.parentTitle = data.attrib.get('parentTitle')
|
||||
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.roles = self.findItems(data, media.Role)
|
||||
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
|
@ -838,6 +842,11 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
|||
""" Returns a human friendly filename. """
|
||||
return '%s.%s' % (self.grandparentTitle.replace(' ', '.'), self.seasonEpisode)
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
""" Alias to self.roles. """
|
||||
return self.roles
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
""" 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. """
|
||||
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
|
||||
def hasIntroMarker(self):
|
||||
""" Returns True if the episode has an intro marker in the xml. """
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue