Add "Recommended" and radio station support to Plex media browser (#64057)

This commit is contained in:
jjlawren 2022-01-25 15:02:23 -06:00 committed by GitHub
parent a371f8f788
commit 34d0f2ffd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 279 additions and 83 deletions

View File

@ -13,6 +13,10 @@ def pretty_title(media, short_name=False):
title = f"{media.seasonEpisode.upper()} - {media.title}"
if not short_name:
title = f"{media.grandparentTitle} - {title}"
elif media.type == "season":
title = media.title
if not short_name:
title = f"{media.parentTitle} - {title}"
elif media.type == "track":
title = f"{media.index}. {media.title}"
else:

View File

@ -1,5 +1,4 @@
"""Support to interface with the Plex API."""
from itertools import islice
import logging
from homeassistant.components.media_player import BrowseMedia
@ -25,6 +24,7 @@ class UnknownMediaType(BrowseError):
"""Unknown media type."""
HUB_PREFIX = "hub:"
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
PLAYLISTS_BROWSE_PAYLOAD = {
"title": "Playlists",
@ -34,20 +34,17 @@ PLAYLISTS_BROWSE_PAYLOAD = {
"can_play": False,
"can_expand": True,
}
LIBRARY_PREFERRED_LIBTYPE = {
"show": "episode",
"artist": "album",
}
ITEM_TYPE_MEDIA_CLASS = {
"album": MEDIA_CLASS_ALBUM,
"artist": MEDIA_CLASS_ARTIST,
"clip": MEDIA_CLASS_VIDEO,
"episode": MEDIA_CLASS_EPISODE,
"mixed": MEDIA_CLASS_DIRECTORY,
"movie": MEDIA_CLASS_MOVIE,
"playlist": MEDIA_CLASS_PLAYLIST,
"season": MEDIA_CLASS_SEASON,
"show": MEDIA_CLASS_TV_SHOW,
"station": MEDIA_CLASS_ARTIST,
"track": MEDIA_CLASS_TRACK,
"video": MEDIA_CLASS_VIDEO,
}
@ -93,11 +90,7 @@ def browse_media( # noqa: C901
"""Create response payload to describe contents of a specific library."""
library = entity.plex_server.library.sectionByID(library_id)
library_info = library_section_payload(library)
library_info.children = []
library_info.children.append(special_library_payload(library_info, "On Deck"))
library_info.children.append(
special_library_payload(library_info, "Recently Added")
)
library_info.children = [special_library_payload(library_info, "Recommended")]
for item in library.all():
try:
library_info.children.append(item_payload(item))
@ -130,6 +123,9 @@ def browse_media( # noqa: C901
return None
if media_info.can_expand:
media_info.children = []
if media.TYPE == "artist":
if (station := media.station()) is not None:
media_info.children.append(station_payload(station))
for item in media:
try:
media_info.children.append(item_payload(item, short_name=True))
@ -137,6 +133,43 @@ def browse_media( # noqa: C901
continue
return media_info
if media_content_id and media_content_id.startswith(HUB_PREFIX):
media_content_id = media_content_id[len(HUB_PREFIX) :]
location, hub_identifier = media_content_id.split(":")
if location == "server":
hub = next(
x
for x in entity.plex_server.library.hubs()
if x.hubIdentifier == hub_identifier
)
media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}"
else:
library_section = entity.plex_server.library.sectionByID(int(location))
hub = next(
x for x in library_section.hubs() if x.hubIdentifier == hub_identifier
)
media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}"
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[hub.type]
except KeyError as err:
raise BrowseError(f"Unknown type received: {hub.type}") from err
payload = {
"title": hub.title,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": media_content_id,
"media_content_type": hub.type,
"can_play": False,
"can_expand": True,
"children": [],
"children_media_class": children_media_class,
}
for item in hub.items:
if hub.type == "station":
payload["children"].append(station_payload(item))
else:
payload["children"].append(item_payload(item))
return BrowseMedia(**payload)
if media_content_id and ":" in media_content_id:
media_content_id, special_folder = media_content_id.split(":")
else:
@ -183,27 +216,11 @@ def browse_media( # noqa: C901
"children_media_class": children_media_class,
}
if special_folder == "On Deck":
items = library_or_section.onDeck()
elif special_folder == "Recently Added":
if library_or_section.TYPE:
libtype = LIBRARY_PREFERRED_LIBTYPE.get(
library_or_section.TYPE, library_or_section.TYPE
)
items = library_or_section.recentlyAdded(libtype=libtype)
else:
recent_iter = (
x
for x in library_or_section.search(sort="addedAt:desc", limit=100)
if x.type in ["album", "episode", "movie"]
)
items = list(islice(recent_iter, 30))
for item in items:
try:
payload["children"].append(item_payload(item))
except UnknownMediaType:
continue
if special_folder == "Recommended":
for item in library_or_section.hubs():
if item.type == "photo":
continue
payload["children"].append(hub_payload(item))
return BrowseMedia(**payload)
@ -275,12 +292,39 @@ def server_payload(plex_server):
can_expand=True,
children_media_class=MEDIA_CLASS_DIRECTORY,
)
server_info.children = []
server_info.children.append(special_library_payload(server_info, "On Deck"))
server_info.children.append(special_library_payload(server_info, "Recently Added"))
server_info.children = [special_library_payload(server_info, "Recommended")]
for library in plex_server.library.sections():
if library.type == "photo":
continue
server_info.children.append(library_section_payload(library))
server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD))
return server_info
def hub_payload(hub):
"""Create response payload for a hub."""
if hasattr(hub, "librarySectionID"):
media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}"
else:
media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}"
payload = {
"title": hub.title,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": media_content_id,
"media_content_type": hub.type,
"can_play": False,
"can_expand": True,
}
return BrowseMedia(**payload)
def station_payload(station):
"""Create response payload for a music station."""
return BrowseMedia(
title=station.title,
media_class=ITEM_TYPE_MEDIA_CLASS[station.type],
media_content_id=station.key,
media_content_type="station",
can_play=True,
can_expand=False,
)

View File

@ -486,6 +486,16 @@ class PlexMediaPlayer(MediaPlayerEntity):
"Plex integration configured without a token, playback may fail"
)
if media_type == "station":
playqueue = self.plex_server.create_station_playqueue(media_id)
try:
self.device.playMedia(playqueue)
except requests.exceptions.ConnectTimeout as exc:
raise HomeAssistantError(
f"Request failed when playing on {self.name}"
) from exc
return
src = json.loads(media_id)
if isinstance(src, int):
src = {"plex_key": src}

View File

@ -599,6 +599,10 @@ class PlexServer:
"""Create playqueue on Plex server."""
return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs)
def create_station_playqueue(self, key):
"""Create playqueue on Plex server using a radio station key."""
return plexapi.playqueue.PlayQueue.fromStationKey(self._plex_server, key)
def get_playqueue(self, playqueue_id):
"""Retrieve existing playqueue from Plex server."""
return plexapi.playqueue.PlayQueue.get(self._plex_server, playqueue_id)

View File

@ -368,6 +368,18 @@ def sonos_resources_fixture():
return load_fixture("plex/sonos_resources.xml")
@pytest.fixture(name="hubs", scope="session")
def hubs_fixture():
"""Load hubs resource payload and return it."""
return load_fixture("plex/hubs.xml")
@pytest.fixture(name="hubs_music_library", scope="session")
def hubs_music_library_fixture():
"""Load music library hubs resource payload and return it."""
return load_fixture("plex/hubs_library_section.xml")
@pytest.fixture(name="entry")
def mock_config_entry():
"""Return the default mocked config entry."""

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<MediaContainer size="8" allowSync="1" identifier="com.plexapp.plugins.library">
<Hub hubKey="/library/metadata/1,35" key="/hubs/home/continueWatching" title="Continue Watching" type="mixed" hubIdentifier="home.continue" context="hub.home.continue" size="2" more="1" style="hero" promoted="1">
<Video ratingKey="1" key="/library/metadata/1" guid="plex://movie/5d776b85594b2b001e6dc641" studio="Studio Entertainment" type="movie" title="Movie 1" librarySectionTitle="Movies" librarySectionID="1" librarySectionKey="/library/sections/1" contentRating="R" summary="Some elaborate summary." rating="9.0" audienceRating="9.5" viewCount="1" lastViewedAt="1505969509" year="2000" tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" art="/library/metadata/1/art/1590245989" duration="9000000" originallyAvailableAt="2000-01-01" addedAt="1377829261" updatedAt="1590245989" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" primaryExtraKey="/library/metadata/195540" ratingImage="rottentomatoes://image.rating.ripe">
<Media aspectRatio="2.35" audioChannels="6" audioCodec="dca" audioProfile="dts" bitrate="7500" container="mkv" duration="9000000" height="544" id="2637" videoCodec="h264" videoFrameRate="24p" videoProfile="high" videoResolution="720" width="1280" />
<Part audioProfile="dts" container="mkv" duration="9000000" file="/storage/videos/video.mkv" id="4631" key="/library/parts/4631/1215643935/file.mkv" size="8500000000" videoProfile="high" />
<Stream bitDepth="8" bitrate="6000" chromaLocation="left" chromaSubsampling="4:2:0" codec="h264" codedHeight="544" codedWidth="1280" default="1" displayTitle="720p (H.264)" extendedDisplayTitle="x264 @ 6000 kbps (720p H.264)" frameRate="23.976" hasScalingMatrix="0" height="544" id="21428" index="0" language="English" languageCode="eng" level="51" profile="high" refFrames="8" scanType="progressive" streamType="1" title="x264 @ 6000 kbps" width="1280" />
<Stream audioChannelLayout="5.1(side)" bitDepth="16" bitrate="1500" channels="6" codec="dca" default="1" displayTitle="English (DTS 5.1)" extendedDisplayTitle="DTS 5.1 @ 1500 kbps (English)" id="21429" index="1" language="English" languageCode="eng" profile="dts" samplingRate="48000" selected="1" streamType="2" title="DTS 5.1 @ 1500 kbps" />
<Stream codec="srt" default="1" displayTitle="English (SRT)" extendedDisplayTitle="English (SRT)" id="21430" index="2" language="English" languageCode="eng" streamType="3" />
<Genre count="119" filter="genre=25578" id="25578" tag="Sci-Fi" />
<Genre count="197" filter="genre=87" id="87" tag="Action" />
<Director count="4" filter="director=100" id="100" tag="Famous Director" />
<Writer count="2" filter="writer=50000" id="50000" tag="A Writer" />
<Producer count="3" filter="producer=2000" id="2000" tag="Dr. Producer" />
<Country count="452" filter="country=1105" id="1105" tag="USA" />
<Role count="25" filter="actor=1" id="1" role="Character 1" tag="Actor 1" thumb="http://4.3.2.1/t/p/original/1.jpg" />
<Role count="2" filter="actor=2" id="2" role="Character 2" tag="Actor 2" thumb="http://4.3.2.1/t/p/original/2.jpg" />
<Role filter="actor=3" id="3" role="Character 3" tag="Actor 3" thumb="http://4.3.2.1/t/p/original/3.jpg" />
</Video>
<Video ratingKey="35" key="/library/metadata/35" parentRatingKey="20" grandparentRatingKey="30" guid="plex://episode/60d5ff1c4a0721002c4e5d75" parentGuid="plex://season/606b5e359efaef002c04bcdd" grandparentGuid="plex://show/5d9c07f7e98e47001eb03fc7" type="episode" title="Episode 5" grandparentKey="/library/metadata/30" parentKey="/library/metadata/20" grandparentTitle="TV Show" parentTitle="Season 1" contentRating="G" summary="Elaborate episode summary." index="5" parentIndex="1" audienceRating="8.0" viewCount="1" lastViewedAt="1631590710" parentYear="2021" thumb="/library/metadata/35/thumb/1631586401" art="/library/metadata/30/art/1630011134" parentThumb="/library/metadata/20/thumb/1630011155" grandparentThumb="/library/metadata/30/thumb/1630011134" grandparentArt="/library/metadata/30/art/1630011134" grandparentTheme="/library/metadata/30/theme/1630011134" duration="3421936" originallyAvailableAt="2021-09-19" addedAt="1631586399" updatedAt="1631586401" audienceRatingImage="themoviedb://image.rating">
<Media id="396946" duration="3421936" bitrate="14119" width="1920" height="1080" aspectRatio="1.78" audioChannels="6" audioCodec="eac3" videoCodec="h264" videoResolution="1080" container="mkv" videoFrameRate="24p" videoProfile="high">
<Part id="397384" key="/library/parts/397384/1631586385/file.mkv" duration="3421936" file="/storage/tvshows/TV Show/Season 1/Episode 5.mkv" size="6040228848" container="mkv" videoProfile="high" />
</Media>
<Genre tag="Action" />
<Genre tag="Animated" />
<Role tag="Some Actor" />
<Role tag="Another One" />
</Video>
</Hub>
<Hub key="/hubs/home/recentlyAdded?type=8" title="Recently Added Music" type="album" hubIdentifier="home.music.recent" context="hub.home.music.recent" size="0" more="0" style="shelf" promoted="1">
</Hub>
<Hub hubKey="/library/metadata/203184,203180,203181,203182,203183,203178" key="/hubs/home/recentlyAdded?type=13" title="Recently Added Photos" type="photo" hubIdentifier="home.photos.recent" context="hub.home.photos.recent" size="6" more="0" style="shelf" promoted="1">
<Photo ratingKey="203184" key="/library/metadata/203184" guid="com.plexapp.agents.none://203184?lang=en" type="photo" title="tmpENmh_T" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2021" thumb="/library/metadata/203184/thumb/1616695947" originallyAvailableAt="2021-03-25" addedAt="1616695947" updatedAt="1616695947" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
<Photo ratingKey="203180" key="/library/metadata/203180" parentRatingKey="203179" guid="com.plexapp.agents.none://203180?lang=en" type="photo" title="1 (1)" parentKey="/library/metadata/203179" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2021" thumb="/library/metadata/203180/thumb/1616695149" originallyAvailableAt="2021-03-20" addedAt="1616694060" updatedAt="1616695149" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
<Photo ratingKey="203181" key="/library/metadata/203181" parentRatingKey="203179" guid="com.plexapp.agents.none://203181?lang=en" type="photo" title="2 (1)" parentKey="/library/metadata/203179" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2021" thumb="/library/metadata/203181/thumb/1616695149" originallyAvailableAt="2021-03-20" addedAt="1616694060" updatedAt="1616695149" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
<Photo ratingKey="203182" key="/library/metadata/203182" parentRatingKey="203179" guid="com.plexapp.agents.none://203182?lang=en" type="photo" title="3" parentKey="/library/metadata/203179" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2021" thumb="/library/metadata/203182/thumb/1616695149" originallyAvailableAt="2021-03-20" addedAt="1616694060" updatedAt="1616695149" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
<Photo ratingKey="203183" key="/library/metadata/203183" parentRatingKey="203179" guid="com.plexapp.agents.none://203183?lang=en" type="photo" title="4" parentKey="/library/metadata/203179" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2021" thumb="/library/metadata/203183/thumb/1616695149" originallyAvailableAt="2021-03-20" addedAt="1616694060" updatedAt="1616695149" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
<Photo ratingKey="203178" key="/library/metadata/203178" guid="com.plexapp.agents.none://203178?lang=en" type="photo" title="tsy-4" librarySectionTitle="Photos" librarySectionID="11" librarySectionKey="/library/sections/11" summary="" index="1" year="2017" thumb="/library/metadata/203178/thumb/1616695149" originallyAvailableAt="2017-04-11" addedAt="1491918998" updatedAt="1616695149" createdAtAccuracy="local" createdAtTZOffset="-18000">
</Photo>
</Hub>
<Hub key="/hubs/home/recentlyAdded?type=1&amp;personal=1" title="Recently Added Videos" type="clip" hubIdentifier="home.videos.recent" context="hub.home.videos.recent" size="0" more="0" style="shelf" promoted="1">
</Hub>
<Hub hubKey="/library/metadata/500,501" key="/playlists/all?type=15&amp;sort=lastViewedAt:desc&amp;playlistType=video,audio" title="Recent Playlists" type="playlist" hubIdentifier="home.playlists" context="hub.home.playlists" size="2" more="0" style="shelf" promoted="1">
<Playlist ratingKey="500" key="/playlists/500/items" guid="com.plexapp.agents.none://9a8f4a48-dd89-40e0-955b-286285350fdf" type="playlist" title="Playlist 1" summary="" smart="0" playlistType="video" composite="/playlists/500/composite/1597983847" viewCount="2" lastViewedAt="1568512403" duration="5054000" leafCount="1" addedAt="1505969338" updatedAt="1597983847">
</Playlist>
<Playlist ratingKey="501" key="/playlists/501/items" guid="com.plexapp.agents.none://9a8f4a48-dd89-40e0-955b-286285350fdf" type="playlist" title="Playlist 2" summary="" smart="0" playlistType="video" composite="/playlists/501/composite/1597983847" viewCount="5" lastViewedAt="1568512403" duration="5054000" leafCount="1" addedAt="1505969339" updatedAt="1597983847">
</Playlist>
</Hub>
</MediaContainer>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<MediaContainer size="1" allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014">
<Hub title="Stations" type="station" hubIdentifier="music.stations.5" context="hub.music.stations" size="3" more="0" style="grid">
<Playlist key="/library/sections/3/stations/1" guid="tv.plex://station/library" type="playlist" title="Library Radio" librarySectionTitle="Music" librarySectionID="3" librarySectionKey="/library/sections/3" summary="" smart="1" playlistType="audio" leafCount="0" icon="playlist://image.radio" radio="1">
</Playlist>
<Playlist key="/library/sections/3/stations/2" guid="tv.plex://station/history" type="playlist" title="Time Travel Radio" librarySectionTitle="Music" librarySectionID="3" librarySectionKey="/library/sections/3" summary="" smart="1" playlistType="audio" leafCount="0" icon="playlist://image.radio" radio="1">
</Playlist>
<Playlist key="/library/sections/3/stations/3" guid="tv.plex://station/album" type="playlist" title="Random Album Radio" librarySectionTitle="Music" librarySectionID="3" librarySectionKey="/library/sections/3" summary="" smart="1" playlistType="audio" leafCount="0" icon="playlist://image.radio" radio="1">
</Playlist>
</Hub>
</MediaContainer>

View File

@ -15,6 +15,8 @@ from .const import DEFAULT_DATA
class MockPlexShow:
"""Mock a plexapi Season instance."""
TAG = "Directory"
TYPE = "show"
ratingKey = 30
title = "TV Show"
type = "show"
@ -27,8 +29,11 @@ class MockPlexShow:
class MockPlexSeason:
"""Mock a plexapi Season instance."""
TAG = "Directory"
TYPE = "season"
ratingKey = 20
title = "Season 1"
parentTitle = "TV Show"
type = "season"
year = 2021
@ -40,6 +45,7 @@ class MockPlexSeason:
class MockPlexEpisode:
"""Mock a plexapi Episode instance."""
TAG = "Video"
ratingKey = 10
title = "Episode 1"
grandparentTitle = "TV Show"
@ -50,6 +56,8 @@ class MockPlexEpisode:
class MockPlexArtist:
"""Mock a plexapi Artist instance."""
TAG = "Directory"
TYPE = "artist"
ratingKey = 300
title = "Artist"
type = "artist"
@ -58,10 +66,15 @@ class MockPlexArtist:
"""Iterate over albums."""
yield MockPlexAlbum()
def station(self):
"""Mock the station artist method."""
return MockPlexStation()
class MockPlexAlbum:
"""Mock a plexapi Album instance."""
TAG = "Directory"
ratingKey = 200
parentTitle = "Artist"
title = "Album"
@ -76,19 +89,30 @@ class MockPlexAlbum:
class MockPlexTrack:
"""Mock a plexapi Track instance."""
TAG = "Track"
index = 1
ratingKey = 100
title = "Track 1"
type = "track"
class MockPlexStation:
"""Mock a plexapi radio station instance."""
TAG = "Playlist"
key = "/library/sections/3/stations/1"
title = "Radio Station"
radio = True
type = "playlist"
async def test_browse_media(
hass,
hass_ws_client,
mock_plex_server,
requests_mock,
library_movies_filtertypes,
empty_payload,
hubs,
hubs_music_library,
):
"""Test getting Plex clients from plex.tv."""
websocket_client = await hass_ws_client(hass)
@ -130,13 +154,18 @@ async def test_browse_media(
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "server"
assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER]
# Library Sections + On Deck + Recently Added + Playlists
assert len(result["children"]) == len(mock_plex_server.library.sections()) + 3
# Library Sections + Recommended + Playlists
assert len(result["children"]) == len(mock_plex_server.library.sections()) + 2
music = next(iter(x for x in result["children"] if x["title"] == "Music"))
tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows"))
playlists = next(iter(x for x in result["children"] if x["title"] == "Playlists"))
special_keys = ["On Deck", "Recently Added"]
special_keys = ["Recommended"]
requests_mock.get(
f"{mock_plex_server.url_in_use}/hubs",
text=hubs,
)
# Browse into a special folder (server)
msg_id += 1
@ -160,27 +189,46 @@ async def test_browse_media(
result[ATTR_MEDIA_CONTENT_ID]
== f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}"
)
assert len(result["children"]) == len(mock_plex_server.library.onDeck())
assert len(result["children"]) == 4 # Hardcoded in fixture
assert result["children"][0]["media_content_type"] == "mixed"
assert result["children"][1]["media_content_type"] == "album"
assert result["children"][2]["media_content_type"] == "clip"
assert result["children"][3]["media_content_type"] == "playlist"
# Browse into a special folder (server): Continue Watching
msg_id += 1
await websocket_client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": media_players[0],
ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE],
ATTR_MEDIA_CONTENT_ID: result["children"][0][ATTR_MEDIA_CONTENT_ID],
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == msg_id
assert msg["type"] == TYPE_RESULT
assert msg["success"]
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "mixed"
requests_mock.get(
f"{mock_plex_server.url_in_use}/hubs/sections/3?includeStations=1",
text=hubs_music_library,
)
# Browse into a special folder (library)
requests_mock.get(
f"{mock_plex_server.url_in_use}/library/sections/1/all?includeMeta=1",
text=library_movies_filtertypes,
)
requests_mock.get(
f"{mock_plex_server.url_in_use}/library/sections/1/collections?includeMeta=1",
text=empty_payload,
)
msg_id += 1
library_section_id = next(iter(mock_plex_server.library.sections())).key
library_section_id = 3
await websocket_client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": media_players[0],
ATTR_MEDIA_CONTENT_TYPE: "library",
ATTR_MEDIA_CONTENT_ID: f"{library_section_id}:{special_keys[1]}",
ATTR_MEDIA_CONTENT_ID: f"{library_section_id}:{special_keys[0]}",
}
)
@ -190,11 +238,30 @@ async def test_browse_media(
assert msg["success"]
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
assert result[ATTR_MEDIA_CONTENT_ID] == f"{library_section_id}:{special_keys[1]}"
assert len(result["children"]) == len(
mock_plex_server.library.sectionByID(library_section_id).recentlyAdded()
assert result[ATTR_MEDIA_CONTENT_ID] == f"{library_section_id}:{special_keys[0]}"
assert len(result["children"]) == 1
# Browse into a library radio station hub
msg_id += 1
await websocket_client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": media_players[0],
ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE],
ATTR_MEDIA_CONTENT_ID: result["children"][0][ATTR_MEDIA_CONTENT_ID],
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == msg_id
assert msg["type"] == TYPE_RESULT
assert msg["success"]
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "station"
assert len(result["children"]) == 3
assert result["children"][0]["title"] == "Library Radio"
# Browse into a Plex TV show library
msg_id += 1
await websocket_client.send_json(
@ -214,10 +281,10 @@ async def test_browse_media(
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "library"
result_id = int(result[ATTR_MEDIA_CONTENT_ID])
# All items in section + On Deck + Recently Added
# All items in section + Hubs
assert (
len(result["children"])
== len(mock_plex_server.library.sectionByID(result_id).all()) + 2
== len(mock_plex_server.library.sectionByID(result_id).all()) + 1
)
# Browse into a Plex TV show
@ -278,7 +345,10 @@ async def test_browse_media(
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "season"
result_id = int(result[ATTR_MEDIA_CONTENT_ID])
assert result["title"] == f"{mock_season.title} ({mock_season.year})"
assert (
result["title"]
== f"{mock_season.parentTitle} - {mock_season.title} ({mock_season.year})"
)
assert (
result["children"][0]["title"]
== f"{mock_episode.seasonEpisode.upper()} - {mock_episode.title}"
@ -332,7 +402,8 @@ async def test_browse_media(
result_id = int(result[ATTR_MEDIA_CONTENT_ID])
assert result[ATTR_MEDIA_CONTENT_TYPE] == "artist"
assert result["title"] == mock_artist.title
assert result["children"][0]["title"] == f"{mock_album.title} ({mock_album.year})"
assert result["children"][0]["title"] == "Radio Station"
assert result["children"][1]["title"] == f"{mock_album.title} ({mock_album.year})"
# Browse into a Plex album
msg_id += 1
@ -400,26 +471,3 @@ async def test_browse_media(
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "playlists"
result_id = result[ATTR_MEDIA_CONTENT_ID]
# Browse recently added items
msg_id += 1
mock_items = [MockPlexAlbum(), MockPlexEpisode(), MockPlexSeason(), MockPlexTrack()]
with patch("plexapi.library.Library.search", return_value=mock_items) as mock_fetch:
await websocket_client.send_json(
{
"id": msg_id,
"type": "media_player/browse_media",
"entity_id": media_players[0],
ATTR_MEDIA_CONTENT_TYPE: "server",
ATTR_MEDIA_CONTENT_ID: f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[1]}",
}
)
msg = await websocket_client.receive_json()
assert msg["success"]
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "server"
result_id = result[ATTR_MEDIA_CONTENT_ID]
for child in result["children"]:
assert child["media_content_type"] in ["album", "episode"]
assert child["media_content_type"] not in ["season", "track"]

View File

@ -33,6 +33,7 @@ class MockPlexMedia:
class MockPlexClip(MockPlexMedia):
"""Minimal mock of plexapi clip object."""
TAG = "Video"
type = "clip"
title = "Clip 1"
@ -40,6 +41,7 @@ class MockPlexClip(MockPlexMedia):
class MockPlexMovie(MockPlexMedia):
"""Minimal mock of plexapi movie object."""
TAG = "Video"
type = "movie"
title = "Movie 1"
@ -47,6 +49,7 @@ class MockPlexMovie(MockPlexMedia):
class MockPlexMusic(MockPlexMedia):
"""Minimal mock of plexapi album object."""
TAG = "Directory"
listType = "audio"
type = "album"
title = "Album"
@ -56,6 +59,7 @@ class MockPlexMusic(MockPlexMedia):
class MockPlexTVEpisode(MockPlexMedia):
"""Minimal mock of plexapi episode object."""
TAG = "Video"
type = "episode"
title = "Episode 5"
grandparentTitle = "TV Show"