Add methods to lock and unlock artwork and posters (#825)

* Add private _edit method

* Add fields attribute to playlists

* Add lock and unlock methods to art, banner, and poster mixins

* Add tests for locking and unlocking art and posters
This commit is contained in:
JonnyWong16 2021-09-12 17:56:21 -07:00 committed by GitHub
parent 168f1d331c
commit 692a23586c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 29 deletions

View file

@ -505,6 +505,17 @@ class PlexPartialObject(PlexObject):
""" Returns True if this is not a full object. """ """ Returns True if this is not a full object. """
return not self.isFullObject() return not self.isFullObject()
def _edit(self, **kwargs):
""" Actually edit an object. """
if 'id' not in kwargs:
kwargs['id'] = self.ratingKey
if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self.type)
part = '/library/sections/%s/all?%s' % (self.librarySectionID,
urlencode(kwargs))
self._server.query(part, method=self._server._session.put)
def edit(self, **kwargs): def edit(self, **kwargs):
""" Edit an object. """ Edit an object.
@ -517,14 +528,7 @@ class PlexPartialObject(PlexObject):
'collection[0].tag.tag': 'Super', 'collection[0].tag.tag': 'Super',
'collection.locked': 0} 'collection.locked': 0}
""" """
if 'id' not in kwargs: self._edit(**kwargs)
kwargs['id'] = self.ratingKey
if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self.type)
part = '/library/sections/%s/all?%s' % (self.librarySectionID,
urlencode(kwargs))
self._server.query(part, method=self._server._session.put)
def _edit_tags(self, tag, items, locked=True, remove=False): def _edit_tags(self, tag, items, locked=True, remove=False):
""" Helper to edit tags. """ Helper to edit tags.

View file

@ -97,6 +97,14 @@ class ArtMixin(ArtUrlMixin):
""" """
art.select() art.select()
def lockArt(self):
""" Lock the background artwork for a Plex object. """
self._edit(**{'art.locked': 1})
def unlockArt(self):
""" Unlock the background artwork for a Plex object. """
self._edit(**{'art.locked': 0})
class BannerUrlMixin(object): class BannerUrlMixin(object):
""" Mixin for Plex objects that can have a banner url. """ """ Mixin for Plex objects that can have a banner url. """
@ -138,6 +146,14 @@ class BannerMixin(BannerUrlMixin):
""" """
banner.select() banner.select()
def lockBanner(self):
""" Lock the banner for a Plex object. """
self._edit(**{'banner.locked': 1})
def unlockBanner(self):
""" Unlock the banner for a Plex object. """
self._edit(**{'banner.locked': 0})
class PosterUrlMixin(object): class PosterUrlMixin(object):
""" Mixin for Plex objects that can have a poster url. """ """ Mixin for Plex objects that can have a poster url. """
@ -184,6 +200,14 @@ class PosterMixin(PosterUrlMixin):
""" """
poster.select() poster.select()
def lockPoster(self):
""" Lock the poster for a Plex object. """
self._edit(**{'thumb.locked': 1})
def unlockPoster(self):
""" Unlock the poster for a Plex object. """
self._edit(**{'thumb.locked': 0})
class RatingMixin(object): class RatingMixin(object):
""" Mixin for Plex objects that can have user star ratings. """ """ Mixin for Plex objects that can have user star ratings. """

View file

@ -2,7 +2,7 @@
import re import re
from urllib.parse import quote_plus, unquote from urllib.parse import quote_plus, unquote
from plexapi import utils from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
@ -24,6 +24,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
content (str): The filter URI string for smart playlists. content (str): The filter URI string for smart playlists.
duration (int): Duration of the playlist in milliseconds. duration (int): Duration of the playlist in milliseconds.
durationInSeconds (int): Duration of the playlist in seconds. durationInSeconds (int): Duration of the playlist in seconds.
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the playlist (com.plexapp.agents.none://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX). guid (str): Plex GUID for the playlist (com.plexapp.agents.none://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
icon (str): Icon URI string for smart playlists. icon (str): Icon URI string for smart playlists.
key (str): API URL (/playlist/<ratingkey>). key (str): API URL (/playlist/<ratingkey>).
@ -48,8 +49,9 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
self.content = data.attrib.get('content') self.content = data.attrib.get('content')
self.duration = utils.cast(int, data.attrib.get('duration')) self.duration = utils.cast(int, data.attrib.get('duration'))
self.durationInSeconds = utils.cast(int, data.attrib.get('durationInSeconds')) self.durationInSeconds = utils.cast(int, data.attrib.get('durationInSeconds'))
self.icon = data.attrib.get('icon') self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid') self.guid = data.attrib.get('guid')
self.icon = data.attrib.get('icon')
self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50 self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50
self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
self.playlistType = data.attrib.get('playlistType') self.playlistType = data.attrib.get('playlistType')
@ -288,6 +290,11 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
})) }))
self._server.query(key, method=self._server._session.put) self._server.query(key, method=self._server._session.put)
def _edit(self, **kwargs):
""" Actually edit the playlist. """
key = '%s%s' % (self.key, utils.joinArgs(kwargs))
self._server.query(key, method=self._server._session.put)
def edit(self, title=None, summary=None): def edit(self, title=None, summary=None):
""" Edit the playlist. """ Edit the playlist.
@ -300,9 +307,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
args['title'] = title args['title'] = title
if summary: if summary:
args['summary'] = summary args['summary'] = summary
self._edit(**args)
key = '%s%s' % (self.key, utils.joinArgs(args))
self._server.query(key, method=self._server._session.put)
def delete(self): def delete(self):
""" Delete the playlist. """ """ Delete the playlist. """

View file

@ -74,6 +74,8 @@ def test_audio_Artist_mixins_edit_advanced_settings(artist):
def test_audio_Artist_mixins_images(artist): def test_audio_Artist_mixins_images(artist):
test_mixins.lock_art(artist)
test_mixins.lock_poster(artist)
test_mixins.edit_art(artist) test_mixins.edit_art(artist)
test_mixins.edit_poster(artist) test_mixins.edit_poster(artist)
test_mixins.attr_artUrl(artist) test_mixins.attr_artUrl(artist)
@ -169,6 +171,8 @@ def test_audio_Album_artist(album):
def test_audio_Album_mixins_images(album): def test_audio_Album_mixins_images(album):
test_mixins.lock_art(album)
test_mixins.lock_poster(album)
test_mixins.edit_art(album) test_mixins.edit_art(album)
test_mixins.edit_poster(album) test_mixins.edit_poster(album)
test_mixins.attr_artUrl(album) test_mixins.attr_artUrl(album)

View file

@ -271,6 +271,8 @@ def test_Collection_art(collection):
def test_Collection_mixins_images(collection): def test_Collection_mixins_images(collection):
test_mixins.lock_art(collection)
test_mixins.lock_poster(collection)
test_mixins.edit_art(collection) test_mixins.edit_art(collection)
test_mixins.edit_poster(collection) test_mixins.edit_poster(collection)
test_mixins.attr_artUrl(collection) test_mixins.attr_artUrl(collection)

View file

@ -78,7 +78,34 @@ def edit_writer(obj):
_test_mixins_tag(obj, "writers", "Writer") _test_mixins_tag(obj, "writers", "Writer")
def _test_mixins_image(obj, attr): def _test_mixins_lock_image(obj, attr):
cap_attr = attr[:-1].capitalize()
lock_img_method = getattr(obj, "lock" + cap_attr)
unlock_img_method = getattr(obj, "unlock" + cap_attr)
field = "thumb" if attr == 'posters' else attr[:-1]
_fields = lambda: [f.name for f in obj.fields]
assert field not in _fields()
lock_img_method()
obj.reload()
assert field in _fields()
unlock_img_method()
obj.reload()
assert field not in _fields()
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")
def _test_mixins_edit_image(obj, attr):
cap_attr = attr[:-1].capitalize() cap_attr = attr[:-1].capitalize()
get_img_method = getattr(obj, attr) get_img_method = getattr(obj, attr)
set_img_method = getattr(obj, "set" + cap_attr) set_img_method = getattr(obj, "set" + cap_attr)
@ -106,7 +133,7 @@ def _test_mixins_image(obj, attr):
images = get_img_method() images = get_img_method()
file_image = [ file_image = [
i for i in images i for i in images
if i.ratingKey.startswith('upload://') and i.ratingKey.endswith(CUTE_CAT_SHA1) if i.ratingKey.startswith("upload://") and i.ratingKey.endswith(CUTE_CAT_SHA1)
] ]
assert file_image assert file_image
# Reset to default image # Reset to default image
@ -115,39 +142,39 @@ def _test_mixins_image(obj, attr):
def edit_art(obj): def edit_art(obj):
_test_mixins_image(obj, 'arts') _test_mixins_edit_image(obj, "arts")
def edit_banner(obj): def edit_banner(obj):
_test_mixins_image(obj, 'banners') _test_mixins_edit_image(obj, "banners")
def edit_poster(obj): def edit_poster(obj):
_test_mixins_image(obj, 'posters') _test_mixins_edit_image(obj, "posters")
def _test_mixins_imageUrl(obj, attr): def _test_mixins_imageUrl(obj, attr):
url = getattr(obj, attr + 'Url') url = getattr(obj, attr + "Url")
if getattr(obj, attr): if getattr(obj, attr):
assert url.startswith(utils.SERVER_BASEURL) assert url.startswith(utils.SERVER_BASEURL)
assert "/library/metadata/" in url or "/library/collections/" in url assert "/library/metadata/" in url or "/library/collections/" in url
assert attr in url or "composite" in url assert attr in url or "composite" in url
if attr == 'thumb': if attr == "thumb":
assert getattr(obj, 'posterUrl') == url assert getattr(obj, "posterUrl") == url
else: else:
assert url is None assert url is None
def attr_artUrl(obj): def attr_artUrl(obj):
_test_mixins_imageUrl(obj, 'art') _test_mixins_imageUrl(obj, "art")
def attr_bannerUrl(obj): def attr_bannerUrl(obj):
_test_mixins_imageUrl(obj, 'banner') _test_mixins_imageUrl(obj, "banner")
def attr_posterUrl(obj): def attr_posterUrl(obj):
_test_mixins_imageUrl(obj, 'thumb') _test_mixins_imageUrl(obj, "thumb")
def _test_mixins_editAdvanced(obj): def _test_mixins_editAdvanced(obj):
@ -163,7 +190,7 @@ def _test_mixins_editAdvanced(obj):
def _test_mixins_editAdvanced_bad_pref(obj): def _test_mixins_editAdvanced_bad_pref(obj):
with pytest.raises(NotFound): with pytest.raises(NotFound):
assert obj.preference('bad-pref') assert obj.preference("bad-pref")
def _test_mixins_defaultAdvanced(obj): def _test_mixins_defaultAdvanced(obj):
@ -188,7 +215,7 @@ def edit_rating(obj):
obj.reload() obj.reload()
assert obj.userRating is None assert obj.userRating is None
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
assert obj.rate('bad-rating') assert obj.rate("bad-rating")
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
assert obj.rate(-1) assert obj.rate(-1)
with pytest.raises(BadRequest): with pytest.raises(BadRequest):

View file

@ -12,6 +12,8 @@ def test_photo_Photoalbum(photoalbum):
def test_photo_Photoalbum_mixins_images(photoalbum): def test_photo_Photoalbum_mixins_images(photoalbum):
# test_mixins.lock_art(photoalbum) # Unlocking photoalbum artwork is broken in Plex
# test_mixins.lock_poster(photoalbum) # Unlocking photoalbum poster is broken in Plex
test_mixins.edit_art(photoalbum) test_mixins.edit_art(photoalbum)
test_mixins.edit_poster(photoalbum) test_mixins.edit_poster(photoalbum)
test_mixins.attr_artUrl(photoalbum) test_mixins.attr_artUrl(photoalbum)

View file

@ -259,5 +259,7 @@ def test_Playlist_exceptions(plex, movies, movie, artist):
def test_Playlist_mixins_images(playlist): def test_Playlist_mixins_images(playlist):
# test_mixins.lock_art(playlist)
test_mixins.lock_poster(playlist)
# test_mixins.edit_art(playlist) # test_mixins.edit_art(playlist)
test_mixins.edit_poster(playlist) test_mixins.edit_poster(playlist)

View file

@ -45,6 +45,8 @@ def test_video_Movie_mixins_edit_advanced_settings(movie):
def test_video_Movie_mixins_images(movie): def test_video_Movie_mixins_images(movie):
test_mixins.lock_art(movie)
test_mixins.lock_poster(movie)
test_mixins.edit_art(movie) test_mixins.edit_art(movie)
test_mixins.edit_poster(movie) test_mixins.edit_poster(movie)
@ -776,6 +778,8 @@ def test_video_Show_mixins_edit_advanced_settings(show):
@pytest.mark.xfail(reason="Changing show art fails randomly") @pytest.mark.xfail(reason="Changing show art fails randomly")
def test_video_Show_mixins_images(show): def test_video_Show_mixins_images(show):
test_mixins.lock_art(show)
test_mixins.lock_poster(show)
test_mixins.edit_art(show) test_mixins.edit_art(show)
test_mixins.edit_poster(show) test_mixins.edit_poster(show)
test_mixins.attr_artUrl(show) test_mixins.attr_artUrl(show)
@ -896,6 +900,8 @@ def test_video_Season_episodes(show):
def test_video_Season_mixins_images(show): def test_video_Season_mixins_images(show):
season = show.season(season=1) season = show.season(season=1)
test_mixins.lock_art(season)
test_mixins.lock_poster(season)
test_mixins.edit_art(season) test_mixins.edit_art(season)
test_mixins.edit_poster(season) test_mixins.edit_poster(season)
test_mixins.attr_artUrl(season) test_mixins.attr_artUrl(season)
@ -1096,6 +1102,8 @@ def test_video_Episode_unwatched(tvshows):
def test_video_Episode_mixins_images(episode): def test_video_Episode_mixins_images(episode):
test_mixins.lock_art(episode)
test_mixins.lock_poster(episode)
# test_mixins.edit_art(episode) # Uploading episode artwork is broken in Plex # test_mixins.edit_art(episode) # Uploading episode artwork is broken in Plex
test_mixins.edit_poster(episode) test_mixins.edit_poster(episode)
test_mixins.attr_artUrl(episode) test_mixins.attr_artUrl(episode)