From 2142514955883323d4c09425f6218221c334321f Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sat, 18 May 2024 12:16:49 -0700 Subject: [PATCH] Add support for editing audience ratings and critic ratings (#1417) * Add support for editing audience ratings and critic ratings * Note: Not all types of ratings are supported/shown in the Plex UIs. However they can be edited/updated and stored in the Plex Media Server database. * Add tests for editing audience rating and critic rating --- plexapi/audio.py | 10 ++++++++ plexapi/collection.py | 4 ++++ plexapi/mixins.py | 52 +++++++++++++++++++++++++++++++++------- plexapi/video.py | 4 ++++ tests/test_audio.py | 6 +++++ tests/test_collection.py | 2 ++ tests/test_mixins.py | 10 +++++++- tests/test_video.py | 8 +++++++ 8 files changed, 86 insertions(+), 10 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 0eb397cc..8f84f3be 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -181,6 +181,7 @@ class Artist( TYPE (str): 'artist' albumSort (int): Setting that indicates how albums are sorted for the artist (-1 = Library default, 0 = Newest first, 1 = Oldest first, 2 = By name). + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. countries (List<:class:`~plexapi.media.Country`>): List country objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -188,6 +189,7 @@ class Artist( key (str): API URL (/library/metadata/). labels (List<:class:`~plexapi.media.Label`>): List of label objects. locations (List): List of folder paths where the artist is found on disk. + rating (float): Artist rating (7.9; 9.8; 8.1). similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. theme (str): URL to theme resource (/library/metadata//theme/). @@ -199,6 +201,7 @@ class Artist( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) self.albumSort = utils.cast(int, data.attrib.get('albumSort', '-1')) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.countries = self.findItems(data, media.Country) self.genres = self.findItems(data, media.Genre) @@ -206,6 +209,7 @@ class Artist( self.key = self.key.replace('/children', '') # FIX_BUG_50 self.labels = self.findItems(data, media.Label) self.locations = self.listAttrs(data, 'path', etag='Location') + self.rating = utils.cast(float, data.attrib.get('rating')) self.similar = self.findItems(data, media.Similar) self.styles = self.findItems(data, media.Style) self.theme = data.attrib.get('theme') @@ -301,6 +305,7 @@ class Album( Attributes: TAG (str): 'Directory' TYPE (str): 'album' + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. formats (List<:class:`~plexapi.media.Format`>): List of format objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -329,6 +334,7 @@ class Album( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.formats = self.findItems(data, media.Format) self.genres = self.findItems(data, media.Genre) @@ -426,6 +432,7 @@ class Track( Attributes: TAG (str): 'Directory' TYPE (str): 'track' + audienceRating (float): Audience rating. chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. chapterSource (str): Unknown collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. @@ -451,6 +458,7 @@ class Track( parentThumb (str): URL to album thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the album for the track. primaryExtraKey (str) API URL for the primary extra for the track. + rating (float): Track rating (7.9; 9.8; 8.1). ratingCount (int): Number of listeners who have scrobbled this track, as reported by Last.fm. skipCount (int): Number of times the track has been skipped. sourceURI (str): Remote server URI (server:///com.plexapp.plugins.library) @@ -465,6 +473,7 @@ class Track( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) Playable._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.chapters = self.findItems(data, media.Chapter) self.chapterSource = data.attrib.get('chapterSource') self.collections = self.findItems(data, media.Collection) @@ -488,6 +497,7 @@ class Track( self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.primaryExtraKey = data.attrib.get('primaryExtraKey') + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.skipCount = utils.cast(int, data.attrib.get('skipCount')) self.sourceURI = data.attrib.get('source') # remote playlist item diff --git a/plexapi/collection.py b/plexapi/collection.py index 5f591a4a..d71ddf2f 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -29,6 +29,7 @@ class Collection( addedAt (datetime): Datetime the collection was added to the library. art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. + audienceRating (float): Audience rating. childCount (int): Number of items in the collection. collectionFilterBasedOnUser (int): Which user's activity is used for the collection filtering. collectionMode (int): How the items in the collection are displayed. @@ -47,6 +48,7 @@ class Collection( librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. maxYear (int): Maximum year for the items in the collection. minYear (int): Minimum year for the items in the collection. + rating (float): Collection rating (7.9; 9.8; 8.1). ratingCount (int): The number of ratings. ratingKey (int): Unique key identifying the collection. smart (bool): True if the collection is a smart collection. @@ -69,6 +71,7 @@ class Collection( self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.childCount = utils.cast(int, data.attrib.get('childCount')) self.collectionFilterBasedOnUser = utils.cast(int, data.attrib.get('collectionFilterBasedOnUser', '0')) self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1')) @@ -87,6 +90,7 @@ class Collection( self.librarySectionTitle = data.attrib.get('librarySectionTitle') self.maxYear = utils.cast(int, data.attrib.get('maxYear')) self.minYear = utils.cast(int, data.attrib.get('minYear')) + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.smart = utils.cast(bool, data.attrib.get('smart', '0')) diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 60c24e26..8571ba63 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -567,6 +567,19 @@ class AddedAtMixin(EditFieldMixin): return self.editField('addedAt', addedAt, locked=locked) +class AudienceRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an audience rating. """ + + def editAudienceRating(self, audienceRating, locked=True): + """ Edit the audience rating. + + Parameters: + audienceRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('audienceRating', audienceRating, locked=locked) + + class ContentRatingMixin(EditFieldMixin): """ Mixin for Plex objects that can have a content rating. """ @@ -580,6 +593,19 @@ class ContentRatingMixin(EditFieldMixin): return self.editField('contentRating', contentRating, locked=locked) +class CriticRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a critic rating. """ + + def editCriticRating(self, criticRating, locked=True): + """ Edit the critic rating. + + Parameters: + criticRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('rating', criticRating, locked=locked) + + class EditionTitleMixin(EditFieldMixin): """ Mixin for Plex objects that can have an edition title. """ @@ -751,7 +777,7 @@ class UserRatingMixin(EditFieldMixin): """ Edit the user rating. Parameters: - userRating (int): The new value. + userRating (float): The new value. locked (bool): True (default) to lock the field, False to unlock the field. """ return self.editField('userRating', userRating, locked=locked) @@ -1145,7 +1171,8 @@ class WatchlistMixin: class MovieEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, EditionTitleMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin ): @@ -1154,7 +1181,8 @@ class MovieEditMixins( class ShowEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, ): @@ -1163,7 +1191,8 @@ class ShowEditMixins( class SeasonEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, LabelMixin ): pass @@ -1171,7 +1200,8 @@ class SeasonEditMixins( class EpisodeEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, DirectorMixin, LabelMixin, WriterMixin ): pass @@ -1179,7 +1209,8 @@ class EpisodeEditMixins( class ArtistEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin ): pass @@ -1187,7 +1218,8 @@ class ArtistEditMixins( class AlbumEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin ): pass @@ -1195,7 +1227,8 @@ class AlbumEditMixins( class TrackEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin ): pass @@ -1218,7 +1251,8 @@ class PhotoEditMixins( class CollectionEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, LabelMixin ): pass diff --git a/plexapi/video.py b/plexapi/video.py index 727ba0f8..609f57f6 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -713,6 +713,7 @@ class Season( Attributes: TAG (str): 'Directory' TYPE (str): 'season' + audienceRating (float): Audience rating. audioLanguage (str): Setting that indicates the preferred audio language. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. @@ -729,6 +730,7 @@ class Season( parentTheme (str): URL to show theme resource (/library/metadata//theme/). parentThumb (str): URL to show thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the show for the season. + rating (float): Season rating (7.9; 9.8; 8.1). ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects. subtitleLanguage (str): Setting that indicates the preferred subtitle language. subtitleMode (int): Setting that indicates the auto-select subtitle mode. @@ -743,6 +745,7 @@ class Season( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audioLanguage = data.attrib.get('audioLanguage', '') self.collections = self.findItems(data, media.Collection) self.guids = self.findItems(data, media.Guid) @@ -759,6 +762,7 @@ class Season( self.parentTheme = data.attrib.get('parentTheme') self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratings = self.findItems(data, media.Rating) self.subtitleLanguage = data.attrib.get('subtitleLanguage', '') self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1')) diff --git a/tests/test_audio.py b/tests/test_audio.py index 3c813d3d..8ebad4f5 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -112,6 +112,8 @@ def test_audio_Artist_mixins_rating(artist): def test_audio_Artist_mixins_fields(artist): test_mixins.edit_added_at(artist) + test_mixins.edit_audience_rating(artist) + test_mixins.edit_critic_rating(artist) test_mixins.edit_sort_title(artist) test_mixins.edit_summary(artist) test_mixins.edit_title(artist) @@ -239,6 +241,8 @@ def test_audio_Album_mixins_rating(album): def test_audio_Album_mixins_fields(album): test_mixins.edit_added_at(album) + test_mixins.edit_audience_rating(album) + test_mixins.edit_critic_rating(album) test_mixins.edit_originally_available(album) test_mixins.edit_sort_title(album) test_mixins.edit_studio(album) @@ -422,6 +426,8 @@ def test_audio_Track_mixins_rating(track): def test_audio_Track_mixins_fields(track): test_mixins.edit_added_at(track) + test_mixins.edit_audience_rating(track) + test_mixins.edit_critic_rating(track) test_mixins.edit_title(track) test_mixins.edit_track_artist(track) test_mixins.edit_track_number(track) diff --git a/tests/test_collection.py b/tests/test_collection.py index 362e8a26..6226a5fa 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -366,7 +366,9 @@ def test_Collection_mixins_rating(collection): def test_Collection_mixins_fields(collection): test_mixins.edit_added_at(collection) + test_mixins.edit_audience_rating(collection) test_mixins.edit_content_rating(collection) + test_mixins.edit_critic_rating(collection) test_mixins.edit_sort_title(collection) test_mixins.edit_summary(collection) test_mixins.edit_title(collection) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 0d111698..be7409cf 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -51,10 +51,18 @@ def edit_added_at(obj): _test_mixins_field(obj, "addedAt", "AddedAt") +def edit_audience_rating(obj): + _test_mixins_field(obj, "audienceRating", "AudienceRating", default=None, value=7.7) + + def edit_content_rating(obj): _test_mixins_field(obj, "contentRating", "ContentRating") +def edit_critic_rating(obj): + _test_mixins_field(obj, "rating", "CriticRating", default=None, value=8.8) + + def edit_edition_title(obj): _test_mixins_field(obj, "editionTitle", "EditionTitle") @@ -104,7 +112,7 @@ def edit_photo_captured_time(obj): def edit_user_rating(obj): - _test_mixins_field(obj, "userRating", "UserRating", default=None, value=10) + _test_mixins_field(obj, "userRating", "UserRating", default=None, value=10.0) def _test_mixins_tag(obj, attr, tag_method): diff --git a/tests/test_video.py b/tests/test_video.py index 040fb2e7..b25fe4b3 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -689,7 +689,9 @@ def test_video_Movie_mixins_rating(movie): def test_video_Movie_mixins_fields(movie): test_mixins.edit_added_at(movie) + test_mixins.edit_audience_rating(movie) test_mixins.edit_content_rating(movie) + test_mixins.edit_critic_rating(movie) test_mixins.edit_originally_available(movie) test_mixins.edit_original_title(movie) test_mixins.edit_sort_title(movie) @@ -955,7 +957,9 @@ def test_video_Show_mixins_rating(show): def test_video_Show_mixins_fields(show): test_mixins.edit_added_at(show) + test_mixins.edit_audience_rating(show) test_mixins.edit_content_rating(show) + test_mixins.edit_critic_rating(show) test_mixins.edit_originally_available(show) test_mixins.edit_original_title(show) test_mixins.edit_sort_title(show) @@ -1112,6 +1116,8 @@ def test_video_Season_mixins_rating(show): def test_video_Season_mixins_fields(show): season = show.season(season=1) test_mixins.edit_added_at(season) + test_mixins.edit_audience_rating(season) + test_mixins.edit_critic_rating(season) test_mixins.edit_summary(season) test_mixins.edit_title(season) test_mixins.edit_user_rating(season) @@ -1322,7 +1328,9 @@ def test_video_Episode_mixins_rating(episode): def test_video_Episode_mixins_fields(episode): test_mixins.edit_added_at(episode) + test_mixins.edit_audience_rating(episode) test_mixins.edit_content_rating(episode) + test_mixins.edit_critic_rating(episode) test_mixins.edit_originally_available(episode) test_mixins.edit_sort_title(episode) test_mixins.edit_summary(episode)