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:
JonnyWong16 2023-07-27 14:45:23 -07:00 committed by GitHub
parent b06afb8f84
commit 9ede493f4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 468 additions and 168 deletions

View file

@ -8,14 +8,12 @@ from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, ArtistEditMixins, AlbumEditMixins, TrackEditMixins
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
) )
from plexapi.playlist import Playlist 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`, """ Base class for all audio objects including :class:`~plexapi.audio.Artist`,
:class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`. :class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`.
@ -132,8 +130,7 @@ class Artist(
Audio, Audio,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
SortTitleMixin, SummaryMixin, TitleMixin, ArtistEditMixins
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
): ):
""" Represents a single Artist. """ Represents a single Artist.
@ -244,8 +241,7 @@ class Album(
Audio, Audio,
UnmatchMatchMixin, RatingMixin, UnmatchMatchMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin, ArtMixin, PosterMixin, ThemeUrlMixin,
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, AlbumEditMixins
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
): ):
""" Represents a single Album. """ Represents a single Album.
@ -364,8 +360,7 @@ class Track(
Audio, Playable, Audio, Playable,
ExtrasMixin, RatingMixin, ExtrasMixin, RatingMixin,
ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin, ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin,
TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, TrackEditMixins
CollectionMixin, LabelMixin, MoodMixin
): ):
""" Represents a single Track. """ Represents a single Track.

View file

@ -152,7 +152,9 @@ class PlexObject:
and attrs. and attrs.
Parameters: 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 cls (:class:`~plexapi.base.PlexObject`): If you know the class of the
items to be fetched, passing this in will help the parser ensure items to be fetched, passing this in will help the parser ensure
it only returns those items. By default we convert the xml elements it only returns those items. By default we convert the xml elements
@ -225,6 +227,9 @@ class PlexObject:
if ekey is None: if ekey is None:
raise BadRequest('ekey was not provided') 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_start = container_start or 0
container_size = container_size or X_PLEX_CONTAINER_SIZE container_size = container_size or X_PLEX_CONTAINER_SIZE
offset = container_start offset = container_start
@ -559,13 +564,10 @@ class PlexPartialObject(PlexObject):
self._edits.update(kwargs) self._edits.update(kwargs)
return self return self
if 'id' not in kwargs:
kwargs['id'] = self.ratingKey
if 'type' not in kwargs: if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self._searchType) kwargs['type'] = utils.searchType(self._searchType)
part = f'/library/sections/{self.librarySectionID}/all{utils.joinArgs(kwargs)}' self.section()._edit(items=self, **kwargs)
self._server.query(part, method=self._server._session.put)
return self return self
def edit(self, **kwargs): def edit(self, **kwargs):

View file

@ -8,8 +8,7 @@ from plexapi.library import LibrarySection, ManagedHub
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, CollectionEditMixins
LabelMixin
) )
from plexapi.utils import deprecated from plexapi.utils import deprecated
@ -19,8 +18,7 @@ class Collection(
PlexPartialObject, PlexPartialObject,
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, CollectionEditMixins
LabelMixin
): ):
""" Represents a single Collection. """ Represents a single Collection.

View file

@ -1,11 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
from datetime import datetime 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 import log, media, utils
from plexapi.base import OPERATORS, PlexObject from plexapi.base import OPERATORS, PlexObject
from plexapi.exceptions import BadRequest, NotFound 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.settings import Setting
from plexapi.utils import cached_property, deprecated from plexapi.utils import cached_property, deprecated
@ -440,6 +444,20 @@ class LibrarySection(PlexObject):
self._getTotalDurationStorage() self._getTotalDurationStorage()
return self._totalStorage 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): def _getTotalDurationStorage(self):
""" Queries the Plex server for the total library duration and storage and caches the values. """ """ Queries the Plex server for the total library duration and storage and caches the values. """
data = self._server.query('/media/providers?includeStorage=1') data = self._server.query('/media/providers?includeStorage=1')
@ -1658,8 +1676,101 @@ class LibrarySection(PlexObject):
params['pageType'] = 'list' params['pageType'] = 'list'
return self._server._buildWebURL(base=base, **params) 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.')
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}')
class MovieSection(LibrarySection): 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. """ Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
Attributes: Attributes:
@ -1719,7 +1830,7 @@ class MovieSection(LibrarySection):
return super(MovieSection, self).sync(**kwargs) 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. """ Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
Attributes: Attributes:
@ -1803,7 +1914,7 @@ class ShowSection(LibrarySection):
return super(ShowSection, self).sync(**kwargs) 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. """ Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
Attributes: Attributes:
@ -1895,7 +2006,7 @@ class MusicSection(LibrarySection):
return super(MusicSection, self).sync(**kwargs) return super(MusicSection, self).sync(**kwargs)
class PhotoSection(LibrarySection): class PhotoSection(LibrarySection, PhotoalbumEditMixins, PhotoEditMixins):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos. """ Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
Attributes: Attributes:
@ -2160,16 +2271,6 @@ class Autotag(LibraryMediaTag):
TAGTYPE = 207 TAGTYPE = 207
@utils.registerPlexObject
class Banner(LibraryMediaTag):
""" Represents a single Banner library media tag.
Attributes:
TAGTYPE (int): 311
"""
TAGTYPE = 311
@utils.registerPlexObject @utils.registerPlexObject
class Chapter(LibraryMediaTag): class Chapter(LibraryMediaTag):
""" Represents a single Chapter library media tag. """ Represents a single Chapter library media tag.
@ -3005,7 +3106,6 @@ class Path(PlexObject):
Attributes: Attributes:
TAG (str): 'Path' TAG (str): 'Path'
home (bool): True if the path is the home directory home (bool): True if the path is the home directory
key (str): API URL (/services/browse/<base64path>) key (str): API URL (/services/browse/<base64path>)
network (bool): True if path is a network location network (bool): True if path is a network location
@ -3037,7 +3137,6 @@ class File(PlexObject):
Attributes: Attributes:
TAG (str): 'File' TAG (str): 'File'
key (str): API URL (/services/browse/<base64path>) key (str): API URL (/services/browse/<base64path>)
path (str): Full path to file path (str): Full path to file
title (str): File name title (str): File name
@ -3048,3 +3147,105 @@ class File(PlexObject):
self.key = data.attrib.get('key') self.key = data.attrib.get('key')
self.path = data.attrib.get('path') self.path = data.attrib.get('path')
self.title = data.attrib.get('title') 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)

View file

@ -955,7 +955,7 @@ class Review(PlexObject):
class BaseResource(PlexObject): class BaseResource(PlexObject):
""" Base class for all Art, Banner, Poster, and Theme objects. """ Base class for all Art, Poster, and Theme objects.
Attributes: Attributes:
TAG (str): 'Photo' or 'Track' TAG (str): 'Photo' or 'Track'
@ -987,11 +987,6 @@ class Art(BaseResource):
TAG = 'Photo' TAG = 'Photo'
class Banner(BaseResource):
""" Represents a single Banner object. """
TAG = 'Photo'
class Poster(BaseResource): class Poster(BaseResource):
""" Represents a single Poster object. """ """ Represents a single Poster object. """
TAG = 'Photo' TAG = 'Photo'

View file

@ -138,7 +138,7 @@ class SplitMergeMixin:
if not isinstance(ratingKeys, list): if not isinstance(ratingKeys, list):
ratingKeys = str(ratingKeys).split(',') 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) self._server.query(key, method=self._server._session.put)
return self return self
@ -328,7 +328,19 @@ class ArtUrlMixin:
return self._server.url(art, includeToken=True) if art else None 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. """ """ Mixin for Plex objects that can have background artwork. """
def arts(self): def arts(self):
@ -360,65 +372,6 @@ class ArtMixin(ArtUrlMixin):
art.select() art.select()
return self 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: class PosterUrlMixin:
""" Mixin for Plex objects that can have a poster url. """ """ Mixin for Plex objects that can have a poster url. """
@ -435,7 +388,19 @@ class PosterUrlMixin:
return self.thumbUrl 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. """ """ Mixin for Plex objects that can have posters. """
def posters(self): def posters(self):
@ -467,14 +432,6 @@ class PosterMixin(PosterUrlMixin):
poster.select() poster.select()
return self 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: class ThemeUrlMixin:
""" Mixin for Plex objects that can have a theme url. """ """ 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 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. """ """ Mixin for Plex objects that can have themes. """
def themes(self): def themes(self):
@ -519,14 +488,6 @@ class ThemeMixin(ThemeUrlMixin):
'Re-upload the theme using "uploadTheme" to set it.' '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: class EditFieldMixin:
""" Mixin for editing Plex object fields. """ """ Mixin for editing Plex object fields. """
@ -751,6 +712,19 @@ class PhotoCapturedTimeMixin(EditFieldMixin):
return self.editField('originallyAvailableAt', capturedTime, locked=locked) 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: class EditTagsMixin:
""" Mixin for editing Plex object tags. """ """ Mixin for editing Plex object tags. """
@ -780,7 +754,7 @@ class EditTagsMixin:
items = [items] items = [items]
if not remove: if not remove:
tags = getattr(self, self._tagPlural(tag)) tags = getattr(self, self._tagPlural(tag), [])
items = tags + items items = tags + items
edits = self._tagHelper(self._tagSingular(tag), items, locked, remove) edits = self._tagHelper(self._tagSingular(tag), items, locked, remove)
@ -821,7 +795,7 @@ class EditTagsMixin:
if remove: if remove:
tagname = f'{tag}[].tag.tag-' 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: else:
for i, item in enumerate(items): for i, item in enumerate(items):
tagname = f'{str(tag)}[{i}].tag.tag' tagname = f'{str(tag)}[{i}].tag.tag'
@ -1134,3 +1108,84 @@ class WatchlistMixin:
ratingKey = self.guid.rsplit('/', 1)[-1] ratingKey = self.guid.rsplit('/', 1)[-1]
data = account.query(f"{account.METADATA}/library/metadata/{ratingKey}/availabilities") data = account.query(f"{account.METADATA}/library/metadata/{ratingKey}/availabilities")
return self.findItems(data) 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

View file

@ -8,8 +8,7 @@ from plexapi.exceptions import BadRequest
from plexapi.mixins import ( from plexapi.mixins import (
RatingMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin, PhotoalbumEditMixins, PhotoEditMixins
TagMixin
) )
@ -18,7 +17,7 @@ class Photoalbum(
PlexPartialObject, PlexPartialObject,
RatingMixin, RatingMixin,
ArtMixin, PosterMixin, ArtMixin, PosterMixin,
AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin PhotoalbumEditMixins
): ):
""" Represents a single Photoalbum (collection of photos). """ Represents a single Photoalbum (collection of photos).
@ -146,8 +145,7 @@ class Photo(
PlexPartialObject, Playable, PlexPartialObject, Playable,
RatingMixin, RatingMixin,
ArtUrlMixin, PosterUrlMixin, ArtUrlMixin, PosterUrlMixin,
AddedAtMixin, PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin, PhotoEditMixins
TagMixin
): ):
""" Represents a single Photo. """ Represents a single Photo.

View file

@ -170,7 +170,7 @@ class PlayQueue(PlexObject):
} }
if isinstance(items, list): 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}") uri_args = quote_plus(f"/library/metadata/{item_keys}")
args["uri"] = f"library:///directory/{uri_args}" args["uri"] = f"library:///directory/{uri_args}"
args["type"] = items[0].listType args["type"] = items[0].listType

View file

@ -81,7 +81,7 @@ class Settings(PlexObject):
params[setting.id] = quote(setting._setValue) params[setting.id] = quote(setting._setValue)
if not params: if not params:
raise BadRequest('No setting have been modified.') 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}' url = f'{self.key}?{querystr}'
self._server.query(url, self._server._session.put) self._server.query(url, self._server._session.put)
self.reload() self.reload()

View file

@ -7,15 +7,13 @@ from plexapi.base import Playable, PlexPartialObject, PlexSession
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.mixins import ( from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
StudioMixin, SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin WatchlistMixin
) )
class Video(PlexPartialObject, PlayedUnplayedMixin, AddedAtMixin): class Video(PlexPartialObject, PlayedUnplayedMixin):
""" Base class for all video objects including :class:`~plexapi.video.Movie`, """ Base class for all video objects including :class:`~plexapi.video.Movie`,
:class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`, :class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`,
:class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`. :class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`.
@ -309,9 +307,7 @@ class Movie(
Video, Playable, Video, Playable,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, MovieEditMixins,
SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin WatchlistMixin
): ):
""" Represents a single Movie. """ Represents a single Movie.
@ -453,10 +449,8 @@ class Movie(
class Show( class Show(
Video, Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, BannerMixin, PosterMixin, ThemeMixin, ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, ShowEditMixins,
SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, GenreMixin, LabelMixin,
WatchlistMixin WatchlistMixin
): ):
""" Represents a single Show (including all seasons and episodes). """ Represents a single Show (including all seasons and episodes).
@ -474,7 +468,6 @@ class Show(
autoDeletionItemPolicyWatchedLibrary (int): Setting that indicates if episodes are deleted 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, after being watched for the show (0 = Never, 1 = After a day, 7 = After a week,
100 = On next refresh). 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. childCount (int): Number of seasons (including Specials) in the show.
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
contentRating (str) Content rating (PG-13; NR; TV-G). contentRating (str) Content rating (PG-13; NR; TV-G).
@ -528,7 +521,6 @@ class Show(
int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0')) int, data.attrib.get('autoDeletionItemPolicyUnwatchedLibrary', '0'))
self.autoDeletionItemPolicyWatchedLibrary = utils.cast( self.autoDeletionItemPolicyWatchedLibrary = utils.cast(
int, data.attrib.get('autoDeletionItemPolicyWatchedLibrary', '0')) int, data.attrib.get('autoDeletionItemPolicyWatchedLibrary', '0'))
self.banner = data.attrib.get('banner')
self.childCount = utils.cast(int, data.attrib.get('childCount')) self.childCount = utils.cast(int, data.attrib.get('childCount'))
self.collections = self.findItems(data, media.Collection) self.collections = self.findItems(data, media.Collection)
self.contentRating = data.attrib.get('contentRating') self.contentRating = data.attrib.get('contentRating')
@ -666,8 +658,7 @@ class Season(
Video, Video,
AdvancedSettingsMixin, ExtrasMixin, RatingMixin, AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin, ArtMixin, PosterMixin, ThemeUrlMixin,
SummaryMixin, TitleMixin, SeasonEditMixins
CollectionMixin, LabelMixin
): ):
""" Represents a single Show Season (including all episodes). """ Represents a single Show Season (including all episodes).
@ -820,8 +811,7 @@ class Episode(
Video, Playable, Video, Playable,
ExtrasMixin, RatingMixin, ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin, ArtMixin, PosterMixin, ThemeUrlMixin,
ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, EpisodeEditMixins
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
): ):
""" Represents a single Shows Episode. """ Represents a single Shows Episode.

View file

@ -416,10 +416,6 @@ def is_art(key):
return is_metadata(key, contains="/art/") return is_metadata(key, contains="/art/")
def is_banner(key):
return is_metadata(key, contains="/banner/")
def is_thumb(key): def is_thumb(key):
return is_metadata(key, contains="/thumb/") return is_metadata(key, contains="/thumb/")

View file

@ -110,6 +110,7 @@ def test_audio_Artist_mixins_fields(artist):
test_mixins.edit_sort_title(artist) test_mixins.edit_sort_title(artist)
test_mixins.edit_summary(artist) test_mixins.edit_summary(artist)
test_mixins.edit_title(artist) test_mixins.edit_title(artist)
test_mixins.edit_user_rating(artist)
def test_audio_Artist_mixins_tags(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_studio(album)
test_mixins.edit_summary(album) test_mixins.edit_summary(album)
test_mixins.edit_title(album) test_mixins.edit_title(album)
test_mixins.edit_user_rating(album)
def test_audio_Album_mixins_tags(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_artist(track)
test_mixins.edit_track_number(track) test_mixins.edit_track_number(track)
test_mixins.edit_track_disc_number(track) test_mixins.edit_track_disc_number(track)
test_mixins.edit_user_rating(track)
def test_audio_Track_mixins_tags(track): def test_audio_Track_mixins_tags(track):

View file

@ -347,6 +347,7 @@ def test_Collection_mixins_fields(collection):
test_mixins.edit_sort_title(collection) test_mixins.edit_sort_title(collection)
test_mixins.edit_summary(collection) test_mixins.edit_summary(collection)
test_mixins.edit_title(collection) test_mixins.edit_title(collection)
test_mixins.edit_user_rating(collection)
def test_Collection_mixins_tags(collection): def test_Collection_mixins_tags(collection):

View file

@ -844,3 +844,69 @@ def _do_test_library_search(library, obj, field, operator, searchValue):
assert obj not in results assert obj not in results
else: else:
assert obj in results 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")

View file

@ -13,14 +13,16 @@ CUTE_CAT_SHA1 = "9f7003fc401761d8e0b0364d428b2dab2f789dbb"
AUDIO_STUB_SHA1 = "1abc20d5fdc904201bf8988ca6ef30f96bb73617" 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) edit_field_method = getattr(obj, "edit" + field_method)
_value = lambda: getattr(obj, attr) _value = lambda: getattr(obj, attr)
_fields = lambda: [f for f in obj.fields if f.name == attr] _fields = lambda: [f for f in obj.fields if f.name == attr]
# Check field does not match to begin with # Check field does not match to begin with
default_value = _value() default_value = default or _value()
if isinstance(default_value, datetime): if value:
test_value = value
elif isinstance(default_value, datetime):
test_value = TEST_MIXIN_DATE test_value = TEST_MIXIN_DATE
elif isinstance(default_value, int): elif isinstance(default_value, int):
test_value = default_value + 1 test_value = default_value + 1
@ -101,6 +103,10 @@ def edit_photo_captured_time(obj):
_test_mixins_field(obj, "originallyAvailableAt", "CapturedTime") _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): def _test_mixins_tag(obj, attr, tag_method):
add_tag_method = getattr(obj, "add" + tag_method) add_tag_method = getattr(obj, "add" + tag_method)
remove_tag_method = getattr(obj, "remove" + tag_method) remove_tag_method = getattr(obj, "remove" + tag_method)
@ -205,10 +211,6 @@ def lock_art(obj):
_test_mixins_lock_image(obj, "arts") _test_mixins_lock_image(obj, "arts")
def lock_banner(obj):
_test_mixins_lock_image(obj, "banners")
def lock_poster(obj): def lock_poster(obj):
_test_mixins_lock_image(obj, "posters") _test_mixins_lock_image(obj, "posters")
@ -273,10 +275,6 @@ def edit_art(obj):
_test_mixins_edit_image(obj, "arts") _test_mixins_edit_image(obj, "arts")
def edit_banner(obj):
_test_mixins_edit_image(obj, "banners")
def edit_poster(obj): def edit_poster(obj):
_test_mixins_edit_image(obj, "posters") _test_mixins_edit_image(obj, "posters")
@ -297,10 +295,6 @@ def attr_artUrl(obj):
_test_mixins_imageUrl(obj, "art") _test_mixins_imageUrl(obj, "art")
def attr_bannerUrl(obj):
_test_mixins_imageUrl(obj, "banner")
def attr_posterUrl(obj): def attr_posterUrl(obj):
_test_mixins_imageUrl(obj, "thumb") _test_mixins_imageUrl(obj, "thumb")

View file

@ -34,6 +34,7 @@ def test_photo_Photoalbum_mixins_fields(photoalbum):
test_mixins.edit_sort_title(photoalbum) test_mixins.edit_sort_title(photoalbum)
test_mixins.edit_summary(photoalbum) test_mixins.edit_summary(photoalbum)
test_mixins.edit_title(photoalbum) test_mixins.edit_title(photoalbum)
test_mixins.edit_user_rating(photoalbum)
def test_photo_Photoalbum_PlexWebURL(plex, 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_summary(photo)
test_mixins.edit_title(photo) test_mixins.edit_title(photo)
test_mixins.edit_photo_captured_time(photo) test_mixins.edit_photo_captured_time(photo)
test_mixins.edit_user_rating(photo)
def test_photo_Photo_mixins_tags(photo): def test_photo_Photo_mixins_tags(photo):

View file

@ -668,12 +668,13 @@ def test_video_Movie_mixins_fields(movie):
test_mixins.edit_summary(movie) test_mixins.edit_summary(movie)
test_mixins.edit_tagline(movie) test_mixins.edit_tagline(movie)
test_mixins.edit_title(movie) test_mixins.edit_title(movie)
test_mixins.edit_user_rating(movie)
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
test_mixins.edit_edition_title(movie) test_mixins.edit_edition_title(movie)
@pytest.mark.authenticated @pytest.mark.authenticated
def test_video_Movie_mixins_fields(movie): def test_video_Movie_mixins_fields_edition(movie):
test_mixins.edit_edition_title(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_summary(show)
test_mixins.edit_tagline(show) test_mixins.edit_tagline(show)
test_mixins.edit_title(show) test_mixins.edit_title(show)
test_mixins.edit_user_rating(show)
def test_video_Show_mixins_tags(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_added_at(season)
test_mixins.edit_summary(season) test_mixins.edit_summary(season)
test_mixins.edit_title(season) test_mixins.edit_title(season)
test_mixins.edit_user_rating(season)
def test_video_Season_mixins_tags(show): 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_sort_title(episode)
test_mixins.edit_summary(episode) test_mixins.edit_summary(episode)
test_mixins.edit_title(episode) test_mixins.edit_title(episode)
test_mixins.edit_user_rating(episode)
def test_video_Episode_mixins_tags(episode): def test_video_Episode_mixins_tags(episode):