diff --git a/resources/language/resource.language.en_GB/strings.po b/resources/language/resource.language.en_GB/strings.po index df3d6b30..c8b44945 100644 --- a/resources/language/resource.language.en_GB/strings.po +++ b/resources/language/resource.language.en_GB/strings.po @@ -777,3 +777,11 @@ msgstr "" msgctxt "#42191" msgid "Optional password needed to authenticate with the proxy." msgstr "" + +msgctxt "#32192" +msgid "Remove watched status from Kodi when rewatching" +msgstr "" + +msgctxt "#32193" +msgid "Include specials" +msgstr "" diff --git a/resources/lib/kodiUtilities.py b/resources/lib/kodiUtilities.py index 5ee276e9..2feb20b2 100644 --- a/resources/lib/kodiUtilities.py +++ b/resources/lib/kodiUtilities.py @@ -186,9 +186,9 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") -> data["ids"] = utilities.guessBestTraktId(id, type)[0] if "lastplayed" in data: - episode["watched_at"] = utilities.convertDateTimeToUTC(data["lastplayed"]) + episode["watched_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data["lastplayed"])) if "dateadded" in data: - episode["collected_at"] = utilities.convertDateTimeToUTC(data["dateadded"]) + episode["collected_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data["dateadded"])) if "runtime" in data: episode["runtime"] = data["runtime"] episode["rating"] = ( @@ -205,9 +205,9 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") -> if checkExclusion(data.pop("file")): return if "lastplayed" in data: - data["watched_at"] = utilities.convertDateTimeToUTC(data.pop("lastplayed")) + data["watched_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data.pop("lastplayed"))) if "dateadded" in data: - data["collected_at"] = utilities.convertDateTimeToUTC(data.pop("dateadded")) + data["collected_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data.pop("dateadded"))) if data["playcount"] is None: data["plays"] = 0 else: diff --git a/resources/lib/syncEpisodes.py b/resources/lib/syncEpisodes.py index 860abcbe..b4989390 100644 --- a/resources/lib/syncEpisodes.py +++ b/resources/lib/syncEpisodes.py @@ -284,9 +284,11 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ ) # will keep the data in python structures - just like the KODI response - show = show.to_dict() - - showsWatched["shows"].append(show) + show_dict = show.to_dict() + # reset_at is not included when calling `.to_dict()` + # but needed for watched shows to know whether to reset the watched state + show_dict["reset_at"] = utilities.toIso8601DateTime(show.reset_at) if hasattr(show, "reset_at") else None + showsWatched["shows"].append(show_dict) i = 0 x = float(len(traktShowsRated)) @@ -583,6 +585,12 @@ def __addEpisodesToKodiWatched( updateKodiTraktShows = copy.deepcopy(traktShows) updateKodiKodiShows = copy.deepcopy(kodiShows) + if kodiUtilities.getSettingAsBool("kodi_episode_reset"): + utilities.updateTraktLastWatchedBasedOnResetAt( + updateKodiTraktShows, + kodiUtilities.getSettingAsBool("kodi_episode_reset_specials") + ) + kodiShowsUpdate = utilities.compareEpisodes( updateKodiTraktShows, updateKodiKodiShows, @@ -620,8 +628,8 @@ def __addEpisodesToKodiWatched( { "episodeid": episode["ids"]["episodeid"], "playcount": episode["plays"], - "lastplayed": utilities.convertUtcToDateTime( - episode["last_watched_at"] + "lastplayed": utilities.toDateTime( + utilities.fromIso8601DateTime(episode["last_watched_at"]) ), } ) diff --git a/resources/lib/syncMovies.py b/resources/lib/syncMovies.py index c0323a96..8da32f56 100644 --- a/resources/lib/syncMovies.py +++ b/resources/lib/syncMovies.py @@ -397,8 +397,8 @@ def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dic "params": { "movieid": kodiMoviesToUpdate[i]["movieid"], "playcount": kodiMoviesToUpdate[i]["plays"], - "lastplayed": utilities.convertUtcToDateTime( - kodiMoviesToUpdate[i]["last_watched_at"] + "lastplayed": utilities.toDateTime( + utilities.fromIso8601DateTime(kodiMoviesToUpdate[i]["last_watched_at"]) ), }, "id": i, diff --git a/resources/lib/utilities.py b/resources/lib/utilities.py index 72183480..6e8beaf0 100644 --- a/resources/lib/utilities.py +++ b/resources/lib/utilities.py @@ -10,6 +10,7 @@ import dateutil.parser from datetime import datetime from dateutil.tz import tzutc, tzlocal +import arrow # make strptime call prior to doing anything, to try and prevent threading # errors @@ -213,42 +214,49 @@ def findEpisodeMatchInList(id: str, seasonNumber: int, episodeNumber: int, list_ return {} -def convertDateTimeToUTC(toConvert: Optional[str]) -> Optional[str]: - if toConvert: - dateFormat = "%Y-%m-%d %H:%M:%S" - try: - naive = datetime.strptime(toConvert, dateFormat) - except TypeError: - naive = datetime(*(time.strptime(toConvert, dateFormat)[0:6])) - - try: - local = naive.replace(tzinfo=tzlocal()) - utc = local.astimezone(tzutc()) - except ValueError: - logger.debug( - "convertDateTimeToUTC() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime utcnow" - ) - utc = datetime.utcnow() - return str(utc) - else: - return toConvert - - -def convertUtcToDateTime(toConvert: Optional[str]) -> Optional[str]: - if toConvert: - dateFormat = "%Y-%m-%d %H:%M:%S" - try: - naive = dateutil.parser.parse(toConvert) - utc = naive.replace(tzinfo=tzutc()) - local = utc.astimezone(tzlocal()) - except ValueError: - logger.debug( - "convertUtcToDateTime() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime now" - ) - local = datetime.now() - return local.strftime(dateFormat) - else: - return toConvert +def toDateTime(value: Optional[datetime]) -> Optional[str]: + if not value: + return None + + return value.strftime('%Y-%m-%d %H:%M:%S') + + +def fromDateTime(value: Optional[str]) -> Optional[datetime]: + if not value: + return None + + if arrow is None: + raise Exception('"arrow" module is not available') + + # Parse datetime + dt = arrow.get(value, 'YYYY-MM-DD HH:mm:ss') + + # Return datetime object + return dt.datetime + + +def toIso8601DateTime(value: Optional[datetime]) -> Optional[str]: + if not value: + return None + + return value.strftime('%Y-%m-%dT%H:%M:%S') + '.000-00:00' + + +def fromIso8601DateTime(value: Optional[str]) -> Optional[datetime]: + if not value: + return None + + if arrow is None: + raise Exception('"arrow" module is not available') + + # Parse ISO8601 datetime + dt = arrow.get(value, 'YYYY-MM-DDTHH:mm:ss.SZZ') + + # Convert to UTC + dt = dt.to('UTC') + + # Return datetime object + return dt.datetime def createError(ex: Exception) -> str: @@ -484,6 +492,14 @@ def compareEpisodes( if season in season_col2: b = season_col2[season] diff = list(set(a).difference(set(b))) + # only for removing plays from kodi + if watched and restrict: + for key in a: + # update lastplayed in KODI if they don't match trakt + if not key in b or a[key]["plays"] != b[key]["plays"]: + diff.append(key) + # make unique + diff = list(set(diff)) if playback: t = list(set(a).intersection(set(b))) if len(t) > 0: @@ -700,3 +716,17 @@ def _fuzzyMatch(string1: str, string2: str, match_percent: float = 55.0) -> bool return ( difflib.SequenceMatcher(None, string1, string2).ratio() * 100 ) >= match_percent + + +def updateTraktLastWatchedBasedOnResetAt(trakt_shows, update_specials=False): + for show in trakt_shows["shows"]: + if show["reset_at"]: + reset_at = fromIso8601DateTime(show["reset_at"]) + for season in show["seasons"]: + if not update_specials and season["number"] == 0: + continue + for episode in season["episodes"]: + last_watched = fromIso8601DateTime(episode["last_watched_at"]) + if last_watched and last_watched < reset_at: + episode["last_watched_at"] = None + episode["plays"] = 0 diff --git a/resources/settings.xml b/resources/settings.xml index 60c10045..76c763f5 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -580,6 +580,23 @@ true 1 + + + false + 1 + + true + + + + + false + 1 + + true + true + + false