Handle special Plex library sections (#49525)

This commit is contained in:
jjlawren 2021-04-28 12:58:05 -05:00 committed by GitHub
parent 5e07ab17b2
commit 8c311cbaa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 6 deletions

View File

@ -1,4 +1,6 @@
"""Models to represent various Plex objects used in the integration."""
import logging
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
@ -7,7 +9,15 @@ from homeassistant.components.media_player.const import (
)
from homeassistant.util import dt as dt_util
LIVE_TV_SECTION = -4
LIVE_TV_SECTION = "Live TV"
TRANSIENT_SECTION = "Preroll"
UNKNOWN_SECTION = "Unknown"
SPECIAL_SECTIONS = {
-2: TRANSIENT_SECTION,
-4: LIVE_TV_SECTION,
}
_LOGGER = logging.getLogger(__name__)
class PlexSession:
@ -66,8 +76,15 @@ class PlexSession:
if media.duration:
self.media_duration = int(media.duration / 1000)
if media.librarySectionID == LIVE_TV_SECTION:
self.media_library_title = "Live TV"
if media.librarySectionID in SPECIAL_SECTIONS:
self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID]
elif media.librarySectionID < 1:
self.media_library_title = UNKNOWN_SECTION
_LOGGER.warning(
"Unknown library section ID (%s) for title '%s', please create an issue",
media.librarySectionID,
media.title,
)
else:
self.media_library_title = (
media.section().title if media.librarySectionID is not None else ""
@ -115,7 +132,7 @@ class PlexSession:
"""Get the image URL from a media object."""
thumb_url = media.thumbUrl
if media.type == "episode" and not self.plex_server.option_use_episode_art:
if media.librarySectionID == LIVE_TV_SECTION:
if SPECIAL_SECTIONS.get(media.librarySectionID) == LIVE_TV_SECTION:
thumb_url = media.grandparentThumb
else:
thumb_url = media.url(media.grandparentThumb)

View File

@ -278,6 +278,30 @@ def session_plexweb_fixture():
return load_fixture("plex/session_plexweb.xml")
@pytest.fixture(name="session_transient", scope="session")
def session_transient_fixture():
"""Load a transient session payload and return it."""
return load_fixture("plex/session_transient.xml")
@pytest.fixture(name="session_unknown", scope="session")
def session_unknown_fixture():
"""Load a hypothetical unknown session payload and return it."""
return load_fixture("plex/session_unknown.xml")
@pytest.fixture(name="session_live_tv", scope="session")
def session_live_tv_fixture():
"""Load a Live TV session payload and return it."""
return load_fixture("plex/session_live_tv.xml")
@pytest.fixture(name="livetv_sessions", scope="session")
def livetv_sessions_fixture():
"""Load livetv/sessions payload and return it."""
return load_fixture("plex/livetv_sessions.xml")
@pytest.fixture(name="security_token", scope="session")
def security_token_fixture():
"""Load a security token payload and return it."""
@ -393,18 +417,23 @@ def mock_plex_calls(
def setup_plex_server(
hass,
entry,
livetv_sessions,
mock_websocket,
mock_plex_calls,
requests_mock,
empty_payload,
session_default,
session_live_tv,
session_photo,
session_plexweb,
session_transient,
session_unknown,
):
"""Set up and return a mocked Plex server instance."""
async def _wrapper(**kwargs):
"""Wrap the fixture to allow passing arguments to the setup method."""
url = plex_server_url(entry)
config_entry = kwargs.get("config_entry", entry)
disable_clients = kwargs.pop("disable_clients", False)
disable_gdm = kwargs.pop("disable_gdm", True)
@ -415,10 +444,16 @@ def setup_plex_server(
session = session_plexweb
elif session_type == "photo":
session = session_photo
elif session_type == "live_tv":
session = session_live_tv
requests_mock.get(f"{url}/livetv/sessions/live_tv_1", text=livetv_sessions)
elif session_type == "transient":
session = session_transient
elif session_type == "unknown":
session = session_unknown
else:
session = session_default
url = plex_server_url(entry)
requests_mock.get(f"{url}/status/sessions", text=session)
if disable_clients:

View File

@ -8,13 +8,24 @@ import plexapi
import requests
import homeassistant.components.plex.const as const
from homeassistant.components.plex.models import (
LIVE_TV_SECTION,
TRANSIENT_SECTION,
UNKNOWN_SECTION,
)
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE
from homeassistant.const import (
CONF_TOKEN,
CONF_URL,
CONF_VERIFY_SSL,
STATE_IDLE,
STATE_PLAYING,
)
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@ -108,6 +119,66 @@ async def test_setup_with_photo_session(hass, entry, setup_plex_server):
assert sensor.state == "0"
async def test_setup_with_live_tv_session(hass, entry, setup_plex_server):
"""Test setup component with a Live TV session."""
await setup_plex_server(session_type="live_tv")
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
await hass.async_block_till_done()
media_player = hass.states.get(
"media_player.plex_plex_for_android_tv_shield_android_tv"
)
assert media_player.state == STATE_PLAYING
assert media_player.attributes["media_library_title"] == LIVE_TV_SECTION
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == "1"
async def test_setup_with_transient_session(hass, entry, setup_plex_server):
"""Test setup component with a transient session."""
await setup_plex_server(session_type="transient")
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
await hass.async_block_till_done()
media_player = hass.states.get(
"media_player.plex_plex_for_android_tv_shield_android_tv"
)
assert media_player.state == STATE_PLAYING
assert media_player.attributes["media_library_title"] == TRANSIENT_SECTION
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == "1"
async def test_setup_with_unknown_session(hass, entry, setup_plex_server):
"""Test setup component with an unknown session."""
await setup_plex_server(session_type="unknown")
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
await hass.async_block_till_done()
media_player = hass.states.get(
"media_player.plex_plex_for_android_tv_shield_android_tv"
)
assert media_player.state == STATE_PLAYING
assert media_player.attributes["media_library_title"] == UNKNOWN_SECTION
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == "1"
async def test_setup_when_certificate_changed(
hass,
requests_mock,

17
tests/fixtures/plex/livetv_sessions.xml vendored Normal file
View File

@ -0,0 +1,17 @@
<MediaContainer size="1">
<Video ratingKey="203216" guid="plex://episode/6083464579af59002d1e35bc" type="episode" title="The Price Is Right" titleSort="Price Is Right" summary="" index="105" year="2021" addedAt="1619538677" genuineMediaAnalysis="1" grandparentThumb="https://metadata-static.plex.tv/5/gracenote/55af94d4c6e3139f2cd925cd6436d514.jpg" grandparentTitle="The Price Is Right" key="/livetv/sessions/live_tv_1" live="1" parentIndex="49">
<Media id="382631" width="1920" height="1080" aspectRatio="1.78" audioChannels="6" audioCodec="ac3" videoCodec="mpeg2video" videoResolution="1080" container="mpegts" origin="livetv" protocol="hls" uuid="live_tv_1">
<Part id="383069" container="mpegts" deepAnalysisVersion="4" protocol="hls">
<Stream id="770097" streamType="1" codec="mpeg2video" index="0" closedCaptions="1" frameRate="29.970" height="1080" level="4" pixelAspectRatio="1:1" profile="main" scanType="interlaced" width="1920" displayTitle="1080i (MPEG2VIDEO)" extendedDisplayTitle="1080i (MPEG2VIDEO)">
</Stream>
<Stream id="770098" streamType="2" selected="1" codec="ac3" index="1" channels="6" bitrate="384" audioChannelLayout="5.1(side)" requiredBandwidths="384,384,384,384,384,384,384,384" samplingRate="48000" displayTitle="Unknown (AC3 5.1)" extendedDisplayTitle="Unknown (AC3 5.1)">
</Stream>
<Stream id="770099" streamType="2" codec="ac3" index="2" channels="2" bitrate="192" audioChannelLayout="stereo" requiredBandwidths="192,192,192,192,192,192,192,192" samplingRate="48000" displayTitle="Unknown (AC3 Stereo)" extendedDisplayTitle="Unknown (AC3 Stereo)">
</Stream>
<Stream id="770100" streamType="3" codec="eia_608" index="3" embeddedInVideo="1" displayTitle="Unknown (Closed Captions)" extendedDisplayTitle="Unknown (Closed Captions)">
</Stream>
</Part>
<TranscodeSession key="/transcode/sessions/a3d01c51-5480-4474-a0bb-b7a19c96eff0" throttled="0" complete="0" progress="-1" size="-22" speed="0.69999998807907104" duration="7200000" context="static" sourceVideoCodec="" sourceAudioCodec="" videoDecision="copy" audioDecision="copy" protocol="hls" container="mpegts" videoCodec="*" audioCodec="*" audioChannels="2" transcodeHwRequested="1" transcodeHwFullPipeline="0" timeStamp="1619538677.7417972" maxOffsetAvailable="314.764633" minOffsetAvailable="0" />
</Media>
</Video>
</MediaContainer>

14
tests/fixtures/plex/session_live_tv.xml vendored Normal file
View File

@ -0,0 +1,14 @@
<MediaContainer size="1">
<Video addedAt="1619538677" genuineMediaAnalysis="1" grandparentThumb="https://metadata-static.plex.tv/5/gracenote/55af94d4c6e3139f2cd925cd6436d514.jpg" grandparentTitle="The Price Is Right" guid="plex://episode/6083464579af59002d1e35bc" index="105" key="/livetv/sessions/live_tv_1" librarySectionID="-4" live="1" parentIndex="49" ratingKey="203216" sessionKey="97" summary="" title="The Price Is Right" titleSort="Price Is Right" type="episode" year="2021">
<Media id="382631" origin="livetv" uuid="live_tv_1" audioChannels="6" audioCodec="*" container="mkv" height="1080" videoCodec="*" videoFrameRate="NTSC" videoResolution="1080p" width="1920" selected="1">
<Part id="383069" container="mkv" height="1080" width="1920" decision="transcode" selected="1">
<Stream bitrate="2147483647" closedCaptions="1" codec="*" displayTitle="1080i (MPEG2VIDEO)" extendedDisplayTitle="1080i (MPEG2VIDEO)" frameRate="29.969999999999999" height="1080" id="770097" streamType="1" width="1920" decision="copy" location="direct" />
<Stream bitrate="384" bitrateMode="cbr" channels="6" codec="*" displayTitle="Unknown (AC3 5.1)" extendedDisplayTitle="Unknown (AC3 5.1)" id="770098" selected="1" streamType="2" decision="copy" location="direct" />
</Part>
</Media>
<User id="1" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User 1" />
<Player address="1.2.3.11" device="SHIELD Android TV" deviceClass="stb" local="1" machineIdentifier="1234567890123456-com-plexapp-android" model="darcy" platform="Android" platformVersion="9" product="Plex for Android (TV)" profile="Android" protocolVersion="1" relayed="0" remotePublicAddress="10.20.30.40" secure="1" state="playing" title="SHIELD Android TV" userID="1" vendor="NVIDIA" version="8.9.2.21619" />
<Session id="session_id_1" bandwidth="10000000" location="lan" />
<TranscodeSession key="/transcode/sessions/g3z46t06s5y0qp6pimggs49g" throttled="0" complete="0" progress="-1" size="-22" speed="2" duration="7200000" context="streaming" sourceVideoCodec="mpeg2video" sourceAudioCodec="ac3" videoDecision="copy" audioDecision="copy" protocol="http" container="mkv" videoCodec="*" audioCodec="*" audioChannels="6" transcodeHwRequested="1" transcodeHwFullPipeline="0" timeStamp="1619538677.7417972" maxOffsetAvailable="304.55399999999997" minOffsetAvailable="0" />
</Video>
</MediaContainer>

View File

@ -0,0 +1,13 @@
<MediaContainer size="1">
<Video addedAt="1619533889" duration="6000" genuineMediaAnalysis="1" guid="analyzed.prerolls://c1226b1fd517d6d7177ac8bebf24e0165e5feeb6" index="1" key="/library/metadata/203214" librarySectionID="-2" ratingKey="203214" sessionKey="95" summary="" thumb="/library/metadata/203214/thumb/1619533890" title="fancy_preroll" type="clip" updatedAt="1619533890" viewOffset="0">
<Media audioProfile="lc" id="382629" origin="local" videoProfile="main" audioChannels="2" audioCodec="aac" bitrate="1886" container="mp4" duration="6016" height="404" optimizedForStreaming="1" protocol="dash" videoCodec="h264" videoFrameRate="60p" videoResolution="SD" width="720" selected="1">
<Part audioProfile="lc" id="383067" videoProfile="main" bitrate="1886" container="mp4" duration="6016" height="404" optimizedForStreaming="1" protocol="dash" width="720" decision="transcode" selected="1">
<Stream bitrate="1724" codec="h264" colorPrimaries="bt709" default="1" displayTitle="1080p (H.264)" extendedDisplayTitle="1080p (H.264)" frameRate="59.939999999999998" height="404" id="770091" language="English" languageCode="eng" streamType="1" width="720" decision="transcode" location="segments-video" />
<Stream bitrate="162" bitrateMode="cbr" channels="2" codec="aac" default="1" displayTitle="English (AAC Stereo)" extendedDisplayTitle="English (AAC Stereo)" id="770092" language="English" languageCode="eng" selected="1" streamType="2" decision="transcode" location="segments-audio" />
</Part>
</Media>
<Session bandwidth="1981" id="session_id_1" location="lan" />
<User id="1" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User 1" />
<Player address="1.2.3.11" device="SHIELD Android TV" deviceClass="stb" local="1" machineIdentifier="1234567890123456-com-plexapp-android" model="darcy" platform="Android" platformVersion="9" product="Plex for Android (TV)" profile="Android" protocolVersion="1" relayed="0" remotePublicAddress="10.20.30.40" secure="1" state="playing" title="SHIELD Android TV" userID="1" vendor="NVIDIA" version="8.9.2.21619" />
</Video>
</MediaContainer>

13
tests/fixtures/plex/session_unknown.xml vendored Normal file
View File

@ -0,0 +1,13 @@
<MediaContainer size="1">
<Video addedAt="1619533889" duration="99999" genuineMediaAnalysis="1" guid="no_idea" index="1" key="/library/metadata/9876543210" librarySectionID="-3" ratingKey="9876543210" sessionKey="999" summary="" thumb="/library/metadata/9876543210/thumb/1619533890" title="some_unknown_item" type="clip" updatedAt="1619533890" viewOffset="0">
<Media audioProfile="lc" id="382629" origin="local" videoProfile="main" audioChannels="2" audioCodec="aac" bitrate="1886" container="mp4" duration="6016" height="404" optimizedForStreaming="1" protocol="dash" videoCodec="h264" videoFrameRate="60p" videoResolution="SD" width="720" selected="1">
<Part audioProfile="lc" id="383067" videoProfile="main" bitrate="1886" container="mp4" duration="6016" height="404" optimizedForStreaming="1" protocol="dash" width="720" decision="transcode" selected="1">
<Stream bitrate="1724" codec="h264" colorPrimaries="bt709" default="1" displayTitle="1080p (H.264)" extendedDisplayTitle="1080p (H.264)" frameRate="59.939999999999998" height="404" id="770091" language="English" languageCode="eng" streamType="1" width="720" decision="transcode" location="segments-video" />
<Stream bitrate="162" bitrateMode="cbr" channels="2" codec="aac" default="1" displayTitle="English (AAC Stereo)" extendedDisplayTitle="English (AAC Stereo)" id="770092" language="English" languageCode="eng" selected="1" streamType="2" decision="transcode" location="segments-audio" />
</Part>
</Media>
<Session bandwidth="1981" id="session_id_1" location="lan" />
<User id="1" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User 1" />
<Player address="1.2.3.11" device="SHIELD Android TV" deviceClass="stb" local="1" machineIdentifier="1234567890123456-com-plexapp-android" model="darcy" platform="Android" platformVersion="9" product="Plex for Android (TV)" profile="Android" protocolVersion="1" relayed="0" remotePublicAddress="10.20.30.40" secure="1" state="playing" title="SHIELD Android TV" userID="1" vendor="NVIDIA" version="8.9.2.21619" />
</Video>
</MediaContainer>