From 68a95cc44d2e43bc9443aea33495b568a1de8766 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:25:50 -0700 Subject: [PATCH 1/6] Add support for Square Art --- plexapi/media.py | 5 ++++ plexapi/mixins.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++ plexapi/utils.py | 1 + plexapi/video.py | 10 ++++---- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index 718b5b7c6..945de4ead 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -1116,6 +1116,11 @@ class Poster(BaseResource): TAG = 'Photo' +class SquareArt(BaseResource): + """ Represents a single Square Art object. """ + TAG = 'Photo' + + class Theme(BaseResource): """ Represents a single Theme object. """ TAG = 'Track' diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 95f785fcc..83a777f91 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -520,6 +520,67 @@ def setPoster(self, poster): return self +class SquareArtUrlMixin: + """ Mixin for Plex objects that can have a square art url. """ + + @property + def squareArtUrl(self): + """ Return the square art url for the Plex object. """ + image = next((i for i in self.images if i.type == 'squareArt'), None) + return self._server.url(image.url, includeToken=True) if image else None + + +class SquareArtLockMixin: + """ Mixin for Plex objects that can have a locked square art. """ + + def lockSquareArt(self): + """ Lock the square art for a Plex object. """ + return self._edit(**{'squareArt.locked': 1}) + + def unlockSquareArt(self): + """ Unlock the square art for a Plex object. """ + return self._edit(**{'squareArt.locked': 0}) + + +class SquareArtMixin(SquareArtUrlMixin, SquareArtLockMixin): + """ Mixin for Plex objects that can have square art. """ + + def squareArts(self): + """ Returns list of available :class:`~plexapi.media.SquareArt` objects. """ + return self.fetchItems(f'/library/metadata/{self.ratingKey}/squareArts', cls=media.SquareArt) + + def uploadSquareArt(self, url=None, filepath=None): + """ Upload a square art 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}/squareArts?url={quote_plus(url)}' + self._server.query(key, method=self._server._session.post) + elif filepath: + key = f'/library/metadata/{self.ratingKey}/squareArts' + data = openOrRead(filepath) + self._server.query(key, method=self._server._session.post, data=data) + return self + + def setSquareArt(self, squareArt): + """ Set the square art for a Plex object. + + Parameters: + squareArt (:class:`~plexapi.media.SquareArt`): The square art object to select. + """ + squareArt.select() + return self + + def deleteSquareArt(self): + """ Delete the square art from a Plex object. """ + key = f'/library/metadata/{self.ratingKey}/squareArt' + self._server.query(key, method=self._server._session.delete) + return self + + class ThemeUrlMixin: """ Mixin for Plex objects that can have a theme url. """ diff --git a/plexapi/utils.py b/plexapi/utils.py index bbff6a8e0..06ad1bda6 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -92,6 +92,7 @@ 'network': 319, 'showOrdering': 322, 'clearLogo': 323, + 'squareArt': 325, 'place': 400, } REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()} diff --git a/plexapi/video.py b/plexapi/video.py index 597bbca7f..cdebaa94c 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -8,7 +8,7 @@ from plexapi.exceptions import BadRequest from plexapi.mixins import ( AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, - ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin, + ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, ThemeMixin, MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins, WatchlistMixin ) @@ -338,7 +338,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F class Movie( Video, Playable, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, ThemeMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, MovieEditMixins, WatchlistMixin ): @@ -545,7 +545,7 @@ def metadataDirectory(self): class Show( Video, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, ThemeMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin, ShowEditMixins, WatchlistMixin ): @@ -792,7 +792,7 @@ def metadataDirectory(self): class Season( Video, AdvancedSettingsMixin, ExtrasMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, SeasonEditMixins ): """ Represents a single Season. @@ -974,7 +974,7 @@ def metadataDirectory(self): class Episode( Video, Playable, ExtrasMixin, RatingMixin, - ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin, + ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, EpisodeEditMixins ): """ Represents a single Episode. From 26e42272ac72548477ee66ba0aaf1ef31fa0329d Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:05:05 -0700 Subject: [PATCH 2/6] Add tests for square art --- tests/test_mixins.py | 19 +++++++++++++++++++ tests/test_video.py | 11 +++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index be7409cf6..c507edb6f 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -223,11 +223,16 @@ def lock_poster(obj): _test_mixins_lock_image(obj, "posters") +def lock_square_art(obj): + _test_mixins_lock_image(obj, "squareArts") + + def _test_mixins_edit_image(obj, attr): cap_attr = attr[:-1].capitalize() 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) @@ -287,6 +298,10 @@ def edit_poster(obj): _test_mixins_edit_image(obj, "posters") +def edit_square_art(obj): + _test_mixins_edit_image(obj, "squareArts") + + def _test_mixins_imageUrl(obj, attr): url = getattr(obj, attr + "Url") if getattr(obj, attr): @@ -307,6 +322,10 @@ def attr_posterUrl(obj): _test_mixins_imageUrl(obj, "thumb") +def attr_squareArtUrl(obj): + _test_mixins_imageUrl(obj, "squareArt") + + def _test_mixins_edit_theme(obj): _fields = lambda: [f.name for f in obj.fields] diff --git a/tests/test_video.py b/tests/test_video.py index 2b10ff0b6..b55affe25 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -695,8 +695,10 @@ def test_video_Movie_mixins_edit_advanced_settings(movie): def test_video_Movie_mixins_images(movie): test_mixins.lock_art(movie) test_mixins.lock_poster(movie) + test_mixins.lock_square_art(movie) test_mixins.edit_art(movie) test_mixins.edit_poster(movie) + test_mixins.edit_square_art(movie) def test_video_Movie_mixins_themes(movie): @@ -962,10 +964,13 @@ def test_video_Show_mixins_edit_advanced_settings(show): def test_video_Show_mixins_images(show): test_mixins.lock_art(show) test_mixins.lock_poster(show) + test_mixins.lock_square_art(show) test_mixins.edit_art(show) test_mixins.edit_poster(show) + test_mixins.edit_square_art(show) test_mixins.attr_artUrl(show) test_mixins.attr_posterUrl(show) + test_mixins.attr_squareArtUrl(show) def test_video_Show_mixins_themes(show): @@ -1119,10 +1124,13 @@ def test_video_Season_mixins_images(show): season = show.season(season=1) test_mixins.lock_art(season) test_mixins.lock_poster(season) + test_mixins.lock_square_art(season) test_mixins.edit_art(season) test_mixins.edit_poster(season) + test_mixins.edit_square_art(season) test_mixins.attr_artUrl(season) test_mixins.attr_posterUrl(season) + test_mixins.attr_squareArtUrl(season) def test_video_Season_mixins_themes(show): @@ -1337,10 +1345,13 @@ def test_video_Episode_unwatched(tvshows): def test_video_Episode_mixins_images(episode): test_mixins.lock_art(episode) test_mixins.lock_poster(episode) + test_mixins.lock_square_art(episode) # test_mixins.edit_art(episode) # Uploading episode artwork is broken in Plex test_mixins.edit_poster(episode) + test_mixins.edit_square_art(episode) test_mixins.attr_artUrl(episode) test_mixins.attr_posterUrl(episode) + test_mixins.attr_squareArtUrl(episode) def test_video_Episode_mixins_themes(episode): From da6d65eaceb572c3c595fb8438a73c84558c5886 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:53:01 -0700 Subject: [PATCH 3/6] Add square art to artist/album/track, collection, photoalbum/photo, playlist --- plexapi/audio.py | 8 ++++---- plexapi/collection.py | 4 ++-- plexapi/photo.py | 6 +++--- plexapi/playlist.py | 4 ++-- plexapi/video.py | 5 +++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 3bc6f514e..0b8b6d4ab 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -12,7 +12,7 @@ from plexapi.exceptions import BadRequest from plexapi.mixins import ( AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, - ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin, + ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, ThemeMixin, ThemeUrlMixin, ArtistEditMixins, AlbumEditMixins, TrackEditMixins ) from plexapi.playlist import Playlist @@ -181,7 +181,7 @@ def sonicallySimilar( class Artist( Audio, AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, - ArtMixin, PosterMixin, ThemeMixin, + ArtMixin, PosterMixin, SquareArtMixin, ThemeMixin, ArtistEditMixins ): """ Represents a single Artist. @@ -351,7 +351,7 @@ def metadataDirectory(self): class Album( Audio, SplitMergeMixin, UnmatchMatchMixin, RatingMixin, - ArtMixin, PosterMixin, ThemeUrlMixin, + ArtMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, AlbumEditMixins ): """ Represents a single Album. @@ -504,7 +504,7 @@ def metadataDirectory(self): class Track( Audio, Playable, ExtrasMixin, RatingMixin, - ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin, + ArtUrlMixin, PosterUrlMixin, SquareArtUrlMixin, ThemeUrlMixin, TrackEditMixins ): """ Represents a single Track. diff --git a/plexapi/collection.py b/plexapi/collection.py index 308604a0b..b35514fc6 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -8,7 +8,7 @@ from plexapi.library import LibrarySection, ManagedHub from plexapi.mixins import ( AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, - ArtMixin, PosterMixin, ThemeMixin, + ArtMixin, PosterMixin, SquareArtMixin, ThemeMixin, CollectionEditMixins ) from plexapi.utils import deprecated @@ -18,7 +18,7 @@ class Collection( PlexPartialObject, AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin, - ArtMixin, PosterMixin, ThemeMixin, + ArtMixin, PosterMixin, SquareArtMixin, ThemeMixin, CollectionEditMixins ): """ Represents a single Collection. diff --git a/plexapi/photo.py b/plexapi/photo.py index e7c7239e8..478f33c93 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -8,7 +8,7 @@ from plexapi.exceptions import BadRequest from plexapi.mixins import ( RatingMixin, - ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, + ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, PhotoalbumEditMixins, PhotoEditMixins ) @@ -17,7 +17,7 @@ class Photoalbum( PlexPartialObject, RatingMixin, - ArtMixin, PosterMixin, + ArtMixin, PosterMixin, SquareArtMixin, PhotoalbumEditMixins ): """ Represents a single Photoalbum (collection of photos). @@ -159,7 +159,7 @@ def metadataDirectory(self): class Photo( PlexPartialObject, Playable, RatingMixin, - ArtUrlMixin, PosterUrlMixin, + ArtUrlMixin, PosterUrlMixin, SquareArtUrlMixin, PhotoEditMixins ): """ Represents a single Photo. diff --git a/plexapi/playlist.py b/plexapi/playlist.py index 0fc79bf50..7c05f1097 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -8,7 +8,7 @@ from plexapi.base import Playable, PlexPartialObject, cached_data_property from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.library import LibrarySection, MusicSection -from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin, PlaylistEditMixins +from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin, SquareArtMixin, PlaylistEditMixins from plexapi.utils import deprecated @@ -16,7 +16,7 @@ class Playlist( PlexPartialObject, Playable, SmartFilterMixin, - ArtMixin, PosterMixin, + ArtMixin, PosterMixin, SquareArtMixin, PlaylistEditMixins ): """ Represents a single Playlist. diff --git a/plexapi/video.py b/plexapi/video.py index cdebaa94c..02fed7648 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -8,7 +8,8 @@ from plexapi.exceptions import BadRequest from plexapi.mixins import ( AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, - ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin, ThemeMixin, + ArtUrlMixin, ArtMixin, LogoMixin, LogoUrlMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, + ThemeUrlMixin, ThemeMixin, MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins, WatchlistMixin ) @@ -1250,7 +1251,7 @@ def metadataDirectory(self): @utils.registerPlexObject class Clip( Video, Playable, - ArtUrlMixin, PosterUrlMixin + ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin ): """ Represents a single Clip. From 90aa3a538d8c8747cf63b0e6cd9a620f447f4d62 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:55:14 -0700 Subject: [PATCH 4/6] Add square art tests --- tests/test_audio.py | 7 +++++++ tests/test_collection.py | 3 +++ tests/test_photo.py | 2 ++ tests/test_playlist.py | 2 ++ 4 files changed, 14 insertions(+) diff --git a/tests/test_audio.py b/tests/test_audio.py index 43f941d36..8560b149a 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -104,10 +104,13 @@ def test_audio_Artist_mixins_edit_advanced_settings(artist): def test_audio_Artist_mixins_images(artist): test_mixins.lock_art(artist) test_mixins.lock_poster(artist) + test_mixins.lock_square_art(artist) test_mixins.edit_art(artist) test_mixins.edit_poster(artist) + test_mixins.edit_square_art(artist) test_mixins.attr_artUrl(artist) test_mixins.attr_posterUrl(artist) + test_mixins.attr_squareArtUrl(artist) def test_audio_Artist_mixins_themes(artist): @@ -234,10 +237,13 @@ def test_audio_Album_artist(album): def test_audio_Album_mixins_images(album): test_mixins.lock_art(album) test_mixins.lock_poster(album) + test_mixins.lock_square_art(album) test_mixins.edit_art(album) test_mixins.edit_poster(album) + test_mixins.edit_square_art(album) test_mixins.attr_artUrl(album) test_mixins.attr_posterUrl(album) + test_mixins.attr_squareArtUrl(album) def test_audio_Album_mixins_themes(album): @@ -425,6 +431,7 @@ def test_audio_Track_sonicAdventure(account_plexpass, music): def test_audio_Track_mixins_images(track): test_mixins.attr_artUrl(track) test_mixins.attr_posterUrl(track) + test_mixins.attr_squareArtUrl(track) def test_audio_Track_mixins_themes(track): diff --git a/tests/test_collection.py b/tests/test_collection.py index 7522e9430..11e7daf0e 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -353,10 +353,13 @@ def test_Collection_art(collection): def test_Collection_mixins_images(collection): test_mixins.lock_art(collection) test_mixins.lock_poster(collection) + test_mixins.lock_square_art(collection) test_mixins.edit_art(collection) test_mixins.edit_poster(collection) + test_mixins.edit_square_art(collection) test_mixins.attr_artUrl(collection) test_mixins.attr_posterUrl(collection) + test_mixins.attr_squareArtUrl(collection) def test_Collection_mixins_themes(collection): diff --git a/tests/test_photo.py b/tests/test_photo.py index b1613bc44..3c4449ae6 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -21,8 +21,10 @@ def test_photo_Photoalbum_mixins_images(photoalbum): # test_mixins.lock_poster(photoalbum) # Unlocking photoalbum poster is broken in Plex test_mixins.edit_art(photoalbum) test_mixins.edit_poster(photoalbum) + test_mixins.lock_square_art(photoalbum) test_mixins.attr_artUrl(photoalbum) test_mixins.attr_posterUrl(photoalbum) + test_mixins.attr_squareArtUrl(photoalbum) def test_photo_Photoalbum_mixins_rating(photoalbum): diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 564176e37..a40e8094d 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -328,8 +328,10 @@ def test_Playlist_PlexWebURL(plex, show): def test_Playlist_mixins_images(playlist): test_mixins.lock_art(playlist) test_mixins.lock_poster(playlist) + test_mixins.lock_square_art(playlist) test_mixins.edit_art(playlist) test_mixins.edit_poster(playlist) + test_mixins.edit_square_art(playlist) def test_Playlist_mixins_fields(playlist): From 235232f2ab28f850932cf1a84fc7fbbc115a038d Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:10:58 -0700 Subject: [PATCH 5/6] Add sqaureArt property --- plexapi/mixins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 83a777f91..f23180e0d 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -523,11 +523,15 @@ def setPoster(self, poster): class SquareArtUrlMixin: """ Mixin for Plex objects that can have a square art url. """ + @property + def squareArt(self): + """ Return the API path to the square art image. """ + return next((i.url for i in self.images if i.type == 'backgroundSquare'), None) + @property def squareArtUrl(self): """ Return the square art url for the Plex object. """ - image = next((i for i in self.images if i.type == 'squareArt'), None) - return self._server.url(image.url, includeToken=True) if image else None + return self._server.url(self.squareArt, includeToken=True) if self.squareArt else None class SquareArtLockMixin: From fa43351eaf4c6cea0a5d3e5ead576c887a1314a0 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:53:52 -0700 Subject: [PATCH 6/6] Fix docstring typos --- plexapi/mixins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plexapi/mixins.py b/plexapi/mixins.py index f23180e0d..37a0449ec 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -382,7 +382,7 @@ def uploadArt(self, url=None, filepath=None): 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. + filepath (str): The full file path to the image to upload or file-like object. """ if url: key = f'/library/metadata/{self.ratingKey}/arts?url={quote_plus(url)}' @@ -437,7 +437,7 @@ def uploadLogo(self, url=None, filepath=None): 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. + filepath (str): The full file path to the image to upload or file-like object. """ if url: key = f'/library/metadata/{self.ratingKey}/clearLogos?url={quote_plus(url)}' @@ -499,7 +499,7 @@ def uploadPoster(self, url=None, filepath=None): 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. + filepath (str): The full file path to the image to upload or file-like object. """ if url: key = f'/library/metadata/{self.ratingKey}/posters?url={quote_plus(url)}' @@ -558,7 +558,7 @@ def uploadSquareArt(self, url=None, filepath=None): 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. + filepath (str): The full file path to the image to upload or file-like object. """ if url: key = f'/library/metadata/{self.ratingKey}/squareArts?url={quote_plus(url)}'