From 354ed69eafe4851e4823b9782e72a201ecec0cd3 Mon Sep 17 00:00:00 2001 From: scott967 Date: Sat, 16 Dec 2023 11:50:51 -1000 Subject: [PATCH] [script.extendedinfo] 6.0.7 1. rework tmdb authentication -- current api key is being rejected by tmdb 2. fix script dialog management when play video from script 3. lutils131/utils change use of variable name "time" 4. kutils131/utils add timeout protection on calls to "requests" 5. kutils131/youtube fix handing of youtube supplied "duration" 6. add/improve docstrings various locations 7. use "f-string" formatting various locations 8. fix pylint nags various locations --- script.extendedinfo/addon.xml | 4 +- script.extendedinfo/changelog.txt | 4 + script.extendedinfo/default.py | 2 +- .../resources/kutil131/actionhandler.py | 1 + .../resources/kutil131/dialogbaselist.py | 1 + .../resources/kutil131/kodiaddon.py | 12 +- .../resources/kutil131/listitem.py | 5 +- .../resources/kutil131/localdb.py | 6 +- .../resources/kutil131/player.py | 56 +++- .../resources/kutil131/utils.py | 114 +++++--- .../resources/kutil131/youtube.py | 24 +- .../resources/lib/bandsintown.py | 8 +- .../resources/lib/dialogs/dialogmovieinfo.py | 2 +- .../resources/lib/dialogs/dialogvideolist.py | 2 +- script.extendedinfo/resources/lib/lastfm.py | 9 +- script.extendedinfo/resources/lib/process.py | 2 +- .../resources/lib/theaudiodb.py | 72 +++-- .../resources/lib/themoviedb.py | 250 +++++++++++++----- script.extendedinfo/resources/lib/trakt.py | 54 ++-- .../resources/lib/windowmanager.py | 14 +- script.extendedinfo/resources/settings.xml | 12 + ...pt-script.extendedinfo-DialogVideoInfo.xml | 2 +- 22 files changed, 456 insertions(+), 200 deletions(-) diff --git a/script.extendedinfo/addon.xml b/script.extendedinfo/addon.xml index cd738a793..f7314c232 100644 --- a/script.extendedinfo/addon.xml +++ b/script.extendedinfo/addon.xml @@ -1,5 +1,5 @@ - + @@ -24,7 +24,7 @@ http://forum.kodi.tv/showthread.php?tid=160558 https://github.com/scott967/script.extendedinfo scott967@kodi.tv - Python 3 fixes and Nexus/Matrix updates by scott967. Original addon for Leia and prior by phil65 (Philipp Temminghoff). Requires Kutils module version 1.3.0 from Kodi official repo + Python 3 fixes and Omega/Nexus/Matrix updates by scott967. Original addon for Leia and prior by phil65 (Philipp Temminghoff). resources/icon.png resources/fanart.jpg diff --git a/script.extendedinfo/changelog.txt b/script.extendedinfo/changelog.txt index c4cc25072..09184aa84 100644 --- a/script.extendedinfo/changelog.txt +++ b/script.extendedinfo/changelog.txt @@ -1,3 +1,7 @@ +v6.0.7 +- reworked tmdb logon / accreditation +- various code refactoring / pylint items / docstrings (WIP) + v6.0.5 - removed support for broken/obsolete addons artwork downloader, couch potato, sick rage, trakt helper - removed support for youtube-dl diff --git a/script.extendedinfo/default.py b/script.extendedinfo/default.py index bdc856f17..7a19408ed 100644 --- a/script.extendedinfo/default.py +++ b/script.extendedinfo/default.py @@ -24,7 +24,7 @@ def pass_list_to_skin(name: str, data, prefix: str = "", limit: int = False) -> name (str): Type of data being returned derived from runscript info parameter. Used to construct the skin window property key eg. topratedmovies from invocation parameter info= - data (kutils.itemlist.ItemList): collection of ListItems + data (kutils131.itemlist.ItemList): collection of ListItems (Video or Audio) prefix (str, optional): Optional prefix for the name. May be set as a param in runscript. Defaults to "". diff --git a/script.extendedinfo/resources/kutil131/actionhandler.py b/script.extendedinfo/resources/kutil131/actionhandler.py index 20673be72..bacc5d441 100644 --- a/script.extendedinfo/resources/kutil131/actionhandler.py +++ b/script.extendedinfo/resources/kutil131/actionhandler.py @@ -109,6 +109,7 @@ "symbols": xbmcgui.ACTION_SYMBOLS, "cursorleft": xbmcgui.ACTION_CURSOR_LEFT, "cursorright": xbmcgui.ACTION_CURSOR_RIGHT, + "built-in": xbmcgui.ACTION_BUILT_IN_FUNCTION, "showtime": xbmcgui.ACTION_SHOW_OSD_TIME, "analogseekforward": xbmcgui.ACTION_ANALOG_SEEK_FORWARD, "analogseekback": xbmcgui.ACTION_ANALOG_SEEK_BACK, diff --git a/script.extendedinfo/resources/kutil131/dialogbaselist.py b/script.extendedinfo/resources/kutil131/dialogbaselist.py index a248c3884..c8187d515 100644 --- a/script.extendedinfo/resources/kutil131/dialogbaselist.py +++ b/script.extendedinfo/resources/kutil131/dialogbaselist.py @@ -1,6 +1,7 @@ # Copyright (C) 2015 - Philipp Temminghoff # This program is Free Software see LICENSE file for details +from __future__ import annotations import xbmc import xbmcgui diff --git a/script.extendedinfo/resources/kutil131/kodiaddon.py b/script.extendedinfo/resources/kutil131/kodiaddon.py index fe55df6a6..950f3b38f 100644 --- a/script.extendedinfo/resources/kutil131/kodiaddon.py +++ b/script.extendedinfo/resources/kutil131/kodiaddon.py @@ -113,14 +113,14 @@ def encode_string(clear: str) -> str: enc = [] key = str(uuid.getnode()) clear_enc = clear.encode() - for i in range(len(clear_enc)): + for i, ele in enumerate(clear_enc): key_c = key[i % len(key)] - enc_c = chr((clear_enc[i] + ord(key_c)) % 256) + enc_c = chr((ele + ord(key_c)) % 256) enc.append(enc_c) return base64.urlsafe_b64encode("".join(enc).encode()).decode() -def decode_string(enc: str) -> str: +def decode_string(enc: str, uuick: str='') -> str: """return decoded string (encoded with uuid) Args: @@ -130,10 +130,10 @@ def decode_string(enc: str) -> str: str: the decoded string """ dec = [] - key = str(uuid.getnode()) + key = str(uuid.getnode()) if not uuick else uuick enc = base64.urlsafe_b64decode(enc.encode()).decode() - for i in range(len(enc)): + for i, ele in enumerate(enc): key_c = key[i % len(key)] - dec_c = ((256 + ord(enc[i]) - ord(key_c)) % 256).to_bytes(1, 'little') + dec_c = ((256 + ord(ele) - ord(key_c)) % 256).to_bytes(1, 'little') dec.append(dec_c) return bytes.join(b'', dec).decode() diff --git a/script.extendedinfo/resources/kutil131/listitem.py b/script.extendedinfo/resources/kutil131/listitem.py index b04213398..8f36ed596 100644 --- a/script.extendedinfo/resources/kutil131/listitem.py +++ b/script.extendedinfo/resources/kutil131/listitem.py @@ -125,6 +125,7 @@ def __getitem__(self, key): def __repr__(self): return "\n".join(["Label:", self.label, "Label2:", self.label2, + "Path:", self.path, "InfoLabels:", utils.dump_dict(self._infos), "Properties:", utils.dump_dict(self._properties), "Artwork:", utils.dump_dict(self._artwork), @@ -387,7 +388,7 @@ class VideoItem(ListItem): """Kodi video listitem, based on built-in datatypes Args: - ListItem (class): The Kutils ListItem class + ListItem (class): The kutils131 ListItem class """ def __init__(self, *args, **kwargs): @@ -407,7 +408,7 @@ def __repr__(self): def from_listitem(self, listitem: xbmcgui.ListItem): """ - xbmcgui listitem -> kodi65 listitem + xbmcgui listitem -> kutils131 listitem """ info = listitem.getVideoInfoTag() self.label = listitem.getLabel() diff --git a/script.extendedinfo/resources/kutil131/localdb.py b/script.extendedinfo/resources/kutil131/localdb.py index 527400b7f..b08a15e44 100644 --- a/script.extendedinfo/resources/kutil131/localdb.py +++ b/script.extendedinfo/resources/kutil131/localdb.py @@ -191,7 +191,7 @@ def handle_movie(self, movie:dict) -> VideoItem: 'playcount': movie.get("playcount"), 'setid': movie.get("setid"), 'top250': movie.get("top250"), - 'imdbnumber': movie.get("uniqueid").get("imdb", ""), + 'imdbnumber': movie.get("uniqueid", {}).get("imdb", ""), 'userrating': movie.get('userrating'), 'trailer': trailer, 'rating': round(float(movie['rating']), 1), @@ -413,7 +413,7 @@ def get_imdb_id(self, media_type, dbid): data = kodijson.get_json(method="VideoLibrary.GetMovieDetails", params={"properties": ["uniqueid", "title", "year"], "movieid": int(dbid)}) if "result" in data and "moviedetails" in data["result"]: - try: + try: return data['result']['moviedetails']['uniqueid']['imdb'] except KeyError: return None @@ -441,7 +441,7 @@ def get_tmdb_id(self, media_type, dbid): if "result" in data and "tvshowdetails" in data["result"]: return data['result']['tvshowdetails']['uniqueid'].get('tmdb', None) return None - + def get_tvshow_id_by_episode(self, dbid): if not dbid: return None diff --git a/script.extendedinfo/resources/kutil131/player.py b/script.extendedinfo/resources/kutil131/player.py index 9d16a6e60..3a4a73c90 100644 --- a/script.extendedinfo/resources/kutil131/player.py +++ b/script.extendedinfo/resources/kutil131/player.py @@ -16,35 +16,52 @@ def __init__(self, *args, **kwargs): def onPlayBackEnded(self): self.stopped = True + self.started = False def onPlayBackStopped(self): self.stopped = True + self.started = False def onPlayBackError(self): self.stopped = True + self.started = False def onAVStarted(self): self.started = True self.stopped = False + def onPlayBackStarted(self): + self.started = True + self.stopped = False + @busy.set_busy - def youtube_info_by_id(self, youtube_id): - vid = utils.get_youtube_info(youtube_id) + def youtube_info_by_id(self, youtube_id) -> None: + """function uses inop YTStreamextractor + + Args: + youtube_id (_type_): _description_ + + Returns: + _type_: function retained for future use + """ + #vid = utils.get_youtube_info(youtube_id) + vid = {} if not vid: return None, None - listitem = xbmcgui.ListItem(label=vid.title) - listitem.setArt({'thumb': vid.thumbnail}) - listitem.setInfo(type='video', - infoLabels={"genre": vid.sourceName, - "plot": vid.description}) - return vid.streamURL(), listitem + #listitem = xbmcgui.ListItem(label=vid.title) + #listitem.setArt({'thumb': vid.thumbnail}) + #listitem.setInfo(type='video', + # infoLabels={"genre": vid.sourceName, + # "plot": vid.description}) + #return vid.streamURL(), listitem def wait_for_video_end(self): monitor: xbmc.Monitor = xbmc.Monitor() while not monitor.waitForAbort(1.0): + if monitor.abortRequested(): + break if self.stopped: break - self.stopped = False def wait_for_video_start(self): @@ -53,11 +70,28 @@ def wait_for_video_start(self): """ monitor = xbmc.Monitor() timeout = 15 - while not monitor.waitForAbort(1.0): #wait 10 sec to see if video starts + while not monitor.waitForAbort(1.5): #wait to see if video starts + if monitor.abortRequested(): + break + timeout += -1 + if self.started: + break + if timeout == 0: + self.stopped = True + break + + def wait_for_kodivideo_start(self): + """Timer called from dialogmovieinfo that checks if Kodi can play selected listitem + Sets a 20 sec timer to attempt play local db media. If + timer ends videoplayer self.stopped is set + """ + monitor = xbmc.Monitor() + timeout = 20 + while not monitor.waitForAbort(1): #wait to see if video starts if monitor.abortRequested(): break timeout += -1 - if self.stopped or self.started: + if self.started: break if timeout == 0: self.stopped = True diff --git a/script.extendedinfo/resources/kutil131/utils.py b/script.extendedinfo/resources/kutil131/utils.py index 45cff0fd8..b5130382b 100644 --- a/script.extendedinfo/resources/kutil131/utils.py +++ b/script.extendedinfo/resources/kutil131/utils.py @@ -24,7 +24,16 @@ from resources.kutil131 import addon -def youtube_info_by_id(youtube_id): +def youtube_info_by_id(youtube_id) -> tuple: + """Gets youtube video info from YDSStreamExtractor + Currently inop due to YDSStreamextractor not maintained. + + Args: + youtube_id (_type_): _description_ + + Returns: + _type_: _description_ + """ #vid = get_youtube_info(youtube_id) vid = None #added if not vid: @@ -164,14 +173,14 @@ def get_skin_string(name): """ get String with name *name """ - return xbmc.getInfoLabel("Skin.String(%s)" % name) + return xbmc.getInfoLabel(f"Skin.String({name})") def set_skin_string(name, value): """ Set String *name to value *value """ - xbmc.executebuiltin("Skin.SetString(%s, %s)" % (name, value)) + xbmc.executebuiltin(f"Skin.SetString({name}, {value})") def run_async(func): @@ -222,14 +231,14 @@ def extract_youtube_id(raw_string): # YDStreamExtractor.handleDownload(vid) -def notify(header="", message="", icon=addon.ICON, time=5000, sound=True): +def notify(header="", message="", icon=addon.ICON, ntime=5000, sound=True): """ show kodi notification dialog """ xbmcgui.Dialog().notification(heading=header, message=message, icon=icon, - time=time, + time=ntime, sound=sound) @@ -256,19 +265,19 @@ def get_year(year_string): return year_string[:4] if year_string else "" -def format_time(time:int, time_format=None): +def format_time(ftime:int, time_format=None): """ get formatted time time (int): duration in secs time_format = h, m or None """ try: - intTime = int(time) + intTime = int(ftime) except Exception: - return time + return ftime #hour = str(intTime / 60) #minute = str(intTime % 60).zfill(2) - minute, second = divmod(time, 60) + minute, second = divmod(ftime, 60) hour, minute = divmod(minute, 60) if time_format == "h": return str(hour) @@ -358,29 +367,33 @@ def calculate_age(born, died=False): else: return "" actor_born = born.split("-") - base_age = int(ref_day[0]) - int(actor_born[0]) + try: + base_age = int(ref_day[0]) - int(actor_born[0]) + except ValueError as err: + log(f'utils.calculate_age fail for actor_born {actor_born} with error {err}') + return "" if len(actor_born) > 1: diff_months = int(ref_day[1]) - int(actor_born[1]) diff_days = int(ref_day[2]) - int(actor_born[2]) if diff_months < 0 or (diff_months == 0 and diff_days < 0): base_age -= 1 elif diff_months == 0 and diff_days == 0 and not died: - notify("%s (%i)" % (addon.LANG(32158), base_age)) + notify(f"{addon.LANG(32158)} ({base_age})") return base_age def get_http(url, headers=False): """ - fetches data from *url, returns it as a string + fetches data from *url as http GET, returns it as a string """ succeed = 0 if not headers: - headers = {'User-agent': 'Kodi/17.0 ( fbacher@kodi.tv )'} + headers = {'User-agent': 'Kodi/19.0 ( fbacher@kodi.tv )'} while (succeed < 2) and (not xbmc.Monitor().abortRequested()): try: - request = requests.get(url, headers=headers) + request = requests.get(url, headers=headers, timeout=10) return request.text - except Exception as err: + except requests.exceptions.RequestException as err: log(f"get_http: could not get data from {url} exception {err}") xbmc.sleep(1000) succeed += 1 @@ -391,9 +404,13 @@ def post(url, values, headers): """ retuns answer to post request """ - request = requests.post(url=url, - data=json.dumps(values), - headers=headers) + try: + request = requests.post(url=url, + data=json.dumps(values), + headers=headers, + timeout=10) + except requests.exceptions.RequestException as err: + log(f"get_http: could not get data from {url} exception {err}") return json.loads(request.text) @@ -401,9 +418,13 @@ def delete(url, values, headers): """ returns answer to delete request """ - request = requests.delete(url=url, - data=json.dumps(values), - headers=headers) + try: + request = requests.delete(url=url, + data=json.dumps(values), + headers=headers, + timeout=10) + except requests.exceptions.RequestException as err: + log(f"get_http: could not get data from {url} exception {err}") return json.loads(request.text) @@ -433,20 +454,20 @@ def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> di if prop: return prop except Exception: - # utils.log("could not load prop data for %s" % url) pass path = os.path.join(cache_path, hashed_url + ".txt") if xbmcvfs.exists(path) and ((now - os.path.getmtime(path)) < cache_seconds): results = read_from_file(path) - # utils.log("loaded file for %s. time: %f" % (url, time.time() - now)) else: response = get_http(url, headers) try: results = json.loads(response) # utils.log("download %s. time: %f" % (url, time.time() - now)) - save_to_file(results, hashed_url, cache_path) + if "status_code" in response and response.get("status_code") == 1: + save_to_file(results, hashed_url, cache_path) except Exception as err: - log(f"Exception: Could not get new JSON data from {url} with error {err}. Trying to fallback to cache") + log(f"Exception: Could not get new JSON data from {url} " + f"with error {err}. Trying to fallback to cache") #log(f'kutils131.utils.get_JSON_response {response}') results = read_from_file(path) if xbmcvfs.exists(path) else [] if not results: @@ -456,21 +477,32 @@ def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> di return results -def dict_to_windowprops(data=None, prefix="", window_id=10000): +def dict_to_windowprops(data:dict=None, prefix="", window_id=10000): + """Sets window property keys / values from dict + + Args: + data (dict optional): the data to be set as properties Defaults to None. + prefix (str, optional): a prefix for the property key Defaults to "". + window_id (int, optional): Kodi window id. Defaults to 10000. + """ window = xbmcgui.Window(window_id) if not data: return None for (key, value) in data.items(): value = str(value) - window.setProperty('%s%s' % (prefix, key), value) + window.setProperty(f'{prefix}{key}', value) def get_file(url): clean_url = translate_path(urllib.parse.unquote(url)).replace("image://", "") clean_url = clean_url.rstrip("/") cached_thumb = xbmc.getCacheThumbName(clean_url) - vid_cache_file = os.path.join("special://profile/Thumbnails/Video", cached_thumb[0], cached_thumb) - cache_file_jpg = os.path.join("special://profile/Thumbnails/", cached_thumb[0], cached_thumb[:-4] + ".jpg").replace("\\", "/") + vid_cache_file = os.path.join("special://profile/Thumbnails/Video", + cached_thumb[0], + cached_thumb) + cache_file_jpg = os.path.join("special://profile/Thumbnails/", + cached_thumb[0], + cached_thumb[:-4] + ".jpg").replace("\\", "/") cache_file_png = cache_file_jpg[:-4] + ".png" if xbmcvfs.exists(cache_file_jpg): log("cache_file_jpg Image: " + url + "-->" + cache_file_jpg) @@ -487,9 +519,9 @@ def get_file(url): response = urllib.request.urlopen(request, timeout=3) data = response.read() response.close() - log('image downloaded: ' + clean_url) + log(f'image downloaded: {clean_url}') except Exception: - log('image download failed: ' + clean_url) + log(f'image download failed: {clean_url}') return "" if not data: return "" @@ -499,7 +531,7 @@ def get_file(url): f.write(data) return translate_path(image) except Exception: - log('failed to save image ' + url) + log(f'failed to save image {url}') return "" @@ -509,12 +541,12 @@ def fetch_musicbrainz_id(artist, artist_id=-1): uses musicbrainz.org """ base_url = "http://musicbrainz.org/ws/2/artist/?fmt=json" - url = '&query=artist:%s' % urllib.parse.quote_plus(artist.encode('utf-8')) + url = f'&query=artist:{urllib.parse.quote_plus(artist.encode("utf-8"))}' results = get_JSON_response(url=base_url + url, cache_days=30, folder="MusicBrainz") if results and len(results["artists"]) > 0: - log("found artist id for %s: %s" % (artist, results["artists"][0]["id"])) + log(f'found artist id for {artist}: {results["artists"][0]["id"]}') return results["artists"][0]["id"] else: return None @@ -545,7 +577,7 @@ def dict_to_listitems(data=None): return [] itemlist = [] for (count, result) in enumerate(data): - listitem = xbmcgui.ListItem('%s' % (str(count))) + listitem = xbmcgui.ListItem(f'{str(count)}') for (key, value) in result.items(): if not value: continue @@ -562,7 +594,7 @@ def dict_to_listitems(data=None): return itemlist -def pretty_date(time=False): +def pretty_date(btime=False): """ Get a datetime object or a int() Epoch timestamp and return a pretty string like 'an hour ago', 'Yesterday', '3 months ago', @@ -570,11 +602,11 @@ def pretty_date(time=False): # https://stackoverflow.com/questions/1551382/user-friendly-time-format-in-python """ now = datetime.datetime.now() - if type(time) is int: - diff = now - datetime.datetime.fromtimestamp(time) - elif isinstance(time, datetime.datetime): - diff = now - time - elif not time: + if isinstance(btime, int): + diff = now - datetime.datetime.fromtimestamp(btime) + elif isinstance(btime, datetime.datetime): + diff = now - btime + elif not btime: diff = now - now second_diff = diff.seconds day_diff = diff.days diff --git a/script.extendedinfo/resources/kutil131/youtube.py b/script.extendedinfo/resources/kutil131/youtube.py index b00640bf9..52a813e30 100644 --- a/script.extendedinfo/resources/kutil131/youtube.py +++ b/script.extendedinfo/resources/kutil131/youtube.py @@ -75,19 +75,23 @@ def handle_videos(results:list[dict], extended=False, api_key=''): return videos -def get_duration_in_seconds(duration:str): +def get_duration_in_seconds(duration:str) -> int: """ convert youtube duration string to seconds int """ if not duration.endswith('S'): duration = duration + '0S' - duration = duration[2:-1].replace("H", "M").split("M") - if len(duration) == 3: - return int(duration[0]) * 3600 + int(duration[1]) * 60 + int(duration[2]) - elif len(duration) == 2: - return int(duration[0]) * 60 + int(duration[1]) - else: - return int(duration[0]) + try: + duration = duration[2:-1].replace("H", "M").split("M") + if len(duration) == 3: + return int(duration[0]) * 3600 + int(duration[1]) * 60 + int(duration[2]) + elif len(duration) == 2: + return int(duration[0]) * 60 + int(duration[1]) + else: + return int(duration[0]) + except Exception as err: + utils.log(f'kutils131.youtube unable decode youtube duration of {duration} error {err}') + return 0 def get_formatted_duration(duration): @@ -213,10 +217,10 @@ def search(search_str="", hd="", orderby="relevance", limit=40, extended=True, utils.log('youtube get_data ERROR: {error}'.format(error=results.get('error').get('message'))) if not results or 'items' not in results.keys(): return None - + # Give initial value to keep IDE happy as well as in case we drop through all # choices - + listitems: ItemList = ItemList() if media_type == "video": listitems = handle_videos(results["items"], extended=extended, api_key=api_key) diff --git a/script.extendedinfo/resources/lib/bandsintown.py b/script.extendedinfo/resources/lib/bandsintown.py index faa01157d..336760988 100644 --- a/script.extendedinfo/resources/lib/bandsintown.py +++ b/script.extendedinfo/resources/lib/bandsintown.py @@ -18,17 +18,17 @@ # TVRAGE_KEY = 'VBp9BuIr5iOiBeWCFRMG' API_KEY = '' -BASE_URL = "http://api.bandsintown.com/events/search?format=json&api_version=2.0&app_id=%s&" % API_KEY +BASE_URL = f"http://api.bandsintown.com/events/search?format=json&api_version=2.0&app_id={API_KEY}&" def handle_events(results: list) -> ItemList: - """converts a list of BandsinTown events to a kutils ItemList + """converts a list of BandsinTown events to a kutils131 ItemList Args: results (list): list of event dicts Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ events = ItemList() for event in results: @@ -54,7 +54,7 @@ def get_near_events(artists: str) -> ItemList: # not possible with api 2.0 artists (str): _description_ Returns: - ItemList: A kutils ItemList of VideoItems for artist events + ItemList: A kutils131 ItemList of VideoItems for artist events """ arts = [urllib.parse.quote(art['artist'].encode("utf-8")) for art in artists[:50]] diff --git a/script.extendedinfo/resources/lib/dialogs/dialogmovieinfo.py b/script.extendedinfo/resources/lib/dialogs/dialogmovieinfo.py index adc809209..792f7cf0a 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogmovieinfo.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogmovieinfo.py @@ -145,7 +145,7 @@ def reviews_list(self, control_id): author = self.FocusedItem(control_id).getProperty("author") text = "[B]%s[/B][CR]%s" % (author, self.FocusedItem(control_id).getProperty("content")) - xbmcgui.Dialog().textviewer(heading=addon.LANG(207), + xbmcgui.Dialog().textviewer(heading=addon.LANG(183), text=text) @ch.click(ID_LIST_KEYWORDS) diff --git a/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py b/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py index 13ad8c75f..78e7d3fd7 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py @@ -375,7 +375,7 @@ def open_media(self, control_id): def set_company_filter(self, control_id): result = xbmcgui.Dialog().input(heading=addon.LANG(16017), type=xbmcgui.INPUT_ALPHANUM) - if not result or result < 0: + if not result: return None items = tmdb.search_companies(result) if len(items) > 1: diff --git a/script.extendedinfo/resources/lib/lastfm.py b/script.extendedinfo/resources/lib/lastfm.py index 7bd73f1d8..4f268540a 100644 --- a/script.extendedinfo/resources/lib/lastfm.py +++ b/script.extendedinfo/resources/lib/lastfm.py @@ -40,14 +40,14 @@ def _handle_albums(results: dict) -> ItemList: 'mbid': album.get('mbid', ""), 'mediatype': "album", 'thumb': album['image'][-1]['#text'], - 'label': "%s - %s" % (album['artist']['name'], album['name']), + 'label': f"{album['artist']['name']} - {album['name']}", 'title': album['name']}) albums.append(album) return albums def _handle_artists(results) -> ItemList: - """Converts TADB artist query to kutils ItemList + """Converts TADB artist query to kutils131 ItemList Args: results (_type_): _description_ @@ -111,7 +111,7 @@ def get_similar_artists(artist_mbid: str) -> ItemList: artist_mbid (str): The musicbrainz id for the artist Returns: - ItemList: a kutils object that wraps a list of artists info dicts + ItemList: a kutils131 object that wraps a list of artists info dicts """ if not artist_mbid: return ItemList(content_type="artists") @@ -163,8 +163,7 @@ def get_data(method: str, params=None, cache_days=0.5) -> dict: params["api_key"] = LAST_FM_API_KEY params["format"] = "json" params = {k: str(v) for k, v in params.items() if v} - url = "{base_url}{params}".format(base_url=BASE_URL, - params=urllib.parse.urlencode(params)) + url = f"{BASE_URL}{urllib.parse.urlencode(params)}" return utils.get_JSON_response(url=url, cache_days=cache_days, folder="LastFM") diff --git a/script.extendedinfo/resources/lib/process.py b/script.extendedinfo/resources/lib/process.py index 3dd6e260f..bbcdee822 100644 --- a/script.extendedinfo/resources/lib/process.py +++ b/script.extendedinfo/resources/lib/process.py @@ -359,7 +359,7 @@ def start_info_actions(info: str, params: dict[str, str]): mode="search") finally: addon.clear_global('infodialogs.active') - elif info == 'extendedinfo': # called with movie + elif info == 'extendedinfo': # called with movie id if addon.get_global('infodialogs.active'): return None addon.set_global('infodialogs.active', "true") diff --git a/script.extendedinfo/resources/lib/theaudiodb.py b/script.extendedinfo/resources/lib/theaudiodb.py index ecdd45d6e..9f24a4d99 100644 --- a/script.extendedinfo/resources/lib/theaudiodb.py +++ b/script.extendedinfo/resources/lib/theaudiodb.py @@ -23,14 +23,14 @@ PLUGIN_BASE = 'plugin://script.extendedinfo/?info=' -def _handle_albums(results): - """[summary] +def _handle_albums(results:dict) -> ItemList: + """Creates an ItemList of kutils131 AudioItems Args: - results ([type]): [description] + results (dict): TADB album info Returns: - [type]: [description] + ItemList: kutils131 ItemList of AudioItems """ albums = ItemList(content_type="albums") if not results.get('album'): @@ -45,8 +45,7 @@ def _handle_albums(results): elif item.get('strDescription'): desc = item['strDescription'] if item.get('strReview'): - desc += "[CR][CR][B]%s:[/B][CR][CR]%s" % ( - addon.LANG(185), item['strReview']) + desc += f"[CR][CR][B]{addon.LANG(185)}:[/B][CR][CR]{item['strReview']}" album = AudioItem(label=item['strAlbum'], path="") album.set_infos({'artist': item['strArtist'], @@ -77,43 +76,67 @@ def _handle_albums(results): def _handle_tracks(results: dict) -> ItemList: + """Creates an ItemList of track AudioItems + + Args: + results (dict): TADB tracks + + Returns: + ItemList: The kutils131 itemlist of the tracts + """ tracks = ItemList(content_type="songs") if not results.get('track'): return tracks for item in results['track']: youtube_id = utils.extract_youtube_id(item.get('strMusicVid', '')) track = AudioItem(label=item['strTrack'], - path="%syoutubevideo&&id=%s" % (PLUGIN_BASE, youtube_id)) + path=f"{PLUGIN_BASE}youtubevideo&&id={youtube_id}") track.set_infos({'title': item['strTrack'], 'album': item['strAlbum'], 'artist': item['strArtist'], 'mediatype': "song"}) track.set_properties({'mbid': item['strMusicBrainzID']}) track.set_artwork( - {'thumb': "http://i.ytimg.com/vi/%s/0.jpg" % youtube_id}) + {'thumb': f"http://i.ytimg.com/vi/{youtube_id}/0.jpg"}) tracks.append(track) return tracks -def _handle_musicvideos(results): +def _handle_musicvideos(results:dict) -> ItemList: + """Creates an ItemList of TADB VideoItems + + Args: + results (dict): TADB musicvideos + + Returns: + ItemList: the kutils131 ItemList of musicvideos + """ mvids = ItemList(content_type="musicvideos") if not results.get('mvids'): return mvids for item in results['mvids']: youtube_id = utils.extract_youtube_id(item.get('strMusicVid', '')) mvid = VideoItem(label=item['strTrack'], - path="%syoutubevideo&&id=%s" % (PLUGIN_BASE, youtube_id)) + path=f"{PLUGIN_BASE}youtubevideo&&id={youtube_id}") mvid.set_infos({'title': item['strTrack'], 'plot': item['strDescriptionEN'], 'mediatype': "musicvideo"}) mvid.set_properties({'id': item['idTrack']}) mvid.set_artwork( - {'thumb': "http://i.ytimg.com/vi/%s/0.jpg" % youtube_id}) + {'thumb': f"http://i.ytimg.com/vi/{youtube_id}/0.jpg"}) mvids.append(mvid) return mvids def extended_artist_info(results: dict) -> dict: + """Gets artist info from TADB and returns artist dict + + Args: + results (dict): TADB artist info + + Returns: + dict: artist details using Kodi properties keywords + """ if not results.get('artists'): return {} local_bio = 'strBiography' + addon.setting("LanguageID").upper() @@ -164,7 +187,7 @@ def get_artist_discography(search_str) -> ItemList: search_str (str): Artist name Returns: - [ItemList]: Kutils list instance of AudioItems + [ItemList]: kutils131 ItemList instance of AudioItems """ if not search_str: return ItemList(content_type="albums") @@ -214,7 +237,17 @@ def get_most_loved_tracks(search_str="", mbid="") -> ItemList | list: return _handle_tracks(results) -def get_album_details(audiodb_id="", mbid=""): +def get_album_details(audiodb_id="", mbid="") -> ItemList | list: + """Creates ItemList of TADB alubm detals + + Args: + audiodb_id (str, optional): TADB album id "". + mbid (str, optional): mbid album groupd id Defaults to "". + + Returns: + list: empty if no results + ItemList: kutils131 ItemList of album AudioItems + """ if audiodb_id: url = 'album' params = {"m": audiodb_id} @@ -227,7 +260,15 @@ def get_album_details(audiodb_id="", mbid=""): return _handle_albums(results)[0] -def get_musicvideos(audiodb_id): +def get_musicvideos(audiodb_id) -> ItemList: + """Creates ItemList of musicvideo Videoitems + + Args: + audiodb_id (str): TADB id + + Returns: + ItemList: kutils131 ItemList + """ if not audiodb_id: return ItemList(content_type="musicvideos") params = {"i": audiodb_id} @@ -265,7 +306,6 @@ def get_data(url: str, params: dict) -> dict: if tadb_key is None or tadb_key == '': tadb_key = AUDIO_DB_KEY #limited function key params: dict = {k: str(v) for k, v in params.items() if v} - url: str = "{0}/{1}/{2}.php?{3}".format(BASE_URL, - tadb_key, url, urllib.parse.urlencode(params)) + url: str = f"{BASE_URL}/{tadb_key}/{url}.php?{urllib.parse.urlencode(params)}" return utils.get_JSON_response(url=url, folder="TheAudioDB") diff --git a/script.extendedinfo/resources/lib/themoviedb.py b/script.extendedinfo/resources/lib/themoviedb.py index 99369d590..f08fa7dc7 100644 --- a/script.extendedinfo/resources/lib/themoviedb.py +++ b/script.extendedinfo/resources/lib/themoviedb.py @@ -10,7 +10,8 @@ creation Public functions: - set_rating: sets local videodb userrating or TMDB user rating (called from button in dialogvideoinfo) + set_rating: sets local videodb userrating or TMDB user rating + (called from button in dialogvideoinfo) change_fav_status: sets TMDB user favorite (called from button in dialogvideoinfo) create_list: creates a new user list on TMDB remove_list_dialog: opens a Kodi select dialog to allow user to select @@ -18,13 +19,13 @@ change_list_status: Adds or removes a video item from user's TMDB list get_account_lists: gets the user's TMDB lists get_certification_list: gets the TMDB certifications ("MPAA") - handle_movies/tvshows/episodes: creates a kutils ItemList instance - of kutils VideoItems instances with Kodi listitem properties for the + handle_movies/tvshows/episodes: creates a kutils131 ItemList instance + of kutils131 VideoItems instances with Kodi listitem properties for the video media type to display as Kodi container content items - handle_lists: adds user TMDB lists to kutils ItemList instance for display + handle_lists: adds user TMDB lists to kutils131 ItemList instance for display in Kodi cantainer content - handle_seasons: adds seasons to kutils ItemList instance - handle_videos: adds video clips as kutils VideoItems to kutils ItemList instance + handle_seasons: adds seasons to kutils131 ItemList instance + handle_videos: adds video clips as kutils131 VideoItems to kutils131 ItemList instance search_companies: gets the TMDB company ID for company (studio) name string multi_search: performs TMDB multisearch "Multi search currently supports searching for movies, tv shows and people in a single request." @@ -58,7 +59,7 @@ extended_actor_info: sets Kodi listitem properties as a kutils VideoItem instance and additionally returns a dict of kutil itemlists instances - get_movie_lists: gets kutils ItemList instance for movie lists + get_movie_lists: gets kutils131 ItemList instance for movie lists get_rated_media_items: queries TMDB for user media ratings get_fav_items: queries TMDB for user favorites get_movies_from_list: queries TMDB for movie list @@ -81,11 +82,17 @@ import urllib.parse import urllib.request -from resources.kutil131 import ItemList, addon, kodijson, selectdialog - -from resources.kutil131 import VideoItem, local_db, utils - -TMDB_KEY = '34142515d9d23817496eeb4ff1d223d0' +from resources.kutil131 import (ItemList, VideoItem, addon, kodiaddon, kodijson, local_db, + selectdialog, utils) + +TMDB_TOKEN = ('wpjCrsKBwpjClnvCmsKiwoDCmcKCf8KFwrB8ZsKFwpl-bWXCnsKqesKgwprCh8K' + 'HwpzChMKgesKhwoJ7wqXCmn3CssKIwpzChcKHwpjCscKJwojCisKiwofCm37CoMK' + 'QdHdlwoJ7wonCrsKBacKKYX3Csn9iwoN3wpjCsX93fcKqwoLCn35pwo_CmX9pfsK' + 'hwonCrcKBe8KGwqh9wqV7ZcKQwop6wrF-ZsKJwq7ChnVxbcKEdMKLwqrCj8KkfsK' + 'fwo3CisKCwqR5wqbChMKawphmd8KjwpPCrX1twpDCqnrCoMKZd8KiwpnCmMKkwob' + 'CnMKOesKDwpV8e8KAYsKQwot_wrHCkcKLbcKswoLCm8KfwrDCnMKBZHfCgMKAwpT' + 'CjsKJwpzCr8KWwpbCkcKnwoV5wpliesKYaWzChcKJwovCoMKua3htwojCj396wox' + '8woLChcKQwqDCscKDwpHCpMKe') POSTER_SIZES = ["w92", "w154", "w185", "w342", "w500", "w780", "original"] LOGO_SIZES = ["w45", "w92", "w154", "w185", "w300", "w500", "original"] BACKDROP_SIZES = ["w300", "w780", "w1280", "original"] @@ -94,13 +101,16 @@ HEADERS = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'User-agent': 'Kodi/17.0 ( phil65@kodi.tv )' + 'User-agent': 'Kodi/19.0 ( scott967@kodi.tv )', + 'Authorization': '' } IMAGE_BASE_URL = "http://image.tmdb.org/t/p/" POSTER_SIZE = "w500" URL_BASE = "https://api.themoviedb.org/3/" -ALL_MOVIE_PROPS = "account_states,alternative_titles,credits,images,keywords,release_dates,videos,translations,similar,reviews,lists,rating" -ALL_TV_PROPS = "account_states,alternative_titles,content_ratings,credits,external_ids,images,keywords,rating,similar,translations,videos" +ALL_MOVIE_PROPS = ("account_states,alternative_titles,credits,images,keywords," + "release_dates,videos,translations,similar,reviews,lists,rating") +ALL_TV_PROPS = ("account_states,alternative_titles,content_ratings,credits," + "external_ids,images,keywords,rating,similar,translations,videos") ALL_ACTOR_PROPS = "tv_credits,movie_credits,combined_credits,images,tagged_images" ALL_SEASON_PROPS = "videos,images,external_ids,credits" ALL_EPISODE_PROPS = "account_states,credits,external_ids,images,rating,videos" @@ -125,13 +135,31 @@ "planned": addon.LANG(32076)} +def _mulitple_repl(text:str) -> str: + """ replaces html tags in text with Kodi label formats + TRANS is a literal dict with regex patterns to match and replace + Args: + text (str): string to replace tags + + Returns: + str: string with Kodi formatting + + """ + TRANS = {"": "", + "": "[B]", + "": "[/B]", + "": "[I]", + "": "[/I]"} + regex = re.compile(f"({'|'.join(map(re.escape, TRANS.keys()))})") + return regex.sub(lambda mo: TRANS[mo.group()], text) + class LoginProvider: """ logs into TMDB for user or guest and gets corresponding session or guest session id """ def __init__(self, *args, **kwargs) -> LoginProvider: - """Creates a new session for user at tmdb + """Creates a new user for accessing tmdb Returns: LoginProvider: session for user @@ -142,12 +170,22 @@ def __init__(self, *args, **kwargs) -> LoginProvider: self.username = kwargs.get("username") self.password = kwargs.get("password") + def reset_session_id(self): + """Resets the user session_id in settings when tmdb authentication fails + This will require obraining a new session_id via get_session_id + """ + utils.log('tmdb.LoginProvider tmdb authentication failed, resetting session_id') + if addon.setting("session_id"): + addon.set_setting("session_id", "") + + def check_login(self) -> bool: """determines if user has an active login (session id) on tmdb when opening a tmdb-based dialog see https://developers.themoviedb.org/3/authentication/how-do-i-generate-a-session-id for the tmdb protocol Note: in api v4 this will become mandatory. Optional in v3 + Note2: checks addon settings for a saved session id first. Returns: bool: true if user has an active session id from tmdb @@ -157,7 +195,7 @@ def check_login(self) -> bool: return False def get_account_id(self) -> str: - """returns TMDB account id + """returns TMDB account id. Requires an active session id Returns: str: the tmdb account id or None @@ -185,6 +223,21 @@ def get_guest_session_id(self) -> str: return None return str(response["guest_session_id"]) + def test_session_id(self, session_id) -> bool: + """tests session_id by getting account_id + If no account_id returned session_id is invalid. + + Args: + session_id (str): a session_id stored in settings + + Returns: + bool: True if session_id got and account_id + """ + response = get_data(url="account", + params={"session_id": session_id}, + cache_days=999999) + return response.get("status_code") != 1 + def get_session_id(self, cache_days=999) -> str: """gets the tmdb session id from addon settings or creates one if not found @@ -196,12 +249,16 @@ def get_session_id(self, cache_days=999) -> str: str: the tmdb session id """ if addon.setting("session_id"): - return addon.setting("session_id") + self.session_id = addon.setting("session_id") + if self.test_session_id(self.session_id): + return addon.setting("session_id") self.create_session_id() return self.session_id def create_session_id(self) -> None: """gets session id from tmdb as self.session_id and saves it in addon settings + 1. get request token from tmdb + 2. create session with login using username/pass """ response = get_data(url="authentication/token/new", cache_days=0) @@ -221,7 +278,7 @@ def create_session_id(self) -> None: def set_rating(media_type, media_id, rating, dbid=None): - ''' + '''Sets rating for a user media item on tmdb account and Kodi userrating media_type: movie, tv or episode media_id: tmdb_id / episode ident array rating: rating value (1 - 10, 0 for deleting) @@ -236,13 +293,13 @@ def set_rating(media_type, media_id, rating, dbid=None): params["session_id"] = Login.get_session_id() else: params["guest_session_id"] = Login.get_guest_session_id() + utils.log('tmdb.set_rating no login use guest session id') if media_type == "episode": if not media_id[1]: media_id[1] = "0" - url = "tv/%s/season/%s/episode/%s/rating" % ( - media_id[0], media_id[1], media_id[2]) + url = f"tv/{media_id[0]}/season/{media_id[1]}/episode/{media_id[2]}/rating" else: - url = "%s/%s/rating" % (media_type, media_id) + url = f"{media_type}/{media_id}/rating" results = send_request(url=url, params=params, values={"value": "%.1f" % @@ -254,16 +311,37 @@ def set_rating(media_type, media_id, rating, dbid=None): def send_request(url, params, values, delete=False): - params["api_key"] = TMDB_KEY + """formats a tmdb api query url + + Args: + url (_type_): _description_ + params (_type_): _description_ + values (_type_): _description_ + delete (bool, optional): request is a post or delete. Defaults to False. + + Returns: + _type_: _description_ + """ + HEADERS['Authorization'] = 'Bearer ' + kodiaddon.decode_string(TMDB_TOKEN, uuick=addon.setting('tmdb_tok')) params = {k: str(v) for k, v in params.items() if v} - url = "%s%s?%s" % (URL_BASE, url, urllib.parse.urlencode(params)) + url = f"{URL_BASE}{url}?{urllib.parse.urlencode(params)}" if delete: return utils.delete(url, values=values, headers=HEADERS) else: return utils.post(url, values=values, headers=HEADERS) -def change_fav_status(media_id=None, media_type="movie", status="true"): +def change_fav_status(media_id=None, media_type="movie", status="true") -> str: + """Updates user favorites list on tmdb + + Args: + media_id (_type_, optional): tmdb id. Defaults to None. + media_type (str, optional): _tmdb medi type. Defaults to "movie". + status (str, optional): tmdb favorite status. Defaults to "true". + + Returns: + str: tmdb result status message + """ session_id = Login.get_session_id() if not session_id: utils.notify("Could not get session id") @@ -271,7 +349,7 @@ def change_fav_status(media_id=None, media_type="movie", status="true"): values = {"media_type": media_type, "media_id": media_id, "favorite": status} - results = send_request(url="account/%s/favorite" % Login.get_account_id(), + results = send_request(url=f"account/{Login.get_account_id()}/favorite", params={"session_id": session_id}, values=values) if results: @@ -293,7 +371,15 @@ def create_list(list_name): return results["list_id"] -def remove_list_dialog(account_lists): +def remove_list_dialog(account_lists) -> bool: + """opens select dialog to remove list from user tmdb lists + + Args: + account_lists (_type_): user's existing tgmdb lists + + Returns: + bool: True if user successfully removed a list + """ index = selectdialog.open(header=addon.LANG(32138), listitems=account_lists) if index >= 0: @@ -302,7 +388,16 @@ def remove_list_dialog(account_lists): def remove_list(list_id): - results = send_request(url="list/%s" % list_id, + """removes user selected list from user's tmdb lists + raises toast with results from tmdb + + Args: + list_id (_type_): id of user tmdb list to remove + + Returns: + _type_: _description_ + """ + results = send_request(url=f"list/{list_id}", params={"session_id": Login.get_session_id()}, values={'media_id': list_id}, delete=True) @@ -312,8 +407,16 @@ def remove_list(list_id): def change_list_status(list_id, movie_id, status): + """adds or removes item from user tmdb list + raises toast with results from tmdb + + Args: + list_id (_type_): the user list to update + movie_id (_type_): th eitem tmdb id to update + status (_type_): add or remove item + """ method = "add_item" if status else "remove_item" - results = send_request(url="list/%s/%s" % (list_id, method), + results = send_request(url=f"list/{list_id}/{method}", params={"session_id": Login.get_session_id()}, values={'media_id': movie_id}) if results: @@ -328,7 +431,7 @@ def get_account_lists(cache_days=0): account_id = Login.get_account_id() if not session_id or not account_id: return [] - response = get_data(url="account/%s/lists" % (account_id), + response = get_data(url=f"account/{account_id}/lists", params={"session_id": session_id}, cache_days=cache_days) return response["results"] @@ -406,7 +509,7 @@ def handle_multi_search(results): return listitems -def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemList: +def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemList[VideoItem]: """takes a list of movies (dicts) and adds local db data and then sorts as an ItemList The tmdb movie keys are converted to extendedinfo keys and genre ids converted to localized text strings, then a VideoItem is created for each movie. The @@ -419,7 +522,7 @@ def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemL sortkey (str, optional): key to sort the movies. Defaults to "year". Returns: - ItemList: a kutils ItemList of the movies to display in a Kodi container + ItemList: a kutils131 ItemList of the movies to display in a Kodi container """ response: dict = get_data(url="genre/movie/list", params={"language": addon.setting("LanguageID")}, @@ -440,7 +543,7 @@ def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemL 'mediatype': "movie", 'country': movie.get('original_language'), 'plot': movie.get('overview'), - 'Trailer': "%splaytrailer&&id=%s" % (PLUGIN_BASE, movie.get("id")), + 'Trailer': f"{PLUGIN_BASE}playtrailer&&id={movie.get('id')}", 'genre': " / ".join([i for i in genres if i]), 'votes': movie.get('vote_count'), 'year': utils.get_year(release_date), @@ -481,7 +584,7 @@ def handle_tvshows(results:list[dict], local_first=True, sortkey="year"): elif len(tv["episode_run_time"]) == 1: duration = "%i" % (tv["episode_run_time"][0]) newtv = VideoItem(label=tv.get('name'), - path=PLUGIN_BASE + 'extendedtvinfo&&id=%s' % tmdb_id) + path=f'{PLUGIN_BASE}extendedtvinfo&&id={tmdb_id}') newtv.set_infos({'originaltitle': tv.get('original_name', ""), 'title': tv.get('name'), 'duration': duration, @@ -510,7 +613,15 @@ def handle_tvshows(results:list[dict], local_first=True, sortkey="year"): return tvshows -def handle_episodes(results): +def handle_episodes(results:list[dict]) -> ItemList[VideoItem]: + """Creates an ItemList of VideoItems for episodes + + Args: + results (_type_): tmdb episode details + + Returns: + _type_: Kutils131 ItemList of episode VideoItmes + """ listitems = ItemList(content_type="episodes") for item in results: title = item.get("name") @@ -535,7 +646,7 @@ def handle_episodes(results): return listitems -def handle_release_dates(results:list[dict]) -> ItemList: +def handle_release_dates(results:list[dict]) -> ItemList[VideoItem]: """Creates ItemList of video mpaa cert and dates as VideoItems Args: @@ -573,7 +684,7 @@ def handle_release_dates(results:list[dict]) -> ItemList: return listitems -def handle_content_ratings(results): +def handle_content_ratings(results:list[dict]) -> ItemList[VideoItem]: listitems = ItemList() for item in results: listitem = VideoItem(label=item['rating']) @@ -583,12 +694,20 @@ def handle_content_ratings(results): return listitems -def handle_reviews(results): +def handle_reviews(results:list[dict]) -> ItemList[VideoItem]: + """Creates an ItemList of VideoItems for tmdb reviews + + Args: + results (_type_): tmdb review details + + Returns: + ItemList: Kutils131 ItemList of review VideoItmes + """ listitems = ItemList() for item in results: listitem = VideoItem(label=item.get('author')) listitem.set_properties({'author': item.get('author'), - 'content': re.sub("", "", item.get('content')).lstrip(), + 'content': _mulitple_repl(item.get('content')).lstrip(), 'id': item.get('id'), 'url': item.get('url')}) listitems.append(listitem) @@ -638,7 +757,15 @@ def handle_lists(results:list[dict]) -> ItemList[VideoItem]: return listitems -def handle_seasons(results): +def handle_seasons(results:list[dict]) -> ItemList[VideoItem]: + """Creates an ItemList of VideoItems for seasons + + Args: + results (_type_): tmdb season details + + Returns: + ItemList: Kutils131 ItemList of season VideoItmes + """ listitems = ItemList(content_type="seasons") for item in results: season = item.get('season_number') @@ -678,8 +805,8 @@ def handle_videos(results:list[dict]) -> ItemList[VideoItem]: return listitems -def handle_people(results: list[dict], select: bool = False) -> ItemList[VideoItem]: - """converts list of tmdb people into kutils videoitems +def handle_people(results:list[dict], select: bool = False) -> ItemList[VideoItem]: + """converts list of tmdb people into kutils131 videoitems The VideoItem properties are tmdb query results Args: @@ -687,7 +814,7 @@ def handle_people(results: list[dict], select: bool = False) -> ItemList[VideoIt select (bool): True if people are to be added to select dialog listing Returns: - ItemList: A kutils ItemList of VideoItems for tmdb persons + ItemList: A kutils131 ItemList of VideoItems for tmdb persons """ people = ItemList(content_type="actors") for item in results: @@ -728,7 +855,7 @@ def handle_images(results:list[dict]) -> ItemList[VideoItem]: results (list[dict]): image list Returns: - ItemList: kutils itemlist of the images as VideoItems type 'music'? + ItemList: kutils131 itemlist of the images as VideoItems type 'music'? """ images = ItemList(content_type="images") for item in results: @@ -880,7 +1007,7 @@ def get_set_id(set_name): return response["results"][0]["id"] -def get_data(url: str = "", params: dict = None, cache_days: float = 14) -> dict: +def get_data(url:str = "", params:dict = None, cache_days:float = 14) -> dict|None: """Queries tmdb api v3 or local cache Args: @@ -895,15 +1022,17 @@ def get_data(url: str = "", params: dict = None, cache_days: float = 14) -> dict TMDB response """ params = params if params else {} - params["api_key"] = TMDB_KEY + HEADERS['Authorization'] = 'Bearer ' + kodiaddon.decode_string(TMDB_TOKEN, uuick=addon.setting('tmdb_tok')) params = {k: str(v) for k, v in params.items() if v} url = "%s%s?%s" % (URL_BASE, url, urllib.parse.urlencode(params)) - response = utils.get_JSON_response(url, cache_days, "TheMovieDB") + response = utils.get_JSON_response(url, cache_days, folder='TheMovieDB', headers=HEADERS) if not response: utils.log("tmdb.get_data No response from TMDB") return None if "status_code" in response and response.get("status_code") != 1: - utils.log(f'tmdb.get_data FAIL TMDB status code: {response.get("status_code")} {traceback.format_stack(limit=-3)}') + utils.log(f'tmdb.get_data FAIL TMDB status code: {response.get("status_code")} - {response.get("status_message")} {traceback.format_stack(limit=-3)}') + if response.get('status_code') == 3: + Login.reset_session_id() return None return response @@ -921,7 +1050,7 @@ def get_company_data(company_id): def get_credit_info(credit_id): if not credit_id: return [] - return get_data(url="credit/%s" % (credit_id), + return get_data(url=f"credit/{credit_id}", params={"language": addon.setting("LanguageID")}, cache_days=30) @@ -1030,14 +1159,13 @@ def extended_movie_info(movie_id=None, dbid=None, cache_days=14) -> tuple[VideoI cache_days (int, optional): Days to use cached info. Defaults to 14. Returns: - tuple: kutils VideoItem of movie info - dict of key str value kutils ItemList + tuple: kutils131 VideoItem of movie info + dict of key str value kutils131 ItemList dict of account states """ if not movie_id: return None - info: dict | None = get_movie( - movie_id=movie_id, cache_days=cache_days) + info: dict | None = get_movie(movie_id=movie_id, cache_days=cache_days) if not info or info.get('success') is False: utils.notify("Could not get tmdb movie information") return (None, None, None) @@ -1064,7 +1192,7 @@ def extended_movie_info(movie_id=None, dbid=None, cache_days=14) -> tuple[VideoI 'writer': " / ".join(authors), 'plot': info.get('overview'), 'originaltitle': info.get('original_title'), - 'Country': info.get('original_language'), + 'country': info.get('original_language'), 'imdbnumber': info.get('imdb_id'), 'genre': " / ".join([i["name"] for i in info["genres"]]), 'year': utils.get_year(info.get("release_date")), @@ -1285,8 +1413,8 @@ def extended_actor_info(actor_id: int) -> tuple[VideoItem, dict[str, ItemList]]: Returns: info[VideoItem]: a populated Kodi listitem - lists[dict]: a dict of kutils Itemlists (one per category) Itemlist is sequence - of kutils VideoItems + lists[dict]: a dict of kutils131 Itemlists (one per category) Itemlist is sequence + of kutils131 VideoItems None: if no results from tmdb """ if not actor_id: @@ -1407,7 +1535,7 @@ def get_fav_items(media_type, sort_by=None, page=1): return itemlist -def get_movies_from_list(list_id, cache_days=5): +def get_movies_from_list(list_id:str, cache_days=5): ''' get movie dict list from tmdb list. ''' @@ -1436,7 +1564,7 @@ def get_actor_credits(actor_id, media_type): def get_movie(movie_id, light=False, cache_days=30) -> dict | None: - """gets details from tmdb for a moview with tmdb movie-id + """gets details from tmdb for a movie with tmdb movie-id Args: movie_id (str): tmdb movie id @@ -1448,17 +1576,16 @@ def get_movie(movie_id, light=False, cache_days=30) -> dict | None: Union[dict, None]: A dict of movie infos. If no response from TMDB returns None """ - params = {"include_image_language": "en,null,%s" % addon.setting("LanguageID"), + params = {"include_image_language": f"en,null,{addon.setting('LanguageID')}", "language": addon.setting("LanguageID"), "append_to_response": None if light else ALL_MOVIE_PROPS } if Login.check_login(): params["session_id"] = Login.get_session_id() - return get_data(url="movie/%s" % (movie_id), + return get_data(url=f"movie/{movie_id}", params=params, cache_days=cache_days) - def get_similar_movies(movie_id): ''' get dict list containing movies similar to *movie_id @@ -1468,7 +1595,6 @@ def get_similar_movies(movie_id): return [] return handle_movies(response["similar"]["results"]) - def get_similar_tvshows(tvshow_id): ''' return list with similar tvshows for show with *tvshow_id (TMDB ID) diff --git a/script.extendedinfo/resources/lib/trakt.py b/script.extendedinfo/resources/lib/trakt.py index c05502d7b..5148f67f6 100644 --- a/script.extendedinfo/resources/lib/trakt.py +++ b/script.extendedinfo/resources/lib/trakt.py @@ -8,14 +8,14 @@ Public functions: get_episodes(content) gets upcoming episodes content shows or premiering shows content premieres - returns a kutils ItemList + returns a kutils131 ItemList get_shows(show_type) gets tvshows for showtype trending/popular/anticipated - returns a kutils ItemList + returns a kutils131 ItemList get_shows_from_time(show_type, period) gets tvshos for showtype collected/played/ watched for previous month - returns a kutils ItemList + returns a kutils131 ItemList get_movies(movie_type) gets movies for movietype trending/popular/anticipated - returns a kutils ItemList + returns a kutils131 ItemList get_movies_from_time(movie_type, period) gets movies forf movietype collected/ played/watched for previous month get_similar(media_type, imdb_id) gets related mediatype show(s)/movie(s) from @@ -49,7 +49,7 @@ def get_episodes(content): content (str): enum shows (upcoming) or premieres (new shows) Returns: - ItemList: a kutils ItemList instance of VideoItems + ItemList: a kutils131 ItemList instance of VideoItems """ shows = ItemList(content_type="episodes") url = "" @@ -72,12 +72,9 @@ def get_episodes(content): ep = episode["episode"] tv = episode["show"] title = ep["title"] if ep["title"] else "" - label = "{0} - {1}x{2}. {3}".format(tv["title"], - ep["season"], - ep["number"], - title) + label = f'{tv["title"]} - {ep["season"]}x{ep["number"]}. {title}' show = VideoItem(label=label, - path=PLUGIN_BASE + 'extendedtvinfo&&tvdb_id=%s' % tv["ids"]["tvdb"]) + path=f'{PLUGIN_BASE}extendedtvinfo&&tvdb_id={tv["ids"]["tvdb"]}') show.set_infos({'title': title, 'aired': ep["first_aired"], 'season': ep["season"], @@ -114,21 +111,20 @@ def get_episodes(content): def handle_movies(results): - """helper function creates kutils VideoItems and adds to an ItemList + """helper function creates kutils131 VideoItems and adds to an ItemList Args: results (list): a list of dicts, each dict is Trakt data for movie Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ movies = ItemList(content_type="movies") path = 'extendedinfo&&id=%s' if addon.bool_setting( "infodialog_onclick") else "playtrailer&&id=%s" for i in results: item = i["movie"] if "movie" in i else i - trailer = "%syoutubevideo&&id=%s" % ( - PLUGIN_BASE, utils.extract_youtube_id(item["trailer"])) + trailer = f'{PLUGIN_BASE}youtubevideo&&id={utils.extract_youtube_id(item["trailer"])}' movie = VideoItem(label=item["title"], path=PLUGIN_BASE + path % item["ids"]["tmdb"]) movie.set_infos({'title': item["title"], @@ -163,20 +159,20 @@ def handle_movies(results): def handle_tvshows(results): - """helper function creates kutils VideoItems and adds to an ItemList + """helper function creates kutils131 VideoItems and adds to an ItemList Args: results (list): a list of dicts, each dict is Trakt data for show Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ shows = ItemList(content_type="tvshows") for i in results: item = i["show"] if "show" in i else i airs = item.get("airs", {}) show = VideoItem(label=item["title"], - path='%sextendedtvinfo&&tvdb_id=%s' % (PLUGIN_BASE, item['ids']["tvdb"])) + path=f'{PLUGIN_BASE}extendedtvinfo&&tvdb_id={item["ids"]["tvdb"]}') show.set_infos({'mediatype': "tvshow", 'title': item["title"], 'duration': item["runtime"] * 60 if item["runtime"] else "", @@ -221,9 +217,9 @@ def get_shows(show_type): show_type (str): enum trending/popular/anticipated Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ - results = get_data(url='shows/%s' % show_type, + results = get_data(url=f'shows/{show_type}', params={"extended": "full"}) return handle_tvshows(results) if results else [] @@ -236,9 +232,9 @@ def get_shows_from_time(show_type, period="monthly"): period (str, optional): enum daily/weekly/monthly/yearly/all Defaults to "monthly" Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ - results = get_data(url='shows/%s/%s' % (show_type, period), + results = get_data(url=f'shows/{show_type}/{period}', params={"extended": "full"}) return handle_tvshows(results) if results else [] @@ -250,9 +246,9 @@ def get_movies(movie_type): movie_type (str): enum trending/popular/anticipated Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ - results = get_data(url='movies/%s' % movie_type, + results = get_data(url=f'movies/{movie_type}', params={"extended": "full"}) return handle_movies(results) if results else [] @@ -265,9 +261,9 @@ def get_movies_from_time(movie_type, period="monthly"): period (str, optional): enum daily/weekly/monthly/yearly/all Defaults to "monthly" Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ - results = get_data(url='movies/%s/%s' % (movie_type, period), + results = get_data(url=f'movies/{movie_type}/{period}', params={"extended": "full"}) return handle_movies(results) if results else [] @@ -280,11 +276,11 @@ def get_similar(media_type, imdb_id): imdb_id (str): the imbd id for show or movie Returns: - ItemList: a kutils ItemList of VideoItems + ItemList: a kutils131 ItemList of VideoItems """ if not imdb_id or not media_type: return None - results = get_data(url='%ss/%s/related' % (media_type, imdb_id), + results = get_data(url=f'{media_type}s/{imdb_id}/related', params={"extended": "full"}) if not results: return None @@ -307,11 +303,11 @@ def get_data(url, params=None, cache_days=10): Returns: dict: a dict from the deserialized JSON response from api or None - Note: kutils does not return the GET failure code (ie if not 200) + Note: kutils131 does not return the GET failure code (ie if not 200) """ params = params if params else {} params["limit"] = 10 - url = "%s%s?%s" % (BASE_URL, url, urllib.parse.urlencode(params)) + url = f"{BASE_URL}{url}?{urllib.parse.urlencode(params)}" return utils.get_JSON_response(url=url, folder="Trakt", headers=HEADERS, diff --git a/script.extendedinfo/resources/lib/windowmanager.py b/script.extendedinfo/resources/lib/windowmanager.py index 2199645b3..23eb9b135 100644 --- a/script.extendedinfo/resources/lib/windowmanager.py +++ b/script.extendedinfo/resources/lib/windowmanager.py @@ -242,8 +242,9 @@ def open_infodialog(self, dialog): """opens the info dialog Args: - dialog (DialogActorInfo): a DialogActorinfo instance of a Kodi dialog - self.info is a kutils.VideoItem or AudioItem to display in dialog + dialog (DialogActorInfo | DialogMovieInfo | DialogTVShowInfo | DialogEpisodeInfo | DialogSeasonInfo): + a Dialog*Info instance of a Kodi dialog + dialog.info is a kutils131.VideoItem or AudioItem to display in dialog """ if dialog.info: self.open_dialog(dialog) @@ -255,7 +256,7 @@ def open_dialog(self, dialog): """Opens a Kodi dialog managing a stack of dialogs Args: - dialog (DialogActorInfo): a Kodi xml dialog window + dialog (DialogVideoList | DialogYoutubeList | Dialog*Info): a Kodi xml dialog window """ if self.active_dialog: self.window_stack.append(self.active_dialog) @@ -274,7 +275,12 @@ def open_dialog(self, dialog): # addon.set_global("infobackground", self.saved_background) # self.window_stack = [] # return None - if self.window_stack and not self.monitor.abortRequested(): +# +# if self.window_stack and not self.monitor.abortRequested(): +# + if self.window_stack: + while not self.monitor.abortRequested() and player.started and not player.stopped: + self.monitor.waitForAbort(2) self.active_dialog = self.window_stack.pop() xbmc.sleep(300) try: diff --git a/script.extendedinfo/resources/settings.xml b/script.extendedinfo/resources/settings.xml index c85574644..b44adf6f0 100644 --- a/script.extendedinfo/resources/settings.xml +++ b/script.extendedinfo/resources/settings.xml @@ -4,6 +4,18 @@ + + 0 + false + 35704479108606 + + false + + + 32172 + true + + 0 true diff --git a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogVideoInfo.xml b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogVideoInfo.xml index f199dd760..e7d75b482 100644 --- a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogVideoInfo.xml +++ b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogVideoInfo.xml @@ -265,7 +265,7 @@ 44 44 !String.IsEmpty(Window.Property(favorite)) - !String.IsEmpty(Window.Property(tmdb_logged_in)) + !String.IsEmpty(Window.Property(tmdb_logged_in)) + !String.IsEmpty(Window.Property(FavButton_Label)) !String.IsEqual(Window.Property(type),season) + !String.IsEqual(Window.Property(type),episode)