From f0ed19c2fe6fe0f482979af5e0e5a89c9ffac9c1 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Fri, 26 Aug 2022 12:14:24 -0700 Subject: [PATCH] Add `PlayedUnplayedMixin` (#984) * Add PlayedUnplayedMixin * Deprecate Video isPlayed, markWatched, and markUnwatched * Rename all deprecated methods * Return self to enable method chaining * Update tests for audio markPlayed / markUnplayed using history * Remove duplicate video history tests --- README.rst | 4 +- plexapi/audio.py | 4 +- plexapi/base.py | 7 ++-- plexapi/mixins.py | 39 +++++++++++++++++++ plexapi/video.py | 27 +++---------- tests/test_actions.py | 14 ------- tests/test_client.py | 8 ++-- tests/test_collection.py | 2 +- tests/test_history.py | 49 ++++++++++++++---------- tests/test_library.py | 6 +-- tests/test_server.py | 4 +- tests/test_sync.py | 16 ++++---- tests/test_video.py | 83 ++++++++++++++++------------------------ 13 files changed, 132 insertions(+), 131 deletions(-) diff --git a/README.rst b/README.rst index 7fbdca3a..cc321b56 100644 --- a/README.rst +++ b/README.rst @@ -81,8 +81,8 @@ Usage Examples .. code-block:: python - # Example 2: Mark all Game of Thrones episodes watched. - plex.library.section('TV Shows').get('Game of Thrones').markWatched() + # Example 2: Mark all Game of Thrones episodes as played. + plex.library.section('TV Shows').get('Game of Thrones').markPlayed() .. code-block:: python diff --git a/plexapi/audio.py b/plexapi/audio.py index f22b3e3a..93543f7b 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -6,7 +6,7 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject, PlexSession from plexapi.exceptions import BadRequest from plexapi.mixins import ( - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, + AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin, @@ -15,7 +15,7 @@ from plexapi.mixins import ( from plexapi.playlist import Playlist -class Audio(PlexPartialObject): +class Audio(PlexPartialObject, PlayedUnplayedMixin): """ Base class for all audio objects including :class:`~plexapi.audio.Artist`, :class:`~plexapi.audio.Album`, and :class:`~plexapi.audio.Track`. diff --git a/plexapi/base.py b/plexapi/base.py index 91d0e23d..73815c08 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -785,9 +785,10 @@ class Playable: def updateProgress(self, time, state='stopped'): """ Set the watched progress for this video. - Note that setting the time to 0 will not work. - Use `markWatched` or `markUnwatched` to achieve - that goal. + Note that setting the time to 0 will not work. + Use :func:`~plexapi.mixins.PlayedMixin.markPlayed` or + :func:`~plexapi.mixins.PlayedMixin.markUnplayed` to achieve + that goal. Parameters: time (int): milliseconds watched diff --git a/plexapi/mixins.py b/plexapi/mixins.py index f85a7206..a5f80631 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -255,6 +255,45 @@ class HubsMixin: return self.findItems(data, Hub) +class PlayedUnplayedMixin: + """ Mixin for Plex objects that can be marked played and unplayed. """ + + @property + def isPlayed(self): + """ Returns True if this video is played. """ + return bool(self.viewCount > 0) if self.viewCount else False + + def markPlayed(self): + """ Mark the Plex object as played. """ + key = '/:/scrobble' + params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} + self._server.query(key, params=params) + return self + + def markUnplayed(self): + """ Mark the Plex object as unplayed. """ + key = '/:/unscrobble' + params = {'key': self.ratingKey, 'identifier': 'com.plexapp.plugins.library'} + self._server.query(key, params=params) + return self + + @property + @deprecated('use "isPlayed" instead', stacklevel=3) + def isWatched(self): + """ Returns True if the show is watched. """ + return self.isPlayed + + @deprecated('use "markPlayed" instead') + def markWatched(self): + """ Mark the video as played. """ + self.markPlayed() + + @deprecated('use "markUnplayed" instead') + def markUnwatched(self): + """ Mark the video as unplayed. """ + self.markUnplayed() + + class RatingMixin: """ Mixin for Plex objects that can have user star ratings. """ diff --git a/plexapi/video.py b/plexapi/video.py index f105e5b4..82984152 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -6,7 +6,7 @@ from plexapi import media, utils from plexapi.base import Playable, PlexPartialObject, PlexSession from plexapi.exceptions import BadRequest from plexapi.mixins import ( - AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin, + AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin, ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, @@ -15,7 +15,7 @@ from plexapi.mixins import ( ) -class Video(PlexPartialObject): +class Video(PlexPartialObject, PlayedUnplayedMixin): """ Base class for all video objects including :class:`~plexapi.video.Movie`, :class:`~plexapi.video.Show`, :class:`~plexapi.video.Season`, :class:`~plexapi.video.Episode`, and :class:`~plexapi.video.Clip`. @@ -71,25 +71,10 @@ class Video(PlexPartialObject): self.userRating = utils.cast(float, data.attrib.get('userRating')) self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0)) - @property - def isWatched(self): - """ Returns True if this video is watched. """ - return bool(self.viewCount > 0) if self.viewCount else False - def url(self, part): """ Returns the full url for something. Typically used for getting a specific image. """ return self._server.url(part, includeToken=True) if part else None - def markWatched(self): - """ Mark the video as played. """ - key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey - self._server.query(key) - - def markUnwatched(self): - """ Mark the video as unplayed. """ - key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey - self._server.query(key) - def augmentation(self): """ Returns a list of :class:`~plexapi.library.Hub` objects. Augmentation returns hub items relating to online media sources @@ -523,8 +508,8 @@ class Show( return self.roles @property - def isWatched(self): - """ Returns True if the show is fully watched. """ + def isPlayed(self): + """ Returns True if the show is fully played. """ return bool(self.viewedLeafCount == self.leafCount) def onDeck(self): @@ -678,8 +663,8 @@ class Season( ] if p]) @property - def isWatched(self): - """ Returns True if the season is fully watched. """ + def isPlayed(self): + """ Returns True if the season is fully played. """ return bool(self.viewedLeafCount == self.leafCount) @property diff --git a/tests/test_actions.py b/tests/test_actions.py index 39afbcde..50dc95d2 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,20 +1,6 @@ # -*- coding: utf-8 -*- -def test_mark_movie_watched(movie): - movie.markUnwatched() - 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.' - - def test_refresh_section(tvshows): tvshows.refresh() diff --git a/tests/test_client.py b/tests/test_client.py index 4f4f846e..cb2fec8c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -111,8 +111,8 @@ def test_client_playback(plex, client, movies, proxy): client.stop(mtype) time.sleep(1) finally: - print("movie.markWatched") - movie.markWatched() + print("movie.markPlayed") + movie.markPlayed() time.sleep(2) @@ -138,6 +138,6 @@ def test_client_timeline(plex, client, movies, proxy): time.sleep(10) assert client.isPlayingMedia() is False finally: - print("movie.markWatched()") - movie.markWatched() + print("movie.markPlayed()") + movie.markPlayed() time.sleep(2) diff --git a/tests/test_collection.py b/tests/test_collection.py index 88c063ab..c649b344 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -161,7 +161,7 @@ def test_Collection_add_move_remove(collection, movies): assert movie not in collection # Reset collection sort due to bug with corrupted XML response # for movies that have been moved in a collection and have - # progress (updateProgress) or marked as played (markWatched) + # progress (updateProgress) or marked as played (markPlayed) collection.sortUpdate("release") diff --git a/tests/test_history.py b/tests/test_history.py index bc23eb1b..35ca86cc 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -2,76 +2,85 @@ def test_history_Movie(movie): - movie.markWatched() + movie.markPlayed() history = movie.history() assert len(history) - movie.markUnwatched() + movie.markUnplayed() def test_history_Show(show): - show.markWatched() + show.markPlayed() history = show.history() assert len(history) - show.markUnwatched() + show.markUnplayed() def test_history_Season(show): season = show.season("Season 1") - season.markWatched() + season.markPlayed() history = season.history() assert len(history) - season.markUnwatched() + season.markUnplayed() def test_history_Episode(episode): - episode.markWatched() + episode.markPlayed() history = episode.history() assert len(history) - episode.markUnwatched() + episode.markUnplayed() def test_history_Artist(artist): + artist.markPlayed() history = artist.history() + assert len(history) + artist.markUnplayed() def test_history_Album(album): + album.markPlayed() history = album.history() + assert len(history) + album.markUnplayed() def test_history_Track(track): + track.markPlayed() history = track.history() + assert len(history) + track.markUnplayed() def test_history_MyAccount(account, movie, show): - movie.markWatched() - show.markWatched() + movie.markPlayed() + show.markPlayed() history = account.history() assert len(history) - movie.markUnwatched() - show.markUnwatched() + movie.markUnplayed() + show.markUnplayed() def test_history_MyLibrary(plex, movie, show): - movie.markWatched() - show.markWatched() + movie.markPlayed() + show.markPlayed() history = plex.library.history() assert len(history) - movie.markUnwatched() - show.markUnwatched() + movie.markUnplayed() + show.markUnplayed() def test_history_MySection(plex, movie): - movie.markWatched() + movie.markPlayed() history = plex.library.section("Movies").history() assert len(history) - movie.markUnwatched() + movie.markUnplayed() def test_history_MyServer(plex, movie): - movie.markWatched() + movie.markPlayed() history = plex.history() assert len(history) - movie.markUnwatched() + movie.markUnplayed() def test_history_User(account, shared_username): diff --git a/tests/test_library.py b/tests/test_library.py index 5982ba54..103ed1f9 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -120,7 +120,7 @@ def test_library_fetchItem(plex, movie): def test_library_onDeck(plex, movie): movie.updateProgress(movie.duration // 4) # set progress to 25% assert movie in plex.library.onDeck() - movie.markUnwatched() + movie.markUnplayed() def test_library_recentlyAdded(plex): @@ -264,10 +264,10 @@ def test_library_deleteMediaPreviews(movies): def test_library_MovieSection_onDeck(movie, movies, tvshows, episode): movie.updateProgress(movie.duration // 4) # set progress to 25% assert movie in movies.onDeck() - movie.markUnwatched() + movie.markUnplayed() episode.updateProgress(episode.duration // 4) assert episode in tvshows.onDeck() - episode.markUnwatched() + episode.markUnplayed() def test_library_MovieSection_searchMovies(movies): diff --git a/tests/test_server.py b/tests/test_server.py index 883ba58c..48beb300 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -208,10 +208,10 @@ def test_server_playlists(plex, show): def test_server_history(plex, movie): - movie.markWatched() + movie.markPlayed() history = plex.history() assert len(history) - movie.markUnwatched() + movie.markUnplayed() def test_server_Server_query(plex): diff --git a/tests/test_sync.py b/tests/test_sync.py index 6c00c807..31386789 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -125,7 +125,7 @@ def test_add_episode_to_sync(clear_sync_device, episode): def test_limited_watched(clear_sync_device, show): - show.markUnwatched() + show.markUnplayed() new_item = show.sync( VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=False ) @@ -143,7 +143,7 @@ def test_limited_watched(clear_sync_device, show): ) assert 5 == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] - episodes[0].markWatched() + episodes[0].markPlayed() show._server.refreshSync() media_list = utils.wait_until( get_media, delay=0.25, timeout=3, item=item, server=show._server @@ -153,7 +153,7 @@ def test_limited_watched(clear_sync_device, show): def test_limited_unwatched(clear_sync_device, show): - show.markUnwatched() + show.markUnplayed() new_item = show.sync( VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=True ) @@ -171,7 +171,7 @@ def test_limited_unwatched(clear_sync_device, show): ) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] - episodes[0].markWatched() + episodes[0].markPlayed() show._server.refreshSync() episodes = show.episodes(viewCount=0)[:5] media_list = utils.wait_until( @@ -182,7 +182,7 @@ def test_limited_unwatched(clear_sync_device, show): def test_unlimited_and_watched(clear_sync_device, show): - show.markUnwatched() + show.markUnplayed() new_item = show.sync( VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=False ) @@ -200,7 +200,7 @@ def test_unlimited_and_watched(clear_sync_device, show): ) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] - episodes[0].markWatched() + episodes[0].markPlayed() show._server.refreshSync() episodes = show.episodes() media_list = utils.wait_until( @@ -211,7 +211,7 @@ def test_unlimited_and_watched(clear_sync_device, show): def test_unlimited_and_unwatched(clear_sync_device, show): - show.markUnwatched() + show.markUnplayed() new_item = show.sync( VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=True ) @@ -229,7 +229,7 @@ def test_unlimited_and_unwatched(clear_sync_device, show): ) assert len(episodes) == len(media_list) assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] - episodes[0].markWatched() + episodes[0].markPlayed() show._server.refreshSync() episodes = show.episodes(viewCount=0) media_list = utils.wait_until( diff --git a/tests/test_video.py b/tests/test_video.py index 27d301d4..0b127599 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -340,6 +340,16 @@ def test_video_Movie_isFullObject_and_reload(plex): assert len(movie_via_section_search.roles) >= 3 +def test_video_movie_watched(movie): + movie.markUnplayed() + movie.markPlayed() + movie.reload() + assert movie.viewCount == 1 + movie.markUnplayed() + movie.reload() + assert movie.viewCount == 0 + + def test_video_Movie_isPartialObject(movie): assert movie.isPartialObject() movie._autoReload = False @@ -410,13 +420,6 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle): pass -def test_video_Movie_history(movie): - movie.markWatched() - history = movie.history() - assert len(history) - movie.markUnwatched() - - def test_video_Movie_match(movies): sectionAgent = movies.agent sectionAgents = [agent.identifier for agent in movies.agents() if agent.shortIdentifier != 'none'] @@ -760,30 +763,23 @@ def test_video_Show_episode(show): show.episode(season=1337, episode=1337) -def test_video_Show_history(show): - show.markWatched() - history = show.history() - assert len(history) - show.markUnwatched() - - def test_video_Show_watched(tvshows): show = tvshows.get("The 100") episode = show.episodes()[0] - episode.markWatched() + episode.markPlayed() watched = show.watched() assert len(watched) == 1 and watched[0].title == "Pilot" - episode.markUnwatched() + episode.markUnplayed() def test_video_Show_unwatched(tvshows): show = tvshows.get("The 100") episodes = show.episodes() episode = episodes[0] - episode.markWatched() + episode.markPlayed() unwatched = show.unwatched() assert len(unwatched) == len(episodes) - 1 - episode.markUnwatched() + episode.markUnplayed() def test_video_Show_settings(show): @@ -803,7 +799,7 @@ def test_video_Show_reload(plex): def test_video_Show_episodes(tvshows): show = tvshows.get("The 100") episodes = show.episodes() - episodes[0].markWatched() + episodes[0].markPlayed() unwatched = show.episodes(viewCount=0) assert len(unwatched) == len(episodes) - 1 @@ -836,16 +832,16 @@ def test_video_Show_analyze(show): show = show.analyze() -def test_video_Show_markWatched(show): - show.markWatched() +def test_video_Show_markPlayed(show): + show.markPlayed() show.reload() - assert show.isWatched + assert show.isPlayed -def test_video_Show_markUnwatched(show): - show.markUnwatched() +def test_video_Show_markUnplayed(show): + show.markUnplayed() show.reload() - assert not show.isWatched + assert not show.isPlayed def test_video_Show_refresh(show): @@ -856,8 +852,8 @@ def test_video_Show_get(show): assert show.get("Winter Is Coming").title == "Winter Is Coming" -def test_video_Show_isWatched(show): - assert not show.isWatched +def test_video_Show_isPlayed(show): + assert not show.isPlayed def test_video_Show_section(show): @@ -933,14 +929,6 @@ def test_video_Season(show): assert show.season("Season 1") == seasons[0] -def test_video_Season_history(show): - season = show.season("Season 1") - season.markWatched() - history = season.history() - assert len(history) - season.markUnwatched() - - def test_video_Season_attrs(show): season = show.season("Season 1") assert utils.is_datetime(season.addedAt) @@ -988,16 +976,16 @@ def test_video_Season_show(show): def test_video_Season_watched(show): season = show.season("Season 1") - season.markWatched() + season.markPlayed() season.reload() - assert season.isWatched + assert season.isPlayed def test_video_Season_unwatched(show): season = show.season("Season 1") - season.markUnwatched() + season.markUnplayed() season.reload() - assert not season.isWatched + assert not season.isPlayed def test_video_Season_get(show): @@ -1082,13 +1070,6 @@ def test_video_Episode(show): show.episode(season=1337, episode=1337) -def test_video_Episode_history(episode): - episode.markWatched() - history = episode.history() - assert len(history) - episode.markUnwatched() - - def test_video_Episode_hidden_season(episode): assert episode.skipParent is False assert episode.parentRatingKey @@ -1182,7 +1163,7 @@ def test_video_Episode_attrs(episode): if episode.writers: assert "David Benioff" in [i.tag for i in episode.writers] assert episode.year is None - assert episode.isWatched in [True, False] + assert episode.isPlayed in [True, False] assert len(episode.locations) == 1 assert len(episode.locations[0]) >= 10 assert episode.seasonEpisode == "s01e01" @@ -1222,20 +1203,20 @@ def test_video_Episode_attrs(episode): def test_video_Episode_watched(tvshows): season = tvshows.get("The 100").season(1) episode = season.episode(1) - episode.markWatched() + episode.markPlayed() watched = season.watched() assert len(watched) == 1 and watched[0].title == "Pilot" - episode.markUnwatched() + episode.markUnplayed() def test_video_Episode_unwatched(tvshows): season = tvshows.get("The 100").season(1) episodes = season.episodes() episode = episodes[0] - episode.markWatched() + episode.markPlayed() unwatched = season.unwatched() assert len(unwatched) == len(episodes) - 1 - episode.markUnwatched() + episode.markUnplayed() @pytest.mark.xfail(reason="Changing images fails randomly")