From 9af3e7061187da723ece173b60d0d96a0c3f5a17 Mon Sep 17 00:00:00 2001 From: scott967 Date: Tue, 1 Oct 2024 12:27:20 -1000 Subject: [PATCH] [script.extendedinfo] 6.0.9 Bug fix / code improvements / documentation --- script.extendedinfo/README.md | 2 +- script.extendedinfo/addon.xml | 4 +- script.extendedinfo/changelog.txt | 24 +- script.extendedinfo/default.py | 2 + script.extendedinfo/plugin.py | 2 + .../resources/kutil131/busyhandler.py | 16 +- .../resources/kutil131/dialogbaselist.py | 39 ++- .../resources/kutil131/kodiaddon.py | 53 +++ .../resources/kutil131/kodijson.py | 31 +- .../resources/kutil131/listitem.py | 25 +- .../resources/kutil131/localdb.py | 61 +++- .../resources/kutil131/player.py | 2 +- .../resources/kutil131/t9_search.py | 2 +- .../resources/kutil131/utils.py | 51 ++- .../resources/kutil131/youtube.py | 122 ++++--- .../resources/lib/dialogs/dialogactorinfo.py | 8 +- .../resources/lib/dialogs/dialogbaseinfo.py | 45 ++- .../resources/lib/dialogs/dialogtvshowinfo.py | 2 +- .../resources/lib/dialogs/dialogvideoinfo.py | 17 +- .../resources/lib/dialogs/dialogvideolist.py | 28 +- .../lib/dialogs/dialogyoutubelist.py | 208 +++++++++++- script.extendedinfo/resources/lib/process.py | 29 +- .../resources/lib/theaudiodb.py | 4 +- .../resources/lib/themoviedb.py | 308 ++++++++++-------- script.extendedinfo/resources/lib/trakt.py | 153 ++++----- .../resources/lib/windowmanager.py | 39 ++- script.extendedinfo/resources/settings.xml | 60 +++- .../script-script.extendedinfo-DialogInfo.xml | 104 +++++- ...pt-script.extendedinfo-DialogVideoInfo.xml | 79 +++-- .../script-script.extendedinfo-VideoList.xml | 30 +- ...script-script.extendedinfo-YoutubeList.xml | 2 +- 31 files changed, 1124 insertions(+), 428 deletions(-) diff --git a/script.extendedinfo/README.md b/script.extendedinfo/README.md index 2f8ae33dd..3a858d12e 100644 --- a/script.extendedinfo/README.md +++ b/script.extendedinfo/README.md @@ -10,7 +10,7 @@ Example: - keep Attention to the parameter separators ("&&") -### Rotten Tomatoes +### Rotten Tomatoes (No longer available for Kodi users) ``` RunScript(script.extendedinfo,info=intheatermovies) diff --git a/script.extendedinfo/addon.xml b/script.extendedinfo/addon.xml index f7314c232..9e4612035 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 Omega/Nexus/Matrix updates by scott967. Original addon for Leia and prior by phil65 (Philipp Temminghoff). + Python 3 fixes and Piers/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 09184aa84..5483dfd06 100644 --- a/script.extendedinfo/changelog.txt +++ b/script.extendedinfo/changelog.txt @@ -1,3 +1,25 @@ +v6.0.9 +- language handling improved for tmdb. New language settings for + - used by tmdb +- tmdb art now prioritizes the tmdb language in settings +- fixed a logic error when displaying tvshow info dialog for seasons +- fixed caching of query results in local addon_data +- use "playmedia" instead of "runscript" to launch youtube addon +- fix handling of trakt "airing episodes" using current api +- improve design of LoginProvider class to better manage tmdb user logins +- use f-strings instead of %s or .format() style when modifying code +- add documentation (docstrings and type annotations) when modifying code +- make some functions private as needed +- improve debug level logging- fix handling of JSON responses for various urls +- improve query of video database uniqueid +- fix moviedbbrowser +- fix issue where filters not always removed in Video List dialog +- fix live Youtube videos exception on parsing duration P0D +- fix context menu to update local artwork was not available +- fix skinned dialogs not displaying local poster +- fix issue where favorite status for tmdb items not correctly set/unset +- fix classic keyboard (on screen keyboard) input exceptions + v6.0.7 - reworked tmdb logon / accreditation - various code refactoring / pylint items / docstrings (WIP) @@ -7,7 +29,7 @@ v6.0.5 - removed support for youtube-dl - renamed all files to lowercase - various code refactoring to current python 3.8 standards -- remove dependency on kutils (incorporate mdoules within addon) +- remove dependency on kutils (incorporate modules within addon) - addon.xml bump ver 6.0.5 - add menucontrol to skin files - add dependency on autocompletion (required due to changes in autocompletion library) diff --git a/script.extendedinfo/default.py b/script.extendedinfo/default.py index 7a19408ed..b4c5355d9 100644 --- a/script.extendedinfo/default.py +++ b/script.extendedinfo/default.py @@ -64,6 +64,8 @@ def __init__(self): """ utils.log(f"version {addon.VERSION} started") addon.set_global("extendedinfo_running", "true") + if not addon.bool_setting("setting_update_6.0.9"): + addon.update_lang_setting() self._parse_argv() for info in self.infos: listitems = process.start_info_actions(info, self.params) diff --git a/script.extendedinfo/plugin.py b/script.extendedinfo/plugin.py index def23c071..afa97404b 100644 --- a/script.extendedinfo/plugin.py +++ b/script.extendedinfo/plugin.py @@ -32,6 +32,8 @@ def __init__(self): """ utils.log(f"plugin version {addon.VERSION} started") addon.set_global("extendedinfo_running", "true") + if not addon.bool_setting("setting_update_6.0.9"): + addon.update_lang_setting() self._parse_argv() for info in self.infos: listitems = process.start_info_actions(info, self.params) diff --git a/script.extendedinfo/resources/kutil131/busyhandler.py b/script.extendedinfo/resources/kutil131/busyhandler.py index 06440077c..e43b1bb7c 100644 --- a/script.extendedinfo/resources/kutil131/busyhandler.py +++ b/script.extendedinfo/resources/kutil131/busyhandler.py @@ -1,5 +1,11 @@ # Copyright (C) 2015 - Philipp Temminghoff # This program is Free Software see LICENSE file for details +"""_summary_Creates a Busyhandler instance as busyhandler + +Returns: + Busyhandler: manage display of "busy" dialog + Note that kutil131 __init__ imports busyhandler as "busy" +""" import traceback from functools import wraps @@ -15,6 +21,9 @@ class BusyHandler: Class to deal with busydialog handling """ def __init__(self, *args, **kwargs): + """Initializes the handler with no dialog and enabled + self.busy is the nember of active busy requests + """ self.busy = 0 self.enabled = True @@ -41,6 +50,11 @@ def show_busy(self): self.busy += 1 def set_progress(self, percent): + """Not implemented + + Args: + percent (int): 0-99 completion + """ pass def hide_busy(self): @@ -66,7 +80,7 @@ def decorator(cls, *args, **kwargs): result = func(cls, *args, **kwargs) except Exception: utils.log(traceback.format_exc()) - utils.notify("Error", "please contact add-on author") + utils.notify("Busy Error", "please contact add-on author") finally: self.hide_busy() return result diff --git a/script.extendedinfo/resources/kutil131/dialogbaselist.py b/script.extendedinfo/resources/kutil131/dialogbaselist.py index c8187d515..1cd916a25 100644 --- a/script.extendedinfo/resources/kutil131/dialogbaselist.py +++ b/script.extendedinfo/resources/kutil131/dialogbaselist.py @@ -18,6 +18,9 @@ class DialogBaseList: + """ + BaseList for MediaBrowsers (handles filtering, sorting) + """ viewid = { 'WALL 3D' : '67', 'BANNER' : '52', @@ -50,12 +53,6 @@ class DialogBaseList: 'Fanart' : '502' } - - - """ - BaseList for MediaBrowsers (handles filtering, sorting) - """ - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.search_str = kwargs.get('search_str', "") @@ -180,7 +177,7 @@ def open_search(self, control_id): if addon.bool_setting("classic_search"): result = xbmcgui.Dialog().input(heading=addon.LANG(16017), type=xbmcgui.INPUT_ALPHANUM) - if result and result > -1: + if result: self.search(result) else: T9Search(call=self.search, @@ -243,12 +240,13 @@ def set_filter_label(self): build filter label for UI based on active filters """ filters = [] + self.filter_label = '' for item in self.filters: filter_label = item["label"].replace("|", " | ").replace(",", " + ") - filters.append("[COLOR FFAAAAAA]%s:[/COLOR] %s" % (item["typelabel"], filter_label)) + filters.append(f"[COLOR FFAAAAAA]{item['typelabel']}:[/COLOR] {filter_label}") self.filter_label: str = " - ".join(filters) - def update_content(self, force_update=False): + def update_content(self, force_update:bool=False): """ fetch listitems and pagination info based on current state """ @@ -333,10 +331,14 @@ def update(self, force_update=False): self.update_content(force_update=force_update) self.update_ui() - def choose_sort_method(self, sort_key): - """ - open dialog and let user choose sortmethod - returns True if sorthmethod changed + def choose_sort_method(self, sort_key:str) -> bool: + """open dialog and let user choose sortmethod + + Args: + sort_key (str): enum string for sort options movie/tv/favorites/list/rating + + Returns: + bool: True if sorthmethod changed """ listitems = list(self.SORTS[sort_key].values()) sort_strings = list(self.SORTS[sort_key].keys()) @@ -350,12 +352,13 @@ def choose_sort_method(self, sort_key): self.sort_label = listitems[index] return True - def choose_filter(self, filter_code, header, options): + def choose_filter(self, filter_code:str, header:int, options:list[tuple]): """ open dialog and let user choose filter from *options filter gets removed in case value is empty - filter_code: filter code from API - options: list of tuples with 2 items each: first is value, second is label + filter_code(str): filter code from API + header(int): strings.po localized index + options(list[tuple]): list of tuples with 2 items each: first is value, second is label """ values = [i[0] for i in options] labels = [i[1] for i in options] @@ -379,7 +382,7 @@ def toggle_filter(self, filter_code): else: pass # add filter... - def find_filter_position(self, filter_code): + def find_filter_position(self, filter_code:str): """ find position of specific filter in filter list """ @@ -388,7 +391,7 @@ def find_filter_position(self, filter_code): return i return -1 - def remove_filter(self, filter_code): + def remove_filter(self, filter_code:str): """ remove filter with specific filter_code from filter list """ diff --git a/script.extendedinfo/resources/kutil131/kodiaddon.py b/script.extendedinfo/resources/kutil131/kodiaddon.py index 950f3b38f..3184a20bb 100644 --- a/script.extendedinfo/resources/kutil131/kodiaddon.py +++ b/script.extendedinfo/resources/kutil131/kodiaddon.py @@ -13,6 +13,49 @@ HOME = xbmcgui.Window(10000) +TMDB_ISO_639 = {"ar-EG": "Arabic-Egy", +"ar-SA": "Arabic-Sau", +"bg-BG": "Bulgarian", +"ca-ES": "Catalan", +"hr-HR": "Croatian", +"cs-CZ": "Czech", +"da-DK": "Danish", +"nl-BE": "Dutch-Bel", +"nl-NL": "Dutch-Nld", +"en-AU": "English-Aus", +"en-CA": "English-Can", +"en-GB": "English-Gbr", +"en-US": "English-Usa", +"fi-FI": "Finnish", +"fr-CA": "French-Can", +"fr-FR": "French-Fra", +"de-DE": "German", +"el-GR": "Greek", +"he-IL": "Hebrew", +"hi-IN": "Hindi", +"hu-HU": "Hungarian", +"ga-IE": "Irish", +"it-IT": "Italian", +"ja-JP": "Japanese", +"kn-IN": "Kannada", +"ko-KR": "Korean", +"zh-CN": "Mandarin-China", +"zh-SG": "Mandarin-Sgp", +"zh-TW": "Mandarin-Twn", +"no-NO": "Norwegian", +"fa-IR": "Persian", +"pl-PL": "Polish", +"pt-BR": "Portuguese-Bra", +"pt-PT": "Portuguese-Por", +"ru-RU": "Russian", +"sl-SL": "Slovenian", +"es-AR": "Spanish-Arg", +"es-ES": "Spanish-Esp", +"es-MX": "Spanish-Mex", +"sv-SE": "Swedish", +"th-TH": "Thai", +"tr-TR": "Turkish"} + class Addon: """ @@ -91,6 +134,16 @@ def set_global(self, setting_name: str, setting_value: str) ->None: """ HOME.setProperty(setting_name, setting_value) + def update_lang_setting(self) -> None: + """updates user settings from old ISO 639-1 to ISO 639-1-ISO 3166 + """ + old_lang = self.addon.getSetting("LanguageID") + for lang_key in TMDB_ISO_639: + if lang_key.startswith(old_lang): + self.addon.setSetting("LanguageIDv2", lang_key) + self.addon.setSettingBool("setting_update_6.0.9", True) + break + def get_global(self, setting_name): return HOME.getProperty(setting_name) diff --git a/script.extendedinfo/resources/kutil131/kodijson.py b/script.extendedinfo/resources/kutil131/kodijson.py index 44233df12..4c54acf48 100644 --- a/script.extendedinfo/resources/kutil131/kodijson.py +++ b/script.extendedinfo/resources/kutil131/kodijson.py @@ -117,18 +117,31 @@ def get_favourites(): params={"type": None, "properties": ["path", "thumbnail", "window", "windowparameter"]}) -def set_art(media_type, art, dbid): - """ - set artwork via json +def set_art(media_type:str, art:dict, dbid:int) -> dict: + """set artwork via json + + Args: + media_type (str): enum Kodi media type for JSON + art (dict): dict of str arttype : str art URL + dbid (int): Kodi media id from dataabase + + Returns: + dict: the JSON results from Kodi """ - return get_json(method="VideoLibrary.Set%sDetails" % media_type, + return get_json(method=f"VideoLibrary.Set{media_type}Details", params={"art": art, - "%sid" % media_type.lower(): int(dbid)}) + f"{media_type.lower()}id": int(dbid)}) -def get_json(method, params): - """ - communicate with kodi JSON-RPC +def get_json(method:str, params) -> dict: + """communicate with kodi JSON-RPC + + Args: + method (str): the JSON-RPC method + params (dict): the JSON_RPC params + + Returns: + dict: JSON_RPC results """ - json_query = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "%s", "params": %s, "id": 1}' % (method, json.dumps(params))) + json_query = xbmc.executeJSONRPC(f'{{"jsonrpc": "2.0", "method": "{method}", "params": {json.dumps(params)}, "id": 1}}') return json.loads(json_query) diff --git a/script.extendedinfo/resources/kutil131/listitem.py b/script.extendedinfo/resources/kutil131/listitem.py index 8f36ed596..d8d69de81 100644 --- a/script.extendedinfo/resources/kutil131/listitem.py +++ b/script.extendedinfo/resources/kutil131/listitem.py @@ -261,7 +261,13 @@ def get_properties(self): return {k: v for k, v in self._properties.items() if v} def get_listitem(self) -> xbmcgui.ListItem: - #listitem: xbmcgui.ListItem + """Creates a Kodi listitem from kutil131 ListItem -- + handles setting listitem for Kodi Matrix and post-Matrix methods + (videoInfoTag) + + Returns: + xbmcgui.ListItem: the Kodi listitem + """ listitem = xbmcgui.ListItem(label=str(self.label) if self.label else "", label2=str(self.label2) if self.label2 else "", path=self.path) @@ -408,7 +414,7 @@ def __repr__(self): def from_listitem(self, listitem: xbmcgui.ListItem): """ - xbmcgui listitem -> kutils131 listitem + xbmcgui listitem -> kutil131 listitem """ info = listitem.getVideoInfoTag() self.label = listitem.getLabel() @@ -442,7 +448,15 @@ def from_listitem(self, listitem: xbmcgui.ListItem): "imdbnumber": info.getIMDBNumber(), "year": info.getYear()} - def update_from_listitem(self, listitem: ListItem): + def update_from_listitem(self, listitem: ListItem) -> VideoItem: + """Updates a VideoItem from Kodi xbmc.ListItem + + Args: + listitem (xbmcgui.ListItem): a Kodi ListItem + + Returns: + VideoItem: the kutil131 VideoItem with added info + """ if not listitem: return None super().update_from_listitem(listitem) @@ -452,6 +466,11 @@ def update_from_listitem(self, listitem: ListItem): self.set_cast(listitem.cast) def get_listitem(self) -> xbmcgui.ListItem: + """Gets Kodi ListItem and adds video-unique data from VideoItem + + Returns: + xbmcgui.ListItem: the Kodi ListItem using new classes for post-Matrix + """ listitem = super().get_listitem() #Use listitem for Matrix, Nexus complains so use videoinfo tag setters if KODI_MATRIX: diff --git a/script.extendedinfo/resources/kutil131/localdb.py b/script.extendedinfo/resources/kutil131/localdb.py index b08a15e44..9b5861eda 100644 --- a/script.extendedinfo/resources/kutil131/localdb.py +++ b/script.extendedinfo/resources/kutil131/localdb.py @@ -1,6 +1,8 @@ # Copyright (C) 2016 - Philipp Temminghoff # This program is Free Software see LICENSE file for details +from __future__ import annotations + import itertools import json @@ -220,14 +222,19 @@ def handle_movie(self, movie:dict) -> VideoItem: db_movie.set_cast(movie.get("cast")) return db_movie - def handle_tvshow(self, tvshow): - """ - convert tvshow data to listitems + def handle_tvshow(self, tvshow:dict) -> VideoItem: + """converts tvshow Kodi db local data to VideoItem + + Args: + tvshow (dict of Kodi JSON TVShowDetails): + + Returns: + VideoItem: a kutil131 VideoItem for the Kodi db local info """ if addon.setting("infodialog_onclick") != "false": - path = PLUGIN_BASE + 'extendedtvinfo&&dbid=%s' % tvshow['tvshowid'] + path = PLUGIN_BASE + f'extendedtvinfo&&dbid={tvshow["tvshowid"]}' else: - path = PLUGIN_BASE + 'action&&id=ActivateWindow(videos,videodb://tvshows/titles/%s/,return)' % tvshow['tvshowid'] + path = PLUGIN_BASE + f'action&&id=ActivateWindow(videos,videodb://tvshows/titles/{tvshow["tvshowid"]}/,return)' db_tvshow = VideoItem(label=tvshow.get("label"), path=path) db_tvshow.set_infos({'title': tvshow.get('label'), @@ -265,9 +272,14 @@ def get_movie(self, movie_id) -> VideoItem: return self.handle_movie(response["result"]["moviedetails"]) return {} - def get_tvshow(self, tvshow_id): - """ - get info from db for tvshow with *tvshow_id + def get_tvshow(self, tvshow_id:int) -> VideoItem: + """gets info from db for tvshow with *tvshow_id + + Args: + tvshow_id (int): the Kodi tv show dbid + + Returns: + VideoItem: a kutil131 VideoItem for the Kodi local db tv show """ response = kodijson.get_json(method="VideoLibrary.GetTVShowDetails", params={"properties": TV_PROPS, "tvshowid": tvshow_id}) @@ -315,10 +327,18 @@ def get_compare_info(self, media_type="movie", items=None): self.info[media_type] = dct - def merge_with_local(self, media_type, items, library_first=True, sortkey=False): - """ - merge *items from online sources with local db info (and sort) + def merge_with_local(self, media_type:str, items:list[VideoItem], library_first:bool=True, sortkey:str='') -> ItemList[VideoItem]: + """merge *items from online sources with local db info (and sort) works for movies and tvshows + + Args: + media_type (str): enum movie or tvshow + items (list[VideoItem]): list of VideoItems to find Kodi db local info + library_first (bool, optional): Should library items be returned before local items in ItemList. Defaults to True. + sortkey (str, optional):enum key for sorting the ItemList Defaults to ''. + + Returns: + ItemList[VideoItem]: The ItemList sorted and with Kodi db local items added """ get_list = kodijson.get_movies if media_type == "movie" else kodijson.get_tvshows self.get_compare_info(media_type, @@ -406,7 +426,16 @@ def get_artist_mbid(self, dbid): mbid = data['result']['artistdetails'].get('musicbrainzartistid') return mbid if mbid else None - def get_imdb_id(self, media_type, dbid): + def get_imdb_id(self, media_type:str, dbid:int) -> tuple[str,str]: + """gets the imdb id from unique id and title + + Args: + media_type (str): "movie" or "tvshow" + dbid (int): The Kodi video db dbid for the item + + Returns: + tuple: the imdb and title (if not found return null string) + """ if not dbid: return None if media_type == "movie": @@ -414,17 +443,17 @@ def get_imdb_id(self, media_type, dbid): params={"properties": ["uniqueid", "title", "year"], "movieid": int(dbid)}) if "result" in data and "moviedetails" in data["result"]: try: - return data['result']['moviedetails']['uniqueid']['imdb'] + return data['result']['moviedetails']['uniqueid']['imdb'], '' except KeyError: - return None + return '', data['result']['moviedetails']['title'] elif media_type == "tvshow": data = kodijson.get_json(method="VideoLibrary.GetTVShowDetails", params={"properties": ["uniqueid", "title", "year"], "tvshowid": int(dbid)}) if "result" in data and "tvshowdetails" in data["result"]: try: - return data['result']['tvshowdetails']['uniqueid']['imdb'] + return data['result']['tvshowdetails']['uniqueid']['imdb'], '' except KeyError: - return None + return '', data['result']['tvshowdetails']['title'] return None def get_tmdb_id(self, media_type, dbid): diff --git a/script.extendedinfo/resources/kutil131/player.py b/script.extendedinfo/resources/kutil131/player.py index 3a4a73c90..3d1236a3e 100644 --- a/script.extendedinfo/resources/kutil131/player.py +++ b/script.extendedinfo/resources/kutil131/player.py @@ -78,7 +78,7 @@ def wait_for_video_start(self): break if timeout == 0: self.stopped = True - break + break def wait_for_kodivideo_start(self): """Timer called from dialogmovieinfo that checks if Kodi can play selected listitem diff --git a/script.extendedinfo/resources/kutil131/t9_search.py b/script.extendedinfo/resources/kutil131/t9_search.py index 66c2d7604..e6f438c22 100644 --- a/script.extendedinfo/resources/kutil131/t9_search.py +++ b/script.extendedinfo/resources/kutil131/t9_search.py @@ -214,7 +214,7 @@ def use_classic_search(self): self.close() result = xbmcgui.Dialog().input(heading=addon.LANG(16017), type=xbmcgui.INPUT_ALPHANUM) - if result and result > -1: + if result: self.search_str = result self.callback(self.search_str) self.save_autocomplete() diff --git a/script.extendedinfo/resources/kutil131/utils.py b/script.extendedinfo/resources/kutil131/utils.py index a45541d9e..bc9ab9882 100644 --- a/script.extendedinfo/resources/kutil131/utils.py +++ b/script.extendedinfo/resources/kutil131/utils.py @@ -1,5 +1,8 @@ # Copyright (C) 2015 - Philipp Temminghoff # This program is Free Software see LICENSE file for details + +from __future__ import annotations + import datetime import hashlib import json @@ -198,9 +201,14 @@ def async_func(*args, **kwargs): return async_func -def contextmenu(options): - """ - pass list of tuples (index, label), get index +def contextmenu(options:list[tuple]) -> str: + """pass list of tuples (index, label), get index + + Args: + options (list[tuple]): the context menu items with display text + + Returns: + str: the selected option or None if nothing selected """ index = xbmcgui.Dialog().contextmenu(list=[i[1] for i in options]) if index > -1: @@ -400,9 +408,16 @@ def get_http(url, headers=False): return None -def post(url, values, headers): - """ - retuns answer to post request +def post(url:str, values:dict, headers:str) -> dict: + """retuns answer to post request {'success' : True} if succeeded + + Args: + url (str): the api endpoint with query (if any) + values (dict): the body content for the post + headers (str): the http headers + + Returns: + dict: results from server for the post """ try: request = requests.post(url=url, @@ -414,7 +429,7 @@ def post(url, values, headers): return json.loads(request.text) -def delete(url, values, headers): +def delete(url:str, values:dict, headers:str) ->dict: """ returns answer to delete request """ @@ -428,7 +443,7 @@ def delete(url, values, headers): return json.loads(request.text) -def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> dict: +def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> list[dict] | dict: """gets JSON response for *url, makes use of prop and file cache. Args: @@ -438,7 +453,7 @@ def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> di headers (bool, optional): headers to use in https request. Defaults to False. Returns: - dict: a deserialized JSON query response or None + list[dict]: a deserialized JSON query response or None """ now = time.time() hashed_url = hashlib.md5(url.encode("utf-8", "ignore")).hexdigest() @@ -448,6 +463,7 @@ def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> di addon.clear_global(hashed_url) addon.clear_global(hashed_url + "_timestamp") prop_time = addon.get_global(hashed_url + "_timestamp") + # get data from home window property if prop_time and now - float(prop_time) < cache_seconds: try: prop = json.loads(addon.get_global(hashed_url)) @@ -455,18 +471,27 @@ def get_JSON_response(url="", cache_days=7.0, folder=False, headers=False) -> di return prop except Exception: pass + # get data from local disk cache file 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) + #for trakt acticipatedmovies results is list of dict per movie else: + #log(f'kutil131.utils.get_JSON_response get_http headers {headers}') #debug + # data not cached query online source response = get_http(url, headers) try: results = json.loads(response) - # utils.log("download %s. time: %f" % (url, time.time() - now)) - if "status_code" in results and results.get("status_code") == 1: + if folder == 'TheMovieDB': + if ("results" in results): + # utils.log("download %s. time: %f" % (url, time.time() - now)) + if (("status_code" in results and results.get("status_code") == 1) or + not ("status_code" in results)): + save_to_file(results, hashed_url, cache_path) + else: save_to_file(results, hashed_url, cache_path) except Exception as err: - log(f"Exception: Could not get new JSON data from {url} " + log(f"kutil131.utils.get_JSON_response 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 [] @@ -546,7 +571,7 @@ def fetch_musicbrainz_id(artist, artist_id=-1): cache_days=30, folder="MusicBrainz") if results and len(results["artists"]) > 0: - log(f'found artist id for {artist}: {results["artists"][0]["id"]}') + #log(f'kutil131.utils.fetch_mbid found artist id for {artist}: {results["artists"][0]["id"]}') return results["artists"][0]["id"] else: return None diff --git a/script.extendedinfo/resources/kutil131/youtube.py b/script.extendedinfo/resources/kutil131/youtube.py index 52a813e30..9e7d155ae 100644 --- a/script.extendedinfo/resources/kutil131/youtube.py +++ b/script.extendedinfo/resources/kutil131/youtube.py @@ -1,6 +1,17 @@ # Copyright (C) 2015 - Philipp Temminghoff # This program is Free Software see LICENSE file for details +"""Handles youtube queries using youtube api +See: https://developers.google.com/youtube/v3/docs + +Public functions: + get_duration_in_seconds: duration of a youtube video as int seconds + get_formatted_duration: duration of youtube video as HH:MM:SS string + search: build youtube query for youtbe api and _get_data to return ItemList of VideoItems + get_playlist_videos: Gets an ItemList of youtube videos for a playlist + get_user_playlists: Gets an itemlist of user youtube videos +""" + from __future__ import annotations import html import itertools @@ -14,22 +25,22 @@ PLUGIN_BASE = "plugin://script.extendedinfo/?info=" -def handle_videos(results:list[dict], extended=False, api_key=''): +def _handle_videos(results:list[dict], extended=False, api_key='') -> ItemList[VideoItem]: """ - Process video api result to ItemList + Process video api results to ItemList :param api_key: api_key to pass to YouTube """ - videos = ItemList(content_type="videos") + videos:ItemList[VideoItem] = ItemList(content_type="videos") for item in results: snippet = item["snippet"] thumb = snippet["thumbnails"]["high"]["url"] if "thumbnails" in snippet else "" try: video_id = item["id"]["videoId"] - except Exception: + except AttributeError: video_id = snippet["resourceId"]["videoId"] video = VideoItem(label=html.unescape(snippet["title"]), - path=PLUGIN_BASE + 'youtubevideo&&id=%s' % video_id) + path=f'{PLUGIN_BASE}youtubevideo&&id={video_id}') video.set_infos({'plot': html.unescape(snippet["description"]), 'mediatype': "video", 'premiered': snippet["publishedAt"][:10]}) @@ -45,7 +56,7 @@ def handle_videos(results:list[dict], extended=False, api_key=''): params = {"part": "contentDetails,statistics", "id": ",".join([i.get_property("youtube_id") for i in videos]), "key": api_key} - ext_results = get_data(method="videos", + ext_results = _get_data(method="videos", params=params) if not ext_results or not 'items' in ext_results.keys(): return videos @@ -74,11 +85,13 @@ def handle_videos(results:list[dict], extended=False, api_key=''): break return videos - def get_duration_in_seconds(duration:str) -> int: """ convert youtube duration string to seconds int """ + #utils.log(f'kutil131.youtube.get_duraction_in_secs duration {duration}') #debug + if duration == ('P0D' or 'P0D0S'): #live stream so no duration + return 0 if not duration.endswith('S'): duration = duration + '0S' try: @@ -90,24 +103,24 @@ def get_duration_in_seconds(duration:str) -> int: else: return int(duration[0]) except Exception as err: - utils.log(f'kutils131.youtube unable decode youtube duration of {duration} error {err}') + utils.log(f'kutil131.youtube unable decode youtube duration of {duration} error {err}') return 0 - -def get_formatted_duration(duration): +def get_formatted_duration(duration:str) -> str: """ convert youtube duration string to formatted duration """ - duration = duration[2:-1].replace("H", "M").split("M") + if duration == ('P0D' or 'P0D0S'): #live stream so no duration + return "00:00" + duration:list = duration[2:-1].replace("H", "M").split("M") if len(duration) == 3: - return "{}:{}:{}".format(duration[0].zfill(2), duration[1].zfill(2), duration[2].zfill(2)) + return f"{duration[0].zfill(2)}:{duration[1].zfill(2)}:{duration[2].zfill(2)}" elif len(duration) == 2: - return "{}:{}".format(duration[0].zfill(2), duration[1].zfill(2)) + return f"{duration[0].zfill(2)}:{duration[1].zfill(2)}" else: - return "00:{}".format(duration[0].zfill(2)) + return f"00:{duration[0].zfill(2)}" - -def handle_playlists(results, api_key=''): +def _handle_playlists(results, api_key=''): """ process playlist api result to ItemList @@ -123,7 +136,7 @@ def handle_playlists(results, api_key=''): except Exception: playlist_id = snippet["resourceId"]["playlistId"] playlist = VideoItem(label=snippet["title"], - path=PLUGIN_BASE + 'youtubeplaylist&&id=%s' % playlist_id) + path=f'{PLUGIN_BASE}youtubeplaylist&&id={playlist_id}') playlist.set_infos({'plot': snippet["description"], "mediatype": "video", 'premiered': snippet["publishedAt"][:10]}) @@ -136,15 +149,14 @@ def handle_playlists(results, api_key=''): params = {"id": ",".join([i.get_property("youtube_id") for i in playlists]), "part": "contentDetails", "key": api_key} - ext_results = get_data(method="playlists", + ext_results = _get_data(method="playlists", params=params) for item, ext_item in itertools.product(playlists, ext_results["items"]): if item.get_property("youtube_id") == ext_item['id']: item.set_property("itemcount", ext_item['contentDetails']['itemCount']) return playlists - -def handle_channels(results, api_key=''): +def _handle_channels(results, api_key=''): """ process channel api result to ItemList @@ -160,7 +172,7 @@ def handle_channels(results, api_key=''): except Exception: channel_id = snippet["resourceId"]["channelId"] channel = VideoItem(label=html.unescape(snippet["title"]), - path=PLUGIN_BASE + 'youtubechannel&&id=%s' % channel_id) + path=f'{PLUGIN_BASE}youtubechannel&&id={channel_id}') channel.set_infos({'plot': html.unescape(snippet["description"]), 'mediatype': "video", 'premiered': snippet["publishedAt"][:10]}) @@ -172,7 +184,7 @@ def handle_channels(results, api_key=''): params = {"id": ",".join(channel_ids), "part": "contentDetails,statistics,brandingSettings", "key": api_key} - ext_results = get_data(method="channels", + ext_results = _get_data(method="channels", params=params) for item, ext_item in itertools.product(channels, ext_results["items"]): if item.get_property("youtube_id") == ext_item['id']: @@ -180,28 +192,50 @@ def handle_channels(results, api_key=''): item.set_art("fanart", ext_item["brandingSettings"]["image"].get("bannerTvMediumImageUrl")) return channels +def _get_data(method:str, params:dict=None, cache_days:float=0.5) -> dict | None: + """Formats youtube query and returns youtube search results or None -def get_data(method, params=None, cache_days=0.5): - """ - fetch data from youtube API + Args: + method (str): youtube method -- + search: Returns a collection of search results that match the query parameters specified in the API request + A search result set identifies matching video, channel, and playlist resources + playlists: Returns a collection of playlists that match the API request parameters + playlistItems: Returns a collection of playlist items that match the API request parameters + channels: Returns a collection of zero or more channel resources that match the request criteria + videos: Returns a list of videos that match the API request parameters + params (dict, optional): youtube filters. See youtube API and DialogYoutubeList. Defaults to None. + cache_days (float, optional): period cached results are valid. Defaults to 0.5. + + Returns: + dict or None: Youtube search results videos """ params = params if params else {} -# params["key"] = YT_KEY params = {k: str(v) for k, v in iter(params.items()) if v} - url = "{base_url}{method}?{params}".format(base_url=BASE_URL, - method=method, - params=urllib.parse.urlencode(params)) + url = f"{BASE_URL}{method}?{urllib.parse.urlencode(params)}" return utils.get_JSON_response(url=url, cache_days=cache_days, folder="YouTube") - def search(search_str="", hd="", orderby="relevance", limit=40, extended=True, - page="", filters=None, media_type="video", api_key="") -> ItemList: - """ - returns ItemList according to search term, filters etc. + page="", filters:dict=None, media_type="video", api_key="") -> ItemList[VideoItem]: + """Runs youtube search method using parameters and filters - :param api_key: api_key to pass to YouTube + Args: + search_str (str, optional): youtube search string. + Can also use the Boolean NOT (-) and OR (| URL-escaped) operators. Defaults to "". + hd (str, optional): true/false hd (>=720) video. Defaults to "". + orderby (str, optional): results sort order. Defaults to "relevance". + date, rating, relevance, title, videoCount (for channels) + viewCount + limit (int, optional): videos to return. Defaults to 40. + extended (bool, optional): return extended meta data. Defaults to True. + page (str, optional): specific page in the result set that should be returned. Defaults to "". + filters (dict, optional): _description_. Defaults to None. + media_type (str, optional): video/playlist/channel. Defaults to "video". + api_key (str): user youtube api key (from setting). Defaults to "". + + Returns: + ItemList: kutil131 ItemList of VideoItems """ params = {"part": "id,snippet", "maxResults": limit, @@ -211,10 +245,10 @@ def search(search_str="", hd="", orderby="relevance", limit=40, extended=True, "hd": str(hd and not hd == "false"), "q": search_str.replace('"', ''), "key" : api_key} - results = get_data(method="search", + results = _get_data(method="search", params=utils.merge_dicts(params, filters if filters else {})) - if 'error' in results.keys(): - utils.log('youtube get_data ERROR: {error}'.format(error=results.get('error').get('message'))) + if results and ('error' in results.keys()): + utils.log(f'youtube _get_data ERROR: {results.get("error").get("message")}') if not results or 'items' not in results.keys(): return None @@ -223,18 +257,17 @@ def search(search_str="", hd="", orderby="relevance", limit=40, extended=True, listitems: ItemList = ItemList() if media_type == "video": - listitems = handle_videos(results["items"], extended=extended, api_key=api_key) + listitems = _handle_videos(results["items"], extended=extended, api_key=api_key) elif media_type == "playlist": - listitems = handle_playlists(results["items"], api_key=api_key) + listitems = _handle_playlists(results["items"], api_key=api_key) elif media_type == "channel": - listitems = handle_channels(results["items"], api_key=api_key) + listitems = _handle_channels(results["items"], api_key=api_key) listitems.total_pages = results["pageInfo"]["resultsPerPage"] listitems.totals = results["pageInfo"]["totalResults"] listitems.next_page_token = results.get("nextPageToken", "") listitems.prev_page_token = results.get("prevPageToken", "") return listitems - def get_playlist_videos(playlist_id=""): """ returns ItemList from playlist with *playlist_id @@ -244,12 +277,11 @@ def get_playlist_videos(playlist_id=""): params = {"part": "id,snippet", "maxResults": "50", "playlistId": playlist_id} - results = get_data(method="playlistItems", + results = _get_data(method="playlistItems", params=params) if not results: return [] - return handle_videos(results["items"]) - + return _handle_videos(results["items"]) def get_user_playlists(username=""): """ @@ -257,7 +289,7 @@ def get_user_playlists(username=""): """ params = {"part": "contentDetails", "forUsername": username} - results = get_data(method="channels", + results = _get_data(method="channels", params=params) if not results["items"]: return None diff --git a/script.extendedinfo/resources/lib/dialogs/dialogactorinfo.py b/script.extendedinfo/resources/lib/dialogs/dialogactorinfo.py index bd4e6be9e..b8a9903e3 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogactorinfo.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogactorinfo.py @@ -4,17 +4,17 @@ """Provides the DialogActorInfo class that implements a dialog XML window. The Actor Info is added to the window properties -from a kutils VideoItem. Panels of VideoItems are added from -kutils ItemLists and a Youtube list. +from a kutil131 VideoItem. Panels of VideoItems are added from +kutil131 ItemLists and a Youtube list. The class hierarchy is: xbmcgui.Window -------------- xbmcgui.WindowXML / xbmcgui.WindowDialogMixin --------------- - xbmc.WindowDialogXML / kutils.windows.WindowMixin + xbmc.WindowDialogXML / kutil131.windows.WindowMixin --------------- - kutils.windows.DialogXML + kutil131.windows.DialogXML --------------- DialogBaseInfo --------------- diff --git a/script.extendedinfo/resources/lib/dialogs/dialogbaseinfo.py b/script.extendedinfo/resources/lib/dialogs/dialogbaseinfo.py index 3ed1b5789..51b4936dd 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogbaseinfo.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogbaseinfo.py @@ -26,31 +26,31 @@ class DialogBaseInfo(windows.DialogXML): dialog types (eg actor info or movie info) Args: - windows.DialogXML (DialogXML): a kutils class derived from xbmcgui.WindowXMLDialog - and kutils WindowMixin classes + windows.DialogXML (DialogXML): a kutil131 class derived from xbmcgui.WindowXMLDialog + and kutil131 WindowMixin classes Returns: - _type_: _description_ + DialogBaseInfo: class instance """ ACTION_PREVIOUS_MENU = [92, 9] ACTION_EXIT_SCRIPT = [13, 10] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.logged_in: bool = tmdb.Login.check_login() + self.logged_in: bool = tmdb.tmdb_login.check_login() self.bouncing = False self.last_focus = None self.lists = None self.states = False self.yt_listitems = [] - self.info = VideoItem() # kutils listitem + self.info = VideoItem() # kutil131 listitem self.last_control = None self.last_position = None def onInit(self, *args, **kwargs): super().onInit() # self.set_buttons() - self.info.to_windowprops(window_id=self.window_id) #kutils sets dialog window + self.info.to_windowprops(window_id=self.window_id) #kutil131 sets dialog window #properties from the info VideoItem(listitem) for container_id, key in self.LISTS: try: @@ -111,8 +111,8 @@ def bounce(self, identifier): self.clearProperty("Bounce.%s" % identifier) self.bouncing = False - @ch.click_by_type("music") - # hack: use "music" until "pictures" got added to core + @ch.click_by_type("song") + # hack: use "song" was "music" until "pictures" got added to core def open_image(self, control_id): key = [key for container_id, key in self.LISTS if container_id == control_id][0] @@ -148,9 +148,19 @@ def open_episode_info(self, control_id): season=info.getSeason(), episode=info.getEpisode()) - @ch.context("music") - def thumbnail_options(self, control_id): - listitem = self.FocusedItem(control_id) + #@ch.context("music") not working testing "song" as hack + @ch.context("song") + def thumbnail_options(self, control_id:int) -> None: + """sets a Kodi library item poster or fanart from tmdb art + + Args: + control_id (int): the dialog window control id that has focus (image) + + Returns: + None + """ + utils.log(f'DialogBaseInfo thumbnail_options called for contextmenu song with control id {control_id}') + listitem:xbmcgui.ListItem = self.FocusedItem(control_id) art_type = listitem.getProperty("type") options = [] if self.info.get_info("dbid") and art_type == "poster": @@ -164,9 +174,11 @@ def thumbnail_options(self, control_id): return None action = utils.contextmenu(options=options) if action == "db_art": - kodijson.set_art(media_type=self.getProperty("type"), - art={art_type: listitem.get_art("original")}, + art_result = kodijson.set_art(media_type=self.getProperty("type"), + art={art_type: listitem.getArt("original")}, dbid=self.info.get_info("dbid")) + if art_result and art_result.get('result') == 'OK': + utils.notify(addon.NAME, f'{addon.LANG(32119)} / {xbmc.getLocalizedString(24138)}') elif action == "movie_info": wm.open_movie_info(movie_id=listitem.getProperty("movie_id"), dbid=listitem.getVideoInfoTag().getDbId()) @@ -178,6 +190,7 @@ def video_context_menu(self, control_id): #utils.download_video(self.FocusedItem( # control_id).getProperty("youtube_id")) pass + utils.notify(addon.NAME, xbmc.getLocalizedString(10005)) @ch.context("movie") def movie_context_menu(self, control_id): @@ -269,7 +282,7 @@ def get_youtube_vids(self, search_str): try: youtube_list = self.getControl(ID_LIST_YOUTUBE) except Exception as err: - utils.log(f'DialogBaseInfo.get_youtube_vids threw exception {err}') + utils.log(f'DialogBaseInfo.get_youtube_vids getControl for ID_LIST_YOUTUBE threw exception {err}') return None if not self.yt_listitems: user_key = addon.setting("Youtube API Key") @@ -299,11 +312,11 @@ def open_credit_dialog(self, credit_id): return None listitem = listitems[index] if listitem["mediatype"] == "episode": - wm.open_episode_info(season=listitem["season"], + wm.open_episode_info(season=int(listitem["season"]), episode=listitem["episode"], tvshow_id=info["media"]["id"]) elif listitem["mediatype"] == "season": - wm.open_season_info(season=listitem["season"], + wm.open_season_info(season=int(listitem["season"]), tvshow_id=info["media"]["id"]) def update_states(self): diff --git a/script.extendedinfo/resources/lib/dialogs/dialogtvshowinfo.py b/script.extendedinfo/resources/lib/dialogs/dialogtvshowinfo.py index 898ccef12..0b372e3e6 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogtvshowinfo.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogtvshowinfo.py @@ -84,7 +84,7 @@ def browse_tvshow(self, control_id): @ch.click(ID_LIST_SEASONS) def open_season_dialog(self, control_id): - info = self.FocusedItem(control_id).getVideoInfoTag() + info:xbmc.InfoTagVideo = self.FocusedItem(control_id).getVideoInfoTag() wm.open_season_info(tvshow_id=self.info.get_property("id"), season=info.getSeason(), tvshow=self.info.get_info("title")) diff --git a/script.extendedinfo/resources/lib/dialogs/dialogvideoinfo.py b/script.extendedinfo/resources/lib/dialogs/dialogvideoinfo.py index 0ff42769a..dae4f06f5 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogvideoinfo.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogvideoinfo.py @@ -23,12 +23,14 @@ class DialogVideoInfo(DialogBaseInfo): """ - + handles click events for items common for all video types Args: - DialogBaseInfo (_type_): _description_ + DialogBaseInfo (class): The parent class for info dialogs """ def __init__(self, *args, **kwargs): + """runs __init__ on DialogBaseClass + """ super().__init__(*args, **kwargs) def onClick(self, control_id): @@ -61,10 +63,17 @@ def show_manage_dialog(self, control_id): xbmc.executebuiltin(item) @ch.click(ID_BUTTON_FAV) - def change_list_status(self, control_id): + def change_list_status(self, control_id:int): + """toggles "starred" or "unstarred" on tmdb user fav list + status false means not a favorite. The tmdb endpoint is a post + to account/{account_id}/favorite + + Args: + control_id (int): the control id that was clicked + """ tmdb.change_fav_status(media_id=self.info.get_property("id"), media_type=self.TYPE_ALT, - status=str(not bool(self.states["favorite"])).lower()) + status=not bool(self.states["favorite"])) self.update_states() @ch.click(ID_BUTTON_SETRATING) diff --git a/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py b/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py index 78e7d3fd7..3b9735eb5 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogvideolist.py @@ -37,22 +37,22 @@ def get_window(window_type): - """Wrapper gets new DialogVideoList instance + """Wrapper gets new DialogVideoList class Args: window_type (class instance): xbmc XML dialog window or xbmc XML window objects Returns: - [DialogVideoList]: a new XML dialog or window + type[DialogVideoList]: a new XML dialog or window class """ class DialogVideoList(DialogBaseList, window_type): """Dialog Video List class Args: - DialogBaseList: Super class for dialog windows - window_type (kutils.windows class): Super class for Kodi xbmc.WindowXML + DialogBaseList: DialogXML or WindowXML super class for dialog windows + window_type (kutil131.windows class): DialogXML or WindowXML super class for Kodi xbmc.WindowXML """ @@ -103,7 +103,7 @@ class DialogVideoList(DialogBaseList, window_type): def __init__(self, *args, **kwargs): self.type = kwargs.get('type', "movie") self.list_id = kwargs.get("list_id", False) - self.logged_in = tmdb.Login.check_login() + self.logged_in = tmdb.tmdb_login.check_login() super().__init__(*args, **kwargs) def onClick(self, control_id): @@ -201,6 +201,10 @@ def default_sort(self): @ch.click(ID_BUTTON_SORT) def get_sort_type(self, control_id): + if self.sort_label and (self.sort_label == "Vote average"): + update_filter_vote = True + else: + update_filter_vote = False if not self.choose_sort_method(self.sort_key): return None if self.sort == "vote_average": @@ -208,6 +212,8 @@ def get_sort_type(self, control_id): value="10", label="10", reset=False) + elif update_filter_vote: + self.remove_filter(key="vote_count.gte") self.update() def add_filter(self, **kwargs): @@ -220,6 +226,14 @@ def add_filter(self, **kwargs): super().add_filter(force_overwrite=kwargs["key"].endswith((".gte", ".lte")), **kwargs) + def remove_filter(self, **kwargs): + """removes the vote_count filter if added by sort method vote_average + + kwargs[key] (str): the filter key to be removed + """ + if kwargs["key"] == 'vote_count.gte': + super().remove_filter(kwargs["key"]) + @ch.click(ID_BUTTON_ORDER) def toggle_order(self, control_id): self.order = "desc" if self.order == "asc" else "asc" @@ -262,7 +276,7 @@ def open_account_menu(self, control_id): @ch.click(ID_BUTTON_GENREFILTER) def set_genre_filter(self, control_id): - params = {"language": addon.setting("LanguageID")} + params = {"language": addon.setting("LanguageIDv2")} response = tmdb.get_data(url="genre/%s/list" % (self.type), params=params, cache_days=100) @@ -475,7 +489,7 @@ def fetch_data(self, force=False): # TODO: rewrite else: #self.mode == "filter" self.set_filter_label() params = {"sort_by": sort_by, - "language": addon.setting("LanguageID"), + "language": addon.setting("LanguageIDv2"), "page": self.page, "include_adult": include_adult} filters = {item["type"]: item["id"] for item in self.filters} diff --git a/script.extendedinfo/resources/lib/dialogs/dialogyoutubelist.py b/script.extendedinfo/resources/lib/dialogs/dialogyoutubelist.py index cc3206ef9..eeaaf3e29 100644 --- a/script.extendedinfo/resources/lib/dialogs/dialogyoutubelist.py +++ b/script.extendedinfo/resources/lib/dialogs/dialogyoutubelist.py @@ -2,6 +2,8 @@ # Modifications copyright (C) 2022 - Scott Smart # This program is Free Software see LICENSE file for details +from __future__ import annotations + import datetime import xbmcgui @@ -21,16 +23,148 @@ ID_BUTTON_DEFINITIONFILTER = 5012 ID_BUTTON_TYPEFILTER = 5013 - -def get_window(window_type): +ISO_639_1_CODES = { + "ar-ae": "Arabic (U.A.E.)", + "ar-bh": "Arabic (Bahrain)", + "ar-dz": "Arabic (Algeria)", + "ar-eg": "Arabic (Egypt)", + "ar-sa": "Arabic (Saudi Arabia)", + "ar-sy": "Arabic (Syria)", + "ar": "Arabic", + "be": "Belarusian", + "bg": "Bulgarian", + "bn": "Bengali", + "bs": "Bosnian", + "cs": "Czech", + "da": "Danish", + "de-at": "German (Austria)", + "de-ch": "German (Switzerland)", + "de-de": "German (Germany)", + "de-li": "German (Liechtenstein)", + "de-lu": "German (Luxembourg)", + "de": "German", + "el": "Greek", + "en-au": "English (Australia)", + "en-ca": "English (Canada)", + "en-gb": "English (United Kingdom)", + "en-nz": "English (New Zealand)", + "en-us": "English (United States)", + "en-za": "English (South Africa)", + "en": "English", + "es-ar": "Spanish (Argentina)", + "es-cl": "Spanish (Chile)", + "es-es": "Spanish (Spain)", + "es-mx": "Spanish (Mexico)", + "es-pe": "Spanish (Peru)", + "es-pr": "Spanish (Puerto Rico)", + "es-us": "Spanish (United States)", + "es": "Spanish", + "et": "Estonian", + "fa": "Farsi", + "fi": "Finnish", + "fr-be": "French (Belgium)", + "fr-ca": "French (Canada)", + "fr-ch": "French (Switzerland)", + "fr-fr": "French (France)", + "fr-lu": "French (Luxembourg)", + "fr-mc": "French (Monaco)", + "fr": "French", + "ga": "Irish", + "he": "Hebrew", + "hi": "Hindi", + "hr-ba": "Croatian (Bosnia and Herzegovina)", + "hr-hr": "Croatian (Croatia)", + "hr": "Croatian", + "ht": "Haitian", + "hu": "Hungarian", + "hy": "Armenian", + "id": "Indonesian", + "in": "Indonesian", + "is": "Icelandic", + "it-ch": "Italian (Switzerland)", + "it-it": "Italian (Italy)", + "it": "Italian", + "iw": "Hebrew", + "ja": "Japanese", + "km": "Cambodian", + "kn": "Kannada", + "ko": "Korean", + "ks": "Kashmiri", + "ku": "Kurdish", + "ls": "Slovenian", + "lt": "Lithuanian", + "lv": "Latvian", + "ml": "Malayalam", + "ms-my": "Malay (Malaysia)", + "ms": "Malay", + "my": "Burmese", + "nl-be": "Dutch (Belgium)", + "nl-nl": "Dutch (Netherlands)", + "nl": "Dutch", + "nn": "Norwegian (Nynorsk)", + "no": "Norwegian", + "pa": "Punjabi", + "pl": "Polish", + "pt-br": "Portuguese (Brazil)", + "pt-pt": "Portuguese (Portugal)", + "pt": "Portuguese", + "ro": "Romanian", + "ru": "Russian", + "se-fi": "Sami (Finland)", + "se-no": "Sami (Norway)", + "se-se": "Sami (Sweden)", + "se": "Sami", + "sh": "Serbo-Croatian", + "sk": "Slovak", + "sl": "Slovenian", + "sq": "Albanian", + "sr-ba": "Serbian (Bosnia and Herzegovina)", + "sr-sp": "Serbian (Serbia and Montenegro)", + "sr": "Serbian", + "sv-fi": "Swedish (Finland)", + "sv-se": "Swedish (Sweden)", + "sv": "Swedish", + "ta": "Tamil", + "th": "Thai", + "tr": "Turkish", + "uk": "Ukrainian", + "vi": "Vietnamese", + "zh-cn": "Chinese (China)", + "zh-hk": "Chinese (Hong Kong SAR)", + "zh-mo": "Chinese (Macau SAR)", + "zh-sg": "Chinese (Singapore)", + "zh-tw": "Chinese (Taiwan)", + "zh": "Chinese", +} + + +def get_window(window_type:type[xbmcgui.WindowXML]) -> type[DialogBaseList]: + """Creates a DialogYoutubeList class inheriting from window_type class + + Args: + window_type (xbmcgui.WindowXML): WindowXML or one of its subclasses (DialogXML) + + Returns: + DialogYoutubeList class: Class inheriting from WindowXML or its subclasses + """ class DialogYoutubeList(DialogBaseList, window_type): + """Creates WindowXML dialog + + Args: + DialogBaseList (class): Parent class for dialog + window_type (class): xbmcgui parent class + + Returns: + DialogYoutubeList: dialog XML window + """ TYPES = ["video", "playlist", "channel"] FILTERS = {"channelId": addon.LANG(19029), "publishedAfter": addon.LANG(172), "regionCode": addon.LANG(248), + "relevanceLanguage": addon.LANG(248), "videoDimension": addon.LANG(32057), "videoDuration": addon.LANG(180), "videoCaption": addon.LANG(287), @@ -81,7 +215,12 @@ def onAction(self, action): ch.serve_action(action, self.getFocusId(), self) @ch.click_by_type("video") - def main_list_click(self, control_id): + def main_list_click(self, control_id: int): + """handles user select action on container listitem + + Args: + control_id (int): control id of focused container + """ listitem = self.FocusedItem(control_id) youtube_id = listitem.getProperty("youtube_id") media_type = listitem.getProperty("type") @@ -96,6 +235,14 @@ def main_list_click(self, control_id): @ch.click(ID_BUTTON_PUBLISHEDFILTER) def set_published_filter(self, control_id): + """Sets youtube filter publishedAfter based on user input day/week/month/year/custom + + Args: + control_id (_type_): _description_ + + Returns: + None + """ options = [(1, addon.LANG(32062)), (7, addon.LANG(32063)), (31, addon.LANG(32064)), @@ -120,13 +267,21 @@ def set_published_filter(self, control_id): @ch.click(ID_BUTTON_LANGUAGEFILTER) def set_language_filter(self, control_id): - options = [("en", "en"), - ("de", "de"), - ("fr", "fr")] - self.choose_filter("regionCode", 32151, options) + """Sets Youtube relevanceLanguage filter from ISO 639-1 two-letter language code. + + Args: + control_id (_type_): _description_ + """ + options = [(k, v) for k, v in ISO_639_1_CODES.items()] + self.choose_filter("relevanceLanguage", 32151, options) @ch.click(ID_BUTTON_DIMENSIONFILTER) def set_dimension_filter(self, control_id): + """Sets Youtube videoDimension filter + + Args: + control_id (_type_): _description_ + """ options = [("2d", "2D"), ("3d", "3D"), ("any", addon.LANG(593))] @@ -134,6 +289,13 @@ def set_dimension_filter(self, control_id): @ch.click(ID_BUTTON_DURATIONFILTER) def set_duration_filter(self, control_id): + """Sets Youtube videoDuration filter + long >20 min + medium >4 and <20 min + + Args: + control_id (_type_): _description_ + """ options = [("long", addon.LANG(33013)), ("medium", addon.LANG(601)), ("short", addon.LANG(33012)), @@ -142,6 +304,11 @@ def set_duration_filter(self, control_id): @ch.click(ID_BUTTON_CAPTIONFILTER) def set_caption_filter(self, control_id): + """Sets Youtube videoCaption filter + + Args: + control_id (_type_): _description_ + """ options = [("closedCaption", addon.LANG(107)), ("none", addon.LANG(106)), ("any", addon.LANG(593))] @@ -149,6 +316,12 @@ def set_caption_filter(self, control_id): @ch.click(ID_BUTTON_DEFINITIONFILTER) def set_definition_filter(self, control_id): + """Sets Youtube videoCaption filter + high >= 720 + + Args: + control_id (_type_): _description_ + """ options = [("high", addon.LANG(419)), ("standard", addon.LANG(602)), ("any", addon.LANG(593))] @@ -156,6 +329,11 @@ def set_definition_filter(self, control_id): @ch.click(ID_BUTTON_TYPEFILTER) def set_type_filter(self, control_id): + """Sets Youtube videoType filter + + Args: + control_id (_type_): _description_ + """ options = [("movie", addon.LANG(20338)), ("episode", addon.LANG(20359)), ("any", addon.LANG(593))] @@ -163,6 +341,14 @@ def set_type_filter(self, control_id): @ch.click(ID_BUTTON_SORTTYPE) def get_sort_type(self, control_id): + """Sets the youtube ItemList sort order + + Args: + control_id (_type_): _description_ + + Returns: + _type_: _description_ + """ if not self.choose_sort_method(self.type): return None self.update() @@ -171,8 +357,7 @@ def get_sort_type(self, control_id): def context_menu(self, control_id): listitem = self.FocusedItem(control_id) if self.type == "video": - more_vids = "{} [B]{}[/B]".format(addon.LANG(32081), - listitem.getProperty("channel_title")) + more_vids = f"{addon.LANG(32081)} [B]{listitem.getProperty('channel_title')}[/B]" index = xbmcgui.Dialog().contextmenu( list=[addon.LANG(32069), more_vids]) if index < 0: @@ -202,8 +387,7 @@ def default_sort(self): def add_filter(self, **kwargs): kwargs["typelabel"] = self.FILTERS[kwargs["key"]] - super().add_filter(force_overwrite=True, - **kwargs) + super().add_filter(force_overwrite=True, **kwargs) def fetch_data(self, force=False): self.set_filter_label() @@ -228,7 +412,7 @@ def open(self, search_str="", filters=None, sort="relevance", filter_label="", m open video list, deal with window stack """ YouTube = get_window(windows.DialogXML) - dialog = YouTube('script-%s-YoutubeList.xml' % addon.NAME, addon.PATH, + dialog = YouTube(f'script-{addon.NAME}-YoutubeList.xml', addon.PATH, search_str=search_str, filters=[] if not filters else filters, filter_label=filter_label, diff --git a/script.extendedinfo/resources/lib/process.py b/script.extendedinfo/resources/lib/process.py index bbcdee822..f6a333f9f 100644 --- a/script.extendedinfo/resources/lib/process.py +++ b/script.extendedinfo/resources/lib/process.py @@ -39,7 +39,7 @@ def start_info_actions(info: str, params: dict[str, str]): params (dict[str,str]): Optional parameters for the action Returns: - [ItemList]: a kodi utils ItemList of VideoItems/Music + [ItemList]: a kodi utils ItemList of VideoItems/MusicItems """ if "artistname" in params: params["artistname"] = params.get( @@ -116,7 +116,7 @@ def start_info_actions(info: str, params: dict[str, str]): if tmdb_id: tvshow_id = tmdb_id elif dbid and int(dbid) > 0: - tvdb_id = local_db.get_imdb_id("tvshow", dbid) + tvdb_id = local_db.get_imdb_id("tvshow", dbid)[0] if tvdb_id: tvshow_id = tmdb.get_show_tmdb_id(tvdb_id) elif tvdb_id: @@ -181,7 +181,7 @@ def start_info_actions(info: str, params: dict[str, str]): elif info == 'traktsimilarmovies': if params.get("id") or params.get("dbid"): if params.get("dbid"): - movie_id = local_db.get_imdb_id("movie", params["dbid"]) + movie_id = local_db.get_imdb_id("movie", params["dbid"])[0] else: movie_id = params["id"] return trakt.get_similar("movie", movie_id) @@ -193,7 +193,7 @@ def start_info_actions(info: str, params: dict[str, str]): params["dbid"]) else: tvshow_id = local_db.get_imdb_id(media_type="tvshow", - dbid=params["dbid"]) + dbid=params["dbid"])[0] else: tvshow_id = params["id"] return trakt.get_similar("show", tvshow_id) @@ -285,17 +285,19 @@ def start_info_actions(info: str, params: dict[str, str]): dbid=params.get("dbid"), resume=params.get("resume", "true")) elif info == "openinfodialog": + # ListItem.DBType is used to determine what dialog to open + # For Kodi video library items dbid and title are used if xbmc.getCondVisibility("System.HasActiveModalDialog"): container_id = "" else: container_id = f'Container({utils.get_infolabel("System.CurrentControlId")})' - dbid = utils.get_infolabel(f'{container_id}ListItem.DBID') - db_type = utils.get_infolabel(f'{container_id}ListItem.DBType') + dbid:str = utils.get_infolabel(f'{container_id}ListItem.DBID') + db_type:str = utils.get_infolabel(f'{container_id}ListItem.DBType') if db_type == "movie": params = {"dbid": dbid, "id": utils.get_infolabel(f'{container_id}ListItem.Property(id)'), "name": utils.get_infolabel(f'{container_id}ListItem.Title')} - utils.log(f'process.start_info_actions call exendedinfo with {params}') + #utils.log(f'process.start_info_actions for movie call exendedinfo with {params}') start_info_actions("extendedinfo", params) elif db_type == "tvshow": params = {"dbid": dbid, @@ -305,6 +307,7 @@ def start_info_actions(info: str, params: dict[str, str]): start_info_actions("extendedtvinfo", params) elif db_type == "season": params = {"tvshow": utils.get_infolabel(f'{container_id}ListItem.TVShowTitle'), + "dbid": utils.get_infolabel(f'{container_id}ListItem.DBID'), "season": utils.get_infolabel(f'{container_id}ListItem.Season')} start_info_actions("seasoninfo", params) elif db_type == "episode": @@ -350,7 +353,7 @@ def start_info_actions(info: str, params: dict[str, str]): if not search_str and params.get("search"): result = xbmcgui.Dialog().input(heading=addon.LANG(16017), type=xbmcgui.INPUT_ALPHANUM) - if result and result > -1: + if result: search_str = result else: addon.clear_global('infodialogs.active') @@ -398,7 +401,7 @@ def start_info_actions(info: str, params: dict[str, str]): try: wm.open_season_info(tvshow=params.get("tvshow"), dbid=params.get("dbid"), - season=params.get("season")) + season=int(params.get("season"))) finally: addon.clear_global('infodialogs.active') elif info == 'extendedepisodeinfo': @@ -409,7 +412,7 @@ def start_info_actions(info: str, params: dict[str, str]): wm.open_episode_info(tvshow=params.get("tvshow"), tvshow_id=params.get("tvshow_id"), dbid=params.get("dbid"), - episode=params.get("episode"), + episode=int(params.get("episode")), season=int(params.get("season"))) finally: addon.clear_global('infodialogs.active') @@ -432,8 +435,8 @@ def start_info_actions(info: str, params: dict[str, str]): name=params.get("name")) elif media_type == "tv" and params.get("dbid"): tvdb_id = local_db.get_imdb_id(media_type="tvshow", - dbid=params["dbid"]) - tmdb_id = tmdb.get_show_tmdb_id(tvdb_id=tvdb_id) + dbid=params["dbid"])[0] + tmdb_id = tmdb.get_show_tmdb_id(tvdb_id) else: return False rating = utils.input_userrating() @@ -455,7 +458,7 @@ def start_info_actions(info: str, params: dict[str, str]): movie_id = params["id"] elif int(params.get("dbid", -1)) > 0: movie_id = local_db.get_imdb_id(media_type="movie", - dbid=params["dbid"]) + dbid=params["dbid"])[0] elif params.get("imdb_id"): movie_id = tmdb.get_movie_tmdb_id(params["imdb_id"]) else: diff --git a/script.extendedinfo/resources/lib/theaudiodb.py b/script.extendedinfo/resources/lib/theaudiodb.py index 9f24a4d99..863fc2c90 100644 --- a/script.extendedinfo/resources/lib/theaudiodb.py +++ b/script.extendedinfo/resources/lib/theaudiodb.py @@ -139,7 +139,7 @@ def extended_artist_info(results: dict) -> dict: """ if not results.get('artists'): return {} - local_bio = 'strBiography' + addon.setting("LanguageID").upper() + local_bio = 'strBiography' + addon.setting("LanguageIDv2").upper().split('-', maxsplit=1)[0] artist = results['artists'][0] description = "" if local_bio in artist and artist[local_bio]: @@ -196,7 +196,7 @@ def get_artist_discography(search_str) -> ItemList: return _handle_albums(results) -def get_artist_details(search_str) -> ItemList | dict: +def get_artist_details(search_str:str) -> ItemList | dict: """gets artist details from TADB Args: diff --git a/script.extendedinfo/resources/lib/themoviedb.py b/script.extendedinfo/resources/lib/themoviedb.py index 8cfdcdd70..1786532ea 100644 --- a/script.extendedinfo/resources/lib/themoviedb.py +++ b/script.extendedinfo/resources/lib/themoviedb.py @@ -6,7 +6,7 @@ see: https://developers.themoviedb.org/3/getting-started Public variables: - Login(LoginProvider): a TMDB session instance that logs into TMDB on + tmdb_login(LoginProvider): a TMDB session instance that logs into TMDB on creation Public functions: @@ -158,6 +158,18 @@ class LoginProvider: logs into TMDB for user or guest and gets corresponding session or guest session id """ + LOGIN_VALID = False + + @classmethod + def update_login(cls, status:bool): + """Updates the Login / session id status for tmdb + + Args: + status (bool): the current status of the session id. A valid session + id can be used in queries + """ + cls.LOGIN_VALID = status + def __init__(self, *args, **kwargs) -> LoginProvider: """Creates a new user for accessing tmdb @@ -172,11 +184,12 @@ def __init__(self, *args, **kwargs) -> LoginProvider: 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 + This will require obtaining 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", "") + LoginProvider.update_login(False) def check_login(self) -> bool: @@ -190,6 +203,8 @@ def check_login(self) -> bool: Returns: bool: true if user has an active session id from tmdb """ + if LoginProvider.LOGIN_VALID: + return True if self.username and self.password: return bool(self.get_session_id()) return False @@ -233,12 +248,12 @@ def test_session_id(self, session_id) -> bool: Returns: bool: True if session_id got and account_id """ + if LoginProvider.LOGIN_VALID: + return addon.setting("session_id") response = get_data(url="account", params={"session_id": session_id}, cache_days=999999) - if response and "status_code" in response: - return response.get("status_code") == 1 - return False + return response and response.get("id") def get_session_id(self, cache_days=999) -> str: """gets the tmdb session id from addon settings or creates one if not found @@ -253,8 +268,11 @@ def get_session_id(self, cache_days=999) -> str: if addon.setting("session_id"): self.session_id = addon.setting("session_id") if self.test_session_id(self.session_id): + LoginProvider.update_login(True) return addon.setting("session_id") self.create_session_id() + if self.session_id: + LoginProvider.update_login(True) return self.session_id def create_session_id(self) -> None: @@ -264,12 +282,15 @@ def create_session_id(self) -> None: """ response = get_data(url="authentication/token/new", cache_days=0) - params = {"request_token": response["request_token"], - "username": self.username, - "password": self.password} - response = get_data(url="authentication/token/validate_with_login", - params=params, - cache_days=0) + if response and response.get("request_token"): + params = {"request_token": response["request_token"], + "username": self.username, + "password": self.password} + response = get_data(url="authentication/token/validate_with_login", + params=params, + cache_days=0) + else: + return if response and response.get("success"): request_token = response["request_token"] response = get_data(url="authentication/session/new", @@ -291,10 +312,10 @@ def set_rating(media_type, media_id, rating, dbid=None): if dbid: kodijson.set_userrating(media_type, dbid, rating) params = {} - if Login.check_login(): - params["session_id"] = Login.get_session_id() + if tmdb_login.check_login(): + params["session_id"] = tmdb_login.get_session_id() else: - params["guest_session_id"] = Login.get_guest_session_id() + params["guest_session_id"] = tmdb_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]: @@ -312,17 +333,17 @@ def set_rating(media_type, media_id, rating, dbid=None): return True -def send_request(url, params, values, delete=False): - """formats a tmdb api query url +def send_request(url:str, params:dict, values:dict, delete=False) ->dict: + """formats a tmdb api query url and payload for htttp delete or post Args: - url (_type_): _description_ - params (_type_): _description_ - values (_type_): _description_ - delete (bool, optional): request is a post or delete. Defaults to False. + url (str): tmdb url for account data + params (dict): tmdb query string as dict + values (dict): payload to post to tmdb as json + delete (bool, optional): request is a post or delete. Defaults to False (post). Returns: - _type_: _description_ + dict: the tmdb results {'success : True} if succeeded """ 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} @@ -333,30 +354,33 @@ def send_request(url, params, values, delete=False): return utils.post(url, values=values, headers=HEADERS) -def change_fav_status(media_id=None, media_type="movie", status="true") -> str: - """Updates user favorites list on tmdb +def change_fav_status(media_id:int=None, media_type:str="movie", status:bool=True) -> bool: + """Updates user favorites 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". + media_id (int, optional): tmdb id. Defaults to None. + media_type (str, optional): tmdb media type movie/tv. Defaults to "movie". + status (bool, optional): tmdb favorite new status . Defaults to True. Returns: - str: tmdb result status message + bool: tmdb result status """ - session_id = Login.get_session_id() + session_id = tmdb_login.get_session_id() if not session_id: utils.notify("Could not get session id") return None values = {"media_type": media_type, "media_id": media_id, "favorite": status} - results = send_request(url=f"account/{Login.get_account_id()}/favorite", + results = send_request(url=f"account/{tmdb_login.get_account_id()}/favorite", params={"session_id": session_id}, values=values) - if results: + if results and results.get('success'): utils.notify(addon.NAME, results["status_message"]) - + return True + else: + utils.notify(addon.NAME, results.get("status_message", "No response")) + return False def create_list(list_name): ''' @@ -366,7 +390,7 @@ def create_list(list_name): values = {'name': list_name, 'description': 'List created by ExtendedInfo Script for Kodi.'} results = send_request(url="list", - params={"session_id": Login.get_session_id()}, + params={"session_id": tmdb_login.get_session_id()}, values=values) if results: utils.notify(addon.NAME, results["status_message"]) @@ -400,7 +424,7 @@ def remove_list(list_id): _type_: _description_ """ results = send_request(url=f"list/{list_id}", - params={"session_id": Login.get_session_id()}, + params={"session_id": tmdb_login.get_session_id()}, values={'media_id': list_id}, delete=True) if results: @@ -419,7 +443,7 @@ def change_list_status(list_id, movie_id, status): """ method = "add_item" if status else "remove_item" results = send_request(url=f"list/{list_id}/{method}", - params={"session_id": Login.get_session_id()}, + params={"session_id": tmdb_login.get_session_id()}, values={'media_id': movie_id}) if results: utils.notify(addon.NAME, results["status_message"]) @@ -429,8 +453,8 @@ def get_account_lists(cache_days=0): ''' returns movie lists for TMDB user ''' - session_id = Login.get_session_id() - account_id = Login.get_account_id() + session_id = tmdb_login.get_session_id() + account_id = tmdb_login.get_account_id() if not session_id or not account_id: return [] response = get_data(url=f"account/{account_id}/lists", @@ -527,7 +551,7 @@ def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemL 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")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=30) ids: list[int] = [item["id"] for item in response["genres"]] labels: list[str] = [item["name"] for item in response["genres"]] @@ -570,7 +594,7 @@ def handle_movies(results: list[dict], local_first=True, sortkey="year") ->ItemL def handle_tvshows(results:list[dict], local_first=True, sortkey="year"): tvshows = ItemList(content_type="tvshows") response = get_data(url="genre/tv/list", - params={"language": addon.setting("LanguageID")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=30) ids = [item["id"] for item in response["genres"]] labels = [item["name"] for item in response["genres"]] @@ -628,7 +652,7 @@ def handle_episodes(results:list[dict]) -> ItemList[VideoItem]: for item in results: title = item.get("name") if not title: - title = "%s %s" % (addon.LANG(20359), item.get('episode_number')) + title = f"{addon.LANG(20359)} {item.get('episode_number')}" listitem = {'label': title} listitem = VideoItem(label=title, artwork=get_image_urls(still=item.get("still_path"))) @@ -771,7 +795,7 @@ def handle_seasons(results:list[dict]) -> ItemList[VideoItem]: listitems = ItemList(content_type="seasons") for item in results: season = item.get('season_number') - listitem = VideoItem(label=addon.LANG(20381) if season == 0 else "%s %s" % (addon.LANG(20373), season), + listitem = VideoItem(label=addon.LANG(20381) if season == 0 else f"{addon.LANG(20373)} {season}", properties={'id': item.get('id')}, artwork=get_image_urls(poster=item.get("poster_path"))) listitem.set_infos({'mediatype': "season", @@ -795,7 +819,7 @@ def handle_videos(results:list[dict]) -> ItemList[VideoItem]: for item in results: listitem = VideoItem(label=item.get('name'), size=item.get('size'), - artwork={'thumb': "http://i.ytimg.com/vi/%s/0.jpg" % item.get('key')}) + artwork={'thumb': f"http://i.ytimg.com/vi/{item.get('key')}/0.jpg"}) listitem.set_infos({'mediatype': "video"}) listitem.set_properties({'iso_639_1': item.get('iso_639_1'), 'type': item.get('type'), @@ -875,7 +899,8 @@ def handle_images(results:list[dict]) -> ItemList[VideoItem]: if poster_path: image.update_artwork( {'mediaposter': IMAGE_BASE_URL + POSTER_SIZE + poster_path}) - image.set_info("mediatype", "music") + #image.set_info("mediatype", "music") music stopped working testing song as hack + image.set_info("mediatype", "song") images.append(image) return images @@ -930,9 +955,9 @@ def multi_search(search_str, page=1, cache_days=1): def get_list_movies(list_id, force): - url = "list/%s" % (list_id) + url = f"list/{list_id}" response = get_data(url=url, - params={"language": addon.setting("LanguageID")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=0 if force else 2) if not response: return None @@ -973,7 +998,7 @@ def get_person_info(person_label:str, skip_dialog=False) -> dict: response = get_data(url="search/person", params=params, cache_days=30) - if not response or "results" not in response: + if not response or not response.get("results",[]): return False people: list[dict] = [i for i in response["results"] if i["name"] == person_label] if len(people) > 1 and not skip_dialog: @@ -1000,7 +1025,7 @@ def get_keywords(search_label): def get_set_id(set_name): params = {"query": set_name.replace("[", "").replace("]", "").replace("Kollektion", "Collection"), - "language": addon.setting("LanguageID")} + "language": addon.setting("LanguageIDv2")} response = get_data(url="search/collection", params=params, cache_days=14) @@ -1026,7 +1051,7 @@ def get_data(url:str = "", params:dict = None, cache_days:float = 14) -> dict|No params = params if params else {} 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)}" response = utils.get_JSON_response(url, cache_days, folder='TheMovieDB', headers=HEADERS) if not response: utils.log("tmdb.get_data No response from TMDB") @@ -1034,7 +1059,7 @@ def get_data(url:str = "", params:dict = None, cache_days:float = 14) -> dict|No 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")} - {response.get("status_message")} {traceback.format_stack(limit=-3)}') if response.get('status_code') == 3: - Login.reset_session_id() + tmdb_login.reset_session_id() return None return response @@ -1042,7 +1067,7 @@ def get_data(url:str = "", params:dict = None, cache_days:float = 14) -> dict|No def get_company_data(company_id): if not company_id: return [] - response = get_data(url="company/%s/movies" % (company_id), + response = get_data(url=f"company/{company_id}/movies", cache_days=30) if not response or not response.get("results"): return [] @@ -1053,11 +1078,11 @@ def get_credit_info(credit_id): if not credit_id: return [] return get_data(url=f"credit/{credit_id}", - params={"language": addon.setting("LanguageID")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=30) -def get_account_props(states) -> dict: +def get_account_props(states:dict) -> dict: return {"FavButton_Label": addon.LANG(32155) if states.get("favorite") else addon.LANG(32154), "favorite": "True" if states.get("favorite") else "", "rated": int(states["rated"]["value"]) if states["rated"] else "", @@ -1093,7 +1118,7 @@ def get_image_urls(poster=None, still=None, fanart=None, profile: str =None) -> return images -def get_movie_tmdb_id(imdb_id:str=None, name:str=None, dbid:int=None): +def get_movie_tmdb_id(imdb_id:str=None, name:str=None, dbid:int=None) ->int: """Gets tmdb id for movie @@ -1107,21 +1132,29 @@ def get_movie_tmdb_id(imdb_id:str=None, name:str=None, dbid:int=None): or fall back to title """ if dbid and (int(dbid) > 0): - imdb_id = local_db.get_imdb_id("movie", dbid) + imdb_id, name = local_db.get_imdb_id("movie", dbid) if imdb_id: params = {"external_source": "imdb_id", - "language": addon.setting("LanguageID")} - response = get_data(url="find/tt%s" % (imdb_id.replace("tt", "")), + "language": addon.setting("LanguageIDv2")} + response = get_data(url=f"find/tt{imdb_id.replace('tt', '')}", params=params) if response and response["movie_results"]: return response["movie_results"][0]["id"] return search_media(media_name = name) if name else None +def get_show_tmdb_id(extdb_id=None, source="tvdb_id"): + """gets the tmdb id from source id + + Args: + extdb_id (_type_, optional): external (imdb/tvdb) id to look up tmdb id. Defaults to None. + source (str, optional): the id source. Defaults to "tvdb_id". -def get_show_tmdb_id(tvdb_id=None, source="tvdb_id"): + Returns: + _type_: tmdb id if found or None + """ params = {"external_source": source, - "language": addon.setting("LanguageID")} - response = get_data(url="find/%s" % (tvdb_id), + "language": addon.setting("LanguageIDv2")} + response = get_data(url=f"find/{extdb_id}", params=params) if not response or not response["tv_results"]: utils.notify("TVShow Info not available.") @@ -1131,8 +1164,8 @@ def get_show_tmdb_id(tvdb_id=None, source="tvdb_id"): def get_show_id(tmdb_id=None, return_id="imdb_id"): params = {"append_to_response": "external_ids", - "language": addon.setting("LanguageID")} - response = get_data(url="tv/%s" % (tmdb_id), + "language": addon.setting("LanguageIDv2")} + response = get_data(url=f"tv/{tmdb_id}", params=params) if not response: utils.notify("TVShow Info not available.") @@ -1185,7 +1218,7 @@ def extended_movie_info(movie_id=None, dbid=None, cache_days=14) -> tuple[VideoI mpaa = info['release_dates']['results'][0]['release_dates'][0]['certification'] movie_set:dict = info.get("belongs_to_collection") movie = VideoItem(label=info.get('title'), - path=PLUGIN_BASE + 'youtubevideo&&id=%s' % info.get("id", "")) + path=PLUGIN_BASE + f"youtubevideo&&id={info.get('id', '')}") movie.set_infos({'title': info.get('title'), 'tagline': info.get('tagline'), 'duration': info.get('runtime'), @@ -1246,20 +1279,30 @@ def get_tvshow(tvshow_id, cache_days=30, light=False): if not tvshow_id: return None params = {"append_to_response": None if light else ALL_TV_PROPS, - "language": addon.setting("LanguageID"), - "include_image_language": "en,null,%s" % addon.setting("LanguageID")} - if Login.check_login(): - params["session_id"] = Login.get_session_id() - return get_data(url="tv/%s" % (tvshow_id), + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"} + if tmdb_login.check_login(): + params["session_id"] = tmdb_login.get_session_id() + return get_data(url=f"tv/{tvshow_id}", params=params, cache_days=cache_days) -def extended_tvshow_info(tvshow_id=None, cache_days=7, dbid=None): - ''' - get listitem with extended info for tvshow with *tvshow_id - merge in info from *dbid if available - ''' +def extended_tvshow_info(tvshow_id:int=None, cache_days:int=7, dbid:str=None) -> tuple[VideoItem,dict,dict]|bool: + """creates VideoItem for a tv show, adding any local info if in library + creates associated list of ListItems for the VideoItem and user tmdb account list info + + Args: + tvshow_id (int, optional): the TVDB or TMDB id for the show. Defaults to None. + cache_days (int, optional): cache valid for lookups. Defaults to 7. + dbid (str, optional): the Kodi dbid for the item (if local). Defaults to None. + + Returns: + tuple[VideoItem,dict,dict]|bool: The tv show VideoItem + a dict of ItemLists associated with the show + a dict of user tmdb lists for tv shows + If no tmdb info return False + """ info = get_tvshow(tvshow_id, cache_days) if not info: return False @@ -1268,10 +1311,9 @@ def extended_tvshow_info(tvshow_id=None, cache_days=7, dbid=None): ) if "videos" in info else [] tmdb_id = info.get("id", "") if len(info.get("episode_run_time", -1)) > 1: - duration = "%i - %i" % (min(info["episode_run_time"]), - max(info["episode_run_time"])) + duration = f"{min(info['episode_run_time'])} - {max(info['episode_run_time'])}" elif len(info.get("episode_run_time", -1)) == 1: - duration = "%i" % (info["episode_run_time"][0]) + duration = f"{info['episode_run_time'][0]}" else: duration = "" mpaas = info['content_ratings']['results'] @@ -1283,7 +1325,7 @@ def extended_tvshow_info(tvshow_id=None, cache_days=7, dbid=None): else: mpaa = "" tvshow = VideoItem(label=info.get('name'), - path=PLUGIN_BASE + 'extendedtvinfo&&id=%s' % tmdb_id) + path=PLUGIN_BASE + f'extendedtvinfo&&id={tmdb_id}') tvshow.set_infos({'title': info.get('name'), 'originaltitle': info.get('original_name', ""), 'duration': duration, @@ -1313,7 +1355,7 @@ def extended_tvshow_info(tvshow_id=None, cache_days=7, dbid=None): local_item = local_db.get_tvshow(dbid) tvshow.update_from_listitem(local_item) else: - tvshow = local_db.merge_with_local("tvshow", [tvshow])[0] + tvshow:VideoItem = local_db.merge_with_local("tvshow", [tvshow])[0] # hack to get tmdb rating instead of local one tvshow.set_info("rating", round( info['vote_average'], 1) if info.get('vote_average') else "") @@ -1342,9 +1384,9 @@ def extended_season_info(tvshow_id, season_number): return None tvshow = get_tvshow(tvshow_id) params = {"append_to_response": ALL_SEASON_PROPS, - "language": addon.setting("LanguageID"), - "include_image_language": "en,null,%s" % addon.setting("LanguageID")} - response = get_data(url="tv/%s/season/%s" % (tvshow_id, season_number), + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"} + response = get_data(url=f"tv/{tvshow_id}/season/{season_number}", params=params, cache_days=7) if not response: @@ -1355,7 +1397,7 @@ def extended_season_info(tvshow_id, season_number): elif season_number == "0": title = addon.LANG(20381) else: - title = "%s %s" % (addon.LANG(20373), season_number) + title = f"{addon.LANG(20373)} {season_number}" season = VideoItem(label=title) season.set_infos({'plot': response["overview"], 'tvshowtitle': tvshow.get('name'), @@ -1380,11 +1422,11 @@ def get_episode(tvshow_id, season, episode, cache_days=7): if not season: season = 0 params = {"append_to_response": ALL_EPISODE_PROPS, - "language": addon.setting("LanguageID"), - "include_image_language": "en,null,%s" % addon.setting("LanguageID")} - if Login.check_login(): - params["session_id"] = Login.get_session_id() - return get_data(url="tv/%s/season/%s/episode/%s" % (tvshow_id, season, episode), + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"} + if tmdb_login.check_login(): + params["session_id"] = tmdb_login.get_session_id() + return get_data(url=f"tv/{tvshow_id}/season/{season}/episode/{episode}", params=params, cache_days=cache_days) @@ -1421,8 +1463,10 @@ def extended_actor_info(actor_id: int) -> tuple[VideoItem, dict[str, ItemList]]: """ if not actor_id: return None - data: dict = get_data(url="person/%s" % (actor_id), - params={"append_to_response": ALL_ACTOR_PROPS}, + data: dict = get_data(url=f"person/{actor_id}", + params={"append_to_response": ALL_ACTOR_PROPS, + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"}, cache_days=1) if not data: utils.notify("Could not find actor info") @@ -1470,31 +1514,31 @@ def get_movie_lists(movie_id) -> ItemList: return handle_lists(data["lists"]["results"]) -def get_rated_media_items(media_type, sort_by=None, page=1, cache_days=0): +def get_rated_media_items(media_type:str, sort_by:str='', page:int=1, cache_days:int=0) -> ItemList[VideoItem]: ''' takes "tv/episodes", "tv" or "movies" ''' - if Login.check_login(): - session_id = Login.get_session_id() - account_id = Login.get_account_id() + if tmdb_login.check_login(): + session_id = tmdb_login.get_session_id() + account_id = tmdb_login.get_account_id() if not session_id: utils.notify("Could not get session id") return [] params = {"sort_by": sort_by, "page": page, "session_id": session_id, - "language": addon.setting("LanguageID")} - data = get_data(url="account/%s/rated/%s" % (account_id, media_type), + "language": addon.setting("LanguageIDv2")} + data = get_data(url=f"account/{account_id}/rated/{media_type}", params=params, cache_days=cache_days) else: - session_id = Login.get_guest_session_id() + session_id = tmdb_login.get_guest_session_id() if not session_id: utils.notify("Could not get session id") return [] - params = {"language": addon.setting("LanguageID"), + params = {"language": addon.setting("LanguageIDv2"), "page": page} - data = get_data(url="guest_session/%s/rated/%s" % (session_id, media_type), + data = get_data(url=f"guest_session/{session_id}/rated/{media_type}", params=params, cache_days=0) if media_type == "tv/episodes": @@ -1512,16 +1556,16 @@ def get_fav_items(media_type, sort_by=None, page=1): ''' takes "tv/episodes", "tv" or "movies" ''' - session_id = Login.get_session_id() - account_id = Login.get_account_id() + session_id = tmdb_login.get_session_id() + account_id = tmdb_login.get_account_id() if not session_id: utils.notify("Could not get session id") return [] params = {"sort_by": sort_by, - "language": addon.setting("LanguageID"), + "language": addon.setting("LanguageIDv2"), "page": page, "session_id": session_id} - data = get_data(url="account/%s/favorite/%s" % (account_id, media_type), + data = get_data(url=f"account/{account_id}/favorite/{media_type}", params=params, cache_days=0) if "results" not in data: @@ -1542,7 +1586,7 @@ def get_movies_from_list(list_id:str, cache_days=5): get movie dict list from tmdb list. ''' data = get_data(url=f"list/{list_id}", - params={"language": addon.setting("LanguageID")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=cache_days) return handle_movies(data["items"], False, None) if data else [] @@ -1560,7 +1604,7 @@ def get_actor_credits(actor_id, media_type): ''' media_type: movie or tv ''' - response = get_data(url="person/%s/%s_credits" % (actor_id, media_type), + response = get_data(url=f"person/{actor_id}/{media_type}_credits", cache_days=1) return handle_movies(response["cast"]) @@ -1570,7 +1614,7 @@ def get_movie(movie_id, light=False, cache_days=30) -> dict | None: Args: movie_id (str): tmdb movie id - light (bool, optional): return limited info. Defaults to False. + light (bool, optional): return limited info (for trakt art). Defaults to False. cache_days (int, None):days to use cache vice new query. Defaults to 30. @@ -1578,20 +1622,26 @@ 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": f"en,null,{addon.setting('LanguageID')}", - "language": addon.setting("LanguageID"), + params = {"include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en", + "language": addon.setting("LanguageIDv2"), "append_to_response": None if light else ALL_MOVIE_PROPS } - if Login.check_login(): - params["session_id"] = Login.get_session_id() + if tmdb_login.check_login(): + #params["session_id"] = tmdb_login.get_session_id() + params["session_id"] = addon.setting("session_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 - ''' +def get_similar_movies(movie_id:int) ->ItemList[VideoItem]: + """Queries tmdb for a movie to obtain similar movies + + Args: + movie_id (int): tmdb id + + Returns: + ItemList[VideoItem]: list of kutil131 videoitems of similar movies + """ response = get_movie(movie_id) if not response or not response.get("similar"): return [] @@ -1602,11 +1652,11 @@ def get_similar_tvshows(tvshow_id): return list with similar tvshows for show with *tvshow_id (TMDB ID) ''' params = {"append_to_response": ALL_TV_PROPS, - "language": addon.setting("LanguageID"), - "include_image_language": "en,null,%s" % addon.setting("LanguageID")} - if Login.check_login(): - params["session_id"] = Login.get_session_id() - response = get_data(url="tv/%s" % (tvshow_id), + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"} + if tmdb_login.check_login(): + params["session_id"] = tmdb_login.get_session_id() + response = get_data(url=f"tv/{tvshow_id}", params=params, cache_days=10) if not response.get("similar"): @@ -1619,8 +1669,8 @@ def get_tvshows(tvshow_type): return list with tv shows available types: airing, on_the_air, top_rated, popular ''' - response = get_data(url="tv/%s" % (tvshow_type), - params={"language": addon.setting("LanguageID")}, + response = get_data(url=f"tv/{tvshow_type}", + params={"language": addon.setting("LanguageIDv2")}, cache_days=0.3) if not response.get("results"): return [] @@ -1637,7 +1687,7 @@ def get_movies(movie_type: str) -> list | dict: list: [description] """ response = get_data(url=f'movie/{movie_type}', - params={"language": addon.setting("LanguageID")}, + params={"language": addon.setting("LanguageIDv2")}, cache_days=0.3) if not response.get("results"): return [] @@ -1655,9 +1705,9 @@ def get_set_movies(set_id:str) -> tuple[ItemList,dict]: a dict of set info """ params = {"append_to_response": "images", - "language": addon.setting("LanguageID"), - "include_image_language": "en,null,%s" % addon.setting("LanguageID")} - response = get_data(url="collection/%s" % (set_id), + "language": addon.setting("LanguageIDv2"), + "include_image_language": f"{addon.setting('LanguageIDv2').split('-', maxsplit=1)[0]},null,en"} + response = get_data(url=f"collection/{set_id}", params=params, cache_days=14) if not response: @@ -1675,8 +1725,8 @@ def get_person_movies(person_id): ''' get movies from person with *person_id ''' - response = get_data(url="person/%s/credits" % (person_id), - params={"language": addon.setting("LanguageID")}, + response = get_data(url=f"person/{person_id}/credits", + params={"language": addon.setting("LanguageIDv2")}, cache_days=14) # return handle_movies(response["crew"]) + handle_movies(response["cast"]) if not response or "crew" not in response: @@ -1693,7 +1743,7 @@ def sort_lists(lists: ItemList) -> ItemList: Returns: ItemList: the itemlist ordered by account list first """ - if not Login.check_login(): + if not tmdb_login.check_login(): return lists ids = [i["id"] for i in get_account_lists(10)] own_lists = [i for i in lists if i.get_property("id") in ids] @@ -1709,10 +1759,10 @@ def search_media(media_name=None, year='', media_type="movie", cache_days=1): ''' if not media_name: return None - params = {"query": "{} {}".format(media_name, year) if year else media_name, - "language": addon.setting("language"), + params = {"query": f"{media_name}{' ' + year if year else ''}", + "language": addon.setting("LanguageIDv2"), "include_adult": addon.setting("include_adults").lower()} - response = get_data(url="search/%s" % (media_type), + response = get_data(url=f"search/{media_type}", params=params, cache_days=cache_days) if response == "Empty": @@ -1722,5 +1772,5 @@ def search_media(media_name=None, year='', media_type="movie", cache_days=1): return item['id'] -Login = LoginProvider(username=addon.setting("tmdb_username"), +tmdb_login = LoginProvider(username=addon.setting("tmdb_username"), password=addon.setting("tmdb_password")) diff --git a/script.extendedinfo/resources/lib/trakt.py b/script.extendedinfo/resources/lib/trakt.py index 5148f67f6..308ba5b6d 100644 --- a/script.extendedinfo/resources/lib/trakt.py +++ b/script.extendedinfo/resources/lib/trakt.py @@ -11,7 +11,7 @@ returns a kutils131 ItemList get_shows(show_type) gets tvshows for showtype trending/popular/anticipated returns a kutils131 ItemList - get_shows_from_time(show_type, period) gets tvshos for showtype collected/played/ + get_shows_from_time(show_type, period) gets tvshows for showtype collected/played/ watched for previous month returns a kutils131 ItemList get_movies(movie_type) gets movies for movietype trending/popular/anticipated @@ -22,13 +22,16 @@ an imdb id. """ +from __future__ import annotations + import datetime import urllib.error import urllib.parse import urllib.request -from resources.kutil131 import ItemList, addon +import xbmc +from resources.kutil131 import ItemList, addon from resources.kutil131 import VideoItem, local_db, utils from resources.lib import themoviedb as tmdb @@ -42,99 +45,97 @@ PLUGIN_BASE = "plugin://script.extendedinfo/?info=" -def get_episodes(content): - """gets upcoming/premiering episodes from today +def get_episodes(content:str) -> ItemList[VideoItem]: + """gets upcoming 14 days/premiering episodes from today Args: content (str): enum shows (upcoming) or premieres (new shows) Returns: - ItemList: a kutils131 ItemList instance of VideoItems + ItemList: a kutil131 ItemList instance of VideoItems """ shows = ItemList(content_type="episodes") url = "" if content == "shows": - url = f'calendars/shows/{datetime.date.today()}/14' + url = f'calendars/all/shows/{datetime.date.today()}/14' elif content == "premieres": - url = f'calendars/shows/premieres/{datetime.date.today()}/14' + url = f'calendars/all/shows/premieres/{datetime.date.today()}/14' results = get_data(url=url, params={"extended": "full"}, cache_days=0.3) - count = 1 if not results: return None - #results is a dict. Each key is an ISO date string (day) with value as a - #list of episodes for that date (episode), episode is a dict with keys airs-at, - #episode (ep), and show (tv) Get the first 20 episodes and create an ItemList + #results is a list of dict. Each dict contains "first aired" an ISO date string (day), + #episode is a dict, + #show is a dict. Get the first 20 episodes and create an ItemList #for each episode as VideoItem - for day in results.items(): - for episode in day[1]: #dict of episode - ep = episode["episode"] - tv = episode["show"] - title = ep["title"] if ep["title"] else "" - label = f'{tv["title"]} - {ep["season"]}x{ep["number"]}. {title}' - show = VideoItem(label=label, - path=f'{PLUGIN_BASE}extendedtvinfo&&tvdb_id={tv["ids"]["tvdb"]}') - show.set_infos({'title': title, - 'aired': ep["first_aired"], - 'season': ep["season"], - 'episode': ep["number"], - 'tvshowtitle': tv["title"], - 'mediatype': "episode", - 'year': tv.get("year"), - 'duration': tv["runtime"] * 60 if tv["runtime"] else "", - 'studio': tv["network"], - 'plot': tv["overview"], - 'country': tv["country"], - 'status': tv["status"], - 'trailer': tv["trailer"], - 'imdbnumber': ep["ids"]["imdb"], - 'rating': tv["rating"], - 'genre': " / ".join(tv["genres"]), - 'mpaa': tv["certification"]}) - show.set_properties({'tvdb_id': ep["ids"]["tvdb"], - 'id': ep["ids"]["tvdb"], - 'imdb_id': ep["ids"]["imdb"], - 'homepage': tv["homepage"]}) - if tv["ids"].get("tmdb"): - art_info = tmdb.get_tvshow(tv["ids"]["tmdb"], light=True) - if art_info: - show.set_artwork(tmdb.get_image_urls(poster=art_info.get("poster_path", ""), - fanart=art_info.get("backdrop_path", ""))) - shows.append(show) - count += 1 - if count > 20: - break + count = 1 + for airing_episode in results: + air_date = airing_episode["first_aired"] + ep:dict = airing_episode["episode"] + tv:dict = airing_episode["show"] + title = airing_episode["episode"].get("title", xbmc.getLocalizedString(231)) + label = f'{tv["title"]} - {ep["season"]}x{ep["number"]}. {title}' + show = VideoItem(label=label, + path=f'{PLUGIN_BASE}extendedtvinfo&&tvdb_id={tv["ids"]["tvdb"]}') + show.set_infos({'title': title, + 'aired': air_date, + 'season': ep["season"], + 'episode': ep["number"], + 'tvshowtitle': tv["title"], + 'mediatype': "episode", + 'year': tv.get("year"), + 'duration': tv.get("runtime", 0) * 60, + 'studio': tv["network"], + 'plot': tv["overview"], + 'country': tv["country"], + 'status': tv["status"], + 'trailer': tv["trailer"], + 'imdbnumber': ep["ids"]["imdb"], + 'rating': tv["rating"], + 'genre': " / ".join(tv["genres"]), + 'mpaa': tv.get("certification","")}) + show.set_properties({'tvdb_id': ep["ids"]["tvdb"], + 'id': ep["ids"]["tvdb"], + 'imdb_id': ep["ids"]["imdb"], + 'homepage': tv["homepage"]}) + if tv["ids"].get("tmdb"): + art_info = tmdb.get_tvshow(tv["ids"]["tmdb"], light=True) + if art_info: + show.set_artwork(tmdb.get_image_urls(poster=art_info.get("poster_path", ""), + fanart=art_info.get("backdrop_path", ""))) + shows.append(show) + count += 1 if count > 20: break return shows -def handle_movies(results): - """helper function creates kutils131 VideoItems and adds to an ItemList +def handle_movies(results:list[dict]) -> ItemList[VideoItem]: + """helper function creates kutil131 VideoItems and adds to an ItemList Args: results (list): a list of dicts, each dict is Trakt data for movie Returns: - ItemList: a kutils131 ItemList of VideoItems + ItemList: a kutil131 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 + item:dict = i["movie"] if "movie" in i else i 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"], + movie.set_infos({'title': item.get("title", ""), 'duration': item["runtime"] * 60 if item["runtime"] else "", - 'tagline': item["tagline"], + 'tagline': item.get("tagline", ""), 'mediatype': "movie", 'trailer': trailer, - 'year': item["year"], - 'mpaa': item["certification"], - 'plot': item["overview"], + 'year': item.get("year", ""), + 'mpaa': item.get("certification", ""), + 'plot': item.get("overview", ""), 'imdbnumber': item["ids"]["imdb"], 'premiered': item["released"], 'rating': round(item["rating"], 1), @@ -146,7 +147,9 @@ def handle_movies(results): 'watchers': item.get("watchers"), 'language': item.get("language"), 'homepage': item.get("homepage")}) - art_info = tmdb.get_movie(item["ids"]["tmdb"], light=True) + if item["ids"].get("tmdb"): + utils.log('trakt.handle_movies get art from tmdb') + art_info = tmdb.get_movie(item["ids"]["tmdb"], light=True) if art_info: movie.set_artwork(tmdb.get_image_urls(poster=art_info.get("poster_path"), fanart=art_info.get("backdrop_path"))) @@ -159,13 +162,13 @@ def handle_movies(results): def handle_tvshows(results): - """helper function creates kutils131 VideoItems and adds to an ItemList + """helper function creates kutil131 VideoItems and adds to an ItemList Args: results (list): a list of dicts, each dict is Trakt data for show Returns: - ItemList: a kutils131 ItemList of VideoItems + ItemList: a kutil131 ItemList of VideoItems """ shows = ItemList(content_type="tvshows") for i in results: @@ -174,27 +177,27 @@ def handle_tvshows(results): show = VideoItem(label=item["title"], path=f'{PLUGIN_BASE}extendedtvinfo&&tvdb_id={item["ids"]["tvdb"]}') show.set_infos({'mediatype': "tvshow", - 'title': item["title"], + 'title': item.get("title", ""), 'duration': item["runtime"] * 60 if item["runtime"] else "", 'year': item["year"], 'premiered': item["first_aired"][:10], - 'country': item["country"], + 'country': item.get("country", ""), 'rating': round(item["rating"], 1), 'votes': item["votes"], 'imdbnumber': item['ids']["imdb"], - 'mpaa': item["certification"], - 'trailer': item["trailer"], + 'mpaa': item.get("certification", ""), + 'trailer': item.get("trailer", ""), 'status': item.get("status"), - 'studio': item["network"], + 'studio': item.get("network", ""), 'genre': " / ".join(item["genres"]), - 'plot': item["overview"]}) + 'plot': item.get("overview", "")}) show.set_properties({'id': item['ids']["tmdb"], 'tvdb_id': item['ids']["tvdb"], 'imdb_id': item['ids']["imdb"], 'trakt_id': item['ids']["trakt"], - 'language': item["language"], + 'language': item.get("language", ""), 'aired_episodes': item["aired_episodes"], - 'homepage': item["homepage"], + 'homepage': item.get("homepage", ""), 'airday': airs.get("day"), 'airshorttime': airs.get("time"), 'watchers': item.get("watchers")}) @@ -217,7 +220,7 @@ def get_shows(show_type): show_type (str): enum trending/popular/anticipated Returns: - ItemList: a kutils131 ItemList of VideoItems + ItemList: a kutil131 ItemList of VideoItems """ results = get_data(url=f'shows/{show_type}', params={"extended": "full"}) @@ -239,21 +242,21 @@ def get_shows_from_time(show_type, period="monthly"): return handle_tvshows(results) if results else [] -def get_movies(movie_type): +def get_movies(movie_type:str) -> ItemList[VideoItem]: """gets Trakt full data for movies of enumerated type Args: movie_type (str): enum trending/popular/anticipated Returns: - ItemList: a kutils131 ItemList of VideoItems + ItemList: a kutil131 ItemList of VideoItems """ results = get_data(url=f'movies/{movie_type}', params={"extended": "full"}) return handle_movies(results) if results else [] -def get_movies_from_time(movie_type, period="monthly"): +def get_movies_from_time(movie_type:str, period="monthly") -> ItemList[VideoItem]: """gets Trakt full data for movies of enumerated type for enumerated period Args: @@ -290,7 +293,7 @@ def get_similar(media_type, imdb_id): return handle_movies(results) -def get_data(url, params=None, cache_days=10): +def get_data(url:str, params:dict=None, cache_days:int=10) -> list[dict]: """helper function builds query and formats result. First attempts to retrieve data from local cache and then issues a ResT GET to the api if cache data not available @@ -303,7 +306,7 @@ def get_data(url, params=None, cache_days=10): Returns: dict: a dict from the deserialized JSON response from api or None - Note: kutils131 does not return the GET failure code (ie if not 200) + Note: kutil131 does not return the GET failure code (ie if not 200) """ params = params if params else {} params["limit"] = 10 diff --git a/script.extendedinfo/resources/lib/windowmanager.py b/script.extendedinfo/resources/lib/windowmanager.py index 23eb9b135..9b6258874 100644 --- a/script.extendedinfo/resources/lib/windowmanager.py +++ b/script.extendedinfo/resources/lib/windowmanager.py @@ -87,13 +87,14 @@ def open_tvshow_info(self, tmdb_id=None, dbid=None, tvdb_id=None, imdb_id=None, elif tvdb_id: tmdb_id = tmdb.get_show_tmdb_id(tvdb_id) elif imdb_id: - tmdb_id = tmdb.get_show_tmdb_id(tvdb_id=imdb_id, + tmdb_id = tmdb.get_show_tmdb_id(imdb_id, source="imdb_id") elif dbid: - tvdb_id = local_db.get_imdb_id(media_type="tvshow", - dbid=dbid) - if tvdb_id: - tmdb_id = tmdb.get_show_tmdb_id(tvdb_id) + imdb_id = local_db.get_imdb_id(media_type="tvshow", + dbid=dbid)[0] + if imdb_id: + tmdb_id = tmdb.get_show_tmdb_id(imdb_id, + source="imdb_id") elif name: tmdb_id = tmdb.search_media(media_name=name, year="", @@ -105,7 +106,7 @@ def open_tvshow_info(self, tmdb_id=None, dbid=None, tvdb_id=None, imdb_id=None, busy.hide_busy() self.open_infodialog(dialog) - def open_season_info(self, tvshow_id=None, season: int = None, tvshow=None, dbid=None): + def open_season_info(self, tvshow_id=None, season:int=None, tvshow:str=None, dbid:str=None): """ open season info, deal with window stack needs *season AND (*tvshow_id OR *tvshow) @@ -114,7 +115,7 @@ def open_season_info(self, tvshow_id=None, season: int = None, tvshow=None, dbid from .dialogs.dialogseasoninfo import DialogSeasonInfo if not tvshow_id: params = {"query": tvshow, - "language": addon.setting("language")} + "language": addon.setting("LanguageIDv2")} response = tmdb.get_data(url="search/tv", params=params, cache_days=30) @@ -122,22 +123,26 @@ def open_season_info(self, tvshow_id=None, season: int = None, tvshow=None, dbid tvshow_id = str(response['results'][0]['id']) else: params = {"query": re.sub(r'\(.*?\)', '', tvshow), - "language": addon.setting("language")} + "language": addon.setting("LanguageIDv2")} response = tmdb.get_data(url="search/tv", params=params, cache_days=30) if response["results"]: tvshow_id = str(response['results'][0]['id']) - dialog = DialogSeasonInfo(INFO_XML, - addon.PATH, - id=tvshow_id, - season=max(0, season), - dbid=int(dbid) if dbid and int(dbid) > 0 else None) - busy.hide_busy() - self.open_infodialog(dialog) + if tvshow_id: + dialog = DialogSeasonInfo(INFO_XML, + addon.PATH, + id=tvshow_id, + season=max(0, int(season)), + dbid=int(dbid) if dbid and int(dbid) > 0 else None) + busy.hide_busy() + self.open_infodialog(dialog) + else: + busy.hide_busy() + utils.notify(addon.NAME, f"TMDB - {xbmc.getLocalizedString(195)}") - def open_episode_info(self, tvshow_id=None, season=None, episode=None, tvshow=None, dbid=None): + def open_episode_info(self, tvshow_id=None, season:int=None, episode=None, tvshow=None, dbid=None): """ open season info, deal with window stack needs (*tvshow_id OR *tvshow) AND *season AND *episode @@ -297,7 +302,7 @@ def play_youtube_video(self, youtube_id="", listitem=None): if self.active_dialog and self.active_dialog.window_type == "dialog": self.active_dialog.close() xbmc.executebuiltin("Dialog.Close(movieinformation)") - xbmc.executebuiltin("RunPlugin(plugin://plugin.video.youtube/play/?video_id=" + + xbmc.executebuiltin("PlayMedia(plugin://plugin.video.youtube/play/?video_id=" + youtube_id + "&screensaver=true&incognito=true)") if self.active_dialog and self.active_dialog.window_type == "dialog": player.wait_for_video_start() #30 sec timeout diff --git a/script.extendedinfo/resources/settings.xml b/script.extendedinfo/resources/settings.xml index b44adf6f0..ff8e0f70f 100644 --- a/script.extendedinfo/resources/settings.xml +++ b/script.extendedinfo/resources/settings.xml @@ -102,7 +102,7 @@ - 0 + 4 en @@ -140,6 +140,64 @@ 32159 + + 4 + false + + + + 0 + en-GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 32159 + + 0 diff --git a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogInfo.xml b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogInfo.xml index 99f9b2cfa..4891d1c15 100644 --- a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogInfo.xml +++ b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-DialogInfo.xml @@ -277,9 +277,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -355,9 +365,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -460,9 +480,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] + scale + 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] scale 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -538,9 +568,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -643,9 +683,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -721,9 +771,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -826,9 +886,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -904,9 +974,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] + scale + 0 + String.IsEmpty(ListItem.DBID) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] scale 0 + !String.IsEmpty(ListItem.DBID) 55 @@ -1125,7 +1205,7 @@ 28 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -1156,7 +1236,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -1222,7 +1302,7 @@ 28 505 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale @@ -1261,7 +1341,7 @@ 28 505 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 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 e7d75b482..9a2468fcf 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 @@ -736,7 +736,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -788,7 +788,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -877,7 +877,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale @@ -937,7 +937,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale @@ -1037,9 +1037,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.dbid) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.dbid) 310 @@ -1116,9 +1126,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 + String.IsEmpty(ListItem.dbid) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + 0 + !String.IsEmpty(ListItem.dbid) 55 @@ -1275,8 +1295,17 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale + String.IsEmpty(ListItem.dbid) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] + scale + !String.IsEmpty(ListItem.dbid) 55 @@ -1352,9 +1381,19 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] + scale + 0 + String.IsEmpty(ListItem.dbid) + + + 10 + 224 + 325 + $INFO[ListItem.Art(poster)] scale 0 + !String.IsEmpty(ListItem.dbid) 55 @@ -1715,7 +1754,7 @@ 50 273 100 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] keep @@ -1752,7 +1791,7 @@ 50 273 100 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] keep @@ -1829,7 +1868,7 @@ 30 313 100 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] keep @@ -1866,7 +1905,7 @@ 30 313 100 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] keep @@ -2306,7 +2345,7 @@ 28 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -2345,7 +2384,7 @@ 28 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -2413,7 +2452,7 @@ 28 505 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 200 @@ -2452,7 +2491,7 @@ 28 505 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 200 @@ -2519,7 +2558,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -2588,7 +2627,7 @@ 10 224 325 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -2665,7 +2704,7 @@ 10 320 200 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 @@ -2714,7 +2753,7 @@ 10 320 200 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] scale 0 diff --git a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-VideoList.xml b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-VideoList.xml index ff88ba62a..dcdd15d34 100644 --- a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-VideoList.xml +++ b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-VideoList.xml @@ -134,7 +134,7 @@ 200 common/black.png 3 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] Control.IsVisible(50) @@ -283,7 +283,19 @@ overlays/shadow.png 20 keep - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] + String.IsEmpty(ListItem.DBID) + + + -15 + 0 + 250 + 350 + overlays/shadow.png + 20 + keep + $INFO[ListItem.Art(poster)] + !String.IsEmpty(ListItem.DBID) 55 @@ -340,7 +352,19 @@ overlays/shadow.png 20 keep - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] + String.IsEmpty(ListItem.DBID) + + + -15 + 0 + 250 + 350 + overlays/shadow.png + 20 + keep + $INFO[ListItem.Art(poster)] + !String.IsEmpty(ListItem.DBID) 55 diff --git a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-YoutubeList.xml b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-YoutubeList.xml index 3f8b9351d..16bfbb7e2 100644 --- a/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-YoutubeList.xml +++ b/script.extendedinfo/resources/skins/Default/1080i/script-script.extendedinfo-YoutubeList.xml @@ -6,7 +6,7 @@ 500 - $INFO[ListItem.Thumb] + $INFO[ListItem.Art(thumb)] 1920 1080 400