From ee0597b01507660374b4a552f43812429bde1c3e Mon Sep 17 00:00:00 2001 From: razze Date: Fri, 23 Oct 2026 12:41:47 +0200 Subject: [PATCH] Fix Ruff findings --- default.py | 24 +- defaultscript.py | 7 +- resources/lib/deviceAuthDialog.py | 17 +- resources/lib/globals.py | 2 - resources/lib/kodiUtilities.py | 102 ++-- resources/lib/kodilogging.py | 16 +- resources/lib/obfuscation.py | 9 +- resources/lib/rating.py | 225 +++++--- resources/lib/script.py | 508 +++++++++++------- resources/lib/scrobbler.py | 145 +++-- resources/lib/service.py | 264 ++++----- resources/lib/sqlitequeue.py | 63 +-- resources/lib/sync.py | 88 +-- resources/lib/syncEpisodes.py | 235 ++++---- resources/lib/syncMovies.py | 152 +++--- resources/lib/traktContextMenu.py | 31 +- resources/lib/traktapi.py | 191 ++++--- resources/lib/utilities.py | 214 ++++---- ruff.toml | 2 +- scripts/inject_keys.py | 2 + ...odiUtilities.py => test_kodi_utilities.py} | 7 +- tests/test_obfuscation.py | 6 +- tests/test_traktapi.py | 21 +- tests/test_utilities.py | 4 +- 24 files changed, 1251 insertions(+), 1084 deletions(-) rename tests/{test_kodiUtilities.py => test_kodi_utilities.py} (85%) diff --git a/default.py b/default.py index f95c0898..2ff1132a 100644 --- a/default.py +++ b/default.py @@ -1,26 +1,26 @@ -# -*- coding: utf-8 -*- -# import logging + import xbmcaddon + from resources.lib import kodilogging +from resources.lib.kodiUtilities import getSetting, setSetting from resources.lib.service import traktService -from resources.lib.utilities import createError, checkIfNewVersion -from resources.lib.kodiUtilities import setSetting, getSetting +from resources.lib.utilities import checkIfNewVersion, createError -__addon__ = xbmcaddon.Addon('script.trakt') -__addonversion__ = __addon__.getAddonInfo('version') -__addonid__ = __addon__.getAddonInfo('id') +__addon__ = xbmcaddon.Addon("script.trakt") +__addonversion__ = __addon__.getAddonInfo("version") +__addonid__ = __addon__.getAddonInfo("id") kodilogging.config() logger = logging.getLogger(__name__) -logger.debug("Loading '%s' version '%s'" % (__addonid__, __addonversion__)) -if checkIfNewVersion(str(getSetting('version')), str(__addonversion__)): - setSetting('version', __addonversion__) +logger.debug(f"Loading '{__addonid__}' version '{__addonversion__}'") +if checkIfNewVersion(str(getSetting("version")), str(__addonversion__)): + setSetting("version", __addonversion__) try: traktService().run() -except Exception as ex: +except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = createError(ex) logger.fatal(message) -logger.debug("'%s' shutting down." % __addonid__) +logger.debug(f"'{__addonid__}' shutting down.") diff --git a/defaultscript.py b/defaultscript.py index a6fd7cee..0edf6d30 100644 --- a/defaultscript.py +++ b/defaultscript.py @@ -1,14 +1,17 @@ -# -*- coding: utf-8 -*- import logging + import xbmcaddon + from resources.lib import script logger = logging.getLogger(__name__) __addon__ = xbmcaddon.Addon("script.trakt") + def Main(): script.run() -if __name__ == '__main__': + +if __name__ == "__main__": Main() diff --git a/resources/lib/deviceAuthDialog.py b/resources/lib/deviceAuthDialog.py index 91f61398..09612b40 100644 --- a/resources/lib/deviceAuthDialog.py +++ b/resources/lib/deviceAuthDialog.py @@ -1,8 +1,10 @@ -import xbmcgui +import logging import time + import xbmcaddon -from resources.lib.kodiUtilities import notification, setSetting, getString -import logging +import xbmcgui + +from resources.lib.kodiUtilities import getString, notification, setSetting __addon__ = xbmcaddon.Addon("script.trakt") @@ -32,7 +34,8 @@ def onInit(self) -> None: authcode = self.getControl(AUTHCODE_LABEL) warning = self.getControl(WARNING_LABEL) instuction.setLabel( - getString(32159).format("[COLOR red]" + self.url + "[/COLOR]")) + getString(32159).format("[COLOR red]" + self.url + "[/COLOR]") + ) authcode.setLabel(self.code) warning.setLabel(getString(32162)) @@ -47,15 +50,15 @@ def onFocus(self, control: xbmcgui.Control) -> None: pass def onClick(self, control: xbmcgui.Control) -> None: - logger.debug('onClick: %s' % (control)) + logger.debug(f"onClick: {control}") if control == LATER_BUTTON: notification(getString(32157), getString(32150), 5000) - setSetting('last_reminder', str(int(time.time()))) + setSetting("last_reminder", str(int(time.time()))) if control == NEVER_BUTTON: notification(getString(32157), getString(32151), 5000) - setSetting('last_reminder', '-1') + setSetting("last_reminder", "-1") if control in [LATER_BUTTON, NEVER_BUTTON]: self.close() diff --git a/resources/lib/globals.py b/resources/lib/globals.py index 3f38c44f..24e65fb9 100644 --- a/resources/lib/globals.py +++ b/resources/lib/globals.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# from typing import Any traktapi: Any = None diff --git a/resources/lib/kodiUtilities.py b/resources/lib/kodiUtilities.py index 5ee276e9..abbab17e 100644 --- a/resources/lib/kodiUtilities.py +++ b/resources/lib/kodiUtilities.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- -# +import json +import logging +import re import xbmc -import xbmcgui import xbmcaddon -import json -import re -import logging -from typing import Tuple, List, Dict, Union, Optional -from resources.lib import utilities +import xbmcgui +from resources.lib import utilities # read settings __addon__ = xbmcaddon.Addon("script.trakt") @@ -20,7 +17,10 @@ def notification( - header: str, message: str, time: int = 5000, icon: str = __addon__.getAddonInfo("icon") + header: str, + message: str, + time: int = 5000, + icon: str = __addon__.getAddonInfo("icon"), ) -> None: xbmcgui.Dialog().notification(header, message, icon, time) @@ -33,7 +33,7 @@ def getSetting(setting: str) -> str: return __addon__.getSetting(setting).strip() -def setSetting(setting: str, value: Union[str, int, float, bool]) -> None: +def setSetting(setting: str, value: str | float | bool) -> None: __addon__.setSetting(setting, str(value)) @@ -59,7 +59,7 @@ def getString(string_id: int) -> str: return __addon__.getLocalizedString(string_id) -def kodiJsonRequest(params: Dict) -> Optional[Union[Dict, List]]: +def kodiJsonRequest(params: dict) -> dict | list | None: data = json.dumps(params) request = xbmc.executeJSONRPC(data) @@ -70,7 +70,7 @@ def kodiJsonRequest(params: Dict) -> Optional[Union[Dict, List]]: return response["result"] return None except KeyError: - logger.warn("[%s] %s" % (params["method"], response["error"]["message"])) + logger.warning("[{}] {}".format(params["method"], response["error"]["message"])) return None @@ -113,18 +113,21 @@ def checkExclusion(fullpath: str) -> bool: # Path exclusions ExcludePath = getSetting("ExcludePath") - if ExcludePath != "" and getSettingAsBool("ExcludePathOption"): - if fullpath.startswith(ExcludePath): - logger.debug( - "checkExclusion(): Video is from location, which is currently set as excluded path 1." - ) - return True + if ( + ExcludePath != "" + and getSettingAsBool("ExcludePathOption") + and fullpath.startswith(ExcludePath) + ): + logger.debug( + "checkExclusion(): Video is from location, which is currently set as excluded path 1." + ) + return True found = False for x in range(2, 13): found |= utilities.checkExcludePath( - getSetting("ExcludePath%i" % x), - getSettingAsBool("ExcludePathOption%i" % x), + getSetting(f"ExcludePath{x:d}"), + getSettingAsBool(f"ExcludePathOption{x:d}"), fullpath, x, ) @@ -132,7 +135,9 @@ def checkExclusion(fullpath: str) -> bool: return found -def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") -> Optional[Dict]: +def kodiRpcToTraktMediaObject( + type: str, data: dict, mode: str = "collected" +) -> dict | None: if type == "show": if "uniqueid" in data: data["ids"] = data.pop("uniqueid") @@ -194,9 +199,12 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") -> episode["rating"] = ( data["userrating"] if "userrating" in data and data["userrating"] > 0 else 0 ) - if mode == "watched" and episode["watched"]: - return episode - elif mode == "collected" and episode["collected"]: + if ( + mode == "watched" + and episode["watched"] + or mode == "collected" + and episode["collected"] + ): return episode else: return @@ -231,7 +239,7 @@ def kodiRpcToTraktMediaObject(type: str, data: Dict, mode: str = "collected") -> return -def kodiRpcToTraktMediaObjects(data: Dict, mode: str = "collected") -> Optional[List]: +def kodiRpcToTraktMediaObjects(data: dict, mode: str = "collected") -> list | None: if "tvshows" in data: shows = data["tvshows"] @@ -252,8 +260,8 @@ def kodiRpcToTraktMediaObjects(data: Dict, mode: str = "collected") -> Optional[ if episodeObject: a_episodes[s_no].append(episodeObject) - for episode in a_episodes: - seasons.append({"number": episode, "episodes": a_episodes[episode]}) + for episode, episodes in a_episodes.items(): + seasons.append({"number": episode, "episodes": episodes}) return seasons elif "movies" in data: @@ -271,7 +279,7 @@ def kodiRpcToTraktMediaObjects(data: Dict, mode: str = "collected") -> Optional[ return -def getShowDetailsFromKodi(showID: int, fields: List) -> Optional[Dict]: +def getShowDetailsFromKodi(showID: int, fields: list) -> dict | None: result = kodiJsonRequest( { "jsonrpc": "2.0", @@ -280,7 +288,7 @@ def getShowDetailsFromKodi(showID: int, fields: List) -> Optional[Dict]: "id": 1, } ) - logger.debug("getShowDetailsFromKodi(): %s" % str(result)) + logger.debug(f"getShowDetailsFromKodi(): {result!s}") if not result: logger.debug("getShowDetailsFromKodi(): Result from Kodi was empty.") @@ -293,7 +301,7 @@ def getShowDetailsFromKodi(showID: int, fields: List) -> Optional[Dict]: return None -def getSeasonDetailsFromKodi(seasonID: int, fields: List) -> Optional[Dict]: +def getSeasonDetailsFromKodi(seasonID: int, fields: list) -> dict | None: result = kodiJsonRequest( { "jsonrpc": "2.0", @@ -302,7 +310,7 @@ def getSeasonDetailsFromKodi(seasonID: int, fields: List) -> Optional[Dict]: "id": 1, } ) - logger.debug("getSeasonDetailsFromKodi(): %s" % str(result)) + logger.debug(f"getSeasonDetailsFromKodi(): {result!s}") if not result: logger.debug("getSeasonDetailsFromKodi(): Result from Kodi was empty.") @@ -318,7 +326,7 @@ def getSeasonDetailsFromKodi(seasonID: int, fields: List) -> Optional[Dict]: # get a single episode from kodi given the id -def getEpisodeDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: +def getEpisodeDetailsFromKodi(libraryId: int, fields: list) -> dict | None: result = kodiJsonRequest( { "jsonrpc": "2.0", @@ -327,7 +335,7 @@ def getEpisodeDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: "id": 1, } ) - logger.debug("getEpisodeDetailsFromKodi(): %s" % str(result)) + logger.debug(f"getEpisodeDetailsFromKodi(): {result!s}") if not result: logger.debug("getEpisodeDetailsFromKodi(): Result from Kodi was empty.") @@ -359,7 +367,7 @@ def getEpisodeDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: # get a single movie from kodi given the id -def getMovieDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: +def getMovieDetailsFromKodi(libraryId: int, fields: list) -> dict | None: result = kodiJsonRequest( { "jsonrpc": "2.0", @@ -368,7 +376,7 @@ def getMovieDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: "id": 1, } ) - logger.debug("getMovieDetailsFromKodi(): %s" % str(result)) + logger.debug(f"getMovieDetailsFromKodi(): {result!s}") if not result: logger.debug("getMovieDetailsFromKodi(): Result from Kodi was empty.") @@ -381,7 +389,7 @@ def getMovieDetailsFromKodi(libraryId: int, fields: List) -> Optional[Dict]: return None -def checkAndConfigureProxy() -> Optional[str]: +def checkAndConfigureProxy() -> str | None: proxyActive = kodiJsonRequest( { "jsonrpc": "2.0", @@ -481,10 +489,10 @@ def checkAndConfigureProxy() -> Optional[str]: return None -def getMediaType() -> Optional[str]: +def getMediaType() -> str | None: listType = xbmc.getInfoLabel("ListItem.DBTYPE") - xbmc.log("list item type: %s" % listType, xbmc.LOGINFO) + xbmc.log(f"list item type: {listType}", xbmc.LOGINFO) if listType == "movie": return "movie" @@ -498,7 +506,7 @@ def getMediaType() -> Optional[str]: return None -def getInfoLabelDetails(result: Dict) -> Tuple[str, Dict]: +def getInfoLabelDetails(result: dict) -> tuple[str, dict]: type = result["item"]["type"] data = {"action": "started"} # check type of item @@ -520,8 +528,7 @@ def getInfoLabelDetails(result: Dict) -> Tuple[str, Dict]: if video_ids: data["video_ids"] = json.loads(video_ids) logger.debug( - "getInfoLabelDetails info - ids: %s, showtitle: %s, Year: %s, Season: %s, Episode: %s" - % (video_ids, showtitle, year, season, episode) + f"getInfoLabelDetails info - ids: {video_ids}, showtitle: {showtitle}, Year: {year}, Season: {season}, Episode: {episode}" ) if season >= 0 and episode > 0 and (showtitle or video_ids): @@ -535,8 +542,9 @@ def getInfoLabelDetails(result: Dict) -> Tuple[str, Dict]: if year.isdigit(): data["year"] = int(year) logger.debug( - "getInfoLabelDetails - Playing a non-library 'episode' - %s - S%02dE%02d - %s." - % (data["showtitle"], data["season"], data["episode"], data["title"]) + "getInfoLabelDetails - Playing a non-library 'episode' - {:s} - S{:02d}E{:02d} - {:s}.".format( + data["showtitle"], data["season"], data["episode"], data["title"] + ) ) elif (year or video_ids) and season < 0 and not title: # we have a year or video_id and no season/showtitle info, enough for a movie @@ -546,8 +554,9 @@ def getInfoLabelDetails(result: Dict) -> Tuple[str, Dict]: data["year"] = int(year) data["title"] = utilities.regex_year(showtitle)[0] or showtitle logger.debug( - "getInfoLabelDetails - Playing a non-library 'movie' - %s (%s)." - % (data["title"], data.get("year", "NaN")) + "getInfoLabelDetails - Playing a non-library 'movie' - {} ({}).".format( + data["title"], data.get("year", "NaN") + ) ) elif showtitle or title: title, season, episode = utilities.regex_tvshow(title) @@ -558,8 +567,7 @@ def getInfoLabelDetails(result: Dict) -> Tuple[str, Dict]: data["episode"] = episode data["title"] = data["showtitle"] = title or showtitle logger.debug( - "getInfoLabelDetails - Title: %s, showtitle: %s, season: %d, episode: %d" - % (title, showtitle, season, episode) + f"getInfoLabelDetails - Title: {title:s}, showtitle: {showtitle:s}, season: {season:d}, episode: {episode:d}" ) else: logger.debug( diff --git a/resources/lib/kodilogging.py b/resources/lib/kodilogging.py index 186d0066..f6bbaaab 100644 --- a/resources/lib/kodilogging.py +++ b/resources/lib/kodilogging.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2015 Thomas Amland # @@ -16,20 +15,20 @@ # along with this program. If not, see . -from resources.lib.kodiUtilities import getSettingAsBool - import logging + import xbmc import xbmcaddon +from resources.lib.kodiUtilities import getSettingAsBool -class KodiLogHandler(logging.StreamHandler): +class KodiLogHandler(logging.StreamHandler): def __init__(self) -> None: logging.StreamHandler.__init__(self) - addon_id = xbmcaddon.Addon().getAddonInfo('id') - prefix = "[%s] " % addon_id - formatter = logging.Formatter(prefix + '%(name)s: %(message)s') + addon_id = xbmcaddon.Addon().getAddonInfo("id") + prefix = f"[{addon_id}] " + formatter = logging.Formatter(prefix + "%(name)s: %(message)s") self.setFormatter(formatter) def emit(self, record: logging.LogRecord) -> None: @@ -41,12 +40,13 @@ def emit(self, record: logging.LogRecord) -> None: logging.DEBUG: xbmc.LOGDEBUG, logging.NOTSET: xbmc.LOGNONE, } - if getSettingAsBool('debug'): + if getSettingAsBool("debug"): xbmc.log(self.format(record), levels[record.levelno]) def flush(self) -> None: pass + def config() -> None: logger = logging.getLogger() logger.addHandler(KodiLogHandler()) diff --git a/resources/lib/obfuscation.py b/resources/lib/obfuscation.py index dd8b2129..d8f30d8e 100644 --- a/resources/lib/obfuscation.py +++ b/resources/lib/obfuscation.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- -from typing import List, Union - - -def deobfuscate(data: Union[List[int], str]) -> str: +def deobfuscate(data: list[int] | str) -> str: if not data or not isinstance(data, list): return "" return "".join(chr(b ^ 0x42) for b in data) -def obfuscate(data: str) -> List[int]: + +def obfuscate(data: str) -> list[int]: if not data: return [] return [ord(c) ^ 0x42 for c in data] diff --git a/resources/lib/rating.py b/resources/lib/rating.py index db7e749d..e8ce0cbf 100644 --- a/resources/lib/rating.py +++ b/resources/lib/rating.py @@ -1,24 +1,25 @@ -# -*- coding: utf-8 -*- """Module used to launch rating dialogues and send ratings to Trakt""" +import logging +from typing import Any, ClassVar + import xbmcaddon import xbmcgui -from resources.lib import utilities -from resources.lib import kodiUtilities -from typing import Dict, List, Optional, Any, Union -from resources.lib import globals -import logging + +from resources.lib import globals, kodiUtilities, utilities logger = logging.getLogger(__name__) __addon__ = xbmcaddon.Addon("script.trakt") -def ratingCheck(media_type: str, items_to_rate: List[Dict], watched_time: float, total_time: float) -> None: +def ratingCheck( + media_type: str, items_to_rate: list[dict], watched_time: float, total_time: float +) -> None: """Check if a video should be rated and if so launches the rating dialog""" - logger.debug("Rating Check called for '%s'" % media_type) - if not kodiUtilities.getSettingAsBool("rate_%s" % media_type): - logger.debug("'%s' is configured to not be rated." % media_type) + logger.debug(f"Rating Check called for '{media_type}'") + if not kodiUtilities.getSettingAsBool(f"rate_{media_type}"): + logger.debug(f"'{media_type}' is configured to not be rated.") return if items_to_rate is None: logger.debug("Summary information is empty, aborting.") @@ -27,83 +28,98 @@ def ratingCheck(media_type: str, items_to_rate: List[Dict], watched_time: float, if watched >= kodiUtilities.getSettingAsFloat("rate_min_view_time"): rateMedia(media_type, items_to_rate) else: - logger.debug("'%s' does not meet minimum view time for rating (watched: %0.2f%%, minimum: %0.2f%%)" % ( - media_type, watched, kodiUtilities.getSettingAsFloat("rate_min_view_time"))) + logger.debug( + "'{}' does not meet minimum view time for rating (watched: {:0.2f}%, minimum: {:0.2f}%)".format( + media_type, + watched, + kodiUtilities.getSettingAsFloat("rate_min_view_time"), + ) + ) -def rateMedia(media_type: str, itemsToRate: List[Dict], unrate: bool = False, rating: Optional[Union[int, str]] = None) -> None: +def rateMedia( + media_type: str, + itemsToRate: list[dict], + unrate: bool = False, + rating: int | str | None = None, +) -> None: """Launches the rating dialog""" for summary_info in itemsToRate: if not utilities.isValidMediaType(media_type): logger.debug("Not a valid media type") return - elif 'user' not in summary_info: + elif "user" not in summary_info: logger.debug("No user data") return s = utilities.getFormattedItemName(media_type, summary_info) - logger.debug("Summary Info %s" % summary_info) + logger.debug(f"Summary Info {summary_info}") if unrate: rating = None - if summary_info['user']['ratings']['rating'] > 0: + if summary_info["user"]["ratings"]["rating"] > 0: rating = 0 if rating is not None: - logger.debug("'%s' is being unrated." % s) + logger.debug(f"'{s}' is being unrated.") __rateOnTrakt(rating, media_type, summary_info, unrate=True) else: - logger.debug("'%s' has not been rated, so not unrating." % s) + logger.debug(f"'{s}' has not been rated, so not unrating.") return - rerate = kodiUtilities.getSettingAsBool('rate_rerate') + rerate = kodiUtilities.getSettingAsBool("rate_rerate") if rating is not None: - if summary_info['user']['ratings']['rating'] == 0: + if summary_info["user"]["ratings"]["rating"] == 0: logger.debug( - "Rating for '%s' is being set to '%d' manually." % (s, rating)) + f"Rating for '{s:s}' is being set to '{rating:d}' manually." + ) __rateOnTrakt(rating, media_type, summary_info) else: if rerate: - if not summary_info['user']['ratings']['rating'] == rating: + if summary_info["user"]["ratings"]["rating"] != rating: logger.debug( - "Rating for '%s' is being set to '%d' manually." % (s, rating)) + f"Rating for '{s:s}' is being set to '{rating:d}' manually." + ) __rateOnTrakt(rating, media_type, summary_info) else: - kodiUtilities.notification( - kodiUtilities.getString(32043), s) - logger.debug( - "'%s' already has a rating of '%d'." % (s, rating)) + kodiUtilities.notification(kodiUtilities.getString(32043), s) + logger.debug(f"'{s:s}' already has a rating of '{rating:d}'.") else: - kodiUtilities.notification( - kodiUtilities.getString(32041), s) - logger.debug("'%s' is already rated." % s) + kodiUtilities.notification(kodiUtilities.getString(32041), s) + logger.debug(f"'{s}' is already rated.") return - if summary_info['user']['ratings'] and summary_info['user']['ratings']['rating']: + if ( + summary_info["user"]["ratings"] + and summary_info["user"]["ratings"]["rating"] + ): if not rerate: - logger.debug("'%s' has already been rated." % s) + logger.debug(f"'{s}' has already been rated.") kodiUtilities.notification(kodiUtilities.getString(32041), s) return else: - logger.debug("'%s' is being re-rated." % s) + logger.debug(f"'{s}' is being re-rated.") gui = RatingDialog( "script-trakt-RatingDialog.xml", - __addon__.getAddonInfo('path'), + __addon__.getAddonInfo("path"), media_type, summary_info, - rerate + rerate, ) gui.doModal() if gui.rating: rating = gui.rating - if rerate: - if summary_info['user']['ratings'] and summary_info['user']['ratings']['rating'] > 0 and rating == summary_info['user']['ratings']['rating']: - rating = 0 + if rerate and ( + summary_info["user"]["ratings"] + and summary_info["user"]["ratings"]["rating"] > 0 + and rating == summary_info["user"]["ratings"]["rating"] + ): + rating = 0 if rating == 0 or rating == "unrate": __rateOnTrakt(rating, gui.media_type, gui.media, unrate=True) @@ -118,33 +134,53 @@ def rateMedia(media_type: str, itemsToRate: List[Dict], unrate: bool = False, ra rating = None -def __rateOnTrakt(rating: Union[int, str], media_type: str, media: Dict, unrate: bool = False) -> None: - logger.debug("Sending rating (%s) to Trakt.tv" % rating) +def __rateOnTrakt( + rating: int | str, media_type: str, media: dict, unrate: bool = False +) -> None: + logger.debug(f"Sending rating ({rating}) to Trakt.tv") params = media if utilities.isMovie(media_type): - key = 'movies' - params['rating'] = rating - if 'movieid' in media: - kodiUtilities.kodiJsonRequest({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.SetMovieDetails", "params": { - "movieid": media['movieid'], "userrating": rating}}) + key = "movies" + params["rating"] = rating + if "movieid" in media: + kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "id": 1, + "method": "VideoLibrary.SetMovieDetails", + "params": {"movieid": media["movieid"], "userrating": rating}, + } + ) elif utilities.isShow(media_type): - key = 'shows' + key = "shows" # we need to remove this key or trakt will be confused - del(params["seasons"]) - params['rating'] = rating - if 'tvshowid' in media: - kodiUtilities.kodiJsonRequest({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.SetTVShowDetails", "params": { - "tvshowid": media['tvshowid'], "userrating": rating}}) + del params["seasons"] + params["rating"] = rating + if "tvshowid" in media: + kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "id": 1, + "method": "VideoLibrary.SetTVShowDetails", + "params": {"tvshowid": media["tvshowid"], "userrating": rating}, + } + ) elif utilities.isSeason(media_type): - key = 'shows' - params['seasons'] = [{'rating': rating, 'number': media['season']}] + key = "shows" + params["seasons"] = [{"rating": rating, "number": media["season"]}] elif utilities.isEpisode(media_type): - key = 'episodes' - params['rating'] = rating - if 'episodeid' in media: - kodiUtilities.kodiJsonRequest({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.SetEpisodeDetails", "params": { - "episodeid": media['episodeid'], "userrating": rating}}) + key = "episodes" + params["rating"] = rating + if "episodeid" in media: + kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "id": 1, + "method": "VideoLibrary.SetEpisodeDetails", + "params": {"episodeid": media["episodeid"], "userrating": rating}, + } + ) else: return root = {key: [params]} @@ -156,8 +192,12 @@ def __rateOnTrakt(rating: Union[int, str], media_type: str, media: Dict, unrate: if data: s = utilities.getFormattedItemName(media_type, media) - if 'not_found' in data and not data['not_found']['movies'] and not data['not_found']['episodes'] and not data['not_found']['shows']: - + if ( + "not_found" in data + and not data["not_found"]["movies"] + and not data["not_found"]["episodes"] + and not data["not_found"]["shows"] + ): if not unrate: kodiUtilities.notification(kodiUtilities.getString(32040), s) else: @@ -168,12 +208,12 @@ def __rateOnTrakt(rating: Union[int, str], media_type: str, media: Dict, unrate: class RatingDialog(xbmcgui.WindowXMLDialog): media_type: str - media: Dict - rating: Optional[Union[int, str]] + media: dict + rating: int | str | None rerate: bool default_rating: int - buttons = { + buttons: ClassVar[dict[int, int]] = { 11030: 1, 11031: 2, 11032: 3, @@ -183,10 +223,10 @@ class RatingDialog(xbmcgui.WindowXMLDialog): 11036: 7, 11037: 8, 11038: 9, - 11039: 10 + 11039: 10, } - focus_labels = { + focus_labels: ClassVar[dict[int, int]] = { 11030: 32028, 11031: 32029, 11032: 32030, @@ -196,26 +236,39 @@ class RatingDialog(xbmcgui.WindowXMLDialog): 11036: 32034, 11037: 32035, 11038: 32036, - 11039: 32027 + 11039: 32027, } - def __init__(self, xmlFile: str, resourcePath: str, media_type: str, media: Dict, rerate: bool) -> None: + def __init__( + self, + xmlFile: str, + resourcePath: str, + media_type: str, + media: dict, + rerate: bool, + ) -> None: self.media_type = media_type self.media = media self.rating = None self.rerate = rerate - self.default_rating = kodiUtilities.getSettingAsInt('rating_default') + self.default_rating = kodiUtilities.getSettingAsInt("rating_default") - def __new__(cls, xmlFile: str, resourcePath: str, media_type: str, media: Dict, rerate: bool) -> Any: - return super(RatingDialog, cls).__new__(cls, xmlFile, resourcePath) + def __new__( + cls, xmlFile: str, resourcePath: str, media_type: str, media: dict, rerate: bool + ) -> Any: + return super().__new__(cls, xmlFile, resourcePath) def onInit(self) -> None: s = utilities.getFormattedItemName(self.media_type, self.media) self.getControl(10012).setLabel(s) rateID = 11029 + self.default_rating - if self.rerate and self.media['user']['ratings'] and int(self.media['user']['ratings']['rating']) > 0: - rateID = 11029 + int(self.media['user']['ratings']['rating']) + if ( + self.rerate + and self.media["user"]["ratings"] + and int(self.media["user"]["ratings"]["rating"]) > 0 + ): + rateID = 11029 + int(self.media["user"]["ratings"]["rating"]) self.setFocus(self.getControl(rateID)) def onClick(self, controlID: int) -> None: @@ -227,19 +280,21 @@ def onFocus(self, controlID: int) -> None: if controlID in self.focus_labels: s = kodiUtilities.getString(self.focus_labels[controlID]) - if self.rerate: - if self.media['user']['ratings'] and self.media['user']['ratings']['rating'] == self.buttons[controlID]: - if utilities.isMovie(self.media_type): - s = kodiUtilities.getString(32037) - elif utilities.isShow(self.media_type): - s = kodiUtilities.getString(32038) - elif utilities.isEpisode(self.media_type): - s = kodiUtilities.getString(32039) - elif utilities.isSeason(self.media_type): - s = kodiUtilities.getString(32132) - else: - pass + if self.rerate and ( + self.media["user"]["ratings"] + and self.media["user"]["ratings"]["rating"] == self.buttons[controlID] + ): + if utilities.isMovie(self.media_type): + s = kodiUtilities.getString(32037) + elif utilities.isShow(self.media_type): + s = kodiUtilities.getString(32038) + elif utilities.isEpisode(self.media_type): + s = kodiUtilities.getString(32039) + elif utilities.isSeason(self.media_type): + s = kodiUtilities.getString(32132) + else: + pass self.getControl(10013).setLabel(s) else: - self.getControl(10013).setLabel('') + self.getControl(10013).setLabel("") diff --git a/resources/lib/script.py b/resources/lib/script.py index aa3d3478..ae5a54d9 100644 --- a/resources/lib/script.py +++ b/resources/lib/script.py @@ -1,28 +1,26 @@ -# -*- coding: utf-8 -*- -import xbmc -import sys import logging -from resources.lib import utilities -from resources.lib import kodiUtilities -from resources.lib import sqlitequeue -from typing import Dict +import sys + +import xbmc + +from resources.lib import kodiUtilities, sqlitequeue, utilities from resources.lib.traktContextMenu import traktContextMenu logger = logging.getLogger(__name__) -def __getArguments() -> Dict: +def __getArguments() -> dict: data = None default_actions = {0: "sync"} if len(sys.argv) == 1: - data = {'action': default_actions[0]} + data = {"action": default_actions[0]} else: data = {} for item in sys.argv: values = item.split("=") if len(values) == 2: data[values[0].lower()] = values[1] - data['action'] = data['action'].lower() + data["action"] = data["action"].lower() return data @@ -31,16 +29,16 @@ def run() -> None: args = __getArguments() data = {} - xbmc.log("start trakt with arguments: %s" % args, xbmc.LOGINFO) + xbmc.log(f"start trakt with arguments: {args}", xbmc.LOGINFO) - if args['action'] == 'auth_info': - data['action'] = 'auth_info' + if args["action"] == "auth_info": + data["action"] = "auth_info" - if args['action'] == 'contextmenu': + if args["action"] == "contextmenu": buttons = [] media_type = kodiUtilities.getMediaType() - if media_type in ['movie', 'show', 'season', 'episode']: + if media_type in ["movie", "show", "season", "episode"]: buttons.append("rate") buttons.append("togglewatched") buttons.append("addtowatchlist") @@ -55,37 +53,43 @@ def run() -> None: if _action is None: return - logger.debug("'%s' selected from trakt.tv action menu" % _action) - args['action'] = _action + logger.debug(f"'{_action}' selected from trakt.tv action menu") + args["action"] = _action - if args['action'] == 'sync': - data = {'action': 'manualSync', 'silent': False} - if 'silent' in args: - data['silent'] = (args['silent'].lower() == 'true') - data['library'] = "all" - if 'library' in args and args['library'] in ['episodes', 'movies']: - data['library'] = args['library'] + if args["action"] == "sync": + data = {"action": "manualSync", "silent": False} + if "silent" in args: + data["silent"] = args["silent"].lower() == "true" + data["library"] = "all" + if "library" in args and args["library"] in ["episodes", "movies"]: + data["library"] = args["library"] - elif args['action'] in ['rate', 'unrate']: - data = {'action': args['action']} + elif args["action"] in ["rate", "unrate"]: + data = {"action": args["action"]} media_type = None - if 'media_type' in args and 'dbid' in args: - media_type = args['media_type'] + if "media_type" in args and "dbid" in args: + media_type = args["media_type"] try: - data['dbid'] = int(args['dbid']) + data["dbid"] = int(args["dbid"]) except ValueError: logger.debug( - "Manual %s triggered for library item, but DBID is invalid." % args['action']) + "Manual {} triggered for library item, but DBID is invalid.".format( + args["action"] + ) + ) return - elif 'media_type' in args and 'remoteid' in args: - media_type = args['media_type'] - data['remoteid'] = args['remoteid'] + elif "media_type" in args and "remoteid" in args: + media_type = args["media_type"] + data["remoteid"] = args["remoteid"] try: - data['season'] = int(args['season']) - data['episode'] = int(args['episode']) + data["season"] = int(args["season"]) + data["episode"] = int(args["episode"]) except ValueError: logger.debug( - "Error parsing season or episode for manual %s" % args['action']) + "Error parsing season or episode for manual {}".format( + args["action"] + ) + ) return except KeyError: pass @@ -94,95 +98,152 @@ def run() -> None: if not utilities.isValidMediaType(media_type): logger.debug("Error, not in video library.") return - data['dbid'] = int(xbmc.getInfoLabel('ListItem.DBID')) - + data["dbid"] = int(xbmc.getInfoLabel("ListItem.DBID")) - if media_type is None or media_type == 'None': + if media_type is None or media_type == "None": media_type = kodiUtilities.getMediaType() - xbmc.log("Got the mediatype from selected item: %s" % media_type, xbmc.LOGINFO) + xbmc.log( + f"Got the mediatype from selected item: {media_type}", xbmc.LOGINFO + ) if media_type is None: logger.debug( - "Manual %s triggered on an unsupported content container." % args['action']) + "Manual {} triggered on an unsupported content container.".format( + args["action"] + ) + ) elif utilities.isValidMediaType(media_type): - data['media_type'] = media_type - if 'dbid' in data: - logger.debug("Manual %s of library '%s' with an ID of '%s'." % ( - args['action'], media_type, data['dbid'])) + data["media_type"] = media_type + if "dbid" in data: + logger.debug( + "Manual {} of library '{}' with an ID of '{}'.".format( + args["action"], media_type, data["dbid"] + ) + ) if utilities.isMovie(media_type): result = kodiUtilities.getMovieDetailsFromKodi( - data['dbid'], ['imdbnumber', 'uniqueid', 'title', 'year']) + data["dbid"], ["imdbnumber", "uniqueid", "title", "year"] + ) if not result: logger.debug( - "No data was returned from Kodi, aborting manual %s." % args['action']) + "No data was returned from Kodi, aborting manual {}.".format( + args["action"] + ) + ) return elif utilities.isShow(media_type): - tvshow_id = data['dbid'] + tvshow_id = data["dbid"] elif utilities.isSeason(media_type): result = kodiUtilities.getSeasonDetailsFromKodi( - data['dbid'], ['tvshowid', 'season']) + data["dbid"], ["tvshowid", "season"] + ) if not result: logger.debug( - "No data was returned from Kodi, aborting manual %s." % args['action']) + "No data was returned from Kodi, aborting manual {}.".format( + args["action"] + ) + ) return - tvshow_id = result['tvshowid'] - data['season'] = result['season'] + tvshow_id = result["tvshowid"] + data["season"] = result["season"] elif utilities.isEpisode(media_type): result = kodiUtilities.getEpisodeDetailsFromKodi( - data['dbid'], ['season', 'episode', 'tvshowid']) + data["dbid"], ["season", "episode", "tvshowid"] + ) if not result: logger.debug( - "No data was returned from Kodi, aborting manual %s." % args['action']) + "No data was returned from Kodi, aborting manual {}.".format( + args["action"] + ) + ) return - tvshow_id = result['tvshowid'] - data['season'] = result['season'] - data['episode'] = result['episode'] - - if utilities.isShow(media_type) or utilities.isSeason(media_type) or utilities.isEpisode(media_type): + tvshow_id = result["tvshowid"] + data["season"] = result["season"] + data["episode"] = result["episode"] + + if ( + utilities.isShow(media_type) + or utilities.isSeason(media_type) + or utilities.isEpisode(media_type) + ): result = kodiUtilities.getShowDetailsFromKodi( - tvshow_id, ['imdbnumber', 'uniqueid']) + tvshow_id, ["imdbnumber", "uniqueid"] + ) if not result: logger.debug( - "No data was returned from Kodi, aborting manual %s." % args['action']) + "No data was returned from Kodi, aborting manual {}.".format( + args["action"] + ) + ) return - data['video_ids'] = result['uniqueid'] + data["video_ids"] = result["uniqueid"] else: - data['video_id'] = data['remoteid'] - if 'season' in data and 'episode' in data: - logger.debug("Manual %s of non-library '%s' S%02dE%02d, with an ID of '%s'." % ( - args['action'], media_type, data['season'], data['episode'], data['remoteid'])) - elif 'season' in data: - logger.debug("Manual %s of non-library '%s' S%02d, with an ID of '%s'." % - (args['action'], media_type, data['season'], data['remoteid'])) + data["video_id"] = data["remoteid"] + if "season" in data and "episode" in data: + logger.debug( + "Manual {:s} of non-library '{:s}' S{:02d}E{:02d}, with an ID of '{:s}'.".format( + args["action"], + media_type, + data["season"], + data["episode"], + data["remoteid"], + ) + ) + elif "season" in data: + logger.debug( + "Manual {:s} of non-library '{:s}' S{:02d}, with an ID of '{:s}'.".format( + args["action"], media_type, data["season"], data["remoteid"] + ) + ) else: - logger.debug("Manual %s of non-library '%s' with an ID of '%s'." % - (args['action'], media_type, data['remoteid'])) - - if args['action'] == 'rate' and 'rating' in args: - if args['rating'] in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']: - data['rating'] = int(args['rating']) - - data = {'action': 'manualRating', 'ratingData': data} + logger.debug( + "Manual {} of non-library '{}' with an ID of '{}'.".format( + args["action"], media_type, data["remoteid"] + ) + ) + + if ( + args["action"] == "rate" + and "rating" in args + and args["rating"] + in [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ] + ): + data["rating"] = int(args["rating"]) + + data = {"action": "manualRating", "ratingData": data} else: - logger.debug("Manual %s of '%s' is unsupported." % - (args['action'], media_type)) + logger.debug( + "Manual {} of '{}' is unsupported.".format(args["action"], media_type) + ) - elif args['action'] == 'togglewatched': + elif args["action"] == "togglewatched": media_type = kodiUtilities.getMediaType() - if media_type in ['movie', 'show', 'season', 'episode']: - data = {'media_type': media_type} + if media_type in ["movie", "show", "season", "episode"]: + data = {"media_type": media_type} if utilities.isMovie(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getMovieDetailsFromKodi( - dbid, ['imdbnumber', 'uniqueid', 'title', 'year', 'playcount']) + dbid, ["imdbnumber", "uniqueid", "title", "year", "playcount"] + ) if result: - if result['playcount'] == 0: - data['ids'] = result['uniqueid'] + if result["playcount"] == 0: + data["ids"] = result["uniqueid"] else: logger.debug("Movie alread marked as watched in Kodi.") else: @@ -190,131 +251,168 @@ def run() -> None: return elif utilities.isEpisode(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getEpisodeDetailsFromKodi( - dbid, ['showtitle', 'season', 'episode', 'tvshowid', 'playcount']) + dbid, ["showtitle", "season", "episode", "tvshowid", "playcount"] + ) if result: - if result['playcount'] == 0: - data['ids'] = result['show_ids'] - data['season'] = result['season'] - data['number'] = result['episode'] - data['title'] = result['showtitle'] + if result["playcount"] == 0: + data["ids"] = result["show_ids"] + data["season"] = result["season"] + data["number"] = result["episode"] + data["title"] = result["showtitle"] else: - logger.debug( - "Episode already marked as watched in Kodi.") + logger.debug("Episode already marked as watched in Kodi.") else: logger.debug("Error getting episode details from Kodi.") return elif utilities.isSeason(media_type): showID = None - showTitle = xbmc.getInfoLabel('ListItem.TVShowTitle') - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShows', 'params': { - 'properties': ['title', 'imdbnumber', 'uniqueid', 'year']}, 'id': 0}) - if result and 'tvshows' in result: - for show in result['tvshows']: - if show['title'] == showTitle: - showID = show['tvshowid'] - data['ids'] = show['uniqueid'] - data['title'] = show['title'] + showTitle = xbmc.getInfoLabel("ListItem.TVShowTitle") + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetTVShows", + "params": { + "properties": ["title", "imdbnumber", "uniqueid", "year"] + }, + "id": 0, + } + ) + if result and "tvshows" in result: + for show in result["tvshows"]: + if show["title"] == showTitle: + showID = show["tvshowid"] + data["ids"] = show["uniqueid"] + data["title"] = show["title"] break else: logger.debug("Error getting TV shows from Kodi.") return - season = xbmc.getInfoLabel('ListItem.Season') + season = xbmc.getInfoLabel("ListItem.Season") if season == "": season = 0 else: season = int(season) - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetEpisodes', 'params': { - 'tvshowid': showID, 'season': season, 'properties': ['season', 'episode', 'playcount']}, 'id': 0}) - if result and 'episodes' in result: + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetEpisodes", + "params": { + "tvshowid": showID, + "season": season, + "properties": ["season", "episode", "playcount"], + }, + "id": 0, + } + ) + if result and "episodes" in result: episodes = [] - for episode in result['episodes']: - if episode['playcount'] == 0: - episodes.append(episode['episode']) + for episode in result["episodes"]: + if episode["playcount"] == 0: + episodes.append(episode["episode"]) if len(episodes) == 0: logger.debug( - "'%s - Season %d' is already marked as watched." % (showTitle, season)) + f"'{showTitle:s} - Season {season:d}' is already marked as watched." + ) return - data['season'] = season - data['episodes'] = episodes + data["season"] = season + data["episodes"] = episodes else: logger.debug( - "Error getting episodes from '%s' for Season %d" % (showTitle, season)) + f"Error getting episodes from '{showTitle:s}' for Season {season:d}" + ) return elif utilities.isShow(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getShowDetailsFromKodi( - dbid, ['year', 'imdbnumber', 'uniqueid']) + dbid, ["year", "imdbnumber", "uniqueid"] + ) if not result: logger.debug("Error getting show details from Kodi.") return - showTitle = result['label'] - data['ids'] = result['uniqueid'] - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetEpisodes', 'params': { - 'tvshowid': dbid, 'properties': ['season', 'episode', 'playcount', 'showtitle']}, 'id': 0}) - if result and 'episodes' in result: + showTitle = result["label"] + data["ids"] = result["uniqueid"] + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetEpisodes", + "params": { + "tvshowid": dbid, + "properties": [ + "season", + "episode", + "playcount", + "showtitle", + ], + }, + "id": 0, + } + ) + if result and "episodes" in result: i = 0 s = {} - for e in result['episodes']: - data['title'] = e['showtitle'] - season = str(e['season']) + for e in result["episodes"]: + data["title"] = e["showtitle"] + season = str(e["season"]) if season not in s: s[season] = [] - if e['playcount'] == 0: - s[season].append(e['episode']) + if e["playcount"] == 0: + s[season].append(e["episode"]) i += 1 if i == 0: - logger.debug( - "'%s' is already marked as watched." % showTitle) + logger.debug(f"'{showTitle}' is already marked as watched.") return - data['seasons'] = dict((k, v) - for k, v in list(s.items()) if v) + data["seasons"] = {k: v for k, v in list(s.items()) if v} else: logger.debug( - "Error getting episode details for '%s' from Kodi." % showTitle) + f"Error getting episode details for '{showTitle}' from Kodi." + ) return if len(data) > 1: - logger.debug("Marking '%s' with the following data '%s' as watched on Trakt.tv" % ( - media_type, str(data))) - data['action'] = 'markWatched' + logger.debug( + f"Marking '{media_type}' with the following data '{data!s}' as watched on Trakt.tv" + ) + data["action"] = "markWatched" # execute toggle watched action xbmc.executebuiltin("Action(ToggleWatched)") - elif args['action'] == 'addtowatchlist': + elif args["action"] == "addtowatchlist": media_type = kodiUtilities.getMediaType() - if media_type in ['movie', 'show', 'season', 'episode']: - data = {'media_type': media_type} + if media_type in ["movie", "show", "season", "episode"]: + data = {"media_type": media_type} if utilities.isMovie(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getMovieDetailsFromKodi( - dbid, ['imdbnumber', 'uniqueid', 'title', 'year', 'playcount']) + dbid, ["imdbnumber", "uniqueid", "title", "year", "playcount"] + ) if result: - data['ids'] = result['uniqueid'] + data["ids"] = result["uniqueid"] else: logger.debug("Error getting movie details from Kodi.") return elif utilities.isEpisode(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getEpisodeDetailsFromKodi( - dbid, ['showtitle', 'season', 'episode', 'tvshowid', 'playcount']) + dbid, ["showtitle", "season", "episode", "tvshowid", "playcount"] + ) if result: - data['ids'] = result['show_ids'] - data['season'] = result['season'] - data['number'] = result['episode'] - data['title'] = result['showtitle'] + data["ids"] = result["show_ids"] + data["season"] = result["season"] + data["number"] = result["episode"] + data["title"] = result["showtitle"] else: logger.debug("Error getting episode details from Kodi.") @@ -322,78 +420,110 @@ def run() -> None: elif utilities.isSeason(media_type): showID = None - showTitle = xbmc.getInfoLabel('ListItem.TVShowTitle') - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetTVShows', 'params': { - 'properties': ['title', 'imdbnumber', 'uniqueid', 'year']}, 'id': 0}) - if result and 'tvshows' in result: - for show in result['tvshows']: - if show['title'] == showTitle: - showID = show['tvshowid'] - data['id'] = show['imdbnumber'] - data['title'] = show['title'] + showTitle = xbmc.getInfoLabel("ListItem.TVShowTitle") + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetTVShows", + "params": { + "properties": ["title", "imdbnumber", "uniqueid", "year"] + }, + "id": 0, + } + ) + if result and "tvshows" in result: + for show in result["tvshows"]: + if show["title"] == showTitle: + showID = show["tvshowid"] + data["id"] = show["imdbnumber"] + data["title"] = show["title"] break else: logger.debug("Error getting TV shows from Kodi.") return - season = xbmc.getInfoLabel('ListItem.Season') + season = xbmc.getInfoLabel("ListItem.Season") if season == "": season = 0 else: season = int(season) - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetEpisodes', - 'params': {'tvshowid': showID, 'season': season, - 'properties': ['season', 'episode', 'playcount']}, - 'id': 0}) - if result and 'episodes' in result: + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetEpisodes", + "params": { + "tvshowid": showID, + "season": season, + "properties": ["season", "episode", "playcount"], + }, + "id": 0, + } + ) + if result and "episodes" in result: episodes = [] - for episode in result['episodes']: - if episode['playcount'] == 0: - episodes.append(episode['episode']) + for episode in result["episodes"]: + if episode["playcount"] == 0: + episodes.append(episode["episode"]) - data['season'] = season - data['episodes'] = episodes + data["season"] = season + data["episodes"] = episodes else: logger.debug( - "Error getting episodes from '%s' for Season %d" % (showTitle, season)) + f"Error getting episodes from '{showTitle:s}' for Season {season:d}" + ) return elif utilities.isShow(media_type): - dbid = int(xbmc.getInfoLabel('ListItem.DBID')) + dbid = int(xbmc.getInfoLabel("ListItem.DBID")) result = kodiUtilities.getShowDetailsFromKodi( - dbid, ['year', 'imdbnumber', 'uniqueid']) + dbid, ["year", "imdbnumber", "uniqueid"] + ) if not result: logger.debug("Error getting show details from Kodi.") return - showTitle = result['label'] - data['ids'] = result['uniqueid'] - result = kodiUtilities.kodiJsonRequest({'jsonrpc': '2.0', 'method': 'VideoLibrary.GetEpisodes', - 'params': {'tvshowid': dbid, 'properties': - ['season', 'episode', 'playcount', 'showtitle']}, 'id': 0}) - if result and 'episodes' in result: + showTitle = result["label"] + data["ids"] = result["uniqueid"] + result = kodiUtilities.kodiJsonRequest( + { + "jsonrpc": "2.0", + "method": "VideoLibrary.GetEpisodes", + "params": { + "tvshowid": dbid, + "properties": [ + "season", + "episode", + "playcount", + "showtitle", + ], + }, + "id": 0, + } + ) + if result and "episodes" in result: s = {} - for e in result['episodes']: - data['title'] = e['showtitle'] - season = str(e['season']) + for e in result["episodes"]: + data["title"] = e["showtitle"] + season = str(e["season"]) if season not in s: s[season] = [] - if e['playcount'] == 0: - s[season].append(e['episode']) + if e["playcount"] == 0: + s[season].append(e["episode"]) - data['seasons'] = dict((k, v) - for k, v in list(s.items()) if v) + data["seasons"] = {k: v for k, v in list(s.items()) if v} else: logger.debug( - "Error getting episode details for '%s' from Kodi." % showTitle) + f"Error getting episode details for '{showTitle}' from Kodi." + ) return if len(data) > 1: - logger.debug("Adding '%s' with the following data '%s' to users watchlist on Trakt.tv" - % (media_type, str(data))) - data['action'] = 'addtowatchlist' + logger.debug( + f"Adding '{media_type}' with the following data '{data!s}' to users watchlist on Trakt.tv" + ) + data["action"] = "addtowatchlist" q = sqlitequeue.SqliteQueue() - if 'action' in data: - logger.debug("Queuing for dispatch: %s" % data) + if "action" in data: + logger.debug(f"Queuing for dispatch: {data}") q.append(data) diff --git a/resources/lib/scrobbler.py b/resources/lib/scrobbler.py index 75543b2f..92303900 100644 --- a/resources/lib/scrobbler.py +++ b/resources/lib/scrobbler.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- -# - -import xbmc -import time import logging -from typing import Dict, List, Optional, Any -from resources.lib import utilities -from resources.lib import kodiUtilities import math +import time +from typing import Any + +import xbmc + +from resources.lib import kodiUtilities, utilities from resources.lib.rating import ratingCheck logger = logging.getLogger(__name__) @@ -25,14 +23,15 @@ class Scrobbler: videoDuration: float = 1 watchedTime: float = 0 pausedAt: float = 0 - curVideo: Optional[Dict] = None - curVideoInfo: Optional[Dict] = None + curVideo: dict | None = None + curVideoInfo: dict | None = None playlistIndex: int = 0 - traktShowSummary: Optional[Dict] = None - videosToRate: List[Dict] = [] + traktShowSummary: dict | None = None + videosToRate: list[dict] def __init__(self, api: Any) -> None: self.traktapi = api + self.videosToRate = [] def _currentEpisode(self, watchedPercent: float, episodeCount: int) -> int: split = 100 / episodeCount @@ -66,8 +65,7 @@ def transitionCheck(self, isSeek: bool = False) -> None: self.watchedTime = t else: logger.debug( - "Current playlist item changed! Not updating time! (%d -> %d)" - % (self.playlistIndex, position) + f"Current playlist item changed! Not updating time! ({self.playlistIndex:d} -> {position:d})" ) # do transition check every minute @@ -82,14 +80,12 @@ def transitionCheck(self, isSeek: bool = False) -> None: if self.curMPEpisode != epIndex: response = self.__scrobble("stop") if response is not None: - logger.debug("Scrobble response: %s" % str(response)) + logger.debug(f"Scrobble response: {response!s}") self.videosToRate.append(self.curVideoInfo) # update current information self.curMPEpisode = epIndex episode_details = kodiUtilities.getEpisodeDetailsFromKodi( - self.curVideo["multi_episode_data"][ - self.curMPEpisode - ], + self.curVideo["multi_episode_data"][self.curMPEpisode], [ "showtitle", "season", @@ -101,8 +97,10 @@ def transitionCheck(self, isSeek: bool = False) -> None: ], ) if episode_details: - self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject( - "episode", episode_details + self.curVideoInfo = ( + kodiUtilities.kodiRpcToTraktMediaObject( + "episode", episode_details + ) ) else: self.curVideoInfo = None @@ -117,7 +115,7 @@ def transitionCheck(self, isSeek: bool = False) -> None: activePlayers = kodiUtilities.kodiJsonRequest( {"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1} ) - logger.debug("Scrobble - activePlayers: %s" % activePlayers) + logger.debug(f"Scrobble - activePlayers: {activePlayers}") playerId = int(activePlayers[0]["playerid"]) logger.debug("Scrobble - Doing Player.GetItem kodiJsonRequest") result = kodiUtilities.kodiJsonRequest( @@ -129,12 +127,12 @@ def transitionCheck(self, isSeek: bool = False) -> None: } ) if result: - logger.debug("Scrobble - %s" % result) - type, curVideo = kodiUtilities.getInfoLabelDetails(result) + logger.debug(f"Scrobble - {result}") + _type, curVideo = kodiUtilities.getInfoLabelDetails(result) if curVideo != self.curVideo: response = self.__scrobble("stop") if response is not None: - logger.debug("Scrobble response: %s" % str(response)) + logger.debug(f"Scrobble response: {response!s}") logger.debug("Scrobble PVR transition") # update current information self.curVideo = curVideo @@ -152,8 +150,7 @@ def transitionCheck(self, isSeek: bool = False) -> None: "Scrobble Couldn't set curVideoInfo for movie type" ) logger.debug( - "Scrobble Movie type, curVideoInfo: %s" - % self.curVideoInfo + f"Scrobble Movie type, curVideoInfo: {self.curVideoInfo}" ) elif utilities.isEpisode(self.curVideo["type"]): @@ -182,28 +179,26 @@ def transitionCheck(self, isSeek: bool = False) -> None: } if "year" in self.curVideo: - self.traktShowSummary[ - "year" - ] = self.curVideo["year"] + self.traktShowSummary["year"] = ( + self.curVideo["year"] + ) else: logger.debug( "Scrobble Couldn't set curVideoInfo/traktShowSummary for episode type" ) logger.debug( - "Scrobble Episode type, curVideoInfo: %s" - % self.curVideoInfo + f"Scrobble Episode type, curVideoInfo: {self.curVideoInfo}" ) logger.debug( - "Scrobble Episode type, traktShowSummary: %s" - % self.traktShowSummary + f"Scrobble Episode type, traktShowSummary: {self.traktShowSummary}" ) response = self.__scrobble("start") elif isSeek: self.__scrobble("start") - def playbackStarted(self, data: Dict) -> None: - logger.debug("playbackStarted(data: %s)" % data) + def playbackStarted(self, data: dict) -> None: + logger.debug(f"playbackStarted(data: {data})") if not data: return self.curVideo = data @@ -215,11 +210,11 @@ def playbackStarted(self, data: Dict) -> None: and "id" not in self.curVideo and "video_ids" not in self.curVideo ): - logger.debug("Aborting scrobble to avoid fallback: %s" % (self.curVideo)) + logger.debug(f"Aborting scrobble to avoid fallback: {self.curVideo}") return if "type" in self.curVideo: - logger.debug("Watching: %s" % self.curVideo["type"]) + logger.debug("Watching: {}".format(self.curVideo["type"])) if not xbmc.Player().isPlayingVideo(): logger.debug("Suddenly stopped watching item") return @@ -242,8 +237,8 @@ def playbackStarted(self, data: Dict) -> None: ) else: self.videoDuration = xbmc.Player().getTotalTime() - except Exception as e: - logger.debug("Suddenly stopped watching item: %s" % str(e)) + except Exception as e: # noqa: BLE001 - preserve the existing broad failure boundary + logger.debug(f"Suddenly stopped watching item: {e!s}") self.curVideo = None return @@ -287,7 +282,7 @@ def playbackStarted(self, data: Dict) -> None: } else: logger.debug("Couldn't set curVideoInfo for movie type") - logger.debug("Movie type, curVideoInfo: %s" % self.curVideoInfo) + logger.debug(f"Movie type, curVideoInfo: {self.curVideoInfo}") elif utilities.isEpisode(self.curVideo["type"]): if "id" in self.curVideo: @@ -304,7 +299,9 @@ def playbackStarted(self, data: Dict) -> None: ], ) if episodeDetailsKodi: - title, year = utilities.regex_year(episodeDetailsKodi["showtitle"]) + title, year = utilities.regex_year( + episodeDetailsKodi["showtitle"] + ) if not year: self.traktShowSummary = { "title": episodeDetailsKodi["showtitle"], @@ -313,7 +310,9 @@ def playbackStarted(self, data: Dict) -> None: else: self.traktShowSummary = {"title": title, "year": year} if "show_ids" in episodeDetailsKodi: - self.traktShowSummary["ids"] = episodeDetailsKodi["show_ids"] + self.traktShowSummary["ids"] = episodeDetailsKodi[ + "show_ids" + ] self.curVideoInfo = kodiUtilities.kodiRpcToTraktMediaObject( "episode", episodeDetailsKodi ) @@ -321,8 +320,9 @@ def playbackStarted(self, data: Dict) -> None: 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." - % self.curVideo["id"] + "Episode details from Kodi was empty, ID ({:d}) seems invalid, aborting further scrobbling of this episode.".format( + self.curVideo["id"] + ) ) self.curVideo = None self.isPlaying = False @@ -368,10 +368,8 @@ def playbackStarted(self, data: Dict) -> None: ): self.isMultiPartEpisode = True - logger.debug("Episode type, curVideoInfo: %s" % self.curVideoInfo) - logger.debug( - "Episode type, traktShowSummary: %s" % self.traktShowSummary - ) + logger.debug(f"Episode type, curVideoInfo: {self.curVideoInfo}") + logger.debug(f"Episode type, traktShowSummary: {self.traktShowSummary}") self.isPlaying = True self.isPaused = False @@ -386,7 +384,7 @@ def playbackStarted(self, data: Dict) -> None: and utilities.isMovie(self.curVideo["type"]) and "ids" in self.curVideoInfo ): - best_id, id_type = utilities.best_id( + best_id, _ = utilities.best_id( self.curVideoInfo["ids"], self.curVideo["type"] ) result = {"movie": self.traktapi.getMovieSummary(best_id).to_dict()} @@ -395,7 +393,7 @@ def playbackStarted(self, data: Dict) -> None: and utilities.isEpisode(self.curVideo["type"]) and "ids" in self.traktShowSummary ): - best_id, id_type = utilities.best_id( + best_id, _id_type = utilities.best_id( self.traktShowSummary["ids"], self.curVideo["type"] ) result = { @@ -416,7 +414,7 @@ def playbackStarted(self, data: Dict) -> None: self.__preFetchUserRatings(result) - def __preFetchUserRatings(self, result: Dict) -> None: + def __preFetchUserRatings(self, result: dict) -> None: if result: if ( utilities.isMovie(self.curVideo["type"]) @@ -452,7 +450,7 @@ def __preFetchUserRatings(self, result: Dict) -> None: "trakt", ) } - logger.debug("Pre-Fetch result: %s; Info: %s" % (result, self.curVideoInfo)) + logger.debug(f"Pre-Fetch result: {result}; Info: {self.curVideoInfo}") def playbackResumed(self) -> None: if not self.isPlaying or self.isPVR: @@ -461,7 +459,7 @@ def playbackResumed(self) -> None: logger.debug("playbackResumed()") if self.isPaused: p = time.time() - self.pausedAt - logger.debug("Resumed after: %s" % str(p)) + logger.debug(f"Resumed after: {p!s}") self.pausedAt = 0 self.isPaused = False self.__scrobble("start") @@ -471,7 +469,7 @@ def playbackPaused(self) -> None: return logger.debug("playbackPaused()") - logger.debug("Paused after: %s" % str(self.watchedTime)) + logger.debug(f"Paused after: {self.watchedTime!s}") self.isPaused = True self.pausedAt = time.time() self.__scrobble("pause") @@ -520,7 +518,7 @@ def __calculateWatchedPercent(self) -> float: else: return 0 - def __scrobble(self, status: str) -> Optional[Dict]: + def __scrobble(self, status: str) -> dict | None: if not self.curVideoInfo: return @@ -535,19 +533,19 @@ def __scrobble(self, status: str) -> Optional[Dict]: ) if response is not None: self.__scrobbleNotification(response) - logger.debug("Scrobble response: %s" % str(response)) + logger.debug(f"Scrobble response: {response!s}") return response else: logger.debug( - "Failed to scrobble movie: %s | %s | %s" - % (self.curVideoInfo, watchedPercent, status) + f"Failed to scrobble movie: {self.curVideoInfo} | {watchedPercent} | {status}" ) elif utilities.isEpisode(self.curVideo["type"]) and scrobbleEpisodeOption: if self.isMultiPartEpisode: logger.debug( - "Multi-part episode, scrobbling part %d of %d." - % (self.curMPEpisode + 1, self.curVideo["multi_episode_count"]) + "Multi-part episode, scrobbling part {:d} of {:d}.".format( + self.curMPEpisode + 1, self.curVideo["multi_episode_count"] + ) ) adjustedDuration = int( self.videoDuration / self.curVideo["multi_episode_count"] @@ -557,10 +555,8 @@ def __scrobble(self, status: str) -> Optional[Dict]: / adjustedDuration ) * 100 - logger.debug( - "scrobble sending show object: %s" % str(self.traktShowSummary) - ) - logger.debug("scrobble sending episode object: %s" % str(self.curVideoInfo)) + logger.debug(f"scrobble sending show object: {self.traktShowSummary!s}") + logger.debug(f"scrobble sending episode object: {self.curVideoInfo!s}") response = self.traktapi.scrobbleEpisode( self.traktShowSummary, self.curVideoInfo, watchedPercent, status ) @@ -573,7 +569,9 @@ def __scrobble(self, status: str) -> Optional[Dict]: # but rather an alternative title. To handle this case, call the Trakt search function. if response is None: logger.debug( - "Searching for show title: %s" % self.traktShowSummary["title"] + "Searching for show title: {}".format( + self.traktShowSummary["title"] + ) ) # This text query API is basically the same as searching on the website. Works with alternative # titles, unlike the scrobble function. @@ -583,16 +581,13 @@ def __scrobble(self, status: str) -> Optional[Dict]: if not newResp: logger.debug("Empty Response from getTextQuery, giving up") else: - logger.debug( - "Got Response from getTextQuery: %s" % str(newResp) - ) + logger.debug(f"Got Response from getTextQuery: {newResp!s}") # We got something back. Have to assume the first show found is the right one; if there's more than # one, there's no way to know which to use. Pull the primary title from the response (and the year, # just because it's there). showObj = {"title": newResp[0].title, "year": newResp[0].year} logger.debug( - "scrobble sending getTextQuery first show object: %s" - % str(showObj) + f"scrobble sending getTextQuery first show object: {showObj!s}" ) # Now we can attempt the scrobble again, using the primary title this time. response = self.traktapi.scrobbleEpisode( @@ -605,8 +600,7 @@ def __scrobble(self, status: str) -> Optional[Dict]: self.curVideoInfo["title"], response["episode"]["title"], 50.0 ): logger.debug( - "scrobble sending incorrect scrobbleEpisode stopping: %sx%s - %s != %s" - % ( + "scrobble sending incorrect scrobbleEpisode stopping: {}x{} - {} != {}".format( self.curVideoInfo["season"], self.curVideoInfo["number"], self.curVideoInfo["title"], @@ -616,15 +610,14 @@ def __scrobble(self, status: str) -> Optional[Dict]: self.stopScrobbler = True self.__scrobbleNotification(response) - logger.debug("Scrobble response: %s" % str(response)) + logger.debug(f"Scrobble response: {response!s}") return response else: logger.debug( - "Failed to scrobble episode: %s | %s | %s | %s" - % (self.traktShowSummary, self.curVideoInfo, watchedPercent, status) + f"Failed to scrobble episode: {self.traktShowSummary} | {self.curVideoInfo} | {watchedPercent} | {status}" ) - def __scrobbleNotification(self, info: Dict) -> None: + def __scrobbleNotification(self, info: dict) -> None: if not self.curVideoInfo: return diff --git a/resources/lib/service.py b/resources/lib/service.py index 9d28f4ff..8f99f00b 100644 --- a/resources/lib/service.py +++ b/resources/lib/service.py @@ -1,19 +1,17 @@ -# -*- coding: utf-8 -*- -import threading import logging -from typing import Any, Dict, Optional, Callable -import xbmc -import time -import xbmcgui import re -import urllib.request -import urllib.parse +import threading +import time import urllib.error +import urllib.parse +import urllib.request +from collections.abc import Callable +from typing import Any -from resources.lib import globals -from resources.lib import sqlitequeue -from resources.lib import utilities -from resources.lib import kodiUtilities +import xbmc +import xbmcgui + +from resources.lib import globals, kodiUtilities, sqlitequeue, utilities from resources.lib.rating import rateMedia from resources.lib.scrobbler import Scrobbler from resources.lib.sync import Sync @@ -23,21 +21,21 @@ class traktService: - scrobbler: Optional[Scrobbler] = None - updateTagsThread: Optional[threading.Thread] = None - syncThread: Optional[threading.Thread] = None + scrobbler: Scrobbler | None = None + updateTagsThread: threading.Thread | None = None + syncThread: threading.Thread | None = None dispatchQueue: sqlitequeue.SqliteQueue = sqlitequeue.SqliteQueue() def __init__(self) -> None: threading.Thread.name = "trakt" - def _dispatchQueue(self, data: Dict) -> None: - logger.debug("Queuing for dispatch: %s" % data) + def _dispatchQueue(self, data: dict) -> None: + logger.debug(f"Queuing for dispatch: {data}") self.dispatchQueue.append(data) - def _dispatch(self, data: Dict) -> None: + def _dispatch(self, data: dict) -> None: try: - logger.debug("Dispatch: %s" % data) + logger.debug(f"Dispatch: {data}") action = data["action"] if action == "started": del data["action"] @@ -85,15 +83,15 @@ def _dispatch(self, data: Dict) -> None: # init traktapi class globals.traktapi = traktAPI(True) else: - logger.debug("Unknown dispatch action, '%s'." % action) - except Exception as ex: + logger.debug(f"Unknown dispatch action, '{action}'.") + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logger.fatal(message) def run(self) -> None: startup_delay = kodiUtilities.getSettingAsInt("startup_delay") if startup_delay: - logger.debug("Delaying startup by %d seconds." % startup_delay) + logger.debug(f"Delaying startup by {startup_delay:d} seconds.") xbmc.sleep(startup_delay * 1000) logger.debug("Service thread starting.") @@ -118,7 +116,7 @@ def run(self) -> None: while not self.Monitor.abortRequested(): while len(self.dispatchQueue) and (not self.Monitor.abortRequested()): data = self.dispatchQueue.get() - logger.debug("Queued dispatch: %s" % data) + logger.debug(f"Queued dispatch: {data}") self._dispatch(data) if xbmc.Player().isPlayingVideo(): @@ -139,15 +137,14 @@ def run(self) -> None: if self.syncThread.is_alive(): self.syncThread.join() - def doManualRating(self, data: Dict) -> None: + def doManualRating(self, data: dict) -> None: action = data["action"] media_type = data["media_type"] summaryInfo = None if not utilities.isValidMediaType(media_type): logger.debug( - "doManualRating(): Invalid media type '%s' passed for manual %s." - % (media_type, action) + f"doManualRating(): Invalid media type '{media_type}' passed for manual {action}." ) return @@ -157,16 +154,18 @@ def doManualRating(self, data: Dict) -> None: if "video_ids" in data: logger.debug( - "Getting data for manual %s of %s: video_ids: |%s| dbid: |%s|" - % (action, media_type, data.get("video_ids"), data.get("dbid")) + "Getting data for manual {} of {}: video_ids: |{}| dbid: |{}|".format( + action, media_type, data.get("video_ids"), data.get("dbid") + ) ) best_id, id_type = utilities.best_id(data["video_ids"], media_type) else: logger.debug( - "Getting data for manual %s of %s: video_id: |%s| dbid: |%s|" - % (action, media_type, data.get("video_id"), data.get("dbid")) + "Getting data for manual {} of {}: video_id: |{}| dbid: |{}|".format( + action, media_type, data.get("video_id"), data.get("dbid") + ) ) temp_ids, id_type = utilities.guessBestTraktId( @@ -176,8 +175,7 @@ def doManualRating(self, data: Dict) -> None: if not id_type: logger.debug( - "doManualRating(): Unrecognized id_type: |%s|-|%s|." - % (media_type, best_id) + f"doManualRating(): Unrecognized id_type: |{media_type}|-|{best_id}|." ) return @@ -185,7 +183,7 @@ def doManualRating(self, data: Dict) -> None: if not ids: logger.debug( - "doManualRating(): No Results for: |%s|-|%s|." % (media_type, best_id) + f"doManualRating(): No Results for: |{media_type}|-|{best_id}|." ) return @@ -235,20 +233,20 @@ def doManualRating(self, data: Dict) -> None: "doManualRating(): Summary info was empty, possible problem retrieving data from Trakt.tv" ) - def doAddToWatchlist(self, data: Dict) -> None: + def doAddToWatchlist(self, data: dict) -> None: media_type = data["media_type"] if utilities.isMovie(media_type): - best_id, id_type = utilities.best_id(data["ids"], media_type) + best_id, _id_type = utilities.best_id(data["ids"], media_type) summaryInfo = globals.traktapi.getMovieSummary(best_id).to_dict() if summaryInfo: s = utilities.getFormattedItemName(media_type, summaryInfo) logger.debug( - "doAddToWatchlist(): '%s' trying to add to users watchlist." % s + f"doAddToWatchlist(): '{s}' trying to add to users watchlist." ) params = {"movies": [summaryInfo]} - logger.debug("doAddToWatchlist(): %s" % str(params)) + logger.debug(f"doAddToWatchlist(): {params!s}") result = globals.traktapi.addToWatchlist(params) if result: @@ -269,7 +267,7 @@ def doAddToWatchlist(self, data: Dict) -> None: } ] } - logger.debug("doAddToWatchlist(): %s" % str(summaryInfo)) + logger.debug(f"doAddToWatchlist(): {summaryInfo!s}") s = utilities.getFormattedItemName(media_type, data) result = globals.traktapi.addToWatchlist(summaryInfo) @@ -284,8 +282,9 @@ def doAddToWatchlist(self, data: Dict) -> None: s = utilities.getFormattedItemName(media_type, data) logger.debug( - "doAddToWatchlist(): '%s - Season %d' trying to add to users watchlist." - % (data["ids"], data["season"]) + "doAddToWatchlist(): '{:s} - Season {:d}' trying to add to users watchlist.".format( + data["ids"], data["season"] + ) ) result = globals.traktapi.addToWatchlist(summaryInfo) @@ -296,7 +295,7 @@ def doAddToWatchlist(self, data: Dict) -> None: elif utilities.isShow(media_type): summaryInfo = {"shows": [{"ids": data["ids"]}]} s = utilities.getFormattedItemName(media_type, data) - logger.debug("doAddToWatchlist(): %s" % str(summaryInfo)) + logger.debug(f"doAddToWatchlist(): {summaryInfo!s}") result = globals.traktapi.addToWatchlist(summaryInfo) if result: @@ -304,28 +303,26 @@ def doAddToWatchlist(self, data: Dict) -> None: else: kodiUtilities.notification(kodiUtilities.getString(32166), s) - def doMarkWatched(self, data: Dict) -> None: + def doMarkWatched(self, data: dict) -> None: media_type = data["media_type"] if utilities.isMovie(media_type): - best_id, id_type = utilities.best_id(data["ids"], media_type) + best_id, _id_type = utilities.best_id(data["ids"], media_type) summaryInfo = globals.traktapi.getMovieSummary(best_id).to_dict() - if summaryInfo: - if not summaryInfo["watched"]: - s = utilities.getFormattedItemName(media_type, summaryInfo) - logger.debug( - "doMarkWatched(): '%s' is not watched on Trakt, marking it as watched." - % s - ) - params = {"movies": [summaryInfo]} - logger.debug("doMarkWatched(): %s" % str(params)) + if summaryInfo and not summaryInfo["watched"]: + s = utilities.getFormattedItemName(media_type, summaryInfo) + logger.debug( + f"doMarkWatched(): '{s}' is not watched on Trakt, marking it as watched." + ) + params = {"movies": [summaryInfo]} + logger.debug(f"doMarkWatched(): {params!s}") - result = globals.traktapi.addToHistory(params) - if result: - kodiUtilities.notification(kodiUtilities.getString(32113), s) - else: - kodiUtilities.notification(kodiUtilities.getString(32114), s) + result = globals.traktapi.addToHistory(params) + if result: + kodiUtilities.notification(kodiUtilities.getString(32113), s) + else: + kodiUtilities.notification(kodiUtilities.getString(32114), s) elif utilities.isEpisode(media_type): summaryInfo = { "shows": [ @@ -340,7 +337,7 @@ def doMarkWatched(self, data: Dict) -> None: } ] } - logger.debug("doMarkWatched(): %s" % str(summaryInfo)) + logger.debug(f"doMarkWatched(): {summaryInfo!s}") s = utilities.getFormattedItemName(media_type, data) result = globals.traktapi.addToHistory(summaryInfo) @@ -362,8 +359,7 @@ def doMarkWatched(self, data: Dict) -> None: summaryInfo["shows"][0]["seasons"][0]["episodes"].append({"number": ep}) logger.debug( - "doMarkWatched(): '%s - Season %d' has %d episode(s) that are going to be marked as watched." - % ( + "doMarkWatched(): '{:s} - Season {:d}' has {:d} episode(s) that are going to be marked as watched.".format( data["id"], data["season"], len(summaryInfo["shows"][0]["seasons"][0]["episodes"]), @@ -376,7 +372,7 @@ def doMarkWatched(self, data: Dict) -> None: summaryInfo = {"shows": [{"ids": data["ids"], "seasons": []}]} if summaryInfo: s = utilities.getFormattedItemName(media_type, data) - logger.debug("data: %s" % data) + logger.debug(f"data: {data}") for season in data["seasons"]: episodeJson = [] for episode in data["seasons"][season]: @@ -387,9 +383,9 @@ def doMarkWatched(self, data: Dict) -> None: self.addEpisodesToHistory(summaryInfo, s) - def addEpisodesToHistory(self, summaryInfo: Dict, s: str) -> None: + def addEpisodesToHistory(self, summaryInfo: dict, s: str) -> None: if len(summaryInfo["shows"][0]["seasons"][0]["episodes"]) > 0: - logger.debug("doMarkWatched(): %s" % str(summaryInfo)) + logger.debug(f"doMarkWatched(): {summaryInfo!s}") result = globals.traktapi.addToHistory(summaryInfo) if result: @@ -400,7 +396,9 @@ def addEpisodesToHistory(self, summaryInfo: Dict, s: str) -> None: else: kodiUtilities.notification(kodiUtilities.getString(32114), s) - def doSync(self, manual: bool = False, silent: bool = False, library: str = "all") -> None: + def doSync( + self, manual: bool = False, silent: bool = False, library: str = "all" + ) -> None: self.syncThread = syncThread(manual, silent, library) self.syncThread.start() @@ -410,7 +408,9 @@ class syncThread(threading.Thread): _runSilent: bool = False _library: str = "all" - def __init__(self, isManual: bool = False, runSilent: bool = False, library: str = "all") -> None: + def __init__( + self, isManual: bool = False, runSilent: bool = False, library: str = "all" + ) -> None: threading.Thread.__init__(self) self.name = "trakt-sync" self._isManual = isManual @@ -428,7 +428,7 @@ def run(self) -> None: class traktMonitor(xbmc.Monitor): - action: Callable[[Dict], None] + action: Callable[[dict], None] scanning_video: bool = False def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -450,7 +450,7 @@ def onNotification(self, sender: str, method: str, data: str) -> None: def onScanFinished(self, database: str) -> None: if database == "video": self.scanning_video = False - logger.debug("[traktMonitor] onScanFinished(database: %s)" % database) + logger.debug(f"[traktMonitor] onScanFinished(database: {database})") data = {"action": "scanFinished"} self.action(data) @@ -458,9 +458,7 @@ def onScanFinished(self, database: str) -> None: def onDatabaseScanStarted(self, database: str) -> None: if database == "video": self.scanning_video = True - logger.debug( - "[traktMonitor] onDatabaseScanStarted(database: %s)" % database - ) + logger.debug(f"[traktMonitor] onDatabaseScanStarted(database: {database})") def onCleanFinished(self, database: str) -> None: if database == "video" and not self.scanning_video: # Ignore clean on update. @@ -470,10 +468,10 @@ def onCleanFinished(self, database: str) -> None: class traktPlayer(xbmc.Player): _playing: bool = False - plIndex: Optional[int] = None - action: Callable[[Dict], None] - type: Optional[str] = None - id: Optional[int] = None + plIndex: int | None = None + action: Callable[[dict], None] + type: str | None = None + id: int | None = None def __init__(self, *args: Any, **kwargs: Any) -> None: self.action = kwargs["action"] @@ -509,7 +507,7 @@ def onAVStarted(self) -> None: {"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 1} ) logger.debug( - "[traktPlayer] onAVStarted() - activePlayers: %s" % activePlayers + f"[traktPlayer] onAVStarted() - activePlayers: {activePlayers}" ) playerId = int(activePlayers[0]["playerid"]) logger.debug( @@ -527,7 +525,7 @@ def onAVStarted(self) -> None: } ) if result: - logger.debug("[traktPlayer] onAVStarted() - %s" % result) + logger.debug(f"[traktPlayer] onAVStarted() - {result}") # check for exclusion _filename = None try: @@ -541,15 +539,13 @@ def onAVStarted(self) -> None: custom_proprties = result["item"].get("customproperties") if custom_proprties and "script.trakt.exclude" in custom_proprties: logger.debug( - "[traktPlayer] onAVStarted() - '%s' has exclusion property, ignoring." - % _filename + f"[traktPlayer] onAVStarted() - '{_filename}' has exclusion property, ignoring." ) return if kodiUtilities.checkExclusion(_filename): logger.debug( - "[traktPlayer] onAVStarted() - '%s' is in exclusion settings, ignoring." - % _filename + f"[traktPlayer] onAVStarted() - '{_filename}' is in exclusion settings, ignoring." ) return @@ -591,7 +587,7 @@ def onAVStarted(self) -> None: } ) if result: - logger.debug("[traktPlayer] onAVStarted() - %s" % result) + logger.debug(f"[traktPlayer] onAVStarted() - {result}") tvshowid = int(result["episodedetails"]["tvshowid"]) season = int(result["episodedetails"]["season"]) currentfile = result["episodedetails"]["file"] @@ -610,9 +606,7 @@ def onAVStarted(self) -> None: } ) if result: - logger.debug( - "[traktPlayer] onAVStarted() - %s" % result - ) + logger.debug(f"[traktPlayer] onAVStarted() - {result}") # make sure episodes array exists in results if "episodes" in result: multi = [] @@ -659,28 +653,23 @@ def onAVStarted(self) -> None: # IDs to feed to the scrobbler. Still, much easier than previous versions! foundShowName = xbmc.getInfoLabel("VideoPlayer.Title") logger.debug( - "[traktPlayer] onAVStarted() - Found VideoPlayer.Title: %s" - % foundShowName + f"[traktPlayer] onAVStarted() - Found VideoPlayer.Title: {foundShowName}" ) foundEpisodeName = xbmc.getInfoLabel("VideoPlayer.EpisodeName") logger.debug( - "[traktPlayer] onAVStarted() - Found VideoPlayer.EpisodeName: %s" - % foundEpisodeName + f"[traktPlayer] onAVStarted() - Found VideoPlayer.EpisodeName: {foundEpisodeName}" ) foundEpisodeYear = xbmc.getInfoLabel("VideoPlayer.Year") logger.debug( - "[traktPlayer] onAVStarted() - Found VideoPlayer.Year: %s" - % foundEpisodeYear + f"[traktPlayer] onAVStarted() - Found VideoPlayer.Year: {foundEpisodeYear}" ) foundSeason = xbmc.getInfoLabel("VideoPlayer.Season") logger.debug( - "[traktPlayer] onAVStarted() - Found VideoPlayer.Season: %s" - % foundSeason + f"[traktPlayer] onAVStarted() - Found VideoPlayer.Season: {foundSeason}" ) foundEpisode = xbmc.getInfoLabel("VideoPlayer.Episode") logger.debug( - "[traktPlayer] onAVStarted() - Found VideoPlayer.Episode: %s" - % foundEpisode + f"[traktPlayer] onAVStarted() - Found VideoPlayer.Episode: {foundEpisode}" ) if foundShowName and foundEpisodeName and foundEpisodeYear: # If the show/episode/year are populated, we can skip all the mess of trying to extract the info from the @@ -703,17 +692,14 @@ def onAVStarted(self) -> None: xbmc.getInfoLabel("Player.Filename") ) logger.debug( - "[traktPlayer] onAVStarted() - Found unknown video type with label: %s. Might be a PVR episode, searching Trakt for it." - % foundLabel + f"[traktPlayer] onAVStarted() - Found unknown video type with label: {foundLabel}. Might be a PVR episode, searching Trakt for it." ) logger.debug( - "[traktPlayer] onAVStarted() - After urllib.unquote: %s." - % foundLabel + f"[traktPlayer] onAVStarted() - After urllib.unquote: {foundLabel}." ) splitLabel = foundLabel.rsplit(", ", 3) logger.debug( - "[traktPlayer] onAVStarted() - Post-split of label: %s " - % splitLabel + f"[traktPlayer] onAVStarted() - Post-split of label: {splitLabel} " ) if len(splitLabel) != 4: logger.debug( @@ -721,8 +707,7 @@ def onAVStarted(self) -> None: ) splitLabel = foundLabel.rsplit(", ", 2) logger.debug( - "[traktPlayer] onAVStarted() - Post-split of label: %s " - % splitLabel + f"[traktPlayer] onAVStarted() - Post-split of label: {splitLabel} " ) if len(splitLabel) != 3: logger.debug( @@ -731,15 +716,13 @@ def onAVStarted(self) -> None: return foundShowAndEpInfo = splitLabel[0] logger.debug( - "[traktPlayer] onAVStarted() - show plus episode info: %s" - % foundShowAndEpInfo + f"[traktPlayer] onAVStarted() - show plus episode info: {foundShowAndEpInfo}" ) splitShowAndEpInfo = re.split( r" (s\d\de\d\d)? ?\((\d\d\d\d)\) ", foundShowAndEpInfo, 1 ) logger.debug( - "[traktPlayer] onAVStarted() - Post-split of show plus episode info: %s " - % splitShowAndEpInfo + f"[traktPlayer] onAVStarted() - Post-split of show plus episode info: {splitShowAndEpInfo} " ) if len(splitShowAndEpInfo) != 4: logger.debug( @@ -748,18 +731,15 @@ def onAVStarted(self) -> None: return foundShowName = splitShowAndEpInfo[0] logger.debug( - "[traktPlayer] onAVStarted() - using show name: %s" - % foundShowName + f"[traktPlayer] onAVStarted() - using show name: {foundShowName}" ) foundEpisodeName = splitShowAndEpInfo[3] logger.debug( - "[traktPlayer] onAVStarted() - using episode name: %s" - % foundEpisodeName + f"[traktPlayer] onAVStarted() - using episode name: {foundEpisodeName}" ) foundEpisodeYear = splitShowAndEpInfo[2] logger.debug( - "[traktPlayer] onAVStarted() - using episode year: %s" - % foundEpisodeYear + f"[traktPlayer] onAVStarted() - using episode year: {foundEpisodeYear}" ) epYear = None try: @@ -767,8 +747,7 @@ def onAVStarted(self) -> None: except ValueError: epYear = None logger.debug( - "[traktPlayer] onAVStarted() - verified episode year: %d" - % epYear + f"[traktPlayer] onAVStarted() - verified episode year: {epYear:d}" ) # All right, now we have the show name, episode name, and (maybe) episode year. All good, but useless for # scrobbling since Trakt only understands IDs, not names. @@ -787,8 +766,7 @@ def onAVStarted(self) -> None: ) else: logger.debug( - "[traktPlayer] onAVStarted() - Got Response from getTextQuery: %s" - % str(newResp) + f"[traktPlayer] onAVStarted() - Got Response from getTextQuery: {newResp!s}" ) # We got something back. See if one of the returned values is for the show we're looking for. Often it's # not, but since there's no way to tell the search which show we want, this is all we can do. @@ -796,8 +774,7 @@ def onAVStarted(self) -> None: for thisResp in newResp: compareShowName = thisResp.show.title logger.debug( - "[traktPlayer] onAVStarted() - comparing show name: %s" - % compareShowName + f"[traktPlayer] onAVStarted() - comparing show name: {compareShowName}" ) if thisResp.show.title == foundShowName: logger.debug( @@ -830,8 +807,7 @@ def onAVStarted(self) -> None: # titles, unlike the scrobble function. Though we can't use the episode year since that would only # match the show if we're dealing with season 1. logger.debug( - "[traktPlayer] onAVStarted() - Searching for show title via getTextQuery: %s" - % foundShowName + f"[traktPlayer] onAVStarted() - Searching for show title via getTextQuery: {foundShowName}" ) newResp = globals.traktapi.getTextQuery( foundShowName, "show", None @@ -842,8 +818,7 @@ def onAVStarted(self) -> None: ) else: logger.debug( - "[traktPlayer] onAVStarted() - Got Show Response from getTextQuery: %s" - % str(newResp) + f"[traktPlayer] onAVStarted() - Got Show Response from getTextQuery: {newResp!s}" ) # We got something back. Have to assume the first show found is the right one; if there's more than # one, there's no way to know which to use. Pull the ids from the show data, and store 'em for scrobbling. @@ -871,14 +846,12 @@ def onAVStarted(self) -> None: else: # Got the list back. Go through each season. logger.debug( - "[traktPlayer] onAVStarted() - Got response with seasons: %s" - % str(epQueryResp) + f"[traktPlayer] onAVStarted() - Got response with seasons: {epQueryResp!s}" ) for eachSeason in epQueryResp: # For each season, check each episode. logger.debug( - "[traktPlayer] onAVStarted() - Processing season: %s" - % str(eachSeason) + f"[traktPlayer] onAVStarted() - Processing season: {eachSeason!s}" ) for eachEpisodeNumber in eachSeason.episodes: thisEpTitle = None @@ -890,8 +863,7 @@ def onAVStarted(self) -> None: except: # noqa: E722 thisEpTitle = None logger.debug( - "[traktPlayer] onAVStarted() - Checking episode number %d with title %s" - % (eachEpisodeNumber, thisEpTitle) + f"[traktPlayer] onAVStarted() - Checking episode number {eachEpisodeNumber:d} with title {thisEpTitle:s}" ) if foundEpisodeName == thisEpTitle: # Found it! Save the data. The scrobbler wants season and episode number. Which for some @@ -912,8 +884,9 @@ def onAVStarted(self) -> None: if data["season"]: # OK, that's everything. Data should be all set for scrobbling. logger.debug( - "[traktPlayer] onAVStarted() - Playing a non-library 'episode' : show trakt key %s, season: %d, episode: %d" - % (data["video_ids"], data["season"], data["episode"]) + "[traktPlayer] onAVStarted() - Playing a non-library 'episode' : show trakt key {:s}, season: {:d}, episode: {:d}".format( + data["video_ids"], data["season"], data["episode"] + ) ) else: # Still no data? Too bad, have to give up. @@ -923,8 +896,7 @@ def onAVStarted(self) -> None: return else: logger.debug( - "[traktPlayer] onAVStarted() - Video type '%s' unrecognized, skipping." - % self.type + f"[traktPlayer] onAVStarted() - Video type '{self.type}' unrecognized, skipping." ) return @@ -939,8 +911,7 @@ def onAVStarted(self) -> None: self.onPlayBackEnded() self.plIndex = pos logger.debug( - "[traktPlayer] onAVStarted() - Playlist contains %d item(s), and is currently on item %d" - % (plSize, (pos + 1)) + f"[traktPlayer] onAVStarted() - Playlist contains {plSize:d} item(s), and is currently on item {pos + 1:d}" ) self._playing = True @@ -953,7 +924,7 @@ def onPlayBackEnded(self) -> None: xbmcgui.Window(10000).clearProperty("script.trakt.ids") xbmcgui.Window(10000).clearProperty("script.trakt.paused") if self._playing: - logger.debug("[traktPlayer] onPlayBackEnded() - %s" % self.isPlayingVideo()) + logger.debug(f"[traktPlayer] onPlayBackEnded() - {self.isPlayingVideo()}") self._playing = False self.plIndex = None data = {"action": "ended"} @@ -964,9 +935,7 @@ def onPlayBackStopped(self) -> None: xbmcgui.Window(10000).clearProperty("script.trakt.ids") xbmcgui.Window(10000).clearProperty("script.trakt.paused") if self._playing: - logger.debug( - "[traktPlayer] onPlayBackStopped() - %s" % self.isPlayingVideo() - ) + logger.debug(f"[traktPlayer] onPlayBackStopped() - {self.isPlayingVideo()}") self._playing = False self.plIndex = None data = {"action": "stopped"} @@ -975,40 +944,34 @@ def onPlayBackStopped(self) -> None: # called when user pauses a playing file def onPlayBackPaused(self) -> None: if self._playing: - logger.debug( - "[traktPlayer] onPlayBackPaused() - %s" % self.isPlayingVideo() - ) + logger.debug(f"[traktPlayer] onPlayBackPaused() - {self.isPlayingVideo()}") data = {"action": "paused"} self.action(data) # called when user resumes a paused file def onPlayBackResumed(self) -> None: if self._playing: - logger.debug( - "[traktPlayer] onPlayBackResumed() - %s" % self.isPlayingVideo() - ) + logger.debug(f"[traktPlayer] onPlayBackResumed() - {self.isPlayingVideo()}") data = {"action": "resumed"} self.action(data) # called when user queues the next item def onQueueNextItem(self) -> None: if self._playing: - logger.debug("[traktPlayer] onQueueNextItem() - %s" % self.isPlayingVideo()) + logger.debug(f"[traktPlayer] onQueueNextItem() - {self.isPlayingVideo()}") # called when players speed changes. (eg. user FF/RW) def onPlayBackSpeedChanged(self, speed: int) -> None: if self._playing: logger.debug( - "[traktPlayer] onPlayBackSpeedChanged(speed: %s) - %s" - % (str(speed), self.isPlayingVideo()) + f"[traktPlayer] onPlayBackSpeedChanged(speed: {speed!s}) - {self.isPlayingVideo()}" ) # called when user seeks to a time def onPlayBackSeek(self, time: int, offset: int) -> None: if self._playing: logger.debug( - "[traktPlayer] onPlayBackSeek(time: %s, offset: %s) - %s" - % (str(time), str(offset), self.isPlayingVideo()) + f"[traktPlayer] onPlayBackSeek(time: {time!s}, offset: {offset!s}) - {self.isPlayingVideo()}" ) data = {"action": "seek", "time": time, "offset": offset} self.action(data) @@ -1017,8 +980,7 @@ def onPlayBackSeek(self, time: int, offset: int) -> None: def onPlayBackSeekChapter(self, chapter: int) -> None: if self._playing: logger.debug( - "[traktPlayer] onPlayBackSeekChapter(chapter: %s) - %s" - % (str(chapter), self.isPlayingVideo()) + f"[traktPlayer] onPlayBackSeekChapter(chapter: {chapter!s}) - {self.isPlayingVideo()}" ) data = {"action": "seekchapter", "chapter": chapter} self.action(data) diff --git a/resources/lib/sqlitequeue.py b/resources/lib/sqlitequeue.py index 443c9988..682490f5 100644 --- a/resources/lib/sqlitequeue.py +++ b/resources/lib/sqlitequeue.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- - import os import sqlite3 -from json import loads, dumps - +from json import dumps, loads from time import sleep try: @@ -11,49 +8,45 @@ except ImportError: from _dummy_thread import get_ident -import xbmcvfs -import xbmcaddon import logging -from typing import Any, Iterator, Optional, Dict +from collections.abc import Iterator +from typing import Any + +import xbmcaddon +import xbmcvfs logger = logging.getLogger(__name__) -__addon__ = xbmcaddon.Addon('script.trakt') +__addon__ = xbmcaddon.Addon("script.trakt") -# code from http://flask.pocoo.org/snippets/88/ with some modifications -class SqliteQueue(object): +# code from http://flask.pocoo.org/snippets/88/ with some modifications +class SqliteQueue: _create = ( - 'CREATE TABLE IF NOT EXISTS queue ' - '(' - ' id INTEGER PRIMARY KEY AUTOINCREMENT,' - ' item BLOB' - ')' - ) - _count = 'SELECT COUNT(*) FROM queue' - _iterate = 'SELECT id, item FROM queue' - _append = 'INSERT INTO queue (item) VALUES (?)' - _write_lock = 'BEGIN IMMEDIATE' - _get = ( - 'SELECT id, item FROM queue ' - 'ORDER BY id LIMIT 1' - ) - _del = 'DELETE FROM queue WHERE id = ?' - _peek = ( - 'SELECT item FROM queue ' - 'ORDER BY id LIMIT 1' - ) - _purge = 'DELETE FROM queue' + "CREATE TABLE IF NOT EXISTS queue " + "(" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " item BLOB" + ")" + ) + _count = "SELECT COUNT(*) FROM queue" + _iterate = "SELECT id, item FROM queue" + _append = "INSERT INTO queue (item) VALUES (?)" + _write_lock = "BEGIN IMMEDIATE" + _get = "SELECT id, item FROM queue ORDER BY id LIMIT 1" + _del = "DELETE FROM queue WHERE id = ?" + _peek = "SELECT item FROM queue ORDER BY id LIMIT 1" + _purge = "DELETE FROM queue" path: str - _connection_cache: Dict[int, sqlite3.Connection] + _connection_cache: dict[int, sqlite3.Connection] def __init__(self) -> None: self.path = xbmcvfs.translatePath(__addon__.getAddonInfo("profile")) if not xbmcvfs.exists(self.path): - logger.debug("Making path structure: %s" % repr(self.path)) + logger.debug(f"Making path structure: {self.path!r}") xbmcvfs.mkdir(self.path) - self.path = os.path.join(self.path, 'queue.db') + self.path = os.path.join(self.path, "queue.db") self._connection_cache = {} with self._get_conn() as conn: conn.execute(self._create) @@ -83,7 +76,7 @@ def append(self, obj: Any) -> None: with self._get_conn() as conn: conn.execute(self._append, (obj_buffer,)) - def get(self, sleep_wait: bool = True) -> Optional[Any]: + def get(self, sleep_wait: bool = True) -> Any | None: keep_pooling = True wait = 0.1 max_wait = 2 @@ -110,7 +103,7 @@ def get(self, sleep_wait: bool = True) -> Optional[Any]: return loads(obj_buffer) return None - def peek(self) -> Optional[Any]: + def peek(self) -> Any | None: with self._get_conn() as conn: cursor = conn.execute(self._peek) row = cursor.fetchone() diff --git a/resources/lib/sync.py b/resources/lib/sync.py index 536fdca9..47892e39 100644 --- a/resources/lib/sync.py +++ b/resources/lib/sync.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - - import logging from typing import Any import xbmc import xbmcgui + from resources.lib import syncEpisodes, syncMovies from resources.lib.kodiUtilities import getSettingAsBool @@ -13,7 +11,7 @@ logger = logging.getLogger(__name__) -class Sync(): +class Sync: traktapi: Any = None show_progress: bool = False run_silent: bool = False @@ -22,67 +20,92 @@ class Sync(): notify: bool = False notify_during_playback: bool = False - def __init__(self, show_progress: bool = False, run_silent: bool = False, library: str = "all", api: Any = None) -> None: + def __init__( + self, + show_progress: bool = False, + run_silent: bool = False, + library: str = "all", + api: Any = None, + ) -> None: self.traktapi = api self.show_progress = show_progress self.run_silent = run_silent self.library = library if self.show_progress and self.run_silent: logger.debug("Sync is being run silently.") - self.sync_on_update = getSettingAsBool('sync_on_update') - self.notify = getSettingAsBool('show_sync_notifications') - self.notify_during_playback = not getSettingAsBool("hide_notifications_playback") + self.sync_on_update = getSettingAsBool("sync_on_update") + self.notify = getSettingAsBool("show_sync_notifications") + self.notify_during_playback = not getSettingAsBool( + "hide_notifications_playback" + ) def __syncCheck(self, media_type: str) -> bool: - return self.__syncCollectionCheck(media_type) or self.__syncWatchedCheck(media_type) or self.__syncPlaybackCheck(media_type) or self.__syncRatingsCheck() + return ( + self.__syncCollectionCheck(media_type) + or self.__syncWatchedCheck(media_type) + or self.__syncPlaybackCheck(media_type) + or self.__syncRatingsCheck() + ) def __syncPlaybackCheck(self, media_type: str) -> bool: - if media_type == 'movies': - return getSettingAsBool('trakt_movie_playback') + if media_type == "movies": + return getSettingAsBool("trakt_movie_playback") else: - return getSettingAsBool('trakt_episode_playback') + return getSettingAsBool("trakt_episode_playback") def __syncCollectionCheck(self, media_type: str) -> bool: - if media_type == 'movies': - return getSettingAsBool('add_movies_to_trakt') or getSettingAsBool('clean_trakt_movies') + if media_type == "movies": + return getSettingAsBool("add_movies_to_trakt") or getSettingAsBool( + "clean_trakt_movies" + ) else: - return getSettingAsBool('add_episodes_to_trakt') or getSettingAsBool('clean_trakt_episodes') + return getSettingAsBool("add_episodes_to_trakt") or getSettingAsBool( + "clean_trakt_episodes" + ) def __syncRatingsCheck(self) -> bool: - return getSettingAsBool('trakt_sync_ratings') + return getSettingAsBool("trakt_sync_ratings") def __syncWatchedCheck(self, media_type: str) -> bool: - if media_type == 'movies': - return getSettingAsBool('trakt_movie_playcount') or getSettingAsBool('kodi_movie_playcount') + if media_type == "movies": + return getSettingAsBool("trakt_movie_playcount") or getSettingAsBool( + "kodi_movie_playcount" + ) else: - return getSettingAsBool('trakt_episode_playcount') or getSettingAsBool('kodi_episode_playcount') + return getSettingAsBool("trakt_episode_playcount") or getSettingAsBool( + "kodi_episode_playcount" + ) @property def show_notification(self) -> bool: - return not self.show_progress and self.sync_on_update and self.notify and (self.notify_during_playback or not xbmc.Player().isPlayingVideo()) + return ( + not self.show_progress + and self.sync_on_update + and self.notify + and (self.notify_during_playback or not xbmc.Player().isPlayingVideo()) + ) def sync(self) -> None: logger.debug("Starting synchronization with Trakt.tv") - if self.__syncCheck('movies'): + if self.__syncCheck("movies"): if self.library in ["all", "movies"]: syncMovies.SyncMovies(self, progress) else: - logger.debug( - "Movie sync is being skipped for this manual sync.") + logger.debug("Movie sync is being skipped for this manual sync.") else: logger.debug("Movie sync is disabled, skipping.") - if self.__syncCheck('episodes'): + if self.__syncCheck("episodes"): if self.library in ["all", "episodes"]: - if not (self.__syncCheck('movies') and self.IsCanceled()): + if not (self.__syncCheck("movies") and self.IsCanceled()): syncEpisodes.SyncEpisodes(self, progress) else: logger.debug( - "Episode sync is being skipped because movie sync was canceled.") + "Episode sync is being skipped because movie sync was canceled." + ) else: - logger.debug( - "Episode sync is being skipped for this manual sync.") + logger.debug("Episode sync is being skipped for this manual sync.") else: logger.debug("Episode sync is disabled, skipping.") @@ -97,20 +120,19 @@ def IsCanceled(self) -> bool: def UpdateProgress(self, *args: Any, **kwargs: Any) -> None: if self.show_progress and not self.run_silent: - line1 = "" line2 = "" line3 = "" - if 'line1' in kwargs: + if "line1" in kwargs: line1 = kwargs["line1"] - if 'line2' in kwargs: + if "line2" in kwargs: line2 = kwargs["line2"] - if 'line3' in kwargs: + if "line3" in kwargs: line3 = kwargs["line3"] percent = args[0] - message = f'{line1}\n{line2}\n{line3}' + message = f"{line1}\n{line2}\n{line3}" progress.update(percent, message) diff --git a/resources/lib/syncEpisodes.py b/resources/lib/syncEpisodes.py index 860abcbe..5a3c324e 100644 --- a/resources/lib/syncEpisodes.py +++ b/resources/lib/syncEpisodes.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- - import copy import logging -from typing import Dict, Tuple, Union, Optional, Any +from typing import Any from resources.lib import kodiUtilities, utilities @@ -16,14 +14,12 @@ def __init__(self, sync: Any, progress: Any) -> None: self.sync = sync if self.sync.show_notification: kodiUtilities.notification( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32050)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32050)}", kodiUtilities.getString(32061), ) # Sync started if self.sync.show_progress and not self.sync.run_silent: progress.create( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32050)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32050)}", "", ) @@ -87,8 +83,7 @@ def __init__(self, sync: Any, progress: Any) -> None: if self.sync.show_notification: kodiUtilities.notification( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32050)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32050)}", kodiUtilities.getString(32062), ) # Sync complete @@ -99,22 +94,19 @@ def __init__(self, sync: Any, progress: Any) -> None: progress.close() logger.debug( - "[Episodes Sync] Shows on Trakt.tv (%d), shows in Kodi (%d)." - % (len(traktShowsCollected["shows"]), len(kodiShowsCollected["shows"])) + "[Episodes Sync] Shows on Trakt.tv ({:d}), shows in Kodi ({:d}).".format( + len(traktShowsCollected["shows"]), len(kodiShowsCollected["shows"]) + ) ) logger.debug( - "[Episodes Sync] Episodes on Trakt.tv (%d), episodes in Kodi (%d)." - % ( - utilities.countEpisodes(traktShowsCollected), - utilities.countEpisodes(kodiShowsCollected), - ) + f"[Episodes Sync] Episodes on Trakt.tv ({utilities.countEpisodes(traktShowsCollected):d}), episodes in Kodi ({utilities.countEpisodes(kodiShowsCollected):d})." ) logger.debug("[Episodes Sync] Complete.") """ begin code for episode sync """ - def __kodiLoadShows(self) -> Tuple[Optional[Dict], Optional[Dict]]: + def __kodiLoadShows(self) -> tuple[dict | None, dict | None]: self.sync.UpdateProgress( 1, line1=kodiUtilities.getString(32094), @@ -135,7 +127,7 @@ def __kodiLoadShows(self) -> Tuple[Optional[Dict], Optional[Dict]]: return None, None tvshows = kodiUtilities.kodiRpcToTraktMediaObjects(data) - logger.debug("[Episode Sync] Getting shows from kodi finished %s" % tvshows) + logger.debug(f"[Episode Sync] Getting shows from kodi finished {tvshows}") if tvshows is None: return None, None @@ -154,8 +146,9 @@ def __kodiLoadShows(self) -> Tuple[Optional[Dict], Optional[Dict]]: if "ids" not in show_col1: logger.debug( - "[Episodes Sync] Tvshow %s has no imdbnumber or uniqueid" - % show_col1["tvshowid"] + "[Episodes Sync] Tvshow {} has no imdbnumber or uniqueid".format( + show_col1["tvshowid"] + ) ) continue @@ -191,13 +184,16 @@ def __kodiLoadShows(self) -> Tuple[Optional[Dict], Optional[Dict]]: ) if not data: logger.debug( - "[Episodes Sync] There was a problem getting episode data for '%s', aborting sync." - % show["title"] + "[Episodes Sync] There was a problem getting episode data for '{}', aborting sync.".format( + show["title"] + ) ) return None, None elif "episodes" not in data: logger.debug( - "[Episodes Sync] '%s' has no episodes in Kodi." % show["title"] + "[Episodes Sync] '{}' has no episodes in Kodi.".format( + show["title"] + ) ) continue @@ -218,7 +214,9 @@ def __kodiLoadShows(self) -> Tuple[Optional[Dict], Optional[Dict]]: self.sync.UpdateProgress(10, line2=kodiUtilities.getString(32098)) return resultCollected, resultWatched - def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[Dict, bool], Union[Dict, bool]]: + def __traktLoadShows( + self, + ) -> tuple[dict | bool, dict | bool, dict | bool, dict | bool]: self.sync.UpdateProgress( 10, line1=kodiUtilities.getString(32099), @@ -252,17 +250,15 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ ) traktEpisodesRated = list(traktEpisodesRated.items()) - except Exception: + except Exception: # noqa: BLE001 - preserve the existing broad failure boundary logger.debug( "[Episodes Sync] Invalid Trakt.tv show list, possible error getting data from Trakt, aborting Trakt.tv collection/watched/rated update." ) return False, False, False, False - i = 0 x = float(len(traktShowsCollected)) showsCollected = {"shows": []} - for _, show in traktShowsCollected: - i += 1 + for i, (_, show) in enumerate(traktShowsCollected, start=1): y = ((i / x) * 4) + 12 self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32102) % (i, x) @@ -273,11 +269,9 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ showsCollected["shows"].append(show) - i = 0 x = float(len(traktShowsWatched)) showsWatched = {"shows": []} - for _, show in traktShowsWatched: - i += 1 + for i, (_, show) in enumerate(traktShowsWatched, start=1): y = ((i / x) * 4) + 16 self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32102) % (i, x) @@ -288,11 +282,9 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ showsWatched["shows"].append(show) - i = 0 x = float(len(traktShowsRated)) showsRated = {"shows": []} - for _, show in traktShowsRated: - i += 1 + for i, (_, show) in enumerate(traktShowsRated, start=1): y = ((i / x) * 4) + 20 self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32102) % (i, x) @@ -303,11 +295,9 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ showsRated["shows"].append(show) - i = 0 x = float(len(traktEpisodesRated)) episodesRated = {"shows": []} - for _, show in traktEpisodesRated: - i += 1 + for i, (_, show) in enumerate(traktEpisodesRated, start=1): y = ((i / x) * 4) + 20 self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32102) % (i, x) @@ -322,7 +312,9 @@ def __traktLoadShows(self) -> Tuple[Union[Dict, bool], Union[Dict, bool], Union[ return showsCollected, showsWatched, showsRated, episodesRated - def __traktLoadShowsPlaybackProgress(self, fromPercent: int, toPercent: int) -> Union[Dict, bool, None]: + def __traktLoadShowsPlaybackProgress( + self, fromPercent: int, toPercent: int + ) -> dict | bool | None: if ( kodiUtilities.getSettingAsBool("trakt_episode_playback") and not self.sync.IsCanceled() @@ -336,18 +328,15 @@ def __traktLoadShowsPlaybackProgress(self, fromPercent: int, toPercent: int) -> logger.debug("[Playback Sync] Getting playback progress from Trakt.tv") try: traktProgressShows = self.sync.traktapi.getEpisodePlaybackProgress() - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary logger.debug( - "[Playback Sync] Invalid Trakt.tv progress list, possible error getting data from Trakt, aborting Trakt.tv playback update. Error: %s" - % ex + f"[Playback Sync] Invalid Trakt.tv progress list, possible error getting data from Trakt, aborting Trakt.tv playback update. Error: {ex}" ) return False - i = 0 x = float(len(traktProgressShows)) showsProgress = {"shows": []} - for show in traktProgressShows: - i += 1 + for i, (show) in enumerate(traktProgressShows, start=1): y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32120) % (i, x) @@ -363,7 +352,7 @@ def __traktLoadShowsPlaybackProgress(self, fromPercent: int, toPercent: int) -> return showsProgress def __addEpisodesToTraktCollection( - self, kodiShows: Dict, traktShows: Dict, fromPercent: int, toPercent: int + self, kodiShows: dict, traktShows: dict, fromPercent: int, toPercent: int ) -> None: if ( kodiUtilities.getSettingAsBool("add_episodes_to_trakt") @@ -392,13 +381,13 @@ def __addEpisodesToTraktCollection( ) return logger.debug( - "[Episodes Sync] %i show(s) have episodes (%d) to be added to your Trakt.tv collection." - % (len(traktShowsAdd["shows"]), utilities.countEpisodes(traktShowsAdd)) + "[Episodes Sync] {:d} show(s) have episodes ({:d}) to be added to your Trakt.tv collection.".format( + len(traktShowsAdd["shows"]), utilities.countEpisodes(traktShowsAdd) + ) ) for show in traktShowsAdd["shows"]: logger.debug( - "[Episodes Sync] Episodes added: %s" - % self.__getShowAsString(show, short=True) + f"[Episodes Sync] Episodes added: {self.__getShowAsString(show, short=True)}" ) self.sync.UpdateProgress( @@ -411,29 +400,26 @@ def __addEpisodesToTraktCollection( chunksize = 50 chunked_episodes = utilities.chunks(traktShowsAdd["shows"], chunksize) errorcount = 0 - i = 0 x = float(len(traktShowsAdd["shows"])) - for chunk in chunked_episodes: + for i, (chunk) in enumerate(chunked_episodes, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32069) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32069) % (min(x, i * chunksize), x), ) request = {"shows": chunk} - logger.debug("[traktAddEpisodes] Shows to add %s" % request) + logger.debug(f"[traktAddEpisodes] Shows to add {request}") try: self.sync.traktapi.addToCollection(request) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) errorcount += 1 - logger.debug("[traktAddEpisodes] Finished with %d error(s)" % errorcount) + logger.debug(f"[traktAddEpisodes] Finished with {errorcount:d} error(s)") self.sync.UpdateProgress( toPercent, line2=kodiUtilities.getString(32105) @@ -441,7 +427,7 @@ def __addEpisodesToTraktCollection( ) def __deleteEpisodesFromTraktCollection( - self, traktShows: Dict, kodiShows: Dict, fromPercent: int, toPercent: int + self, traktShows: dict, kodiShows: dict, fromPercent: int, toPercent: int ) -> None: if ( kodiUtilities.getSettingAsBool("clean_trakt_episodes") @@ -469,13 +455,13 @@ def __deleteEpisodesFromTraktCollection( return logger.debug( - "[Episodes Sync] %i show(s) will have episodes removed from Trakt.tv collection." - % len(traktShowsRemove["shows"]) + "[Episodes Sync] {:d} show(s) will have episodes removed from Trakt.tv collection.".format( + len(traktShowsRemove["shows"]) + ) ) for show in traktShowsRemove["shows"]: logger.debug( - "[Episodes Sync] Episodes removed: %s" - % self.__getShowAsString(show, short=True) + f"[Episodes Sync] Episodes removed: {self.__getShowAsString(show, short=True)}" ) self.sync.UpdateProgress( @@ -485,10 +471,10 @@ def __deleteEpisodesFromTraktCollection( % utilities.countEpisodes(traktShowsRemove), ) - logger.debug("[traktRemoveEpisodes] Shows to remove %s" % traktShowsRemove) + logger.debug(f"[traktRemoveEpisodes] Shows to remove {traktShowsRemove}") try: self.sync.traktapi.removeFromCollection(traktShowsRemove) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) @@ -499,7 +485,7 @@ def __deleteEpisodesFromTraktCollection( ) def __addEpisodesToTraktWatched( - self, kodiShows: Dict, traktShows: Dict, fromPercent: int, toPercent: int + self, kodiShows: dict, traktShows: dict, fromPercent: int, toPercent: int ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_episode_playcount") @@ -529,13 +515,13 @@ def __addEpisodesToTraktWatched( return logger.debug( - "[Episodes Sync] %i show(s) are missing playcounts on Trakt.tv" - % len(traktShowsUpdate["shows"]) + "[Episodes Sync] {:d} show(s) are missing playcounts on Trakt.tv".format( + len(traktShowsUpdate["shows"]) + ) ) for show in traktShowsUpdate["shows"]: logger.debug( - "[Episodes Sync] Episodes updated: %s" - % self.__getShowAsString(show, short=True) + f"[Episodes Sync] Episodes updated: {self.__getShowAsString(show, short=True)}" ) self.sync.UpdateProgress( @@ -544,29 +530,27 @@ def __addEpisodesToTraktWatched( line2=kodiUtilities.getString(32070) % (len(traktShowsUpdate["shows"])), ) errorcount = 0 - i = 0 x = float(len(traktShowsUpdate["shows"])) - for show in traktShowsUpdate["shows"]: + for i, (show) in enumerate(traktShowsUpdate["shows"], start=1): if self.sync.IsCanceled(): return epCount = utilities.countEpisodes([show]) title = show["title"] - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line2=title, line3=kodiUtilities.getString(32073) % epCount ) s = {"shows": [show]} - logger.debug("[traktUpdateEpisodes] Shows to update %s" % s) + logger.debug(f"[traktUpdateEpisodes] Shows to update {s}") try: self.sync.traktapi.addToHistory(s) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) errorcount += 1 - logger.debug("[traktUpdateEpisodes] Finished with %d error(s)" % errorcount) + logger.debug(f"[traktUpdateEpisodes] Finished with {errorcount:d} error(s)") self.sync.UpdateProgress( toPercent, line2=kodiUtilities.getString(32072) % (len(traktShowsUpdate["shows"])), @@ -574,7 +558,12 @@ def __addEpisodesToTraktWatched( ) def __addEpisodesToKodiWatched( - self, traktShows: Dict, kodiShows: Dict, kodiShowsCollected: Dict, fromPercent: int, toPercent: int + self, + traktShows: dict, + kodiShows: dict, + kodiShowsCollected: dict, + fromPercent: int, + toPercent: int, ) -> None: if ( kodiUtilities.getSettingAsBool("kodi_episode_playcount") @@ -602,14 +591,15 @@ def __addEpisodesToKodiWatched( return logger.debug( - "[Episodes Sync] %i show(s) shows are missing playcounts on Kodi" - % len(kodiShowsUpdate["shows"]) + "[Episodes Sync] {:d} show(s) shows are missing playcounts on Kodi".format( + len(kodiShowsUpdate["shows"]) + ) ) for s in [ - "%s" % self.__getShowAsString(s, short=True) + f"{self.__getShowAsString(s, short=True)}" for s in kodiShowsUpdate["shows"] ]: - logger.debug("[Episodes Sync] Episodes updated: %s" % s) + logger.debug(f"[Episodes Sync] Episodes updated: {s}") # logger.debug("kodiShowsUpdate: %s" % kodiShowsUpdate) episodes = [] @@ -640,28 +630,27 @@ def __addEpisodesToKodiWatched( ], chunksize, ) - i = 0 x = float(len(episodes)) - for chunk in chunked_episodes: + for i, (chunk) in enumerate(chunked_episodes, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32108) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32108) % (min(x, i * chunksize), x), ) - logger.debug("[Episodes Sync] chunk %s" % str(chunk)) + logger.debug(f"[Episodes Sync] chunk {chunk!s}") result = kodiUtilities.kodiJsonRequest(chunk) - logger.debug("[Episodes Sync] result %s" % str(result)) + logger.debug(f"[Episodes Sync] result {result!s}") self.sync.UpdateProgress( toPercent, line2=kodiUtilities.getString(32109) % len(episodes) ) - def __addEpisodeProgressToKodi(self, traktShows: Dict, kodiShows: Dict, fromPercent: int, toPercent: int) -> None: + def __addEpisodeProgressToKodi( + self, traktShows: dict, kodiShows: dict, fromPercent: int, toPercent: int + ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_episode_playback") and traktShows @@ -687,14 +676,15 @@ def __addEpisodeProgressToKodi(self, traktShows: Dict, kodiShows: Dict, fromPerc return logger.debug( - "[Episodes Sync] %i show(s) shows are missing progress in Kodi" - % len(kodiShowsUpdate["shows"]) + "[Episodes Sync] {:d} show(s) shows are missing progress in Kodi".format( + len(kodiShowsUpdate["shows"]) + ) ) for s in [ - "%s" % self.__getShowAsString(s, short=True) + f"{self.__getShowAsString(s, short=True)}" for s in kodiShowsUpdate["shows"] ]: - logger.debug("[Episodes Sync] Episodes updated: %s" % s) + logger.debug(f"[Episodes Sync] Episodes updated: {s}") episodes = [] for show in kodiShowsUpdate["shows"]: @@ -744,17 +734,14 @@ def __addEpisodeProgressToKodi(self, traktShows: Dict, kodiShows: Dict, fromPerc ], chunksize, ) - i = 0 x = float(len(episodes)) - for chunk in chunked_episodes: + for i, (chunk) in enumerate(chunked_episodes, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32130) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32130) % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) @@ -763,7 +750,9 @@ def __addEpisodeProgressToKodi(self, traktShows: Dict, kodiShows: Dict, fromPerc toPercent, line2=kodiUtilities.getString(32131) % len(episodes) ) - def __syncShowsRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int, toPercent: int) -> None: + def __syncShowsRatings( + self, traktShows: dict, kodiShows: dict, fromPercent: int, toPercent: int + ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_sync_ratings") and traktShows @@ -785,8 +774,9 @@ def __syncShowsRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int logger.debug("[Episodes Sync] Trakt show ratings are up to date.") else: logger.debug( - "[Episodes Sync] %i show(s) will have show ratings added on Trakt" - % len(traktShowsToUpdate["shows"]) + "[Episodes Sync] {:d} show(s) will have show ratings added on Trakt".format( + len(traktShowsToUpdate["shows"]) + ) ) self.sync.UpdateProgress( @@ -814,8 +804,9 @@ def __syncShowsRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int logger.debug("[Episodes Sync] Kodi show ratings are up to date.") else: logger.debug( - "[Episodes Sync] %i show(s) will have show ratings added in Kodi" - % len(kodiShowsUpdate["shows"]) + "[Episodes Sync] {:d} show(s) will have show ratings added in Kodi".format( + len(kodiShowsUpdate["shows"]) + ) ) shows = [] @@ -841,18 +832,16 @@ def __syncShowsRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int ], chunksize, ) - i = 0 x = float(len(shows)) - for chunk in chunked_episodes: + for i, (chunk) in enumerate(chunked_episodes, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line1="", line2=kodiUtilities.getString(32177) - % ((i) * chunksize if (i) * chunksize < x else x, x), + % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) @@ -861,7 +850,9 @@ def __syncShowsRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int toPercent, line2=kodiUtilities.getString(32178) % len(shows) ) - def __syncEpisodeRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: int, toPercent: int) -> None: + def __syncEpisodeRatings( + self, traktShows: dict, kodiShows: dict, fromPercent: int, toPercent: int + ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_sync_ratings") and traktShows @@ -883,8 +874,9 @@ def __syncEpisodeRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: i logger.debug("[Episodes Sync] Trakt episode ratings are up to date.") else: logger.debug( - "[Episodes Sync] %i show(s) will have episode ratings added on Trakt" - % len(traktShowsToUpdate["shows"]) + "[Episodes Sync] {:d} show(s) will have episode ratings added on Trakt".format( + len(traktShowsToUpdate["shows"]) + ) ) self.sync.UpdateProgress( @@ -909,14 +901,15 @@ def __syncEpisodeRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: i logger.debug("[Episodes Sync] Kodi episode ratings are up to date.") else: logger.debug( - "[Episodes Sync] %i show(s) will have episode ratings added in Kodi" - % len(kodiShowsUpdate["shows"]) + "[Episodes Sync] {:d} show(s) will have episode ratings added in Kodi".format( + len(kodiShowsUpdate["shows"]) + ) ) for s in [ - "%s" % self.__getShowAsString(s, short=True) + f"{self.__getShowAsString(s, short=True)}" for s in kodiShowsUpdate["shows"] ]: - logger.debug("[Episodes Sync] Episodes updated: %s" % s) + logger.debug(f"[Episodes Sync] Episodes updated: {s}") episodes = [] for show in kodiShowsUpdate["shows"]: @@ -946,18 +939,16 @@ def __syncEpisodeRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: i ], chunksize, ) - i = 0 x = float(len(episodes)) - for chunk in chunked_episodes: + for i, (chunk) in enumerate(chunked_episodes, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line1="", line2=kodiUtilities.getString(32174) - % ((i) * chunksize if (i) * chunksize < x else x, x), + % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) @@ -966,7 +957,7 @@ def __syncEpisodeRatings(self, traktShows: Dict, kodiShows: Dict, fromPercent: i toPercent, line2=kodiUtilities.getString(32175) % len(episodes) ) - def __getShowAsString(self, show: Dict, short: bool = False) -> str: + def __getShowAsString(self, show: dict, short: bool = False) -> str: p = [] if "seasons" in show: for season in show["seasons"]: @@ -974,7 +965,7 @@ def __getShowAsString(self, show: Dict, short: bool = False) -> str: if short: s = ", ".join( [ - "S%02dE%02d" % (season["number"], i["number"]) + "S{:02d}E{:02d}".format(season["number"], i["number"]) for i in season["episodes"] ] ) @@ -982,15 +973,15 @@ def __getShowAsString(self, show: Dict, short: bool = False) -> str: episodes = ", ".join( [str(i) for i in show["shows"]["seasons"][season]] ) - s = "Season: %d, Episodes: %s" % (season, episodes) + s = f"Season: {season:d}, Episodes: {episodes:s}" p.append(s) else: p = ["All"] if "tvdb" in show["ids"]: - return "%s [tvdb: %s] - %s" % ( + return "{} [tvdb: {}] - {}".format( show["title"], show["ids"]["tvdb"], ", ".join(p), ) else: - return "%s [tvdb: No id] - %s" % (show["title"], ", ".join(p)) + return "{} [tvdb: No id] - {}".format(show["title"], ", ".join(p)) diff --git a/resources/lib/syncMovies.py b/resources/lib/syncMovies.py index c0323a96..114b6875 100644 --- a/resources/lib/syncMovies.py +++ b/resources/lib/syncMovies.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- - import copy import logging -from typing import Dict, List, Optional, Any, Union +from typing import Any from resources.lib import kodiUtilities, utilities @@ -16,14 +14,12 @@ def __init__(self, sync: Any, progress: Any) -> None: self.sync = sync if self.sync.show_notification: kodiUtilities.notification( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32046)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32046)}", kodiUtilities.getString(32061), ) # Sync started if sync.show_progress and not sync.run_silent: progress.create( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32046)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32046)}", "", ) @@ -35,7 +31,7 @@ def __init__(self, sync: Any, progress: Any) -> None: return try: traktMovies = self.__traktLoadMovies() - except Exception: + except Exception: # noqa: BLE001 - preserve the existing broad failure boundary logger.debug( "[Movies Sync] Error getting Trakt.tv movie list, aborting movie Sync." ) @@ -65,18 +61,16 @@ def __init__(self, sync: Any, progress: Any) -> None: if self.sync.show_notification: kodiUtilities.notification( - "%s %s" - % (kodiUtilities.getString(32045), kodiUtilities.getString(32046)), + f"{kodiUtilities.getString(32045)} {kodiUtilities.getString(32046)}", kodiUtilities.getString(32062), ) # Sync complete logger.debug( - "[Movies Sync] Movies on Trakt.tv (%d), movies in Kodi (%d)." - % (len(traktMovies), len(kodiMovies)) + f"[Movies Sync] Movies on Trakt.tv ({len(traktMovies):d}), movies in Kodi ({len(kodiMovies):d})." ) logger.debug("[Movies Sync] Complete.") - def __kodiLoadMovies(self) -> Optional[List[Dict]]: + def __kodiLoadMovies(self) -> list[dict] | None: self.sync.UpdateProgress(1, line2=kodiUtilities.getString(32079)) logger.debug("[Movies Sync] Getting movie data from Kodi") @@ -111,7 +105,7 @@ def __kodiLoadMovies(self) -> Optional[List[Dict]]: return kodi_movies - def __traktLoadMovies(self) -> List[Dict]: + def __traktLoadMovies(self) -> list[dict]: self.sync.UpdateProgress( 10, line1=kodiUtilities.getString(32079), @@ -140,7 +134,9 @@ def __traktLoadMovies(self) -> List[Dict]: return movies - def __traktLoadMoviesPlaybackProgress(self, fromPercent: int, toPercent: int) -> Union[Dict, bool]: + def __traktLoadMoviesPlaybackProgress( + self, fromPercent: int, toPercent: int + ) -> dict | bool: if ( kodiUtilities.getSettingAsBool("trakt_movie_playback") and not self.sync.IsCanceled() @@ -150,17 +146,15 @@ def __traktLoadMoviesPlaybackProgress(self, fromPercent: int, toPercent: int) -> logger.debug("[Movies Sync] Getting playback progress from Trakt.tv") try: traktProgressMovies = self.sync.traktapi.getMoviePlaybackProgress() - except Exception: + except Exception: # noqa: BLE001 - preserve the existing broad failure boundary logger.debug( "[Movies Sync] Invalid Trakt.tv playback progress list, possible error getting data from Trakt, aborting Trakt.tv playback update." ) return False - i = 0 x = float(len(traktProgressMovies)) moviesProgress = {"movies": []} - for movie in traktProgressMovies: - i += 1 + for i, (movie) in enumerate(traktProgressMovies, start=1): y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32123) % (i, x) @@ -176,7 +170,11 @@ def __traktLoadMoviesPlaybackProgress(self, fromPercent: int, toPercent: int) -> return moviesProgress def __addMoviesToTraktCollection( - self, kodiMovies: List[Dict], traktMovies: List[Dict], fromPercent: int, toPercent: int + self, + kodiMovies: list[dict], + traktMovies: list[dict], + fromPercent: int, + toPercent: int, ) -> None: if ( kodiUtilities.getSettingAsBool("add_movies_to_trakt") @@ -192,8 +190,7 @@ def __addMoviesToTraktCollection( ) utilities.sanitizeMovies(traktMoviesToAdd) logger.debug( - "[Movies Sync] Compared movies, found %s to add." - % len(traktMoviesToAdd) + f"[Movies Sync] Compared movies, found {len(traktMoviesToAdd)} to add." ) if len(traktMoviesToAdd) == 0: @@ -203,12 +200,11 @@ def __addMoviesToTraktCollection( logger.debug("[Movies Sync] Trakt.tv movie collection is up to date.") return - titles = ", ".join(["%s" % (m["title"]) for m in traktMoviesToAdd]) + titles = ", ".join(["{}".format(m["title"]) for m in traktMoviesToAdd]) logger.debug( - "[Movies Sync] %i movie(s) will be added to Trakt.tv collection." - % len(traktMoviesToAdd) + f"[Movies Sync] {len(traktMoviesToAdd):d} movie(s) will be added to Trakt.tv collection." ) - logger.debug("[Movies Sync] Movies to add : %s" % titles) + logger.debug(f"[Movies Sync] Movies to add : {titles}") self.sync.UpdateProgress( fromPercent, @@ -219,7 +215,7 @@ def __addMoviesToTraktCollection( # logger.debug("Movies to add: %s" % moviesToAdd) try: self.sync.traktapi.addToCollection(moviesToAdd) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) @@ -228,7 +224,11 @@ def __addMoviesToTraktCollection( ) def __deleteMoviesFromTraktCollection( - self, traktMovies: List[Dict], kodiMovies: List[Dict], fromPercent: int, toPercent: int + self, + traktMovies: list[dict], + kodiMovies: list[dict], + fromPercent: int, + toPercent: int, ) -> None: if ( kodiUtilities.getSettingAsBool("clean_trakt_movies") @@ -245,8 +245,7 @@ def __deleteMoviesFromTraktCollection( ) utilities.sanitizeMovies(traktMoviesToRemove) logger.debug( - "[Movies Sync] Compared movies, found %s to remove." - % len(traktMoviesToRemove) + f"[Movies Sync] Compared movies, found {len(traktMoviesToRemove)} to remove." ) if len(traktMoviesToRemove) == 0: @@ -258,12 +257,11 @@ def __deleteMoviesFromTraktCollection( ) return - titles = ", ".join(["%s" % (m["title"]) for m in traktMoviesToRemove]) + titles = ", ".join(["{}".format(m["title"]) for m in traktMoviesToRemove]) logger.debug( - "[Movies Sync] %i movie(s) will be removed from Trakt.tv collection." - % len(traktMoviesToRemove) + f"[Movies Sync] {len(traktMoviesToRemove):d} movie(s) will be removed from Trakt.tv collection." ) - logger.debug("[Movies Sync] Movies removed: %s" % titles) + logger.debug(f"[Movies Sync] Movies removed: {titles}") self.sync.UpdateProgress( fromPercent, @@ -273,7 +271,7 @@ def __deleteMoviesFromTraktCollection( moviesToRemove = {"movies": traktMoviesToRemove} try: self.sync.traktapi.removeFromCollection(moviesToRemove) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) @@ -283,7 +281,11 @@ def __deleteMoviesFromTraktCollection( ) def __addMoviesToTraktWatched( - self, kodiMovies: List[Dict], traktMovies: List[Dict], fromPercent: int, toPercent: int + self, + kodiMovies: list[dict], + traktMovies: list[dict], + fromPercent: int, + toPercent: int, ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_movie_playcount") @@ -307,12 +309,11 @@ def __addMoviesToTraktWatched( logger.debug("[Movies Sync] Trakt.tv movie playcount is up to date") return - titles = ", ".join(["%s" % (m["title"]) for m in traktMoviesToUpdate]) + titles = ", ".join(["{}".format(m["title"]) for m in traktMoviesToUpdate]) logger.debug( - "[Movies Sync] %i movie(s) playcount will be updated on Trakt.tv" - % len(traktMoviesToUpdate) + f"[Movies Sync] {len(traktMoviesToUpdate):d} movie(s) playcount will be updated on Trakt.tv" ) - logger.debug("[Movies Sync] Movies updated: %s" % titles) + logger.debug(f"[Movies Sync] Movies updated: {titles}") self.sync.UpdateProgress( fromPercent, @@ -324,35 +325,38 @@ def __addMoviesToTraktWatched( [movie for movie in traktMoviesToUpdate], chunksize ) errorcount = 0 - i = 0 x = float(len(traktMoviesToUpdate)) - for chunk in chunked_movies: + for i, (chunk) in enumerate(chunked_movies, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32093) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32093) % (min(x, i * chunksize), x), ) params = {"movies": chunk} # logger.debug("moviechunk: %s" % params) try: self.sync.traktapi.addToHistory(params) - except Exception as ex: + except Exception as ex: # noqa: BLE001 - preserve the existing broad failure boundary message = utilities.createError(ex) logging.fatal(message) errorcount += 1 - logger.debug("[Movies Sync] Movies updated: %d error(s)" % errorcount) + logger.debug(f"[Movies Sync] Movies updated: {errorcount:d} error(s)") self.sync.UpdateProgress( toPercent, line2=kodiUtilities.getString(32087) % len(traktMoviesToUpdate), ) - def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dict], fromPercent: int, toPercent: int) -> None: + def __addMoviesToKodiWatched( + self, + traktMovies: list[dict], + kodiMovies: list[dict], + fromPercent: int, + toPercent: int, + ) -> None: if ( kodiUtilities.getSettingAsBool("kodi_movie_playcount") and not self.sync.IsCanceled() @@ -375,12 +379,11 @@ def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dic logger.debug("[Movies Sync] Kodi movie playcount is up to date.") return - titles = ", ".join(["%s" % (m["title"]) for m in kodiMoviesToUpdate]) + titles = ", ".join(["{}".format(m["title"]) for m in kodiMoviesToUpdate]) logger.debug( - "[Movies Sync] %i movie(s) playcount will be updated in Kodi" - % len(kodiMoviesToUpdate) + f"[Movies Sync] {len(kodiMoviesToUpdate):d} movie(s) playcount will be updated in Kodi" ) - logger.debug("[Movies Sync] Movies to add: %s" % titles) + logger.debug(f"[Movies Sync] Movies to add: {titles}") self.sync.UpdateProgress( fromPercent, @@ -407,17 +410,14 @@ def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dic ], chunksize, ) - i = 0 x = float(len(kodiMoviesToUpdate)) - for chunk in chunked_movies: + for i, (chunk) in enumerate(chunked_movies, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32089) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32089) % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) @@ -427,7 +427,13 @@ def __addMoviesToKodiWatched(self, traktMovies: List[Dict], kodiMovies: List[Dic line2=kodiUtilities.getString(32090) % len(kodiMoviesToUpdate), ) - def __addMovieProgressToKodi(self, traktMovies: Dict, kodiMovies: List[Dict], fromPercent: int, toPercent: int) -> None: + def __addMovieProgressToKodi( + self, + traktMovies: dict, + kodiMovies: list[dict], + fromPercent: int, + toPercent: int, + ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_movie_playback") and traktMovies @@ -451,8 +457,7 @@ def __addMovieProgressToKodi(self, traktMovies: Dict, kodiMovies: List[Dict], fr return logger.debug( - "[Movies Sync] %i movie(s) progress will be updated in Kodi" - % len(kodiMoviesToUpdate) + f"[Movies Sync] {len(kodiMoviesToUpdate):d} movie(s) progress will be updated in Kodi" ) self.sync.UpdateProgress( @@ -494,17 +499,14 @@ def __addMovieProgressToKodi(self, traktMovies: Dict, kodiMovies: List[Dict], fr ], chunksize, ) - i = 0 x = float(len(kodiMoviesToUpdate)) - for chunk in chunked_movies: + for i, (chunk) in enumerate(chunked_movies, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), - line2=kodiUtilities.getString(32127) - % ((i) * chunksize if (i) * chunksize < x else x, x), + line2=kodiUtilities.getString(32127) % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) @@ -513,7 +515,13 @@ def __addMovieProgressToKodi(self, traktMovies: Dict, kodiMovies: List[Dict], fr line2=kodiUtilities.getString(32128) % len(kodiMoviesToUpdate), ) - def __syncMovieRatings(self, traktMovies: List[Dict], kodiMovies: List[Dict], fromPercent: int, toPercent: int) -> None: + def __syncMovieRatings( + self, + traktMovies: list[dict], + kodiMovies: list[dict], + fromPercent: int, + toPercent: int, + ) -> None: if ( kodiUtilities.getSettingAsBool("trakt_sync_ratings") and traktMovies @@ -535,8 +543,7 @@ def __syncMovieRatings(self, traktMovies: List[Dict], kodiMovies: List[Dict], fr logger.debug("[Movies Sync] Trakt movie ratings are up to date.") else: logger.debug( - "[Movies Sync] %i movie(s) ratings will be updated on Trakt" - % len(traktMoviesToUpdate) + f"[Movies Sync] {len(traktMoviesToUpdate):d} movie(s) ratings will be updated on Trakt" ) self.sync.UpdateProgress( @@ -563,8 +570,7 @@ def __syncMovieRatings(self, traktMovies: List[Dict], kodiMovies: List[Dict], fr logger.debug("[Movies Sync] Kodi movie ratings are up to date.") else: logger.debug( - "[Movies Sync] %i movie(s) ratings will be updated in Kodi" - % len(kodiMoviesToUpdate) + f"[Movies Sync] {len(kodiMoviesToUpdate):d} movie(s) ratings will be updated in Kodi" ) self.sync.UpdateProgress( @@ -589,17 +595,15 @@ def __syncMovieRatings(self, traktMovies: List[Dict], kodiMovies: List[Dict], fr ], chunksize, ) - i = 0 x = float(len(kodiMoviesToUpdate)) - for chunk in chunked_movies: + for i, (chunk) in enumerate(chunked_movies, start=1): if self.sync.IsCanceled(): return - i += 1 y = ((i / x) * (toPercent - fromPercent)) + fromPercent self.sync.UpdateProgress( int(y), line2=kodiUtilities.getString(32171) - % ((i) * chunksize if (i) * chunksize < x else x, x), + % (min(x, i * chunksize), x), ) kodiUtilities.kodiJsonRequest(chunk) diff --git a/resources/lib/traktContextMenu.py b/resources/lib/traktContextMenu.py index 792a0b2e..c7ddd1e5 100644 --- a/resources/lib/traktContextMenu.py +++ b/resources/lib/traktContextMenu.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- +from typing import Any import xbmcaddon import xbmcgui -from typing import List, Optional, Any -from resources.lib.utilities import isMovie, isShow, isSeason, isEpisode + from resources.lib.kodiUtilities import getString +from resources.lib.utilities import isEpisode, isMovie, isSeason, isShow __addon__ = xbmcaddon.Addon("script.trakt") @@ -20,12 +20,14 @@ class traktContextMenu(xbmcgui.WindowXMLDialog): - action: Optional[str] = None - buttons: List[str] + action: str | None = None + buttons: list[str] media_type: str - def __new__(cls, media_type: Optional[str] = None, buttons: Optional[List[str]] = None) -> Any: - return super(traktContextMenu, cls).__new__( + def __new__( + cls, media_type: str | None = None, buttons: list[str] | None = None + ) -> Any: + return super().__new__( cls, "script-trakt-ContextMenu.xml", __addon__.getAddonInfo("path"), @@ -36,7 +38,7 @@ def __new__(cls, media_type: Optional[str] = None, buttons: Optional[List[str]] def __init__(self, *args: Any, **kwargs: Any) -> None: self.buttons = kwargs["buttons"] self.media_type = kwargs["media_type"] - super(traktContextMenu, self).__init__() + super().__init__() def onInit(self) -> None: mange_string = ( @@ -78,17 +80,18 @@ def onInit(self) -> None: self.setFocus(actionList) - def newListItem(self, label: str, selected: bool = False, *args: Any, **kwargs: Any) -> xbmcgui.ListItem: + def newListItem( + self, label: str, selected: bool = False, *args: Any, **kwargs: Any + ) -> xbmcgui.ListItem: item = xbmcgui.ListItem(label) item.select(selected) - for key in kwargs: - item.setProperty(key, str(kwargs[key])) + for key, value in kwargs.items(): + item.setProperty(key, str(value)) return item def onAction(self, action: xbmcgui.Action) -> None: - if action.getId() not in ACTION_ITEM_SELECT: - if action in ACTION_CLOSE_LIST: - self.close() + if action.getId() not in ACTION_ITEM_SELECT and action in ACTION_CLOSE_LIST: + self.close() if action in ACTION_ITEM_SELECT: cID = self.getFocusId() diff --git a/resources/lib/traktapi.py b/resources/lib/traktapi.py index 062f7bc3..7c6e7830 100644 --- a/resources/lib/traktapi.py +++ b/resources/lib/traktapi.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- -# import logging import os -import socket import threading import time -from json import dumps, loads -from typing import Any, Dict, Iterable, List, Optional import urllib.error import urllib.parse import urllib.request +from collections.abc import Iterable +from json import dumps, loads +from typing import Any, Optional import xbmcaddon + from resources.lib import deviceAuthDialog from resources.lib.kodiUtilities import ( checkAndConfigureProxy, @@ -21,13 +20,13 @@ notification, setSetting, ) +from resources.lib.obfuscation import deobfuscate from resources.lib.utilities import ( findEpisodeMatchInList, findMovieMatchInList, findSeasonMatchInList, findShowMatchInList, ) -from resources.lib.obfuscation import deobfuscate # read settings __addon__ = xbmcaddon.Addon("script.trakt") @@ -36,12 +35,12 @@ logger = logging.getLogger(__name__) -class TraktObject(object): +class TraktObject: def __init__( self, - data: Optional[Dict] = None, + data: dict | None = None, show: Optional["TraktObject"] = None, - keys: Optional[List] = None, + keys: list | None = None, ) -> None: self._data = data or {} self._keys = keys @@ -49,7 +48,7 @@ def __init__( self.pk = (self._data.get("season"), self._data.get("number")) self.episodes = self._build_episodes() - def _build_episodes(self) -> Dict: + def _build_episodes(self) -> dict: episodes = {} for season in self._data.get("seasons", []) or []: for episode in season.get("episodes", []) or []: @@ -65,20 +64,20 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) @property - def keys(self) -> List: + def keys(self) -> list: if self._keys is not None: return self._keys return list((self._data.get("ids") or {}).items()) - def to_dict(self) -> Dict: + def to_dict(self) -> dict: return dict(self._data) def __repr__(self) -> str: return repr(self._data) -class TraktSeason(object): - def __init__(self, data: Dict) -> None: +class TraktSeason: + def __init__(self, data: dict) -> None: self._data = data self.number = data.get("number") self.episodes = {} @@ -93,7 +92,7 @@ def __repr__(self) -> str: return repr(self._data) -class TraktClient(object): +class TraktClient: api_url = "https://api.trakt.tv" max_retry_after = 60 @@ -102,7 +101,7 @@ def __init__( client_id: str, client_secret: str, user_agent: str, - proxy_url: Optional[str] = None, + proxy_url: str | None = None, ) -> None: self.client_id = client_id self.client_secret = client_secret @@ -113,12 +112,13 @@ def request( self, method: str, path: str, - body: Optional[Dict] = None, - authorization: Optional[Dict] = None, + body: dict | None = None, + authorization: dict | None = None, timeout: int = 30, retry: bool = True, include_headers: bool = False, include_error_code: bool = False, + base_url: str | None = None, ) -> Any: url = self.api_url + path data = dumps(body).encode("utf-8") if body is not None else None @@ -129,7 +129,7 @@ def request( "User-Agent": self.user_agent, } if authorization and authorization.get("access_token"): - headers["Authorization"] = "Bearer %s" % authorization["access_token"] + headers["Authorization"] = "Bearer {}".format(authorization["access_token"]) request = urllib.request.Request(url, data=data, headers=headers, method=method) opener = urllib.request.build_opener() @@ -154,8 +154,7 @@ def request( retry_after = self._retry_after(exc) if retry_after is not None: logger.debug( - "Trakt rate limit reached: retrying %s %s after %s seconds" - % (method, path, retry_after) + f"Trakt rate limit reached: retrying {method} {path} after {retry_after} seconds" ) time.sleep(retry_after) return self.request( @@ -170,17 +169,17 @@ def request( ) if exc.code == 401 and authorization and authorization.get("refresh_token"): raise - logger.debug("Trakt request failed: %s %s -> %s" % (method, path, exc.code)) + logger.debug(f"Trakt request failed: {method} {path} -> {exc.code}") if include_error_code: return None, exc.code return None - except (urllib.error.URLError, TimeoutError, socket.timeout) as exc: - logger.debug("Trakt request failed: %s %s -> %s" % (method, path, exc)) + except (urllib.error.URLError, TimeoutError) as exc: + logger.debug(f"Trakt request failed: {method} {path} -> {exc}") if include_error_code: return None, None return None - def _retry_after(self, exc: urllib.error.HTTPError) -> Optional[int]: + def _retry_after(self, exc: urllib.error.HTTPError) -> int | None: try: retry_after = int(exc.headers.get("Retry-After")) except (TypeError, ValueError): @@ -190,23 +189,23 @@ def _retry_after(self, exc: urllib.error.HTTPError) -> Optional[int]: return None return retry_after - def build_path(self, path: str, params: Optional[Dict] = None) -> str: + def build_path(self, path: str, params: dict | None = None) -> str: if not params: return path clean_params = {k: v for k, v in params.items() if v is not None} if not clean_params: return path separator = "&" if "?" in path else "?" - return "%s%s%s" % (path, separator, urllib.parse.urlencode(clean_params)) + return f"{path}{separator}{urllib.parse.urlencode(clean_params)}" -class traktAPI(object): +class traktAPI: # Placeholders for build-time injection __client_id: str = "TRAKT_CLIENT_ID_PLACEHOLDER" __client_secret: str = "TRAKT_CLIENT_SECRET_PLACEHOLDER" - authorization: Optional[Dict] = None - authDialog: Optional[deviceAuthDialog.DeviceAuthDialog] = None - client: Optional[TraktClient] = None + authorization: dict | None = None + authDialog: deviceAuthDialog.DeviceAuthDialog | None = None + client: TraktClient | None = None def __init__(self, force: bool = False) -> None: logger.debug("Initializing.") @@ -221,7 +220,7 @@ def __init__(self, force: bool = False) -> None: client_id = deobfuscate(self.__client_id) client_secret = deobfuscate(self.__client_secret) - user_agent = "Kodi script.trakt/%s" % __addonversion__ + user_agent = f"Kodi script.trakt/{__addonversion__}" self.client = TraktClient(client_id, client_secret, user_agent, proxyURL) if getSetting("authorization") and not force: @@ -250,8 +249,9 @@ def login(self) -> None: return logger.debug( - 'Enter the code "%s" at %s to authenticate your account' - % (code.get("user_code"), code.get("verification_url")) + 'Enter the code "{}" at {} to authenticate your account'.format( + code.get("user_code"), code.get("verification_url") + ) ) self.authDialog = deviceAuthDialog.DeviceAuthDialog( @@ -268,7 +268,7 @@ def login(self) -> None: del self.authDialog - def _poll_device_token(self, code: Dict) -> None: + def _poll_device_token(self, code: dict) -> None: if not self.client: return started_at = int(time.time()) @@ -297,9 +297,7 @@ def _poll_device_token(self, code: Dict) -> None: time.sleep(interval) continue if error_code in (404, 409, 410, 418): - logger.debug( - "Device authentication stopped with status %s" % error_code - ) + logger.debug(f"Device authentication stopped with status {error_code}") self.on_expired() return time.sleep(interval) @@ -309,7 +307,7 @@ def _request( self, method: str, path: str, - body: Optional[Dict] = None, + body: dict | None = None, authorized: bool = False, timeout: int = 30, include_headers: bool = False, @@ -361,7 +359,7 @@ def _request( retry=False, include_headers=include_headers, ) - logger.debug("Trakt request failed: %s %s -> %s" % (method, path, exc.code)) + logger.debug(f"Trakt request failed: {method} {path} -> {exc.code}") return None def _get( @@ -384,7 +382,7 @@ def _get( def _post( self, path: str, - body: Dict, + body: dict, authorized: bool = True, timeout: int = 30, retry: bool = True, @@ -404,8 +402,8 @@ def _get_all_pages( authorized: bool = False, timeout: int = 90, limit: int = 100, - params: Optional[Dict] = None, - ) -> List: + params: dict | None = None, + ) -> list: if not self.client: return [] @@ -441,12 +439,12 @@ def _get_all_pages( return results - def _get_all_ratings(self, media_type: str) -> List: + def _get_all_ratings(self, media_type: str) -> list: results = [] for rating in range(1, 11): results.extend( self._get_all_pages( - "/sync/ratings/%s/%s" % (media_type, rating), + f"/sync/ratings/{media_type}/{rating}", authorized=True, timeout=90, ) @@ -454,7 +452,7 @@ def _get_all_ratings(self, media_type: str) -> List: return results def _merge_object( - self, store: Dict, item: Dict, media_key: str, metadata_keys: Iterable[str] + self, store: dict, item: dict, media_key: str, metadata_keys: Iterable[str] ) -> None: media = dict(item.get(media_key) or item) for key in metadata_keys: @@ -472,7 +470,7 @@ def _merge_object( self._normalize_video(merged, media) store[key] = TraktObject(merged) - def _normalize_video(self, merged: Dict, incoming: Dict) -> None: + def _normalize_video(self, merged: dict, incoming: dict) -> None: if "plays" in incoming: merged["watched"] = 1 if incoming["plays"] > 0 else 0 elif "watched" not in merged: @@ -490,7 +488,7 @@ def _normalize_video(self, merged: Dict, incoming: Dict) -> None: merged.setdefault("paused_at", None) def _merge_show( - self, store: Dict, item: Dict, metadata_keys: Iterable[str] + self, store: dict, item: dict, metadata_keys: Iterable[str] ) -> None: show = dict(item.get("show") or item) incoming_seasons = item.get("seasons", show.get("seasons", [])) @@ -522,7 +520,7 @@ def _merge_show( ) store[key] = TraktObject(merged) - def _merge_seasons(self, existing: List, incoming: List) -> List: + def _merge_seasons(self, existing: list, incoming: list) -> list: seasons = {season.get("number"): dict(season) for season in existing or []} for season in incoming or []: number = season.get("number") @@ -536,7 +534,7 @@ def _merge_seasons(self, existing: List, incoming: List) -> List: seasons[number] = merged return list(seasons.values()) - def _merge_episodes(self, existing: List, incoming: List) -> List: + def _merge_episodes(self, existing: list, incoming: list) -> list: episodes = {episode.get("number"): dict(episode) for episode in existing or []} for episode in incoming or []: number = episode.get("number") @@ -554,7 +552,7 @@ def on_aborted(self) -> None: if self.authDialog: self.authDialog.close() - def on_authenticated(self, token: Dict) -> None: + def on_authenticated(self, token: dict) -> None: """Triggered when device authentication has been completed :param token: Authentication token details @@ -562,7 +560,7 @@ def on_authenticated(self, token: Dict) -> None: """ self.authorization = token setSetting("authorization", dumps(self.authorization)) - logger.debug("Authentication complete: %r" % token) + logger.debug(f"Authentication complete: {token!r}") if self.authDialog: self.authDialog.close() notification(getString(32157), getString(32152), 3000) @@ -585,7 +583,7 @@ def on_poll(self, callback: Any) -> None: # Continue polling callback(True) - def on_token_refreshed(self, response: Dict) -> None: + def on_token_refreshed(self, response: dict) -> None: # OAuth token refreshed, save token for future calls self.authorization = response setSetting("authorization", dumps(self.authorization)) @@ -600,39 +598,37 @@ def updateUser(self) -> None: setSetting("user", "") def scrobbleEpisode( - self, show: Dict, episode: Dict, percent: float, status: str - ) -> Optional[Dict]: + self, show: dict, episode: dict, percent: float, status: str + ) -> dict | None: if status not in ("start", "pause", "stop"): logger.debug("scrobble() Bad scrobble status") return None return self._post( - "/scrobble/%s" % status, + f"/scrobble/{status}", {"show": show, "episode": episode, "progress": percent}, ) - def scrobbleMovie(self, movie: Dict, percent: float, status: str) -> Optional[Dict]: + def scrobbleMovie(self, movie: dict, percent: float, status: str) -> dict | None: if status not in ("start", "pause", "stop"): logger.debug("scrobble() Bad scrobble status") return None - return self._post( - "/scrobble/%s" % status, {"movie": movie, "progress": percent} - ) + return self._post(f"/scrobble/{status}", {"movie": movie, "progress": percent}) - def getShowsCollected(self, shows: Dict) -> Dict: + def getShowsCollected(self, shows: dict) -> dict: for item in self._get_all_pages( "/sync/collection/shows", authorized=True, timeout=90 ): self._merge_show(shows, item, ("collected_at",)) return shows - def getMoviesCollected(self, movies: Dict) -> Dict: + def getMoviesCollected(self, movies: dict) -> dict: for item in self._get_all_pages( "/sync/collection/movies", authorized=True, timeout=90 ): self._merge_object(movies, item, "movie", ("collected_at",)) return movies - def getShowsWatched(self, shows: Dict) -> Dict: + def getShowsWatched(self, shows: dict) -> dict: # extended=progress is required for the season/episode breakdown; without # it Trakt returns show-level plays only and episode watched-state can't # be synced. All sync endpoints are also paginated, so page through them. @@ -647,7 +643,7 @@ def getShowsWatched(self, shows: Dict) -> Dict: ) return shows - def getMoviesWatched(self, movies: Dict) -> Dict: + def getMoviesWatched(self, movies: dict) -> dict: for item in self._get_all_pages( "/sync/watched/movies", authorized=True, timeout=90 ): @@ -656,28 +652,28 @@ def getMoviesWatched(self, movies: Dict) -> Dict: ) return movies - def getShowsRated(self, shows: Dict) -> Dict: + def getShowsRated(self, shows: dict) -> dict: for item in self._get_all_ratings("shows"): self._merge_show(shows, item, ("rated_at", "rating")) return shows - def getEpisodesRated(self, shows: Dict) -> Dict: + def getEpisodesRated(self, shows: dict) -> dict: for item in self._get_all_ratings("episodes"): self._merge_show(shows, item, ("rated_at", "rating")) return shows - def getMoviesRated(self, movies: Dict) -> Dict: + def getMoviesRated(self, movies: dict) -> dict: for item in self._get_all_ratings("movies"): self._merge_object(movies, item, "movie", ("rated_at", "rating")) return movies - def addToCollection(self, mediaObject: Dict) -> Optional[Dict]: + def addToCollection(self, mediaObject: dict) -> dict | None: return self._post("/sync/collection", mediaObject) - def removeFromCollection(self, mediaObject: Dict) -> Optional[Dict]: + def removeFromCollection(self, mediaObject: dict) -> dict | None: return self._post("/sync/collection/remove", mediaObject) - def addToHistory(self, mediaObject: Dict) -> Optional[Dict]: + def addToHistory(self, mediaObject: dict) -> dict | None: # don't retry this call; it may cause multiple watches return self._request( "POST", @@ -688,17 +684,17 @@ def addToHistory(self, mediaObject: Dict) -> Optional[Dict]: retry=False, ) - def addToWatchlist(self, mediaObject: Dict) -> Optional[Dict]: + def addToWatchlist(self, mediaObject: dict) -> dict | None: return self._post("/sync/watchlist", mediaObject) - def getShowRatingForUser(self, showId: str, idType: str = "tvdb") -> Dict: + def getShowRatingForUser(self, showId: str, idType: str = "tvdb") -> dict: ratings = {} self.getShowsRated(ratings) return findShowMatchInList(showId, ratings, idType) def getSeasonRatingForUser( self, showId: str, season: int, idType: str = "tvdb" - ) -> Dict: + ) -> dict: ratings = {} for item in self._get_all_ratings("seasons"): self._merge_show(ratings, item, ("rated_at", "rating")) @@ -706,25 +702,25 @@ def getSeasonRatingForUser( def getEpisodeRatingForUser( self, showId: str, season: int, episode: int, idType: str = "tvdb" - ) -> Dict: + ) -> dict: ratings = {} self.getEpisodesRated(ratings) return findEpisodeMatchInList(showId, season, episode, ratings, idType) - def getMovieRatingForUser(self, movieId: str, idType: str = "imdb") -> Dict: + def getMovieRatingForUser(self, movieId: str, idType: str = "imdb") -> dict: ratings = {} self.getMoviesRated(ratings) return findMovieMatchInList(movieId, ratings, idType) # Send a rating to Trakt as mediaObject so we can add the rating - def addRating(self, mediaObject: Dict) -> Optional[Dict]: + def addRating(self, mediaObject: dict) -> dict | None: return self._post("/sync/ratings", mediaObject) # Send a rating to Trakt as mediaObject so we can remove the rating - def removeRating(self, mediaObject: Dict) -> Optional[Dict]: + def removeRating(self, mediaObject: dict) -> dict | None: return self._post("/sync/ratings/remove", mediaObject) - def getMoviePlaybackProgress(self) -> List[TraktObject]: + def getMoviePlaybackProgress(self) -> list[TraktObject]: progressMovies = [] for item in self._get_all_pages( "/sync/playback/movies", authorized=True, timeout=90 @@ -737,7 +733,7 @@ def getMoviePlaybackProgress(self) -> List[TraktObject]: return progressMovies - def getEpisodePlaybackProgress(self) -> List[TraktObject]: + def getEpisodePlaybackProgress(self) -> list[TraktObject]: progressEpisodes = [] for item in self._get_all_pages( "/sync/playback/episodes", authorized=True, timeout=90 @@ -756,10 +752,8 @@ def getEpisodePlaybackProgress(self) -> List[TraktObject]: return progressEpisodes - def getMovieSummary( - self, movieId: str, extended: Optional[str] = None - ) -> TraktObject: - path = "/movies/%s" % urllib.parse.quote(str(movieId), safe="") + def getMovieSummary(self, movieId: str, extended: str | None = None) -> TraktObject: + path = "/movies/{}".format(urllib.parse.quote(str(movieId), safe="")) if self.client: path = self.client.build_path(path, {"extended": extended}) result = self._get(path) @@ -768,23 +762,24 @@ def getMovieSummary( return TraktObject(result or {}) def getShowSummary(self, showId: str) -> TraktObject: - result = self._get("/shows/%s" % urllib.parse.quote(str(showId), safe="")) + result = self._get("/shows/{}".format(urllib.parse.quote(str(showId), safe=""))) if result is not None: result.setdefault("seasons", []) return TraktObject(result or {}) - def getShowWithAllEpisodesList(self, showId: str) -> List: + def getShowWithAllEpisodesList(self, showId: str) -> list: result = self._get( - "/shows/%s/seasons?extended=episodes" - % urllib.parse.quote(str(showId), safe=""), + "/shows/{}/seasons?extended=episodes".format( + urllib.parse.quote(str(showId), safe="") + ), timeout=90, ) return [TraktSeason(season) for season in result or []] def getEpisodeSummary( - self, showId: str, season: int, episode: int, extended: Optional[str] = None + self, showId: str, season: int, episode: int, extended: str | None = None ) -> Any: - path = "/shows/%s/seasons/%s/episodes/%s" % ( + path = "/shows/{}/seasons/{}/episodes/{}".format( urllib.parse.quote(str(showId), safe=""), season, episode, @@ -796,10 +791,9 @@ def getEpisodeSummary( result.setdefault("season", season) return TraktObject(result or {}) - def getIdLookup(self, id: str, id_type: str) -> Optional[List]: + def getIdLookup(self, id: str, id_type: str) -> list | None: result = self._get_all_pages( - "/search/%s/%s" - % ( + "/search/{}/{}".format( urllib.parse.quote(str(id_type), safe=""), urllib.parse.quote(str(id), safe=""), ), @@ -807,27 +801,24 @@ def getIdLookup(self, id: str, id_type: str) -> Optional[List]: ) return self._wrap_search_results(result) - def getTextQuery( - self, query: str, type: str, year: Optional[int] - ) -> Optional[List]: + def getTextQuery(self, query: str, type: str, year: int | None) -> list | None: if not self.client: return None if type not in ("movie", "show", "person"): logger.debug( - "Skipping %s text search; current Trakt API contract only exposes movie, show, and person search" - % type + f"Skipping {type} text search; current Trakt API contract only exposes movie, show, and person search" ) return None path = self.client.build_path( - "/search/%s" % urllib.parse.quote(str(type), safe=""), + "/search/{}".format(urllib.parse.quote(str(type), safe="")), {"query": query, "years": year}, ) return self._wrap_search_results(self._get_all_pages(path, timeout=90)) - def getUser(self) -> Optional[Dict]: + def getUser(self) -> dict | None: return self._get("/users/settings", authorized=True) - def _wrap_search_results(self, result: Any) -> Optional[List]: + def _wrap_search_results(self, result: Any) -> list | None: if not result: return None if not isinstance(result, list): diff --git a/resources/lib/utilities.py b/resources/lib/utilities.py index 72183480..1417aef8 100644 --- a/resources/lib/utilities.py +++ b/resources/lib/utilities.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- -# - import difflib -import time -import re import logging +import re +import time import traceback -from typing import Tuple, List, Dict, Union, Optional +from datetime import datetime, timezone + import dateutil.parser -from datetime import datetime -from dateutil.tz import tzutc, tzlocal +from dateutil.tz import tzlocal, tzutc # make strptime call prior to doing anything, to try and prevent threading # errors @@ -38,35 +35,37 @@ def isValidMediaType(type: str) -> bool: return type in ["movie", "show", "season", "episode"] -def chunks(list_data: List, n: int) -> List: +def chunks(list_data: list, n: int) -> list: return [list_data[i : i + n] for i in range(0, len(list_data), n)] -def getFormattedItemName(type: str, info: Dict) -> str: +def getFormattedItemName(type: str, info: dict) -> str: s = "" try: if isShow(type): s = info["title"] elif isEpisode(type): - s = "S%02dE%02d - %s" % (info["season"], info["number"], info["title"]) + s = "S{:02d}E{:02d} - {:s}".format( + info["season"], info["number"], info["title"] + ) elif isSeason(type): if isinstance(info, list): info = info[0] if info["season"] > 0: - s = "%s - Season %d" % (info["title"], info["season"]) + s = "{:s} - Season {:d}".format(info["title"], info["season"]) else: - s = "%s - Specials" % info["title"] + s = "{} - Specials".format(info["title"]) elif isMovie(type): - s = "%s (%s)" % (info["title"], info["year"]) + s = "{} ({})".format(info["title"], info["year"]) except KeyError: s = "" return s -def __findInList(list_data: List, case_sensitive: bool = True, **kwargs) -> Optional[Dict]: +def __findInList(list_data: list, case_sensitive: bool = True, **kwargs) -> dict | None: for item in list_data: i = 0 - for key in kwargs: + for key, expected in kwargs.items(): # because we can need to find at the root level and inside ids this # is is required if key in item: @@ -77,18 +76,20 @@ def __findInList(list_data: List, case_sensitive: bool = True, **kwargs) -> Opti else: continue if not case_sensitive and isinstance(key_val, str): - if key_val.lower() == kwargs[key].lower(): + if key_val.lower() == expected.lower(): i = i + 1 else: # forcing the compare to be done at the string level - if str(key_val) == str(kwargs[key]): + if str(key_val) == str(expected): i = i + 1 if i == len(kwargs): return item return None -def findMediaObject(mediaObjectToMatch: Dict, listToSearch: List, matchByTitleAndYear: bool) -> Optional[Dict]: +def findMediaObject( + mediaObjectToMatch: dict, listToSearch: list, matchByTitleAndYear: bool +) -> dict | None: result = None if ( result is None @@ -134,7 +135,7 @@ def findMediaObject(mediaObjectToMatch: Dict, listToSearch: List, matchByTitleAn return result -def regex_tvshow(label: str) -> Tuple[str, int, int]: +def regex_tvshow(label: str) -> tuple[str, int, int]: regexes = [ # ShowTitle.S01E09; s01e09, s01.e09, s01-e09 r"(.*?)[._ -]s([0-9]+)[._ -]*e([0-9]+)", @@ -148,7 +149,7 @@ def regex_tvshow(label: str) -> Tuple[str, int, int]: ] # ShowTitle - s01ep03, ShowTitle - s1ep03 for regex in regexes: - match = re.search(regex, label, re.I) + match = re.search(regex, label, re.IGNORECASE) if match: show_title, season, episode = match.groups() if show_title: @@ -160,7 +161,7 @@ def regex_tvshow(label: str) -> Tuple[str, int, int]: return "", -1, -1 -def regex_year(title: str) -> Tuple[str, str]: +def regex_year(title: str) -> tuple[str, str]: prog = re.compile(r"^(.+) \((\d{4})\)$") result = prog.match(title) @@ -170,7 +171,7 @@ def regex_year(title: str) -> Tuple[str, str]: return "", "" -def findMovieMatchInList(id: str, listToMatch: Dict, idType: str) -> Dict: +def findMovieMatchInList(id: str, listToMatch: dict, idType: str) -> dict: return next( ( item.to_dict() @@ -181,7 +182,7 @@ def findMovieMatchInList(id: str, listToMatch: Dict, idType: str) -> Dict: ) -def findShowMatchInList(id: str, listToMatch: Dict, idType: str) -> Dict: +def findShowMatchInList(id: str, listToMatch: dict, idType: str) -> dict: return next( ( item.to_dict() @@ -192,9 +193,11 @@ def findShowMatchInList(id: str, listToMatch: Dict, idType: str) -> Dict: ) -def findSeasonMatchInList(id: str, seasonNumber: int, listToMatch: Dict, idType: str) -> Dict: +def findSeasonMatchInList( + id: str, seasonNumber: int, listToMatch: dict, idType: str +) -> dict: show = findShowMatchInList(id, listToMatch, idType) - logger.debug("findSeasonMatchInList %s" % show) + logger.debug(f"findSeasonMatchInList {show}") if "seasons" in show: for season in show["seasons"]: if season["number"] == seasonNumber: @@ -203,7 +206,9 @@ def findSeasonMatchInList(id: str, seasonNumber: int, listToMatch: Dict, idType: return {} -def findEpisodeMatchInList(id: str, seasonNumber: int, episodeNumber: int, list_data: Dict, idType: str) -> Dict: +def findEpisodeMatchInList( + id: str, seasonNumber: int, episodeNumber: int, list_data: dict, idType: str +) -> dict: season = findSeasonMatchInList(id, seasonNumber, list_data, idType) if season: for episode in season["episodes"]: @@ -213,7 +218,7 @@ def findEpisodeMatchInList(id: str, seasonNumber: int, episodeNumber: int, list_ return {} -def convertDateTimeToUTC(toConvert: Optional[str]) -> Optional[str]: +def convertDateTimeToUTC(toConvert: str | None) -> str | None: if toConvert: dateFormat = "%Y-%m-%d %H:%M:%S" try: @@ -228,13 +233,13 @@ def convertDateTimeToUTC(toConvert: Optional[str]) -> Optional[str]: logger.debug( "convertDateTimeToUTC() ValueError: movie/show was collected/watched outside of the unix timespan. Fallback to datetime utcnow" ) - utc = datetime.utcnow() + utc = datetime.now(timezone.utc).replace(tzinfo=None) # noqa: UP017 - retain Python 3.10 compatibility return str(utc) else: return toConvert -def convertUtcToDateTime(toConvert: Optional[str]) -> Optional[str]: +def convertUtcToDateTime(toConvert: str | None) -> str | None: if toConvert: dateFormat = "%Y-%m-%d %H:%M:%S" try: @@ -263,7 +268,7 @@ def createError(ex: Exception) -> str: return template.format(type(ex).__name__, ex.args, traceback.format_exc()) -def guessBestTraktId(id: str, type: str) -> Tuple[Dict, str]: +def guessBestTraktId(id: str, type: str) -> tuple[dict, str]: data = {} id_type = "" if id.startswith("tt"): @@ -281,7 +286,7 @@ def guessBestTraktId(id: str, type: str) -> Tuple[Dict, str]: return data, id_type -def best_id(ids: Dict, type: str) -> Tuple[str, str]: +def best_id(ids: dict, type: str) -> tuple[str, str]: if "trakt" in ids: return ids["trakt"], "trakt" elif "imdb" in ids and isMovie(type): @@ -296,18 +301,19 @@ def best_id(ids: Dict, type: str) -> Tuple[str, str]: return ids["slug"], "slug" -def checkExcludePath(excludePath: str, excludePathEnabled: bool, fullpath: str, x: int) -> bool: +def checkExcludePath( + excludePath: str, excludePathEnabled: bool, fullpath: str, x: int +) -> bool: if excludePath != "" and excludePathEnabled and fullpath.startswith(excludePath): logger.debug( - "checkExclusion(): Video is from location, which is currently set as excluded path %i." - % x + f"checkExclusion(): Video is from location, which is currently set as excluded path {x:d}." ) return True else: return False -def sanitizeMovies(movies: List) -> None: +def sanitizeMovies(movies: list) -> None: # do not remove watched_at and collected_at may cause problems between the # 4 sync types (would probably have to deepcopy etc) for movie in movies: @@ -326,7 +332,7 @@ def sanitizeMovies(movies: List) -> None: # todo add tests -def sanitizeShows(shows: Dict) -> None: +def sanitizeShows(shows: dict) -> None: # do not remove watched_at and collected_at may cause problems between the # 4 sync types (would probably have to deepcopy etc) for show in shows["shows"]: @@ -345,14 +351,14 @@ def sanitizeShows(shows: Dict) -> None: def compareMovies( - movies_col1: List, - movies_col2: List, + movies_col1: list, + movies_col2: list, matchByTitleAndYear: bool, watched: bool = False, restrict: bool = False, playback: bool = False, rating: bool = False, -) -> List: +) -> list: movies = [] for movie_col1 in movies_col1: if movie_col1: @@ -384,20 +390,29 @@ def compareMovies( if "collected" in movie_col2 and not movie_col2["collected"]: movies.append(movie_col1) else: # no match found - if not restrict: - if "collected" in movie_col1 and movie_col1["collected"]: - if watched and (movie_col1["watched"] == 1): - movies.append(movie_col1) - elif rating and movie_col1["rating"] != 0: - movies.append(movie_col1) - elif not watched and not rating: - movies.append(movie_col1) + if ( + not restrict + and movie_col1.get("collected") + and ( + watched + and (movie_col1["watched"] == 1) + or rating + and movie_col1["rating"] != 0 + or not watched + and not rating + ) + ): + movies.append(movie_col1) return movies def compareShows( - shows_col1: Dict, shows_col2: Dict, matchByTitleAndYear: bool, rating: bool = False, restrict: bool = False -) -> Dict: + shows_col1: dict, + shows_col2: dict, + matchByTitleAndYear: bool, + rating: bool = False, + restrict: bool = False, +) -> dict: shows = [] # logger.debug("shows_col1 %s" % shows_col1) # logger.debug("shows_col2 %s" % shows_col2) @@ -454,15 +469,15 @@ def compareShows( # always return shows_col1 if you have enrich it, but don't return shows_col2 def compareEpisodes( - shows_col1: Dict, - shows_col2: Dict, + shows_col1: dict, + shows_col2: dict, matchByTitleAndYear: bool, watched: bool = False, restrict: bool = False, - collected: Union[Dict, bool] = False, + collected: dict | bool = False, playback: bool = False, rating: bool = False, -) -> Dict: +) -> dict: shows = [] # logger.debug("epi shows_col1 %s" % shows_col1) # logger.debug("epi shows_col2 %s" % shows_col2) @@ -577,9 +592,8 @@ def compareEpisodes( if len(eps) > 0: season_diff[season] = eps else: - if not restrict and not rating: - if len(a) > 0: - season_diff[season] = a + if not restrict and not rating and len(a) > 0: + season_diff[season] = a # logger.debug("season_diff %s" % season_diff) if len(season_diff) > 0: # logger.debug("Season_diff") @@ -593,10 +607,8 @@ def compareEpisodes( show["ids"].update(show_col1["ids"]) if show_col2["ids"]: show["ids"].update(show_col2["ids"]) - for seasonKey in season_diff: - episodes = [] - for episodeKey in season_diff[seasonKey]: - episodes.append(season_diff[seasonKey][episodeKey]) + for seasonKey, season_episodes in season_diff.items(): + episodes = list(season_episodes.values()) show["seasons"].append( {"number": seasonKey, "episodes": episodes} ) @@ -605,42 +617,44 @@ def compareEpisodes( # logger.debug("show %s" % show) shows.append(show) else: - if not restrict: - if countEpisodes([show_col1]) > 0: - show = { - "title": show_col1["title"], - "ids": {}, - "year": show_col1["year"], - "seasons": [], - } - if show_col1["ids"]: - show["ids"].update(show_col1["ids"]) - for seasonKey in show_col1["seasons"]: - episodes = [] - for episodeKey in seasonKey["episodes"]: - if watched and (episodeKey["watched"] == 1): - episodes.append(episodeKey) - elif rating and episodeKey["rating"] != 0: - episodes.append(episodeKey) - elif not watched and not rating: - episodes.append(episodeKey) - if len(episodes) > 0: - show["seasons"].append( - { - "number": seasonKey["number"], - "episodes": episodes, - } - ) - - if "tvshowid" in show_col1: - del show_col1["tvshowid"] - if countEpisodes([show]) > 0: - shows.append(show) + if not restrict and countEpisodes([show_col1]) > 0: + show = { + "title": show_col1["title"], + "ids": {}, + "year": show_col1["year"], + "seasons": [], + } + if show_col1["ids"]: + show["ids"].update(show_col1["ids"]) + for seasonKey in show_col1["seasons"]: + episodes = [] + for episodeKey in seasonKey["episodes"]: + if ( + watched + and (episodeKey["watched"] == 1) + or rating + and episodeKey["rating"] != 0 + or not watched + and not rating + ): + episodes.append(episodeKey) + if len(episodes) > 0: + show["seasons"].append( + { + "number": seasonKey["number"], + "episodes": episodes, + } + ) + + if "tvshowid" in show_col1: + del show_col1["tvshowid"] + if countEpisodes([show]) > 0: + shows.append(show) result = {"shows": shows} return result -def countEpisodes(shows: Union[Dict, List], collection: bool = True) -> int: +def countEpisodes(shows: dict | list, collection: bool = True) -> int: count = 0 if "shows" in shows: shows = shows["shows"] @@ -651,15 +665,15 @@ def countEpisodes(shows: Union[Dict, List], collection: bool = True) -> int: if episodeKey is not None: if ( "collected" in episodeKey - and not episodeKey["collected"] == collection + and episodeKey["collected"] != collection ): continue - if "number" in episodeKey and episodeKey["number"]: + if episodeKey.get("number"): count += 1 return count -def __getEpisodes(seasons: List) -> Dict: +def __getEpisodes(seasons: list) -> dict: data = {} for season in seasons: episodes = {} @@ -681,12 +695,12 @@ def checkIfNewVersion(old: str, new: str) -> bool: if old[1] < new[1]: return True # Revision - if old[2] < new[2]: - return True - return False + return old[2] < new[2] -def _to_sec(timedelta_string: str, factors: Tuple[int, ...] = (1, 60, 3600, 86400)) -> float: +def _to_sec( + timedelta_string: str, factors: tuple[int, ...] = (1, 60, 3600, 86400) +) -> float: """[[[days:]hours:]minutes:]seconds -> seconds""" return sum( x * y diff --git a/ruff.toml b/ruff.toml index d8f26b51..64407af9 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,2 +1,2 @@ # Ignore line length for now -ignore = ["E501"] \ No newline at end of file +ignore = ["E501"] diff --git a/scripts/inject_keys.py b/scripts/inject_keys.py index 59cd874b..2f4e786d 100644 --- a/scripts/inject_keys.py +++ b/scripts/inject_keys.py @@ -6,6 +6,7 @@ from resources.lib.obfuscation import deobfuscate, obfuscate + def main(): client_id = os.environ.get("TRAKT_CLIENT_ID") client_secret = os.environ.get("TRAKT_CLIENT_SECRET") @@ -43,5 +44,6 @@ def main(): print(f"Successfully injected obfuscated keys into {target_file}") + if __name__ == "__main__": main() diff --git a/tests/test_kodiUtilities.py b/tests/test_kodi_utilities.py similarity index 85% rename from tests/test_kodiUtilities.py rename to tests/test_kodi_utilities.py index e5aac41c..cc19f981 100644 --- a/tests/test_kodiUtilities.py +++ b/tests/test_kodi_utilities.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -# - -import mock import sys +from unittest import mock xbmc_mock = mock.Mock() sys.modules["xbmc"] = xbmc_mock @@ -10,7 +7,7 @@ sys.modules["xbmcgui"] = xbmcgui_mock xbmcaddon_mock = mock.Mock() sys.modules["xbmcaddon"] = xbmcaddon_mock -from resources.lib import kodiUtilities # noqa: E402 +from resources.lib import kodiUtilities def test_notification(): diff --git a/tests/test_obfuscation.py b/tests/test_obfuscation.py index a657b7e7..9317855a 100644 --- a/tests/test_obfuscation.py +++ b/tests/test_obfuscation.py @@ -1,20 +1,24 @@ -# -*- coding: utf-8 -*- from resources.lib import obfuscation + def test_obfuscate(): assert obfuscation.obfuscate("test") == [54, 39, 49, 54] + def test_deobfuscate(): assert obfuscation.deobfuscate([54, 39, 49, 54]) == "test" + def test_obfuscate_empty(): assert obfuscation.obfuscate("") == [] + def test_deobfuscate_empty(): assert obfuscation.deobfuscate("") == "" assert obfuscation.deobfuscate(None) == "" assert obfuscation.deobfuscate("not a list") == "" + def test_roundtrip(): original = "Hello, World!" assert obfuscation.deobfuscate(obfuscation.obfuscate(original)) == original diff --git a/tests/test_traktapi.py b/tests/test_traktapi.py index 00c34bd7..41d80951 100644 --- a/tests/test_traktapi.py +++ b/tests/test_traktapi.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- - import sys import urllib.error - -import mock +from unittest import mock -class WindowXMLDialog(object): +class WindowXMLDialog: pass @@ -19,8 +16,8 @@ class WindowXMLDialog(object): xbmcaddon_mock.Addon.return_value.getAddonInfo.return_value = "3.8.2" sys.modules["xbmcaddon"] = xbmcaddon_mock -from resources.lib import utilities # noqa: E402 -from resources.lib.traktapi import TraktClient, TraktObject, TraktSeason, traktAPI # noqa: E402 +from resources.lib import utilities +from resources.lib.traktapi import TraktClient, TraktObject, TraktSeason, traktAPI def test_trakt_object_exposes_legacy_contract(): @@ -512,11 +509,13 @@ def test_client_retries_once_after_rate_limit_retry_after(): opener = mock.Mock() opener.open = mock.Mock(side_effect=[rate_limit, response]) - with mock.patch( - "resources.lib.traktapi.urllib.request.build_opener", return_value=opener + with ( + mock.patch( + "resources.lib.traktapi.urllib.request.build_opener", return_value=opener + ), + mock.patch("resources.lib.traktapi.time.sleep") as sleep, ): - with mock.patch("resources.lib.traktapi.time.sleep") as sleep: - result = client.request("GET", "/search/movie") + result = client.request("GET", "/search/movie") assert result == {"ok": True} sleep.assert_called_once_with(1) diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 15aec372..8b26ef04 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- -# - import json + from resources.lib import utilities