diff --git a/plextraktsync/config.default.yml b/plextraktsync/config.default.yml index 6e7f59a858..a453fddeeb 100644 --- a/plextraktsync/config.default.yml +++ b/plextraktsync/config.default.yml @@ -70,6 +70,9 @@ sync: # trakt - Trakt ratings have priority. Existing Plex ratings are overwritten. # plex - Plex ratings have priority. Existing Trakt ratings are overwritten. rating_priority: plex + # watched_status and ratings of media not actually in your Plex server can + # be synced with your Plex online account + plex_online: false # settings for 'watch' command watch: diff --git a/plextraktsync/media.py b/plextraktsync/media.py index 27e4245aab..5314d2ba7f 100644 --- a/plextraktsync/media.py +++ b/plextraktsync/media.py @@ -203,7 +203,7 @@ def mark_watched_trakt(self): ) def mark_watched_plex(self): - self.plex_api.mark_watched(self.plex.item) + self.plex_api.mark_watched(self.plex.item, self.plex.is_discover) @property def trakt_rating(self): @@ -284,6 +284,9 @@ def resolve_trakt(self, tm: TraktItem) -> Media: """Find Plex media from Trakt id using Plex Search and Discover""" result = self.plex.search_online(tm.item.title, tm.type) pm = self._guid_match(result, tm) + if pm is None: + logger.warning(f"Skipping '{tm.item.title}': not found on Plex Discover") + return None return self.make_media(pm, tm.item) def make_media(self, plex: PlexLibraryItem, trakt): diff --git a/plextraktsync/plex/PlexApi.py b/plextraktsync/plex/PlexApi.py index c6470eebd1..94daf07d91 100644 --- a/plextraktsync/plex/PlexApi.py +++ b/plextraktsync/plex/PlexApi.py @@ -200,12 +200,20 @@ def history(self, m, device=False, account=False): yield h @retry() - def mark_watched(self, m): - m.markPlayed() + def mark_watched(self, m, is_discover=False): + if is_discover: + acc = self.account + acc.markPlayed(m) + else: + m.markPlayed() @retry() - def mark_unwatched(self, m): - m.markUnplayed() + def mark_unwatched(self, m, is_discover=False): + if is_discover: + acc = self.account + acc.markUnplayed(m) + else: + m.markUnplayed() def has_sessions(self): try: @@ -285,9 +293,10 @@ def search_online(self, title: str, media_type: str): def reset_show(self, show: Show, reset_date: datetime): reset_count = 0 for ep in show.watched(): - ep_seen_date = PlexLibraryItem(ep).seen_date.replace(tzinfo=None) + item = PlexLibraryItem(ep) + ep_seen_date = item.seen_date.replace(tzinfo=None) if ep_seen_date < reset_date: - self.mark_unwatched(ep) + self.mark_unwatched(ep, item.is_discover) reset_count += 1 else: logger.debug( diff --git a/plextraktsync/sync.py b/plextraktsync/sync.py index f209471503..e66d9865c0 100644 --- a/plextraktsync/sync.py +++ b/plextraktsync/sync.py @@ -68,12 +68,34 @@ def sync(self, walker: Walker, dry_run=False): self.sync_watched(movie, dry_run=dry_run) if not is_partial: listutil.addPlexItemToLists(movie) - if self.config.clear_collected: - movie_trakt_ids.add(movie.trakt_id) + movie_trakt_ids.add(movie.trakt_id) - if movie_trakt_ids: + if self.config.clear_collected and movie_trakt_ids: self.clear_collected(self.trakt.movie_collection, movie_trakt_ids) + remaining_movies_ids = ( + set(self.trakt.watched_movies.keys()).union( + set(self.trakt.ratings["movies"]) + ) - movie_trakt_ids + ) + + if remaining_movies_ids and not is_partial and self.config["plex_online"]: + """Sync ratings and watched status of movies not in Plex library""" + items = set(self.trakt.watched_movies.values()).union( + set(self.trakt.ratings.items["movies"]) + ) + # items is a set() of trakt.movies.Movies already watched or rated (can a user rate without watch?) + sync_items = [] + for tm in items: + if tm.trakt in remaining_movies_ids: + sync_items.append(tm) + remaining_movies_ids.remove(tm.trakt) + for movie in walker.media_from_traktlist(sync_items, title="Trakt watched movies"): + if movie is not None: + self.sync_watched(movie, dry_run=dry_run) + # Rating medias from Plex Discover not implemented yet https://github.com/pkkid/python-plexapi/issues/1137 + # self.sync_ratings(movie, dry_run=dry_run) + shows = set() episode_trakt_ids = set() for episode in walker.find_episodes(): diff --git a/plextraktsync/trakt/TraktApi.py b/plextraktsync/trakt/TraktApi.py index 9d41526c18..1b4e32bd20 100644 --- a/plextraktsync/trakt/TraktApi.py +++ b/plextraktsync/trakt/TraktApi.py @@ -69,7 +69,7 @@ def liked_lists(self): @rate_limit() @retry() def watched_movies(self): - return set(map(lambda m: m.trakt, self.me.watched_movies)) + return {m.trakt: m for m in self.me.watched_movies} @cached_property @rate_limit() @@ -162,7 +162,7 @@ def rate(self, m, rating): @retry() def mark_watched(self, m: TraktMedia, time, show_trakt_id=None): if m.media_type == "movies": - self.watched_movies.add(m.trakt) + self.watched_movies[m.trakt] = m elif m.media_type == "episodes" and show_trakt_id: self.watched_shows.add(show_trakt_id, m.season, m.number) else: diff --git a/plextraktsync/trakt/TraktRatingCollection.py b/plextraktsync/trakt/TraktRatingCollection.py index 3547f32f57..b384584174 100644 --- a/plextraktsync/trakt/TraktRatingCollection.py +++ b/plextraktsync/trakt/TraktRatingCollection.py @@ -4,23 +4,60 @@ from plextraktsync.decorators.flatten import flatten_dict +from trakt.movies import Movie +from trakt.tv import TVEpisode, TVSeason, TVShow + if TYPE_CHECKING: from plextraktsync.trakt.TraktApi import TraktApi +trakt_types = { + "movie": Movie, + "show": TVShow, + "season": TVSeason, + "episode": TVEpisode, +} + class TraktRatingCollection(dict): def __init__(self, trakt: TraktApi): super().__init__() self.trakt = trakt + self.items = dict() def __missing__(self, media_type: str): ratings = self.ratings(media_type) self[media_type] = ratings + self.items[media_type] = self.rating_items(media_type) return ratings @flatten_dict def ratings(self, media_type: str): + """Yield trakt id and rating of all rated media_type""" index = media_type.rstrip("s") for r in self.trakt.get_ratings(media_type): yield r[index]["ids"]["trakt"], r["rating"] + + def rating_items(self, media_type: str): + """Yield TraktMedia of all rated media_type""" + index = media_type.rstrip("s") + for r in self.trakt.get_ratings(media_type): + title = r[index].get("title") + if index == "movie": + show = season = number = None + else: + show = title = r["show"]["title"] + if index == "episode": + season = r[index]["season"] + number = r[index]["number"] + if index == "season": + season = r[index]["number"] + number = None + ids = r[index]["ids"] + yield trakt_types[index]( + title=title, + show=show, + season=season, + number=number, + ids=ids, + )