Skip to content

Commit 44dfaa6

Browse files
committed
Allow query parameters in LibrarySection.hubs() and add Playlist.centroid
* LibrarySection.hubs() now accepts optional query parameters (e.g. count=10, includeMyMixes=1), matching Library.hubs(); includeStations=1 remains the default and may be overridden. * Add Playlist.centroid returning the nested centroid Artist a personalized 'Mix For You' playlist is built around (its only artwork source), guarded against auto-reload on partial objects.
1 parent 29439ab commit 44dfaa6

6 files changed

Lines changed: 56 additions & 3 deletions

File tree

plexapi/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
MediaContainerT = TypeVar("MediaContainerT", bound="MediaContainer")
1717

1818
USER_DONT_RELOAD_FOR_KEYS = set()
19-
_DONT_RELOAD_FOR_KEYS = {'key', 'sourceURI'}
19+
_DONT_RELOAD_FOR_KEYS = {'centroid', 'key', 'sourceURI'}
2020
OPERATORS = {
2121
'exact': lambda v, q: v == q,
2222
'iexact': lambda v, q: v.lower() == q.lower(),

plexapi/library.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,10 +710,17 @@ def resetManagedHubs(self):
710710
key = f'/hubs/sections/{self.key}/manage'
711711
self._server.query(key, method=self._server._session.delete)
712712

713-
def hubs(self):
713+
def hubs(self, **kwargs):
714714
""" Returns a list of available :class:`~plexapi.library.Hub` for this library section.
715+
716+
Parameters:
717+
**kwargs (dict): Optional query parameters to add to the request
718+
(e.g. ``count=10`` to limit the number of items per hub or
719+
``includeMyMixes=1`` to include the personalized "Mixes For You" hub).
720+
``includeStations=1`` is included by default and may be overridden.
715721
"""
716-
key = self._buildQueryKey(f'/hubs/sections/{self.key}', includeStations=1)
722+
kwargs.setdefault('includeStations', 1)
723+
key = self._buildQueryKey(f'/hubs/sections/{self.key}', **kwargs)
717724
return self.fetchItems(key)
718725

719726
def agents(self):

plexapi/playlist.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class Playlist(
2121
TYPE (str): 'playlist'
2222
addedAt (datetime): Datetime the playlist was added to the server.
2323
allowSync (bool): True if you allow syncing playlists.
24+
centroid (:class:`~plexapi.audio.Artist`): The centroid artist a personalized
25+
'Mix For You' playlist is built around, or None for regular playlists.
2426
composite (str): URL to composite image (/playlist/<ratingKey>/composite/<compositeid>)
2527
content (str): The filter URI string for smart playlists.
2628
duration (int): Duration of the playlist in milliseconds.
@@ -76,6 +78,11 @@ def _loadData(self, data):
7678
def fields(self):
7779
return self.findItems(self._data, media.Field)
7880

81+
@cached_data_property
82+
def centroid(self):
83+
from plexapi.audio import Artist
84+
return self.findItem(self._data, cls=Artist, centroid='1')
85+
7986
def __len__(self): # pragma: no cover
8087
return len(self.items())
8188

tests/payloads.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@
4444
</Invite>
4545
</MediaContainer>
4646
"""
47+
48+
MUSIC_MIXES_HUB = """<MediaContainer size="2" identifier="com.plexapp.plugins.library" librarySectionID="1" librarySectionTitle="Music">
49+
<Hub hubIdentifier="music.mixes.1" context="hub.music.mixes" type="playlist" title="Mixes For You" size="1" more="0" style="shelf">
50+
<Playlist guid="" type="playlist" title="Centroid Mix" summary="" smart="1" playlistType="" icon="playlist://image.smart" leafCount="0" key="/library/sections/1/all?artist.id=&amp;or=1&amp;album.id=1,2,3">
51+
<Directory centroid="1" ratingKey="100" key="/library/metadata/100/children" guid="plex://artist/abc" type="artist" title="Centroid Artist" librarySectionTitle="Music" librarySectionID="1" librarySectionKey="/library/sections/1" thumb="/library/metadata/100/thumb/1"/>
52+
</Playlist>
53+
</Hub>
54+
<Hub hubIdentifier="music.playlists.1" context="hub.music.playlists" type="playlist" title="Playlists" size="1" more="0" style="shelf">
55+
<Playlist ratingKey="200" guid="com.plexapp.agents.none://xxx" type="playlist" title="Ordinary Playlist" summary="" smart="0" playlistType="audio" composite="/playlists/200/composite/1" leafCount="5" key="/playlists/200/items"/>
56+
</Hub>
57+
</MediaContainer>
58+
"""

tests/test_library.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,13 @@ def test_library_MovieSection_PlexWebURL_hub(plex, movies):
391391
assert quote_plus(hub.key) in url
392392

393393

394+
def test_library_MusicSection_hubs_kwargs(music):
395+
hubs = music.hubs(count=5)
396+
assert hubs
397+
for hub in hubs:
398+
assert len(hub._partialItems) <= 5
399+
400+
394401
def test_library_ShowSection_all(tvshows):
395402
assert len(tvshows.all(title__iexact="The 100"))
396403

tests/test_playlist.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from . import conftest as utils
88
from . import test_mixins
9+
from .payloads import MUSIC_MIXES_HUB
910

1011

1112
def test_Playlist_attrs(playlist):
@@ -31,6 +32,25 @@ def test_Playlist_attrs(playlist):
3132
assert playlist.isVideo is True
3233
assert playlist.isAudio is False
3334
assert playlist.isPhoto is False
35+
assert playlist.centroid is None
36+
37+
38+
def test_Playlist_centroid(plex, music, requests_mock):
39+
# 'Mix For You' playlists cannot be generated on the bootstrap server,
40+
# so serve a representative hubs response and parse it through the normal path.
41+
requests_mock.get(plex.url(f"/hubs/sections/{music.key}"), text=MUSIC_MIXES_HUB)
42+
hubs = music.hubs(includeMyMixes=1)
43+
mix = next(h for h in hubs if h.context == "hub.music.mixes").items()[0]
44+
assert mix.smart is True
45+
centroid = mix.centroid
46+
assert centroid.type == "artist"
47+
assert centroid.title == "Centroid Artist"
48+
assert centroid.thumb == "/library/metadata/100/thumb/1"
49+
# accessing centroid on a partial playlist without one must not trigger a reload
50+
other = next(h for h in hubs if h.context == "hub.music.playlists").items()[0]
51+
assert other.isPartialObject()
52+
assert other.centroid is None
53+
assert len(requests_mock.request_history) == 1
3454

3555

3656
def test_Playlist_create(plex, show):

0 commit comments

Comments
 (0)