From 06ff7410a6d0d1e16a61826c4b55848914f2e723 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 30 May 2021 15:37:44 -0700 Subject: [PATCH 1/5] Add rating mixin --- plexapi/audio.py | 9 +++++---- plexapi/collection.py | 4 ++-- plexapi/mixins.py | 22 +++++++++++++++++++++- plexapi/photo.py | 6 +++--- plexapi/video.py | 26 ++++++++------------------ 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 25ac659b1..9a78632ce 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -5,7 +5,7 @@ from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin -from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin +from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin @@ -115,7 +115,7 @@ def sync(self, bitrate, client=None, clientId=None, limit=None, title=None): @utils.registerPlexObject -class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin, +class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin): """ Represents a single Artist. @@ -222,7 +222,7 @@ def download(self, savepath=None, keep_original_name=False, **kwargs): @utils.registerPlexObject -class Album(Audio, ArtMixin, PosterMixin, UnmatchMatchMixin, +class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): """ Represents a single Album. @@ -329,7 +329,8 @@ def _defaultSyncTitle(self): @utils.registerPlexObject -class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, CollectionMixin, MoodMixin): +class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, + CollectionMixin, MoodMixin): """ Represents a single Track. Attributes: diff --git a/plexapi/collection.py b/plexapi/collection.py index de4c165df..c35d0f51e 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -2,14 +2,14 @@ from plexapi import media, utils from plexapi.base import PlexPartialObject from plexapi.exceptions import BadRequest -from plexapi.mixins import ArtMixin, PosterMixin +from plexapi.mixins import ArtMixin, PosterMixin, RatingMixin from plexapi.mixins import LabelMixin from plexapi.settings import Setting from plexapi.utils import deprecated @utils.registerPlexObject -class Collection(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin): +class Collection(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin, LabelMixin): """ Represents a single Collection. Attributes: diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 05d1f40fa..d6775829b 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -2,7 +2,7 @@ from urllib.parse import quote_plus, urlencode from plexapi import media, settings, utils -from plexapi.exceptions import NotFound +from plexapi.exceptions import BadRequest, NotFound class AdvancedSettingsMixin(object): @@ -190,6 +190,26 @@ def setPoster(self, poster): poster.select() +class RatingMixin(object): + """ Mixin for Plex objects that can have user star ratings. """ + + def rate(self, rating=None): + """ Rate the Plex object. Note: Plex ratings are displayed out of 5 stars (e.g. rating 7.0 = 3.5 stars). + + Parameters: + rating (float, optional): Rating from 0 to 10. Exclude to reset the rating. + + Raises: + :exc:`~plexapi.exceptions.BadRequest`: If the rating is invalid. + """ + if rating is None: + rating = -1 + elif not isinstance(rating, (int, float)) or rating < 0 or rating > 10: + raise BadRequest('Rating must be between 0 to 10.') + key = '/:/rate?key=%s&identifier=com.plexapp.plugins.library&rating=%s' % (self.ratingKey, rating) + self._server.query(key, method=self._server._session.put) + + class SplitMergeMixin(object): """ Mixin for Plex objects that can be split and merged. """ diff --git a/plexapi/photo.py b/plexapi/photo.py index a83073339..061b3f60e 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -4,11 +4,11 @@ from plexapi import media, utils, video from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest -from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, TagMixin +from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, RatingMixin, TagMixin @utils.registerPlexObject -class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin): +class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin): """ Represents a single Photoalbum (collection of photos). Attributes: @@ -137,7 +137,7 @@ def download(self, savepath=None, keep_original_name=False, showstatus=False): @utils.registerPlexObject -class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin): +class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin): """ Represents a single Photo. Attributes: diff --git a/plexapi/video.py b/plexapi/video.py index 141e29150..f3bd84cc5 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -6,7 +6,7 @@ from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin -from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin +from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin @@ -73,23 +73,14 @@ def url(self, part): return self._server.url(part, includeToken=True) if part else None def markWatched(self): - """ Mark video as watched. """ + """ Mark the video as palyed. """ key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey self._server.query(key) - self.reload() def markUnwatched(self): - """ Mark video unwatched. """ + """ Mark the video as unplayed. """ key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey self._server.query(key) - self.reload() - - def rate(self, rate): - """ Rate video. """ - key = '/:/rate?key=%s&identifier=com.plexapp.plugins.library&rating=%s' % (self.ratingKey, rate) - - self._server.query(key) - self.reload() def _defaultSyncTitle(self): """ Returns str, default title for a new syncItem. """ @@ -249,7 +240,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F @utils.registerPlexObject -class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin, +class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin): """ Represents a single Movie. @@ -387,7 +378,7 @@ def download(self, savepath=None, keep_original_name=False, **kwargs): @utils.registerPlexObject -class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin, +class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, CollectionMixin, GenreMixin, LabelMixin): """ Represents a single Show (including all seasons and episodes). @@ -591,7 +582,7 @@ def download(self, savepath=None, keep_original_name=False, **kwargs): @utils.registerPlexObject -class Season(Video, ArtMixin, PosterMixin, CollectionMixin): +class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin): """ Represents a single Show Season (including all episodes). Attributes: @@ -729,7 +720,8 @@ def _defaultSyncTitle(self): @utils.registerPlexObject -class Episode(Video, Playable, ArtMixin, PosterMixin, CollectionMixin, DirectorMixin, WriterMixin): +class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin, + CollectionMixin, DirectorMixin, WriterMixin): """ Represents a single Shows Episode. Attributes: @@ -865,8 +857,6 @@ def seasonEpisode(self): @property def hasIntroMarker(self): """ Returns True if the episode has an intro marker in the xml. """ - if not self.isFullObject(): - self.reload() return any(marker.type == 'intro' for marker in self.markers) @property From 6e58f7f70efbe829abbc8e7fb38747ade2845e0d Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 30 May 2021 15:47:53 -0700 Subject: [PATCH 2/5] Refactor lastRatedAt userRating attributes --- plexapi/audio.py | 5 +++-- plexapi/collection.py | 4 ++++ plexapi/photo.py | 10 ++++++++-- plexapi/video.py | 9 +++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/plexapi/audio.py b/plexapi/audio.py index 9a78632ce..8f81d9066 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -21,6 +21,7 @@ class Audio(PlexPartialObject): guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c). index (int): Plex index number (often the track number). key (str): API URL (/library/metadata/). + lastRatedAt (datetime): Datetime the item was last rated. lastViewedAt (datetime): Datetime the item was last played. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. @@ -35,7 +36,7 @@ class Audio(PlexPartialObject): titleSort (str): Title to use when sorting (defaults to title). type (str): 'artist', 'album', or 'track'. updatedAt (datatime): Datetime the item was updated. - userRating (float): Rating of the track (0.0 - 10.0) equaling (0 stars - 5 stars). + userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars). viewCount (int): Count of times the item was played. """ @@ -66,7 +67,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) - self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) + self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0)) def url(self, part): diff --git a/plexapi/collection.py b/plexapi/collection.py index c35d0f51e..45c5f8f8b 100644 --- a/plexapi/collection.py +++ b/plexapi/collection.py @@ -29,6 +29,7 @@ class Collection(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin, LabelMix index (int): Plex index number for the collection. key (str): API URL (/library/metadata/). labels (List<:class:`~plexapi.media.Label`>): List of label objects. + lastRatedAt (datetime): Datetime the collection was last rated. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. @@ -45,6 +46,7 @@ class Collection(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin, LabelMix titleSort (str): Title to use when sorting (defaults to title). type (str): 'collection' updatedAt (datatime): Datetime the collection was updated. + userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars). """ TAG = 'Directory' @@ -65,6 +67,7 @@ def _loadData(self, data): self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50 self.labels = self.findItems(data, media.Label) + self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID')) self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') @@ -81,6 +84,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.userRating = utils.cast(float, data.attrib.get('userRating')) @property @deprecated('use "items" instead', stacklevel=3) diff --git a/plexapi/photo.py b/plexapi/photo.py index 061b3f60e..ad8e0706b 100644 --- a/plexapi/photo.py +++ b/plexapi/photo.py @@ -21,6 +21,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin): guid (str): Plex GUID for the photo album (local://229674). index (sting): Plex index number for the photo album. key (str): API URL (/library/metadata/). + lastRatedAt (datetime): Datetime the photo album was last rated. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. @@ -32,7 +33,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin): titleSort (str): Title to use when sorting (defaults to title). type (str): 'photo' updatedAt (datatime): Datetime the photo album was updated. - userRating (float): Rating of the photoalbum (0.0 - 10.0) equaling (0 stars - 5 stars). + userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars). """ TAG = 'Directory' TYPE = 'photo' @@ -46,6 +47,7 @@ def _loadData(self, data): self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50 + self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID')) self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') @@ -57,7 +59,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) - self.userRating = utils.cast(float, data.attrib.get('userRating', 0)) + self.userRating = utils.cast(float, data.attrib.get('userRating')) def album(self, title): """ Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title. @@ -150,6 +152,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn). index (sting): Plex index number for the photo. key (str): API URL (/library/metadata/). + lastRatedAt (datetime): Datetime the photo was last rated. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. @@ -170,6 +173,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi titleSort (str): Title to use when sorting (defaults to title). type (str): 'photo' updatedAt (datatime): Datetime the photo was updated. + userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars). year (int): Year the photo was taken. """ TAG = 'Photo' @@ -186,6 +190,7 @@ def _loadData(self, data): self.guid = data.attrib.get('guid') self.index = utils.cast(int, data.attrib.get('index')) self.key = data.attrib.get('key', '') + self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt')) self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID')) self.librarySectionKey = data.attrib.get('librarySectionKey') self.librarySectionTitle = data.attrib.get('librarySectionTitle') @@ -206,6 +211,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.userRating = utils.cast(float, data.attrib.get('userRating')) self.year = utils.cast(int, data.attrib.get('year')) def photoalbum(self): diff --git a/plexapi/video.py b/plexapi/video.py index f3bd84cc5..999e5adfe 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -22,6 +22,7 @@ class Video(PlexPartialObject): fields (List<:class:`~plexapi.media.Field`>): List of field objects. guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8). key (str): API URL (/library/metadata/). + lastRatedAt (datetime): Datetime the item was last rated. lastViewedAt (datetime): Datetime the item was last played. librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID. librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key. @@ -35,6 +36,7 @@ class Video(PlexPartialObject): titleSort (str): Title to use when sorting (defaults to title). type (str): 'movie', 'show', 'season', 'episode', or 'clip'. updatedAt (datatime): Datetime the item was updated. + userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars). viewCount (int): Count of times the item was played. """ @@ -61,6 +63,7 @@ def _loadData(self, data): self.titleSort = data.attrib.get('titleSort', self.title) self.type = data.attrib.get('type') self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) + self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0)) @property @@ -274,7 +277,6 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). useOriginalTitle (int): Setting that indicates if the original title is used for the movie (-1 = Library default, 0 = No, 1 = Yes). - userRating (float): User rating (2.0; 8.0). viewOffset (int): View offset in milliseconds. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. year (int): Year movie was released. @@ -312,7 +314,6 @@ def _loadData(self, data): self.studio = data.attrib.get('studio') self.tagline = data.attrib.get('tagline') self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) - self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.writers = self.findItems(data, media.Writer) self.year = utils.cast(int, data.attrib.get('year')) @@ -425,7 +426,6 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat theme (str): URL to theme resource (/library/metadata//theme/). useOriginalTitle (int): Setting that indicates if the original title is used for the show (-1 = Library default, 0 = No, 1 = Yes). - userRating (float): User rating (2.0; 8.0). viewedLeafCount (int): Number of items marked as played in the show view. year (int): Year the show was released. """ @@ -468,7 +468,6 @@ def _loadData(self, data): self.tagline = data.attrib.get('tagline') self.theme = data.attrib.get('theme') self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) - self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount')) self.year = utils.cast(int, data.attrib.get('year')) @@ -756,7 +755,6 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin, parentYear (int): Year the season was released. rating (float): Episode rating (7.9; 9.8; 8.1). skipParent (bool): True if the show's seasons are set to hidden. - userRating (float): User rating (2.0; 8.0). viewOffset (int): View offset in milliseconds. writers (List<:class:`~plexapi.media.Writer`>): List of writers objects. year (int): Year the episode was released. @@ -799,7 +797,6 @@ def _loadData(self, data): self.parentYear = utils.cast(int, data.attrib.get('parentYear')) self.rating = utils.cast(float, data.attrib.get('rating')) self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0')) - self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.writers = self.findItems(data, media.Writer) self.year = utils.cast(int, data.attrib.get('year')) From bd8cdb10b7cfd51210279105f220ea791d1a3c42 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 30 May 2021 15:49:28 -0700 Subject: [PATCH 3/5] Update tests for ratings mixin --- tests/test_actions.py | 9 --------- tests/test_audio.py | 12 ++++++++++++ tests/test_collection.py | 4 ++++ tests/test_mixins.py | 19 ++++++++++++++++++- tests/test_photo.py | 8 ++++++++ tests/test_video.py | 17 +++++++++++++++++ 6 files changed, 59 insertions(+), 10 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index 994068d8d..614bbbd45 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -19,12 +19,3 @@ def test_refresh_section(tvshows): def test_refresh_video(movie): movie.refresh() - - -def test_rate_movie(movie): - oldrate = movie.userRating - if oldrate is None: - oldrate = 1 - movie.rate(10.0) - assert movie.userRating == 10.0, 'User rating 10.0 after rating five stars.' - movie.rate(oldrate) diff --git a/tests/test_audio.py b/tests/test_audio.py index 288c3162b..33325a4c5 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -80,6 +80,10 @@ def test_audio_Artist_mixins_images(artist): test_mixins.attr_posterUrl(artist) +def test_audio_Artist_mixins_rating(artist): + test_mixins.edit_rating(artist) + + def test_audio_Artist_mixins_tags(artist): test_mixins.edit_collection(artist) test_mixins.edit_country(artist) @@ -171,6 +175,10 @@ def test_audio_Album_mixins_images(album): test_mixins.attr_posterUrl(album) +def test_audio_Album_mixins_rating(album): + test_mixins.edit_rating(album) + + def test_audio_Album_mixins_tags(album): test_mixins.edit_collection(album) test_mixins.edit_genre(album) @@ -314,6 +322,10 @@ def test_audio_Track_mixins_images(track): test_mixins.attr_posterUrl(track) +def test_audio_Track_mixins_rating(track): + test_mixins.edit_rating(track) + + def test_audio_Track_mixins_tags(track): test_mixins.edit_collection(track) test_mixins.edit_mood(track) diff --git a/tests/test_collection.py b/tests/test_collection.py index c2663a309..c43a129bc 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -114,5 +114,9 @@ def test_Collection_mixins_images(collection): test_mixins.attr_posterUrl(collection) +def test_Collection_mixins_rating(collection): + test_mixins.edit_rating(collection) + + def test_Collection_mixins_tags(collection): test_mixins.edit_label(collection) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index e5c86c080..d3c04f85d 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from plexapi.exceptions import NotFound +from plexapi.exceptions import BadRequest, NotFound from plexapi.utils import tag_singular import pytest @@ -177,3 +177,20 @@ def edit_advanced_settings(obj): _test_mixins_editAdvanced(obj) _test_mixins_editAdvanced_bad_pref(obj) _test_mixins_defaultAdvanced(obj) + + +def edit_rating(obj): + obj.rate(10.0) + obj.reload() + assert utils.is_datetime(obj.lastRatedAt) + assert obj.userRating == 10.0 + obj.rate() + obj.reload() + assert obj.lastRatedAt is None + assert obj.userRating is None + with pytest.raises(BadRequest): + assert obj.rate('bad-rating') + with pytest.raises(BadRequest): + assert obj.rate(-1) + with pytest.raises(BadRequest): + assert obj.rate(100) diff --git a/tests/test_photo.py b/tests/test_photo.py index 85e4c9acc..8d282f81d 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -18,6 +18,14 @@ def test_photo_Photoalbum_mixins_images(photoalbum): test_mixins.attr_posterUrl(photoalbum) +def test_photo_Photoalbum_mixins_rating(photoalbum): + test_mixins.edit_rating(photoalbum) + + +def test_photo_Photo_mixins_rating(photo): + test_mixins.edit_rating(photo) + + def test_photo_Photo_mixins_tags(photo): test_mixins.edit_tag(photo) diff --git a/tests/test_video.py b/tests/test_video.py index 11fb76341..f769be197 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -49,6 +49,10 @@ def test_video_Movie_mixins_images(movie): test_mixins.edit_poster(movie) +def test_video_Movie_mixins_rating(movie): + test_mixins.edit_rating(movie) + + def test_video_Movie_mixins_tags(movie): test_mixins.edit_collection(movie) test_mixins.edit_country(movie) @@ -729,6 +733,10 @@ def test_video_Show_mixins_images(show): test_mixins.attr_posterUrl(show) +def test_video_Show_mixins_rating(show): + test_mixins.edit_rating(show) + + def test_video_Show_mixins_tags(show): test_mixins.edit_collection(show) test_mixins.edit_genre(show) @@ -843,6 +851,11 @@ def test_video_Season_mixins_images(show): test_mixins.attr_posterUrl(season) +def test_video_Season_mixins_rating(show): + season = show.season(season=1) + test_mixins.edit_rating(season) + + def test_video_Season_mixins_tags(show): season = show.season(season=1) test_mixins.edit_collection(season) @@ -1033,6 +1046,10 @@ def test_video_Episode_mixins_images(episode): test_mixins.attr_posterUrl(episode) +def test_video_Episode_mixins_rating(episode): + test_mixins.edit_rating(episode) + + def test_video_Episode_mixins_tags(episode): test_mixins.edit_collection(episode) test_mixins.edit_director(episode) From b269140943013460fc25023c2e249ea3716e35c1 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 30 May 2021 16:07:59 -0700 Subject: [PATCH 4/5] Fix rating mixin tests --- tests/test_actions.py | 2 ++ tests/test_mixins.py | 5 +++-- tests/test_video.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index 614bbbd45..39afbcde1 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -6,9 +6,11 @@ def test_mark_movie_watched(movie): print('Marking movie watched: %s' % movie) print('View count: %s' % movie.viewCount) movie.markWatched() + movie.reload() print('View count: %s' % movie.viewCount) assert movie.viewCount == 1, 'View count 0 after watched.' movie.markUnwatched() + movie.reload() print('View count: %s' % movie.viewCount) assert movie.viewCount == 0, 'View count 1 after unwatched.' diff --git a/tests/test_mixins.py b/tests/test_mixins.py index d3c04f85d..94d494a22 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -185,8 +185,9 @@ def edit_rating(obj): assert utils.is_datetime(obj.lastRatedAt) assert obj.userRating == 10.0 obj.rate() - obj.reload() - assert obj.lastRatedAt is None + # Cannot use obj.reload() since PlexObject.__setattr__() + # will not overwrite userRating with None + obj = obj.fetchItem(obj._details_key) assert obj.userRating is None with pytest.raises(BadRequest): assert obj.rate('bad-rating') diff --git a/tests/test_video.py b/tests/test_video.py index f769be197..9b44c99aa 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -697,11 +697,13 @@ def test_video_Show_analyze(show): def test_video_Show_markWatched(show): show.markWatched() + show.reload() assert show.isWatched def test_video_Show_markUnwatched(show): show.markUnwatched() + show.reload() assert not show.isWatched @@ -814,12 +816,14 @@ def test_video_Season_show(show): def test_video_Season_watched(show): season = show.season("Season 1") season.markWatched() + season.reload() assert season.isWatched def test_video_Season_unwatched(show): season = show.season("Season 1") season.markUnwatched() + season.reload() assert not season.isWatched From 13215152c903b24b5d6f70498e90efbeff3e2acf Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Sun, 6 Jun 2021 13:51:59 -0700 Subject: [PATCH 5/5] Explicit check for parent is not None * Checking if parent is truthy calls the __len__ method which does not work for some Plex objects until after it is initialized with _loadData. --- plexapi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/base.py b/plexapi/base.py index 060c7d906..fc445e32f 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -48,7 +48,7 @@ def __init__(self, server, data, initpath=None, parent=None): self._server = server self._data = data self._initpath = initpath or self.key - self._parent = weakref.ref(parent) if parent else None + self._parent = weakref.ref(parent) if parent is not None else None self._details_key = None if data is not None: self._loadData(data)