Skip to content
Merged
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
99 changes: 54 additions & 45 deletions resources/lib/scrobbler.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,26 @@ def transitionCheck(self, isSeek: bool = False) -> None:
self.videosToRate.append(self.curVideoInfo)
# update current information
self.curMPEpisode = epIndex
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"episode",
kodiUtilities.getEpisodeDetailsFromKodi(
self.curVideo["multi_episode_data"][
self.curMPEpisode
],
[
"showtitle",
"season",
"episode",
"tvshowid",
"uniqueid",
"file",
"playcount",
],
),
episode_details = kodiUtilities.getEpisodeDetailsFromKodi(
self.curVideo["multi_episode_data"][
self.curMPEpisode
],
[
"showtitle",
"season",
"episode",
"tvshowid",
"uniqueid",
"file",
"playcount",
],
)
if episode_details:
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"episode", episode_details
)
else:
self.curVideoInfo = None
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

If episode_details is empty, self.curVideoInfo becomes None but the multi-episode transition continues. playbackEnded() later unconditionally appends self.curVideoInfo to videosToRate, which can introduce None entries and crash the rating flow (it expects dicts). Consider aborting scrobbling/rating for the remainder of this multi-part item when details are empty, or ensure videosToRate never contains None values.

Suggested change
self.curVideoInfo = None
logger.warning(
"Failed to get episode details for multi-part "
"episode index %s; keeping existing curVideoInfo.",
self.curMPEpisode,
)

Copilot uses AI. Check for mistakes.

logger.debug(
"Multi episode transition - call start for next episode"
Expand Down Expand Up @@ -257,21 +260,24 @@ def playbackStarted(self, data: Dict) -> None:
self.isMultiPartEpisode = False
if utilities.isMovie(self.curVideo["type"]):
if "id" in self.curVideo:
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"movie",
kodiUtilities.getMovieDetailsFromKodi(
self.curVideo["id"],
[
"uniqueid",
"imdbnumber",
"title",
"year",
"file",
"lastplayed",
"playcount",
],
),
movieDetailsKodi = kodiUtilities.getMovieDetailsFromKodi(
self.curVideo["id"],
[
"uniqueid",
"imdbnumber",
"title",
"year",
"file",
"lastplayed",
"playcount",
],
)
if movieDetailsKodi:
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"movie", movieDetailsKodi
)
else:
self.curVideoInfo = None
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

When movieDetailsKodi is empty, self.curVideoInfo is set to None but playback continues. Later in this method there are conditions like "ids" in self.curVideoInfo that will raise TypeError when self.curVideoInfo is None (e.g., when scrobbling is disabled but rating is enabled). Consider aborting the playbackStarted flow (similar to the episode invalid-ID path) or ensure subsequent checks are guarded with self.curVideoInfo being a dict before using membership tests.

Suggested change
self.curVideoInfo = None
# Ensure curVideoInfo remains a dict so membership tests like
# `"ids" in self.curVideoInfo` do not raise TypeError.
self.curVideoInfo = {}

Copilot uses AI. Check for mistakes.
elif "video_ids" in self.curVideo:
self.curVideoInfo = {"ids": self.curVideo["video_ids"]}
elif "title" in self.curVideo and "year" in self.curVideo:
Expand All @@ -297,19 +303,22 @@ def playbackStarted(self, data: Dict) -> None:
"playcount",
],
)
title, year = utilities.regex_year(episodeDetailsKodi["showtitle"])
if not year:
self.traktShowSummary = {
"title": episodeDetailsKodi["showtitle"],
"year": episodeDetailsKodi["year"],
}
if episodeDetailsKodi:
title, year = utilities.regex_year(episodeDetailsKodi["showtitle"])
if not year:
self.traktShowSummary = {
"title": episodeDetailsKodi["showtitle"],
"year": episodeDetailsKodi.get("year"),
}
else:
self.traktShowSummary = {"title": title, "year": year}
if "show_ids" in episodeDetailsKodi:
self.traktShowSummary["ids"] = episodeDetailsKodi["show_ids"]
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"episode", episodeDetailsKodi
)
else:
self.traktShowSummary = {"title": title, "year": year}
if "show_ids" in episodeDetailsKodi:
self.traktShowSummary["ids"] = episodeDetailsKodi["show_ids"]
self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject(
"episode", episodeDetailsKodi
)
self.curVideoInfo = None
if not self.curVideoInfo: # getEpisodeDetailsFromKodi was empty
logger.debug(
"Episode details from Kodi was empty, ID (%d) seems invalid, aborting further scrobbling of this episode."
Expand Down Expand Up @@ -399,10 +408,10 @@ def playbackStarted(self, data: Dict) -> None:
}
result["episode"]["season"] = self.curVideoInfo["season"]

if "id" in self.curVideo:
if utilities.isMovie(self.curVideo["type"]):
if result and "id" in self.curVideo:
if utilities.isMovie(self.curVideo["type"]) and "movie" in result:
result["movie"]["movieid"] = self.curVideo["id"]
elif utilities.isEpisode(self.curVideo["type"]):
elif utilities.isEpisode(self.curVideo["type"]) and "episode" in result:
result["episode"]["episodeid"] = self.curVideo["id"]
Comment on lines +411 to 415
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

result can be None here (e.g., self.__scrobble("start") returns None when self.curVideoInfo is missing), but it is passed to __preFetchUserRatings which is annotated as result: Dict. Consider normalizing result to {} (or updating the type hints to Optional[Dict]) to keep the types consistent and avoid future misuse of result as a dict in this block.

Copilot uses AI. Check for mistakes.

self.__preFetchUserRatings(result)
Expand Down
Loading