Skip to content

Commit ceb085d

Browse files
Touchstone64Touchstone64Copilot
authored
Data parity for seasons and episodes (#1593)
* Ensure data parity for seasons and episodes retrieved by section searchSeasons() and searchEpisodes() methods and Show.seasons() and Season.episodes() methods * Extract includeGuids inclusion for parent/child searches into a mixin * Implement the key-building mixin for all parent/child retrieval methods * Add unit tests for all permutations * Fix the mistakes the unit tests inevitably show up * Linting corrections (too many spaces) * Linting corrections (trailing whitespace) * More linting issues * Preserve fetchItem's failure mode when encountering invalid keys Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Improve Season unit tests to provide partial objects have guids * Change the relational key builder to explicitly support filters in its parameters * Correct the implementation of relational key construction in Episode._season(), separating XML attributes from key construction. * Add the TV parent-child mixin to the collection of composite mixins. * Improve the statement of intent when building relational keys * Prevent test local variables from shadowing fixtures * Move any parent-child query params into the key-building stage * Move the relational key builder into PlexObject and remove the parent-child mixin. * Restore the trailing comma for Composite Mixins * Rename the relational key builder to be a query key builder --------- Co-authored-by: Touchstone64 <touchstone64@gweb.me.uk> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e512ccd commit ceb085d

File tree

3 files changed

+115
-14
lines changed

3 files changed

+115
-14
lines changed

plexapi/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ def _buildDetailsKey(self, **kwargs):
176176
details_key += '?' + urlencode(sorted(params.items()))
177177
return details_key
178178

179+
def _buildQueryKey(self, key, **kwargs):
180+
""" Returns a query key suitable for fetching partial objects.
181+
182+
Parameters:
183+
key (str): The key to which options should be added to form a query.
184+
**kwargs (dict): Optional query parameters to add to the key, such as
185+
'excludeAllLeaves=1' or 'index=0'. Additional XML filters should instead
186+
be passed into search functions. See :func:`~plexapi.base.PlexObject.fetchItems`
187+
for details.
188+
189+
"""
190+
if not key:
191+
return None
192+
193+
args = {'includeGuids': 1, **kwargs}
194+
params = utils.joinArgs(args)
195+
196+
return f"{key}{params}"
197+
179198
def _isChildOf(self, **kwargs):
180199
""" Returns True if this object is a child of the given attributes.
181200
This will search the parent objects all the way to the top.

plexapi/video.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ def season(self, title=None, season=None):
710710
Raises:
711711
:exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing.
712712
"""
713-
key = f'{self.key}/children?excludeAllLeaves=1'
713+
key = self._buildQueryKey(f'{self.key}/children', excludeAllLeaves=1)
714714
if title is not None and not isinstance(title, int):
715715
return self.fetchItem(key, Season, title__iexact=title)
716716
elif season is not None or isinstance(title, int):
@@ -723,7 +723,7 @@ def season(self, title=None, season=None):
723723

724724
def seasons(self, **kwargs):
725725
""" Returns a list of :class:`~plexapi.video.Season` objects in the show. """
726-
key = f'{self.key}/children?excludeAllLeaves=1'
726+
key = self._buildQueryKey(f'{self.key}/children', excludeAllLeaves=1)
727727
return self.fetchItems(key, Season, container_size=self.childCount, **kwargs)
728728

729729
def episode(self, title=None, season=None, episode=None):
@@ -737,7 +737,7 @@ def episode(self, title=None, season=None, episode=None):
737737
Raises:
738738
:exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing.
739739
"""
740-
key = f'{self.key}/allLeaves'
740+
key = self._buildQueryKey(f'{self.key}/allLeaves')
741741
if title is not None:
742742
return self.fetchItem(key, Episode, title__iexact=title)
743743
elif season is not None and episode is not None:
@@ -746,7 +746,7 @@ def episode(self, title=None, season=None, episode=None):
746746

747747
def episodes(self, **kwargs):
748748
""" Returns a list of :class:`~plexapi.video.Episode` objects in the show. """
749-
key = f'{self.key}/allLeaves'
749+
key = self._buildQueryKey(f'{self.key}/allLeaves')
750750
return self.fetchItems(key, Episode, **kwargs)
751751

752752
def get(self, title=None, season=None, episode=None):
@@ -906,7 +906,7 @@ def episode(self, title=None, episode=None):
906906
Raises:
907907
:exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing.
908908
"""
909-
key = f'{self.key}/children'
909+
key = self._buildQueryKey(f'{self.key}/children')
910910
if title is not None and not isinstance(title, int):
911911
return self.fetchItem(key, Episode, title__iexact=title)
912912
elif episode is not None or isinstance(title, int):
@@ -919,7 +919,7 @@ def episode(self, title=None, episode=None):
919919

920920
def episodes(self, **kwargs):
921921
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
922-
key = f'{self.key}/children'
922+
key = self._buildQueryKey(f'{self.key}/children')
923923
return self.fetchItems(key, Episode, **kwargs)
924924

925925
def get(self, title=None, episode=None):
@@ -928,7 +928,7 @@ def get(self, title=None, episode=None):
928928

929929
def show(self):
930930
""" Return the season's :class:`~plexapi.video.Show`. """
931-
return self.fetchItem(self.parentKey)
931+
return self.fetchItem(self._buildQueryKey(self.parentKey))
932932

933933
def watched(self):
934934
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
@@ -1136,7 +1136,12 @@ def parentThumb(self):
11361136
def _season(self):
11371137
""" Returns the :class:`~plexapi.video.Season` object by querying for the show's children. """
11381138
if self.grandparentKey and self.parentIndex is not None:
1139-
return self.fetchItem(f'{self.grandparentKey}/children?excludeAllLeaves=1&index={self.parentIndex}')
1139+
key = self._buildQueryKey(
1140+
f'{self.grandparentKey}/children',
1141+
excludeAllLeaves=1,
1142+
index=self.parentIndex
1143+
)
1144+
return self.fetchItem(key)
11401145
return None
11411146

11421147
def __repr__(self):
@@ -1213,11 +1218,11 @@ def hasPreviewThumbnails(self):
12131218

12141219
def season(self):
12151220
"""" Return the episode's :class:`~plexapi.video.Season`. """
1216-
return self.fetchItem(self.parentKey)
1221+
return self.fetchItem(self._buildQueryKey(self.parentKey))
12171222

12181223
def show(self):
12191224
"""" Return the episode's :class:`~plexapi.video.Show`. """
1220-
return self.fetchItem(self.grandparentKey)
1225+
return self.fetchItem(self._buildQueryKey(self.grandparentKey))
12211226

12221227
def _defaultSyncTitle(self):
12231228
""" Returns str, default title for a new syncItem. """

tests/test_video.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from urllib.parse import quote_plus
55

66
import pytest
7+
import plexapi.base
78
import plexapi.utils as plexutils
89
from plexapi.exceptions import BadRequest, NotFound
910
from plexapi.utils import setDatetimeTimezone
@@ -990,6 +991,30 @@ def test_video_Show_isPlayed(show):
990991
assert not show.isPlayed
991992

992993

994+
def test_video_Show_season_guids(show):
995+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
996+
try:
997+
season = show.season("Season 1")
998+
assert season.guids
999+
seasons = show.seasons()
1000+
assert len(seasons) > 0
1001+
assert seasons[0].guids
1002+
finally:
1003+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
1004+
1005+
1006+
def test_video_Show_episode_guids(show):
1007+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
1008+
try:
1009+
episode = show.episode("Winter Is Coming")
1010+
assert episode.guids
1011+
episodes = show.episodes()
1012+
assert len(episodes) > 0
1013+
assert episodes[0].guids
1014+
finally:
1015+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
1016+
1017+
9931018
def test_video_Show_section(show):
9941019
section = show.section()
9951020
assert section.title == "TV Shows"
@@ -1171,10 +1196,15 @@ def test_video_Season_attrs(show):
11711196

11721197

11731198
def test_video_Season_show(show):
1174-
season = show.seasons()[0]
1175-
season_by_name = show.season("Season 1")
1176-
assert show.ratingKey == season.parentRatingKey and season_by_name.parentRatingKey
1177-
assert season.ratingKey == season_by_name.ratingKey
1199+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
1200+
try:
1201+
season = show.seasons()[0]
1202+
season_by_name = show.season("Season 1")
1203+
assert show.ratingKey == season.parentRatingKey and season_by_name.parentRatingKey
1204+
assert season.ratingKey == season_by_name.ratingKey
1205+
assert season.guids
1206+
finally:
1207+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
11781208

11791209

11801210
def test_video_Season_watched(show):
@@ -1213,6 +1243,29 @@ def test_video_Season_episodes(show):
12131243
assert len(episodes) >= 1
12141244

12151245

1246+
def test_video_Season_episode_guids(show):
1247+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
1248+
try:
1249+
season = show.season("Season 1")
1250+
episode = season.episode("Winter Is Coming")
1251+
assert episode.guids
1252+
episodes = season.episodes()
1253+
assert len(episodes) > 0
1254+
assert episodes[0].guids
1255+
finally:
1256+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
1257+
1258+
1259+
def test_video_Season_show_guids(show):
1260+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
1261+
try:
1262+
a_show = show.season("Season 1").show()
1263+
assert a_show
1264+
assert 'tmdb://1399' in [i.id for i in a_show.guids]
1265+
finally:
1266+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
1267+
1268+
12161269
@pytest.mark.xfail(reason="Changing images fails randomly")
12171270
def test_video_Season_mixins_images(show):
12181271
season = show.season(season=1)
@@ -1283,6 +1336,30 @@ def test_video_Episode(show):
12831336
show.episode(season=1337, episode=1337)
12841337

12851338

1339+
def test_video_Episode_parent_guids(show):
1340+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('guids')
1341+
try:
1342+
episodes = show.episodes()
1343+
assert episodes
1344+
episode = episodes[0]
1345+
assert episode
1346+
assert episode.isPartialObject()
1347+
season = episode._season
1348+
assert season
1349+
assert season.isPartialObject()
1350+
assert season.guids
1351+
season = episode.season()
1352+
assert season
1353+
assert season.isPartialObject()
1354+
assert season.guids
1355+
parent_show = episode.show()
1356+
assert parent_show
1357+
assert parent_show.isPartialObject()
1358+
assert parent_show.guids
1359+
finally:
1360+
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.remove('guids')
1361+
1362+
12861363
def test_video_Episode_hidden_season(episode):
12871364
assert episode.skipParent is False
12881365
assert episode.parentRatingKey

0 commit comments

Comments
 (0)