diff --git a/plexapi/base.py b/plexapi/base.py index 86b4f15be..913f0d75a 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -18,7 +18,7 @@ MediaContainerT = TypeVar('MediaContainerT', bound='MediaContainer') USER_DONT_RELOAD_FOR_KEYS: set[str] = set() -_DONT_RELOAD_FOR_KEYS: set[str] = {'key', 'sourceURI'} +_DONT_RELOAD_FOR_KEYS: set[str] = {'centroid', 'key', 'sourceURI'} OPERATORS = { 'exact': lambda v, q: v == q, 'iexact': lambda v, q: v.lower() == q.lower(), diff --git a/plexapi/library.py b/plexapi/library.py index da4e80491..99f74b34f 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -710,10 +710,19 @@ def resetManagedHubs(self): key = f'/hubs/sections/{self.key}/manage' self._server.query(key, method=self._server._session.delete) - def hubs(self): + def hubs(self, **kwargs): """ Returns a list of available :class:`~plexapi.library.Hub` for this library section. - """ - key = self._buildQueryKey(f'/hubs/sections/{self.key}', includeStations=1) + + Parameters: + **kwargs (dict): Optional query parameters to add to the request + (e.g. ``count=10`` to limit the number of items per hub or + ``includeMyMixes=True`` to include the personalized "Mixes For You" hub). + ``includeStations`` is included by default and may be overridden + with ``includeStations=False``. + """ + kwargs.setdefault('includeStations', 1) + kwargs = {k: 1 if v is True else 0 if v is False else v for k, v in kwargs.items()} + key = self._buildQueryKey(f'/hubs/sections/{self.key}', **kwargs) return self.fetchItems(key) def agents(self): diff --git a/plexapi/playlist.py b/plexapi/playlist.py index c42517b16..9fb98d6c3 100644 --- a/plexapi/playlist.py +++ b/plexapi/playlist.py @@ -21,6 +21,8 @@ class Playlist( TYPE (str): 'playlist' addedAt (datetime): Datetime the playlist was added to the server. allowSync (bool): True if you allow syncing playlists. + centroid (:class:`~plexapi.audio.Artist`): The centroid artist a personalized + 'Mix For You' playlist is built around, or None for regular playlists. composite (str): URL to composite image (/playlist//composite/) content (str): The filter URI string for smart playlists. duration (int): Duration of the playlist in milliseconds. @@ -76,6 +78,11 @@ def _loadData(self, data): def fields(self): return self.findItems(self._data, media.Field) + @cached_data_property + def centroid(self): + from plexapi.audio import Artist + return self.findItem(self._data, cls=Artist, centroid='1') + def __len__(self): # pragma: no cover return len(self.items()) diff --git a/tests/payloads.py b/tests/payloads.py index 8c9252ffc..9b20cd289 100644 --- a/tests/payloads.py +++ b/tests/payloads.py @@ -44,3 +44,15 @@ """ + +MUSIC_MIXES_HUB = """ + + + + + + + + + +""" diff --git a/tests/test_library.py b/tests/test_library.py index 2cae6b3cb..a95008ab1 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -391,6 +391,15 @@ def test_library_MovieSection_PlexWebURL_hub(plex, movies): assert quote_plus(hub.key) in url +def test_library_MusicSection_hubs_kwargs(music): + hubs = music.hubs(count=5) + assert hubs + for hub in hubs: + assert len(hub._partialItems) <= 5 + hubs = music.hubs(includeStations=False) + assert not any(h.context == "hub.music.stations" for h in hubs) + + def test_library_ShowSection_all(tvshows): assert len(tvshows.all(title__iexact="The 100")) diff --git a/tests/test_playlist.py b/tests/test_playlist.py index 7c2b1be5a..41914fb37 100644 --- a/tests/test_playlist.py +++ b/tests/test_playlist.py @@ -6,6 +6,7 @@ from . import conftest as utils from . import test_mixins +from .payloads import MUSIC_MIXES_HUB def test_Playlist_attrs(playlist): @@ -31,6 +32,25 @@ def test_Playlist_attrs(playlist): assert playlist.isVideo is True assert playlist.isAudio is False assert playlist.isPhoto is False + assert playlist.centroid is None + + +def test_Playlist_centroid(plex, music, requests_mock): + # 'Mix For You' playlists cannot be generated on the bootstrap server, + # so serve a representative hubs response and parse it through the normal path. + requests_mock.get(plex.url(f"/hubs/sections/{music.key}?includeMyMixes=1"), text=MUSIC_MIXES_HUB) + hubs = music.hubs(includeMyMixes=True) + mix = next(h for h in hubs if h.context == "hub.music.mixes").items()[0] + assert mix.smart is True + centroid = mix.centroid + assert centroid.type == "artist" + assert centroid.title == "Centroid Artist" + assert centroid.thumb == "/library/metadata/100/thumb/1" + # accessing centroid on a partial playlist without one must not trigger a reload + other = next(h for h in hubs if h.context == "hub.music.playlists").items()[0] + assert other.isPartialObject() + assert other.centroid is None + assert len(requests_mock.request_history) == 1 def test_Playlist_create(plex, show):