mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-21 19:23:05 +00:00
Add LibrarySection
methods to multi-edit items (#1184)
* Use generator for string join * Group media type edit mixins * Add UserRatingMixin * Add LibrarySection methods to multi-edit items * Remove deprecated banners * Factor out resource lock/unlock mixins * Update `fetchItems` to accept list of rating keys * Add repr and helper methods to Common object * Update tests
This commit is contained in:
parent
b06afb8f84
commit
9ede493f4c
17 changed files with 468 additions and 168 deletions
|
@ -8,14 +8,12 @@ from plexapi.exceptions import BadRequest, NotFound
|
|||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
|
||||
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
|
||||
AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
|
||||
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
|
||||
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||
ArtistEditMixins, AlbumEditMixins, TrackEditMixins
|
||||
)
|
||||
from plexapi.playlist import Playlist
|
||||
|
||||
|
||||
class Audio(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin):
|
||||
class Audio(PlexPartialObject, PlayedUnplayedMixin):
|
||||
""" Base class for all audio objects including :class:`~plexapi.audio.Artist`,
|
||||
:class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`.
|
||||
|
||||
|
@ -132,8 +130,7 @@ class Artist(
|
|||
Audio,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||
ArtistEditMixins
|
||||
):
|
||||
""" Represents a single Artist.
|
||||
|
||||
|
@ -244,8 +241,7 @@ class Album(
|
|||
Audio,
|
||||
UnmatchMatchMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
|
||||
AlbumEditMixins
|
||||
):
|
||||
""" Represents a single Album.
|
||||
|
||||
|
@ -364,8 +360,7 @@ class Track(
|
|||
Audio, Playable,
|
||||
ExtrasMixin, RatingMixin,
|
||||
ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin,
|
||||
TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin,
|
||||
CollectionMixin, LabelMixin, MoodMixin
|
||||
TrackEditMixins
|
||||
):
|
||||
""" Represents a single Track.
|
||||
|
||||
|
|
|
@ -152,7 +152,9 @@ class PlexObject:
|
|||
and attrs.
|
||||
|
||||
Parameters:
|
||||
ekey (str): API URL path in Plex to fetch items from.
|
||||
ekey (str or List<int>): API URL path in Plex to fetch items from. If a list of ints is passed
|
||||
in, the key will be translated to /library/metadata/<key1,key2,key3>. This allows
|
||||
fetching multiple items only knowing their key-ids.
|
||||
cls (:class:`~plexapi.base.PlexObject`): If you know the class of the
|
||||
items to be fetched, passing this in will help the parser ensure
|
||||
it only returns those items. By default we convert the xml elements
|
||||
|
@ -225,6 +227,9 @@ class PlexObject:
|
|||
if ekey is None:
|
||||
raise BadRequest('ekey was not provided')
|
||||
|
||||
if isinstance(ekey, list) and all(isinstance(key, int) for key in ekey):
|
||||
ekey = f'/library/metadata/{",".join(str(key) for key in ekey)}'
|
||||
|
||||
container_start = container_start or 0
|
||||
container_size = container_size or X_PLEX_CONTAINER_SIZE
|
||||
offset = container_start
|
||||
|
@ -559,13 +564,10 @@ class PlexPartialObject(PlexObject):
|
|||
self._edits.update(kwargs)
|
||||
return self
|
||||
|
||||
if 'id' not in kwargs:
|
||||
kwargs['id'] = self.ratingKey
|
||||
if 'type' not in kwargs:
|
||||
kwargs['type'] = utils.searchType(self._searchType)
|
||||
|
||||
part = f'/library/sections/{self.librarySectionID}/all{utils.joinArgs(kwargs)}'
|
||||
self._server.query(part, method=self._server._session.put)
|
||||
self.section()._edit(items=self, **kwargs)
|
||||
return self
|
||||
|
||||
def edit(self, **kwargs):
|
||||
|
|
|
@ -8,8 +8,7 @@ from plexapi.library import LibrarySection, ManagedHub
|
|||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
LabelMixin
|
||||
CollectionEditMixins
|
||||
)
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
@ -19,8 +18,7 @@ class Collection(
|
|||
PlexPartialObject,
|
||||
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
LabelMixin
|
||||
CollectionEditMixins
|
||||
):
|
||||
""" Represents a single Collection.
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse
|
||||
|
||||
from plexapi import log, media, utils
|
||||
from plexapi.base import OPERATORS, PlexObject
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.mixins import (
|
||||
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
|
||||
ArtistEditMixins, AlbumEditMixins, TrackEditMixins, PhotoalbumEditMixins, PhotoEditMixins
|
||||
)
|
||||
from plexapi.settings import Setting
|
||||
from plexapi.utils import cached_property, deprecated
|
||||
|
||||
|
@ -440,6 +444,20 @@ class LibrarySection(PlexObject):
|
|||
self._getTotalDurationStorage()
|
||||
return self._totalStorage
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
# Intercept to call EditFieldMixin and EditTagMixin methods
|
||||
# based on the item type being batch multi-edited
|
||||
value = super().__getattribute__(attr)
|
||||
if attr.startswith('_'): return value
|
||||
if callable(value) and 'Mixin' in value.__qualname__:
|
||||
if not isinstance(self._edits, dict):
|
||||
raise AttributeError("Must enable batchMultiEdit() to use this method")
|
||||
elif not hasattr(self._edits['items'][0], attr):
|
||||
raise AttributeError(
|
||||
f"Batch multi-editing '{self._edits['items'][0].__class__.__name__}' object has no attribute '{attr}'"
|
||||
)
|
||||
return value
|
||||
|
||||
def _getTotalDurationStorage(self):
|
||||
""" Queries the Plex server for the total library duration and storage and caches the values. """
|
||||
data = self._server.query('/media/providers?includeStorage=1')
|
||||
|
@ -1658,8 +1676,101 @@ class LibrarySection(PlexObject):
|
|||
params['pageType'] = 'list'
|
||||
return self._server._buildWebURL(base=base, **params)
|
||||
|
||||
def _validateItems(self, items):
|
||||
""" Validates the specified items are from this library and of the same type. """
|
||||
if not items:
|
||||
raise BadRequest('No items specified.')
|
||||
|
||||
class MovieSection(LibrarySection):
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
|
||||
itemType = items[0].type
|
||||
for item in items:
|
||||
if item.librarySectionID != self.key:
|
||||
raise BadRequest(f'{item.title} is not from this library.')
|
||||
elif item.type != itemType:
|
||||
raise BadRequest(f'Cannot mix items of different type: {itemType} and {item.type}')
|
||||
|
||||
return items
|
||||
|
||||
def common(self, items):
|
||||
""" Returns a :class:`~plexapi.library.Common` object for the specified items. """
|
||||
params = {
|
||||
'id': ','.join(str(item.ratingKey) for item in self._validateItems(items)),
|
||||
'type': utils.searchType(items[0].type)
|
||||
}
|
||||
part = f'/library/sections/{self.key}/common{utils.joinArgs(params)}'
|
||||
return self.fetchItem(part, cls=Common)
|
||||
|
||||
def _edit(self, items=None, **kwargs):
|
||||
""" Actually edit multiple objects. """
|
||||
if isinstance(self._edits, dict):
|
||||
self._edits.update(kwargs)
|
||||
return self
|
||||
|
||||
kwargs['id'] = ','.join(str(item.ratingKey) for item in self._validateItems(items))
|
||||
if 'type' not in kwargs:
|
||||
kwargs['type'] = utils.searchType(items[0].type)
|
||||
|
||||
part = f'/library/sections/{self.key}/all{utils.joinArgs(kwargs)}'
|
||||
self._server.query(part, method=self._server._session.put)
|
||||
return self
|
||||
|
||||
def multiEdit(self, items, **kwargs):
|
||||
""" Edit multiple objects at once.
|
||||
Note: This is a low level method and you need to know all the field/tag keys.
|
||||
See :class:`~plexapi.LibrarySection.batchMultiEdits` instead.
|
||||
|
||||
Parameters:
|
||||
items (List): List of :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,
|
||||
:class:`~plexapi.photo.Photo`, or :class:`~plexapi.collection.Collection`
|
||||
objects to be edited.
|
||||
kwargs (dict): Dict of settings to edit.
|
||||
"""
|
||||
return self._edit(items, **kwargs)
|
||||
|
||||
def batchMultiEdits(self, items):
|
||||
""" Enable batch multi-editing mode to save API calls.
|
||||
Must call :func:`~plexapi.library.LibrarySection.saveMultiEdits` at the end to save all the edits.
|
||||
See :class:`~plexapi.mixins.EditFieldMixin` and :class:`~plexapi.mixins.EditTagsMixin`
|
||||
for individual field and tag editing methods.
|
||||
|
||||
Parameters:
|
||||
items (List): List of :class:`~plexapi.audio.Audio`, :class:`~plexapi.video.Video`,
|
||||
:class:`~plexapi.photo.Photo`, or :class:`~plexapi.collection.Collection`
|
||||
objects to be edited.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
movies = MovieSection.all()
|
||||
items = [movies[0], movies[3], movies[5]]
|
||||
|
||||
# Batch multi-editing multiple fields and tags in a single API call
|
||||
MovieSection.batchMultiEdits(items)
|
||||
MovieSection.editTitle('A New Title').editSummary('A new summary').editTagline('A new tagline') \\
|
||||
.addCollection('New Collection').removeGenre('Action').addLabel('Favorite')
|
||||
MovieSection.saveMultiEdits()
|
||||
|
||||
"""
|
||||
self._edits = {'items': self._validateItems(items)}
|
||||
return self
|
||||
|
||||
def saveMultiEdits(self):
|
||||
""" Save all the batch multi-edits.
|
||||
See :func:`~plexapi.library.LibrarySection.batchMultiEdits` for details.
|
||||
"""
|
||||
if not isinstance(self._edits, dict):
|
||||
raise BadRequest('Batch multi-editing mode not enabled. Must call `batchMultiEdits()` first.')
|
||||
|
||||
edits = self._edits
|
||||
self._edits = None
|
||||
self._edit(items=edits.pop('items'), **edits)
|
||||
return self
|
||||
|
||||
|
||||
class MovieSection(LibrarySection, MovieEditMixins):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
|
||||
|
||||
Attributes:
|
||||
|
@ -1719,7 +1830,7 @@ class MovieSection(LibrarySection):
|
|||
return super(MovieSection, self).sync(**kwargs)
|
||||
|
||||
|
||||
class ShowSection(LibrarySection):
|
||||
class ShowSection(LibrarySection, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
|
||||
|
||||
Attributes:
|
||||
|
@ -1803,7 +1914,7 @@ class ShowSection(LibrarySection):
|
|||
return super(ShowSection, self).sync(**kwargs)
|
||||
|
||||
|
||||
class MusicSection(LibrarySection):
|
||||
class MusicSection(LibrarySection, ArtistEditMixins, AlbumEditMixins, TrackEditMixins):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
|
||||
|
||||
Attributes:
|
||||
|
@ -1895,7 +2006,7 @@ class MusicSection(LibrarySection):
|
|||
return super(MusicSection, self).sync(**kwargs)
|
||||
|
||||
|
||||
class PhotoSection(LibrarySection):
|
||||
class PhotoSection(LibrarySection, PhotoalbumEditMixins, PhotoEditMixins):
|
||||
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
|
||||
|
||||
Attributes:
|
||||
|
@ -2160,16 +2271,6 @@ class Autotag(LibraryMediaTag):
|
|||
TAGTYPE = 207
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Banner(LibraryMediaTag):
|
||||
""" Represents a single Banner library media tag.
|
||||
|
||||
Attributes:
|
||||
TAGTYPE (int): 311
|
||||
"""
|
||||
TAGTYPE = 311
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Chapter(LibraryMediaTag):
|
||||
""" Represents a single Chapter library media tag.
|
||||
|
@ -3005,7 +3106,6 @@ class Path(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'Path'
|
||||
|
||||
home (bool): True if the path is the home directory
|
||||
key (str): API URL (/services/browse/<base64path>)
|
||||
network (bool): True if path is a network location
|
||||
|
@ -3037,7 +3137,6 @@ class File(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'File'
|
||||
|
||||
key (str): API URL (/services/browse/<base64path>)
|
||||
path (str): Full path to file
|
||||
title (str): File name
|
||||
|
@ -3048,3 +3147,105 @@ class File(PlexObject):
|
|||
self.key = data.attrib.get('key')
|
||||
self.path = data.attrib.get('path')
|
||||
self.title = data.attrib.get('title')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Common(PlexObject):
|
||||
""" Represents a Common element from a library. This object lists common fields between multiple objects.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Common'
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
|
||||
contentRating (str): Content rating of the items.
|
||||
countries (List<:class:`~plexapi.media.Country`>): List of countries objects.
|
||||
directors (List<:class:`~plexapi.media.Director`>): List of director objects.
|
||||
editionTitle (str): Edition title of the items.
|
||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
grandparentRatingKey (int): Grandparent rating key of the items.
|
||||
grandparentTitle (str): Grandparent title of the items.
|
||||
guid (str): Plex GUID of the items.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
index (int): Index of the items.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||
mixedFields (List<str>): List of mixed fields.
|
||||
moods (List<:class:`~plexapi.media.Mood`>): List of mood objects.
|
||||
originallyAvailableAt (datetime): Datetime of the release date of the items.
|
||||
parentRatingKey (int): Parent rating key of the items.
|
||||
parentTitle (str): Parent title of the items.
|
||||
producers (List<:class:`~plexapi.media.Producer`>): List of producer objects.
|
||||
ratingKey (int): Rating key of the items.
|
||||
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
|
||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||
studio (str): Studio name of the items.
|
||||
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
|
||||
summary (str): Summary of the items.
|
||||
tagline (str): Tagline of the items.
|
||||
tags (List<:class:`~plexapi.media.Tag`>): List of tag objects.
|
||||
title (str): Title of the items.
|
||||
titleSort (str): Title to use when sorting of the items.
|
||||
type (str): Type of the media (common).
|
||||
writers (List<:class:`~plexapi.media.Writer`>): List of writer objects.
|
||||
year (int): Year of the items.
|
||||
"""
|
||||
TAG = 'Common'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
self.directors = self.findItems(data, media.Director)
|
||||
self.editionTitle = data.attrib.get('editionTitle')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.guids = self.findItems(data, media.Guid)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.mixedFields = data.attrib.get('mixedFields').split(',')
|
||||
self.moods = self.findItems(data, media.Mood)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'))
|
||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.producers = self.findItems(data, media.Producer)
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.ratings = self.findItems(data, media.Rating)
|
||||
self.roles = self.findItems(data, media.Role)
|
||||
self.studio = data.attrib.get('studio')
|
||||
self.styles = self.findItems(data, media.Style)
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.tags = self.findItems(data, media.Tag)
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort')
|
||||
self.type = data.attrib.get('type')
|
||||
self.writers = self.findItems(data, media.Writer)
|
||||
self.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (
|
||||
self.__class__.__name__,
|
||||
self.commonType,
|
||||
','.join(str(key) for key in self.ratingKeys)
|
||||
)
|
||||
|
||||
@property
|
||||
def commonType(self):
|
||||
""" Returns the media type of the common items. """
|
||||
parsed_query = parse_qs(urlparse(self._initpath).query)
|
||||
return utils.reverseSearchType(parsed_query['type'][0])
|
||||
|
||||
@property
|
||||
def ratingKeys(self):
|
||||
""" Returns a list of rating keys for the common items. """
|
||||
parsed_query = parse_qs(urlparse(self._initpath).query)
|
||||
return [int(value.strip()) for value in parsed_query['id'][0].split(',')]
|
||||
|
||||
def items(self):
|
||||
""" Returns a list of the common items. """
|
||||
return self._server.fetchItems(self.ratingKeys)
|
||||
|
|
|
@ -955,7 +955,7 @@ class Review(PlexObject):
|
|||
|
||||
|
||||
class BaseResource(PlexObject):
|
||||
""" Base class for all Art, Banner, Poster, and Theme objects.
|
||||
""" Base class for all Art, Poster, and Theme objects.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Photo' or 'Track'
|
||||
|
@ -987,11 +987,6 @@ class Art(BaseResource):
|
|||
TAG = 'Photo'
|
||||
|
||||
|
||||
class Banner(BaseResource):
|
||||
""" Represents a single Banner object. """
|
||||
TAG = 'Photo'
|
||||
|
||||
|
||||
class Poster(BaseResource):
|
||||
""" Represents a single Poster object. """
|
||||
TAG = 'Photo'
|
||||
|
|
|
@ -138,7 +138,7 @@ class SplitMergeMixin:
|
|||
if not isinstance(ratingKeys, list):
|
||||
ratingKeys = str(ratingKeys).split(',')
|
||||
|
||||
key = f"{self.key}/merge?ids={','.join([str(r) for r in ratingKeys])}"
|
||||
key = f"{self.key}/merge?ids={','.join(str(r) for r in ratingKeys)}"
|
||||
self._server.query(key, method=self._server._session.put)
|
||||
return self
|
||||
|
||||
|
@ -328,7 +328,19 @@ class ArtUrlMixin:
|
|||
return self._server.url(art, includeToken=True) if art else None
|
||||
|
||||
|
||||
class ArtMixin(ArtUrlMixin):
|
||||
class ArtLockMixin:
|
||||
""" Mixin for Plex objects that can have a locked background artwork. """
|
||||
|
||||
def lockArt(self):
|
||||
""" Lock the background artwork for a Plex object. """
|
||||
return self._edit(**{'art.locked': 1})
|
||||
|
||||
def unlockArt(self):
|
||||
""" Unlock the background artwork for a Plex object. """
|
||||
return self._edit(**{'art.locked': 0})
|
||||
|
||||
|
||||
class ArtMixin(ArtUrlMixin, ArtLockMixin):
|
||||
""" Mixin for Plex objects that can have background artwork. """
|
||||
|
||||
def arts(self):
|
||||
|
@ -360,65 +372,6 @@ class ArtMixin(ArtUrlMixin):
|
|||
art.select()
|
||||
return self
|
||||
|
||||
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 BannerUrlMixin:
|
||||
""" Mixin for Plex objects that can have a banner url. """
|
||||
|
||||
@property
|
||||
def bannerUrl(self):
|
||||
""" Return the banner url for the Plex object. """
|
||||
banner = self.firstAttr('banner')
|
||||
return self._server.url(banner, includeToken=True) if banner else None
|
||||
|
||||
|
||||
class BannerMixin(BannerUrlMixin):
|
||||
""" Mixin for Plex objects that can have banners. """
|
||||
|
||||
def banners(self):
|
||||
""" Returns list of available :class:`~plexapi.media.Banner` objects. """
|
||||
return self.fetchItems(f'/library/metadata/{self.ratingKey}/banners', cls=media.Banner)
|
||||
|
||||
def uploadBanner(self, url=None, filepath=None):
|
||||
""" Upload a banner from a url or filepath.
|
||||
|
||||
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.
|
||||
"""
|
||||
if url:
|
||||
key = f'/library/metadata/{self.ratingKey}/banners?url={quote_plus(url)}'
|
||||
self._server.query(key, method=self._server._session.post)
|
||||
elif filepath:
|
||||
key = f'/library/metadata/{self.ratingKey}/banners'
|
||||
data = openOrRead(filepath)
|
||||
self._server.query(key, method=self._server._session.post, data=data)
|
||||
return self
|
||||
|
||||
def setBanner(self, banner):
|
||||
""" Set the banner for a Plex object.
|
||||
|
||||
Parameters:
|
||||
banner (:class:`~plexapi.media.Banner`): The banner object to select.
|
||||
"""
|
||||
banner.select()
|
||||
return self
|
||||
|
||||
def lockBanner(self):
|
||||
""" Lock the banner for a Plex object. """
|
||||
return self._edit(**{'banner.locked': 1})
|
||||
|
||||
def unlockBanner(self):
|
||||
""" Unlock the banner for a Plex object. """
|
||||
return self._edit(**{'banner.locked': 0})
|
||||
|
||||
|
||||
class PosterUrlMixin:
|
||||
""" Mixin for Plex objects that can have a poster url. """
|
||||
|
@ -435,7 +388,19 @@ class PosterUrlMixin:
|
|||
return self.thumbUrl
|
||||
|
||||
|
||||
class PosterMixin(PosterUrlMixin):
|
||||
class PosterLockMixin:
|
||||
""" Mixin for Plex objects that can have a locked poster. """
|
||||
|
||||
def lockPoster(self):
|
||||
""" Lock the poster for a Plex object. """
|
||||
return self._edit(**{'thumb.locked': 1})
|
||||
|
||||
def unlockPoster(self):
|
||||
""" Unlock the poster for a Plex object. """
|
||||
return self._edit(**{'thumb.locked': 0})
|
||||
|
||||
|
||||
class PosterMixin(PosterUrlMixin, PosterLockMixin):
|
||||
""" Mixin for Plex objects that can have posters. """
|
||||
|
||||
def posters(self):
|
||||
|
@ -467,14 +432,6 @@ class PosterMixin(PosterUrlMixin):
|
|||
poster.select()
|
||||
return self
|
||||
|
||||
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 ThemeUrlMixin:
|
||||
""" Mixin for Plex objects that can have a theme url. """
|
||||
|
@ -486,7 +443,19 @@ class ThemeUrlMixin:
|
|||
return self._server.url(theme, includeToken=True) if theme else None
|
||||
|
||||
|
||||
class ThemeMixin(ThemeUrlMixin):
|
||||
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):
|
||||
|
@ -519,14 +488,6 @@ class ThemeMixin(ThemeUrlMixin):
|
|||
'Re-upload the theme using "uploadTheme" to set it.'
|
||||
)
|
||||
|
||||
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 EditFieldMixin:
|
||||
""" Mixin for editing Plex object fields. """
|
||||
|
@ -751,6 +712,19 @@ class PhotoCapturedTimeMixin(EditFieldMixin):
|
|||
return self.editField('originallyAvailableAt', capturedTime, locked=locked)
|
||||
|
||||
|
||||
class UserRatingMixin(EditFieldMixin):
|
||||
""" Mixin for Plex objects that can have a user rating. """
|
||||
|
||||
def editUserRating(self, userRating, locked=True):
|
||||
""" Edit the user rating.
|
||||
|
||||
Parameters:
|
||||
userRating (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. """
|
||||
|
||||
|
@ -780,7 +754,7 @@ class EditTagsMixin:
|
|||
items = [items]
|
||||
|
||||
if not remove:
|
||||
tags = getattr(self, self._tagPlural(tag))
|
||||
tags = getattr(self, self._tagPlural(tag), [])
|
||||
items = tags + items
|
||||
|
||||
edits = self._tagHelper(self._tagSingular(tag), items, locked, remove)
|
||||
|
@ -821,7 +795,7 @@ class EditTagsMixin:
|
|||
|
||||
if remove:
|
||||
tagname = f'{tag}[].tag.tag-'
|
||||
data[tagname] = ','.join([quote(str(t)) for t in items])
|
||||
data[tagname] = ','.join(quote(str(t)) for t in items)
|
||||
else:
|
||||
for i, item in enumerate(items):
|
||||
tagname = f'{str(tag)}[{i}].tag.tag'
|
||||
|
@ -1134,3 +1108,84 @@ class WatchlistMixin:
|
|||
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
|
||||
|
|
|
@ -8,8 +8,7 @@ from plexapi.exceptions import BadRequest
|
|||
from plexapi.mixins import (
|
||||
RatingMixin,
|
||||
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
|
||||
AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin,
|
||||
TagMixin
|
||||
PhotoalbumEditMixins, PhotoEditMixins
|
||||
)
|
||||
|
||||
|
||||
|
@ -18,7 +17,7 @@ class Photoalbum(
|
|||
PlexPartialObject,
|
||||
RatingMixin,
|
||||
ArtMixin, PosterMixin,
|
||||
AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin
|
||||
PhotoalbumEditMixins
|
||||
):
|
||||
""" Represents a single Photoalbum (collection of photos).
|
||||
|
||||
|
@ -146,8 +145,7 @@ class Photo(
|
|||
PlexPartialObject, Playable,
|
||||
RatingMixin,
|
||||
ArtUrlMixin, PosterUrlMixin,
|
||||
AddedAtMixin, PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
TagMixin
|
||||
PhotoEditMixins
|
||||
):
|
||||
""" Represents a single Photo.
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@ class PlayQueue(PlexObject):
|
|||
}
|
||||
|
||||
if isinstance(items, list):
|
||||
item_keys = ",".join([str(x.ratingKey) for x in items])
|
||||
item_keys = ",".join(str(x.ratingKey) for x in items)
|
||||
uri_args = quote_plus(f"/library/metadata/{item_keys}")
|
||||
args["uri"] = f"library:///directory/{uri_args}"
|
||||
args["type"] = items[0].listType
|
||||
|
|
|
@ -81,7 +81,7 @@ class Settings(PlexObject):
|
|||
params[setting.id] = quote(setting._setValue)
|
||||
if not params:
|
||||
raise BadRequest('No setting have been modified.')
|
||||
querystr = '&'.join([f'{k}={v}' for k, v in params.items()])
|
||||
querystr = '&'.join(f'{k}={v}' for k, v in params.items())
|
||||
url = f'{self.key}?{querystr}'
|
||||
self._server.query(url, self._server._session.put)
|
||||
self.reload()
|
||||
|
|
|
@ -7,15 +7,13 @@ from plexapi.base import Playable, PlexPartialObject, PlexSession
|
|||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
|
||||
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
|
||||
AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin,
|
||||
StudioMixin, SummaryMixin, TaglineMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
|
||||
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
|
||||
WatchlistMixin
|
||||
)
|
||||
|
||||
|
||||
class Video(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin):
|
||||
class Video(PlexPartialObject, PlayedUnplayedMixin):
|
||||
""" Base class for all video objects including :class:`~plexapi.video.Movie`,
|
||||
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
|
||||
:class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`.
|
||||
|
@ -309,9 +307,7 @@ class Movie(
|
|||
Video, Playable,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
SummaryMixin, TaglineMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||
MovieEditMixins,
|
||||
WatchlistMixin
|
||||
):
|
||||
""" Represents a single Movie.
|
||||
|
@ -453,10 +449,8 @@ class Movie(
|
|||
class Show(
|
||||
Video,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, BannerMixin, PosterMixin, ThemeMixin,
|
||||
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||
SummaryMixin, TaglineMixin, TitleMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
ShowEditMixins,
|
||||
WatchlistMixin
|
||||
):
|
||||
""" Represents a single Show (including all seasons and episodes).
|
||||
|
@ -474,7 +468,6 @@ class Show(
|
|||
autoDeletionItemPolicyWatchedLibrary (int): Setting that indicates if episodes are deleted
|
||||
after being watched for the show (0 = Never, 1 = After a day, 7 = After a week,
|
||||
100 = On next refresh).
|
||||
banner (str): Key to banner artwork (/library/metadata/<ratingkey>/banner/<bannerid>).
|
||||
childCount (int): Number of seasons (including Specials) in the show.
|
||||
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
|
@ -528,7 +521,6 @@ class Show(
|
|||
int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0'))
|
||||
self.autoDeletionItemPolicyWatchedLibrary = utils.cast(
|
||||
int, data.attrib.get('autoDeletionItemPolicyWatchedLibrary', '0'))
|
||||
self.banner = data.attrib.get('banner')
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
|
@ -666,8 +658,7 @@ class Season(
|
|||
Video,
|
||||
AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||
SummaryMixin, TitleMixin,
|
||||
CollectionMixin, LabelMixin
|
||||
SeasonEditMixins
|
||||
):
|
||||
""" Represents a single Show Season (including all episodes).
|
||||
|
||||
|
@ -820,8 +811,7 @@ class Episode(
|
|||
Video, Playable,
|
||||
ExtrasMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||
ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
|
||||
EpisodeEditMixins
|
||||
):
|
||||
""" Represents a single Shows Episode.
|
||||
|
||||
|
|
|
@ -416,10 +416,6 @@ def is_art(key):
|
|||
return is_metadata(key, contains="/art/")
|
||||
|
||||
|
||||
def is_banner(key):
|
||||
return is_metadata(key, contains="/banner/")
|
||||
|
||||
|
||||
def is_thumb(key):
|
||||
return is_metadata(key, contains="/thumb/")
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ def test_audio_Artist_mixins_fields(artist):
|
|||
test_mixins.edit_sort_title(artist)
|
||||
test_mixins.edit_summary(artist)
|
||||
test_mixins.edit_title(artist)
|
||||
test_mixins.edit_user_rating(artist)
|
||||
|
||||
|
||||
def test_audio_Artist_mixins_tags(artist):
|
||||
|
@ -238,6 +239,7 @@ def test_audio_Album_mixins_fields(album):
|
|||
test_mixins.edit_studio(album)
|
||||
test_mixins.edit_summary(album)
|
||||
test_mixins.edit_title(album)
|
||||
test_mixins.edit_user_rating(album)
|
||||
|
||||
|
||||
def test_audio_Album_mixins_tags(album):
|
||||
|
@ -408,6 +410,7 @@ def test_audio_Track_mixins_fields(track):
|
|||
test_mixins.edit_track_artist(track)
|
||||
test_mixins.edit_track_number(track)
|
||||
test_mixins.edit_track_disc_number(track)
|
||||
test_mixins.edit_user_rating(track)
|
||||
|
||||
|
||||
def test_audio_Track_mixins_tags(track):
|
||||
|
|
|
@ -347,6 +347,7 @@ def test_Collection_mixins_fields(collection):
|
|||
test_mixins.edit_sort_title(collection)
|
||||
test_mixins.edit_summary(collection)
|
||||
test_mixins.edit_title(collection)
|
||||
test_mixins.edit_user_rating(collection)
|
||||
|
||||
|
||||
def test_Collection_mixins_tags(collection):
|
||||
|
|
|
@ -844,3 +844,69 @@ def _do_test_library_search(library, obj, field, operator, searchValue):
|
|||
assert obj not in results
|
||||
else:
|
||||
assert obj in results
|
||||
|
||||
|
||||
def test_library_common(movies):
|
||||
items = movies.all()
|
||||
common = movies.common(items)
|
||||
assert common.commonType == "movie"
|
||||
assert common.ratingKeys == [m.ratingKey for m in items]
|
||||
assert common.items() == items
|
||||
|
||||
|
||||
def test_library_multiedit(movies, tvshows):
|
||||
movie1, movie2 = movies.all()[:2]
|
||||
show1, show2 = tvshows.all()[:2]
|
||||
|
||||
movie1_title = movie1.title
|
||||
movie2_title = movie2.title
|
||||
show1_title = show1.title
|
||||
|
||||
# Edit multiple titles
|
||||
title = "Test Title"
|
||||
movies.multiEdit([movie1, movie2], **{"title.value": title})
|
||||
assert movie1.reload().title == title
|
||||
assert movie2.reload().title == title
|
||||
|
||||
# Reset titles
|
||||
movie1.editTitle(movie1_title, locked=False).reload()
|
||||
movie2.editTitle(movie2_title, locked=False).reload()
|
||||
assert movie1.title == movie1_title
|
||||
assert movie2.title == movie2_title
|
||||
|
||||
# Test batch multi-editing
|
||||
genre = "Test Genre"
|
||||
tvshows.batchMultiEdits([show1, show2]).addGenre(genre).saveMultiEdits()
|
||||
assert genre in [g.tag for g in show1.reload().genres]
|
||||
assert genre in [g.tag for g in show2.reload().genres]
|
||||
|
||||
# Reset genres
|
||||
tvshows.batchMultiEdits([show1, show2]).removeGenre(genre, locked=False).saveMultiEdits()
|
||||
assert genre not in [g.tag for g in show1.reload().genres]
|
||||
assert genre not in [g.tag for g in show2.reload().genres]
|
||||
|
||||
# Test multi-editing with a single item
|
||||
tvshows.batchMultiEdits(show1).editTitle(title).saveMultiEdits()
|
||||
assert show1.reload().title == title
|
||||
|
||||
# Reset title
|
||||
show1.editTitle(show1_title, locked=False).reload()
|
||||
assert show1.title == show1_title
|
||||
|
||||
|
||||
def test_library_multiedit_exceptions(music, artist, album, photos):
|
||||
with pytest.raises(BadRequest):
|
||||
music.multiEdit([])
|
||||
with pytest.raises(BadRequest):
|
||||
music.multiEdit([artist, album])
|
||||
with pytest.raises(BadRequest):
|
||||
photos.batchMultiEdits(artist)
|
||||
with pytest.raises(BadRequest):
|
||||
photos.saveMultiEdits()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
photos.editTitle("test")
|
||||
with pytest.raises(AttributeError):
|
||||
music.batchMultiEdits(artist).editEdition("test")
|
||||
with pytest.raises(AttributeError):
|
||||
music.batchMultiEdits(album).addCountry("test")
|
||||
|
|
|
@ -13,14 +13,16 @@ CUTE_CAT_SHA1 = "9f7003fc401761d8e0b0364d428b2dab2f789dbb"
|
|||
AUDIO_STUB_SHA1 = "1abc20d5fdc904201bf8988ca6ef30f96bb73617"
|
||||
|
||||
|
||||
def _test_mixins_field(obj, attr, field_method):
|
||||
def _test_mixins_field(obj, attr, field_method, default=None, value=None):
|
||||
edit_field_method = getattr(obj, "edit" + field_method)
|
||||
_value = lambda: getattr(obj, attr)
|
||||
_fields = lambda: [f for f in obj.fields if f.name == attr]
|
||||
|
||||
# Check field does not match to begin with
|
||||
default_value = _value()
|
||||
if isinstance(default_value, datetime):
|
||||
default_value = default or _value()
|
||||
if value:
|
||||
test_value = value
|
||||
elif isinstance(default_value, datetime):
|
||||
test_value = TEST_MIXIN_DATE
|
||||
elif isinstance(default_value, int):
|
||||
test_value = default_value + 1
|
||||
|
@ -101,6 +103,10 @@ def edit_photo_captured_time(obj):
|
|||
_test_mixins_field(obj, "originallyAvailableAt", "CapturedTime")
|
||||
|
||||
|
||||
def edit_user_rating(obj):
|
||||
_test_mixins_field(obj, "userRating", "UserRating", default=None, value=10)
|
||||
|
||||
|
||||
def _test_mixins_tag(obj, attr, tag_method):
|
||||
add_tag_method = getattr(obj, "add" + tag_method)
|
||||
remove_tag_method = getattr(obj, "remove" + tag_method)
|
||||
|
@ -205,10 +211,6 @@ def lock_art(obj):
|
|||
_test_mixins_lock_image(obj, "arts")
|
||||
|
||||
|
||||
def lock_banner(obj):
|
||||
_test_mixins_lock_image(obj, "banners")
|
||||
|
||||
|
||||
def lock_poster(obj):
|
||||
_test_mixins_lock_image(obj, "posters")
|
||||
|
||||
|
@ -273,10 +275,6 @@ def edit_art(obj):
|
|||
_test_mixins_edit_image(obj, "arts")
|
||||
|
||||
|
||||
def edit_banner(obj):
|
||||
_test_mixins_edit_image(obj, "banners")
|
||||
|
||||
|
||||
def edit_poster(obj):
|
||||
_test_mixins_edit_image(obj, "posters")
|
||||
|
||||
|
@ -297,10 +295,6 @@ def attr_artUrl(obj):
|
|||
_test_mixins_imageUrl(obj, "art")
|
||||
|
||||
|
||||
def attr_bannerUrl(obj):
|
||||
_test_mixins_imageUrl(obj, "banner")
|
||||
|
||||
|
||||
def attr_posterUrl(obj):
|
||||
_test_mixins_imageUrl(obj, "thumb")
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ def test_photo_Photoalbum_mixins_fields(photoalbum):
|
|||
test_mixins.edit_sort_title(photoalbum)
|
||||
test_mixins.edit_summary(photoalbum)
|
||||
test_mixins.edit_title(photoalbum)
|
||||
test_mixins.edit_user_rating(photoalbum)
|
||||
|
||||
|
||||
def test_photo_Photoalbum_PlexWebURL(plex, photoalbum):
|
||||
|
@ -55,6 +56,7 @@ def test_photo_Photo_mixins_fields(photo):
|
|||
test_mixins.edit_summary(photo)
|
||||
test_mixins.edit_title(photo)
|
||||
test_mixins.edit_photo_captured_time(photo)
|
||||
test_mixins.edit_user_rating(photo)
|
||||
|
||||
|
||||
def test_photo_Photo_mixins_tags(photo):
|
||||
|
|
|
@ -668,12 +668,13 @@ def test_video_Movie_mixins_fields(movie):
|
|||
test_mixins.edit_summary(movie)
|
||||
test_mixins.edit_tagline(movie)
|
||||
test_mixins.edit_title(movie)
|
||||
test_mixins.edit_user_rating(movie)
|
||||
with pytest.raises(BadRequest):
|
||||
test_mixins.edit_edition_title(movie)
|
||||
|
||||
|
||||
@pytest.mark.authenticated
|
||||
def test_video_Movie_mixins_fields(movie):
|
||||
def test_video_Movie_mixins_fields_edition(movie):
|
||||
test_mixins.edit_edition_title(movie)
|
||||
|
||||
|
||||
|
@ -926,6 +927,7 @@ def test_video_Show_mixins_fields(show):
|
|||
test_mixins.edit_summary(show)
|
||||
test_mixins.edit_tagline(show)
|
||||
test_mixins.edit_title(show)
|
||||
test_mixins.edit_user_rating(show)
|
||||
|
||||
|
||||
def test_video_Show_mixins_tags(show):
|
||||
|
@ -1075,6 +1077,7 @@ def test_video_Season_mixins_fields(show):
|
|||
test_mixins.edit_added_at(season)
|
||||
test_mixins.edit_summary(season)
|
||||
test_mixins.edit_title(season)
|
||||
test_mixins.edit_user_rating(season)
|
||||
|
||||
|
||||
def test_video_Season_mixins_tags(show):
|
||||
|
@ -1286,6 +1289,7 @@ def test_video_Episode_mixins_fields(episode):
|
|||
test_mixins.edit_sort_title(episode)
|
||||
test_mixins.edit_summary(episode)
|
||||
test_mixins.edit_title(episode)
|
||||
test_mixins.edit_user_rating(episode)
|
||||
|
||||
|
||||
def test_video_Episode_mixins_tags(episode):
|
||||
|
|
Loading…
Reference in a new issue