Merge pull request #38 from oddstr13/pr-logging-1

Greatly increase the amount of logging information.
This commit is contained in:
Claus Vium 2019-07-11 07:54:34 +02:00 committed by GitHub
commit e72a124e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 723 additions and 636 deletions

25
.editorconfig Normal file
View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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()
@ -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):
@ -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)
@ -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':

View File

@ -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)

View File

@ -1,16 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import print_function
##################################################################################################
import os
import logging
import traceback
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 +29,7 @@ def config():
logger.addHandler(LogHandler())
logger.setLevel(logging.DEBUG)
def reset():
for handler in logging.getLogger('JELLYFIN').handlers:
@ -59,7 +71,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 +107,10 @@ 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'
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,23 @@ class MyFormatter(logging.Formatter):
self._fmt = format_orig
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__)

View File

@ -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'

View File

@ -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)

View File

@ -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]}

View File

@ -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()
config()

View File

@ -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

View File

@ -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")

View File

@ -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,13 +40,13 @@ 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.")
self.data['app.name'] = name

View File

@ -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 = ("<broadcast>", 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

View File

@ -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

View File

@ -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')

View File

@ -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,

View File

@ -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__":

View File

@ -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:

View File

@ -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

View File

@ -765,7 +765,7 @@ def on_play(data, server):
try:
file = player.getPlayingFile()
except Exception as error:
LOG.error(error)
LOG.exception(error)
return

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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}

View File

@ -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])

View File

@ -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)

View File

@ -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()

View File

@ -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]}

View File

@ -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

View File

@ -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

4
tox.ini Normal file
View File

@ -0,0 +1,4 @@
[flake8]
max-line-length = 9999
import-order-style = pep8
exclude = ./.git,./.vscode,./libraries