From e884085b592e6d423e3e98726f01e3c81d71f117 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:37:23 -0700 Subject: [PATCH 1/4] Add ability to select, lock, and unlock logos --- plexapi/mixins.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 95f785fcc..39f1e6236 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -418,11 +418,11 @@ class LogoLockMixin: def lockLogo(self): """ Lock the logo for a Plex object. """ - raise NotImplementedError('Logo cannot be locked through the API.') + return self._edit(**{'clearLogo.locked': 1}) def unlockLogo(self): """ Unlock the logo for a Plex object. """ - raise NotImplementedError('Logo cannot be unlocked through the API.') + return self._edit(**{'clearLogo.locked': 0}) class LogoMixin(LogoUrlMixin, LogoLockMixin): @@ -451,13 +451,13 @@ def uploadLogo(self, url=None, filepath=None): def setLogo(self, logo): """ Set the logo for a Plex object. - Raises: - :exc:`~plexapi.exceptions.NotImplementedError`: Logo cannot be set through the API. + Parameters: + logo (:class:`~plexapi.media.Logo`): The logo object to select. """ - raise NotImplementedError( - 'Logo cannot be set through the API. ' - 'Re-upload the logo using "uploadLogo" to set it.' - ) + logo.select() + return self + + def deleteLogo(self): class PosterUrlMixin: From fdaf291eaeda9de8f09f0357aeb9062bb11fece9 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:37:37 -0700 Subject: [PATCH 2/4] Add ability to delete resources --- plexapi/mixins.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 39f1e6236..16616f5d7 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -402,6 +402,12 @@ def setArt(self, art): art.select() return self + def deleteArt(self): + """ Delete the art from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/art' + self._server.query(key, method=self._server._session.delete) + return self + class LogoUrlMixin: """ Mixin for Plex objects that can have a logo url. """ @@ -458,6 +464,10 @@ def setLogo(self, logo): return self def deleteLogo(self): + """ Delete the logo from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/clearLogo' + self._server.query(key, method=self._server._session.delete) + return self class PosterUrlMixin: @@ -519,6 +529,12 @@ def setPoster(self, poster): poster.select() return self + def deletePoster(self): + """ Delete the poster from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/thumb' + self._server.query(key, method=self._server._session.delete) + return self + class ThemeUrlMixin: """ Mixin for Plex objects that can have a theme url. """ @@ -580,6 +596,12 @@ def setTheme(self, theme): 'Re-upload the theme using "uploadTheme" to set it.' ) + def deleteTheme(self): + """ Delete the theme from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/theme' + self._server.query(key, method=self._server._session.delete) + return self + class EditFieldMixin: """ Mixin for editing Plex object fields. """ From 759e75e80a568029825b8e99851a8562fb286dd5 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:59:55 -0700 Subject: [PATCH 3/4] Add tests for logos and deleting --- tests/test_mixins.py | 23 +++++++++++++++++++++++ tests/test_video.py | 10 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index be7409cf6..1fea58ca2 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -219,6 +219,10 @@ def lock_art(obj): _test_mixins_lock_image(obj, "arts") +def lock_logo(obj): + _test_mixins_lock_image(obj, "logos") + + def lock_poster(obj): _test_mixins_lock_image(obj, "posters") @@ -228,6 +232,7 @@ def _test_mixins_edit_image(obj, attr): get_img_method = getattr(obj, attr) set_img_method = getattr(obj, "set" + cap_attr) upload_img_method = getattr(obj, "upload" + cap_attr) + delete_img_method = getattr(obj, "delete" + cap_attr) images = get_img_method() if images: default_image = images[0] @@ -270,6 +275,12 @@ def _test_mixins_edit_image(obj, attr): ] assert file_image + # Test delete image + delete_img_method() + images = get_img_method() + selected_image = next((i for i in images if i.selected), None) + assert selected_image is None + # Reset to default image if default_image: set_img_method(default_image) @@ -283,6 +294,10 @@ def edit_art(obj): _test_mixins_edit_image(obj, "arts") +def edit_logo(obj): + _test_mixins_edit_image(obj, "logos") + + def edit_poster(obj): _test_mixins_edit_image(obj, "posters") @@ -330,9 +345,17 @@ def _test_mixins_edit_theme(obj): obj.lockTheme() obj.reload() assert "theme" in _fields() + + # Set the theme with pytest.raises(NotImplementedError): obj.setTheme(themes[0]) + # Delete the theme + obj.deleteTheme() + obj.reload() + selected_theme = next((t for t in obj.themes() if t.selected), None) + assert selected_theme is None + def edit_theme(obj): _test_mixins_edit_theme(obj) diff --git a/tests/test_video.py b/tests/test_video.py index 2b10ff0b6..f65f1f9a7 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -694,8 +694,10 @@ def test_video_Movie_mixins_edit_advanced_settings(movie): @pytest.mark.xfail(reason="Changing images fails randomly") def test_video_Movie_mixins_images(movie): test_mixins.lock_art(movie) + test_mixins.lock_logo(movie) test_mixins.lock_poster(movie) test_mixins.edit_art(movie) + test_mixins.edit_logo(movie) test_mixins.edit_poster(movie) @@ -961,8 +963,10 @@ def test_video_Show_mixins_edit_advanced_settings(show): @pytest.mark.xfail(reason="Changing images fails randomly") def test_video_Show_mixins_images(show): test_mixins.lock_art(show) + test_mixins.lock_logo(show) test_mixins.lock_poster(show) test_mixins.edit_art(show) + test_mixins.edit_logo(show) test_mixins.edit_poster(show) test_mixins.attr_artUrl(show) test_mixins.attr_posterUrl(show) @@ -1118,8 +1122,10 @@ def test_video_Season_episodes(show): def test_video_Season_mixins_images(show): season = show.season(season=1) test_mixins.lock_art(season) + test_mixins.lock_logo(season) test_mixins.lock_poster(season) test_mixins.edit_art(season) + test_mixins.edit_logo(season) test_mixins.edit_poster(season) test_mixins.attr_artUrl(season) test_mixins.attr_posterUrl(season) @@ -1336,8 +1342,10 @@ def test_video_Episode_unwatched(tvshows): @pytest.mark.xfail(reason="Changing images fails randomly") def test_video_Episode_mixins_images(episode): test_mixins.lock_art(episode) + test_mixins.lock_logo(episode) test_mixins.lock_poster(episode) - # test_mixins.edit_art(episode) # Uploading episode artwork is broken in Plex + test_mixins.edit_art(episode) + test_mixins.edit_logo(episode) test_mixins.edit_poster(episode) test_mixins.attr_artUrl(episode) test_mixins.attr_posterUrl(episode) From 1214bc0dd939694bfb3d6698a799fd638694704b Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:09:25 +0000 Subject: [PATCH 4/4] Add tests for music, photos, collections, and playlists --- tests/test_audio.py | 4 ++++ tests/test_collection.py | 2 ++ tests/test_photo.py | 2 ++ tests/test_playlist.py | 2 ++ 4 files changed, 10 insertions(+) diff --git a/tests/test_audio.py b/tests/test_audio.py index 91be580ec..60510d31b 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -103,9 +103,11 @@ def test_audio_Artist_mixins_edit_advanced_settings(artist): @pytest.mark.xfail(reason="Changing images fails randomly") def test_audio_Artist_mixins_images(artist): test_mixins.lock_art(artist) + test_mixins.lock_logo(artist) test_mixins.lock_poster(artist) test_mixins.lock_squareArt(artist) test_mixins.edit_art(artist) + test_mixins.edit_logo(artist) test_mixins.edit_poster(artist) test_mixins.edit_squareArt(artist) test_mixins.attr_artUrl(artist) @@ -237,9 +239,11 @@ def test_audio_Album_artist(album): @pytest.mark.xfail(reason="Changing images fails randomly") def test_audio_Album_mixins_images(album): test_mixins.lock_art(album) + test_mixins.lock_logo(album) test_mixins.lock_poster(album) test_mixins.lock_squareArt(album) test_mixins.edit_art(album) + test_mixins.edit_logo(album) test_mixins.edit_poster(album) test_mixins.edit_squareArt(album) test_mixins.attr_artUrl(album) diff --git a/tests/test_collection.py b/tests/test_collection.py index 03632530c..0b18179a1 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -352,9 +352,11 @@ def test_Collection_art(collection): @pytest.mark.xfail(reason="Changing images fails randomly") def test_Collection_mixins_images(collection): test_mixins.lock_art(collection) + test_mixins.lock_logo(collection) test_mixins.lock_poster(collection) test_mixins.lock_squareArt(collection) test_mixins.edit_art(collection) + test_mixins.edit_logo(collection) test_mixins.edit_poster(collection) test_mixins.edit_squareArt(collection) test_mixins.attr_artUrl(collection) diff --git a/tests/test_photo.py b/tests/test_photo.py index 96fa7b389..487c3f91a 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -18,9 +18,11 @@ def test_photo_Photoalbum(photoalbum): @pytest.mark.xfail(reason="Changing images fails randomly") def test_photo_Photoalbum_mixins_images(photoalbum): test_mixins.edit_art(photoalbum) + test_mixins.edit_logo(photoalbum) test_mixins.edit_poster(photoalbum) test_mixins.edit_squareArt(photoalbum) test_mixins.lock_art(photoalbum) + test_mixins.lock_logo(photoalbum) test_mixins.lock_poster(photoalbum) test_mixins.lock_squareArt(photoalbum) test_mixins.attr_artUrl(photoalbum) diff --git a/tests/test_playlist.py b/tests/test_playlist.py index f1c2f5f13..6e608d022 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -327,9 +327,11 @@ def test_Playlist_PlexWebURL(plex, show): @pytest.mark.xfail(reason="Changing images fails randomly") def test_Playlist_mixins_images(playlist): test_mixins.lock_art(playlist) + test_mixins.lock_logo(playlist) test_mixins.lock_poster(playlist) test_mixins.lock_squareArt(playlist) test_mixins.edit_art(playlist) + test_mixins.edit_logo(playlist) test_mixins.edit_poster(playlist) test_mixins.edit_squareArt(playlist) test_mixins.attr_artUrl(playlist)