From 5df3077a74c8b8b61b83b98714557ff76b1349c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 8 Jul 2019 00:39:19 +0200 Subject: [PATCH 1/6] Added .editorconfig and tox.ini (flake8) --- .editorconfig | 25 +++++++++++++++++++++++++ tox.ini | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 .editorconfig create mode 100644 tox.ini diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0e9268c0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# With more recent updates Visual Studio 2017 supports EditorConfig files out of the box +# Visual Studio Code needs an extension: https://github.com/editorconfig/editorconfig-vscode +# For emacs, vim, np++ and other editors, see here: https://github.com/editorconfig +############################### +# Core EditorConfig Options # +############################### +root = true + +# All files +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +max_line_length = null + +# YAML indentation +[*.{yml,yaml}] +indent_size = 2 + +# XML indentation +[*.xml] +indent_size = 2 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..423d8859 --- /dev/null +++ b/tox.ini @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 9999 +import-order-style = pep8 From 0549a8b0ea8102210e9172190d77126c5615c177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 8 Jul 2019 01:24:05 +0200 Subject: [PATCH 2/6] Add file and line number to log output. --- resources/lib/helper/loghandler.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/resources/lib/helper/loghandler.py b/resources/lib/helper/loghandler.py index 04fa723c..0ac9132e 100644 --- a/resources/lib/helper/loghandler.py +++ b/resources/lib/helper/loghandler.py @@ -1,16 +1,26 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + ################################################################################################## +import os import logging import xbmc - +import xbmcaddon import database + from . import window, settings ################################################################################################## +__addon__ = xbmcaddon.Addon(id='plugin.video.jellyfin') +__pluginpath__ = xbmc.translatePath(__addon__.getAddonInfo('path').decode('utf-8')) + +################################################################################################## + def config(): @@ -18,6 +28,7 @@ def config(): logger.addHandler(LogHandler()) logger.setLevel(logging.DEBUG) + def reset(): for handler in logging.getLogger('JELLYFIN').handlers: @@ -59,7 +70,7 @@ class LogHandler(logging.StreamHandler): string = string.replace(server.encode('utf-8') or "{server}", "{jellyfin-server}") for token in self.sensitive['Token']: - string = string.replace(token.encode('utf-8') or "{token}", "{jellyfin-token}") + string = string.replace(token.encode('utf-8') or "{token}", "{jellyfin-token}") try: xbmc.log(string, level=xbmc.LOGNOTICE) @@ -95,9 +106,11 @@ class MyFormatter(logging.Formatter): # when the logger formatter was instantiated format_orig = self._fmt + self._gen_rel_path(record) + # Replace the original format with one customized by logging level - if record.levelno in (logging.DEBUG, logging.ERROR): - self._fmt = '%(name)s -> %(levelname)s:: %(message)s' + #if record.levelno not in [logging.INFO]: + self._fmt = '%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s' # Call the original formatter class to do the grunt work result = logging.Formatter.format(self, record) @@ -106,3 +119,7 @@ class MyFormatter(logging.Formatter): self._fmt = format_orig return result + + def _gen_rel_path(self, record): + if record.pathname: + record.relpath = os.path.relpath(record.pathname, __pluginpath__) From c321b266f042080f37c42f37d45732c8802fb2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 8 Jul 2019 02:26:54 +0200 Subject: [PATCH 3/6] Add more logging to views.py --- resources/lib/entrypoint/service.py | 18 +++++++++--------- resources/lib/helper/loghandler.py | 1 - resources/lib/views.py | 13 ++++++++++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/resources/lib/entrypoint/service.py b/resources/lib/entrypoint/service.py index f3fb953d..7735a424 100644 --- a/resources/lib/entrypoint/service.py +++ b/resources/lib/entrypoint/service.py @@ -76,7 +76,7 @@ class Service(xbmc.Monitor): try: Views().get_nodes() except Exception as error: - LOG.error(error) + LOG.exception(error) window('jellyfin.connected.bool', True) settings('groupedSets.bool', objects.utils.get_grouped_set()) @@ -87,7 +87,7 @@ class Service(xbmc.Monitor): ''' Keeps the service monitor going. Exit on Kodi shutdown or profile switch. - if profile switch happens more than once, + if profile switch happens more than once, Threads depending on abortRequest will not trigger. ''' self.monitor = monitor.Monitor() @@ -230,7 +230,7 @@ class Service(xbmc.Monitor): if self.waitForAbort(120): return - + self.start_default() elif method == 'Unauthorized': @@ -243,13 +243,13 @@ class Service(xbmc.Monitor): if self.waitForAbort(5): return - + self.start_default() elif method == 'ServerRestarting': if data.get('ServerId'): return - + if settings('restartMsg.bool'): dialog("notification", heading="{jellyfin}", message=_(33006), icon="{jellyfin}") @@ -257,7 +257,7 @@ class Service(xbmc.Monitor): if self.waitForAbort(15): return - + self.start_default() elif method == 'ServerConnect': @@ -318,7 +318,7 @@ class Service(xbmc.Monitor): if not self.library_thread.remove_library(lib): return - + self.library_thread.add_library(data['Id']) xbmc.executebuiltin("Container.Refresh") @@ -326,14 +326,14 @@ class Service(xbmc.Monitor): libraries = data['Id'].split(',') for lib in libraries: - + if not self.library_thread.remove_library(lib): return xbmc.executebuiltin("Container.Refresh") elif method == 'System.OnSleep': - + LOG.info("-->[ sleep ]") window('jellyfin_should_stop.bool', True) diff --git a/resources/lib/helper/loghandler.py b/resources/lib/helper/loghandler.py index 0ac9132e..49b71940 100644 --- a/resources/lib/helper/loghandler.py +++ b/resources/lib/helper/loghandler.py @@ -109,7 +109,6 @@ class MyFormatter(logging.Formatter): self._gen_rel_path(record) # Replace the original format with one customized by logging level - #if record.levelno not in [logging.INFO]: self._fmt = '%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s' # Call the original formatter class to do the grunt work diff --git a/resources/lib/views.py b/resources/lib/views.py index 1e6b8901..9c2fc457 100644 --- a/resources/lib/views.py +++ b/resources/lib/views.py @@ -118,6 +118,7 @@ def verify_kodi_defaults(): src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'), dst=xbmc.translatePath("special://profile/library/video").decode('utf-8')) except Exception as error: + LOG.warning(error) xbmcvfs.mkdir(node_path) for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): @@ -169,6 +170,7 @@ class Views(object): libraries = self.server['api'].get_media_folders()['Items'] views = self.server['api'].get_views()['Items'] except Exception as error: + LOG.exception(error) raise IndexError("Unable to retrieve libraries: %s" % error) libraries.extend([x for x in views if x['Id'] not in [y['Id'] for y in libraries]]) @@ -188,7 +190,7 @@ class Views(object): try: libraries = self.get_libraries() except IndexError as error: - LOG.error(error) + LOG.exception(error) return @@ -273,6 +275,7 @@ class Views(object): try: xml = etree.parse(file).getroot() except Exception: + LOG.warning("Unable to parse file '%s'", file) xml = etree.Element('smartplaylist', {'type': view['Media']}) etree.SubElement(xml, 'name') etree.SubElement(xml, 'match') @@ -316,6 +319,7 @@ class Views(object): try: xml = etree.parse(file).getroot() except Exception: + LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') @@ -371,7 +375,8 @@ class Views(object): try: xml = etree.parse(file).getroot() xml.set('order', str(index)) - except Exception: + except Exception as error: + LOG.exception(error) xml = self.node_root('main', index) etree.SubElement(xml, 'label') @@ -410,6 +415,7 @@ class Views(object): try: xml = etree.parse(file).getroot() except Exception: + LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') @@ -441,6 +447,7 @@ class Views(object): try: xml = etree.parse(file).getroot() except Exception: + LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('folder', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'content') @@ -692,7 +699,7 @@ class Views(object): try: self.media_folders = self.get_libraries() except IndexError as error: - LOG.error(error) + LOG.exception(error) for library in (libraries or []): view = {'Id': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2]} From 9ae99de8dd502645be0a5cd2592ab34ec1141f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Tue, 9 Jul 2019 22:05:28 +0200 Subject: [PATCH 4/6] Increase amount of logging --- resources/lib/database/__init__.py | 21 +- resources/lib/database/jellyfin_db.py | 10 +- resources/lib/downloader.py | 21 +- resources/lib/entrypoint/service.py | 4 +- resources/lib/full_sync.py | 13 +- resources/lib/helper/playutils.py | 5 +- resources/lib/helper/utils.py | 19 +- resources/lib/helper/wrapper.py | 6 +- resources/lib/jellyfin/__init__.py | 4 +- resources/lib/jellyfin/client.py | 3 +- resources/lib/jellyfin/core/api.py | 3 - resources/lib/jellyfin/core/configuration.py | 6 +- .../lib/jellyfin/core/connection_manager.py | 44 +- resources/lib/jellyfin/core/credentials.py | 6 +- resources/lib/jellyfin/core/http.py | 12 +- resources/lib/jellyfin/core/ws_client.py | 3 +- resources/lib/jellyfin/resources/websocket.py | 31 +- resources/lib/library.py | 28 +- resources/lib/monitor.py | 7 +- resources/lib/objects/actions.py | 2 +- resources/lib/objects/kodi/artwork.py | 10 +- resources/lib/objects/movies.py | 11 +- resources/lib/objects/music.py | 35 +- resources/lib/objects/musicvideos.py | 11 +- resources/lib/objects/tvshows.py | 23 +- resources/lib/objects/utils.py | 9 +- resources/lib/player.py | 894 +++++++++--------- resources/lib/webservice.py | 8 +- service.py | 11 +- tox.ini | 1 + 30 files changed, 641 insertions(+), 620 deletions(-) diff --git a/resources/lib/database/__init__.py b/resources/lib/database/__init__.py index a2431e35..372318fd 100644 --- a/resources/lib/database/__init__.py +++ b/resources/lib/database/__init__.py @@ -84,7 +84,7 @@ class Database(object): return path def _discover_database(self, database): - + ''' Use UpdateLibrary(video) to update the date modified on the database file used by Kodi. ''' @@ -138,6 +138,7 @@ class Database(object): try: loaded = self._get_database(databases[file]) if file in databases else file except Exception as error: + LOG.exception(error) for i in range(1, 10): alt_file = "%s-%s" % (file, i) @@ -150,8 +151,8 @@ class Database(object): loaded = None break - except Exception: - pass + except Exception as error: + LOG.exception(error) if discovered and discovered != loaded: @@ -200,7 +201,7 @@ def jellyfin_tables(cursor): columns = cursor.execute("SELECT * FROM jellyfin") if 'jellyfin_parent_id' not in [description[0] for description in columns.description]: - + LOG.info("Add missing column jellyfin_parent_id") cursor.execute("ALTER TABLE jellyfin ADD COLUMN jellyfin_parent_id 'TEXT'") @@ -281,7 +282,7 @@ def reset_kodi(): LOG.warn("[ reset kodi ]") def reset_jellyfin(): - + with Database('jellyfin') as jellyfindb: jellyfindb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'") @@ -327,7 +328,7 @@ def reset_artwork(): def get_sync(): path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/").decode('utf-8') - + if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) @@ -347,7 +348,7 @@ def get_sync(): def save_sync(sync): path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/").decode('utf-8') - + if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) @@ -359,7 +360,7 @@ def save_sync(sync): def get_credentials(): path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/").decode('utf-8') - + if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) @@ -372,7 +373,7 @@ def get_credentials(): with open(os.path.join(path, 'data.txt')) as infile: credentials = json.load(infile) save_credentials(credentials) - + xbmcvfs.delete(os.path.join(path, 'data.txt')) except Exception: credentials = {} @@ -384,7 +385,7 @@ def get_credentials(): def save_credentials(credentials): credentials = credentials or {} path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/").decode('utf-8') - + if not xbmcvfs.exists(path): xbmcvfs.mkdirs(path) diff --git a/resources/lib/database/jellyfin_db.py b/resources/lib/database/jellyfin_db.py index 5365b358..195f5237 100644 --- a/resources/lib/database/jellyfin_db.py +++ b/resources/lib/database/jellyfin_db.py @@ -15,7 +15,6 @@ LOG = logging.getLogger("JELLYFIN."+__name__) class JellyfinDatabase(): - def __init__(self, cursor): self.cursor = cursor @@ -31,7 +30,7 @@ class JellyfinDatabase(): self.cursor.execute(QU.update_reference, args) def update_parent_id(self, *args): - + ''' Parent_id is the parent Kodi id. ''' self.cursor.execute(QU.update_parent, args) @@ -62,7 +61,7 @@ class JellyfinDatabase(): return self.cursor.fetchall() def get_item_by_kodi_id(self, *args): - + try: self.cursor.execute(QU.get_item_by_kodi, args) @@ -105,7 +104,6 @@ class JellyfinDatabase(): def remove_wild_item(self, item_id): self.cursor.execute(QU.delete_item_by_wild, (item_id + "%",)) - def get_view_name(self, item_id): try: @@ -113,6 +111,7 @@ class JellyfinDatabase(): return self.cursor.fetchone()[0] except Exception as error: + LOG.exception(error) return def get_view(self, *args): @@ -159,7 +158,6 @@ class JellyfinDatabase(): self.cursor.execute(QU.get_version) version = self.cursor.fetchone()[0] except Exception as error: - pass + LOG.exception(error) return version - \ No newline at end of file diff --git a/resources/lib/downloader.py b/resources/lib/downloader.py index b65e4868..587cc57b 100644 --- a/resources/lib/downloader.py +++ b/resources/lib/downloader.py @@ -46,7 +46,7 @@ def browse_info(): def _http(action, url, request={}, server_id=None): request.update({'url': url, 'type': action}) - + return Jellyfin(server_id)['http/request'](request) @@ -73,7 +73,8 @@ def validate_view(library_id, item_id): 'Recursive': True, 'Ids': item_id }) - except Exception: + except Exception as error: + LOG.exception(error) return False return True if len(result['Items']) else False @@ -135,8 +136,8 @@ def get_episode_by_show(show_id): query = { 'url': "Shows/%s/Episodes" % show_id, 'params': { - 'EnableUserData': True, - 'EnableImages': True, + 'EnableUserData': True, + 'EnableImages': True, 'UserId': "{UserId}", 'Fields': api.info() } @@ -151,8 +152,8 @@ def get_episode_by_season(show_id, season_id): 'url': "Shows/%s/Episodes" % show_id, 'params': { 'SeasonId': season_id, - 'EnableUserData': True, - 'EnableImages': True, + 'EnableUserData': True, + 'EnableImages': True, 'UserId': "{UserId}", 'Fields': api.info() } @@ -257,7 +258,7 @@ def _get_items(query, server_id=None): items['TotalRecordCount'] = _get(url, test_params, server_id=server_id)['TotalRecordCount'] except Exception as error: - LOG.error("Failed to retrieve the server response %s: %s params:%s", url, error, params) + LOG.exception("Failed to retrieve the server response %s: %s params:%s", url, error, params) else: index = params.get('StartIndex', 0) @@ -268,7 +269,7 @@ def _get_items(query, server_id=None): params['StartIndex'] = index params['Limit'] = LIMIT result = _get(url, params, server_id=server_id) or {'Items': []} - + items['Items'].extend(result['Items']) items['RestorePoint'] = query yield items @@ -366,7 +367,7 @@ class TheVoid(object): if window('jellyfin_should_stop.bool'): LOG.info("Abandon mission! A black hole just swallowed [ %s/%s ]", self.method, self.data['VoidName']) - + return xbmc.sleep(100) @@ -397,8 +398,6 @@ def get_objects(src, filename): LOG.error(error) response = requests.get(src, stream=True, verify=False) - except Exception: - raise dl = xbmcvfs.File(path, 'w') dl.write(response.content) diff --git a/resources/lib/entrypoint/service.py b/resources/lib/entrypoint/service.py index 7735a424..e393a59d 100644 --- a/resources/lib/entrypoint/service.py +++ b/resources/lib/entrypoint/service.py @@ -137,7 +137,7 @@ class Service(xbmc.Monitor): self.connect.register() setup.Setup() except Exception as error: - LOG.error(error) + LOG.exception(error) def stop_default(self): @@ -361,7 +361,7 @@ class Service(xbmc.Monitor): try: self.connect.register() except Exception as error: - LOG.error(error) + LOG.exception(error) elif method == 'GUI.OnScreensaverDeactivated': diff --git a/resources/lib/full_sync.py b/resources/lib/full_sync.py index aa5b3b1b..50560cea 100644 --- a/resources/lib/full_sync.py +++ b/resources/lib/full_sync.py @@ -39,7 +39,7 @@ class FullSync(object): def __init__(self, library, server): - ''' You can call all big syncing methods here. + ''' You can call all big syncing methods here. Initial, update, repair, remove. ''' self.__dict__ = self._shared_state @@ -181,7 +181,7 @@ class FullSync(object): def start(self): - + ''' Main sync process. ''' LOG.info("starting sync with %s", self.sync['Libraries']) @@ -248,8 +248,9 @@ class FullSync(object): raise except Exception as error: + LOG.exception(error) - if not 'Failed to validate path' in error: + if 'Failed to validate path' not in error: dialog("ok", heading="{jellyfin}", line1=_(33119)) LOG.error("full sync exited unexpectedly") @@ -271,7 +272,7 @@ class FullSync(object): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): - + self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] @@ -413,7 +414,7 @@ class FullSync(object): obj.artist(artist, library=library) for albums in server.get_albums_by_artist(artist['Id']): - + for album in albums['Items']: obj.album(album) @@ -546,7 +547,7 @@ class FullSync(object): if library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove(library_id) - + elif 'Mixed:%s' % library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove('Mixed:%s' % library_id) diff --git a/resources/lib/helper/playutils.py b/resources/lib/helper/playutils.py index a7423d5b..68899ddf 100644 --- a/resources/lib/helper/playutils.py +++ b/resources/lib/helper/playutils.py @@ -484,7 +484,7 @@ class PlayUtils(object): try: subs.append(self.download_external_subs(url, filename)) except Exception as error: - LOG.error(error) + LOG.exception(error) subs.append(url) else: subs.append(url) @@ -512,7 +512,8 @@ class PlayUtils(object): try: response = requests.get(src, stream=True, verify=False) response.raise_for_status() - except Exception as e: + except Exception as error: + LOG.exception(error) raise else: response.encoding = 'utf-8' diff --git a/resources/lib/helper/utils.py b/resources/lib/helper/utils.py index 97906727..546bdf01 100644 --- a/resources/lib/helper/utils.py +++ b/resources/lib/helper/utils.py @@ -172,7 +172,7 @@ def should_stop(): return False def get_screensaver(): - + ''' Get the current screensaver value. ''' result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"}) @@ -182,7 +182,7 @@ def get_screensaver(): return "" def set_screensaver(value): - + ''' Toggle the screensaver ''' params = { @@ -198,7 +198,7 @@ class JSONRPC(object): jsonrpc = "2.0" def __init__(self, method, **kwargs): - + self.method = method for arg in kwargs: @@ -249,7 +249,7 @@ def values(item, keys): return (item[key.replace('{', "").replace('}', "")] if type(key) == str and key.startswith('{') else key for key in keys) def indent(elem, level=0): - + ''' Prettify xml docs. ''' try: @@ -266,7 +266,8 @@ def indent(elem, level=0): else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i - except Exception: + except Exception as error: + LOG.exception(error) return def write_xml(content, file): @@ -292,7 +293,7 @@ def delete_folder(path=None): if delete_path: xbmcvfs.delete(path) - + LOG.info("DELETE %s", path) def delete_recursive(path, dirs): @@ -314,7 +315,7 @@ def unzip(path, dest, folder=None): ''' path = urllib.quote_plus(path) root = "zip://" + path + '/' - + if folder: xbmcvfs.mkdir(os.path.join(dest, folder)) @@ -431,7 +432,7 @@ def normalize_string(text): return text def split_list(itemlist, size): - + ''' Split up list in pieces of size. Will generate a list of lists ''' return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] @@ -447,6 +448,6 @@ def convert_to_local(date): return date.strftime('%Y-%m-%dT%H:%M:%S') except Exception as error: - LOG.error(error) + LOG.exception(error) return str(date) diff --git a/resources/lib/helper/wrapper.py b/resources/lib/helper/wrapper.py index c9649c01..71c07280 100644 --- a/resources/lib/helper/wrapper.py +++ b/resources/lib/helper/wrapper.py @@ -84,10 +84,11 @@ def stop(default=None): def wrapper(*args, **kwargs): try: - if should_stop(): + if should_stop(): # ??? TODO: Fixme raise Exception except Exception as error: + LOG.exception(error) if default is not None: return default @@ -142,7 +143,8 @@ def library_check(): try: views = self.jellyfin_db.get_views_by_media('music')[0] - except Exception: + except Exception as error: + LOG.exception(error) return view = {'Id': views[0], 'Name': views[1]} diff --git a/resources/lib/jellyfin/__init__.py b/resources/lib/jellyfin/__init__.py index 06046328..1ee4399e 100644 --- a/resources/lib/jellyfin/__init__.py +++ b/resources/lib/jellyfin/__init__.py @@ -112,6 +112,7 @@ class Jellyfin(object): @ensure_client() def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) return self.client[self.server_id][key] def construct(self): @@ -123,4 +124,5 @@ class Jellyfin(object): else: LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id) -config() \ No newline at end of file + +config() diff --git a/resources/lib/jellyfin/client.py b/resources/lib/jellyfin/client.py index c57bf729..e58a7bea 100644 --- a/resources/lib/jellyfin/client.py +++ b/resources/lib/jellyfin/client.py @@ -18,7 +18,7 @@ LOG = logging.getLogger('JELLYFIN.'+__name__) def callback(message, data): - ''' Callback function should received message, data + ''' Callback function should received message, data message: string data: json dictionary ''' @@ -83,6 +83,7 @@ class JellyfinClient(object): self.http.stop_session() def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) if key.startswith('config'): return self.config[key.replace('config/', "", 1)] if "/" in key else self.config diff --git a/resources/lib/jellyfin/core/api.py b/resources/lib/jellyfin/core/api.py index d81a3a17..21b2d913 100644 --- a/resources/lib/jellyfin/core/api.py +++ b/resources/lib/jellyfin/core/api.py @@ -334,6 +334,3 @@ class API(object): return self._delete("Videos/ActiveEncodings", params={ 'DeviceId': device_id }) - - def delete_item(self, item_id): - return self.items("/%s" % item_id, "DELETE") diff --git a/resources/lib/jellyfin/core/configuration.py b/resources/lib/jellyfin/core/configuration.py index ae1f1ccd..0ad9850c 100644 --- a/resources/lib/jellyfin/core/configuration.py +++ b/resources/lib/jellyfin/core/configuration.py @@ -16,6 +16,7 @@ LOG = logging.getLogger('JELLYFIN.'+__name__) ################################################################################################# + class Config(object): def __init__(self): @@ -25,6 +26,7 @@ class Config(object): self.http() def __shortcuts__(self, key): + LOG.debug("__shortcuts__(%r)", key) if key == "auth": return self.auth @@ -38,14 +40,16 @@ class Config(object): return def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) return self.data.get(key, self.__shortcuts__(key)) def __setitem__(self, key, value): self.data[key] = value def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): - LOG.info("Begin app constructor.") + # import traceback + # LOG.debug(''.join(['\n'] + traceback.format_stack())) self.data['app.name'] = name self.data['app.version'] = version diff --git a/resources/lib/jellyfin/core/connection_manager.py b/resources/lib/jellyfin/core/connection_manager.py index 516d4f51..7d197de6 100644 --- a/resources/lib/jellyfin/core/connection_manager.py +++ b/resources/lib/jellyfin/core/connection_manager.py @@ -60,6 +60,7 @@ class ConnectionManager(object): self.http = HTTP(client) def __shortcuts__(self, key): + LOG.debug("__shortcuts__(%r)", key) if key == "clear": return self.clear_data @@ -97,6 +98,7 @@ class ConnectionManager(object): return def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) return self.__shortcuts__(key) def clear_data(self): @@ -120,7 +122,7 @@ class ConnectionManager(object): self.config['auth.token'] = None def get_available_servers(self): - + LOG.info("Begin getAvailableServers") # Clone the credentials @@ -163,12 +165,12 @@ class ConnectionManager(object): } result = self._request_url(request, False) - except Exception as error: # Failed to login - LOG.error(error) + except Exception as error: # Failed to login + LOG.exception(error) return False else: self._on_authenticated(result, options) - + return result def connect_to_address(self, address, options={}): @@ -184,7 +186,8 @@ class ConnectionManager(object): try: public_info = self._try_connect(address, options=options) - except Exception: + except Exception as error: + LOG.exception(error) return _on_fail() else: LOG.info("connectToAddress %s succeeded", address) @@ -238,7 +241,7 @@ class ConnectionManager(object): return {} servers = self.credentials.get_credentials()['Servers'] - + for server in servers: if server['Id'] == server_id: return server @@ -258,14 +261,14 @@ class ConnectionManager(object): try: return self.http.request(request) except Exception as error: - LOG.error(error) + LOG.exception(error) raise def _add_app_info(self): return "%s/%s" % (self.config['app.name'], self.config['app.version']) def _get_headers(self, request): - + headers = request.setdefault('headers', {}) if request.get('dataType') == "json": @@ -350,9 +353,9 @@ class ConnectionManager(object): try: result = self._try_connect(address, timeout, options) - + except Exception: - LOG.error("test failed for connection mode %s with server %s", mode, server.get('Name')) + LOG.exception("test failed for connection mode %s with server %s", mode, server.get('Name')) if enable_retry: # TODO: wake on lan and retry @@ -401,17 +404,17 @@ class ConnectionManager(object): if a > b: return 1 - + return 0 def _string_equals_ignore_case(self, str1, str2): return (str1 or "").lower() == (str2 or "").lower() def _server_discovery(self): - + MULTI_GROUP = ("", 7359) MESSAGE = "who is JellyfinServer?" - + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) # This controls the socket.timeout exception @@ -419,7 +422,7 @@ class ConnectionManager(object): sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1) - + LOG.debug("MultiGroup : %s", str(MULTI_GROUP)) LOG.debug("Sending UDP Data: %s", MESSAGE) @@ -428,20 +431,20 @@ class ConnectionManager(object): try: sock.sendto(MESSAGE, MULTI_GROUP) except Exception as error: - LOG.error(error) + LOG.exception(error) return servers while True: try: data, addr = sock.recvfrom(1024) # buffer size servers.append(json.loads(data)) - + except socket.timeout: LOG.info("Found Servers: %s", servers) return servers - + except Exception as e: - LOG.error("Error trying to find servers: %s", e) + LOG.exception("Error trying to find servers: %s", e) return servers def _get_last_used_server(self): @@ -488,7 +491,7 @@ class ConnectionManager(object): return servers def _convert_endpoint_address_to_manual_address(self, info): - + if info.get('Address') and info.get('EndpointAddress'): address = info['EndpointAddress'].split(':')[0] @@ -529,7 +532,7 @@ class ConnectionManager(object): self.config['auth.user_id'] = server.pop('UserId', None) self.config['auth.token'] = server.pop('AccessToken', None) - + elif verify_authentication and server.get('AccessToken'): if self._validate_authentication(server, connection_mode, options) is not False: @@ -579,6 +582,7 @@ class ConnectionManager(object): }) self._update_server_info(server, system_info) except Exception as error: + LOG.exception(error) server['UserId'] = None server['AccessToken'] = None diff --git a/resources/lib/jellyfin/core/credentials.py b/resources/lib/jellyfin/core/credentials.py index 83c4f924..aa1838b3 100644 --- a/resources/lib/jellyfin/core/credentials.py +++ b/resources/lib/jellyfin/core/credentials.py @@ -39,7 +39,7 @@ class Credentials(object): if not isinstance(self.credentials, dict): raise ValueError("invalid credentials format") - except Exception as e: # File is either empty or missing + except Exception as e: # File is either empty or missing LOG.warn(e) self.credentials = {} @@ -83,7 +83,7 @@ class Credentials(object): for existing in servers: if existing['Id'] == server['Id']: - + # Merge the data if server.get('DateLastAccessed'): if self._date_object(server['DateLastAccessed']) > self._date_object(existing['DateLastAccessed']): @@ -133,5 +133,5 @@ class Credentials(object): # TypeError: attribute of type 'NoneType' is not callable # Known Kodi/python error date_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6])) - + return date_obj diff --git a/resources/lib/jellyfin/core/http.py b/resources/lib/jellyfin/core/http.py index 65e6589e..dea4bc2b 100644 --- a/resources/lib/jellyfin/core/http.py +++ b/resources/lib/jellyfin/core/http.py @@ -15,6 +15,7 @@ LOG = logging.getLogger('Jellyfin.'+__name__) ################################################################################################# + class HTTP(object): session = None @@ -26,6 +27,7 @@ class HTTP(object): self.config = client['config'] def __shortcuts__(self, key): + LOG.debug("__shortcuts__(%r)", key) if key == "request": return self.request @@ -33,7 +35,7 @@ class HTTP(object): return def start_session(self): - + self.session = requests.Session() max_retries = self.config['http.max_retries'] @@ -41,7 +43,7 @@ class HTTP(object): self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries)) def stop_session(self): - + if self.session is None: return @@ -90,7 +92,7 @@ class HTTP(object): try: r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data) - r.content # release the connection + r.content # release the connection if not self.keep_alive and self.session is not None: self.stop_session() @@ -137,7 +139,7 @@ class HTTP(object): raise HTTPException("Unauthorized", error) - elif r.status_code == 500: # log and ignore. + elif r.status_code == 500: # log and ignore. LOG.error("--[ 500 response ] %s", error) return @@ -214,7 +216,7 @@ class HTTP(object): def _authorization(self, data): - auth = "MediaBrowser " + auth = "MediaBrowser " auth += "Client=%s, " % self.config['app.name'].encode('utf-8') auth += "Device=%s, " % self.config['app.device_name'].encode('utf-8') auth += "DeviceId=%s, " % self.config['app.device_id'].encode('utf-8') diff --git a/resources/lib/jellyfin/core/ws_client.py b/resources/lib/jellyfin/core/ws_client.py index 1784eca7..02d1e398 100644 --- a/resources/lib/jellyfin/core/ws_client.py +++ b/resources/lib/jellyfin/core/ws_client.py @@ -31,6 +31,7 @@ class WSClient(threading.Thread): threading.Thread.__init__(self) def __shortcuts__(self, key): + LOG.debug("__shortcuts__(%r)", key) if key == "send": return self.send @@ -54,7 +55,7 @@ class WSClient(threading.Thread): server = self.client['config/auth.server'] server = server.replace('https', "wss") if server.startswith('https') else server.replace('http', "ws") wsc_url = "%s/embywebsocket?api_key=%s&device_id=%s" % (server, token, device_id) - + LOG.info("Websocket url: %s", wsc_url) self.wsc = websocket.WebSocketApp(wsc_url, diff --git a/resources/lib/jellyfin/resources/websocket.py b/resources/lib/jellyfin/resources/websocket.py index 00733296..bf27ed69 100644 --- a/resources/lib/jellyfin/resources/websocket.py +++ b/resources/lib/jellyfin/resources/websocket.py @@ -633,7 +633,7 @@ class WebSocket(object): self._cont_data[1] += frame.data else: self._cont_data = [frame.opcode, frame.data] - + if frame.fin: data = self._cont_data self._cont_data = None @@ -706,12 +706,12 @@ class WebSocket(object): reason: the reason to close. This must be string. """ - + try: self.sock.shutdown(socket.SHUT_RDWR) except: pass - + ''' if self.connected: if status < 0 or status >= ABNF.LENGTH_16: @@ -746,6 +746,7 @@ class WebSocket(object): except socket.timeout as e: raise WebSocketTimeoutException(e.args[0]) except Exception as e: + logger.exception(e) if "timed out" in e.args[0]: raise WebSocketTimeoutException(e.args[0]) else: @@ -844,7 +845,7 @@ class WebSocketApp(object): """ self.keep_running = False if(self.sock != None): - self.sock.close() + self.sock.close() def _send_ping(self, interval): while True: @@ -885,20 +886,21 @@ class WebSocketApp(object): thread.start() while self.keep_running: - + try: data = self.sock.recv() - + if data is None or self.keep_running == False: break - self._callback(self.on_message, data) - - except Exception, e: - #print str(e.args[0]) + self._callback(self.on_message, data) + + except Exception as e: if "timed out" not in e.args[0]: + logger.exception(e) raise e - except Exception, e: + except Exception as e: + logger.exception(e) self._callback(self.on_error, e) finally: if thread: @@ -911,11 +913,8 @@ class WebSocketApp(object): if callback: try: callback(self, *args) - except Exception, e: - logger.error(e) - if True:#logger.isEnabledFor(logging.DEBUG): - _, _, tb = sys.exc_info() - traceback.print_tb(tb) + except Exception as e: + logger.exception(e) if __name__ == "__main__": diff --git a/resources/lib/library.py b/resources/lib/library.py index 16af50ce..a33a6407 100644 --- a/resources/lib/library.py +++ b/resources/lib/library.py @@ -128,7 +128,7 @@ class Library(threading.Thread): @stop() def service(self): - + ''' If error is encountered, it will rerun this function. Start new "daemon threads" to process library updates. (actual daemon thread is not supported in Kodi) @@ -170,7 +170,7 @@ class Library(threading.Thread): xbmc.executebuiltin('InhibitIdleShutdown(true)') self.screensaver = get_screensaver() set_screensaver(value="") - + if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']): self.pending_refresh = False @@ -230,7 +230,7 @@ class Library(threading.Thread): ''' for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)): if queue[0].qsize() and len(self.download_threads) < DTHREADS: - + new_thread = GetItemWorker(self.server, queue[0], queue[1]) new_thread.start() LOG.info("-->[ q:download/%s ]", id(new_thread)) @@ -296,7 +296,7 @@ class Library(threading.Thread): new_thread = RemovedWorker(queue, self.music_database_lock, "music", self.server, self.direct_path) else: new_thread = RemovedWorker(queue, self.database_lock, "video", self.server, self.direct_path) - + new_thread.start() LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread)) self.writer_threads['removed'].append(new_thread) @@ -316,8 +316,8 @@ class Library(threading.Thread): def startup(self): - ''' Run at startup. - Check databases. + ''' Run at startup. + Check databases. Check for the server plugin. ''' self.test_databases() @@ -334,10 +334,10 @@ class Library(threading.Thread): Views().get_nodes() except Exception as error: - LOG.error(error) + LOG.exception(error) elif not settings('SyncInstallRunDone.bool'): - + with FullSync(self, self.server) as sync: sync.libraries() @@ -350,7 +350,7 @@ class Library(threading.Thread): for plugin in self.server['api'].get_plugins(): if plugin['Name'] in ("Jellyfin.Kodi Sync Queue", "Kodi companion", "Kodi Sync Queue"): - + if not self.fast_sync(): dialog("ok", heading="{jellyfin}", line1=_(33128)) @@ -435,7 +435,7 @@ class Library(threading.Thread): self.userdata(result['UserDataChanged']) self.removed(result['ItemsRemoved']) - + filters.extend(["tvshows", "boxsets", "musicvideos", "music"]) # Get only movies. @@ -454,12 +454,12 @@ class Library(threading.Thread): return True def save_last_sync(self): - + try: time_now = datetime.strptime(self.server['config/server-time'].split(', ', 1)[1], '%d %b %Y %H:%M:%S GMT') - timedelta(minutes=2) except Exception as error: - LOG.error(error) + LOG.exception(error) time_now = datetime.utcnow() - timedelta(minutes=2) last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz') @@ -703,7 +703,9 @@ class SortWorker(threading.Thread): try: media = database.get_media_by_id(item_id) self.output[media].put({'Id': item_id, 'Type': media}) - except Exception: + except Exception as error: + LOG.exception(error) + items = database.get_media_by_parent_id(item_id) if not items: diff --git a/resources/lib/monitor.py b/resources/lib/monitor.py index bc34b8a8..9e6b9c39 100644 --- a/resources/lib/monitor.py +++ b/resources/lib/monitor.py @@ -113,13 +113,14 @@ class Monitor(xbmc.Monitor): self.server_instance(data['ServerId']) except Exception as error: - LOG.error(error) + LOG.exception(error) dialog("ok", heading="{jellyfin}", line1=_(33142)) return server = Jellyfin(data['ServerId']) - except Exception: + except Exception as error: + LOG.exception(error) server = Jellyfin() if method == 'GetItem': @@ -328,7 +329,7 @@ class Monitor(xbmc.Monitor): try: session = server['api'].get_device(self.device_id) except Exception as error: - LOG.error(error) + LOG.exception(error) return diff --git a/resources/lib/objects/actions.py b/resources/lib/objects/actions.py index 9826d3df..de919d8c 100644 --- a/resources/lib/objects/actions.py +++ b/resources/lib/objects/actions.py @@ -765,7 +765,7 @@ def on_play(data, server): try: file = player.getPlayingFile() except Exception as error: - LOG.error(error) + LOG.exception(error) return diff --git a/resources/lib/objects/kodi/artwork.py b/resources/lib/objects/kodi/artwork.py index c30ce155..5b7acc8d 100644 --- a/resources/lib/objects/kodi/artwork.py +++ b/resources/lib/objects/kodi/artwork.py @@ -136,7 +136,7 @@ class Artwork(object): return text def single_urlencode(self, text): - + ''' urlencode needs a utf-string. return the result as unicode ''' @@ -211,8 +211,8 @@ class GetArtworkWorker(threading.Thread): prep.url = "http://%s:%s/image/image://%s" % (self.kodi['host'], self.kodi['port'], url) s.send(prep, timeout=(0.01, 0.01)) s.content # release the connection - except Exception: - pass + except Exception as error: + LOG.exception(error) self.queue.task_done() @@ -361,11 +361,11 @@ class Artwork(object): def _cache_all_music_entries(self, pdialog): with Database('music') as cursor_music: - + cursor_music.execute("SELECT url FROM art") result = cursor_music.fetchall() total = len(result) - + log.info("Image cache sync about to process %s images", total) count = 0 diff --git a/resources/lib/objects/movies.py b/resources/lib/objects/movies.py index c1b2f5b7..b5f8ef05 100644 --- a/resources/lib/objects/movies.py +++ b/resources/lib/objects/movies.py @@ -35,6 +35,7 @@ class Movies(KodiDb): KodiDb.__init__(self, videodb.cursor) def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) if key == 'Movie': return self.movie @@ -49,7 +50,7 @@ class Movies(KodiDb): @jellyfin_item() @library_check() def movie(self, item, e_item, library): - + ''' If item does not exist, entry will be added. If item exists, entry will be updated. ''' @@ -175,7 +176,7 @@ class Movies(KodiDb): obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] except Exception as error: - LOG.error("Failed to get trailer: %s", error) + LOG.exception("Failed to get trailer: %s", error) obj['Trailer'] = None def get_path_filename(self, obj): @@ -205,7 +206,7 @@ class Movies(KodiDb): @stop() @jellyfin_item() def boxset(self, item, e_item): - + ''' If item does not exist, entry will be added. If item exists, entry will be updated. @@ -286,7 +287,7 @@ class Movies(KodiDb): @stop() @jellyfin_item() def userdata(self, item, e_item): - + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar ''' @@ -339,7 +340,7 @@ class Movies(KodiDb): elif obj['Media'] == 'set': for movie in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_movie_obj)): - + temp_obj = dict(obj) temp_obj['MovieId'] = movie[1] temp_obj['Movie'] = movie[0] diff --git a/resources/lib/objects/music.py b/resources/lib/objects/music.py index cb1c1c4a..d1c502fd 100644 --- a/resources/lib/objects/music.py +++ b/resources/lib/objects/music.py @@ -35,6 +35,7 @@ class Music(KodiDb): KodiDb.__init__(self, musicdb.cursor) def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) if key in ('MusicArtist', 'AlbumArtist'): return self.artist @@ -100,7 +101,7 @@ class Music(KodiDb): self.item_ids.append(obj['Id']) def artist_add(self, obj): - + ''' Add object to kodi. safety checks: It looks like Jellyfin supports the same artist multiple times. @@ -168,7 +169,7 @@ class Music(KodiDb): self.item_ids.append(obj['Id']) def album_add(self, obj): - + ''' Add object to kodi. ''' obj['AlbumId'] = self.get_album(*values(obj, QU.get_album_obj)) @@ -176,7 +177,7 @@ class Music(KodiDb): LOG.info("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id']) def album_update(self, obj): - + ''' Update object to kodi. ''' self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) @@ -219,7 +220,7 @@ class Music(KodiDb): self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] except Exception as error: - LOG.error(error) + LOG.exception(error) continue self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj)) @@ -301,7 +302,7 @@ class Music(KodiDb): return not update def song_add(self, obj): - + ''' Add object to kodi. Verify if there's an album associated. @@ -327,7 +328,7 @@ class Music(KodiDb): LOG.debug("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) def song_update(self, obj): - + ''' Update object to kodi. ''' self.update_path(*values(obj, QU.update_path_obj)) @@ -337,7 +338,7 @@ class Music(KodiDb): LOG.info("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title']) def get_song_path_filename(self, obj, api): - + ''' Get the path and filename and build it into protocol://path ''' obj['Path'] = api.get_file_path(obj['Path']) @@ -355,7 +356,7 @@ class Music(KodiDb): obj['Filename'] = "stream.%s?static=true" % obj['Container'] def song_artist_discography(self, obj): - + ''' Update the artist's discography. ''' artists = [] @@ -375,7 +376,7 @@ class Music(KodiDb): self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] except Exception as error: - LOG.error(error) + LOG.exception(error) continue self.link(*values(temp_obj, QU.update_link_obj)) @@ -390,7 +391,7 @@ class Music(KodiDb): obj['AlbumArtists'] = artists def song_artist_link(self, obj): - + ''' Assign main artists to song. Artist does not exist in jellyfin database, create the reference. ''' @@ -409,7 +410,7 @@ class Music(KodiDb): self.artist(self.server['api'].get_item(temp_obj['Id']), library=None) temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0] except Exception as error: - LOG.error(error) + LOG.exception(error) continue self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj)) @@ -424,7 +425,7 @@ class Music(KodiDb): @stop() @jellyfin_item() def userdata(self, item, e_item): - + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar ''' @@ -452,7 +453,7 @@ class Music(KodiDb): @stop() @jellyfin_item() def remove(self, item_id, e_item): - + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar @@ -468,7 +469,7 @@ class Music(KodiDb): return if obj['Media'] == 'song': - + self.remove_song(obj['KodiId'], obj['Id']) self.jellyfin_db.remove_wild_item(obj['id']) @@ -513,19 +514,19 @@ class Music(KodiDb): self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) def remove_artist(self, kodi_id, item_id): - + self.artwork.delete(kodi_id, "artist") self.delete(kodi_id) LOG.info("DELETE artist [%s] %s", kodi_id, item_id) def remove_album(self, kodi_id, item_id): - + self.artwork.delete(kodi_id, "album") self.delete_album(kodi_id) LOG.info("DELETE album [%s] %s", kodi_id, item_id) def remove_song(self, kodi_id, item_id): - + self.artwork.delete(kodi_id, "song") self.delete_song(kodi_id) LOG.info("DELETE song [%s] %s", kodi_id, item_id) diff --git a/resources/lib/objects/musicvideos.py b/resources/lib/objects/musicvideos.py index e4beb00d..44dd33e6 100644 --- a/resources/lib/objects/musicvideos.py +++ b/resources/lib/objects/musicvideos.py @@ -35,6 +35,7 @@ class MusicVideos(KodiDb): KodiDb.__init__(self, videodb.cursor) def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) if key == 'MusicVideo': return self.musicvideo @@ -142,7 +143,7 @@ class MusicVideos(KodiDb): return not update def musicvideo_add(self, obj): - + ''' Add object to kodi. ''' obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) @@ -153,7 +154,7 @@ class MusicVideos(KodiDb): LOG.info("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) def musicvideo_update(self, obj): - + ''' Update object to kodi. ''' self.update(*values(obj, QU.update_musicvideo_obj)) @@ -161,7 +162,7 @@ class MusicVideos(KodiDb): LOG.info("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title']) def get_path_filename(self, obj): - + ''' Get the path and filename and build it into protocol://path ''' obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] @@ -187,7 +188,7 @@ class MusicVideos(KodiDb): @stop() @jellyfin_item() def userdata(self, item, e_item): - + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar ''' @@ -220,7 +221,7 @@ class MusicVideos(KodiDb): @jellyfin_item() def remove(self, item_id, e_item): - ''' Remove mvideoid, fileid, pathid, jellyfin reference. + ''' Remove mvideoid, fileid, pathid, jellyfin reference. ''' obj = {'Id': item_id} diff --git a/resources/lib/objects/tvshows.py b/resources/lib/objects/tvshows.py index 33d96dbd..fa7d614e 100644 --- a/resources/lib/objects/tvshows.py +++ b/resources/lib/objects/tvshows.py @@ -38,6 +38,7 @@ class TVShows(KodiDb): KodiDb.__init__(self, videodb.cursor) def __getitem__(self, key): + LOG.debug("__getitem__(%r)", key) if key == 'Series': return self.tvshow @@ -152,7 +153,7 @@ class TVShows(KodiDb): self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_pool_obj)) LOG.info("POOL %s [%s/%s]", obj['Title'], obj['Id'], obj['SeriesId']) season_episodes[season['Id']] = season['SeriesId'] - + try: self.jellyfin_db.get_item_by_id(season['Id'])[0] self.item_ids.append(season['Id']) @@ -188,7 +189,7 @@ class TVShows(KodiDb): LOG.info("ADD tvshow [%s/%s/%s] %s: %s", obj['TopPathId'], obj['PathId'], obj['ShowId'], obj['Title'], obj['Id']) def tvshow_update(self, obj): - + ''' Update object to kodi. ''' obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_unique_id_tvshow_obj)) @@ -363,7 +364,7 @@ class TVShows(KodiDb): return not update def episode_add(self, obj): - + ''' Add object to kodi. ''' obj['RatingId'] = self.create_entry_rating() @@ -388,9 +389,9 @@ class TVShows(KodiDb): LOG.debug("ADD episode [%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['Id'], obj['Title']) def episode_update(self, obj): - + ''' Update object to kodi. - ''' + ''' obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_episode_obj)) self.update_ratings(*values(obj, QU.update_rating_episode_obj)) @@ -451,7 +452,7 @@ class TVShows(KodiDb): @stop() @jellyfin_item() def userdata(self, item, e_item): - + ''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar @@ -476,7 +477,7 @@ class TVShows(KodiDb): self.remove_tag(*values(obj, QU.delete_tag_episode_obj)) elif obj['Media'] == "episode": - + obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) @@ -511,7 +512,7 @@ class TVShows(KodiDb): @stop() @jellyfin_item() def remove(self, item_id, e_item): - + ''' Remove showid, fileid, pathid, jellyfin reference. There's no episodes left, delete show and any possible remaining seasons ''' @@ -558,7 +559,7 @@ class TVShows(KodiDb): obj['ParentId'] = obj['KodiId'] for season in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): - + temp_obj = dict(obj) temp_obj['ParentId'] = season[1] @@ -594,7 +595,7 @@ class TVShows(KodiDb): self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj)) def remove_tvshow(self, kodi_id, item_id): - + self.artwork.delete(kodi_id, "tvshow") self.delete_tvshow(kodi_id) LOG.debug("DELETE tvshow [%s] %s", kodi_id, item_id) @@ -630,7 +631,7 @@ class TVShows(KodiDb): obj['ParentId'] = obj['KodiId'] for season in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_season_obj)): - + temp_obj = dict(obj) temp_obj['ParentId'] = season[1] child.append(season[0]) diff --git a/resources/lib/objects/utils.py b/resources/lib/objects/utils.py index 9f3b5052..4048a118 100644 --- a/resources/lib/objects/utils.py +++ b/resources/lib/objects/utils.py @@ -12,6 +12,7 @@ LOG = logging.getLogger("JELLYFIN."+__name__) ################################################################################################# + def get_play_action(): ''' I could not figure out a way to listen to kodi setting changes? @@ -22,16 +23,14 @@ def get_play_action(): try: return options[result['result']['value']] except Exception as error: - log.error("Returning play action due to error: %s", error) + LOG.exception("Returning play action due to error: %s", error) return options[1] + def get_grouped_set(): ''' Get if boxsets should be grouped ''' result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) - try: - return result['result']['value'] - except Exception as error: - return False + return result.get('result', {}).get('value', False) diff --git a/resources/lib/player.py b/resources/lib/player.py index 8f5e9c1d..712c9bb9 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -1,447 +1,447 @@ -# -*- coding: utf-8 -*- - -################################################################################################# - -import json -import logging -import os - -import xbmc -import xbmcvfs - -from objects.obj import Objects -from helper import _, api, window, settings, dialog, event, silent_catch, JSONRPC -from jellyfin import Jellyfin - -################################################################################################# - -LOG = logging.getLogger("JELLYFIN."+__name__) - -################################################################################################# - - -class Player(xbmc.Player): - - played = {} - up_next = False - - def __init__(self): - xbmc.Player.__init__(self) - - @silent_catch() - def get_playing_file(self): - return self.getPlayingFile() - - @silent_catch() - def get_file_info(self, file): - return self.played[file] - - def is_playing_file(self, file): - return file in self.played - - def onPlayBackStarted(self): - - ''' We may need to wait for info to be set in kodi monitor. - Accounts for scenario where Kodi starts playback and exits immediately. - First, ensure previous playback terminated correctly in Jellyfin. - ''' - self.stop_playback() - self.up_next = False - count = 0 - monitor = xbmc.Monitor() - - try: - current_file = self.getPlayingFile() - except Exception: - - while count < 5: - try: - current_file = self.getPlayingFile() - count = 0 - break - except Exception: - count += 1 - - if monitor.waitForAbort(1): - return - else: - LOG.info('Cancel playback report') - - return - - items = window('jellyfin_play.json') - item = None - - while not items: - - if monitor.waitForAbort(2): - return - - items = window('jellyfin_play.json') - count += 1 - - if count == 20: - LOG.info("Could not find jellyfin prop...") - - return - - for item in items: - if item['Path'] == current_file.decode('utf-8'): - items.pop(items.index(item)) - - break - else: - item = items.pop(0) - - window('jellyfin_play.json', items) - - self.set_item(current_file, item) - data = { - 'QueueableMediaTypes': "Video,Audio", - 'CanSeek': True, - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PlayMethod': item['PlayMethod'], - 'VolumeLevel': item['Volume'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'IsPaused': item['Paused'], - 'IsMuted': item['Muted'], - 'PlaySessionId': item['PlaySessionId'], - 'AudioStreamIndex': item['AudioStreamIndex'], - 'SubtitleStreamIndex': item['SubtitleStreamIndex'] - } - item['Server']['api'].session_playing(data) - window('jellyfin.skip.%s.bool' % item['Id'], True) - - if monitor.waitForAbort(2): - return - - if item['PlayOption'] == 'Addon': - self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex']) - - def set_item(self, file, item): - - ''' Set playback information. - ''' - try: - item['Runtime'] = int(item['Runtime']) - except (TypeError, ValueError): - try: - item['Runtime'] = int(self.getTotalTime()) - LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime']) - except Exception: - item['Runtime'] = 0 - LOG.info("Runtime is missing, Using Zero") - - try: - seektime = self.getTime() - except Exception: # at this point we should be playing and if not then bail out - return - - result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) - result = result.get('result', {}) - volume = result.get('volume') - muted = result.get('muted') - - item.update({ - 'File': file, - 'CurrentPosition': item.get('CurrentPosition') or int(seektime), - 'Muted': muted, - 'Volume': volume, - 'Server': Jellyfin(item['ServerId']).get_client(), - 'Paused': False - }) - - self.played[file] = item - LOG.info("-->[ play/%s ] %s", item['Id'], item) - - def set_audio_subs(self, audio=None, subtitle=None): - - ''' Only for after playback started - ''' - LOG.info("Setting audio: %s subs: %s", audio, subtitle) - current_file = self.get_playing_file() - - if self.is_playing_file(current_file): - - item = self.get_file_info(current_file) - mapping = item['SubsMapping'] - - if audio and len(self.getAvailableAudioStreams()) > 1: - self.setAudioStream(audio - 1) - - if subtitle == -1 or subtitle is None: - self.showSubtitles(False) - - return - - tracks = len(self.getAvailableAudioStreams()) - - if mapping: - for index in mapping: - - if mapping[index] == subtitle: - self.setSubtitleStream(int(index)) - - break - else: - self.setSubtitleStream(len(mapping) + subtitle - tracks - 1) - else: - self.setSubtitleStream(subtitle - tracks - 1) - - def detect_audio_subs(self, item): - - params = { - 'playerid': 1, - 'properties': ["currentsubtitle","currentaudiostream","subtitleenabled"] - } - result = JSONRPC('Player.GetProperties').execute(params) - result = result.get('result') - - try: # Audio tracks - audio = result['currentaudiostream']['index'] - except (KeyError, TypeError): - audio = 0 - - try: # Subtitles tracks - subs = result['currentsubtitle']['index'] - except (KeyError, TypeError): - subs = 0 - - try: # If subtitles are enabled - subs_enabled = result['subtitleenabled'] - except (KeyError, TypeError): - subs_enabled = False - - item['AudioStreamIndex'] = audio + 1 - - if not subs_enabled or not len(self.getAvailableSubtitleStreams()): - item['SubtitleStreamIndex'] = None - - return - - mapping = item['SubsMapping'] - tracks = len(self.getAvailableAudioStreams()) - - if mapping: - if str(subs) in mapping: - item['SubtitleStreamIndex'] = mapping[str(subs)] - else: - item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1 - else: - item['SubtitleStreamIndex'] = subs + tracks + 1 - - def next_up(self): - - item = self.get_file_info(self.get_playing_file()) - objects = Objects() - - if item['Type'] != 'Episode' or not item.get('CurrentEpisode'): - return - - next_items = item['Server']['api'].get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id']) - - for index, next_item in enumerate(next_items['Items']): - if next_item['Id'] == item['Id']: - - try: - next_item = next_items['Items'][index + 1] - except IndexError: - LOG.warn("No next up episode.") - - return - - break - - API = api.API(next_item, item['Server']['auth/server-address']) - data = objects.map(next_item, "UpNext") - artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True) - data['art'] = { - 'tvshow.poster': artwork.get('Series.Primary'), - 'tvshow.fanart': None, - 'thumb': artwork.get('Primary') - } - if artwork['Backdrop']: - data['art']['tvshow.fanart'] = artwork['Backdrop'][0] - - next_info = { - 'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'}, - 'current_episode': item['CurrentEpisode'], - 'next_episode': data - } - - LOG.info("--[ next up ] %s", next_info) - event("upnext_data", next_info, hexlify=True) - - def onPlayBackPaused(self): - current_file = self.get_playing_file() - - if self.is_playing_file(current_file): - - self.get_file_info(current_file)['Paused'] = True - self.report_playback() - LOG.debug("-->[ paused ]") - - def onPlayBackResumed(self): - current_file = self.get_playing_file() - - if self.is_playing_file(current_file): - - self.get_file_info(current_file)['Paused'] = False - self.report_playback() - LOG.debug("--<[ paused ]") - - def onPlayBackSeek(self, time, seekOffset): - - ''' Does not seem to work in Leia?? - ''' - if self.is_playing_file(self.get_playing_file()): - - self.report_playback() - LOG.info("--[ seek ]") - - def report_playback(self, report=True): - - ''' Report playback progress to jellyfin server. - Check if the user seek. - ''' - current_file = self.get_playing_file() - - if not self.is_playing_file(current_file): - return - - item = self.get_file_info(current_file) - - if window('jellyfin.external.bool'): - return - - if not report: - - previous = item['CurrentPosition'] - item['CurrentPosition'] = int(self.getTime()) - - if int(item['CurrentPosition']) == 1: - return - - try: - played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100 - except ZeroDivisionError: # Runtime is 0. - played = 0 - - if played > 2.0 and not self.up_next: - - self.up_next = True - self.next_up() - - if (item['CurrentPosition'] - previous) < 30: - - return - - - result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) - result = result.get('result', {}) - item['Volume'] = result.get('volume') - item['Muted'] = result.get('muted') - item['CurrentPosition'] = int(self.getTime()) - self.detect_audio_subs(item) - - data = { - 'QueueableMediaTypes': "Video,Audio", - 'CanSeek': True, - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PlayMethod': item['PlayMethod'], - 'VolumeLevel': item['Volume'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'IsPaused': item['Paused'], - 'IsMuted': item['Muted'], - 'PlaySessionId': item['PlaySessionId'], - 'AudioStreamIndex': item['AudioStreamIndex'], - 'SubtitleStreamIndex': item['SubtitleStreamIndex'] - } - item['Server']['api'].session_progress(data) - - def onPlayBackStopped(self): - - ''' Will be called when user stops playing a file. - ''' - window('jellyfin_play', clear=True) - self.stop_playback() - LOG.info("--<[ playback ]") - - def onPlayBackEnded(self): - - ''' Will be called when kodi stops playing a file. - ''' - self.stop_playback() - LOG.info("--<<[ playback ]") - - def stop_playback(self): - - ''' Stop all playback. Check for external player for positionticks. - ''' - if not self.played: - return - - LOG.info("Played info: %s", self.played) - - for file in self.played: - item = self.get_file_info(file) - - window('jellyfin.skip.%s.bool' % item['Id'], True) - - if window('jellyfin.external.bool'): - window('jellyfin.external', clear=True) - - if int(item['CurrentPosition']) == 1: - item['CurrentPosition'] = int(item['Runtime']) - - data = { - 'ItemId': item['Id'], - 'MediaSourceId': item['MediaSourceId'], - 'PositionTicks': int(item['CurrentPosition'] * 10000000), - 'PlaySessionId': item['PlaySessionId'] - } - item['Server']['api'].session_stop(data) - - if item.get('LiveStreamId'): - - LOG.info("<[ livestream/%s ]", item['LiveStreamId']) - item['Server']['api'].close_live_stream(item['LiveStreamId']) - - elif item['PlayMethod'] == 'Transcode': - - LOG.info("<[ transcode/%s ]", item['Id']) - item['Server']['api'].close_transcode(item['DeviceId']) - - - path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/temp/").decode('utf-8') - - if xbmcvfs.exists(path): - dirs, files = xbmcvfs.listdir(path) - - for file in files: - xbmcvfs.delete(os.path.join(path, file.decode('utf-8'))) - - result = item['Server']['api'].get_item(item['Id']) or {} - - if 'UserData' in result and result['UserData']['Played']: - delete = False - - if result['Type'] == 'Episode' and settings('deleteTV.bool'): - delete = True - elif result['Type'] == 'Movie' and settings('deleteMovies.bool'): - delete = True - - if not settings('offerDelete.bool'): - delete = False - - if delete: - LOG.info("Offer delete option") - - if dialog("yesno", heading=_(30091), line1=_(33015), autoclose=120000): - item['Server']['api'].delete_item(item['Id']) - - window('jellyfin.external_check', clear=True) - - self.played.clear() +# -*- coding: utf-8 -*- + +################################################################################################# + +import json +import logging +import os + +import xbmc +import xbmcvfs + +from objects.obj import Objects +from helper import _, api, window, settings, dialog, event, silent_catch, JSONRPC +from jellyfin import Jellyfin + +################################################################################################# + +LOG = logging.getLogger("JELLYFIN."+__name__) + +################################################################################################# + + +class Player(xbmc.Player): + + played = {} + up_next = False + + def __init__(self): + xbmc.Player.__init__(self) + + @silent_catch() + def get_playing_file(self): + return self.getPlayingFile() + + @silent_catch() + def get_file_info(self, file): + return self.played[file] + + def is_playing_file(self, file): + return file in self.played + + def onPlayBackStarted(self): + + ''' We may need to wait for info to be set in kodi monitor. + Accounts for scenario where Kodi starts playback and exits immediately. + First, ensure previous playback terminated correctly in Jellyfin. + ''' + self.stop_playback() + self.up_next = False + count = 0 + monitor = xbmc.Monitor() + + try: + current_file = self.getPlayingFile() + except Exception: + + while count < 5: + try: + current_file = self.getPlayingFile() + count = 0 + break + except Exception: + count += 1 + + if monitor.waitForAbort(1): + return + else: + LOG.info('Cancel playback report') + + return + + items = window('jellyfin_play.json') + item = None + + while not items: + + if monitor.waitForAbort(2): + return + + items = window('jellyfin_play.json') + count += 1 + + if count == 20: + LOG.info("Could not find jellyfin prop...") + + return + + for item in items: + if item['Path'] == current_file.decode('utf-8'): + items.pop(items.index(item)) + + break + else: + item = items.pop(0) + + window('jellyfin_play.json', items) + + self.set_item(current_file, item) + data = { + 'QueueableMediaTypes': "Video,Audio", + 'CanSeek': True, + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PlayMethod': item['PlayMethod'], + 'VolumeLevel': item['Volume'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'IsPaused': item['Paused'], + 'IsMuted': item['Muted'], + 'PlaySessionId': item['PlaySessionId'], + 'AudioStreamIndex': item['AudioStreamIndex'], + 'SubtitleStreamIndex': item['SubtitleStreamIndex'] + } + item['Server']['api'].session_playing(data) + window('jellyfin.skip.%s.bool' % item['Id'], True) + + if monitor.waitForAbort(2): + return + + if item['PlayOption'] == 'Addon': + self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex']) + + def set_item(self, file, item): + + ''' Set playback information. + ''' + try: + item['Runtime'] = int(item['Runtime']) + except (TypeError, ValueError): + try: + item['Runtime'] = int(self.getTotalTime()) + LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime']) + except Exception: + item['Runtime'] = 0 + LOG.info("Runtime is missing, Using Zero") + + try: + seektime = self.getTime() + except Exception: # at this point we should be playing and if not then bail out + return + + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) + result = result.get('result', {}) + volume = result.get('volume') + muted = result.get('muted') + + item.update({ + 'File': file, + 'CurrentPosition': item.get('CurrentPosition') or int(seektime), + 'Muted': muted, + 'Volume': volume, + 'Server': Jellyfin(item['ServerId']).get_client(), + 'Paused': False + }) + + self.played[file] = item + LOG.info("-->[ play/%s ] %s", item['Id'], item) + + def set_audio_subs(self, audio=None, subtitle=None): + + ''' Only for after playback started + ''' + LOG.info("Setting audio: %s subs: %s", audio, subtitle) + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + item = self.get_file_info(current_file) + mapping = item['SubsMapping'] + + if audio and len(self.getAvailableAudioStreams()) > 1: + self.setAudioStream(audio - 1) + + if subtitle == -1 or subtitle is None: + self.showSubtitles(False) + + return + + tracks = len(self.getAvailableAudioStreams()) + + if mapping: + for index in mapping: + + if mapping[index] == subtitle: + self.setSubtitleStream(int(index)) + + break + else: + self.setSubtitleStream(len(mapping) + subtitle - tracks - 1) + else: + self.setSubtitleStream(subtitle - tracks - 1) + + def detect_audio_subs(self, item): + + params = { + 'playerid': 1, + 'properties': ["currentsubtitle", "currentaudiostream", "subtitleenabled"] + } + result = JSONRPC('Player.GetProperties').execute(params) + result = result.get('result') + + try: # Audio tracks + audio = result['currentaudiostream']['index'] + except (KeyError, TypeError): + audio = 0 + + try: # Subtitles tracks + subs = result['currentsubtitle']['index'] + except (KeyError, TypeError): + subs = 0 + + try: # If subtitles are enabled + subs_enabled = result['subtitleenabled'] + except (KeyError, TypeError): + subs_enabled = False + + item['AudioStreamIndex'] = audio + 1 + + if not subs_enabled or not len(self.getAvailableSubtitleStreams()): + item['SubtitleStreamIndex'] = None + + return + + mapping = item['SubsMapping'] + tracks = len(self.getAvailableAudioStreams()) + + if mapping: + if str(subs) in mapping: + item['SubtitleStreamIndex'] = mapping[str(subs)] + else: + item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1 + else: + item['SubtitleStreamIndex'] = subs + tracks + 1 + + def next_up(self): + + item = self.get_file_info(self.get_playing_file()) + objects = Objects() + + if item['Type'] != 'Episode' or not item.get('CurrentEpisode'): + return + + next_items = item['Server']['api'].get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id']) + + for index, next_item in enumerate(next_items['Items']): + if next_item['Id'] == item['Id']: + + try: + next_item = next_items['Items'][index + 1] + except IndexError: + LOG.warn("No next up episode.") + + return + + break + + API = api.API(next_item, item['Server']['auth/server-address']) + data = objects.map(next_item, "UpNext") + artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True) + data['art'] = { + 'tvshow.poster': artwork.get('Series.Primary'), + 'tvshow.fanart': None, + 'thumb': artwork.get('Primary') + } + if artwork['Backdrop']: + data['art']['tvshow.fanart'] = artwork['Backdrop'][0] + + next_info = { + 'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'}, + 'current_episode': item['CurrentEpisode'], + 'next_episode': data + } + + LOG.info("--[ next up ] %s", next_info) + event("upnext_data", next_info, hexlify=True) + + def onPlayBackPaused(self): + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + self.get_file_info(current_file)['Paused'] = True + self.report_playback() + LOG.debug("-->[ paused ]") + + def onPlayBackResumed(self): + current_file = self.get_playing_file() + + if self.is_playing_file(current_file): + + self.get_file_info(current_file)['Paused'] = False + self.report_playback() + LOG.debug("--<[ paused ]") + + def onPlayBackSeek(self, time, seekOffset): + + ''' Does not seem to work in Leia?? + ''' + if self.is_playing_file(self.get_playing_file()): + + self.report_playback() + LOG.info("--[ seek ]") + + def report_playback(self, report=True): + + ''' Report playback progress to jellyfin server. + Check if the user seek. + ''' + current_file = self.get_playing_file() + + if not self.is_playing_file(current_file): + return + + item = self.get_file_info(current_file) + + if window('jellyfin.external.bool'): + return + + if not report: + + previous = item['CurrentPosition'] + item['CurrentPosition'] = int(self.getTime()) + + if int(item['CurrentPosition']) == 1: + return + + try: + played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100 + except ZeroDivisionError: # Runtime is 0. + played = 0 + + if played > 2.0 and not self.up_next: + + self.up_next = True + self.next_up() + + if (item['CurrentPosition'] - previous) < 30: + + return + + + result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) + result = result.get('result', {}) + item['Volume'] = result.get('volume') + item['Muted'] = result.get('muted') + item['CurrentPosition'] = int(self.getTime()) + self.detect_audio_subs(item) + + data = { + 'QueueableMediaTypes': "Video,Audio", + 'CanSeek': True, + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PlayMethod': item['PlayMethod'], + 'VolumeLevel': item['Volume'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'IsPaused': item['Paused'], + 'IsMuted': item['Muted'], + 'PlaySessionId': item['PlaySessionId'], + 'AudioStreamIndex': item['AudioStreamIndex'], + 'SubtitleStreamIndex': item['SubtitleStreamIndex'] + } + item['Server']['api'].session_progress(data) + + def onPlayBackStopped(self): + + ''' Will be called when user stops playing a file. + ''' + window('jellyfin_play', clear=True) + self.stop_playback() + LOG.info("--<[ playback ]") + + def onPlayBackEnded(self): + + ''' Will be called when kodi stops playing a file. + ''' + self.stop_playback() + LOG.info("--<<[ playback ]") + + def stop_playback(self): + + ''' Stop all playback. Check for external player for positionticks. + ''' + if not self.played: + return + + LOG.info("Played info: %s", self.played) + + for file in self.played: + item = self.get_file_info(file) + + window('jellyfin.skip.%s.bool' % item['Id'], True) + + if window('jellyfin.external.bool'): + window('jellyfin.external', clear=True) + + if int(item['CurrentPosition']) == 1: + item['CurrentPosition'] = int(item['Runtime']) + + data = { + 'ItemId': item['Id'], + 'MediaSourceId': item['MediaSourceId'], + 'PositionTicks': int(item['CurrentPosition'] * 10000000), + 'PlaySessionId': item['PlaySessionId'] + } + item['Server']['api'].session_stop(data) + + if item.get('LiveStreamId'): + + LOG.info("<[ livestream/%s ]", item['LiveStreamId']) + item['Server']['api'].close_live_stream(item['LiveStreamId']) + + elif item['PlayMethod'] == 'Transcode': + + LOG.info("<[ transcode/%s ]", item['Id']) + item['Server']['api'].close_transcode(item['DeviceId']) + + + path = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/temp/").decode('utf-8') + + if xbmcvfs.exists(path): + dirs, files = xbmcvfs.listdir(path) + + for file in files: + xbmcvfs.delete(os.path.join(path, file.decode('utf-8'))) + + result = item['Server']['api'].get_item(item['Id']) or {} + + if 'UserData' in result and result['UserData']['Played']: + delete = False + + if result['Type'] == 'Episode' and settings('deleteTV.bool'): + delete = True + elif result['Type'] == 'Movie' and settings('deleteMovies.bool'): + delete = True + + if not settings('offerDelete.bool'): + delete = False + + if delete: + LOG.info("Offer delete option") + + if dialog("yesno", heading=_(30091), line1=_(33015), autoclose=120000): + item['Server']['api'].delete_item(item['Id']) + + window('jellyfin.external_check', clear=True) + + self.played.clear() diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index ab17e21e..3eb51da4 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -33,7 +33,7 @@ class WebService(threading.Thread): conn.request("QUIT", "/") conn.getresponse() except Exception as error: - pass + LOG.exception(error) def run(self): @@ -46,7 +46,7 @@ class WebService(threading.Thread): server.serve_forever() except Exception as error: - if '10053' not in error: # ignore host diconnected errors + if '10053' not in error: # ignore host diconnected errors LOG.exception(error) LOG.info("---<[ webservice ]") @@ -132,13 +132,13 @@ class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.wfile.write(path) except IndexError as error: + LOG.exception(error) - xbmc.log(str(error), xbmc.LOGWARNING) self.send_error(404, "Exception occurred: %s" % error) except Exception as error: + LOG.exception(error) - xbmc.log(str(error), xbmc.LOGWARNING) self.send_error(500, "Exception occurred: %s" % error) return diff --git a/service.py b/service.py index adb58c05..1176e314 100644 --- a/service.py +++ b/service.py @@ -45,8 +45,8 @@ DELAY = int(settings('startupDelay') if settings('SyncInstallRunDone.bool') else class ServiceManager(threading.Thread): - ''' Service thread. - To allow to restart and reload modules internally. + ''' Service thread. + To allow to restart and reload modules internally. ''' exception = None @@ -64,12 +64,13 @@ class ServiceManager(threading.Thread): service.service() except Exception as error: + LOG.exception(error) if service is not None: - if not 'ExitService' in error: + if 'ExitService' not in error: service.shutdown() - + if 'RestartService' in error: service.reload_objects() @@ -91,7 +92,7 @@ if __name__ == "__main__": try: session = ServiceManager() session.start() - session.join() # Block until the thread exits. + session.join() # Block until the thread exits. if 'RestartService' in session.exception: continue diff --git a/tox.ini b/tox.ini index 423d8859..c32ca951 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,4 @@ [flake8] max-line-length = 9999 import-order-style = pep8 +exclude = ./.git,./.vscode,./libraries From c559f2fff6e17419c977e637f53267740e9b99ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Wed, 10 Jul 2019 00:18:40 +0200 Subject: [PATCH 5/6] Strip pluginpath from logged stacktraces --- resources/lib/helper/loghandler.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/resources/lib/helper/loghandler.py b/resources/lib/helper/loghandler.py index 49b71940..5890bb7a 100644 --- a/resources/lib/helper/loghandler.py +++ b/resources/lib/helper/loghandler.py @@ -7,6 +7,7 @@ from __future__ import print_function import os import logging +import traceback import xbmc import xbmcaddon @@ -119,6 +120,22 @@ class MyFormatter(logging.Formatter): return result + def formatException(self, exc_info): + _pluginpath_real = os.path.realpath(__pluginpath__) + res = [] + + for o in traceback.format_exception(*exc_info): + if o.startswith(' File "'): + # If this split can't handle your file names, you should seriously consider renaming your files. + fn = o.split(' File "', 2)[1].split('", line ', 1)[0] + rfn = os.path.realpath(fn) + if rfn.startswith(_pluginpath_real): + o = o.replace(fn, os.path.relpath(rfn, _pluginpath_real)) + + res.append(o) + + return ''.join(res) + def _gen_rel_path(self, record): if record.pathname: record.relpath = os.path.relpath(record.pathname, __pluginpath__) From d36a46901652375232f9093c202e57eef785fae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Wed, 10 Jul 2019 16:37:19 +0200 Subject: [PATCH 6/6] Remove some commented code --- resources/lib/jellyfin/core/configuration.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/lib/jellyfin/core/configuration.py b/resources/lib/jellyfin/core/configuration.py index 0ad9850c..41c0c5c3 100644 --- a/resources/lib/jellyfin/core/configuration.py +++ b/resources/lib/jellyfin/core/configuration.py @@ -48,8 +48,6 @@ class Config(object): def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): LOG.info("Begin app constructor.") - # import traceback - # LOG.debug(''.join(['\n'] + traceback.format_stack())) self.data['app.name'] = name self.data['app.version'] = version