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