Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync watched status and ratings with Plex Discover #1676

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plextraktsync/config.default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion plextraktsync/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
21 changes: 15 additions & 6 deletions plextraktsync/plex/PlexApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
glensc marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down Expand Up @@ -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(
Expand Down
28 changes: 25 additions & 3 deletions plextraktsync/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can definitely rate movie without watching it:

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():
Expand Down
4 changes: 2 additions & 2 deletions plextraktsync/trakt/TraktApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions plextraktsync/trakt/TraktRatingCollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method does not yield. the whole method returns a dict. see @flatten_dict decorator

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](
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point of yield in the other method was if you used @flatten_dict. how does it work without it for you? not tested code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reporting this.
I need to dive in this code again but have no time for this at the moment.
It was tested successfuly on my library.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you put the generator to set():

set(self.trakt.ratings.items["movies"])

it's what the @flatten_set would do

title=title,
show=show,
season=season,
number=number,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

season and number could be undefined. you should probably use elseif rather another if and throw for unsupported index value.

ids=ids,
)
Loading