Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create separate PlexHistory objects #1185

Merged
merged 2 commits into from
Jul 28, 2023
Merged
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
15 changes: 14 additions & 1 deletion plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from urllib.parse import quote_plus

from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject, PlexSession
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
Expand Down Expand Up @@ -482,3 +482,16 @@ def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Track._loadData(self, data)
PlexSession._loadData(self, data)


@utils.registerPlexObject
class TrackHistory(PlexHistory, Track):
""" Represents a single Track history entry
loaded from :func:`~plexapi.server.PlexServer.history`.
"""
_HISTORYTYPE = True

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Track._loadData(self, data)
PlexHistory._loadData(self, data)
41 changes: 33 additions & 8 deletions plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ def _buildItem(self, elem, cls=None, initpath=None):
etype = elem.attrib.get('streamType', elem.attrib.get('tagType', elem.attrib.get('type')))
ehash = f'{elem.tag}.{etype}' if etype else elem.tag
if initpath == '/status/sessions':
ehash = f"{ehash}.{'session'}"
ehash = f"{ehash}.session"
elif initpath.startswith('/status/sessions/history'):
ehash = f"{ehash}.history"
ecls = utils.PLEXOBJECTS.get(ehash, utils.PLEXOBJECTS.get(elem.tag))
# log.debug('Building %s as %s', elem.tag, ecls.__name__)
if ecls is not None:
Expand Down Expand Up @@ -506,7 +508,7 @@ def __getattribute__(self, attr):
if attr.startswith('_'): return value
if value not in (None, []): return value
if self.isFullObject(): return value
if isinstance(self, PlexSession): return value
if isinstance(self, (PlexSession, PlexHistory)): return value
if self._autoReload is False: return value
# Log the reload.
clsname = self.__class__.__name__
Expand Down Expand Up @@ -695,17 +697,11 @@ class Playable:
Albums which are all not playable.

Attributes:
viewedAt (datetime): Datetime item was last viewed (history).
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
playQueueItemID (int): PlayQueue item ID (only populated for :class:`~plexapi.playlist.PlayQueue` items).
"""

def _loadData(self, data):
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
self.deviceID = utils.cast(int, data.attrib.get('deviceID')) # history
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue

Expand Down Expand Up @@ -926,6 +922,35 @@ def stop(self, reason=''):
return self._server.query(key, params=params)


class PlexHistory(object):
""" This is a general place to store functions specific to media that is a Plex history item.

Attributes:
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
historyKey (str): API URL (/status/sessions/history/<historyID>).
viewedAt (datetime): Datetime item was last watched.
"""

def _loadData(self, data):
self.accountID = utils.cast(int, data.attrib.get('accountID'))
self.deviceID = utils.cast(int, data.attrib.get('deviceID'))
self.historyKey = data.attrib.get('historyKey')
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt'))

def _reload(self, **kwargs):
""" Reload the data for the history entry. """
raise NotImplementedError('History objects cannot be reloaded. Use source() to get the source media item.')

def source(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

this is an unfortunate method name, causing conflict:

what inspired this method name? maybe rename to origin or item or original?

Copy link
Contributor

Choose a reason for hiding this comment

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

@JonnyWong16: do you have answer? may give some ideas what other name to use here instead:

""" Return the source media object for the history entry. """
return self.fetchItem(self._details_key)

def delete(self):
""" Delete the history entry. """
return self._server.query(self.historyKey, method=self._server._session.delete)


class MediaContainer(PlexObject):
""" Represents a single MediaContainer.

Expand Down
4 changes: 3 additions & 1 deletion plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ def registerPlexObject(cls):
etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE))
ehash = f'{cls.TAG}.{etype}' if etype else cls.TAG
if getattr(cls, '_SESSIONTYPE', None):
ehash = f"{ehash}.{'session'}"
ehash = f"{ehash}.session"
elif getattr(cls, '_HISTORYTYPE', None):
ehash = f"{ehash}.history"
if ehash in PLEXOBJECTS:
raise Exception(f'Ambiguous PlexObject definition {cls.__name__}(tag={cls.TAG}, type={etype}) '
f'with {PLEXOBJECTS[ehash].__name__}')
Expand Down
41 changes: 40 additions & 1 deletion plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from urllib.parse import quote_plus

from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject, PlexSession
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
Expand Down Expand Up @@ -1119,3 +1119,42 @@ def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Clip._loadData(self, data)
PlexSession._loadData(self, data)


@utils.registerPlexObject
class MovieHistory(PlexHistory, Movie):
""" Represents a single Movie history entry
loaded from :func:`~plexapi.server.PlexServer.history`.
"""
_HISTORYTYPE = True

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Movie._loadData(self, data)
PlexHistory._loadData(self, data)


@utils.registerPlexObject
class EpisodeHistory(PlexHistory, Episode):
""" Represents a single Episode history entry
loaded from :func:`~plexapi.server.PlexServer.history`.
"""
_HISTORYTYPE = True

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Episode._loadData(self, data)
PlexHistory._loadData(self, data)


@utils.registerPlexObject
class ClipHistory(PlexHistory, Clip):
""" Represents a single Clip history entry
loaded from :func:`~plexapi.server.PlexServer.history`.
"""
_HISTORYTYPE = True

def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Clip._loadData(self, data)
PlexHistory._loadData(self, data)
1 change: 0 additions & 1 deletion tests/test_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,6 @@ def test_audio_Track_attrs(album):
assert utils.is_datetime(track.updatedAt)
assert utils.is_int(track.viewCount, gte=0)
assert track.viewOffset == 0
assert track.viewedAt is None
assert track.year is None
assert track.url(None) is None
assert media.aspectRatio is None
Expand Down
19 changes: 19 additions & 0 deletions tests/test_history.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from . import conftest as utils


def test_history_Movie(movie):
Expand Down Expand Up @@ -83,6 +84,24 @@ def test_history_MyServer(plex, movie):
movie.markUnplayed()


def test_history_PlexHistory(plex, movie):
movie.markPlayed()
history = plex.history()
assert len(history)

hist = history[0]
assert hist.source() == movie
assert hist.accountID
assert hist.deviceID
assert hist.historyKey
assert utils.is_datetime(hist.viewedAt)
assert hist.guid is None
hist.delete()

history = plex.history()
assert hist not in history


def test_history_User(account, shared_username):
user = account.user(shared_username)
history = user.history()
Expand Down
7 changes: 0 additions & 7 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,6 @@ def test_server_playlists(plex, show):
playlist.delete()


def test_server_history(plex, movie):
movie.markPlayed()
history = plex.history()
assert len(history)
movie.markUnplayed()


def test_server_Server_query(plex):
assert plex.query("/")
with pytest.raises(NotFound):
Expand Down
1 change: 0 additions & 1 deletion tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ def test_video_Movie_attrs(movies):
assert movie.userRating is None
assert movie.viewCount == 0
assert utils.is_int(movie.viewOffset, gte=0)
assert movie.viewedAt is None
assert movie.year == 2009
# Audio
audio = movie.media[0].parts[0].audioStreams()[0]
Expand Down