diff --git a/plextraktsync/media.py b/plextraktsync/media.py index dd3842ff82..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 @@ -89,9 +90,12 @@ 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) - ms = self.mf.resolve_any(ps) + ps = self.plex.show + 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 diff --git a/plextraktsync/plan/Walker.py b/plextraktsync/plan/Walker.py index 2e5918ecab..a8f0a31a9d 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): @@ -100,11 +102,26 @@ 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: + # 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: continue - yield from self.episode_from_show(show) + if show: + m.show = show + show_cache[show_id] = m.show + yield m def walk_shows(self, shows: set[Media], title="Processing Shows"): if not shows: @@ -137,6 +154,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: diff --git a/plextraktsync/plex/PlexLibraryItem.py b/plextraktsync/plex/PlexLibraryItem.py index 2027729001..c50b85fe43 100644 --- a/plextraktsync/plex/PlexLibraryItem.py +++ b/plextraktsync/plex/PlexLibraryItem.py @@ -22,6 +22,11 @@ class PlexLibraryItem: def __init__(self, item: PlexMedia, plex: PlexApi = None): self.item = item self.plex = plex + self._show = None + + @property + def key(self): + return self.item.ratingKey @property def is_legacy_agent(self): @@ -312,6 +317,26 @@ 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 + + @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: diff --git a/plextraktsync/plex/PlexLibrarySection.py b/plextraktsync/plex/PlexLibrarySection.py index 47b51a4329..2785cd479f 100644 --- a/plextraktsync/plex/PlexLibrarySection.py +++ b/plextraktsync/plex/PlexLibrarySection.py @@ -61,6 +61,15 @@ def find_by_id(self, id: str | int) -> PlexMedia | None: except NotFound: return None + def search_episodes(self): + if self.section.type == "show": + from plextraktsync.plex.PlexShowSectionPager import \ + PlexShowSectionPager + + return PlexShowSectionPager(section=self.section, plex=self.plex) + + return None + def all(self, max_items: int): libtype = self.section.TYPE key = self.section._buildSearchKey(libtype=libtype, returnKwargs=False) diff --git a/plextraktsync/plex/PlexShowSectionPager.py b/plextraktsync/plex/PlexShowSectionPager.py new file mode 100644 index 0000000000..0a9f50c397 --- /dev/null +++ b/plextraktsync/plex/PlexShowSectionPager.py @@ -0,0 +1,44 @@ +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): + return self.section.totalViewSize(libtype="episode") + + def __iter__(self): + 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 diff --git a/plextraktsync/trakt/TraktApi.py b/plextraktsync/trakt/TraktApi.py index ac322e24c8..51e698d2e4 100644 --- a/plextraktsync/trakt/TraktApi.py +++ b/plextraktsync/trakt/TraktApi.py @@ -211,15 +211,24 @@ 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) + te = self.find_episode_guid(guid, lookup) + if not te: + return None + + # NOTE: overwrites property of type str + te.show = ts + + return te + 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":