mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add "Recommended" and radio station support to Plex media browser (#64057)
This commit is contained in:
parent
a371f8f788
commit
34d0f2ffd7
@ -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:
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
59
tests/components/plex/fixtures/hubs.xml
Normal file
59
tests/components/plex/fixtures/hubs.xml
Normal 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&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&sort=lastViewedAt:desc&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>
|
11
tests/components/plex/fixtures/hubs_library_section.xml
Normal file
11
tests/components/plex/fixtures/hubs_library_section.xml
Normal 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>
|
@ -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"]
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user