mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Handle special Plex library sections (#49525)
This commit is contained in:
parent
5e07ab17b2
commit
8c311cbaa0
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
17
tests/fixtures/plex/livetv_sessions.xml
vendored
Normal 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
14
tests/fixtures/plex/session_live_tv.xml
vendored
Normal 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>
|
13
tests/fixtures/plex/session_transient.xml
vendored
Normal file
13
tests/fixtures/plex/session_transient.xml
vendored
Normal 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
13
tests/fixtures/plex/session_unknown.xml
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user