Skip to content
Open
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
2 changes: 1 addition & 1 deletion plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
15 changes: 12 additions & 3 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
7 changes: 7 additions & 0 deletions plexapi/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ratingKey>/composite/<compositeid>)
content (str): The filter URI string for smart playlists.
duration (int): Duration of the playlist in milliseconds.
Expand Down Expand Up @@ -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())

Expand Down
12 changes: 12 additions & 0 deletions tests/payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@
</Invite>
</MediaContainer>
"""

MUSIC_MIXES_HUB = """<MediaContainer size="2" identifier="com.plexapp.plugins.library" librarySectionID="1" librarySectionTitle="Music">
<Hub hubIdentifier="music.mixes.1" context="hub.music.mixes" type="playlist" title="Mixes For You" size="1" more="0" style="shelf">
<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">
<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"/>
</Playlist>
</Hub>
<Hub hubIdentifier="music.playlists.1" context="hub.music.playlists" type="playlist" title="Playlists" size="1" more="0" style="shelf">
<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"/>
</Hub>
</MediaContainer>
"""
9 changes: 9 additions & 0 deletions tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
20 changes: 20 additions & 0 deletions tests/test_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from . import conftest as utils
from . import test_mixins
from .payloads import MUSIC_MIXES_HUB


def test_Playlist_attrs(playlist):
Expand All @@ -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):
Expand Down