mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-12-25 10:16:11 +00:00
Increase amount of logging
This commit is contained in:
parent
c321b266f0
commit
9ae99de8dd
30 changed files with 641 additions and 620 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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':
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -765,7 +765,7 @@ def on_play(data, server):
|
|||
try:
|
||||
file = player.getPlayingFile()
|
||||
except Exception as error:
|
||||
LOG.error(error)
|
||||
LOG.exception(error)
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
11
service.py
11
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
|
||||
|
|
1
tox.ini
1
tox.ini
|
@ -1,3 +1,4 @@
|
|||
[flake8]
|
||||
max-line-length = 9999
|
||||
import-order-style = pep8
|
||||
exclude = ./.git,./.vscode,./libraries
|
||||
|
|
Loading…
Reference in a new issue