From be2874bb8d6ee39e11432a6d7462b6de011bf24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 22:50:03 +0300 Subject: [PATCH 01/24] Add pluggy==1.4.0 dependency --- Pipfile | 1 + Pipfile.lock | 11 ++++++++++- requirements.txt | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 85a8d5c3f0..55d93ea6b7 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ oauthlib = "==3.2.2" pfzy = {version="==0.3.4", markers="python_version >= '3.7' and python_version < '4.0'"} platformdirs = {version="==4.2.0", python_version=">='3.7'"} plexapi = "==4.15.11" +pluggy = "==1.4.0" prompt-toolkit = "==3.0.43" pygments = "==2.17.2" python-dotenv = "==1.0.1" diff --git a/Pipfile.lock b/Pipfile.lock index 17f0a7af61..9db9e27075 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "747a6c3bccdb1421b59ebf85ad14b2d39af7b5af427532428c04d180dadec452" + "sha256": "2cb5b8abe9edbd7115bc76582758ab7d37a1ddff6494824f0796f1c3d05aeff7" }, "pipfile-spec": 6, "requires": { @@ -241,6 +241,15 @@ "markers": "python_version >= '3.8'", "version": "==4.15.11" }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, "prompt-toolkit": { "hashes": [ "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", diff --git a/requirements.txt b/requirements.txt index b923650bae..b720b60e8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ oauthlib==3.2.2; python_version >= '3.6' pfzy==0.3.4; python_version >= '3.7' and python_version < '4.0' platformdirs==4.2.0; python_version >= '3.7' and python_version >= '3.8' plexapi==4.15.11; python_version >= '3.8' +pluggy==1.4.0; python_version >= '3.8' prompt-toolkit==3.0.43; python_full_version >= '3.7.0' pygments==2.17.2; python_version >= '3.7' python-dotenv==1.0.1; python_version >= '3.8' From ceed649ebafe0194f7e2d0f5fa7c69f3e0ec9d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 22:55:06 +0300 Subject: [PATCH 02/24] Add plugin hookspec, hookimpl --- plextraktsync/plugin/__init__.py | 1 + plextraktsync/plugin/plugin.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 plextraktsync/plugin/__init__.py create mode 100644 plextraktsync/plugin/plugin.py diff --git a/plextraktsync/plugin/__init__.py b/plextraktsync/plugin/__init__.py new file mode 100644 index 0000000000..ad11af728f --- /dev/null +++ b/plextraktsync/plugin/__init__.py @@ -0,0 +1 @@ +from .plugin import hookimpl, hookspec # noqa: F401 diff --git a/plextraktsync/plugin/plugin.py b/plextraktsync/plugin/plugin.py new file mode 100644 index 0000000000..5a7ed3591a --- /dev/null +++ b/plextraktsync/plugin/plugin.py @@ -0,0 +1,4 @@ +import pluggy + +hookspec = pluggy.HookspecMarker("PlexTraktSync") +hookimpl = pluggy.HookimplMarker("PlexTraktSync") From 35f24260b1a4101d39fad2f833e64b2cceed8f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 22:47:34 +0300 Subject: [PATCH 03/24] Add SyncPluginInterface spec --- .../sync/plugin/SyncPluginInterface.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 plextraktsync/sync/plugin/SyncPluginInterface.py diff --git a/plextraktsync/sync/plugin/SyncPluginInterface.py b/plextraktsync/sync/plugin/SyncPluginInterface.py new file mode 100644 index 0000000000..1b69bd8bf3 --- /dev/null +++ b/plextraktsync/sync/plugin/SyncPluginInterface.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plextraktsync.plugin import hookspec + +if TYPE_CHECKING: + from plextraktsync.media.Media import Media + from plextraktsync.plan.Walker import Walker + from plextraktsync.sync.Sync import Sync + from plextraktsync.trakt.TraktUserListCollection import \ + TraktUserListCollection + + +class SyncPluginInterface: + """A hook specification namespace.""" + + @hookspec + def init(self, sync: Sync, trakt_lists: TraktUserListCollection, is_partial: bool, dry_run: bool): + """Hook called at sync process initialization""" + + @hookspec + def fini(self, walker: Walker, trakt_lists: TraktUserListCollection, dry_run: bool): + """Hook called at sync process finalization""" + + @hookspec + def walk_movie(self, movie: Media, dry_run: bool): + """Hook called walk a movie media object""" + + @hookspec + def walk_episode(self, episode: Media, dry_run: bool): + """Hook called walk a episode media object""" From b87f4ed41bb1015bbe0c9c50d6520f86a5ac85eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 23:05:20 +0300 Subject: [PATCH 04/24] Create ClearCollectedPlugin plugin --- plextraktsync/sync/ClearCollectedPlugin.py | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 plextraktsync/sync/ClearCollectedPlugin.py diff --git a/plextraktsync/sync/ClearCollectedPlugin.py b/plextraktsync/sync/ClearCollectedPlugin.py new file mode 100644 index 0000000000..2a70f0961b --- /dev/null +++ b/plextraktsync/sync/ClearCollectedPlugin.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable + +from plextraktsync.factory import logging +from plextraktsync.media.Media import Media +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.sync.Sync import Sync + from plextraktsync.trakt.TraktApi import TraktApi + from plextraktsync.trakt.types import TraktMedia + + +class ClearCollectedPlugin: + logger = logging.getLogger(__name__) + + def __init__(self, trakt: TraktApi): + self.trakt = trakt + self.episode_trakt_ids = set() + self.movie_trakt_ids = set() + self.is_partial = None + + @staticmethod + def enabled(config: SyncConfig): + return config.clear_collected + + @classmethod + def factory(cls, sync: Sync): + return cls(sync.trakt) + + @hookimpl + def init(self, is_partial: bool): + self.is_partial = is_partial + if is_partial: + self.logger.warning("Running partial library sync. Clear collected will be disabled.") + + @hookimpl + def fini(self, dry_run: bool): + if self.is_partial: + return + + self.clear_collected(self.trakt.movie_collection, self.movie_trakt_ids, dry_run=dry_run) + self.clear_collected(self.trakt.episodes_collection, self.episode_trakt_ids, dry_run=dry_run) + + @hookimpl + def walk_movie(self, movie: Media): + if self.is_partial: + return + + self.movie_trakt_ids.add(movie.trakt_id) + + def clear_collected(self, existing_items: Iterable[TraktMedia], keep_ids: set[int], dry_run): + from plextraktsync.trakt.trakt_set import trakt_set + + existing_ids = trakt_set(existing_items) + delete_ids = existing_ids - keep_ids + delete_items = (tm for tm in existing_items if tm.trakt in delete_ids) + + n = len(delete_ids) + for i, tm in enumerate(delete_items, start=1): + self.logger.info(f"Remove from Trakt collection ({i}/{n}): {tm}") + if not dry_run: + self.trakt.remove_from_collection(tm) From 0331ff5241105e16325de5d6ac95ce54f2f09c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 23:22:17 +0300 Subject: [PATCH 05/24] Create LikedListsPlugin plugin --- plextraktsync/sync/LikedListsPlugin.py | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 plextraktsync/sync/LikedListsPlugin.py diff --git a/plextraktsync/sync/LikedListsPlugin.py b/plextraktsync/sync/LikedListsPlugin.py new file mode 100644 index 0000000000..07df13cf6a --- /dev/null +++ b/plextraktsync/sync/LikedListsPlugin.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plextraktsync.factory import logging +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.sync.Sync import Sync + from plextraktsync.trakt.TraktApi import TraktApi + from plextraktsync.trakt.TraktUserListCollection import \ + TraktUserListCollection + + +class LikedListsPlugin: + logger = logging.getLogger(__name__) + + def __init__(self, trakt: TraktApi): + self.trakt = trakt + + @staticmethod + def enabled(config: SyncConfig): + return config.sync_liked_lists + + @classmethod + def factory(cls, sync: Sync): + return cls(sync.trakt) + + @hookimpl + def init(self, trakt_lists: TraktUserListCollection, is_partial: bool): + if is_partial: + self.logger.warning("Partial walk, disabling liked lists updating. " + "Liked lists won't update because it needs full library sync.") + else: + trakt_lists.load_lists(self.trakt.liked_lists) From ac6d413fb48bd0b17fbd74e29d5d168c5737caaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 23:26:16 +0300 Subject: [PATCH 06/24] Create AddCollectionPlugin plugin --- plextraktsync/sync/AddCollectionPlugin.py | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 plextraktsync/sync/AddCollectionPlugin.py diff --git a/plextraktsync/sync/AddCollectionPlugin.py b/plextraktsync/sync/AddCollectionPlugin.py new file mode 100644 index 0000000000..ed3eaeeb09 --- /dev/null +++ b/plextraktsync/sync/AddCollectionPlugin.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plextraktsync.factory import logging +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.media.Media import Media + from plextraktsync.sync.Sync import Sync + + +class AddCollectionPlugin: + logger = logging.getLogger(__name__) + + @staticmethod + def enabled(config: SyncConfig): + return config.plex_to_trakt["collection"] + + @classmethod + def factory(cls, sync: Sync): + return cls() + + @hookimpl + def walk_movie(self, movie: Media, dry_run: bool): + self.sync_collection(movie, dry_run=dry_run) + + @hookimpl + def walk_episode(self, episode: Media, dry_run: bool): + self.sync_collection(episode, dry_run=dry_run) + + def sync_collection(self, m: Media, dry_run: bool): + if m.is_collected: + return + + self.logger.info(f"Adding to Trakt collection: {m.title_link}", extra={"markup": True}) + + if not dry_run: + m.add_to_collection() From 4d4101a51bc115e8af1974bde3fe4e9360120be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 16:18:30 +0300 Subject: [PATCH 07/24] Create SyncWatchedPlugin plugin --- plextraktsync/sync/SyncWatchedPlugin.py | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 plextraktsync/sync/SyncWatchedPlugin.py diff --git a/plextraktsync/sync/SyncWatchedPlugin.py b/plextraktsync/sync/SyncWatchedPlugin.py new file mode 100644 index 0000000000..f13bb24dec --- /dev/null +++ b/plextraktsync/sync/SyncWatchedPlugin.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plextraktsync.factory import logging +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.media.Media import Media + from plextraktsync.sync.Sync import Sync + + +class SyncWatchedPlugin: + logger = logging.getLogger(__name__) + + def __init__(self, config: SyncConfig): + self.plex_to_trakt = config.plex_to_trakt["watched_status"] + self.trakt_to_plex = config.trakt_to_plex["watched_status"] + + @classmethod + def enabled(cls, sync: Sync): + return sync.config.sync_watched_status + + @classmethod + def factory(cls, sync: Sync): + return cls(config=sync.config) + + @hookimpl + def walk_movie(self, movie: Media, dry_run: bool): + self.sync_watched(movie, dry_run=dry_run) + + @hookimpl + def walk_episode(self, episode: Media, dry_run: bool): + self.sync_watched(episode, dry_run=dry_run) + + def sync_watched(self, m: Media, dry_run: bool): + if m.watched_on_plex is m.watched_on_trakt: + return + + if m.watched_on_plex: + if not self.plex_to_trakt: + return + + if m.is_episode and m.watched_before_reset: + show = m.plex.item.show() + self.logger.info(f"Show '{show.title}' has been reset in trakt at {m.show_reset_at}.") + self.logger.info(f"Marking '{show.title}' as unwatched in Plex.") + if not dry_run: + m.reset_show() + else: + self.logger.info(f"Marking as watched in Trakt: {m.title_link}", extra={"markup": True}) + if not dry_run: + m.mark_watched_trakt() + elif m.watched_on_trakt: + if not self.trakt_to_plex: + return + self.logger.info(f"Marking as watched in Plex: {m.title_link}", extra={"markup": True}) + if not dry_run: + m.mark_watched_plex() From 282e182ea773d095bdc3b9318ef878e6c54cc350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 15:59:53 +0300 Subject: [PATCH 08/24] Create SyncRatingsPlugin plugin --- plextraktsync/sync/SyncRatingsPlugin.py | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 plextraktsync/sync/SyncRatingsPlugin.py diff --git a/plextraktsync/sync/SyncRatingsPlugin.py b/plextraktsync/sync/SyncRatingsPlugin.py new file mode 100644 index 0000000000..cd6287015f --- /dev/null +++ b/plextraktsync/sync/SyncRatingsPlugin.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plextraktsync.factory import logging +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.media.Media import Media + from plextraktsync.plan.Walker import Walker + from plextraktsync.sync.Sync import Sync + + +class SyncRatingsPlugin: + logger = logging.getLogger(__name__) + + def __init__(self, config: SyncConfig): + self.rating_priority = config["rating_priority"] + self.plex_to_trakt = config.plex_to_trakt["ratings"] + self.trakt_to_plex = config.trakt_to_plex["ratings"] + self.shows = None + + @staticmethod + def enabled(config: SyncConfig): + return config.sync_ratings + + @classmethod + def factory(cls, sync: Sync): + return cls(config=sync.config) + + @hookimpl + def walk_movie(self, movie: Media, dry_run: bool): + self.sync_ratings(movie, dry_run=dry_run) + + @hookimpl + def walk_episode(self, episode: Media, dry_run: bool): + self.sync_ratings(episode, dry_run=dry_run) + + if episode.show: + self.shows.add(episode.show) + + @hookimpl + def init(self): + self.shows = set() + + @hookimpl + def fini(self, walker: Walker, dry_run: bool): + for show in walker.walk_shows(self.shows, title="Syncing show ratings"): + self.sync_ratings(show, dry_run=dry_run) + + def sync_ratings(self, m: Media, dry_run: bool): + if m.plex_rating == m.trakt_rating: + return + + has_trakt = m.trakt_rating is not None + has_plex = m.plex_rating is not None + rate = None + + if self.rating_priority == "none": + # Only rate items with missing rating + if self.plex_to_trakt and has_plex and not has_trakt: + rate = "trakt" + elif self.trakt_to_plex and has_trakt and not has_plex: + rate = "plex" + + elif self.rating_priority == "trakt": + # If two-way rating sync, Trakt rating takes precedence over Plex rating + if self.trakt_to_plex and has_trakt: + rate = "plex" + elif self.plex_to_trakt and has_plex: + rate = "trakt" + + elif self.rating_priority == "plex": + # If two-way rating sync, Plex rating takes precedence over Trakt rating + if self.plex_to_trakt and has_plex: + rate = "trakt" + elif self.trakt_to_plex and has_trakt: + rate = "plex" + + if rate == "trakt": + self.logger.info(f"Rating {m.title_link} with {m.plex_rating} on Trakt (was {m.trakt_rating})", extra={"markup": True}) + if not dry_run: + m.trakt_rate() + + elif rate == "plex": + self.logger.info(f"Rating {m.title_link} with {m.trakt_rating} on Plex (was {m.plex_rating})", extra={"markup": True}) + if not dry_run: + m.plex_rate() From 8138a92f6639582a78b8fd16349cc8dddbbf270d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 18:13:49 +0300 Subject: [PATCH 09/24] Create WatchListPlugin plugin --- plextraktsync/sync/SyncWatchedPlugin.py | 6 +- plextraktsync/sync/WatchListPlugin.py | 125 ++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 plextraktsync/sync/WatchListPlugin.py diff --git a/plextraktsync/sync/SyncWatchedPlugin.py b/plextraktsync/sync/SyncWatchedPlugin.py index f13bb24dec..1fa9d0e98e 100644 --- a/plextraktsync/sync/SyncWatchedPlugin.py +++ b/plextraktsync/sync/SyncWatchedPlugin.py @@ -18,9 +18,9 @@ def __init__(self, config: SyncConfig): self.plex_to_trakt = config.plex_to_trakt["watched_status"] self.trakt_to_plex = config.trakt_to_plex["watched_status"] - @classmethod - def enabled(cls, sync: Sync): - return sync.config.sync_watched_status + @staticmethod + def enabled(config: SyncConfig): + return config.sync_watched_status @classmethod def factory(cls, sync: Sync): diff --git a/plextraktsync/sync/WatchListPlugin.py b/plextraktsync/sync/WatchListPlugin.py new file mode 100644 index 0000000000..e5f09583c5 --- /dev/null +++ b/plextraktsync/sync/WatchListPlugin.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from plextraktsync.decorators.measure_time import measure_time +from plextraktsync.factory import logging +from plextraktsync.plugin import hookimpl + +if TYPE_CHECKING: + from plextraktsync.config.SyncConfig import SyncConfig + from plextraktsync.media.Media import Media + from plextraktsync.plan.Walker import Walker + from plextraktsync.plex.PlexApi import PlexApi + from plextraktsync.sync.Sync import Sync + from plextraktsync.trakt.TraktApi import TraktApi + from plextraktsync.trakt.TraktUserListCollection import \ + TraktUserListCollection + + +class WatchListPlugin: + logger = logging.getLogger(__name__) + + def __init__(self, config: SyncConfig, plex: PlexApi, trakt: TraktApi): + self.config = config + self.plex = plex + self.trakt = trakt + + @staticmethod + def enabled(config: SyncConfig): + return any([ + config.plex_to_trakt["watchlist"], + config.trakt_to_plex["watchlist"], + ]) + + @classmethod + def factory(cls, sync: Sync): + return cls( + config=sync.config, + plex=sync.plex, + trakt=sync.trakt, + ) + + @hookimpl + def init(self, trakt_lists: TraktUserListCollection, is_partial: bool): + if self.config.update_plex_wl_as_pl: + if is_partial: + self.logger.warning("Running partial library sync. " + "Watchlist as playlist won't update because it needs full library sync.") + else: + trakt_lists.add_watchlist(self.trakt.watchlist_movies) + + def fini(self, walker: Walker, dry_run: bool): + if walker.config.walk_watchlist and self.sync_wl: + with measure_time("Updated watchlist"): + self.sync_watchlist(walker, dry_run=dry_run) + + if self.config.update_plex_wl_as_pl or self.config.sync_liked_lists: + if dry_run: + self.logger.warning("Running partial library sync. " + "Liked lists won't update because it needs full library sync.") + + @cached_property + def plex_wl(self): + from plextraktsync.plex.PlexWatchList import PlexWatchList + + return PlexWatchList(self.plex.watchlist()) + + @cached_property + def sync_wl(self): + return self.config.sync_wl and len(self.plex_wl) > 0 + + @cached_property + def trakt_wl(self): + from plextraktsync.trakt.TraktWatchlist import TraktWatchList + + return TraktWatchList(self.trakt.watchlist_movies + self.trakt.watchlist_shows) + + def watchlist_sync_item(self, m: Media, dry_run: bool): + if m.plex is None: + if self.config.update_plex_wl: + self.logger.info(f"Skipping {m.title_link} from Trakt watchlist because not found in Plex Discover", extra={"markup": True}) + elif self.config.update_trakt_wl: + self.logger.info(f"Removing {m.title_link} from Trakt watchlist", extra={"markup": True}) + if not dry_run: + m.remove_from_trakt_watchlist() + return + + if m in self.plex_wl: + if m not in self.trakt_wl: + if self.config.update_trakt_wl: + self.logger.info(f"Adding {m.title_link} to Trakt watchlist", extra={"markup": True}) + if not dry_run: + m.add_to_trakt_watchlist() + else: + self.logger.info(f"Removing {m.title_link} from Plex watchlist", extra={"markup": True}) + if not dry_run: + m.remove_from_plex_watchlist() + else: + # Plex Online search is inaccurate, and it doesn't offer search by id. + # Remove known match from trakt watchlist, so that the search would not be attempted. + # Example, trakt id 187634 where title mismatches: + # - "The Vortex": https://trakt.tv/movies/the-vortex-2012 + # - "Big Bad Bugs": https://app.plex.tv/desktop/#!/provider/tv.plex.provider.vod/details?key=%2Flibrary%2Fmetadata%2F5d776b1cad5437001f7936f4 + del self.trakt_wl[m] + elif m in self.trakt_wl: + if self.config.update_plex_wl: + self.logger.info(f"Adding {m.title_link} to Plex watchlist", extra={"markup": True}) + if not dry_run: + m.add_to_plex_watchlist() + else: + self.logger.info(f"Removing {m.title_link} from Trakt watchlist", extra={"markup": True}) + if not dry_run: + m.remove_from_trakt_watchlist() + + def sync_watchlist(self, walker: Walker, dry_run: bool): + # NOTE: Plex watchlist sync removes matching items from trakt lists + # See the comment above around "del self.trakt_wl[m]" + for m in walker.media_from_plexlist(self.plex_wl): + self.watchlist_sync_item(m, dry_run) + + # Because Plex syncing might have emptied the watchlists, skip printing the 0/0 progress + if len(self.trakt_wl): + for m in walker.media_from_traktlist(self.trakt_wl): + self.watchlist_sync_item(m, dry_run) From 2f6fb4a8145fb75c475883e6e166486cbf920d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 00:33:05 +0300 Subject: [PATCH 10/24] Package plugin namespaces --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 4a4735a2d3..b82271f05e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,9 +36,11 @@ packages = plextraktsync.mixin plextraktsync.plan plextraktsync.plex + plextraktsync.plugin plextraktsync.queue plextraktsync.rich plextraktsync.sync + plextraktsync.sync.plugin plextraktsync.trakt plextraktsync.util plextraktsync.watch From 9e3677600c6f4ebbc64e03370a91ef36740d19dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 00:42:25 +0300 Subject: [PATCH 11/24] Add SyncPluginManager class --- .../sync/plugin/SyncPluginManager.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 plextraktsync/sync/plugin/SyncPluginManager.py diff --git a/plextraktsync/sync/plugin/SyncPluginManager.py b/plextraktsync/sync/plugin/SyncPluginManager.py new file mode 100644 index 0000000000..30102c5886 --- /dev/null +++ b/plextraktsync/sync/plugin/SyncPluginManager.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from functools import cached_property + +import pluggy + + +class SyncPluginManager: + @cached_property + def pm(self): + from .SyncPluginInterface import SyncPluginInterface + + pm = pluggy.PluginManager("PlexTraktSync") + pm.add_hookspecs(SyncPluginInterface) + + return pm + + @cached_property + def hook(self): + return self.pm.hook From e8db85d5df167930694572a8f7f8c0434c64bf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 10 Apr 2024 07:58:51 +0300 Subject: [PATCH 12/24] Add register plugins method to SyncPluginManager --- .../sync/plugin/SyncPluginManager.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plextraktsync/sync/plugin/SyncPluginManager.py b/plextraktsync/sync/plugin/SyncPluginManager.py index 30102c5886..efe6f15c6a 100644 --- a/plextraktsync/sync/plugin/SyncPluginManager.py +++ b/plextraktsync/sync/plugin/SyncPluginManager.py @@ -1,11 +1,19 @@ from __future__ import annotations from functools import cached_property +from typing import TYPE_CHECKING import pluggy +from plextraktsync.factory import logging + +if TYPE_CHECKING: + from plextraktsync.sync.Sync import Sync + class SyncPluginManager: + logger = logging.getLogger(__name__) + @cached_property def pm(self): from .SyncPluginInterface import SyncPluginInterface @@ -18,3 +26,26 @@ def pm(self): @cached_property def hook(self): return self.pm.hook + + @property + def plugins(self): + from ..AddCollectionPlugin import AddCollectionPlugin + from ..ClearCollectedPlugin import ClearCollectedPlugin + from ..LikedListsPlugin import LikedListsPlugin + from ..SyncRatingsPlugin import SyncRatingsPlugin + from ..SyncWatchedPlugin import SyncWatchedPlugin + from ..WatchListPlugin import WatchListPlugin + yield AddCollectionPlugin + yield ClearCollectedPlugin + yield LikedListsPlugin + yield SyncRatingsPlugin + yield SyncWatchedPlugin + yield WatchListPlugin + + def register_plugins(self, sync: Sync): + for plugin in self.plugins: + enabled = plugin.enabled(sync.config) + self.logger.info(f"Enable sync plugin '{plugin.__name__}': {enabled}") + if not enabled: + continue + self.pm.register(plugin.factory(sync)) From c7eeb084b5cc95948b6cd4ea57600971897c134d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 00:47:17 +0300 Subject: [PATCH 13/24] Export SyncPluginManager class --- plextraktsync/sync/plugin/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 plextraktsync/sync/plugin/__init__.py diff --git a/plextraktsync/sync/plugin/__init__.py b/plextraktsync/sync/plugin/__init__.py new file mode 100644 index 0000000000..37ceedf3d9 --- /dev/null +++ b/plextraktsync/sync/plugin/__init__.py @@ -0,0 +1 @@ +from .SyncPluginManager import SyncPluginManager # noqa: F401 From 4eb79c1d9cb3228ebe6eb4fa1d008b5345acdc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 8 Apr 2024 22:59:53 +0300 Subject: [PATCH 14/24] Add SyncPluginManager to Sync class --- plextraktsync/sync/Sync.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 17121412a9..85e91a27b2 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -25,6 +25,7 @@ def __init__(self, config: SyncConfig, plex: PlexApi, trakt: TraktApi): self.config = config self.plex = plex self.trakt = trakt + self.walker = None @cached_property def plex_wl(self): @@ -43,9 +44,16 @@ def trakt_wl(self): return TraktWatchList(self.trakt.watchlist_movies + self.trakt.watchlist_shows) def sync(self, walker: Walker, dry_run=False): + self.walker = walker trakt_lists = TraktUserListCollection() is_partial = walker.is_partial and not dry_run + from plextraktsync.sync.plugin import SyncPluginManager + pm = SyncPluginManager() + pm.register_plugins(self) + + pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial) + if is_partial and self.config.clear_collected: self.logger.warning("Running partial library sync. Clear collected will be disabled.") @@ -64,6 +72,7 @@ def sync(self, walker: Walker, dry_run=False): if self.config.need_library_walk: movie_trakt_ids = set() for movie in walker.find_movies(): + pm.hook.walk_movie(movie=movie, dry_run=dry_run) self.sync_collection(movie, dry_run=dry_run) self.sync_ratings(movie, dry_run=dry_run) self.sync_watched(movie, dry_run=dry_run) @@ -78,6 +87,7 @@ def sync(self, walker: Walker, dry_run=False): shows = set() episode_trakt_ids = set() for episode in walker.find_episodes(): + pm.hook.walk_episode(episode=episode, dry_run=dry_run) self.sync_collection(episode, dry_run=dry_run) self.sync_ratings(episode, dry_run=dry_run) self.sync_watched(episode, dry_run=dry_run) @@ -108,6 +118,8 @@ def sync(self, walker: Walker, dry_run=False): with measure_time("Updated watchlist"): self.sync_watchlist(walker, dry_run=dry_run) + pm.hook.fini(walker=walker, trakt_lists=trakt_lists, dry_run=dry_run) + def sync_collection(self, m: Media, dry_run=False): if not self.config.plex_to_trakt["collection"]: return From 5ffb8937163f73195c586c8b4225cdb647dc54f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 10 Apr 2024 07:49:14 +0300 Subject: [PATCH 15/24] Sync: Add dry_run to init call --- plextraktsync/sync/Sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 85e91a27b2..4ced94e6a5 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -52,7 +52,7 @@ def sync(self, walker: Walker, dry_run=False): pm = SyncPluginManager() pm.register_plugins(self) - pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial) + pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial, dry_run=dry_run) if is_partial and self.config.clear_collected: self.logger.warning("Running partial library sync. Clear collected will be disabled.") From 8e5218b78269e3f590076d660b04ccce01c8452a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 15:31:16 +0300 Subject: [PATCH 16/24] Cleanup sync_collection from Sync --- plextraktsync/sync/Sync.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 4ced94e6a5..ddc74f4151 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -73,7 +73,6 @@ def sync(self, walker: Walker, dry_run=False): movie_trakt_ids = set() for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) - self.sync_collection(movie, dry_run=dry_run) self.sync_ratings(movie, dry_run=dry_run) self.sync_watched(movie, dry_run=dry_run) if not is_partial: @@ -88,7 +87,6 @@ def sync(self, walker: Walker, dry_run=False): episode_trakt_ids = set() for episode in walker.find_episodes(): pm.hook.walk_episode(episode=episode, dry_run=dry_run) - self.sync_collection(episode, dry_run=dry_run) self.sync_ratings(episode, dry_run=dry_run) self.sync_watched(episode, dry_run=dry_run) if not is_partial: @@ -120,18 +118,6 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.fini(walker=walker, trakt_lists=trakt_lists, dry_run=dry_run) - def sync_collection(self, m: Media, dry_run=False): - if not self.config.plex_to_trakt["collection"]: - return - - if m.is_collected: - return - - self.logger.info(f"Adding to Trakt collection: {m.title_link}", extra={"markup": True}) - - if not dry_run: - m.add_to_collection() - def sync_ratings(self, m: Media, dry_run=False): if not self.config.sync_ratings: return From 1ba567a48438db66e72f8691e200a379b1adfd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 15:58:51 +0300 Subject: [PATCH 17/24] Cleanup clear_collected from Sync --- plextraktsync/sync/Sync.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index ddc74f4151..f99b1f6d75 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -8,14 +8,11 @@ from plextraktsync.trakt.TraktUserListCollection import TraktUserListCollection if TYPE_CHECKING: - from typing import Iterable - from plextraktsync.config.SyncConfig import SyncConfig from plextraktsync.media.Media import Media from plextraktsync.plan.Walker import Walker from plextraktsync.plex.PlexApi import PlexApi from plextraktsync.trakt.TraktApi import TraktApi - from plextraktsync.trakt.types import TraktMedia class Sync: @@ -54,9 +51,6 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial, dry_run=dry_run) - if is_partial and self.config.clear_collected: - self.logger.warning("Running partial library sync. Clear collected will be disabled.") - if self.config.update_plex_wl_as_pl: if is_partial: self.logger.warning("Running partial library sync. Watchlist as playlist won't update because it needs full library sync.") @@ -70,37 +64,25 @@ def sync(self, walker: Walker, dry_run=False): trakt_lists.load_lists(self.trakt.liked_lists) if self.config.need_library_walk: - movie_trakt_ids = set() for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) self.sync_ratings(movie, dry_run=dry_run) self.sync_watched(movie, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(movie) - if self.config.clear_collected: - movie_trakt_ids.add(movie.trakt_id) - - if movie_trakt_ids: - self.clear_collected(self.trakt.movie_collection, movie_trakt_ids) shows = set() - episode_trakt_ids = set() for episode in walker.find_episodes(): pm.hook.walk_episode(episode=episode, dry_run=dry_run) self.sync_ratings(episode, dry_run=dry_run) self.sync_watched(episode, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(episode) - if self.config.clear_collected: - episode_trakt_ids.add(episode.trakt_id) if self.config.sync_ratings and episode.show: # collect shows for later ratings sync shows.add(episode.show) - if episode_trakt_ids: - self.clear_collected(self.trakt.episodes_collection, episode_trakt_ids) - for show in walker.walk_shows(shows, title="Syncing show ratings"): self.sync_ratings(show, dry_run=dry_run) @@ -238,16 +220,3 @@ def sync_watchlist(self, walker: Walker, dry_run=False): if len(self.trakt_wl): for m in walker.media_from_traktlist(self.trakt_wl): self.watchlist_sync_item(m, dry_run) - - def clear_collected(self, existing_items: Iterable[TraktMedia], keep_ids: set[int], dry_run=False): - from plextraktsync.trakt.trakt_set import trakt_set - - existing_ids = trakt_set(existing_items) - delete_ids = existing_ids - keep_ids - delete_items = (tm for tm in existing_items if tm.trakt in delete_ids) - - n = len(delete_ids) - for i, tm in enumerate(delete_items, start=1): - self.logger.info(f"Remove from Trakt collection ({i}/{n}): {tm}") - if not dry_run: - self.trakt.remove_from_collection(tm) From 666d62fe570734b90ecc7648aa2554c90a95e672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 16:11:52 +0300 Subject: [PATCH 18/24] Cleanup sync_ratings from Sync --- plextraktsync/sync/Sync.py | 55 -------------------------------------- 1 file changed, 55 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index f99b1f6d75..88fd88707e 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -66,26 +66,16 @@ def sync(self, walker: Walker, dry_run=False): if self.config.need_library_walk: for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) - self.sync_ratings(movie, dry_run=dry_run) self.sync_watched(movie, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(movie) - shows = set() for episode in walker.find_episodes(): pm.hook.walk_episode(episode=episode, dry_run=dry_run) - self.sync_ratings(episode, dry_run=dry_run) self.sync_watched(episode, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(episode) - if self.config.sync_ratings and episode.show: - # collect shows for later ratings sync - shows.add(episode.show) - - for show in walker.walk_shows(shows, title="Syncing show ratings"): - self.sync_ratings(show, dry_run=dry_run) - if self.config.update_plex_wl_as_pl or self.config.sync_liked_lists: if is_partial: self.logger.warning("Running partial library sync. Liked lists won't update because it needs full library sync.") @@ -100,51 +90,6 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.fini(walker=walker, trakt_lists=trakt_lists, dry_run=dry_run) - def sync_ratings(self, m: Media, dry_run=False): - if not self.config.sync_ratings: - return - - if m.plex_rating == m.trakt_rating: - return - - rating_priority = self.config["rating_priority"] - plex_to_trakt = self.config.plex_to_trakt["ratings"] - trakt_to_plex = self.config.trakt_to_plex["ratings"] - has_trakt = m.trakt_rating is not None - has_plex = m.plex_rating is not None - rate = None - - if rating_priority == "none": - # Only rate items with missing rating - if plex_to_trakt and has_plex and not has_trakt: - rate = "trakt" - elif trakt_to_plex and has_trakt and not has_plex: - rate = "plex" - - elif rating_priority == "trakt": - # If two-way rating sync, Trakt rating takes precedence over Plex rating - if trakt_to_plex and has_trakt: - rate = "plex" - elif plex_to_trakt and has_plex: - rate = "trakt" - - elif rating_priority == "plex": - # If two-way rating sync, Plex rating takes precedence over Trakt rating - if plex_to_trakt and has_plex: - rate = "trakt" - elif trakt_to_plex and has_trakt: - rate = "plex" - - if rate == "trakt": - self.logger.info(f"Rating {m.title_link} with {m.plex_rating} on Trakt (was {m.trakt_rating})", extra={"markup": True}) - if not dry_run: - m.trakt_rate() - - elif rate == "plex": - self.logger.info(f"Rating {m.title_link} with {m.trakt_rating} on Plex (was {m.plex_rating})", extra={"markup": True}) - if not dry_run: - m.plex_rate() - def sync_watched(self, m: Media, dry_run=False): if not self.config.sync_watched_status: return From e74bd8973d005ff111f2206bb417dd56a6af526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 16:23:08 +0300 Subject: [PATCH 19/24] Cleanup sync_watched from Sync --- plextraktsync/sync/Sync.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 88fd88707e..43b8ca1ab4 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -66,13 +66,11 @@ def sync(self, walker: Walker, dry_run=False): if self.config.need_library_walk: for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) - self.sync_watched(movie, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(movie) for episode in walker.find_episodes(): pm.hook.walk_episode(episode=episode, dry_run=dry_run) - self.sync_watched(episode, dry_run=dry_run) if not is_partial: trakt_lists.add_to_lists(episode) @@ -90,34 +88,6 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.fini(walker=walker, trakt_lists=trakt_lists, dry_run=dry_run) - def sync_watched(self, m: Media, dry_run=False): - if not self.config.sync_watched_status: - return - - if m.watched_on_plex is m.watched_on_trakt: - return - - if m.watched_on_plex: - if not self.config.plex_to_trakt["watched_status"]: - return - - if m.is_episode and m.watched_before_reset: - show = m.plex.item.show() - self.logger.info(f"Show '{show.title}' has been reset in trakt at {m.show_reset_at}.") - self.logger.info(f"Marking '{show.title}' as unwatched in Plex.") - if not dry_run: - m.reset_show() - else: - self.logger.info(f"Marking as watched in Trakt: {m.title_link}", extra={"markup": True}) - if not dry_run: - m.mark_watched_trakt() - elif m.watched_on_trakt: - if not self.config.trakt_to_plex["watched_status"]: - return - self.logger.info(f"Marking as watched in Plex: {m.title_link}", extra={"markup": True}) - if not dry_run: - m.mark_watched_plex() - def watchlist_sync_item(self, m: Media, dry_run=False): if m.plex is None: if self.config.update_plex_wl: From 1f1d47fe5d5de65fbf893a74ad8885558b418688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 16:25:07 +0300 Subject: [PATCH 20/24] Cleanup liked lists from Sync --- plextraktsync/sync/Sync.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 43b8ca1ab4..507e80e36b 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -57,12 +57,6 @@ def sync(self, walker: Walker, dry_run=False): else: trakt_lists.add_watchlist(self.trakt.watchlist_movies) - if self.config.sync_liked_lists: - if is_partial: - self.logger.warning("Partial walk, disabling liked lists updating. Liked lists won't update because it needs full library sync.") - else: - trakt_lists.load_lists(self.trakt.liked_lists) - if self.config.need_library_walk: for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) From 7c943ea6b881b01058871af5da1b65f9bd6f7d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 18:01:50 +0300 Subject: [PATCH 21/24] Add add_to_lists state flag --- plextraktsync/sync/Sync.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 507e80e36b..9ff71f2f19 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -44,6 +44,7 @@ def sync(self, walker: Walker, dry_run=False): self.walker = walker trakt_lists = TraktUserListCollection() is_partial = walker.is_partial and not dry_run + add_to_lists = not is_partial from plextraktsync.sync.plugin import SyncPluginManager pm = SyncPluginManager() @@ -52,7 +53,7 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial, dry_run=dry_run) if self.config.update_plex_wl_as_pl: - if is_partial: + if not add_to_lists: self.logger.warning("Running partial library sync. Watchlist as playlist won't update because it needs full library sync.") else: trakt_lists.add_watchlist(self.trakt.watchlist_movies) @@ -60,16 +61,16 @@ def sync(self, walker: Walker, dry_run=False): if self.config.need_library_walk: for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) - if not is_partial: + if add_to_lists: trakt_lists.add_to_lists(movie) for episode in walker.find_episodes(): pm.hook.walk_episode(episode=episode, dry_run=dry_run) - if not is_partial: + if add_to_lists: trakt_lists.add_to_lists(episode) if self.config.update_plex_wl_as_pl or self.config.sync_liked_lists: - if is_partial: + if not add_to_lists: self.logger.warning("Running partial library sync. Liked lists won't update because it needs full library sync.") else: if not dry_run: From 420163f5e78d948b965170f07cf3bd67406105d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 18:10:23 +0300 Subject: [PATCH 22/24] Add is_empty property to TraktUserListCollection --- plextraktsync/trakt/TraktUserListCollection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plextraktsync/trakt/TraktUserListCollection.py b/plextraktsync/trakt/TraktUserListCollection.py index dd7c2a2657..528521a2a0 100644 --- a/plextraktsync/trakt/TraktUserListCollection.py +++ b/plextraktsync/trakt/TraktUserListCollection.py @@ -14,6 +14,10 @@ class TraktUserListCollection(UserList): logger = logging.getLogger(__name__) + @property + def is_empty(self): + return not len(self) + def add_to_lists(self, m: Media): # Skip movie editions # https://support.plex.tv/articles/multiple-editions/#:~:text=Do%20Multiple%20Editions%20work%20with%20watch%20state%20syncing%3F From 5bf79d8ba2ac0d0963e3c64b9cb51b8e87deaf74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 18:10:46 +0300 Subject: [PATCH 23/24] Skip updating lists if it's empty --- plextraktsync/sync/Sync.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 9ff71f2f19..0a598506dd 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -44,7 +44,6 @@ def sync(self, walker: Walker, dry_run=False): self.walker = walker trakt_lists = TraktUserListCollection() is_partial = walker.is_partial and not dry_run - add_to_lists = not is_partial from plextraktsync.sync.plugin import SyncPluginManager pm = SyncPluginManager() @@ -53,11 +52,14 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial, dry_run=dry_run) if self.config.update_plex_wl_as_pl: - if not add_to_lists: + if is_partial: self.logger.warning("Running partial library sync. Watchlist as playlist won't update because it needs full library sync.") else: trakt_lists.add_watchlist(self.trakt.watchlist_movies) + # Skip updating lists if it's empty + add_to_lists = not trakt_lists.is_empty + if self.config.need_library_walk: for movie in walker.find_movies(): pm.hook.walk_movie(movie=movie, dry_run=dry_run) From 35dc35f3da68c8e25542de2cb202874570bafe2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 9 Apr 2024 18:31:14 +0300 Subject: [PATCH 24/24] Cleanup watchlist code from Sync --- plextraktsync/sync/Sync.py | 86 ++------------------------------------ 1 file changed, 3 insertions(+), 83 deletions(-) diff --git a/plextraktsync/sync/Sync.py b/plextraktsync/sync/Sync.py index 0a598506dd..20219c9f0c 100644 --- a/plextraktsync/sync/Sync.py +++ b/plextraktsync/sync/Sync.py @@ -1,6 +1,5 @@ from __future__ import annotations -from functools import cached_property from typing import TYPE_CHECKING from plextraktsync.decorators.measure_time import measure_time @@ -9,7 +8,6 @@ if TYPE_CHECKING: from plextraktsync.config.SyncConfig import SyncConfig - from plextraktsync.media.Media import Media from plextraktsync.plan.Walker import Walker from plextraktsync.plex.PlexApi import PlexApi from plextraktsync.trakt.TraktApi import TraktApi @@ -24,22 +22,6 @@ def __init__(self, config: SyncConfig, plex: PlexApi, trakt: TraktApi): self.trakt = trakt self.walker = None - @cached_property - def plex_wl(self): - from plextraktsync.plex.PlexWatchList import PlexWatchList - - return PlexWatchList(self.plex.watchlist()) - - @cached_property - def sync_wl(self): - return self.config.sync_wl and len(self.plex_wl) > 0 - - @cached_property - def trakt_wl(self): - from plextraktsync.trakt.TraktWatchlist import TraktWatchList - - return TraktWatchList(self.trakt.watchlist_movies + self.trakt.watchlist_shows) - def sync(self, walker: Walker, dry_run=False): self.walker = walker trakt_lists = TraktUserListCollection() @@ -51,12 +33,6 @@ def sync(self, walker: Walker, dry_run=False): pm.hook.init(sync=self, trakt_lists=trakt_lists, is_partial=is_partial, dry_run=dry_run) - if self.config.update_plex_wl_as_pl: - if is_partial: - self.logger.warning("Running partial library sync. Watchlist as playlist won't update because it needs full library sync.") - else: - trakt_lists.add_watchlist(self.trakt.watchlist_movies) - # Skip updating lists if it's empty add_to_lists = not trakt_lists.is_empty @@ -71,64 +47,8 @@ def sync(self, walker: Walker, dry_run=False): if add_to_lists: trakt_lists.add_to_lists(episode) - if self.config.update_plex_wl_as_pl or self.config.sync_liked_lists: - if not add_to_lists: - self.logger.warning("Running partial library sync. Liked lists won't update because it needs full library sync.") - else: - if not dry_run: - with measure_time("Updated liked list"): - trakt_lists.sync() - - if walker.config.walk_watchlist and self.sync_wl: - with measure_time("Updated watchlist"): - self.sync_watchlist(walker, dry_run=dry_run) + if not dry_run and not trakt_lists.is_empty: + with measure_time("Updated liked list"): + trakt_lists.sync() pm.hook.fini(walker=walker, trakt_lists=trakt_lists, dry_run=dry_run) - - def watchlist_sync_item(self, m: Media, dry_run=False): - if m.plex is None: - if self.config.update_plex_wl: - self.logger.info(f"Skipping {m.title_link} from Trakt watchlist because not found in Plex Discover", extra={"markup": True}) - elif self.config.update_trakt_wl: - self.logger.info(f"Removing {m.title_link} from Trakt watchlist", extra={"markup": True}) - if not dry_run: - m.remove_from_trakt_watchlist() - return - - if m in self.plex_wl: - if m not in self.trakt_wl: - if self.config.update_trakt_wl: - self.logger.info(f"Adding {m.title_link} to Trakt watchlist", extra={"markup": True}) - if not dry_run: - m.add_to_trakt_watchlist() - else: - self.logger.info(f"Removing {m.title_link} from Plex watchlist", extra={"markup": True}) - if not dry_run: - m.remove_from_plex_watchlist() - else: - # Plex Online search is inaccurate, and it doesn't offer search by id. - # Remove known match from trakt watchlist, so that the search would not be attempted. - # Example, trakt id 187634 where title mismatches: - # - "The Vortex": https://trakt.tv/movies/the-vortex-2012 - # - "Big Bad Bugs": https://app.plex.tv/desktop/#!/provider/tv.plex.provider.vod/details?key=%2Flibrary%2Fmetadata%2F5d776b1cad5437001f7936f4 - del self.trakt_wl[m] - elif m in self.trakt_wl: - if self.config.update_plex_wl: - self.logger.info(f"Adding {m.title_link} to Plex watchlist", extra={"markup": True}) - if not dry_run: - m.add_to_plex_watchlist() - else: - self.logger.info(f"Removing {m.title_link} from Trakt watchlist", extra={"markup": True}) - if not dry_run: - m.remove_from_trakt_watchlist() - - def sync_watchlist(self, walker: Walker, dry_run=False): - # NOTE: Plex watchlist sync removes matching items from trakt lists - # See the comment above around "del self.trakt_wl[m]" - for m in walker.media_from_plexlist(self.plex_wl): - self.watchlist_sync_item(m, dry_run) - - # Because Plex syncing might have emptied the watchlists, skip printing the 0/0 progress - if len(self.trakt_wl): - for m in walker.media_from_traktlist(self.trakt_wl): - self.watchlist_sync_item(m, dry_run)