Skip to content

Commit b836d24

Browse files
authored
MediaContainer class is now a list (#1373)
* MediaContainer class is now a list - can now support totalSize as returned from server * add tests for media container * Update MediaContainer attributes if previously None when extending
1 parent 5b5d4c6 commit b836d24

File tree

2 files changed

+109
-4
lines changed

2 files changed

+109
-4
lines changed

Diff for: plexapi/base.py

+77-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
# -*- coding: utf-8 -*-
22
import re
3+
from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, TypeVar, Union
34
import weakref
45
from functools import cached_property
56
from urllib.parse import urlencode
67
from xml.etree import ElementTree
8+
from xml.etree.ElementTree import Element
79

810
from plexapi import CONFIG, X_PLEX_CONTAINER_SIZE, log, utils
911
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
1012

13+
if TYPE_CHECKING:
14+
from plexapi.server import PlexServer
15+
16+
PlexObjectT = TypeVar("PlexObjectT", bound='PlexObject')
17+
MediaContainerT = TypeVar("MediaContainerT", bound="MediaContainer")
18+
1119
USER_DONT_RELOAD_FOR_KEYS = set()
1220
_DONT_RELOAD_FOR_KEYS = {'key'}
1321
OPERATORS = {
@@ -253,8 +261,7 @@ def fetchItems(self, ekey, cls=None, container_start=None, container_size=None,
253261
if maxresults is not None:
254262
container_size = min(container_size, maxresults)
255263

256-
results = []
257-
subresults = []
264+
results = MediaContainer[cls](self._server, Element('MediaContainer'), initpath=ekey)
258265
headers = {}
259266

260267
while True:
@@ -332,7 +339,7 @@ def findItems(self, data, cls=None, initpath=None, rtag=None, **kwargs):
332339
if rtag:
333340
data = next(utils.iterXMLBFS(data, rtag), [])
334341
# loop through all data elements to find matches
335-
items = []
342+
items = MediaContainer[cls](self._server, data, initpath=initpath) if data.tag == 'MediaContainer' else []
336343
for elem in data:
337344
if self._checkAttrs(elem, **kwargs):
338345
item = self._buildItemOrNone(elem, cls, initpath)
@@ -1011,7 +1018,11 @@ def delete(self):
10111018
return self._server.query(self.historyKey, method=self._server._session.delete)
10121019

10131020

1014-
class MediaContainer(PlexObject):
1021+
class MediaContainer(
1022+
Generic[PlexObjectT],
1023+
List[PlexObjectT],
1024+
PlexObject,
1025+
):
10151026
""" Represents a single MediaContainer.
10161027
10171028
Attributes:
@@ -1024,11 +1035,71 @@ class MediaContainer(PlexObject):
10241035
librarySectionUUID (str): :class:`~plexapi.library.LibrarySection` UUID.
10251036
mediaTagPrefix (str): "/system/bundle/media/flags/"
10261037
mediaTagVersion (int): Unknown
1038+
offset (int): The offset of current results.
10271039
size (int): The number of items in the hub.
1040+
totalSize (int): The total number of items for the query.
10281041
10291042
"""
10301043
TAG = 'MediaContainer'
10311044

1045+
def __init__(
1046+
self,
1047+
server: "PlexServer",
1048+
data: Element,
1049+
*args: PlexObjectT,
1050+
initpath: Optional[str] = None,
1051+
parent: Optional[PlexObject] = None,
1052+
) -> None:
1053+
# super calls Generic.__init__ which calls list.__init__ eventually
1054+
super().__init__(*args)
1055+
PlexObject.__init__(self, server, data, initpath, parent)
1056+
1057+
def extend(
1058+
self: MediaContainerT,
1059+
__iterable: Union[Iterable[PlexObjectT], MediaContainerT],
1060+
) -> None:
1061+
curr_size = self.size if self.size is not None else len(self)
1062+
super().extend(__iterable)
1063+
# update size, totalSize, and offset
1064+
if not isinstance(__iterable, MediaContainer):
1065+
return
1066+
1067+
# prefer the totalSize of the new iterable even if it is smaller
1068+
self.totalSize = (
1069+
__iterable.totalSize
1070+
if __iterable.totalSize is not None
1071+
else self.totalSize
1072+
) # ideally both should be equal
1073+
1074+
# the size of the new iterable is added to the current size
1075+
self.size = curr_size + (
1076+
__iterable.size if __iterable.size is not None else len(__iterable)
1077+
)
1078+
1079+
# the offset is the minimum of the two, prefering older values
1080+
if self.offset is not None and __iterable.offset is not None:
1081+
self.offset = min(self.offset, __iterable.offset)
1082+
else:
1083+
self.offset = (
1084+
self.offset if self.offset is not None else __iterable.offset
1085+
)
1086+
1087+
# for all other attributes, overwrite with the new iterable's values if previously None
1088+
for key in (
1089+
"allowSync",
1090+
"augmentationKey",
1091+
"identifier",
1092+
"librarySectionID",
1093+
"librarySectionTitle",
1094+
"librarySectionUUID",
1095+
"mediaTagPrefix",
1096+
"mediaTagVersion",
1097+
):
1098+
if (not hasattr(self, key)) or (getattr(self, key) is None):
1099+
if not hasattr(__iterable, key):
1100+
continue
1101+
setattr(self, key, getattr(__iterable, key))
1102+
10321103
def _loadData(self, data):
10331104
self._data = data
10341105
self.allowSync = utils.cast(int, data.attrib.get('allowSync'))
@@ -1039,4 +1110,6 @@ def _loadData(self, data):
10391110
self.librarySectionUUID = data.attrib.get('librarySectionUUID')
10401111
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
10411112
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
1113+
self.offset = utils.cast(int, data.attrib.get("offset"))
10421114
self.size = utils.cast(int, data.attrib.get('size'))
1115+
self.totalSize = utils.cast(int, data.attrib.get("totalSize"))

Diff for: tests/test_fetch_items.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from plexapi.audio import Track
2+
from plexapi.base import MediaContainer
3+
4+
5+
def test_media_container_is_list():
6+
container = MediaContainer(None, None, Track(None, None))
7+
assert isinstance(container, list)
8+
assert len(container) == 1
9+
container.append(Track(None, None))
10+
assert len(container) == 2
11+
12+
13+
def test_media_container_extend():
14+
container_1 = MediaContainer(None, None, Track(None, None))
15+
container_2 = MediaContainer(
16+
None, None, [Track(None, None), Track(None, None)]
17+
)
18+
container_1.size, container_2.size = 1, 2
19+
container_1.offset, container_2.offset = 3, 4
20+
container_1.totalSize = container_2.totalSize = 10
21+
container_1.extend(container_2)
22+
assert container_1.size == 1 + 2
23+
assert container_1.offset == min(3, 4)
24+
assert container_1.totalSize == 10
25+
26+
27+
def test_fetch_items_with_media_container(show):
28+
all_episodes = show.episodes()
29+
some_episodes = show.episodes(maxresults=2)
30+
assert some_episodes.size == 2
31+
assert some_episodes.offset == 0
32+
assert some_episodes.totalSize == len(all_episodes)

0 commit comments

Comments
 (0)