From b62ae9e9c4eb6472e1adc8a0700a38fcd8f04678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 00:47:14 +0300 Subject: [PATCH 01/17] Add search_episodes method to PlexLibrarySection --- plextraktsync/plex/PlexLibrarySection.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plextraktsync/plex/PlexLibrarySection.py b/plextraktsync/plex/PlexLibrarySection.py index 47b51a4329..b92e49b164 100644 --- a/plextraktsync/plex/PlexLibrarySection.py +++ b/plextraktsync/plex/PlexLibrarySection.py @@ -61,6 +61,29 @@ def find_by_id(self, id: str | int) -> PlexMedia | None: except NotFound: return None + def search_episodes(self): + if self.section.type == "show": + maxresults = self.section.searchEpisodes(maxresults=0) + try: + total_size = maxresults.total_size + except AttributeError: + raise RuntimeError("Needs PlexAPI patch") + section = self.section + plex = self.plex + + class Wrapper: + def __len__(self): + return total_size + + def __iter__(self): + for ep in section.searchEpisodes(): + # print(f"giving out {ep}") + yield PlexLibraryItem(ep, plex=plex) + + return Wrapper() + + return None + def all(self, max_items: int): libtype = self.section.TYPE key = self.section._buildSearchKey(libtype=libtype, returnKwargs=False) From 941cf815d96239f86aac833c6741f67c45cb26e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 00:47:34 +0300 Subject: [PATCH 02/17] Add episodes_from_sections to Walker --- plextraktsync/plan/Walker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index 2e5918ecab..0e421d8ec3 100644 --- a/plextraktsync/plan/Walker.py +++ b/plextraktsync/plan/Walker.py @@ -137,6 +137,18 @@ def media_from_sections(self, sections: list[PlexLibrarySection]) -> Generator[P ) yield from it + def episodes_from_sections(self, sections: list[PlexLibrarySection]) -> Generator[PlexLibraryItem, Any, None]: + for section in sections: + with measure_time(f"{section.title_link} processed", extra={"markup": True}): + self.set_window_title(f"Processing {section.title}") + items = section.search_episodes() + it = self.progressbar( + items, + total=len(items), + desc=f"Processing {section.title_link}", + ) + yield from it + def media_from_items(self, libtype: str, items: list) -> Generator[PlexLibraryItem, Any, None]: it = self.progressbar(items, desc=f"Processing {libtype}s") for m in it: From 0b1348d90bf063fd32a6799461491b9835ce32ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 01:22:53 +0300 Subject: [PATCH 03/17] Update find_episodes to use episodes_from_sections --- plextraktsync/plan/Walker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index 0e421d8ec3..dfbffe81e1 100644 --- a/plextraktsync/plan/Walker.py +++ b/plextraktsync/plan/Walker.py @@ -100,11 +100,11 @@ def find_episodes(self): if self.plan.episodes: yield from self.get_plex_episodes(self.plan.episodes) - for ps in self.get_plex_shows(): - show = self.mf.resolve_any(ps) - if not show: + for ep in self.episodes_from_sections(self.plan.show_sections): + m = self.mf.resolve_any(ep) + if not m: continue - yield from self.episode_from_show(show) + yield m def walk_shows(self, shows: set[Media], title="Processing Shows"): if not shows: From 60153b2aae0df1f9c45072b115e0dd3a80a71258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 01:42:59 +0300 Subject: [PATCH 04/17] Add PlexSectionPager class --- plextraktsync/plex/PlexShowSectionPager.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 plextraktsync/plex/PlexShowSectionPager.py diff --git a/plextraktsync/plex/PlexShowSectionPager.py b/plextraktsync/plex/PlexShowSectionPager.py new file mode 100644 index 0000000000..5c3e78d544 --- /dev/null +++ b/plextraktsync/plex/PlexShowSectionPager.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from plextraktsync.plex.PlexLibraryItem import PlexLibraryItem + +if TYPE_CHECKING: + from plexapi.library import ShowSection + + from plextraktsync.plex.PlexApi import PlexApi + + +class PlexShowSectionPager: + def __init__(self, section: ShowSection, plex: PlexApi): + self.section = section + self.plex = plex + + def __len__(self): + return self.total_size + + @cached_property + def total_size(self): + maxresults = self.section.searchEpisodes(maxresults=0) + try: + return maxresults.total_size + except AttributeError: + raise RuntimeError("Needs PlexAPI patch") + + def __iter__(self): + for ep in self.section.searchEpisodes(): + yield PlexLibraryItem(ep, plex=self.plex) From e5c36a76ba48b2e9e8dbbc0935ebbc1b658c1ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 01:44:02 +0300 Subject: [PATCH 05/17] Use PlexShowSectionPager for pagination --- plextraktsync/plex/PlexLibrarySection.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/plextraktsync/plex/PlexLibrarySection.py b/plextraktsync/plex/PlexLibrarySection.py index b92e49b164..2785cd479f 100644 --- a/plextraktsync/plex/PlexLibrarySection.py +++ b/plextraktsync/plex/PlexLibrarySection.py @@ -63,24 +63,10 @@ def find_by_id(self, id: str | int) -> PlexMedia | None: def search_episodes(self): if self.section.type == "show": - maxresults = self.section.searchEpisodes(maxresults=0) - try: - total_size = maxresults.total_size - except AttributeError: - raise RuntimeError("Needs PlexAPI patch") - section = self.section - plex = self.plex - - class Wrapper: - def __len__(self): - return total_size - - def __iter__(self): - for ep in section.searchEpisodes(): - # print(f"giving out {ep}") - yield PlexLibraryItem(ep, plex=plex) - - return Wrapper() + from plextraktsync.plex.PlexShowSectionPager import \ + PlexShowSectionPager + + return PlexShowSectionPager(section=self.section, plex=self.plex) return None From 79cc2a3d23e9f7a40f63be227f73ecee4c45f4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 Sep 2023 02:00:58 +0300 Subject: [PATCH 06/17] PlexShowSectionPager: Fetch results by chunks --- plextraktsync/plex/PlexShowSectionPager.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/plextraktsync/plex/PlexShowSectionPager.py b/plextraktsync/plex/PlexShowSectionPager.py index 5c3e78d544..9b35ee6ba9 100644 --- a/plextraktsync/plex/PlexShowSectionPager.py +++ b/plextraktsync/plex/PlexShowSectionPager.py @@ -28,5 +28,21 @@ def total_size(self): raise RuntimeError("Needs PlexAPI patch") def __iter__(self): - for ep in self.section.searchEpisodes(): - yield PlexLibraryItem(ep, plex=self.plex) + from plexapi import X_PLEX_CONTAINER_SIZE + + max_items = self.total_size + start = 0 + size = X_PLEX_CONTAINER_SIZE + + while True: + items = self.section.searchEpisodes(container_start=start, container_size=size, maxresults=size) + + if not len(items): + break + + for ep in items: + yield PlexLibraryItem(ep, plex=self.plex) + + start += size + if start > max_items: + break From 112587b431d7c06ee80e4cafac5f3339881c450b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 14 Sep 2023 21:21:04 +0300 Subject: [PATCH 07/17] Use section.totalViewSize method --- plextraktsync/plex/PlexShowSectionPager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plextraktsync/plex/PlexShowSectionPager.py b/plextraktsync/plex/PlexShowSectionPager.py index 9b35ee6ba9..0a9f50c397 100644 --- a/plextraktsync/plex/PlexShowSectionPager.py +++ b/plextraktsync/plex/PlexShowSectionPager.py @@ -21,11 +21,7 @@ def __len__(self): @cached_property def total_size(self): - maxresults = self.section.searchEpisodes(maxresults=0) - try: - return maxresults.total_size - except AttributeError: - raise RuntimeError("Needs PlexAPI patch") + return self.section.totalViewSize(libtype="episode") def __iter__(self): from plexapi import X_PLEX_CONTAINER_SIZE From 230f607108adff5e068cd8443972c334831e75e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 16:38:12 +0200 Subject: [PATCH 08/17] Add show_id property to PlexLibraryItem --- plextraktsync/plex/PlexLibraryItem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plextraktsync/plex/PlexLibraryItem.py b/plextraktsync/plex/PlexLibraryItem.py index 2027729001..828a63ebad 100644 --- a/plextraktsync/plex/PlexLibraryItem.py +++ b/plextraktsync/plex/PlexLibraryItem.py @@ -312,6 +312,12 @@ def season_number(self): def episode_number(self): return self.item.index + @property + def show_id(self): + if self.type != "episode": + raise RuntimeError("show_id is valid for episodes only") + return self.item.grandparentRatingKey + @staticmethod def date_value(date): if not date: From 2be8b19196190aee7bdb7da6024099e213090613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 18:36:25 +0200 Subject: [PATCH 09/17] Add key property to PlexLibraryItem --- plextraktsync/plex/PlexLibraryItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plextraktsync/plex/PlexLibraryItem.py b/plextraktsync/plex/PlexLibraryItem.py index 828a63ebad..166fde7273 100644 --- a/plextraktsync/plex/PlexLibraryItem.py +++ b/plextraktsync/plex/PlexLibraryItem.py @@ -23,6 +23,10 @@ def __init__(self, item: PlexMedia, plex: PlexApi = None): self.item = item self.plex = plex + @property + def key(self): + return self.item.ratingKey + @property def is_legacy_agent(self): return not self.item.guid.startswith("plex://") From 5497391849fe873808098199e1bc0ffb771b6503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 18:46:19 +0200 Subject: [PATCH 10/17] Add show setter/getter for PlexLibraryItem --- plextraktsync/plex/PlexLibraryItem.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plextraktsync/plex/PlexLibraryItem.py b/plextraktsync/plex/PlexLibraryItem.py index 166fde7273..c50b85fe43 100644 --- a/plextraktsync/plex/PlexLibraryItem.py +++ b/plextraktsync/plex/PlexLibraryItem.py @@ -22,6 +22,7 @@ class PlexLibraryItem: def __init__(self, item: PlexMedia, plex: PlexApi = None): self.item = item self.plex = plex + self._show = None @property def key(self): @@ -322,6 +323,20 @@ def show_id(self): raise RuntimeError("show_id is valid for episodes only") return self.item.grandparentRatingKey + @property + def show(self): + if self._show is None: + self._show = self.plex.fetch_item(self.show_id) + + return self._show + + @show.setter + def show(self, show): + if self.type != "episode": + raise RuntimeError("show_id is valid for episodes only") + + self._show = show + @staticmethod def date_value(date): if not date: From 655edd0efc22ef2bef10069b4f6306e5ba7581af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 16:37:44 +0200 Subject: [PATCH 11/17] Cache show fetches in find_episodes --- plextraktsync/plan/Walker.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index dfbffe81e1..f67123cb6d 100644 --- a/plextraktsync/plan/Walker.py +++ b/plextraktsync/plan/Walker.py @@ -100,10 +100,16 @@ def find_episodes(self): if self.plan.episodes: yield from self.get_plex_episodes(self.plan.episodes) + show_cache = {} for ep in self.episodes_from_sections(self.plan.show_sections): - m = self.mf.resolve_any(ep) + show_id = ep.show_id + show = show_cache[show_id] if show_id in show_cache else None + m = self.mf.resolve_any(ep, show) if not m: continue + if show: + m.show = show + show_cache[show_id] = m.show yield m def walk_shows(self, shows: set[Media], title="Processing Shows"): From 65a4a2bb6ef0683ff275e1562f668456fd5f9d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 18:47:16 +0200 Subject: [PATCH 12/17] Use self.plex.show property in media.show method --- plextraktsync/media.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plextraktsync/media.py b/plextraktsync/media.py index dd3842ff82..39761c2ece 100644 --- a/plextraktsync/media.py +++ b/plextraktsync/media.py @@ -89,8 +89,7 @@ def trakt_url(self): @property def show(self) -> Media | None: if self._show is None and self.mf and not self.plex.is_discover: - # TODO: fetch show for discover items - ps = self.plex_api.fetch_item(self.plex.item.grandparentRatingKey) + ps = self.plex.show ms = self.mf.resolve_any(ps) self._show = ms From 80b6ea58c9697459c86cfc7807f9dea36221d0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 21:33:32 +0200 Subject: [PATCH 13/17] Add logger to Walker class --- plextraktsync/plan/Walker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index f67123cb6d..191d94f340 100644 --- a/plextraktsync/plan/Walker.py +++ b/plextraktsync/plan/Walker.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from plextraktsync.decorators.measure_time import measure_time +from plextraktsync.factory import logging from plextraktsync.mixin.SetWindowTitle import SetWindowTitle from plextraktsync.plex.PlexGuid import PlexGuid from plextraktsync.plex.PlexLibraryItem import PlexLibraryItem @@ -39,6 +40,7 @@ def __init__( self.trakt = trakt self.mf = mf self.config = config + self.logger = logging.getLogger("PlexTraktSync.Walker") @cached_property def plan(self): From eee415174d3d254c34d3093388e8e51a5c9494cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 18:51:26 +0200 Subject: [PATCH 14/17] Preload plex shows in find_episodes --- plextraktsync/plan/Walker.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index 191d94f340..a8f0a31a9d 100644 --- a/plextraktsync/plan/Walker.py +++ b/plextraktsync/plan/Walker.py @@ -102,9 +102,18 @@ def find_episodes(self): if self.plan.episodes: yield from self.get_plex_episodes(self.plan.episodes) + # Preload plex shows + plex_shows = {} + + self.logger.info("Preload shows data") + for show in self.get_plex_shows(): + plex_shows[show.key] = show + self.logger.info(f"Preloaded shows data ({len(plex_shows)} shows)") + show_cache = {} for ep in self.episodes_from_sections(self.plan.show_sections): show_id = ep.show_id + ep.show = plex_shows[show_id] show = show_cache[show_id] if show_id in show_cache else None m = self.mf.resolve_any(ep, show) if not m: From 80696af4aa14f8b2406c69f481df959f6e7260fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 20:04:40 +0200 Subject: [PATCH 15/17] Extract find_by_episode_guid method --- plextraktsync/trakt/TraktApi.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plextraktsync/trakt/TraktApi.py b/plextraktsync/trakt/TraktApi.py index ac322e24c8..78d543c736 100644 --- a/plextraktsync/trakt/TraktApi.py +++ b/plextraktsync/trakt/TraktApi.py @@ -211,15 +211,18 @@ def remove_from_watchlist(self, m): self.queue.remove_from_watchlist((m.media_type, item)) + def find_by_episode_guid(self, guid: PlexGuid): + ts: TVShow = self.search_by_id(guid.show_id, id_type=guid.provider, media_type="show") + if not ts: + return None + + lookup = TraktLookup(ts) + + return self.find_episode_guid(guid, lookup) + def find_by_guid(self, guid: PlexGuid): if guid.type == "episode" and guid.is_episode: - ts: TVShow = self.search_by_id( - guid.show_id, id_type=guid.provider, media_type="show" - ) - if ts: - lookup = TraktLookup(ts) - - return self.find_episode_guid(guid, lookup) + return self.find_by_episode_guid(guid) else: tm = self.search_by_id(guid.id, id_type=guid.provider, media_type=guid.type) if tm is None and guid.type == "movie": From 2e1f10ef63f0cc97ef4da09cb72dfbc43aaa8c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 21:24:18 +0200 Subject: [PATCH 16/17] Save show object in trakt episode in find_by_episode_guid --- plextraktsync/trakt/TraktApi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plextraktsync/trakt/TraktApi.py b/plextraktsync/trakt/TraktApi.py index 78d543c736..51e698d2e4 100644 --- a/plextraktsync/trakt/TraktApi.py +++ b/plextraktsync/trakt/TraktApi.py @@ -217,8 +217,14 @@ def find_by_episode_guid(self, guid: PlexGuid): return None lookup = TraktLookup(ts) + te = self.find_episode_guid(guid, lookup) + if not te: + return None + + # NOTE: overwrites property of type str + te.show = ts - return self.find_episode_guid(guid, lookup) + return te def find_by_guid(self, guid: PlexGuid): if guid.type == "episode" and guid.is_episode: From dd7b4913f588798b47385e357feea269f301ebe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 7 Jan 2024 21:25:15 +0200 Subject: [PATCH 17/17] Prefer cached trakt show for .show --- plextraktsync/media.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plextraktsync/media.py b/plextraktsync/media.py index 39761c2ece..9247111d1f 100644 --- a/plextraktsync/media.py +++ b/plextraktsync/media.py @@ -6,6 +6,7 @@ from plexapi.exceptions import PlexApiException from requests import RequestException from trakt.errors import TraktException +from trakt.tv import TVShow from plextraktsync.factory import logger from plextraktsync.trakt.TraktLookup import TraktLookup @@ -90,7 +91,11 @@ def trakt_url(self): def show(self) -> Media | None: if self._show is None and self.mf and not self.plex.is_discover: ps = self.plex.show - ms = self.mf.resolve_any(ps) + if isinstance(self.trakt.show, TVShow): + ts = self.trakt.show + ms = self.mf.make_media(trakt=ts, plex=ps) + else: + ms = self.mf.resolve_any(ps) self._show = ms return self._show