Repository: pkkid/python-plexapi

Issue:
`totalSize` of MediaContainer
### What is your feature request?

api returns a bunch of metadata along with items fetched one of which is `totalSize`

it is useful in showing totalItems on the server without fetching all of them

other attributes are `offset` and `size`, standard pagination stuff


### Are there any workarounds?

`fetchItems` already uses this info
https://github.com/pkkid/python-plexapi/blob/abcab4f78d22e86b380327c6014d3148e0248e6d/plexapi/base.py#L258

### Code Snippets

```python
tracks = music_section.searchTracks(maxresults=100)
print(f'showing {len(tracks)} of {tracks.totalSize} starting at {tracks.offset}')
```


### Additional Context

![image](https://github.com/pkkid/python-plexapi/assets/64108942/fc167768-392f-480e-a9a8-06984587ddcb)
as on plex web

Discussion:
Dr-Blank: could you guide me as to where to put the test for this?
also how would the test be implemented?
for example test fetchItems directly
or indirectly, like searchepisodes, and test on the return

tests i can think of:
 1. isinstance list
 2. size, offset, totalSize are int
Dr-Blank: I suppose regression tests are not needed for this PR since the current tests should be enough.

I have added some tests pertaining to new functionality only.
JonnyWong16: This looks greats. It seems to be mostly working, but is it possible to load the other `MediaContainer` attributes as well?

```py
>> results = plex.library.section("Movies (Test)").all()  # results is now a MediaContainer
>> results.librarySectionID  # this is None
```

```xml
<MediaContainer size="17" allowSync="1" art="/:/resources/movie-fanart.jpg" identifier="com.plexapp.plugins.library" librarySectionID="41" librarySectionTitle="Movies (Test)" librarySectionUUID="e9535986-3fd3-4f88-9793-77a50b04dd01" mediaTagPrefix="/system/bundle/media/flags/" mediaTagVersion="1708667686" thumb="/:/resources/movie.png" title1="Movies (Test)" title2="All Movies (Test)" viewGroup="movie" viewMode="65592">
```

`results` is initialized with empty `data = Element('MediaContainer')` which never gets updated with the attributes from the XML.
`subresults` gets initialized with the actual data from the XML (from `items` inside `findItems()`).
Dr-Blank: > is it possible to load the other `MediaContainer` attributes as well?

https://github.com/pkkid/python-plexapi/pull/1373/commits/06819fa614ce3d9d84f0f51980094cc43569b22e solves this

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 849642f..ca57341 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -253,6 +253,8 @@ jobs:
         coverage xml
 
     - name: Upload ${{ matrix.plex }} coverage to Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
+      env:
+        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 
       with:
         flags: ${{ matrix.plex }}
diff --git a/plexapi/base.py b/plexapi/base.py
index 05c058b..0852426 100644
--- a/plexapi/base.py
+++ b/plexapi/base.py
@@ -1,13 +1,21 @@
 # -*- coding: utf-8 -*-
 import re
+from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, TypeVar, Union
 import weakref
 from functools import cached_property
 from urllib.parse import urlencode
 from xml.etree import ElementTree
+from xml.etree.ElementTree import Element
 
 from plexapi import CONFIG, X_PLEX_CONTAINER_SIZE, log, utils
 from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
 
+if TYPE_CHECKING:
+    from plexapi.server import PlexServer
+
+PlexObjectT = TypeVar("PlexObjectT", bound='PlexObject')
+MediaContainerT = TypeVar("MediaContainerT", bound="MediaContainer")
+
 USER_DONT_RELOAD_FOR_KEYS = set()
 _DONT_RELOAD_FOR_KEYS = {'key'}
 OPERATORS = {
@@ -253,8 +261,7 @@ class PlexObject:
         if maxresults is not None:
             container_size = min(container_size, maxresults)
 
-        results = []
-        subresults = []
+        results = MediaContainer[cls](self._server, Element('MediaContainer'), initpath=ekey)
         headers = {}
 
         while True:
@@ -332,7 +339,7 @@ class PlexObject:
         if rtag:
             data = next(utils.iterXMLBFS(data, rtag), [])
         # loop through all data elements to find matches
-        items = []
+        items = MediaContainer[cls](self._server, data, initpath=initpath) if data.tag == 'MediaContainer' else []
         for elem in data:
             if self._checkAttrs(elem, **kwargs):
                 item = self._buildItemOrNone(elem, cls, initpath)
@@ -1011,7 +1018,11 @@ class PlexHistory(object):
         return self._server.query(self.historyKey, method=self._server._session.delete)
 
 
-class MediaContainer(PlexObject):
+class MediaContainer(
+    Generic[PlexObjectT],
+    List[PlexObjectT],
+    PlexObject,
+):
     """ Represents a single MediaContainer.
 
         Attributes:
@@ -1024,11 +1035,71 @@ class MediaContainer(PlexObject):
             librarySectionUUID (str): :class:`~plexapi.library.LibrarySection` UUID.
             mediaTagPrefix (str): "/system/bundle/media/flags/"
             mediaTagVersion (int): Unknown
+            offset (int): The offset of current results.
             size (int): The number of items in the hub.
+            totalSize (int): The total number of items for the query.
 
     """
     TAG = 'MediaContainer'
 
+    def __init__(
+        self,
+        server: "PlexServer",
+        data: Element,
+        *args: PlexObjectT,
+        initpath: Optional[str] = None,
+        parent: Optional[PlexObject] = None,
+    ) -> None:
+        # super calls Generic.__init__ which calls list.__init__ eventually
+        super().__init__(*args)
+        PlexObject.__init__(self, server, data, initpath, parent)
+
+    def extend(
+        self: MediaContainerT,
+        __iterable: Union[Iterable[PlexObjectT], MediaContainerT],
+    ) -> None:
+        curr_size = self.size if self.size is not None else len(self)
+        super().extend(__iterable)
+        # update size, totalSize, and offset
+        if not isinstance(__iterable, MediaContainer):
+            return
+
+        # prefer the totalSize of the new iterable even if it is smaller
+        self.totalSize = (
+            __iterable.totalSize
+            if __iterable.totalSize is not None
+            else self.totalSize
+        )  # ideally both should be equal
+
+        # the size of the new iterable is added to the current size
+        self.size = curr_size + (
+            __iterable.size if __iterable.size is not None else len(__iterable)
+        )
+
+        # the offset is the minimum of the two, prefering older values
+        if self.offset is not None and __iterable.offset is not None:
+            self.offset = min(self.offset, __iterable.offset)
+        else:
+            self.offset = (
+                self.offset if self.offset is not None else __iterable.offset
+            )
+
+        # for all other attributes, overwrite with the new iterable's values if previously None
+        for key in (
+            "allowSync",
+            "augmentationKey",
+            "identifier",
+            "librarySectionID",
+            "librarySectionTitle",
+            "librarySectionUUID",
+            "mediaTagPrefix",
+            "mediaTagVersion",
+        ):
+            if (not hasattr(self, key)) or (getattr(self, key) is None):
+                if not hasattr(__iterable, key):
+                    continue
+                setattr(self, key, getattr(__iterable, key))
+
     def _loadData(self, data):
         self._data = data
         self.allowSync = utils.cast(int, data.attrib.get('allowSync'))
@@ -1039,4 +1110,6 @@ class MediaContainer(PlexObject):
         self.librarySectionUUID = data.attrib.get('librarySectionUUID')
         self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
         self.mediaTagVersion = data.attrib.get('mediaTagVersion')
+        self.offset = utils.cast(int, data.attrib.get("offset"))
         self.size = utils.cast(int, data.attrib.get('size'))
+        self.totalSize = utils.cast(int, data.attrib.get("totalSize"))
diff --git a/plexapi/library.py b/plexapi/library.py
index e14d522..662b546 100644
--- a/plexapi/library.py
+++ b/plexapi/library.py
@@ -383,7 +383,8 @@ class Library(PlexObject):
         part = (f'/library/sections?name={quote_plus(name)}&type={type}&agent={agent}'
                 f'&scanner={quote_plus(scanner)}&language={language}&{urlencode(locations, doseq=True)}')
         if kwargs:
-            part += urlencode(kwargs)
+            prefs_params = {f'prefs[{k}]': v for k, v in kwargs.items()}
+            part += f'&{urlencode(prefs_params)}'
         return self._server.query(part, method=self._server._session.post)
 
     def history(self, maxresults=None, mindate=None):


Test patch:
diff --git a/tests/test_fetch_items.py b/tests/test_fetch_items.py
new file mode 100644
index 0000000..8ebaedc
--- /dev/null
+++ b/tests/test_fetch_items.py
@@ -0,0 +1,32 @@
+from plexapi.audio import Track
+from plexapi.base import MediaContainer
+
+
+def test_media_container_is_list():
+    container = MediaContainer(None, None, Track(None, None))
+    assert isinstance(container, list)
+    assert len(container) == 1
+    container.append(Track(None, None))
+    assert len(container) == 2
+
+
+def test_media_container_extend():
+    container_1 = MediaContainer(None, None, Track(None, None))
+    container_2 = MediaContainer(
+        None, None, [Track(None, None), Track(None, None)]
+    )
+    container_1.size, container_2.size = 1, 2
+    container_1.offset, container_2.offset = 3, 4
+    container_1.totalSize = container_2.totalSize = 10
+    container_1.extend(container_2)
+    assert container_1.size == 1 + 2
+    assert container_1.offset == min(3, 4)
+    assert container_1.totalSize == 10
+
+
+def test_fetch_items_with_media_container(show):
+    all_episodes = show.episodes()
+    some_episodes = show.episodes(maxresults=2)
+    assert some_episodes.size == 2
+    assert some_episodes.offset == 0
+    assert some_episodes.totalSize == len(all_episodes)
diff --git a/tests/test_library.py b/tests/test_library.py
index b34fa51..cc5289d 100644
--- a/tests/test_library.py
+++ b/tests/test_library.py
@@ -193,6 +193,32 @@ def test_library_add_edit_delete(plex, movies, photos):
     assert section not in plex.library.sections()
 
 
+def test_library_add_advanced_settings(plex, movies):
+    # Create Other Videos library = No external metadata scanning
+    section_name = "plexapi_test_advanced_section"
+    movie_location = movies.locations[0]
+    advanced_settings = {"enableCinemaTrailers": 0,
+                         "enableBIFGeneration": 0,
+                         "augmentWithProviderContent": 0,
+                         "enableCreditsMarkerGeneration": 0}
+    plex.library.add(
+        name=section_name,
+        type="movie",
+        agent="com.plexapp.agents.none",
+        scanner="Plex Video Files Scanner",
+        language="xn",
+        location=[movie_location],
+        **advanced_settings
+    )
+    section = plex.library.section(section_name)
+    assert section.title == section_name
+    for setting in section.settings():
+        if setting.value != setting.default:
+            assert advanced_settings.get(setting.id) == setting.value
+    section.delete()
+    assert section not in plex.library.sections()
+
+
 def test_library_Library_cleanBundle(plex):
     plex.library.cleanBundles()