Rewrite Plex tests to use mocked payloads (#44044)

pull/44924/head
jjlawren 2021-01-07 12:56:52 -06:00 committed by GitHub
parent caf14b78d1
commit 0426b211f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1113 additions and 989 deletions

View File

@ -694,8 +694,6 @@ omit =
homeassistant/components/pjlink/media_player.py
homeassistant/components/plaato/*
homeassistant/components/plex/media_player.py
homeassistant/components/plex/models.py
homeassistant/components/plex/sensor.py
homeassistant/components/plum_lightpad/light.py
homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/*

View File

@ -146,7 +146,7 @@ class PlexServer:
available_servers = [
(x.name, x.clientIdentifier)
for x in self.account.resources()
if "server" in x.provides
if "server" in x.provides and x.presence
]
if not available_servers:

View File

@ -3,13 +3,261 @@ from unittest.mock import patch
import pytest
from homeassistant.components.plex.const import DOMAIN
from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS
from homeassistant.const import CONF_URL
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL
from .helpers import websocket_connected
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from .mock_classes import MockGDM
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, load_fixture
def plex_server_url(entry):
"""Return a protocol-less URL from a config entry."""
return entry.data[PLEX_SERVER_CONFIG][CONF_URL].split(":", 1)[-1]
@pytest.fixture(name="album", scope="session")
def album_fixture():
"""Load album payload and return it."""
return load_fixture("plex/album.xml")
@pytest.fixture(name="artist_albums", scope="session")
def artist_albums_fixture():
"""Load artist's albums payload and return it."""
return load_fixture("plex/artist_albums.xml")
@pytest.fixture(name="children_20", scope="session")
def children_20_fixture():
"""Load children payload for item 20 and return it."""
return load_fixture("plex/children_20.xml")
@pytest.fixture(name="children_30", scope="session")
def children_30_fixture():
"""Load children payload for item 30 and return it."""
return load_fixture("plex/children_30.xml")
@pytest.fixture(name="children_200", scope="session")
def children_200_fixture():
"""Load children payload for item 200 and return it."""
return load_fixture("plex/children_200.xml")
@pytest.fixture(name="children_300", scope="session")
def children_300_fixture():
"""Load children payload for item 300 and return it."""
return load_fixture("plex/children_300.xml")
@pytest.fixture(name="empty_library", scope="session")
def empty_library_fixture():
"""Load an empty library payload and return it."""
return load_fixture("plex/empty_library.xml")
@pytest.fixture(name="empty_payload", scope="session")
def empty_payload_fixture():
"""Load an empty payload and return it."""
return load_fixture("plex/empty_payload.xml")
@pytest.fixture(name="grandchildren_300", scope="session")
def grandchildren_300_fixture():
"""Load grandchildren payload for item 300 and return it."""
return load_fixture("plex/grandchildren_300.xml")
@pytest.fixture(name="library_movies_all", scope="session")
def library_movies_all_fixture():
"""Load payload for all items in the movies library and return it."""
return load_fixture("plex/library_movies_all.xml")
@pytest.fixture(name="library_tvshows_all", scope="session")
def library_tvshows_all_fixture():
"""Load payload for all items in the tvshows library and return it."""
return load_fixture("plex/library_tvshows_all.xml")
@pytest.fixture(name="library_music_all", scope="session")
def library_music_all_fixture():
"""Load payload for all items in the music library and return it."""
return load_fixture("plex/library_music_all.xml")
@pytest.fixture(name="library_movies_sort", scope="session")
def library_movies_sort_fixture():
"""Load sorting payload for movie library and return it."""
return load_fixture("plex/library_movies_sort.xml")
@pytest.fixture(name="library_tvshows_sort", scope="session")
def library_tvshows_sort_fixture():
"""Load sorting payload for tvshow library and return it."""
return load_fixture("plex/library_tvshows_sort.xml")
@pytest.fixture(name="library_music_sort", scope="session")
def library_music_sort_fixture():
"""Load sorting payload for music library and return it."""
return load_fixture("plex/library_music_sort.xml")
@pytest.fixture(name="library", scope="session")
def library_fixture():
"""Load library payload and return it."""
return load_fixture("plex/library.xml")
@pytest.fixture(name="library_sections", scope="session")
def library_sections_fixture():
"""Load library sections payload and return it."""
return load_fixture("plex/library_sections.xml")
@pytest.fixture(name="media_1", scope="session")
def media_1_fixture():
"""Load media payload for item 1 and return it."""
return load_fixture("plex/media_1.xml")
@pytest.fixture(name="media_30", scope="session")
def media_30_fixture():
"""Load media payload for item 30 and return it."""
return load_fixture("plex/media_30.xml")
@pytest.fixture(name="media_100", scope="session")
def media_100_fixture():
"""Load media payload for item 100 and return it."""
return load_fixture("plex/media_100.xml")
@pytest.fixture(name="media_200", scope="session")
def media_200_fixture():
"""Load media payload for item 200 and return it."""
return load_fixture("plex/media_200.xml")
@pytest.fixture(name="player_plexweb_resources", scope="session")
def player_plexweb_resources_fixture():
"""Load resources payload for a Plex Web player and return it."""
return load_fixture("plex/player_plexweb_resources.xml")
@pytest.fixture(name="playlists", scope="session")
def playlists_fixture():
"""Load payload for all playlists and return it."""
return load_fixture("plex/playlists.xml")
@pytest.fixture(name="playlist_500", scope="session")
def playlist_500_fixture():
"""Load payload for playlist 500 and return it."""
return load_fixture("plex/playlist_500.xml")
@pytest.fixture(name="playqueue_created", scope="session")
def playqueue_created_fixture():
"""Load payload for playqueue creation response and return it."""
return load_fixture("plex/playqueue_created.xml")
@pytest.fixture(name="plex_server_accounts", scope="session")
def plex_server_accounts_fixture():
"""Load payload accounts on the Plex server and return it."""
return load_fixture("plex/plex_server_accounts.xml")
@pytest.fixture(name="plex_server_base", scope="session")
def plex_server_base_fixture():
"""Load base payload for Plex server info and return it."""
return load_fixture("plex/plex_server_base.xml")
@pytest.fixture(name="plex_server_default", scope="session")
def plex_server_default_fixture(plex_server_base):
"""Load default payload for Plex server info and return it."""
return plex_server_base.format(
name="Plex Server 1", machine_identifier="unique_id_123"
)
@pytest.fixture(name="plex_server_clients", scope="session")
def plex_server_clients_fixture():
"""Load available clients payload for Plex server and return it."""
return load_fixture("plex/plex_server_clients.xml")
@pytest.fixture(name="plextv_account", scope="session")
def plextv_account_fixture():
"""Load account info from plex.tv and return it."""
return load_fixture("plex/plextv_account.xml")
@pytest.fixture(name="plextv_resources_base", scope="session")
def plextv_resources_base_fixture():
"""Load base payload for plex.tv resources and return it."""
return load_fixture("plex/plextv_resources_base.xml")
@pytest.fixture(name="plextv_resources", scope="session")
def plextv_resources_fixture(plextv_resources_base):
"""Load default payload for plex.tv resources and return it."""
return plextv_resources_base.format(second_server_enabled=0)
@pytest.fixture(name="session_base", scope="session")
def session_base_fixture():
"""Load the base session payload and return it."""
return load_fixture("plex/session_base.xml")
@pytest.fixture(name="session_default", scope="session")
def session_default_fixture(session_base):
"""Load the default session payload and return it."""
return session_base.format(user_id=1)
@pytest.fixture(name="session_new_user", scope="session")
def session_new_user_fixture(session_base):
"""Load the new user session payload and return it."""
return session_base.format(user_id=1001)
@pytest.fixture(name="session_photo", scope="session")
def session_photo_fixture():
"""Load a photo session payload and return it."""
return load_fixture("plex/session_photo.xml")
@pytest.fixture(name="session_plexweb", scope="session")
def session_plexweb_fixture():
"""Load a Plex Web session payload and return it."""
return load_fixture("plex/session_plexweb.xml")
@pytest.fixture(name="security_token", scope="session")
def security_token_fixture():
"""Load a security token payload and return it."""
return load_fixture("plex/security_token.xml")
@pytest.fixture(name="show_seasons", scope="session")
def show_seasons_fixture():
"""Load a show's seasons payload and return it."""
return load_fixture("plex/show_seasons.xml")
@pytest.fixture(name="sonos_resources", scope="session")
def sonos_resources_fixture():
"""Load Sonos resources payload and return it."""
return load_fixture("plex/sonos_resources.xml")
@pytest.fixture(name="entry")
@ -23,14 +271,6 @@ def mock_config_entry():
)
@pytest.fixture
def mock_plex_account():
"""Mock the PlexAccount class and return the used instance."""
plex_account = MockPlexAccount()
with patch("plexapi.myplex.MyPlexAccount", return_value=plex_account):
yield plex_account
@pytest.fixture
def mock_websocket():
"""Mock the PlexWebsocket class."""
@ -39,15 +279,112 @@ def mock_websocket():
@pytest.fixture
def setup_plex_server(hass, entry, mock_plex_account, mock_websocket):
def mock_plex_calls(
entry,
requests_mock,
children_20,
children_30,
children_200,
children_300,
empty_library,
grandchildren_300,
library,
library_sections,
library_movies_all,
library_movies_sort,
library_music_all,
library_music_sort,
library_tvshows_all,
library_tvshows_sort,
media_1,
media_30,
media_100,
media_200,
playlists,
playlist_500,
plextv_account,
plextv_resources,
plex_server_accounts,
plex_server_clients,
plex_server_default,
security_token,
):
"""Mock Plex API calls."""
requests_mock.get("https://plex.tv/users/account", text=plextv_account)
requests_mock.get("https://plex.tv/api/resources", text=plextv_resources)
url = plex_server_url(entry)
for server in [url, PLEX_DIRECT_URL]:
requests_mock.get(server, text=plex_server_default)
requests_mock.get(f"{server}/accounts", text=plex_server_accounts)
requests_mock.get(f"{url}/clients", text=plex_server_clients)
requests_mock.get(f"{url}/library", text=library)
requests_mock.get(f"{url}/library/sections", text=library_sections)
requests_mock.get(f"{url}/library/onDeck", text=empty_library)
requests_mock.get(f"{url}/library/sections/1/sorts", text=library_movies_sort)
requests_mock.get(f"{url}/library/sections/2/sorts", text=library_tvshows_sort)
requests_mock.get(f"{url}/library/sections/3/sorts", text=library_music_sort)
requests_mock.get(f"{url}/library/sections/1/all", text=library_movies_all)
requests_mock.get(f"{url}/library/sections/2/all", text=library_tvshows_all)
requests_mock.get(f"{url}/library/sections/3/all", text=library_music_all)
requests_mock.get(f"{url}/library/metadata/200/children", text=children_200)
requests_mock.get(f"{url}/library/metadata/300/children", text=children_300)
requests_mock.get(f"{url}/library/metadata/300/allLeaves", text=grandchildren_300)
requests_mock.get(f"{url}/library/metadata/1", text=media_1)
requests_mock.get(f"{url}/library/metadata/30", text=media_30)
requests_mock.get(f"{url}/library/metadata/100", text=media_100)
requests_mock.get(f"{url}/library/metadata/200", text=media_200)
requests_mock.get(f"{url}/library/metadata/20/children", text=children_20)
requests_mock.get(f"{url}/library/metadata/30/children", text=children_30)
requests_mock.get(f"{url}/playlists", text=playlists)
requests_mock.get(f"{url}/playlists/500/items", text=playlist_500)
requests_mock.get(f"{url}/security/token", text=security_token)
@pytest.fixture
def setup_plex_server(
hass,
entry,
mock_websocket,
mock_plex_calls,
requests_mock,
empty_payload,
session_default,
session_photo,
session_plexweb,
):
"""Set up and return a mocked Plex server instance."""
async def _wrapper(**kwargs):
"""Wrap the fixture to allow passing arguments to the MockPlexServer instance."""
"""Wrap the fixture to allow passing arguments to the setup method."""
config_entry = kwargs.get("config_entry", entry)
disable_clients = kwargs.pop("disable_clients", False)
disable_gdm = kwargs.pop("disable_gdm", True)
plex_server = MockPlexServer(**kwargs)
with patch("plexapi.server.PlexServer", return_value=plex_server), patch(
client_type = kwargs.pop("client_type", None)
session_type = kwargs.pop("session_type", None)
if client_type == "plexweb":
session = session_plexweb
elif session_type == "photo":
session = session_photo
else:
session = session_default
url = plex_server_url(entry)
requests_mock.get(f"{url}/status/sessions", text=session)
if disable_clients:
requests_mock.get(f"{url}/clients", text=empty_payload)
with patch(
"homeassistant.components.plex.GDM",
return_value=MockGDM(disabled=disable_gdm),
):
@ -56,6 +393,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket):
await hass.async_block_till_done()
websocket_connected(mock_websocket)
await hass.async_block_till_done()
plex_server = hass.data[DOMAIN][SERVERS][entry.unique_id]
return plex_server
return _wrapper

View File

@ -61,3 +61,5 @@ DEFAULT_OPTIONS = {
const.CONF_USE_EPISODE_ART: False,
}
}
PLEX_DIRECT_URL = "https://1-2-3-4.123456789001234567890.plex.direct:32400"

View File

@ -1,17 +1,4 @@
"""Mock classes used in tests."""
from functools import lru_cache
from aiohttp.helpers import reify
from plexapi.exceptions import NotFound
from homeassistant.components.plex.const import (
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
PLEX_SERVER_CONFIG,
)
from homeassistant.const import CONF_URL
from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS
GDM_SERVER_PAYLOAD = [
{
@ -94,520 +81,3 @@ class MockGDM:
self.entries = GDM_CLIENT_PAYLOAD
else:
self.entries = GDM_SERVER_PAYLOAD
class MockResource:
"""Mock a PlexAccount resource."""
def __init__(self, index, kind="server"):
"""Initialize the object."""
if kind == "server":
self.name = MOCK_SERVERS[index][CONF_SERVER]
self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name
CONF_SERVER_IDENTIFIER
]
self.provides = ["server"]
self.device = MockPlexServer(index)
else:
self.name = f"plex.tv Resource Player {index+10}"
self.clientIdentifier = f"client-{index+10}"
self.provides = ["player"]
self.device = MockPlexClient(
baseurl=f"http://192.168.0.1{index}:32500", index=index + 10
)
self.presence = index == 0
self.publicAddressMatches = True
def connect(self, timeout):
"""Mock the resource connect method."""
return self.device
class MockPlexAccount:
"""Mock a PlexAccount instance."""
def __init__(self, servers=1, players=3):
"""Initialize the object."""
self._resources = []
for index in range(servers):
self._resources.append(MockResource(index))
for index in range(players):
self._resources.append(MockResource(index, kind="player"))
def resource(self, name):
"""Mock the PlexAccount resource lookup method."""
return [x for x in self._resources if x.name == name][0]
def resources(self):
"""Mock the PlexAccount resources listing method."""
return self._resources
def sonos_speaker(self, speaker_name):
"""Mock the PlexAccount Sonos lookup method."""
return MockPlexSonosClient(speaker_name)
class MockPlexSystemAccount:
"""Mock a PlexSystemAccount instance."""
def __init__(self, index):
"""Initialize the object."""
# Start accountIDs at 1 to set proper owner.
self.name = list(MOCK_USERS)[index]
self.accountID = index + 1
class MockPlexServer:
"""Mock a PlexServer instance."""
def __init__(
self,
index=0,
config_entry=None,
num_users=len(MOCK_USERS),
session_type="video",
):
"""Initialize the object."""
if config_entry:
self._data = config_entry.data
else:
self._data = DEFAULT_DATA
self._baseurl = self._data[PLEX_SERVER_CONFIG][CONF_URL]
self.friendlyName = self._data[CONF_SERVER]
self.machineIdentifier = self._data[CONF_SERVER_IDENTIFIER]
self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users)))
self._clients = []
self._session = None
self._sessions = []
self.set_clients(num_users)
self.set_sessions(num_users, session_type)
self._cache = {}
def set_clients(self, num_clients):
"""Set up mock PlexClients for this PlexServer."""
self._clients = [
MockPlexClient(baseurl=self._baseurl, index=x) for x in range(num_clients)
]
def set_sessions(self, num_sessions, session_type):
"""Set up mock PlexSessions for this PlexServer."""
self._sessions = [
MockPlexSession(self._clients[x], mediatype=session_type, index=x)
for x in range(num_sessions)
]
def clear_clients(self):
"""Clear all active PlexClients."""
self._clients = []
def clear_sessions(self):
"""Clear all active PlexSessions."""
self._sessions = []
def clients(self):
"""Mock the clients method."""
return self._clients
def createToken(self):
"""Mock the createToken method."""
return "temporary_token"
def sessions(self):
"""Mock the sessions method."""
return self._sessions
def systemAccounts(self):
"""Mock the systemAccounts lookup method."""
return self._systemAccounts
def url(self, path, includeToken=False):
"""Mock method to generate a server URL."""
return f"{self._baseurl}{path}"
@property
def accounts(self):
"""Mock the accounts property."""
return set(MOCK_USERS)
@property
def version(self):
"""Mock version of PlexServer."""
return "1.0"
@reify
def library(self):
"""Mock library object of PlexServer."""
return MockPlexLibrary(self)
def playlist(self, playlist):
"""Mock the playlist lookup method."""
return MockPlexMediaItem(playlist, mediatype="playlist")
@lru_cache
def playlists(self):
"""Mock the playlists lookup method with a lazy init."""
return [
MockPlexPlaylist(
self.library.section("Movies").all()
+ self.library.section("TV Shows").all()
),
MockPlexPlaylist(self.library.section("Music").all()),
]
def fetchItem(self, item):
"""Mock the fetchItem method."""
for section in self.library.sections():
result = section.fetchItem(item)
if result:
return result
class MockPlexClient:
"""Mock a PlexClient instance."""
def __init__(self, server=None, baseurl=None, token=None, index=0):
"""Initialize the object."""
self.machineIdentifier = f"client-{index+1}"
self._baseurl = baseurl
self._index = index
def url(self, key):
"""Mock the url method."""
return f"{self._baseurl}{key}"
@property
def device(self):
"""Mock the device attribute."""
return "DEVICE"
@property
def platform(self):
"""Mock the platform attribute."""
return "PLATFORM"
@property
def product(self):
"""Mock the product attribute."""
if self._index == 1:
return "Plex Web"
return "PRODUCT"
@property
def protocolCapabilities(self):
"""Mock the protocolCapabilities attribute."""
return ["playback"]
@property
def state(self):
"""Mock the state attribute."""
return "playing"
@property
def title(self):
"""Mock the title attribute."""
return "TITLE"
@property
def version(self):
"""Mock the version attribute."""
return "1.0"
def proxyThroughServer(self, value=True, server=None):
"""Mock the proxyThroughServer method."""
pass
def playMedia(self, item):
"""Mock the playMedia method."""
pass
class MockPlexSession:
"""Mock a PlexServer.sessions() instance."""
def __init__(self, player, mediatype, index=0):
"""Initialize the object."""
self.TYPE = mediatype
self.usernames = [list(MOCK_USERS)[index]]
self.players = [player]
self._section = MockPlexLibrarySection("Movies")
self.sessionKey = index + 1
@property
def duration(self):
"""Mock the duration attribute."""
return 10000000
@property
def librarySectionID(self):
"""Mock the librarySectionID attribute."""
return 1
@property
def ratingKey(self):
"""Mock the ratingKey attribute."""
return 123
def section(self):
"""Mock the section method."""
return self._section
@property
def summary(self):
"""Mock the summary attribute."""
return "SUMMARY"
@property
def thumbUrl(self):
"""Mock the thumbUrl attribute."""
return "http://1.2.3.4/thumb"
@property
def title(self):
"""Mock the title attribute."""
return "TITLE"
@property
def type(self):
"""Mock the type attribute."""
return "movie"
@property
def viewOffset(self):
"""Mock the viewOffset attribute."""
return 0
@property
def year(self):
"""Mock the year attribute."""
return 2020
class MockPlexLibrary:
"""Mock a Plex Library instance."""
def __init__(self, plex_server):
"""Initialize the object."""
self._plex_server = plex_server
self._sections = {}
for kind in ["Movies", "Music", "TV Shows", "Photos"]:
self._sections[kind] = MockPlexLibrarySection(kind)
def section(self, title):
"""Mock the LibrarySection lookup."""
section = self._sections.get(title)
if section:
return section
raise NotFound
def sections(self):
"""Return all available sections."""
return self._sections.values()
def sectionByID(self, section_id):
"""Mock the sectionByID lookup."""
return [x for x in self.sections() if x.key == section_id][0]
def onDeck(self):
"""Mock an empty On Deck folder."""
return []
def recentlyAdded(self):
"""Mock an empty Recently Added folder."""
return []
class MockPlexLibrarySection:
"""Mock a Plex LibrarySection instance."""
def __init__(self, library):
"""Initialize the object."""
self.title = library
if library == "Music":
self._item = MockPlexArtist("Artist")
elif library == "TV Shows":
self._item = MockPlexShow("TV Show")
else:
self._item = MockPlexMediaItem(library[:-1])
def get(self, query):
"""Mock the get lookup method."""
if self._item.title == query:
return self._item
raise NotFound
def all(self):
"""Mock the all method."""
return [self._item]
def fetchItem(self, ratingKey):
"""Return a specific item."""
for item in self.all():
if item.ratingKey == ratingKey:
return item
if item._children:
for child in item._children:
if child.ratingKey == ratingKey:
return child
def onDeck(self):
"""Mock an empty On Deck folder."""
return []
def recentlyAdded(self):
"""Mock an empty Recently Added folder."""
return self.all()
@property
def type(self):
"""Mock the library type."""
if self.title == "Movies":
return "movie"
if self.title == "Music":
return "artist"
if self.title == "TV Shows":
return "show"
if self.title == "Photos":
return "photo"
@property
def TYPE(self):
"""Return the library type."""
return self.type
@property
def key(self):
"""Mock the key identifier property."""
return str(id(self.title))
def search(self, **kwargs):
"""Mock the LibrarySection search method."""
if kwargs.get("libtype") == "movie":
return self.all()
def update(self):
"""Mock the update call."""
pass
class MockPlexMediaItem:
"""Mock a Plex Media instance."""
def __init__(self, title, mediatype="video", year=2020):
"""Initialize the object."""
self.title = str(title)
self.type = mediatype
self.thumbUrl = "http://1.2.3.4/thumb.png"
self.year = year
self._children = []
def __iter__(self):
"""Provide iterator."""
yield from self._children
@property
def ratingKey(self):
"""Mock the ratingKey property."""
return id(self.title)
class MockPlexPlaylist(MockPlexMediaItem):
"""Mock a Plex Playlist instance."""
def __init__(self, items):
"""Initialize the object."""
super().__init__(f"Playlist ({len(items)} Items)", "playlist")
for item in items:
self._children.append(item)
class MockPlexShow(MockPlexMediaItem):
"""Mock a Plex Show instance."""
def __init__(self, show):
"""Initialize the object."""
super().__init__(show, "show")
for index in range(1, 5):
self._children.append(MockPlexSeason(index))
def season(self, season):
"""Mock the season lookup method."""
return [x for x in self._children if x.title == f"Season {season}"][0]
class MockPlexSeason(MockPlexMediaItem):
"""Mock a Plex Season instance."""
def __init__(self, season):
"""Initialize the object."""
super().__init__(f"Season {season}", "season")
for index in range(1, 10):
self._children.append(MockPlexMediaItem(f"Episode {index}", "episode"))
def episode(self, episode):
"""Mock the episode lookup method."""
return self._children[episode - 1]
class MockPlexAlbum(MockPlexMediaItem):
"""Mock a Plex Album instance."""
def __init__(self, album):
"""Initialize the object."""
super().__init__(album, "album")
for index in range(1, 10):
self._children.append(MockPlexMediaTrack(index))
def track(self, track):
"""Mock the track lookup method."""
try:
return [x for x in self._children if x.title == track][0]
except IndexError:
raise NotFound
def tracks(self):
"""Mock the tracks lookup method."""
return self._children
class MockPlexArtist(MockPlexMediaItem):
"""Mock a Plex Artist instance."""
def __init__(self, artist):
"""Initialize the object."""
super().__init__(artist, "artist")
self._album = MockPlexAlbum("Album")
def album(self, album):
"""Mock the album lookup method."""
return self._album
def get(self, track):
"""Mock the track lookup method."""
return self._album.track(track)
class MockPlexMediaTrack(MockPlexMediaItem):
"""Mock a Plex Track instance."""
def __init__(self, index=1):
"""Initialize the object."""
super().__init__(f"Track {index}", "track")
self.index = index
class MockPlexSonosClient:
"""Mock a PlexSonosClient instance."""
def __init__(self, name):
"""Initialize the object."""
self.name = name
def playMedia(self, item):
"""Mock the playMedia method."""
pass

View File

@ -10,7 +10,7 @@ from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE
from .const import DEFAULT_DATA
async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket):
async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_mock):
"""Test getting Plex clients from plex.tv."""
websocket_client = await hass_ws_client(hass)
@ -51,8 +51,10 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "server"
assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER]
assert len(result["children"]) == len(mock_plex_server.library.sections()) + len(
SPECIAL_METHODS
# Library Sections + Special Sections + Playlists
assert (
len(result["children"])
== len(mock_plex_server.library.sections()) + len(SPECIAL_METHODS) + 1
)
tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows"))
@ -149,9 +151,14 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock
result = msg["result"]
assert result[ATTR_MEDIA_CONTENT_TYPE] == "show"
result_id = int(result[ATTR_MEDIA_CONTENT_ID])
assert result["title"] == mock_plex_server.fetchItem(result_id).title
assert result["title"] == mock_plex_server.fetch_item(result_id).title
# Browse into a non-existent TV season
unknown_key = 99999999999999
requests_mock.get(
f"{mock_plex_server.url_in_use}/library/metadata/{unknown_key}", status_code=404
)
msg_id += 1
await websocket_client.send_json(
{
@ -159,7 +166,7 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock
"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: str(99999999999999),
ATTR_MEDIA_CONTENT_ID: str(unknown_key),
}
)

View File

@ -35,16 +35,11 @@ from homeassistant.const import (
CONF_URL,
CONF_VERIFY_SSL,
)
from homeassistant.setup import async_setup_component
from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN, PLEX_DIRECT_URL
from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import (
MockGDM,
MockPlexAccount,
MockPlexClient,
MockPlexServer,
MockResource,
)
from .mock_classes import MockGDM
from tests.common import MockConfigEntry
@ -82,7 +77,7 @@ async def test_bad_credentials(hass):
assert result["errors"][CONF_TOKEN] == "faulty_credentials"
async def test_bad_hostname(hass):
async def test_bad_hostname(hass, mock_plex_calls):
"""Test when an invalid address is provided."""
await async_process_ha_core_config(
hass,
@ -96,12 +91,9 @@ async def test_bad_hostname(hass):
assert result["step_id"] == "user"
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
), patch.object(
MockResource, "connect", side_effect=requests.exceptions.ConnectionError
), patch(
"plexauth.PlexAuth.initiate_auth"
), patch(
"plexapi.myplex.MyPlexResource.connect",
side_effect=requests.exceptions.ConnectionError,
), patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -148,8 +140,9 @@ async def test_unknown_exception(hass):
assert result["reason"] == "unknown"
async def test_no_servers_found(hass):
async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payload):
"""Test when no servers are on an account."""
requests_mock.get("https://plex.tv/api/resources", text=empty_payload)
await async_process_ha_core_config(
hass,
@ -162,9 +155,7 @@ async def test_no_servers_found(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)
), patch("plexauth.PlexAuth.initiate_auth"), patch(
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -181,11 +172,9 @@ async def test_no_servers_found(hass):
assert result["errors"]["base"] == "no_servers"
async def test_single_available_server(hass):
async def test_single_available_server(hass, mock_plex_calls):
"""Test creating an entry with one server available."""
mock_plex_server = MockPlexServer()
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@ -197,9 +186,7 @@ async def test_single_available_server(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
"plexapi.server.PlexServer", return_value=mock_plex_server
), patch("plexauth.PlexAuth.initiate_auth"), patch(
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -212,20 +199,27 @@ async def test_single_available_server(hass):
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "create_entry"
assert result["title"] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert result["title"] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
result["data"][CONF_SERVER_IDENTIFIER]
== mock_plex_server.machine_identifier
)
assert (
result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
)
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
async def test_multiple_servers_with_selection(hass):
async def test_multiple_servers_with_selection(
hass, mock_plex_calls, requests_mock, plextv_resources_base
):
"""Test creating an entry with multiple servers available."""
mock_plex_server = MockPlexServer()
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@ -237,11 +231,11 @@ async def test_multiple_servers_with_selection(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"plexauth.PlexAuth.initiate_auth"
), patch(
requests_mock.get(
"https://plex.tv/api/resources",
text=plextv_resources_base.format(second_server_enabled=1),
)
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -261,20 +255,27 @@ async def test_multiple_servers_with_selection(hass):
user_input={CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER]},
)
assert result["type"] == "create_entry"
assert result["title"] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert result["title"] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
result["data"][CONF_SERVER_IDENTIFIER]
== mock_plex_server.machine_identifier
)
assert (
result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
)
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
async def test_adding_last_unconfigured_server(hass):
async def test_adding_last_unconfigured_server(
hass, mock_plex_calls, requests_mock, plextv_resources_base
):
"""Test automatically adding last unconfigured server when multiple servers on account."""
mock_plex_server = MockPlexServer()
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@ -294,11 +295,12 @@ async def test_adding_last_unconfigured_server(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"plexauth.PlexAuth.initiate_auth"
), patch(
requests_mock.get(
"https://plex.tv/api/resources",
text=plextv_resources_base.format(second_server_enabled=1),
)
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -311,16 +313,25 @@ async def test_adding_last_unconfigured_server(hass):
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "create_entry"
assert result["title"] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert result["title"] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert (
result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
result["data"][CONF_SERVER_IDENTIFIER]
== mock_plex_server.machine_identifier
)
assert (
result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
)
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
async def test_all_available_servers_configured(hass):
async def test_all_available_servers_configured(
hass, entry, requests_mock, plextv_account, plextv_resources_base
):
"""Test when all available servers are already configured."""
await async_process_ha_core_config(
@ -328,13 +339,7 @@ async def test_all_available_servers_configured(hass):
{"internal_url": "http://example.local:8123"},
)
MockConfigEntry(
domain=DOMAIN,
data={
CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER],
CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER],
},
).add_to_hass(hass)
entry.add_to_hass(hass)
MockConfigEntry(
domain=DOMAIN,
@ -350,9 +355,13 @@ async def test_all_available_servers_configured(hass):
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2)
), patch("plexauth.PlexAuth.initiate_auth"), patch(
requests_mock.get("https://plex.tv/users/account", text=plextv_account)
requests_mock.get(
"https://plex.tv/api/resources",
text=plextv_resources_base.format(second_server_enabled=1),
)
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value=MOCK_TOKEN
):
result = await hass.config_entries.flow.async_configure(
@ -432,32 +441,22 @@ async def test_missing_option_flow(hass, entry, mock_plex_server):
}
async def test_option_flow_new_users_available(
hass, caplog, entry, mock_websocket, setup_plex_server
):
async def test_option_flow_new_users_available(hass, entry, setup_plex_server):
"""Test config options multiselect defaults when new Plex users are seen."""
OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}}
OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"User 1": {"enabled": True}}
entry.options = OPTIONS_OWNER_ONLY
with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient):
mock_plex_server = await setup_plex_server(
config_entry=entry, disable_gdm=False
)
await hass.async_block_till_done()
mock_plex_server = await setup_plex_server(config_entry=entry)
await hass.async_block_till_done()
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
new_users = [x for x in mock_plex_server.accounts if x not in monitored_users]
assert len(monitored_users) == 1
assert len(new_users) == 2
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
result = await hass.config_entries.options.async_init(
entry.entry_id, context={"source": "test"}, data=None
)
@ -465,7 +464,7 @@ async def test_option_flow_new_users_available(
assert result["step_id"] == "plex_mp_settings"
multiselect_defaults = result["data_schema"].schema["monitored_users"].options
assert "[Owner]" in multiselect_defaults["Owner"]
assert "[Owner]" in multiselect_defaults["User 1"]
for user in new_users:
assert "[New]" in multiselect_defaults[user]
@ -529,7 +528,7 @@ async def test_callback_view(hass, aiohttp_client):
assert resp.status == 200
async def test_manual_config(hass):
async def test_manual_config(hass, mock_plex_calls):
"""Test creating via manual configuration."""
await async_process_ha_core_config(
hass,
@ -587,8 +586,6 @@ async def test_manual_config(hass):
assert result["type"] == "form"
assert result["step_id"] == "manual_setup"
mock_plex_server = MockPlexServer()
MANUAL_SERVER = {
CONF_HOST: MOCK_SERVERS[0][CONF_HOST],
CONF_PORT: MOCK_SERVERS[0][CONF_PORT],
@ -647,26 +644,26 @@ async def test_manual_config(hass):
assert result["step_id"] == "manual_setup"
assert result["errors"]["base"] == "ssl_error"
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True
), patch(
with patch("homeassistant.components.plex.PlexWebsocket", autospec=True), patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MANUAL_SERVER
)
assert result["type"] == "create_entry"
assert result["title"] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert result["title"] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
async def test_manual_config_with_token(hass):
async def test_manual_config_with_token(hass, mock_plex_calls):
"""Test creating via manual configuration with only token."""
result = await hass.config_entries.flow.async_init(
@ -683,37 +680,36 @@ async def test_manual_config_with_token(hass):
assert result["type"] == "form"
assert result["step_id"] == "manual_setup"
mock_plex_server = MockPlexServer()
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
"plexapi.server.PlexServer", return_value=mock_plex_server
), patch(
with patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True
):
), patch("homeassistant.components.plex.PlexWebsocket", autospec=True):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN}
)
assert result["type"] == "create_entry"
assert result["title"] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
server_id = result["data"][CONF_SERVER_IDENTIFIER]
mock_plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert result["title"] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name
assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use
assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
async def test_setup_with_limited_credentials(hass, entry, setup_plex_server):
"""Test setup with a user with limited permissions."""
with patch.object(
MockPlexServer, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized
with patch(
"plexapi.server.PlexServer.systemAccounts",
side_effect=plexapi.exceptions.Unauthorized,
) as mock_accounts:
mock_plex_server = await setup_plex_server()
assert mock_accounts.called
plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machineIdentifier]
plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machine_identifier]
assert len(plex_server.accounts) == 0
assert plex_server.owner is None
@ -745,6 +741,7 @@ async def test_integration_discovery(hass):
async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
"""Test setup and reauthorization of a Plex token."""
await async_setup_component(hass, "persistent_notification", {})
await async_process_ha_core_config(
hass,
{"internal_url": "http://example.local:8123"},
@ -752,8 +749,8 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
assert entry.state == ENTRY_STATE_LOADED
with patch.object(
mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized
with patch(
"plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized
), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized):
trigger_plex_update(mock_websocket)
await wait_for_debouncer(hass)
@ -767,9 +764,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
flow_id = flows[0]["flow_id"]
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
"plexapi.server.PlexServer", return_value=mock_plex_server
), patch("plexauth.PlexAuth.initiate_auth"), patch(
with patch("plexauth.PlexAuth.initiate_auth"), patch(
"plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"
):
result = await hass.config_entries.flow.async_configure(flow_id, user_input={})
@ -787,7 +782,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName
assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name
assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL
assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN"

View File

@ -15,11 +15,11 @@ from homeassistant.config_entries import (
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL
from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from tests.common import MockConfigEntry, async_fire_time_changed
@ -31,7 +31,7 @@ async def test_set_config_entry_unique_id(hass, entry, mock_plex_server):
assert (
hass.config_entries.async_entries(const.DOMAIN)[0].unique_id
== mock_plex_server.machineIdentifier
== mock_plex_server.machine_identifier
)
@ -79,9 +79,9 @@ async def test_unload_config_entry(hass, entry, mock_plex_server):
assert entry is config_entries[0]
assert entry.state == ENTRY_STATE_LOADED
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id]
assert loaded_server.plex_server == mock_plex_server
assert loaded_server == mock_plex_server
websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id]
await hass.config_entries.async_unload(entry.entry_id)
@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server):
assert entry.state == ENTRY_STATE_NOT_LOADED
async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server):
async def test_setup_with_photo_session(hass, entry, setup_plex_server):
"""Test setup component with config."""
await setup_plex_server(session_type="photo")
@ -97,7 +97,9 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_
assert entry.state == ENTRY_STATE_LOADED
await hass.async_block_till_done()
media_player = hass.states.get("media_player.plex_product_title")
media_player = hass.states.get(
"media_player.plex_plex_for_android_tv_shield_android_tv"
)
assert media_player.state == STATE_IDLE
await wait_for_debouncer(hass)
@ -106,14 +108,17 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_
assert sensor.state == "0"
async def test_setup_when_certificate_changed(hass, entry):
async def test_setup_when_certificate_changed(
hass,
requests_mock,
empty_payload,
plex_server_accounts,
plex_server_default,
plextv_account,
plextv_resources,
):
"""Test setup component when the Plex certificate has changed."""
old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct"
old_url = f"https://{old_domain}:32400"
OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA)
OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url
await async_setup_component(hass, "persistent_notification", {})
class WrongCertHostnameException(requests.exceptions.SSLError):
"""Mock the exception showing a mismatched hostname."""
@ -123,6 +128,12 @@ async def test_setup_when_certificate_changed(hass, entry):
f"hostname '{old_domain}' doesn't match"
)
old_domain = "1-2-3-4.1111111111ffffff1111111111ffffff.plex.direct"
old_url = f"https://{old_domain}:32400"
OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA)
OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url
old_entry = MockConfigEntry(
domain=const.DOMAIN,
data=OLD_HOSTNAME_DATA,
@ -130,46 +141,45 @@ async def test_setup_when_certificate_changed(hass, entry):
unique_id=DEFAULT_DATA["server_id"],
)
requests_mock.get("https://plex.tv/users/account", text=plextv_account)
requests_mock.get("https://plex.tv/api/resources", text=plextv_resources)
requests_mock.get(old_url, exc=WrongCertHostnameException)
# Test with account failure
with patch(
"plexapi.server.PlexServer", side_effect=WrongCertHostnameException
), patch(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
):
old_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(old_entry.entry_id) is False
await hass.async_block_till_done()
requests_mock.get(f"{old_url}/accounts", status_code=401)
old_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(old_entry.entry_id) is False
await hass.async_block_till_done()
assert old_entry.state == ENTRY_STATE_SETUP_ERROR
await hass.config_entries.async_unload(old_entry.entry_id)
# Test with no servers found
with patch(
"plexapi.server.PlexServer", side_effect=WrongCertHostnameException
), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)):
assert await hass.config_entries.async_setup(old_entry.entry_id) is False
await hass.async_block_till_done()
requests_mock.get(f"{old_url}/accounts", text=plex_server_accounts)
requests_mock.get("https://plex.tv/api/resources", text=empty_payload)
assert await hass.config_entries.async_setup(old_entry.entry_id) is False
await hass.async_block_till_done()
assert old_entry.state == ENTRY_STATE_SETUP_ERROR
await hass.config_entries.async_unload(old_entry.entry_id)
# Test with success
with patch(
"plexapi.server.PlexServer", side_effect=WrongCertHostnameException
), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
assert await hass.config_entries.async_setup(old_entry.entry_id)
await hass.async_block_till_done()
new_url = PLEX_DIRECT_URL
requests_mock.get("https://plex.tv/api/resources", text=plextv_resources)
requests_mock.get(new_url, text=plex_server_default)
requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts)
assert await hass.config_entries.async_setup(old_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert old_entry.state == ENTRY_STATE_LOADED
assert (
old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL]
== entry.data[const.PLEX_SERVER_CONFIG][CONF_URL]
)
assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url
async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server):
async def test_tokenless_server(entry, setup_plex_server):
"""Test setup with a server with token auth disabled."""
TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA)
TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None)
@ -179,18 +189,13 @@ async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server):
assert entry.state == ENTRY_STATE_LOADED
async def test_bad_token_with_tokenless_server(hass, entry):
async def test_bad_token_with_tokenless_server(
hass, entry, mock_websocket, setup_plex_server, requests_mock
):
"""Test setup with a bad token and a server with token auth disabled."""
with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
), patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True
) as mock_websocket:
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
requests_mock.get("https://plex.tv/users/account", status_code=401)
await setup_plex_server()
assert entry.state == ENTRY_STATE_LOADED

View File

@ -3,39 +3,29 @@ from unittest.mock import patch
from plexapi.exceptions import NotFound
from homeassistant.components.plex.const import DOMAIN, SERVERS
async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server):
async def test_plex_tv_clients(
hass, entry, setup_plex_server, requests_mock, player_plexweb_resources
):
"""Test getting Plex clients from plex.tv."""
resource = next(
x
for x in mock_plex_account.resources()
if x.name.startswith("plex.tv Resource Player")
)
with patch.object(resource, "connect", side_effect=NotFound):
mock_plex_server = await setup_plex_server()
requests_mock.get("/resources", text=player_plexweb_resources)
with patch("plexapi.myplex.MyPlexResource.connect", side_effect=NotFound):
await setup_plex_server()
await hass.async_block_till_done()
server_id = mock_plex_server.machineIdentifier
plex_server = hass.data[DOMAIN][SERVERS][server_id]
media_players_before = len(hass.states.async_entity_ids("media_player"))
await hass.config_entries.async_unload(entry.entry_id)
# Ensure one more client is discovered
await hass.config_entries.async_unload(entry.entry_id)
mock_plex_server = await setup_plex_server()
plex_server = hass.data[DOMAIN][SERVERS][server_id]
await setup_plex_server()
media_players_after = len(hass.states.async_entity_ids("media_player"))
assert media_players_after == media_players_before + 1
# Ensure only plex.tv resource client is found
await hass.config_entries.async_unload(entry.entry_id)
mock_plex_server = await setup_plex_server(num_users=0)
plex_server = hass.data[DOMAIN][SERVERS][server_id]
assert len(hass.states.async_entity_ids("media_player")) == 1
# Ensure cache gets called
await plex_server._async_update_platforms()
await hass.async_block_till_done()
# Ensure only plex.tv resource client is found
with patch("plexapi.server.PlexServer.sessions", return_value=[]):
await setup_plex_server(disable_clients=True)
assert len(hass.states.async_entity_ids("media_player")) == 1

View File

@ -10,21 +10,25 @@ from homeassistant.components.media_player.const import (
)
from homeassistant.components.plex.const import (
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
DOMAIN,
PLEX_SERVER_CONFIG,
SERVERS,
SERVICE_PLAY_ON_SONOS,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, CONF_URL
from homeassistant.exceptions import HomeAssistantError
from .const import DEFAULT_OPTIONS, SECONDARY_DATA
from .const import DEFAULT_OPTIONS, MOCK_SERVERS, SECONDARY_DATA
from tests.common import MockConfigEntry
async def test_sonos_playback(hass, mock_plex_server):
async def test_sonos_playback(
hass, mock_plex_server, requests_mock, playqueue_created, sonos_resources
):
"""Test playing media on a Sonos speaker."""
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
# Test Sonos integration lookup failure
@ -43,18 +47,23 @@ async def test_sonos_playback(hass, mock_plex_server):
)
# Test success with plex_key
requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources)
requests_mock.get(
"https://sonos.plex.tv/player/playback/playMedia", status_code=200
)
requests_mock.post("/playqueues", text=playqueue_created)
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch("plexapi.playqueue.PlayQueue.create"):
return_value="Speaker 2",
):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
{
ATTR_ENTITY_ID: "media_player.sonos_kitchen",
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: "2",
ATTR_MEDIA_CONTENT_ID: "100",
},
True,
)
@ -63,8 +72,8 @@ async def test_sonos_playback(hass, mock_plex_server):
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch("plexapi.playqueue.PlayQueue.create"):
return_value="Speaker 2",
):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
@ -80,8 +89,8 @@ async def test_sonos_playback(hass, mock_plex_server):
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch.object(mock_plex_server, "fetchItem", side_effect=NotFound):
return_value="Speaker 2",
), patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
@ -97,7 +106,7 @@ async def test_sonos_playback(hass, mock_plex_server):
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
return_value="Speaker 2",
):
assert await hass.services.async_call(
DOMAIN,
@ -116,7 +125,7 @@ async def test_sonos_playback(hass, mock_plex_server):
), patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
return_value="Speaker 2",
), patch(
"plexapi.playqueue.PlayQueue.create"
):
@ -132,7 +141,17 @@ async def test_sonos_playback(hass, mock_plex_server):
)
async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server):
async def test_playback_multiple_servers(
hass,
setup_plex_server,
requests_mock,
caplog,
empty_payload,
playqueue_created,
plex_server_accounts,
plex_server_base,
sonos_resources,
):
"""Test playing media when multiple servers available."""
secondary_entry = MockConfigEntry(
domain=DOMAIN,
@ -141,21 +160,60 @@ async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server
unique_id=SECONDARY_DATA["server_id"],
)
secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL]
secondary_name = SECONDARY_DATA[CONF_SERVER]
secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER]
requests_mock.get(
secondary_url,
text=plex_server_base.format(
name=secondary_name, machine_identifier=secondary_id
),
)
requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts)
requests_mock.get(f"{secondary_url}/clients", text=empty_payload)
requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload)
await setup_plex_server()
await setup_plex_server(config_entry=secondary_entry)
requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources)
requests_mock.get(
"https://sonos.plex.tv/player/playback/playMedia", status_code=200
)
requests_mock.post("/playqueues", text=playqueue_created)
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="media_player.sonos_kitchen",
), patch("plexapi.playqueue.PlayQueue.create"):
return_value="Speaker 2",
):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
{
ATTR_ENTITY_ID: "media_player.sonos_kitchen",
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{SECONDARY_DATA[CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}',
ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}',
},
True,
)
assert (
"Multiple Plex servers configured, choose with 'plex_server' key" in caplog.text
)
with patch.object(
hass.components.sonos,
"get_coordinator_name",
return_value="Speaker 2",
):
assert await hass.services.async_call(
DOMAIN,
SERVICE_PLAY_ON_SONOS,
{
ATTR_ENTITY_ID: "media_player.sonos_kitchen",
ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC,
ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{MOCK_SERVERS[0][CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}',
},
True,
)

View File

@ -28,29 +28,18 @@ from homeassistant.const import ATTR_ENTITY_ID
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import (
MockPlexAccount,
MockPlexAlbum,
MockPlexArtist,
MockPlexLibrary,
MockPlexLibrarySection,
MockPlexMediaItem,
MockPlexSeason,
MockPlexServer,
MockPlexShow,
)
async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server):
async def test_new_users_available(hass, entry, setup_plex_server):
"""Test setting up when new users available on Plex server."""
MONITORED_USERS = {"Owner": {"enabled": True}}
MONITORED_USERS = {"User 1": {"enabled": True}}
OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS
entry.options = OPTIONS_WITH_USERS
mock_plex_server = await setup_plex_server(config_entry=entry)
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
@ -58,17 +47,18 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve
assert len(monitored_users) == 1
assert len(ignored_users) == 0
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
async def test_new_ignored_users_available(
hass, caplog, entry, mock_websocket, setup_plex_server
hass,
caplog,
entry,
mock_websocket,
setup_plex_server,
requests_mock,
session_new_user,
):
"""Test setting up when new users available on Plex server but are ignored."""
MONITORED_USERS = {"Owner": {"enabled": True}}
MONITORED_USERS = {"User 1": {"enabled": True}}
OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS
OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True
@ -76,43 +66,50 @@ async def test_new_ignored_users_available(
mock_plex_server = await setup_plex_server(config_entry=entry)
server_id = mock_plex_server.machineIdentifier
requests_mock.get(
f"{mock_plex_server.url_in_use}/status/sessions",
text=session_new_user,
)
trigger_plex_update(mock_websocket)
await wait_for_debouncer(hass)
server_id = mock_plex_server.machine_identifier
active_sessions = mock_plex_server._plex_server.sessions()
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
ignored_users = [x for x in mock_plex_server.accounts if x not in monitored_users]
assert len(monitored_users) == 1
assert len(ignored_users) == 2
for ignored_user in ignored_users:
ignored_client = [
x.players[0]
for x in mock_plex_server.sessions()
if x.usernames[0] == ignored_user
][0]
assert (
f"Ignoring {ignored_client.product} client owned by '{ignored_user}'"
in caplog.text
)
x.players[0] for x in active_sessions if x.usernames[0] == ignored_user
]
if ignored_client:
assert (
f"Ignoring {ignored_client[0].product} client owned by '{ignored_user}'"
in caplog.text
)
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
assert sensor.state == str(len(active_sessions))
async def test_network_error_during_refresh(
hass, caplog, mock_plex_server, mock_websocket
):
async def test_network_error_during_refresh(hass, caplog, mock_plex_server):
"""Test network failures during refreshes."""
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
active_sessions = mock_plex_server._plex_server.sessions()
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
assert sensor.state == str(len(active_sessions))
with patch.object(mock_plex_server, "clients", side_effect=RequestException):
with patch("plexapi.server.PlexServer.clients", side_effect=RequestException):
await loaded_server._async_update_platforms()
await hass.async_block_till_done()
@ -129,25 +126,31 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server):
mock_plex_server = await setup_plex_server(disable_gdm=False)
await hass.async_block_till_done()
active_sessions = mock_plex_server._plex_server.sessions()
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
assert sensor.state == str(len(active_sessions))
with patch.object(mock_plex_server, "clients", side_effect=RequestException):
with patch("plexapi.server.PlexServer.clients", side_effect=RequestException):
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
async def test_mark_sessions_idle(
hass, mock_plex_server, mock_websocket, requests_mock, empty_payload
):
"""Test marking media_players as idle when sessions end."""
await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
active_sessions = mock_plex_server._plex_server.sessions()
mock_plex_server.clear_clients()
mock_plex_server.clear_sessions()
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(active_sessions))
url = mock_plex_server.url_in_use
requests_mock.get(f"{url}/clients", text=empty_payload)
requests_mock.get(f"{url}/status/sessions", text=empty_payload)
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
@ -157,43 +160,46 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
assert sensor.state == "0"
async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server):
async def test_ignore_plex_web_client(hass, entry, setup_plex_server):
"""Test option to ignore Plex Web clients."""
OPTIONS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True
entry.options = OPTIONS
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)):
mock_plex_server = await setup_plex_server(config_entry=entry)
await wait_for_debouncer(hass)
mock_plex_server = await setup_plex_server(
config_entry=entry, client_type="plexweb", disable_clients=True
)
await wait_for_debouncer(hass)
active_sessions = mock_plex_server._plex_server.sessions()
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
assert sensor.state == str(len(active_sessions))
media_players = hass.states.async_entity_ids("media_player")
assert len(media_players) == int(sensor.state) - 1
async def test_media_lookups(hass, mock_plex_server, mock_websocket):
async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created):
"""Test media lookups to Plex server."""
server_id = mock_plex_server.machineIdentifier
server_id = mock_plex_server.machine_identifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
# Plex Key searches
media_player_id = hass.states.async_entity_ids("media_player")[0]
with patch("homeassistant.components.plex.PlexServer.create_playqueue"):
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 123,
},
True,
)
with patch.object(MockPlexServer, "fetchItem", side_effect=NotFound):
requests_mock.post("/playqueues", text=playqueue_created)
requests_mock.get("/player/playback/playMedia", status_code=200)
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: media_player_id,
ATTR_MEDIA_CONTENT_TYPE: DOMAIN,
ATTR_MEDIA_CONTENT_ID: 1,
},
True,
)
with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound):
assert await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
@ -206,20 +212,18 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
)
# TV show searches
with patch.object(MockPlexLibrary, "section", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show"
)
is None
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show"
)
with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show"
)
is None
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show"
)
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode"
@ -233,36 +237,34 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
season_number=1,
)
assert loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
season_number=1,
episode_number=3,
)
with patch.object(MockPlexShow, "season", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
)
is None
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
)
with patch.object(MockPlexSeason, "episode", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
episode_number=1,
)
is None
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_EPISODE,
library_name="TV Shows",
show_name="TV Show",
season_number=2,
episode_number=1,
)
is None
)
# Music searches
assert (
@ -286,47 +288,43 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
artist_name="Artist",
album_name="Album",
)
with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Not an Artist",
album_name="Album",
)
is None
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Not an Artist",
album_name="Album",
)
with patch.object(MockPlexArtist, "album", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Not an Album",
)
is None
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name="Not an Album",
)
with patch.object(MockPlexAlbum, "track", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name=" Album",
track_name="Not a Track",
)
is None
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
album_name=" Album",
track_name="Not a Track",
)
with patch.object(MockPlexArtist, "get", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
track_name="Not a Track",
)
is None
is None
)
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
artist_name="Artist",
track_name="Not a Track",
)
is None
)
assert loaded_server.lookup_media(
MEDIA_TYPE_MUSIC,
library_name="Music",
@ -353,44 +351,33 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
)
# Playlist searches
assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="A Playlist")
assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Playlist 1")
assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None
with patch.object(MockPlexServer, "playlist", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist"
)
is None
)
assert (
loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist")
is None
)
# Legacy Movie searches
assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None
assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None
assert loaded_server.lookup_media(
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie"
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie 1"
)
with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie"
)
is None
assert (
loaded_server.lookup_media(
MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie"
)
is None
)
# Movie searches
assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None
assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None
assert loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie"
MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie 1"
)
with patch.object(MockPlexLibrarySection, "search", side_effect=BadRequest):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie"
)
is None
)
with patch.object(MockPlexLibrarySection, "search", return_value=[]):
with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie"
@ -398,25 +385,8 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
is None
)
similar_movies = []
for title in "Duplicate Movie", "Duplicate Movie 2":
similar_movies.append(MockPlexMediaItem(title))
with patch.object(
loaded_server.library.section("Movies"), "search", return_value=similar_movies
):
found_media = loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie"
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie"
)
assert found_media.title == "Duplicate Movie"
duplicate_movies = []
for title in "Duplicate Movie - Original", "Duplicate Movie - Remake":
duplicate_movies.append(MockPlexMediaItem(title))
with patch.object(
loaded_server.library.section("Movies"), "search", return_value=duplicate_movies
):
assert (
loaded_server.lookup_media(
MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie"
)
) is None
) is None

View File

@ -1,6 +1,4 @@
"""Tests for various Plex services."""
from unittest.mock import patch
from homeassistant.components.plex.const import (
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
@ -9,77 +7,84 @@ from homeassistant.components.plex.const import (
SERVICE_REFRESH_LIBRARY,
SERVICE_SCAN_CLIENTS,
)
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
CONF_TOKEN,
CONF_URL,
CONF_VERIFY_SSL,
)
from homeassistant.const import CONF_URL
from .const import MOCK_SERVERS, MOCK_TOKEN
from .mock_classes import MockPlexLibrarySection
from .const import DEFAULT_OPTIONS, SECONDARY_DATA
from tests.common import MockConfigEntry
async def test_refresh_library(hass, mock_plex_server, setup_plex_server):
async def test_refresh_library(
hass,
mock_plex_server,
setup_plex_server,
requests_mock,
empty_payload,
plex_server_accounts,
plex_server_base,
):
"""Test refresh_library service call."""
url = mock_plex_server.url_in_use
refresh = requests_mock.get(f"{url}/library/sections/1/refresh", status_code=200)
# Test with non-existent server
with patch.object(MockPlexLibrarySection, "update") as mock_update:
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"server_name": "Not a Server", "library_name": "Movies"},
True,
)
assert not mock_update.called
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"server_name": "Not a Server", "library_name": "Movies"},
True,
)
assert not refresh.called
# Test with non-existent library
with patch.object(MockPlexLibrarySection, "update") as mock_update:
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Not a Library"},
True,
)
assert not mock_update.called
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Not a Library"},
True,
)
assert not refresh.called
# Test with valid library
with patch.object(MockPlexLibrarySection, "update") as mock_update:
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Movies"},
True,
)
assert mock_update.called
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Movies"},
True,
)
assert refresh.call_count == 1
# Add a second configured server
secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL]
secondary_name = SECONDARY_DATA[CONF_SERVER]
secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER]
requests_mock.get(
secondary_url,
text=plex_server_base.format(
name=secondary_name, machine_identifier=secondary_id
),
)
requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts)
requests_mock.get(f"{secondary_url}/clients", text=empty_payload)
requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload)
entry_2 = MockConfigEntry(
domain=DOMAIN,
data={
CONF_SERVER: MOCK_SERVERS[1][CONF_SERVER],
PLEX_SERVER_CONFIG: {
CONF_TOKEN: MOCK_TOKEN,
CONF_URL: f"https://{MOCK_SERVERS[1][CONF_HOST]}:{MOCK_SERVERS[1][CONF_PORT]}",
CONF_VERIFY_SSL: True,
},
CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][CONF_SERVER_IDENTIFIER],
},
data=SECONDARY_DATA,
options=DEFAULT_OPTIONS,
unique_id=SECONDARY_DATA["server_id"],
)
await setup_plex_server(config_entry=entry_2)
# Test multiple servers available but none specified
with patch.object(MockPlexLibrarySection, "update") as mock_update:
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Movies"},
True,
)
assert not mock_update.called
assert await hass.services.async_call(
DOMAIN,
SERVICE_REFRESH_LIBRARY,
{"library_name": "Movies"},
True,
)
assert refresh.call_count == 1
async def test_scan_clients(hass, mock_plex_server):

4
tests/fixtures/plex/album.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053">
<Directory ratingKey="200" key="/library/metadata/200/children" parentRatingKey="300" guid="plex://album/12345" parentGuid="plex://artist/12345" studio="Warp" type="album" title="Album" parentKey="/library/metadata/300" librarySectionTitle="Music" librarySectionID="3" librarySectionKey="/library/sections/5" parentTitle="Artist" summary="" index="1" viewCount="5" lastViewedAt="1605456703" year="2019" thumb="/library/metadata/200/thumb/1602534481" art="/library/metadata/300/art/1595543202" parentThumb="/library/metadata/300/thumb/1595543202" originallyAvailableAt="2019-01-01" leafCount="9" viewedLeafCount="2" addedAt="1602534474" updatedAt="1602534481" loudnessAnalysisVersion="2">
</Directory>
</MediaContainer>

4
tests/fixtures/plex/artist_albums.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" art="/library/metadata/300/art/1595543202" identifier="com.plexapp.plugins.library" key="300" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" parentIndex="1" parentTitle="Artist" summary="Artist summary." thumb="/library/metadata/300/thumb/1595543202" title1="Music" title2="Artist" viewGroup="album" viewMode="65592">
<Directory ratingKey="200" key="/library/metadata/200/children" parentRatingKey="300" guid="plex://album/12345" parentGuid="plex://artist/12345" studio="Studio" type="album" title="Album" parentKey="/library/metadata/300" parentTitle="Artist" summary="" index="1" viewCount="5" lastViewedAt="1605456703" year="2019" thumb="/library/metadata/200/thumb/1602534481" art="/library/metadata/300/art/1595543202" parentThumb="/library/metadata/300/thumb/1595543202" originallyAvailableAt="2019-01-01" addedAt="1602534474" updatedAt="1602534481" loudnessAnalysisVersion="2">
</Directory>
</MediaContainer>

11
tests/fixtures/plex/children_20.xml vendored Normal file
View File

@ -0,0 +1,11 @@
<MediaContainer allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="2" librarySectionTitle="TV Shows" librarySectionUUID="905308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" size="10"><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/10?lang=en" index="1" key="/library/metadata/10" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="10" summary="Elaborate Summary." thumb="/library/metadata/10/thumb/1590245886" title="Episode 1" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/11?lang=en" index="2" key="/library/metadata/11" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="11" summary="Elaborate Summary." thumb="/library/metadata/11/thumb/1590245886" title="Episode 2" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/12?lang=en" index="3" key="/library/metadata/12" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="12" summary="Elaborate Summary." thumb="/library/metadata/12/thumb/1590245886" title="Episode 3" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/13?lang=en" index="4" key="/library/metadata/13" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="13" summary="Elaborate Summary." thumb="/library/metadata/13/thumb/1590245886" title="Episode 4" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/14?lang=en" index="5" key="/library/metadata/14" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="14" summary="Elaborate Summary." thumb="/library/metadata/14/thumb/1590245886" title="Episode 5" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/15?lang=en" index="6" key="/library/metadata/15" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="15" summary="Elaborate Summary." thumb="/library/metadata/15/thumb/1590245886" title="Episode 6" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/16?lang=en" index="7" key="/library/metadata/16" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="16" summary="Elaborate Summary." thumb="/library/metadata/16/thumb/1590245886" title="Episode 7" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/17?lang=en" index="8" key="/library/metadata/17" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="17" summary="Elaborate Summary." thumb="/library/metadata/17/thumb/1590245886" title="Episode 8" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/18?lang=en" index="9" key="/library/metadata/18" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="18" summary="Elaborate Summary." thumb="/library/metadata/18/thumb/1590245886" title="Episode 9" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video><Video addedAt="1408989944" art="/library/metadata/30/art/1441479050" contentRating="TV-Y" duration="1419520" grandparentArt="/library/metadata/30/art/1441479050" grandparentGuid="com.plexapp.agents.thetvdb://54321?lang=en" grandparentKey="/library/metadata/30" grandparentRatingKey="30" grandparentTheme="/library/metadata/30/theme/1441479050" grandparentThumb="/library/metadata/30/thumb/1441479050" grandparentTitle="TV Show" guid="com.plexapp.agents.thetvdb://12345/1/19?lang=en" index="10" key="/library/metadata/19" lastViewedAt="1438105107" librarySectionID="2" librarySectionKey="/library/sections/2" librarySectionTitle="TV Shows" originallyAvailableAt="2000-01-01" parentGuid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentIndex="1" parentKey="/library/metadata/20" parentRatingKey="20" parentThumb="/library/metadata/20/thumb/1441479050" parentTitle="Season 1" ratingKey="19" summary="Elaborate Summary." thumb="/library/metadata/19/thumb/1590245886" title="Episode 10" type="episode" updatedAt="1590245886" viewCount="14" year="2000">
<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" /></Video></MediaContainer>

1
tests/fixtures/plex/children_200.xml vendored Normal file

File diff suppressed because one or more lines are too long

4
tests/fixtures/plex/children_30.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" art="/library/metadata/30/art/1488495294" banner="/library/metadata/30/banner/1488495294" identifier="com.plexapp.plugins.library" key="30" librarySectionID="2" librarySectionTitle="TV Shows" librarySectionUUID="1d8c8690-2dc5-48e6-9b54-accfacd0067c" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" parentIndex="1" parentTitle="TV Show" parentYear="2000" sortAsc="1" summary="Show summary." theme="/library/metadata/30/theme/1488495294" thumb="/library/metadata/30/thumb/1488495294" title1="TV Shows" title2="TV Show" viewGroup="season" viewMode="458810">
<Directory ratingKey="20" key="/library/metadata/20/children" parentRatingKey="30" guid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentGuid="com.plexapp.agents.thetvdb://12345?lang=en" type="season" title="Season 1" parentKey="/library/metadata/30" parentTitle="TV Show" summary="" index="1" parentIndex="1" viewCount="20" lastViewedAt="1524197296" thumb="/library/metadata/20/thumb/1488495294" art="/library/metadata/30/art/1488495294" parentThumb="/library/metadata/30/thumb/1488495294" parentTheme="/library/metadata/30/theme/1488495294" leafCount="14" viewedLeafCount="14" addedAt="1377827368" updatedAt="1488495294">
</Directory>
</MediaContainer>

4
tests/fixtures/plex/children_300.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" art="/library/metadata/300/art/1595543202" identifier="com.plexapp.plugins.library" key="300" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" parentIndex="1" parentTitle="Artist" summary="Artist summary." thumb="/library/metadata/300/thumb/1595543202" title1="Music" title2="Artist" viewGroup="album" viewMode="65592">
<Directory ratingKey="200" key="/library/metadata/200/children" parentRatingKey="300" guid="plex://album/12345" parentGuid="plex://artist/12345" studio="Studio" type="album" title="Album" parentKey="/library/metadata/300" parentTitle="Artist" summary="" index="1" viewCount="5" lastViewedAt="1605456703" year="2019" thumb="/library/metadata/200/thumb/1602534481" art="/library/metadata/300/art/1595543202" parentThumb="/library/metadata/300/thumb/1595543202" originallyAvailableAt="2019-01-01" addedAt="1602534474" updatedAt="1602534481" loudnessAnalysisVersion="2">
</Directory>
</MediaContainer>

1
tests/fixtures/plex/empty_library.xml vendored Normal file
View File

@ -0,0 +1 @@
<MediaContainer identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" size="0" />

1
tests/fixtures/plex/empty_payload.xml vendored Normal file
View File

@ -0,0 +1 @@
<MediaContainer size="0" />

File diff suppressed because one or more lines are too long

5
tests/fixtures/plex/library.xml vendored Normal file
View File

@ -0,0 +1,5 @@
<MediaContainer size="3" allowSync="0" art="/:/resources/library-art.png" content="" identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" title1="Plex Library" title2="">
<Directory key="sections" title="Library Sections" />
<Directory key="recentlyAdded" title="Recently Added Content" />
<Directory key="onDeck" title="On Deck Content" />
</MediaContainer>

View File

@ -0,0 +1,51 @@
<MediaContainer allowSync="1" art="/:/resources/movie-fanart.jpg" identifier="com.plexapp.plugins.library" librarySectionID="1" librarySectionTitle="Movies" librarySectionUUID="805308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" size="5" sortAsc="1" thumb="/:/resources/movie.png" title="Movies" title1="All Movies" viewGroup="movie" viewMode="131122"><Video addedAt="1377829261" art="/library/metadata/1/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/1" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="1" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" title="Movie 1" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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 addedAt="1377829261" art="/library/metadata/2/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/2" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="2" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/2/thumb/1590245989" title="Movie 2" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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 addedAt="1377829261" art="/library/metadata/3/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/3" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="3" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/3/thumb/1590245989" title="Movie 3" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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 addedAt="1377829261" art="/library/metadata/4/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/4" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="4" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/4/thumb/1590245989" title="Movie 4" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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 addedAt="1377829261" art="/library/metadata/5/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/5" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="5" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/5/thumb/1590245989" title="Movie 5" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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></MediaContainer>

View File

@ -0,0 +1,10 @@
<MediaContainer size="8" allowSync="0" art="/:/resources/movie-fanart.jpg" content="secondary" identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" thumb="/:/resources/video.png" title1="Movies" viewGroup="secondary" viewMode="65592">
<Directory default="asc" defaultDirection="asc" descKey="titleSort:desc" firstCharacterKey="/library/sections/1/firstCharacter" key="titleSort" title="Title" />
<Directory defaultDirection="desc" descKey="originallyAvailableAt:desc" key="originallyAvailableAt" title="Release Date" />
<Directory defaultDirection="desc" descKey="rating:desc" key="rating" title="Critic Rating" />
<Directory defaultDirection="desc" descKey="audienceRating:desc" key="audienceRating" title="Audience Rating" />
<Directory defaultDirection="desc" descKey="duration:desc" key="duration" title="Duration" />
<Directory defaultDirection="desc" descKey="addedAt:desc" key="addedAt" title="Date Added" />
<Directory defaultDirection="desc" descKey="lastViewedAt:desc" key="lastViewedAt" title="Date Viewed" />
<Directory defaultDirection="asc" descKey="mediaHeight:desc" key="mediaHeight" title="Resolution" />
</MediaContainer>

View File

@ -0,0 +1,6 @@
<MediaContainer size="1" allowSync="1" art="/:/resources/artist-fanart.jpg" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" sortAsc="1" thumb="/:/resources/artist.png" title1="Music" title2="All Artists" viewGroup="artist" viewMode="131124">
<Directory ratingKey="300" key="/library/metadata/300/children" guid="plex://artist/12345" type="artist" title="Artist" summary="Artist summary." index="1" viewCount="64" lastViewedAt="1605456703" thumb="/library/metadata/300/thumb/1595543202" art="/library/metadata/300/art/1595543202" addedAt="1595543193" updatedAt="1595543202">
<Genre tag="Electronic" />
<Country tag="United Kingdom" />
</Directory>
</MediaContainer>

View File

@ -0,0 +1,7 @@
<MediaContainer size="5" allowSync="0" art="/:/resources/artist-fanart.jpg" content="secondary" identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" thumb="/:/resources/artist.png" title1="Music" viewGroup="secondary" viewMode="65592">
<Directory default="asc" defaultDirection="asc" descKey="titleSort:desc" firstCharacterKey="/library/sections/3/firstCharacter" key="titleSort" title="Title" />
<Directory defaultDirection="desc" descKey="userRating:desc" key="userRating" title="Rating" />
<Directory defaultDirection="desc" descKey="addedAt:desc" key="addedAt" title="Date Added" />
<Directory defaultDirection="desc" descKey="lastViewedAt:desc" key="lastViewedAt" title="Date Played" />
<Directory defaultDirection="desc" descKey="viewCount:desc" key="viewCount" title="Plays" />
</MediaContainer>

View File

@ -0,0 +1,11 @@
<MediaContainer size="3" allowSync="0" identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" title1="Plex Library">
<Directory allowSync="1" art="/:/resources/movie-fanart.jpg" composite="/library/sections/1/composite/1605409122" filters="1" refreshing="0" thumb="/:/resources/movie.png" key="1" type="movie" title="Movies" agent="com.plexapp.agents.imdb" scanner="Plex Movie Scanner" language="en" uuid="41a28495-035e-46b0-ac84-878f096614da" updatedAt="1602461679" createdAt="1429510140" scannedAt="1605409122" content="1" directory="1" contentChangedAt="116893155" hidden="0">
<Location id="1" path="/storage/movies" />
</Directory>
<Directory allowSync="1" art="/:/resources/show-fanart.jpg" composite="/library/sections/2/composite/1605461424" filters="1" refreshing="0" thumb="/:/resources/show.png" key="2" type="show" title="TV Shows" agent="com.plexapp.agents.thetvdb" scanner="Plex Series Scanner" language="en" uuid="80208576-f7d1-406d-b6d8-aa96a5362131" updatedAt="1602523323" createdAt="1429510140" scannedAt="1605461424" content="1" directory="1" contentChangedAt="117133678" hidden="0">
<Location id="2" path="/storage/tvshows" />
</Directory>
<Directory allowSync="1" art="/:/resources/artist-fanart.jpg" composite="/library/sections/3/composite/1605413685" filters="1" refreshing="0" thumb="/:/resources/artist.png" key="3" type="artist" title="Music" agent="tv.plex.agents.music" scanner="Plex Music" language="en" uuid="1eeace5d-4839-45e8-90b0-8d03b3375744" updatedAt="1602211102" createdAt="1430432959" scannedAt="1605413685" content="1" directory="1" contentChangedAt="116260421" hidden="1">
<Location id="3" path="/storage/music" />
</Directory>
</MediaContainer>

View File

@ -0,0 +1,6 @@
<MediaContainer allowSync="1" art="/:/resources/show-fanart.jpg" identifier="com.plexapp.plugins.library" librarySectionID="2" librarySectionTitle="TV Shows" librarySectionUUID="905308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" size="1" sortAsc="1" thumb="/:/resources/show.png" title="TV Shows" title1="All Shows" viewGroup="show" viewMode="131122"><Directory addedAt="1377827407" art="/library/metadata/30/art/1488495292" banner="/library/metadata/30/banner/1488495292" childCount="5" contentRating="TV-Y" duration="3000000" guid="com.plexapp.agents.thetvdb://12345?lang=en" index="1" key="/library/metadata/30/children" leafCount="100" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/194407" rating="9.0" ratingKey="30" studio="TV Studio" summary="Elaborate summary." theme="/library/metadata/30/theme/1488495292" thumb="/library/metadata/30/thumb/1488495292" title="TV Show" type="show" updatedAt="1488495292" viewedLeafCount="0" year="2000">
<Genre tag="Action" />
<Genre tag="Animated" />
<Role tag="Some Actor" />
<Role tag="Another One" />
</Directory></MediaContainer>

View File

@ -0,0 +1,8 @@
<MediaContainer size="6" allowSync="0" art="/:/resources/show-fanart.jpg" content="secondary" identifier="com.plexapp.plugins.library" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" thumb="/:/resources/show.png" title1="TV Shows" viewGroup="secondary" viewMode="65592">
<Directory default="asc" defaultDirection="asc" descKey="titleSort:desc" firstCharacterKey="/library/sections/2/firstCharacter" key="titleSort" title="Title" />
<Directory defaultDirection="desc" descKey="originallyAvailableAt:desc" key="originallyAvailableAt" title="Release Date" />
<Directory defaultDirection="desc" descKey="rating:desc" key="rating" title="Critic Rating" />
<Directory defaultDirection="desc" descKey="unviewedLeafCount:desc" key="unviewedLeafCount" title="Unplayed" />
<Directory defaultDirection="desc" descKey="episode.addedAt:desc" key="episode.addedAt" title="Last Episode Date Added" />
<Directory defaultDirection="desc" descKey="lastViewedAt:desc" key="lastViewedAt" title="Date Viewed" />
</MediaContainer>

11
tests/fixtures/plex/media_1.xml vendored Normal file
View File

@ -0,0 +1,11 @@
<MediaContainer allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="1" librarySectionTitle="Movies" librarySectionUUID="805308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" size="1"><Video addedAt="1377829261" art="/library/metadata/1/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/1" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="1" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" title="Movie 1" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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" />
<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" /></Video></MediaContainer>

1
tests/fixtures/plex/media_100.xml vendored Normal file
View File

@ -0,0 +1 @@
<MediaContainer allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="005308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" size="1"><Track addedAt="1600999261" art="/library/metadata/300/art/1605462131" duration="250000" grandparentArt="/library/metadata/300/art/1605462131" grandparentGuid="plex://artist/12345" grandparentKey="/library/metadata/300" grandparentRatingKey="300" grandparentThumb="/library/metadata/300/thumb/1605462131" grandparentTitle="Arist Name" guid="plex://track/12345" index="1" key="/library/metadata/100" lastViewedAt="1603309346" librarySectionID="3" librarySectionKey="/library/sections/3" librarySectionTitle="Music" parentGuid="plex://album/12345" parentIndex="1" parentKey="/library/metadata/200" parentRatingKey="200" parentThumb="/library/metadata/200/thumb/1605462119" parentTitle="Album Title" ratingKey="100" summary="" thumb="/library/metadata/200/thumb/1605462119" title="Track 1" type="track" updatedAt="1605462119" viewCount="1"><Media audioChannels="2" audioCodec="mp3" bitrate="256" container="mp3" duration="250000" id="381515" /><Part container="mp3" duration="250000" file="/storage/music/Artist Name/Album Name/Track Name.mp3" id="381939" key="/library/parts/381939/1602996958/file.mp3" size="5000000" /><Stream albumGain="-10.34" albumPeak="1.000000" albumRange="8.429853" audioChannelLayout="stereo" bitrate="256" channels="2" codec="mp3" displayTitle="Unknown (MP3 Stereo)" extendedDisplayTitle="Unknown (MP3 Stereo)" gain="-10.34" id="766687" index="0" loudness="-11.38" lra="7.80" peak="0.870300" samplingRate="44100" selected="1" streamType="2" /></Track></MediaContainer>

4
tests/fixtures/plex/media_200.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="ba0c2140-c6ef-448a-9d1b-31020741d014" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053">
<Directory ratingKey="200" key="/library/metadata/200/children" parentRatingKey="300" guid="plex://album/12345" parentGuid="plex://artist/12345" studio="Warp" type="album" title="Album" parentKey="/library/metadata/300" librarySectionTitle="Music" librarySectionID="3" librarySectionKey="/library/sections/5" parentTitle="Artist" summary="" index="1" viewCount="5" lastViewedAt="1605456703" year="2019" thumb="/library/metadata/200/thumb/1602534481" art="/library/metadata/300/art/1595543202" parentThumb="/library/metadata/300/thumb/1595543202" originallyAvailableAt="2019-01-01" leafCount="9" viewedLeafCount="2" addedAt="1602534474" updatedAt="1602534481" loudnessAnalysisVersion="2">
</Directory>
</MediaContainer>

6
tests/fixtures/plex/media_30.xml vendored Normal file
View File

@ -0,0 +1,6 @@
<MediaContainer allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="2" librarySectionTitle="TV Shows" librarySectionUUID="905308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" size="1"><Directory addedAt="1377827407" art="/library/metadata/30/art/1488495292" banner="/library/metadata/30/banner/1488495292" childCount="5" contentRating="TV-Y" duration="3000000" guid="com.plexapp.agents.thetvdb://12345?lang=en" index="1" key="/library/metadata/30/children" leafCount="100" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/194407" rating="9.0" ratingKey="30" studio="TV Studio" summary="Elaborate summary." theme="/library/metadata/30/theme/1488495292" thumb="/library/metadata/30/thumb/1488495292" title="TV Show" type="show" updatedAt="1488495292" viewedLeafCount="0" year="2000">
<Genre tag="Action" />
<Genre tag="Animated" />
<Role tag="Some Actor" />
<Role tag="Another One" />
<Location path="/storage/tvshows/TV Show" /></Directory></MediaContainer>

View File

@ -0,0 +1 @@
<MediaContainer size="1"><Player deviceClass="pc" machineIdentifier="plexweb_id" platform="Chrome" platformVersion="14.0" product="Plex Web" protocol="plex" protocolCapabilities="timeline,playback,navigation,mirror,playqueues" protocolVersion="3" title="Chrome" version="4.47.1" /></MediaContainer>

11
tests/fixtures/plex/playlist_500.xml vendored Normal file
View File

@ -0,0 +1,11 @@
<MediaContainer composite="/playlists/{key}/composite/1606158679" duration="5000" leafCount="1" playlistType="video" ratingKey="500" size="1" smart="0" title="Playlist 500"><Video addedAt="1377829261" art="/library/metadata/1/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/1" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="1" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" title="Movie 1" type="movie" updatedAt="1590245989" viewCount="1" year="2000">
<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" />
<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" /></Video></MediaContainer>

6
tests/fixtures/plex/playlists.xml vendored Normal file
View File

@ -0,0 +1,6 @@
<MediaContainer size="2">
<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>
</MediaContainer>

View File

@ -0,0 +1 @@
<MediaContainer allowSync="1" identifier="com.plexapp.plugins.library" librarySectionID="3" librarySectionTitle="Music" librarySectionUUID="905308ec-5019-43d4-a449-75d2b9e42f93" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" playQueueID="11111" playQueueSelectedItemID="98610" playQueueSelectedItemOffset="0" playQueueSelectedMetadataItemID="100" playQueueShuffled="0" playQueueSourceURI="library://ba0c2140-c6ef-448a-9d1b-31020741d014/item//library/metadata/100" playQueueTotalCount="1" playQueueVersion="1" size="1"><Track addedAt="1600999261" art="/library/metadata/300/art/1605462131" duration="250000" grandparentArt="/library/metadata/300/art/1605462131" grandparentGuid="plex://artist/12345" grandparentKey="/library/metadata/300" grandparentRatingKey="300" grandparentThumb="/library/metadata/300/thumb/1605462131" grandparentTitle="Arist Name" guid="plex://track/12345" index="1" key="/library/metadata/100" lastViewedAt="1603309346" librarySectionID="3" librarySectionKey="/library/sections/3" librarySectionTitle="Music" parentGuid="plex://album/12345" parentIndex="1" parentKey="/library/metadata/200" parentRatingKey="200" parentThumb="/library/metadata/200/thumb/1605462119" parentTitle="Album Title" ratingKey="100" summary="" thumb="/library/metadata/200/thumb/1605462119" title="Track 1" type="track" updatedAt="1605462119" viewCount="1"><Media audioChannels="2" audioCodec="mp3" bitrate="256" container="mp3" duration="250000" id="381515" playQueueItemID="98610" /><Part container="mp3" duration="250000" file="/storage/music/Artist Name/Album Name/Track Name.mp3" id="381939" key="/library/parts/381939/1602996958/file.mp3" size="5000000" /><Stream albumGain="-10.34" albumPeak="1.000000" albumRange="8.429853" audioChannelLayout="stereo" bitrate="256" channels="2" codec="mp3" displayTitle="Unknown (MP3 Stereo)" extendedDisplayTitle="Unknown (MP3 Stereo)" gain="-10.34" id="766687" index="0" loudness="-11.38" lra="7.80" peak="0.870300" samplingRate="44100" selected="1" streamType="2" /></Track></MediaContainer>

View File

@ -0,0 +1,6 @@
<MediaContainer size="4" identifier="com.plexapp.system.accounts">
<Account id="0" key="/accounts/0" name="" defaultAudioLanguage="en" autoSelectAudio="1" defaultSubtitleLanguage="en" subtitleMode="1" thumb="" />
<Account id="1" key="/accounts/1" name="User 1" defaultAudioLanguage="en" autoSelectAudio="1" defaultSubtitleLanguage="en" subtitleMode="1" thumb="" />
<Account id="1000" key="/accounts/1000" name="User 1000" defaultAudioLanguage="en" autoSelectAudio="1" defaultSubtitleLanguage="en" subtitleMode="1" thumb="" />
<Account id="1001" key="/accounts/1001" name="User 1001" defaultAudioLanguage="en" autoSelectAudio="1" defaultSubtitleLanguage="en" subtitleMode="1" thumb="" />
</MediaContainer>

View File

@ -0,0 +1,27 @@
<MediaContainer size="25" allowCameraUpload="1" allowChannelAccess="1" allowMediaDeletion="1" allowSharing="1" allowSync="1" allowTuners="1" backgroundProcessing="1" certificate="1" companionProxy="1" countryCode="usa" diagnostics="logs,databases,streaminglogs" eventStream="1" friendlyName="{name}" hubSearch="1" itemClusters="1" livetv="7" machineIdentifier="{machine_identifier}" mediaProviders="1" multiuser="1" myPlex="1" myPlexMappingState="mapped" myPlexSigninState="ok" myPlexSubscription="1" myPlexUsername="myplexusername@email.com" offlineTranscode="1" ownerFeatures="adaptive_bitrate,camera_upload,cloudsync,collections,content_filter,download_certificates,dvr,federated-auth,hardware_transcoding,home,hwtranscode,item_clusters,kevin-bacon,livetv,loudness,lyrics,music_videos,news,pass,photo_autotags,photos-v5,photosV6-edit,photosV6-tv-albums,premium_music_metadata,radio,server-manager,session_bandwidth_restrictions,session_kick,shared-radio,sync,trailers,tuner-sharing,type-first,unsupportedtuners,webhooks" photoAutoTag="1" platform="Linux" platformVersion="20.04.1 LTS (Focal Fossa)" pluginHost="1" pushNotifications="0" readOnlyLibraries="0" requestParametersInCookie="1" streamingBrainABRVersion="3" streamingBrainVersion="2" sync="1" transcoderActiveVideoSessions="0" transcoderAudio="1" transcoderLyrics="1" transcoderPhoto="1" transcoderSubtitles="1" transcoderVideo="1" transcoderVideoBitrates="64,96,208,320,720,1500,2000,3000,4000,8000,10000,12000,20000" transcoderVideoQualities="0,1,2,3,4,5,6,7,8,9,10,11,12" transcoderVideoResolutions="128,128,160,240,320,480,768,720,720,1080,1080,1080,1080" updatedAt="1605463238" updater="1" version="1.20.4.3517-ab5e1197c" voiceSearch="1">
<Directory count="1" key="activities" title="activities" />
<Directory count="1" key="butler" title="butler" />
<Directory count="1" key="channels" title="channels" />
<Directory count="1" key="clients" title="clients" />
<Directory count="1" key="devices" title="devices" />
<Directory count="1" key="diagnostics" title="diagnostics" />
<Directory count="1" key="hubs" title="hubs" />
<Directory count="3" key="library" title="library" />
<Directory count="3" key="livetv" title="livetv" />
<Directory count="3" key="media" title="media" />
<Directory count="2" key="metadata" title="metadata" />
<Directory count="1" key="neighborhood" title="neighborhood" />
<Directory count="1" key="playQueues" title="playQueues" />
<Directory count="1" key="player" title="player" />
<Directory count="1" key="playlists" title="playlists" />
<Directory count="1" key="resources" title="resources" />
<Directory count="1" key="search" title="search" />
<Directory count="1" key="server" title="server" />
<Directory count="1" key="servers" title="servers" />
<Directory count="1" key="statistics" title="statistics" />
<Directory count="1" key="system" title="system" />
<Directory count="1" key="transcode" title="transcode" />
<Directory count="2" key="tv%2Eplex%2Eproviders%2Eepg%2Ecloud%3A2" title="tv.plex.providers.epg.cloud:2" />
<Directory count="1" key="updater" title="updater" />
<Directory count="1" key="user" title="user" />
</MediaContainer>

View File

@ -0,0 +1,3 @@
<MediaContainer size="1">
<Server name="SHIELD Android TV" host="1.2.3.11" address="1.2.3.11" port="32500" machineIdentifier="1234567890123456-com-plexapp-android" version="8.8.2.21525" protocol="plex" product="Plex for Android (TV)" deviceClass="mobile" protocolVersion="1" protocolCapabilities="timeline,playback,mirror,playqueues,provider-playback" />
</MediaContainer>

15
tests/fixtures/plex/plextv_account.xml vendored Normal file
View File

@ -0,0 +1,15 @@
<user email="myplexusername@email.com" id="12345" uuid="1234567890" mailing_list_status="active" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" username="User 1" title="User 1" cloudSyncDevice="" locale="" authenticationToken="faketoken" authToken="faketoken" scrobbleTypes="" restricted="0" home="1" guest="0" queueEmail="queue+1234567890@save.plex.tv" queueUid="" hasPassword="true" homeSize="2" maxHomeSize="15" secure="1" certificateVersion="2">
<subscription active="1" status="Active" plan="lifetime">
<feature id="companions_sonos"/>
</subscription>
<roles>
<role id="plexpass"/>
</roles>
<entitlements all="1"/>
<profile_settings default_audio_language="en" default_subtitle_language="en" auto_select_subtitle="1" auto_select_audio="1" default_subtitle_accessibility="0" default_subtitle_forced="0"/>
<services/>
<username>testuser</username>
<email>testuser@email.com</email>
<joined-at type="datetime">2000-01-01 12:34:56 UTC</joined-at>
<authentication-token>faketoken</authentication-token>
</user>

View File

@ -0,0 +1,21 @@
<MediaContainer size="5">
<Device name="Plex Server 1" product="Plex Media Server" productVersion="1.20.4.3517-ab5e1197c" platform="Linux" platformVersion="20.04.1 LTS (Focal Fossa)" device="PC" clientIdentifier="unique_id_123" createdAt="1429510140" lastSeenAt="1605500006" provides="server" owned="1" accessToken="faketoken" publicAddress="10.20.30.40" httpsRequired="0" synced="0" relay="0" dnsRebindingProtection="0" natLoopbackSupported="1" publicAddressMatches="1" presence="1">
<Connection protocol="https" address="1.2.3.4" port="32400" uri="https://1-2-3-4.123456789001234567890.plex.direct:32400" local="1"/>
</Device>
<Device name="Plex Server 2" product="Plex Media Server" productVersion="1.20.4.3517-ab5e1197c" platform="Linux" platformVersion="20.04.1 LTS (Focal Fossa)" device="PC" clientIdentifier="unique_id_456" createdAt="1429510140" lastSeenAt="1605500006" provides="server" owned="1" accessToken="faketoken" publicAddress="10.20.30.40" httpsRequired="0" synced="0" relay="0" dnsRebindingProtection="0" natLoopbackSupported="1" publicAddressMatches="1" presence="{second_server_enabled}">
<Connection protocol="https" address="4.3.2.1" port="32400" uri="https://4-3-2-1.123456789001234567890.plex.direct:32400" local="1"/>
</Device>
<Device name="Chrome" product="Plex Web" productVersion="4.46.2" platform="Chrome" platformVersion="14.0" device="OSX" clientIdentifier="plexweb_id" createdAt="1578086003" lastSeenAt="1605461664" provides="client,player,pubsub-player" owned="1" publicAddress="10.20.30.40" publicAddressMatches="1" presence="1" accessToken="faketoken">
<Connection protocol="https" address="1.2.3.5" port="32400" uri="https://1-2-3-5.123456789001234567890.plex.direct:32400" local="1"/>
<Connection protocol="https" address="10.20.30.40" port="35872" uri="https://10-20-30-40.123456789001234567890.plex.direct:35872" local="0"/>
</Device>
<Device name="AppleTV" product="Plex for Apple TV" productVersion="7.9" platform="tvOS" platformVersion="14.2" device="Apple TV" clientIdentifier="A10E4083-BF1A-4586-B884-C638A32D5285" createdAt="1447217545" lastSeenAt="1605495521" provides="client,player,pubsub-player,provider-playback" owned="1" publicAddress="10.20.30.40" publicAddressMatches="1" presence="0">
<Connection protocol="http" address="1.2.3.6" port="32500" uri="http://1.2.3.6:32500" local="1"/>
</Device>
<Device name="jPhone" product="Plex for iOS" productVersion="7.9" platform="iOS" platformVersion="14.2" device="iPhone" clientIdentifier="CDB83941-F8C2-4B56-989E-F3EFD0165BC1" createdAt="1537584529" lastSeenAt="1605501046" provides="client,controller,sync-target,player,pubsub-player,provider-playback" owned="1" publicAddress="10.20.30.40" publicAddressMatches="1" presence="0">
<Connection protocol="http" address="1.2.3.7" port="32500" uri="http://1.2.3.7:32500" local="1"/>
</Device>
<Device name="SHIELD Android TV" product="Plex for Android (TV)" productVersion="8.8.2.21525" platform="Android" platformVersion="9" device="SHIELD Android TV" clientIdentifier="2f2a5ae50a45837c-com-plexapp-android" createdAt="1584850408" lastSeenAt="1605384938" provides="player,pubsub-player,controller" owned="1" publicAddress="10.20.30.40" publicAddressMatches="1" presence="1">
<Connection protocol="http" address="1.2.3.11" port="32500" uri="http://1.2.3.11:32500" local="1"/>
</Device>
</MediaContainer>

View File

@ -0,0 +1 @@
<MediaContainer size="0" token="transient-1234567890" />

11
tests/fixtures/plex/session_base.xml vendored Normal file
View File

@ -0,0 +1,11 @@
<MediaContainer size="1"><Video addedAt="1377829261" art="/library/metadata/1/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/1" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="1" sessionKey="1" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" title="Movie 1" type="movie" updatedAt="1590245989" viewCount="1" viewOffset="0" year="2000">
<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" />
<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="{user_id}" vendor="NVIDIA" version="8.9.2.21619" /><User id="{user_id}" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User {user_id}" /><Session bandwidth="7000" id="session_id_1" location="lan" /><Media audioChannels="2" audioCodec="aac" audioProfile="dts" bitrate="6000" container="mp4" duration="9000000" height="544" id="2637" optimizedForStreaming="1" protocol="dash" selected="1" videoCodec="h264" videoFrameRate="24p" videoProfile="high" videoResolution="720p" width="1280"><Part audioProfile="dts" bitrate="6000" container="mp4" decision="transcode" duration="9000000" height="544" id="4631" optimizedForStreaming="1" protocol="dash" selected="1" videoProfile="high" width="1280"><Stream bitrate="6000" codec="h264" decision="copy" default="1" displayTitle="720p (H.264)" extendedDisplayTitle="x264 @ 6000 kbps (720p H.264)" frameRate="23.975999999999999" height="544" id="21428" language="English" languageCode="eng" location="segments-video" streamType="1" width="1280" /><Stream bitrate="256" bitrateMode="cbr" channels="2" codec="aac" decision="transcode" default="1" displayTitle="English (DTS 5.1)" extendedDisplayTitle="DTS 5.1 @ 1536 kbps (English)" id="21429" language="English" languageCode="eng" location="segments-audio" selected="1" streamType="2" /></Part></Media></Video></MediaContainer>

5
tests/fixtures/plex/session_photo.xml vendored Normal file
View File

@ -0,0 +1,5 @@
<MediaContainer size="1"><Photo addedAt="1605739344" createdAtAccuracy="local" createdAtTZOffset="-18000" guid="local://999" index="1" key="/library/metadata/999" librarySectionID="4" librarySectionKey="/library/sections/4" librarySectionTitle="Photos" originallyAvailableAt="2020-10-31" ratingKey="999" sessionKey="1" summary="" thumb="/library/metadata/999/thumb/1605739344" title="Photo 1" type="photo" updatedAt="1605739344" viewOffset="0" year="2020">
<Media aspectRatio="1.33" container="jpeg" height="2880" id="381658" width="1620">
<Part container="jpeg" file="/storage/photos/Photo 1.jpeg" id="382082" key="/library/parts/382082/1604162245/file.jpeg" size="500000" />
</Media>
<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" /><User id="1" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User 1" /></Photo></MediaContainer>

11
tests/fixtures/plex/session_plexweb.xml vendored Normal file
View File

@ -0,0 +1,11 @@
<MediaContainer size="1"><Video addedAt="1377829261" art="/library/metadata/1/art/1590245989" audienceRating="9.5" audienceRatingImage="rottentomatoes://image.rating.upright" chapterSource="agent" contentRating="R" duration="9000000" guid="com.plexapp.agents.imdb://tt0123456?lang=en" key="/library/metadata/1" lastViewedAt="1505969509" librarySectionID="1" librarySectionKey="/library/sections/1" librarySectionTitle="Movies" originallyAvailableAt="2000-01-01" primaryExtraKey="/library/metadata/195540" rating="9.0" ratingImage="rottentomatoes://image.rating.certified" ratingKey="1" sessionKey="1" studio="Studio Entertainment" summary="Some elaborate summary." tagline="Witty saying." thumb="/library/metadata/1/thumb/1590245989" title="Movie 1" type="movie" updatedAt="1590245989" viewCount="1" viewOffset="0" year="2000">
<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" />
<Player address="1.2.3.5" device="OSX" deviceClass="pc" machineIdentifier="plexweb_id" model="hosted" platform="Chrome" platformVersion="14.0" product="Plex Web" protocol="plex" protocolVersion="3" remotePublicAddress="10.20.30.40" state="playing" title="Chrome" userID="1" vendor="" version="4" /><User id="1" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=11111" title="User 1" /><Session bandwidth="7000" id="session_id_1" location="lan" /><Media audioChannels="2" audioCodec="aac" audioProfile="dts" bitrate="6000" container="mp4" duration="9000000" height="544" id="2637" optimizedForStreaming="1" protocol="dash" selected="1" videoCodec="h264" videoFrameRate="24p" videoProfile="high" videoResolution="720p" width="1280"><Part audioProfile="dts" bitrate="6000" container="mp4" decision="transcode" duration="9000000" height="544" id="4631" optimizedForStreaming="1" protocol="dash" selected="1" videoProfile="high" width="1280"><Stream bitrate="6000" codec="h264" decision="copy" default="1" displayTitle="720p (H.264)" extendedDisplayTitle="x264 @ 6000 kbps (720p H.264)" frameRate="23.975999999999999" height="544" id="21428" language="English" languageCode="eng" location="segments-video" streamType="1" width="1280" /><Stream bitrate="256" bitrateMode="cbr" channels="2" codec="aac" decision="transcode" default="1" displayTitle="English (DTS 5.1)" extendedDisplayTitle="DTS 5.1 @ 1536 kbps (English)" id="21429" language="English" languageCode="eng" location="segments-audio" selected="1" streamType="2" /></Part></Media></Video></MediaContainer>

4
tests/fixtures/plex/show_seasons.xml vendored Normal file
View File

@ -0,0 +1,4 @@
<MediaContainer size="1" allowSync="1" art="/library/metadata/30/art/1488495294" banner="/library/metadata/30/banner/1488495294" identifier="com.plexapp.plugins.library" key="30" librarySectionID="2" librarySectionTitle="TV Shows" librarySectionUUID="1d8c8690-2dc5-48e6-9b54-accfacd0067c" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1603922053" nocache="1" parentIndex="1" parentTitle="TV Show" parentYear="2000" sortAsc="1" summary="Show summary." theme="/library/metadata/30/theme/1488495294" thumb="/library/metadata/30/thumb/1488495294" title1="TV Shows" title2="TV Show" viewGroup="season" viewMode="458810">
<Directory ratingKey="20" key="/library/metadata/20/children" parentRatingKey="30" guid="com.plexapp.agents.thetvdb://12345/1?lang=en" parentGuid="com.plexapp.agents.thetvdb://12345?lang=en" type="season" title="Season 1" parentKey="/library/metadata/30" parentTitle="TV Show" summary="" index="1" parentIndex="1" viewCount="20" lastViewedAt="1524197296" thumb="/library/metadata/20/thumb/1488495294" art="/library/metadata/30/art/1488495294" parentThumb="/library/metadata/30/thumb/1488495294" parentTheme="/library/metadata/30/theme/1488495294" leafCount="14" viewedLeafCount="14" addedAt="1377827368" updatedAt="1488495294">
</Directory>
</MediaContainer>

View File

@ -0,0 +1,5 @@
<MediaContainer size="3">
<Player title="Speaker 1" machineIdentifier="RINCON_12345678901234561:1234567891" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.11"/>
<Player title="Speaker 2 + 1" machineIdentifier="RINCON_12345678901234562:1234567892" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.12"/>
<Player title="Speaker 3" machineIdentifier="RINCON_12345678901234563:1234567893" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.13"/>
</MediaContainer>