Skip to content

Commit 2a5a676

Browse files
committed
feat: rewatch support
1 parent a4fb7bf commit 2a5a676

File tree

6 files changed

+110
-47
lines changed

6 files changed

+110
-47
lines changed

resources/language/resource.language.en_GB/strings.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,3 +777,11 @@ msgstr ""
777777
msgctxt "#42191"
778778
msgid "Optional password needed to authenticate with the proxy."
779779
msgstr ""
780+
781+
msgctxt "#32192"
782+
msgid "Remove watched status from Kodi when rewatching"
783+
msgstr ""
784+
785+
msgctxt "#32193"
786+
msgid "Include specials"
787+
msgstr ""

resources/lib/kodiUtilities.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") ->
186186
data["ids"] = utilities.guessBestTraktId(id, type)[0]
187187

188188
if "lastplayed" in data:
189-
episode["watched_at"] = utilities.convertDateTimeToUTC(data["lastplayed"])
189+
episode["watched_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data["lastplayed"]))
190190
if "dateadded" in data:
191-
episode["collected_at"] = utilities.convertDateTimeToUTC(data["dateadded"])
191+
episode["collected_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data["dateadded"]))
192192
if "runtime" in data:
193193
episode["runtime"] = data["runtime"]
194194
episode["rating"] = (
@@ -205,9 +205,9 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") ->
205205
if checkExclusion(data.pop("file")):
206206
return
207207
if "lastplayed" in data:
208-
data["watched_at"] = utilities.convertDateTimeToUTC(data.pop("lastplayed"))
208+
data["watched_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data.pop("lastplayed")))
209209
if "dateadded" in data:
210-
data["collected_at"] = utilities.convertDateTimeToUTC(data.pop("dateadded"))
210+
data["collected_at"] = utilities.toIso8601DateTime(utilities.fromDateTime(data.pop("dateadded")))
211211
if data["playcount"] is None:
212212
data["plays"] = 0
213213
else:

resources/lib/syncEpisodes.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,11 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[
284284
)
285285

286286
# will keep the data in python structures - just like the KODI response
287-
show = show.to_dict()
288-
289-
showsWatched["shows"].append(show)
287+
show_dict = show.to_dict()
288+
# reset_at is not included when calling `.to_dict()`
289+
# but needed for watched shows to know whether to reset the watched state
290+
show_dict["reset_at"] = utilities.toIso8601DateTime(show.reset_at) if hasattr(show, "reset_at") else None
291+
showsWatched["shows"].append(show_dict)
290292

291293
i = 0
292294
x = float(len(traktShowsRated))
@@ -583,6 +585,12 @@ def __addEpisodesToKodiWatched(
583585
updateKodiTraktShows = copy.deepcopy(traktShows)
584586
updateKodiKodiShows = copy.deepcopy(kodiShows)
585587

588+
if kodiUtilities.getSettingAsBool("kodi_episode_reset"):
589+
utilities.updateTraktLastWatchedBasedOnResetAt(
590+
updateKodiTraktShows,
591+
kodiUtilities.getSettingAsBool("kodi_episode_reset_specials")
592+
)
593+
586594
kodiShowsUpdate = utilities.compareEpisodes(
587595
updateKodiTraktShows,
588596
updateKodiKodiShows,
@@ -620,8 +628,8 @@ def __addEpisodesToKodiWatched(
620628
{
621629
"episodeid": episode["ids"]["episodeid"],
622630
"playcount": episode["plays"],
623-
"lastplayed": utilities.convertUtcToDateTime(
624-
episode["last_watched_at"]
631+
"lastplayed": utilities.toDateTime(
632+
utilities.fromIso8601DateTime(episode["last_watched_at"])
625633
),
626634
}
627635
)

resources/lib/syncMovies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,8 @@ def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dic
397397
"params": {
398398
"movieid": kodiMoviesToUpdate[i]["movieid"],
399399
"playcount": kodiMoviesToUpdate[i]["plays"],
400-
"lastplayed": utilities.convertUtcToDateTime(
401-
kodiMoviesToUpdate[i]["last_watched_at"]
400+
"lastplayed": utilities.toDateTime(
401+
utilities.fromIso8601DateTime(kodiMoviesToUpdate[i]["last_watched_at"])
402402
),
403403
},
404404
"id": i,

resources/lib/utilities.py

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import dateutil.parser
1111
from datetime import datetime
1212
from dateutil.tz import tzutc, tzlocal
13+
import arrow
1314

1415
# make strptime call prior to doing anything, to try and prevent threading
1516
# errors
@@ -213,42 +214,49 @@ def findEpisodeMatchInList(id: str, seasonNumber: int, episodeNumber: int, list_
213214
return {}
214215

215216

216-
def convertDateTimeToUTC(toConvert: Optional[str]) -> Optional[str]:
217-
if toConvert:
218-
dateFormat = "%Y-%m-%d %H:%M:%S"
219-
try:
220-
naive = datetime.strptime(toConvert, dateFormat)
221-
except TypeError:
222-
naive = datetime(*(time.strptime(toConvert, dateFormat)[0:6]))
223-
224-
try:
225-
local = naive.replace(tzinfo=tzlocal())
226-
utc = local.astimezone(tzutc())
227-
except ValueError:
228-
logger.debug(
229-
"convertDateTimeToUTC() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime utcnow"
230-
)
231-
utc = datetime.utcnow()
232-
return str(utc)
233-
else:
234-
return toConvert
235-
236-
237-
def convertUtcToDateTime(toConvert: Optional[str]) -> Optional[str]:
238-
if toConvert:
239-
dateFormat = "%Y-%m-%d %H:%M:%S"
240-
try:
241-
naive = dateutil.parser.parse(toConvert)
242-
utc = naive.replace(tzinfo=tzutc())
243-
local = utc.astimezone(tzlocal())
244-
except ValueError:
245-
logger.debug(
246-
"convertUtcToDateTime() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime now"
247-
)
248-
local = datetime.now()
249-
return local.strftime(dateFormat)
250-
else:
251-
return toConvert
217+
def toDateTime(value: Optional[datetime]) -> Optional[str]:
218+
if not value:
219+
return None
220+
221+
return value.strftime('%Y-%m-%d %H:%M:%S')
222+
223+
224+
def fromDateTime(value: Optional[str]) -> Optional[datetime]:
225+
if not value:
226+
return None
227+
228+
if arrow is None:
229+
raise Exception('"arrow" module is not available')
230+
231+
# Parse datetime
232+
dt = arrow.get(value, 'YYYY-MM-DD HH:mm:ss')
233+
234+
# Return datetime object
235+
return dt.datetime
236+
237+
238+
def toIso8601DateTime(value: Optional[datetime]) -> Optional[str]:
239+
if not value:
240+
return None
241+
242+
return value.strftime('%Y-%m-%dT%H:%M:%S') + '.000-00:00'
243+
244+
245+
def fromIso8601DateTime(value: Optional[str]) -> Optional[datetime]:
246+
if not value:
247+
return None
248+
249+
if arrow is None:
250+
raise Exception('"arrow" module is not available')
251+
252+
# Parse ISO8601 datetime
253+
dt = arrow.get(value, 'YYYY-MM-DDTHH:mm:ss.SZZ')
254+
255+
# Convert to UTC
256+
dt = dt.to('UTC')
257+
258+
# Return datetime object
259+
return dt.datetime
252260

253261

254262
def createError(ex: Exception) -> str:
@@ -484,6 +492,14 @@ def compareEpisodes(
484492
if season in season_col2:
485493
b = season_col2[season]
486494
diff = list(set(a).difference(set(b)))
495+
# only for removing plays from kodi
496+
if watched and restrict:
497+
for key in a:
498+
# update lastplayed in KODI if they don't match trakt
499+
if not key in b or a[key]["plays"] != b[key]["plays"]:
500+
diff.append(key)
501+
# make unique
502+
diff = list(set(diff))
487503
if playback:
488504
t = list(set(a).intersection(set(b)))
489505
if len(t) > 0:
@@ -700,3 +716,17 @@ def _fuzzyMatch(string1: str, string2: str, match_percent: float = 55.0) -> bool
700716
return (
701717
difflib.SequenceMatcher(None, string1, string2).ratio() * 100
702718
) >= match_percent
719+
720+
721+
def updateTraktLastWatchedBasedOnResetAt(trakt_shows, update_specials=False):
722+
for show in trakt_shows["shows"]:
723+
if show["reset_at"]:
724+
reset_at = fromIso8601DateTime(show["reset_at"])
725+
for season in show["seasons"]:
726+
if not update_specials and season["number"] == 0:
727+
continue
728+
for episode in season["episodes"]:
729+
last_watched = fromIso8601DateTime(episode["last_watched_at"])
730+
if last_watched and last_watched < reset_at:
731+
episode["last_watched_at"] = None
732+
episode["plays"] = 0

resources/settings.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,23 @@
580580
<default>true</default>
581581
<level>1</level>
582582
</setting>
583+
<setting id="kodi_episode_reset" type="boolean" label="32192" help="" parent="kodi_episode_playcount">
584+
<control type="toggle" />
585+
<default>false</default>
586+
<level>1</level>
587+
<dependencies>
588+
<dependency type="visible" setting="kodi_episode_playcount">true</dependency>
589+
</dependencies>
590+
</setting>
591+
<setting id="kodi_episode_reset_specials" type="boolean" label="32193" help="" parent="kodi_episode_reset">
592+
<control type="toggle" />
593+
<default>false</default>
594+
<level>1</level>
595+
<dependencies>
596+
<dependency type="visible" setting="kodi_episode_playcount">true</dependency>
597+
<dependency type="enable" setting="kodi_episode_reset">true</dependency>
598+
</dependencies>
599+
</setting>
583600
<setting id="trakt_episode_playback" type="boolean" label="32118" help="">
584601
<control type="toggle" />
585602
<default>false</default>

0 commit comments

Comments
 (0)