Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The import statement has become quite long and difficult to read. Consider grouping related mixins on separate lines for better readability. For example, URL mixins on one line and regular mixins on another.

Suggested change
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin, ThemeMixin, ThemeUrlMixin,
ArtMixin, PosterMixin, SquareArtMixin, ThemeMixin,
ArtUrlMixin, PosterUrlMixin, SquareArtUrlMixin, ThemeUrlMixin,

Copilot uses AI. Check for mistakes.
ArtistEditMixins, AlbumEditMixins, TrackEditMixins
)
from plexapi.playlist import Playlist
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions plexapi/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +18,7 @@
class Collection(
PlexPartialObject,
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ArtMixin, PosterMixin, SquareArtMixin, ThemeMixin,
CollectionEditMixins
):
""" Represents a single Collection.
Expand Down
5 changes: 5 additions & 0 deletions plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
71 changes: 68 additions & 3 deletions plexapi/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}'
Expand Down Expand Up @@ -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)}'
Expand Down Expand Up @@ -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)}'
Expand All @@ -520,6 +520,71 @@ def setPoster(self, poster):
return self


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. """
return self._server.url(self.squareArt, includeToken=True) if self.squareArt 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 to 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. """

Expand Down
6 changes: 3 additions & 3 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -17,7 +17,7 @@
class Photoalbum(
PlexPartialObject,
RatingMixin,
ArtMixin, PosterMixin,
ArtMixin, PosterMixin, SquareArtMixin,
PhotoalbumEditMixins
):
""" Represents a single Photoalbum (collection of photos).
Expand Down Expand Up @@ -159,7 +159,7 @@ def metadataDirectory(self):
class Photo(
PlexPartialObject, Playable,
RatingMixin,
ArtUrlMixin, PosterUrlMixin,
ArtUrlMixin, PosterUrlMixin, SquareArtUrlMixin,
PhotoEditMixins
):
""" Represents a single Photo.
Expand Down
4 changes: 2 additions & 2 deletions plexapi/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
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


@utils.registerPlexObject
class Playlist(
PlexPartialObject, Playable,
SmartFilterMixin,
ArtMixin, PosterMixin,
ArtMixin, PosterMixin, SquareArtMixin,
PlaylistEditMixins
):
""" Represents a single Playlist.
Expand Down
1 change: 1 addition & 0 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
'network': 319,
'showOrdering': 322,
'clearLogo': 323,
'squareArt': 325,
'place': 400,
}
REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}
Expand Down
13 changes: 7 additions & 6 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ThemeUrlMixin, ThemeMixin,
ArtUrlMixin, ArtMixin, LogoMixin, LogoUrlMixin, PosterUrlMixin, PosterMixin, SquareArtMixin, SquareArtUrlMixin,
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The import statement has become quite long and difficult to read. Consider grouping related mixins on separate lines for better readability. For example, URL mixins on one line and regular mixins on another.

Copilot uses AI. Check for mistakes.
ThemeUrlMixin, ThemeMixin,
MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,
WatchlistMixin
)
Expand Down Expand Up @@ -338,7 +339,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
):
Expand Down Expand Up @@ -545,7 +546,7 @@ def metadataDirectory(self):
class Show(
Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeMixin,
ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeMixin,
ShowEditMixins,
WatchlistMixin
):
Expand Down Expand Up @@ -792,7 +793,7 @@ def metadataDirectory(self):
class Season(
Video,
AdvancedSettingsMixin, ExtrasMixin, RatingMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin,
ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin,
SeasonEditMixins
):
""" Represents a single Season.
Expand Down Expand Up @@ -974,7 +975,7 @@ def metadataDirectory(self):
class Episode(
Video, Playable,
ExtrasMixin, RatingMixin,
ArtMixin, LogoMixin, PosterMixin, ThemeUrlMixin,
ArtMixin, LogoMixin, PosterMixin, SquareArtMixin, ThemeUrlMixin,
EpisodeEditMixins
):
""" Represents a single Episode.
Expand Down Expand Up @@ -1250,7 +1251,7 @@ def metadataDirectory(self):
@utils.registerPlexObject
class Clip(
Video, Playable,
ArtUrlMixin, PosterUrlMixin
ArtUrlMixin, LogoUrlMixin, PosterUrlMixin, SquareArtUrlMixin
):
""" Represents a single Clip.

Expand Down
7 changes: 7 additions & 0 deletions tests/test_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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]

Expand Down
2 changes: 2 additions & 0 deletions tests/test_photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions tests/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading