mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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."""
|
"""Models to represent various Plex objects used in the integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
MEDIA_TYPE_MOVIE,
|
MEDIA_TYPE_MOVIE,
|
||||||
MEDIA_TYPE_MUSIC,
|
MEDIA_TYPE_MUSIC,
|
||||||
@ -7,7 +9,15 @@ from homeassistant.components.media_player.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.util import dt as dt_util
|
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:
|
class PlexSession:
|
||||||
@ -66,8 +76,15 @@ class PlexSession:
|
|||||||
if media.duration:
|
if media.duration:
|
||||||
self.media_duration = int(media.duration / 1000)
|
self.media_duration = int(media.duration / 1000)
|
||||||
|
|
||||||
if media.librarySectionID == LIVE_TV_SECTION:
|
if media.librarySectionID in SPECIAL_SECTIONS:
|
||||||
self.media_library_title = "Live TV"
|
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:
|
else:
|
||||||
self.media_library_title = (
|
self.media_library_title = (
|
||||||
media.section().title if media.librarySectionID is not None else ""
|
media.section().title if media.librarySectionID is not None else ""
|
||||||
@ -115,7 +132,7 @@ class PlexSession:
|
|||||||
"""Get the image URL from a media object."""
|
"""Get the image URL from a media object."""
|
||||||
thumb_url = media.thumbUrl
|
thumb_url = media.thumbUrl
|
||||||
if media.type == "episode" and not self.plex_server.option_use_episode_art:
|
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
|
thumb_url = media.grandparentThumb
|
||||||
else:
|
else:
|
||||||
thumb_url = media.url(media.grandparentThumb)
|
thumb_url = media.url(media.grandparentThumb)
|
||||||
|
@ -278,6 +278,30 @@ def session_plexweb_fixture():
|
|||||||
return load_fixture("plex/session_plexweb.xml")
|
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")
|
@pytest.fixture(name="security_token", scope="session")
|
||||||
def security_token_fixture():
|
def security_token_fixture():
|
||||||
"""Load a security token payload and return it."""
|
"""Load a security token payload and return it."""
|
||||||
@ -393,18 +417,23 @@ def mock_plex_calls(
|
|||||||
def setup_plex_server(
|
def setup_plex_server(
|
||||||
hass,
|
hass,
|
||||||
entry,
|
entry,
|
||||||
|
livetv_sessions,
|
||||||
mock_websocket,
|
mock_websocket,
|
||||||
mock_plex_calls,
|
mock_plex_calls,
|
||||||
requests_mock,
|
requests_mock,
|
||||||
empty_payload,
|
empty_payload,
|
||||||
session_default,
|
session_default,
|
||||||
|
session_live_tv,
|
||||||
session_photo,
|
session_photo,
|
||||||
session_plexweb,
|
session_plexweb,
|
||||||
|
session_transient,
|
||||||
|
session_unknown,
|
||||||
):
|
):
|
||||||
"""Set up and return a mocked Plex server instance."""
|
"""Set up and return a mocked Plex server instance."""
|
||||||
|
|
||||||
async def _wrapper(**kwargs):
|
async def _wrapper(**kwargs):
|
||||||
"""Wrap the fixture to allow passing arguments to the setup method."""
|
"""Wrap the fixture to allow passing arguments to the setup method."""
|
||||||
|
url = plex_server_url(entry)
|
||||||
config_entry = kwargs.get("config_entry", entry)
|
config_entry = kwargs.get("config_entry", entry)
|
||||||
disable_clients = kwargs.pop("disable_clients", False)
|
disable_clients = kwargs.pop("disable_clients", False)
|
||||||
disable_gdm = kwargs.pop("disable_gdm", True)
|
disable_gdm = kwargs.pop("disable_gdm", True)
|
||||||
@ -415,10 +444,16 @@ def setup_plex_server(
|
|||||||
session = session_plexweb
|
session = session_plexweb
|
||||||
elif session_type == "photo":
|
elif session_type == "photo":
|
||||||
session = session_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:
|
else:
|
||||||
session = session_default
|
session = session_default
|
||||||
|
|
||||||
url = plex_server_url(entry)
|
|
||||||
requests_mock.get(f"{url}/status/sessions", text=session)
|
requests_mock.get(f"{url}/status/sessions", text=session)
|
||||||
|
|
||||||
if disable_clients:
|
if disable_clients:
|
||||||
|
@ -8,13 +8,24 @@ import plexapi
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
import homeassistant.components.plex.const as const
|
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 (
|
from homeassistant.config_entries import (
|
||||||
ENTRY_STATE_LOADED,
|
ENTRY_STATE_LOADED,
|
||||||
ENTRY_STATE_NOT_LOADED,
|
ENTRY_STATE_NOT_LOADED,
|
||||||
ENTRY_STATE_SETUP_ERROR,
|
ENTRY_STATE_SETUP_ERROR,
|
||||||
ENTRY_STATE_SETUP_RETRY,
|
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
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
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"
|
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(
|
async def test_setup_when_certificate_changed(
|
||||||
hass,
|
hass,
|
||||||
requests_mock,
|
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