Tool black: auto-format Python code

This commit is contained in:
Odd Stråbø 2024-06-10 09:19:47 +00:00
parent e4d8084c25
commit 7763762212
54 changed files with 6545 additions and 4723 deletions

View File

@ -35,46 +35,48 @@ def create_addon_xml(config: dict, source: str, py_version: str) -> None:
Create addon.xml from template file
"""
# Load template file
with open('{}/.build/template.xml'.format(source), 'r') as f:
with open("{}/.build/template.xml".format(source), "r") as f:
tree = ET.parse(f)
root = tree.getroot()
# Populate dependencies in template
dependencies = config['dependencies'].get(py_version)
dependencies = config["dependencies"].get(py_version)
for dep in dependencies:
ET.SubElement(root.find('requires'), 'import', attrib=dep)
ET.SubElement(root.find("requires"), "import", attrib=dep)
# Populate version string
addon_version = config.get('version')
root.attrib['version'] = '{}+{}'.format(addon_version, py_version)
addon_version = config.get("version")
root.attrib["version"] = "{}+{}".format(addon_version, py_version)
# Populate Changelog
date = datetime.today().strftime('%Y-%m-%d')
changelog = config.get('changelog')
for section in root.findall('extension'):
news = section.findall('news')
date = datetime.today().strftime("%Y-%m-%d")
changelog = config.get("changelog")
for section in root.findall("extension"):
news = section.findall("news")
if news:
news[0].text = 'v{} ({}):\n{}'.format(addon_version, date, changelog)
news[0].text = "v{} ({}):\n{}".format(addon_version, date, changelog)
# Format xml tree
indent(root)
# Write addon.xml
tree.write('{}/addon.xml'.format(source), encoding='utf-8', xml_declaration=True)
tree.write("{}/addon.xml".format(source), encoding="utf-8", xml_declaration=True)
def zip_files(py_version: str, source: str, target: str, dev: bool) -> None:
"""
Create installable addon zip archive
"""
archive_name = 'plugin.video.jellyfin+{}.zip'.format(py_version)
archive_name = "plugin.video.jellyfin+{}.zip".format(py_version)
with zipfile.ZipFile('{}/{}'.format(target, archive_name), 'w') as z:
with zipfile.ZipFile("{}/{}".format(target, archive_name), "w") as z:
for root, dirs, files in os.walk(args.source):
for filename in filter(file_filter, files):
file_path = os.path.join(root, filename)
if dev or folder_filter(file_path):
relative_path = os.path.join('plugin.video.jellyfin', os.path.relpath(file_path, source))
relative_path = os.path.join(
"plugin.video.jellyfin", os.path.relpath(file_path, source)
)
z.write(file_path, relative_path)
@ -83,10 +85,12 @@ def file_filter(file_name: str) -> bool:
True if file_name is meant to be included
"""
return (
not (file_name.startswith('plugin.video.jellyfin') and file_name.endswith('.zip'))
and not file_name.endswith('.pyo')
and not file_name.endswith('.pyc')
and not file_name.endswith('.pyd')
not (
file_name.startswith("plugin.video.jellyfin") and file_name.endswith(".zip")
)
and not file_name.endswith(".pyo")
and not file_name.endswith(".pyc")
and not file_name.endswith(".pyd")
)
@ -95,13 +99,13 @@ def folder_filter(folder_name: str) -> bool:
True if folder_name is meant to be included
"""
filters = [
'.ci',
'.git',
'.github',
'.build',
'.mypy_cache',
'.pytest_cache',
'__pycache__',
".ci",
".git",
".github",
".build",
".mypy_cache",
".pytest_cache",
"__pycache__",
]
for f in filters:
if f in folder_name.split(os.path.sep):
@ -110,33 +114,22 @@ def folder_filter(folder_name: str) -> bool:
return True
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build flags:")
parser.add_argument("--version", type=str, choices=("py2", "py3"), default="py3")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Build flags:')
parser.add_argument(
'--version',
type=str,
choices=('py2', 'py3'),
default='py3')
parser.add_argument("--source", type=Path, default=Path(__file__).absolute().parent)
parser.add_argument(
'--source',
type=Path,
default=Path(__file__).absolute().parent)
parser.add_argument("--target", type=Path, default=Path(__file__).absolute().parent)
parser.add_argument(
'--target',
type=Path,
default=Path(__file__).absolute().parent)
parser.add_argument('--dev', dest='dev', action='store_true')
parser.add_argument("--dev", dest="dev", action="store_true")
parser.set_defaults(dev=False)
args = parser.parse_args()
# Load config file
config_path = os.path.join(args.source, 'release.yaml')
with open(config_path, 'r') as fh:
config_path = os.path.join(args.source, "release.yaml")
with open(config_path, "r") as fh:
release_config = yaml.safe_load(fh)
create_addon_xml(release_config, args.source, args.version)

View File

@ -18,73 +18,69 @@ LOG = LazyLogger(__name__)
def get_addon_name():
''' Used for logging.
'''
return xbmcaddon.Addon(addon_id()).getAddonInfo('name').upper()
"""Used for logging."""
return xbmcaddon.Addon(addon_id()).getAddonInfo("name").upper()
def get_version():
return xbmcaddon.Addon(addon_id()).getAddonInfo('version')
return xbmcaddon.Addon(addon_id()).getAddonInfo("version")
def get_platform():
if xbmc.getCondVisibility('system.platform.osx'):
if xbmc.getCondVisibility("system.platform.osx"):
return "OSX"
elif xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)'):
elif xbmc.getCondVisibility("System.HasAddon(service.coreelec.settings)"):
return "CoreElec"
elif xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'):
elif xbmc.getCondVisibility("System.HasAddon(service.libreelec.settings)"):
return "LibreElec"
elif xbmc.getCondVisibility('System.HasAddon(service.osmc.settings)'):
elif xbmc.getCondVisibility("System.HasAddon(service.osmc.settings)"):
return "OSMC"
elif xbmc.getCondVisibility('system.platform.atv2'):
elif xbmc.getCondVisibility("system.platform.atv2"):
return "ATV2"
elif xbmc.getCondVisibility('system.platform.ios'):
elif xbmc.getCondVisibility("system.platform.ios"):
return "iOS"
elif xbmc.getCondVisibility('system.platform.windows'):
elif xbmc.getCondVisibility("system.platform.windows"):
return "Windows"
elif xbmc.getCondVisibility('system.platform.android'):
elif xbmc.getCondVisibility("system.platform.android"):
return "Linux/Android"
elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'):
elif xbmc.getCondVisibility("system.platform.linux.raspberrypi"):
return "Linux/RPi"
elif xbmc.getCondVisibility('system.platform.linux'):
elif xbmc.getCondVisibility("system.platform.linux"):
return "Linux"
else:
return "Unknown"
def get_device_name():
''' Detect the device name. If deviceNameOpt, then
use the device name in the add-on settings.
Otherwise, fallback to the Kodi device name.
'''
if not settings('deviceNameOpt.bool'):
device_name = xbmc.getInfoLabel('System.FriendlyName')
"""Detect the device name. If deviceNameOpt, then
use the device name in the add-on settings.
Otherwise, fallback to the Kodi device name.
"""
if not settings("deviceNameOpt.bool"):
device_name = xbmc.getInfoLabel("System.FriendlyName")
else:
device_name = settings('deviceName')
device_name = device_name.replace("\"", "_")
device_name = settings("deviceName")
device_name = device_name.replace('"', "_")
device_name = device_name.replace("/", "_")
return device_name
def get_device_id(reset=False):
"""Return the device_id if already loaded.
It will load from jellyfin_guid file. If it's a fresh
setup, it will generate a new GUID to uniquely
identify the setup for all users.
''' Return the device_id if already loaded.
It will load from jellyfin_guid file. If it's a fresh
setup, it will generate a new GUID to uniquely
identify the setup for all users.
window prop: jellyfin_deviceId
'''
client_id = window('jellyfin_deviceId')
window prop: jellyfin_deviceId
"""
client_id = window("jellyfin_deviceId")
if client_id:
return client_id
directory = translate_path('special://profile/addon_data/plugin.video.jellyfin/')
directory = translate_path("special://profile/addon_data/plugin.video.jellyfin/")
if not xbmcvfs.exists(directory):
xbmcvfs.mkdir(directory)
@ -97,27 +93,27 @@ def get_device_id(reset=False):
LOG.debug("Generating a new GUID.")
client_id = str(create_id())
file_guid = xbmcvfs.File(jellyfin_guid, 'w')
file_guid = xbmcvfs.File(jellyfin_guid, "w")
file_guid.write(client_id)
file_guid.close()
LOG.debug("DeviceId loaded: %s", client_id)
window('jellyfin_deviceId', value=client_id)
window("jellyfin_deviceId", value=client_id)
return client_id
def reset_device_id():
window('jellyfin_deviceId', clear=True)
window("jellyfin_deviceId", clear=True)
get_device_id(True)
dialog("ok", "{jellyfin}", translate(33033))
xbmc.executebuiltin('RestartApp')
xbmc.executebuiltin("RestartApp")
def get_info():
return {
'DeviceName': get_device_name(),
'Version': get_version(),
'DeviceId': get_device_id()
"DeviceName": get_device_name(),
"Version": get_version(),
"DeviceId": get_device_id(),
}

View File

@ -16,7 +16,7 @@ from .helper.exceptions import HTTPException
##################################################################################################
LOG = LazyLogger(__name__)
XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo('path'), "default", "1080i")
XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo("path"), "default", "1080i")
##################################################################################################
@ -27,36 +27,37 @@ class Connect(object):
self.info = client.get_info()
def register(self, server_id=None, options={}):
''' Login into server. If server is None, then it will show the proper prompts to login, etc.
If a server id is specified then only a login dialog will be shown for that server.
'''
LOG.info("--[ server/%s ]", server_id or 'default')
"""Login into server. If server is None, then it will show the proper prompts to login, etc.
If a server id is specified then only a login dialog will be shown for that server.
"""
LOG.info("--[ server/%s ]", server_id or "default")
credentials = dict(get_credentials())
servers = credentials['Servers']
servers = credentials["Servers"]
if server_id is None and credentials['Servers']:
credentials['Servers'] = [credentials['Servers'][0]]
if server_id is None and credentials["Servers"]:
credentials["Servers"] = [credentials["Servers"][0]]
elif credentials['Servers']:
elif credentials["Servers"]:
for server in credentials['Servers']:
for server in credentials["Servers"]:
if server['Id'] == server_id:
credentials['Servers'] = [server]
if server["Id"] == server_id:
credentials["Servers"] = [server]
server_select = server_id is None and not settings('SyncInstallRunDone.bool')
new_credentials = self.register_client(credentials, options, server_id, server_select)
server_select = server_id is None and not settings("SyncInstallRunDone.bool")
new_credentials = self.register_client(
credentials, options, server_id, server_select
)
for server in servers:
if server['Id'] == new_credentials['Servers'][0]['Id']:
server = new_credentials['Servers'][0]
if server["Id"] == new_credentials["Servers"][0]["Id"]:
server = new_credentials["Servers"][0]
break
else:
servers = new_credentials['Servers']
servers = new_credentials["Servers"]
credentials['Servers'] = servers
credentials["Servers"] = servers
save_credentials(credentials)
try:
@ -65,36 +66,39 @@ class Connect(object):
LOG.error(error)
def get_ssl(self):
''' Returns boolean value.
True: verify connection.
'''
return settings('sslverify.bool')
"""Returns boolean value.
True: verify connection.
"""
return settings("sslverify.bool")
def get_client(self, server_id=None):
''' Get Jellyfin client.
'''
"""Get Jellyfin client."""
client = Jellyfin(server_id)
client.config.app("Kodi", self.info['Version'], self.info['DeviceName'], self.info['DeviceId'])
client.config.data['http.user_agent'] = "Jellyfin-Kodi/%s" % self.info['Version']
client.config.data['auth.ssl'] = self.get_ssl()
client.config.app(
"Kodi", self.info["Version"], self.info["DeviceName"], self.info["DeviceId"]
)
client.config.data["http.user_agent"] = (
"Jellyfin-Kodi/%s" % self.info["Version"]
)
client.config.data["auth.ssl"] = self.get_ssl()
return client
def register_client(self, credentials=None, options=None, server_id=None, server_selection=False):
def register_client(
self, credentials=None, options=None, server_id=None, server_selection=False
):
client = self.get_client(server_id)
self.client = client
self.connect_manager = client.auth
if server_id is None:
client.config.data['app.default'] = True
client.config.data["app.default"] = True
try:
state = client.authenticate(credentials or {}, options or {})
if state['State'] == CONNECTION_STATE['SignedIn']:
if state["State"] == CONNECTION_STATE["SignedIn"]:
client.callback_ws = event
if server_id is None: # Only assign for default server
@ -102,66 +106,77 @@ class Connect(object):
client.callback = event
self.get_user(client)
settings('serverName', client.config.data['auth.server-name'])
settings('server', client.config.data['auth.server'])
settings("serverName", client.config.data["auth.server-name"])
settings("server", client.config.data["auth.server"])
event('ServerOnline', {'ServerId': server_id})
event('LoadServer', {'ServerId': server_id})
event("ServerOnline", {"ServerId": server_id})
event("LoadServer", {"ServerId": server_id})
return state['Credentials']
return state["Credentials"]
elif (server_selection or state['State'] == CONNECTION_STATE['ServerSelection'] or state['State'] == CONNECTION_STATE['Unavailable'] and not settings('SyncInstallRunDone.bool')):
state['Credentials']['Servers'] = [self.select_servers(state)]
elif (
server_selection
or state["State"] == CONNECTION_STATE["ServerSelection"]
or state["State"] == CONNECTION_STATE["Unavailable"]
and not settings("SyncInstallRunDone.bool")
):
state["Credentials"]["Servers"] = [self.select_servers(state)]
elif state['State'] == CONNECTION_STATE['ServerSignIn']:
if 'ExchangeToken' not in state['Servers'][0]:
elif state["State"] == CONNECTION_STATE["ServerSignIn"]:
if "ExchangeToken" not in state["Servers"][0]:
self.login()
elif state['State'] == CONNECTION_STATE['Unavailable'] and state.get('Status_Code', 0) == 401:
elif (
state["State"] == CONNECTION_STATE["Unavailable"]
and state.get("Status_Code", 0) == 401
):
# If the saved credentials don't work, restart the addon to force the password dialog to open
window('jellyfin.restart', clear=True)
window("jellyfin.restart", clear=True)
elif state['State'] == CONNECTION_STATE['Unavailable']:
raise HTTPException('ServerUnreachable', {})
elif state["State"] == CONNECTION_STATE["Unavailable"]:
raise HTTPException("ServerUnreachable", {})
return self.register_client(state['Credentials'], options, server_id, False)
return self.register_client(state["Credentials"], options, server_id, False)
except RuntimeError as error:
LOG.exception(error)
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id())
xbmc.executebuiltin("Addon.OpenSettings(%s)" % addon_id())
raise Exception('User sign in interrupted')
raise Exception("User sign in interrupted")
except HTTPException as error:
if error.status == 'ServerUnreachable':
event('ServerUnreachable', {'ServerId': server_id})
if error.status == "ServerUnreachable":
event("ServerUnreachable", {"ServerId": server_id})
return client.get_credentials()
def get_user(self, client):
''' Save user info.
'''
"""Save user info."""
self.user = client.jellyfin.get_user()
settings('username', self.user['Name'])
settings("username", self.user["Name"])
if 'PrimaryImageTag' in self.user:
server_address = client.auth.get_server_info(client.auth.server_id)['address']
window('JellyfinUserImage', api.API(self.user, server_address).get_user_artwork(self.user['Id']))
if "PrimaryImageTag" in self.user:
server_address = client.auth.get_server_info(client.auth.server_id)[
"address"
]
window(
"JellyfinUserImage",
api.API(self.user, server_address).get_user_artwork(self.user["Id"]),
)
def select_servers(self, state=None):
state = state or self.connect_manager.connect({'enableAutoLogin': False})
state = state or self.connect_manager.connect({"enableAutoLogin": False})
user = {}
dialog = ServerConnect("script-jellyfin-connect-server.xml", *XML_PATH)
dialog.set_args(
connect_manager=self.connect_manager,
username=user.get('DisplayName', ""),
user_image=user.get('ImageUrl'),
servers=self.connect_manager.get_available_servers()
username=user.get("DisplayName", ""),
user_image=user.get("ImageUrl"),
servers=self.connect_manager.get_available_servers(),
)
dialog.doModal()
@ -182,9 +197,7 @@ class Connect(object):
return self.select_servers()
def setup_manual_server(self):
''' Setup manual servers
'''
"""Setup manual servers"""
client = self.get_client()
client.set_credentials(get_credentials())
manager = client.auth
@ -198,11 +211,9 @@ class Connect(object):
save_credentials(credentials)
def manual_server(self, manager=None):
''' Return server or raise error.
'''
"""Return server or raise error."""
dialog = ServerManual("script-jellyfin-connect-server-manual.xml", *XML_PATH)
dialog.set_args(**{'connect_manager': manager or self.connect_manager})
dialog.set_args(**{"connect_manager": manager or self.connect_manager})
dialog.doModal()
if dialog.is_connected():
@ -213,7 +224,9 @@ class Connect(object):
def login(self):
users = self.connect_manager.get_public_users()
server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address']
server = self.connect_manager.get_server_info(self.connect_manager.server_id)[
"address"
]
if not users:
try:
@ -222,14 +235,14 @@ class Connect(object):
raise RuntimeError("No user selected")
dialog = UsersConnect("script-jellyfin-connect-users.xml", *XML_PATH)
dialog.set_args(**{'server': server, 'users': users})
dialog.set_args(**{"server": server, "users": users})
dialog.doModal()
if dialog.is_user_selected():
user = dialog.get_user()
username = user['Name']
username = user["Name"]
if user['HasPassword']:
if user["HasPassword"]:
LOG.debug("User has password, present manual login")
try:
return self.login_manual(username)
@ -249,14 +262,12 @@ class Connect(object):
return self.login()
def setup_login_manual(self):
''' Setup manual login by itself for default server.
'''
"""Setup manual login by itself for default server."""
client = self.get_client()
client.set_credentials(get_credentials())
manager = client.auth
username = settings('username')
username = settings("username")
try:
self.login_manual(user=username, manager=manager)
except RuntimeError:
@ -266,11 +277,14 @@ class Connect(object):
save_credentials(credentials)
def login_manual(self, user=None, manager=None):
''' Return manual login user authenticated or raise error.
'''
"""Return manual login user authenticated or raise error."""
dialog = LoginManual("script-jellyfin-connect-login-manual.xml", *XML_PATH)
dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}})
dialog.set_args(
**{
"connect_manager": manager or self.connect_manager,
"username": user or {},
}
)
dialog.doModal()
if dialog.is_logged_in():
@ -279,15 +293,13 @@ class Connect(object):
raise RuntimeError("User is not authenticated")
def remove_server(self, server_id):
''' Stop client and remove server.
'''
"""Stop client and remove server."""
Jellyfin(server_id).close()
credentials = get_credentials()
for server in credentials['Servers']:
if server['Id'] == server_id:
credentials['Servers'].remove(server)
for server in credentials["Servers"]:
if server["Id"] == server_id:
credentials["Servers"].remove(server)
break

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
#################################################################################################
import datetime
@ -28,51 +29,56 @@ ADDON_DATA = translate_path("special://profile/addon_data/plugin.video.jellyfin/
class Database(object):
"""This should be called like a context.
i.e. with Database('jellyfin') as db:
db.cursor
db.conn.commit()
"""
''' This should be called like a context.
i.e. with Database('jellyfin') as db:
db.cursor
db.conn.commit()
'''
timeout = 120
discovered = False
discovered_file = None
def __init__(self, db_file=None, commit_close=True):
''' file: jellyfin, texture, music, video, :memory: or path to file
'''
"""file: jellyfin, texture, music, video, :memory: or path to file"""
self.db_file = db_file or "video"
self.commit_close = commit_close
def __enter__(self):
''' Open the connection and return the Database class.
This is to allow for the cursor, conn and others to be accessible.
'''
"""Open the connection and return the Database class.
This is to allow for the cursor, conn and others to be accessible.
"""
self.path = self._sql(self.db_file)
self.conn = sqlite3.connect(self.path, timeout=self.timeout)
self.cursor = self.conn.cursor()
if self.db_file in ('video', 'music', 'texture', 'jellyfin'):
self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi
if self.db_file in ("video", "music", "texture", "jellyfin"):
self.conn.execute(
"PRAGMA journal_mode=WAL"
) # to avoid writing conflict with kodi
LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn))
if not window('jellyfin_db_check.bool') and self.db_file == 'jellyfin':
if not window("jellyfin_db_check.bool") and self.db_file == "jellyfin":
window('jellyfin_db_check.bool', True)
window("jellyfin_db_check.bool", True)
jellyfin_tables(self.cursor)
self.conn.commit()
# Migration for #162
if self.db_file == 'music':
query = self.conn.execute('SELECT * FROM path WHERE strPath LIKE "%/emby/%"')
if self.db_file == "music":
query = self.conn.execute(
'SELECT * FROM path WHERE strPath LIKE "%/emby/%"'
)
contents = query.fetchall()
if contents:
for item in contents:
new_path = item[1].replace('/emby/', '/')
self.conn.execute('UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(new_path, item[0]))
new_path = item[1].replace("/emby/", "/")
self.conn.execute(
'UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(
new_path, item[0]
)
)
return self
@ -97,68 +103,68 @@ 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.
"""
if database == "video":
''' Use UpdateLibrary(video) to update the date modified
on the database file used by Kodi.
'''
if database == 'video':
xbmc.executebuiltin('UpdateLibrary(video)')
xbmc.executebuiltin("UpdateLibrary(video)")
xbmc.sleep(200)
databases = translate_path("special://database/")
types = {
'video': "MyVideos",
'music': "MyMusic",
'texture': "Textures"
}
types = {"video": "MyVideos", "music": "MyMusic", "texture": "Textures"}
database = types[database]
dirs, files = xbmcvfs.listdir(databases)
target = {'db_file': '', 'version': 0}
target = {"db_file": "", "version": 0}
for db_file in reversed(files):
if (db_file.startswith(database)
and not db_file.endswith('-wal')
and not db_file.endswith('-shm')
and not db_file.endswith('db-journal')):
if (
db_file.startswith(database)
and not db_file.endswith("-wal")
and not db_file.endswith("-shm")
and not db_file.endswith("db-journal")
):
version_string = re.search('{}(.*).db'.format(database), db_file)
version_string = re.search("{}(.*).db".format(database), db_file)
version = int(version_string.group(1))
if version > target['version']:
target['db_file'] = db_file
target['version'] = version
if version > target["version"]:
target["db_file"] = db_file
target["version"] = version
LOG.debug("Discovered database: %s", target)
self.discovered_file = target['db_file']
self.discovered_file = target["db_file"]
return translate_path("special://database/%s" % target['db_file'])
return translate_path("special://database/%s" % target["db_file"])
def _sql(self, db_file):
''' Get the database path based on the file objects/obj_map.json
Compatible check, in the event multiple db version are supported with the same Kodi version.
Discover by file as a last resort.
'''
"""Get the database path based on the file objects/obj_map.json
Compatible check, in the event multiple db version are supported with the same Kodi version.
Discover by file as a last resort.
"""
databases = obj.Objects().objects
if db_file not in ('video', 'music', 'texture') or databases.get('database_set%s' % db_file):
if db_file not in ("video", "music", "texture") or databases.get(
"database_set%s" % db_file
):
return self._get_database(databases[db_file], True)
discovered = self._discover_database(db_file) if not databases.get('database_set%s' % db_file) else None
discovered = (
self._discover_database(db_file)
if not databases.get("database_set%s" % db_file)
else None
)
databases[db_file] = discovered
self.discovered = True
databases['database_set%s' % db_file] = True
databases["database_set%s" % db_file] = True
LOG.info("Database locked in: %s", databases[db_file])
return databases[db_file]
def __exit__(self, exc_type, exc_val, exc_tb):
''' Close the connection and cursor.
'''
"""Close the connection and cursor."""
changes = self.conn.total_changes
if exc_type is not None: # errors raised
@ -175,41 +181,43 @@ class Database(object):
def jellyfin_tables(cursor):
''' Create the tables for the jellyfin database.
jellyfin, view, version
'''
"""Create the tables for the jellyfin database.
jellyfin, view, version
"""
cursor.execute(
"""CREATE TABLE IF NOT EXISTS jellyfin(
jellyfin_id TEXT UNIQUE, media_folder TEXT, jellyfin_type TEXT, media_type TEXT,
kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER,
checksum INTEGER, jellyfin_parent_id TEXT)""")
checksum INTEGER, jellyfin_parent_id TEXT)"""
)
cursor.execute(
"""CREATE TABLE IF NOT EXISTS view(
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""")
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)"""
)
cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
columns = cursor.execute("SELECT * FROM jellyfin")
if 'jellyfin_parent_id' not in [description[0] for description in columns.description]:
if "jellyfin_parent_id" not in [
description[0] for description in columns.description
]:
LOG.debug("Add missing column jellyfin_parent_id")
cursor.execute("ALTER TABLE jellyfin ADD COLUMN jellyfin_parent_id 'TEXT'")
def reset():
''' Reset both the jellyfin database and the kodi database.
'''
"""Reset both the jellyfin database and the kodi database."""
from ..views import Views
views = Views()
if not dialog("yesno", "{jellyfin}", translate(33074)):
return
window('jellyfin_should_stop.bool', True)
window("jellyfin_should_stop.bool", True)
count = 10
while window('jellyfin_sync.bool'):
while window("jellyfin_sync.bool"):
LOG.info("Sync is running...")
count -= 1
@ -239,12 +247,12 @@ def reset():
if xbmcvfs.exists(os.path.join(ADDON_DATA, "sync.json")):
xbmcvfs.delete(os.path.join(ADDON_DATA, "sync.json"))
settings('enableMusic.bool', False)
settings('MinimumSetup', "")
settings('MusicRescan.bool', False)
settings('SyncInstallRunDone.bool', False)
settings("enableMusic.bool", False)
settings("MinimumSetup", "")
settings("MusicRescan.bool", False)
settings("SyncInstallRunDone.bool", False)
dialog("ok", "{jellyfin}", translate(33088))
xbmc.executebuiltin('RestartApp')
xbmc.executebuiltin("RestartApp")
def reset_kodi():
@ -256,18 +264,20 @@ def reset_kodi():
name = table[0]
# These tables are populated by Kodi and we shouldn't wipe them
if name not in ['version', 'videoversiontype']:
if name not in ["version", "videoversiontype"]:
videodb.cursor.execute("DELETE FROM " + name)
if settings('enableMusic.bool') or dialog("yesno", "{jellyfin}", translate(33162)):
if settings("enableMusic.bool") or dialog("yesno", "{jellyfin}", translate(33162)):
with Database('music') as musicdb:
musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
with Database("music") as musicdb:
musicdb.cursor.execute(
"SELECT tbl_name FROM sqlite_master WHERE type='table'"
)
for table in musicdb.cursor.fetchall():
name = table[0]
if name != 'version':
if name != "version":
musicdb.cursor.execute("DELETE FROM " + name)
LOG.info("[ reset kodi ]")
@ -275,13 +285,15 @@ def reset_kodi():
def reset_jellyfin():
with Database('jellyfin') as jellyfindb:
jellyfindb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
with Database("jellyfin") as jellyfindb:
jellyfindb.cursor.execute(
"SELECT tbl_name FROM sqlite_master WHERE type='table'"
)
for table in jellyfindb.cursor.fetchall():
name = table[0]
if name not in ('version', 'view'):
if name not in ("version", "view"):
jellyfindb.cursor.execute("DELETE FROM " + name)
jellyfindb.cursor.execute("DROP table IF EXISTS jellyfin")
@ -292,10 +304,8 @@ def reset_jellyfin():
def reset_artwork():
''' Remove all existing texture.
'''
thumbnails = translate_path('special://thumbnails/')
"""Remove all existing texture."""
thumbnails = translate_path("special://thumbnails/")
if xbmcvfs.exists(thumbnails):
dirs, ignore = xbmcvfs.listdir(thumbnails)
@ -307,13 +317,13 @@ def reset_artwork():
LOG.debug("DELETE thumbnail %s", thumb)
xbmcvfs.delete(os.path.join(thumbnails, directory, thumb))
with Database('texture') as texdb:
with Database("texture") as texdb:
texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
for table in texdb.cursor.fetchall():
name = table[0]
if name != 'version':
if name != "version":
texdb.cursor.execute("DELETE FROM " + name)
LOG.info("[ reset artwork ]")
@ -327,18 +337,18 @@ def get_sync():
xbmcvfs.mkdirs(ADDON_DATA)
try:
with open(os.path.join(ADDON_DATA, 'sync.json'), 'rb') as infile:
with open(os.path.join(ADDON_DATA, "sync.json"), "rb") as infile:
sync = json.load(infile)
except Exception:
sync = {}
sync['Libraries'] = sync.get('Libraries', [])
sync['RestorePoint'] = sync.get('RestorePoint', {})
sync['Whitelist'] = list(set(sync.get('Whitelist', [])))
sync['SortedViews'] = sync.get('SortedViews', [])
sync["Libraries"] = sync.get("Libraries", [])
sync["RestorePoint"] = sync.get("RestorePoint", {})
sync["Whitelist"] = list(set(sync.get("Whitelist", [])))
sync["SortedViews"] = sync.get("SortedViews", [])
# Temporary cleanup from #494/#511, remove in a future version
sync['Libraries'] = [lib_id for lib_id in sync['Libraries'] if ',' not in lib_id]
sync["Libraries"] = [lib_id for lib_id in sync["Libraries"] if "," not in lib_id]
return sync
@ -348,12 +358,12 @@ def save_sync(sync):
if not xbmcvfs.exists(ADDON_DATA):
xbmcvfs.mkdirs(ADDON_DATA)
sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
sync["Date"] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
with open(os.path.join(ADDON_DATA, 'sync.json'), 'wb') as outfile:
with open(os.path.join(ADDON_DATA, "sync.json"), "wb") as outfile:
data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False)
if isinstance(data, text_type):
data = data.encode('utf-8')
data = data.encode("utf-8")
outfile.write(data)
@ -365,30 +375,30 @@ def get_credentials():
xbmcvfs.mkdirs(ADDON_DATA)
try:
with open(os.path.join(ADDON_DATA, 'data.json'), 'rb') as infile:
with open(os.path.join(ADDON_DATA, "data.json"), "rb") as infile:
credentials = json.load(infile)
except IOError:
credentials = {}
credentials['Servers'] = credentials.get('Servers', [])
credentials["Servers"] = credentials.get("Servers", [])
# Migration for #145
# TODO: CLEANUP for 1.0.0 release
for server in credentials['Servers']:
for server in credentials["Servers"]:
# Functionality removed in #60
if 'RemoteAddress' in server:
del server['RemoteAddress']
if 'ManualAddress' in server:
server['address'] = server['ManualAddress']
del server['ManualAddress']
if "RemoteAddress" in server:
del server["RemoteAddress"]
if "ManualAddress" in server:
server["address"] = server["ManualAddress"]
del server["ManualAddress"]
# If manual is present, local should always be here, but better to be safe
if 'LocalAddress' in server:
del server['LocalAddress']
elif 'LocalAddress' in server:
server['address'] = server['LocalAddress']
del server['LocalAddress']
if 'LastConnectionMode' in server:
del server['LastConnectionMode']
if "LocalAddress" in server:
del server["LocalAddress"]
elif "LocalAddress" in server:
server["address"] = server["LocalAddress"]
del server["LocalAddress"]
if "LastConnectionMode" in server:
del server["LastConnectionMode"]
return credentials
@ -399,21 +409,21 @@ def save_credentials(credentials):
if not xbmcvfs.exists(ADDON_DATA):
xbmcvfs.mkdirs(ADDON_DATA)
try:
with open(os.path.join(ADDON_DATA, 'data.json'), 'wb') as outfile:
with open(os.path.join(ADDON_DATA, "data.json"), "wb") as outfile:
data = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False)
if isinstance(data, text_type):
data = data.encode('utf-8')
data = data.encode("utf-8")
outfile.write(data)
except Exception:
LOG.exception("Failed to save credentials:")
def get_item(kodi_id, media):
''' Get jellyfin item based on kodi id and media.
'''
with Database('jellyfin') as jellyfindb:
item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(kodi_id, media)
"""Get jellyfin item based on kodi id and media."""
with Database("jellyfin") as jellyfindb:
item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(
kodi_id, media
)
if not item:
LOG.debug("Not an jellyfin item")

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
#################################################################################################
from . import queries as QU
@ -14,7 +15,7 @@ LOG = LazyLogger(__name__)
##################################################################################################
class JellyfinDatabase():
class JellyfinDatabase:
def __init__(self, cursor):
self.cursor = cursor
@ -32,9 +33,7 @@ class JellyfinDatabase():
self.cursor.execute(QU.update_reference, args)
def update_parent_id(self, *args):
''' Parent_id is the parent Kodi id.
'''
"""Parent_id is the parent Kodi id."""
self.cursor.execute(QU.update_parent, args)
def get_item_id_by_parent_id(self, *args):
@ -160,8 +159,8 @@ class JellyfinDatabase():
return self.cursor.fetchone()
def add_version(self, *args):
'''
"""
We only ever want one value here, so erase the existing contents first
'''
"""
self.cursor.execute(QU.delete_version)
self.cursor.execute(QU.add_version, args)

View File

@ -94,16 +94,126 @@ INSERT OR REPLACE INTO jellyfin(jellyfin_id, kodi_id, kodi_fileid, kodi_pat
media_type, parent_id, checksum, media_folder, jellyfin_parent_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None]
add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, None, None]
add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None]
add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", None, "{JellyfinParentId}"]
add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
add_reference_movie_obj = [
"{Id}",
"{MovieId}",
"{FileId}",
"{PathId}",
"Movie",
"movie",
None,
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_reference_boxset_obj = [
"{Id}",
"{SetId}",
None,
None,
"BoxSet",
"set",
None,
"{Checksum}",
None,
None,
]
add_reference_tvshow_obj = [
"{Id}",
"{ShowId}",
None,
"{PathId}",
"Series",
"tvshow",
None,
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_reference_season_obj = [
"{Id}",
"{SeasonId}",
None,
None,
"Season",
"season",
"{ShowId}",
None,
None,
None,
]
add_reference_pool_obj = [
"{SeriesId}",
"{ShowId}",
None,
"{PathId}",
"Series",
"tvshow",
None,
"{Checksum}",
"{LibraryId}",
None,
]
add_reference_episode_obj = [
"{Id}",
"{EpisodeId}",
"{FileId}",
"{PathId}",
"Episode",
"episode",
"{SeasonId}",
"{Checksum}",
None,
"{JellyfinParentId}",
]
add_reference_mvideo_obj = [
"{Id}",
"{MvideoId}",
"{FileId}",
"{PathId}",
"MusicVideo",
"musicvideo",
None,
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_reference_artist_obj = [
"{Id}",
"{ArtistId}",
None,
None,
"{ArtistType}",
"artist",
None,
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_reference_album_obj = [
"{Id}",
"{AlbumId}",
None,
None,
"MusicAlbum",
"album",
None,
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_reference_song_obj = [
"{Id}",
"{SongId}",
None,
"{PathId}",
"Audio",
"song",
"{AlbumId}",
"{Checksum}",
"{LibraryId}",
"{JellyfinParentId}",
]
add_view = """
INSERT OR REPLACE INTO view(view_id, view_name, media_type)
VALUES (?, ?, ?)

View File

@ -47,8 +47,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
def onInit(self):
if window('JellyfinUserImage'):
self.getControl(USER_IMAGE).setImage(window('JellyfinUserImage'))
if window("JellyfinUserImage"):
self.getControl(USER_IMAGE).setImage(window("JellyfinUserImage"))
LOG.info("options: %s", self._options)
self.list_ = self.getControl(LIST)
@ -63,21 +63,35 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
if (
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
and self.getFocusId() == LIST
):
option = self.list_.getSelectedItem()
self.selected_option = ensure_text(option.getLabel())
LOG.info('option selected: %s', self.selected_option)
LOG.info("option selected: %s", self.selected_option)
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlImage(0, 0, 0, 0,
filename=os.path.join(media, "white.png"),
aspectRatio=0,
colorDiffuse="ff111111")
media = os.path.join(
xbmcaddon.Addon(addon_id()).getAddonInfo("path"),
"resources",
"skins",
"default",
"media",
)
control = xbmcgui.ControlImage(
0,
0,
0,
0,
filename=os.path.join(media, "white.png"),
aspectRatio=0,
colorDiffuse="ff111111",
)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)

View File

@ -18,7 +18,7 @@ SIGN_IN = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {'Invalid': 1, 'Empty': 2}
ERROR = {"Invalid": 1, "Empty": 2}
##################################################################################################
@ -76,7 +76,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
if not user:
# Display error
self._error(ERROR['Empty'], translate('empty_user'))
self._error(ERROR["Empty"], translate("empty_user"))
LOG.error("Username cannot be null")
elif self._login(user, password):
@ -88,7 +88,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
def onAction(self, action):
if self.error == ERROR['Empty'] and self.user_field.getText():
if self.error == ERROR["Empty"] and self.user_field.getText():
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
@ -102,12 +102,12 @@ class LoginManual(xbmcgui.WindowXMLDialog):
textColor="FF00A4DC",
disabledColor="FF888888",
focusTexture="-",
noFocusTexture="-"
noFocusTexture="-",
)
# TODO: Kodi 17 compat removal cleanup
if kodi_version() < 18:
kwargs['isPassword'] = password
kwargs["isPassword"] = password
control = xbmcgui.ControlEdit(0, 0, 0, 0, **kwargs)
@ -126,11 +126,13 @@ class LoginManual(xbmcgui.WindowXMLDialog):
def _login(self, username, password):
server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address']
server = self.connect_manager.get_server_info(self.connect_manager.server_id)[
"address"
]
result = self.connect_manager.login(server, username, password)
if not result:
self._error(ERROR['Invalid'], translate('invalid_auth'))
self._error(ERROR["Invalid"], translate("invalid_auth"))
return False
else:
self._user = result
@ -140,9 +142,9 @@ class LoginManual(xbmcgui.WindowXMLDialog):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true')
self.error_toggle.setVisibleCondition("true")
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('false')
self.error_toggle.setVisibleCondition("false")

View File

@ -64,8 +64,10 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
self.list_ = self.getControl(LIST)
for server in self.servers:
server_type = "wifi" if server.get('ExchangeToken') else "network"
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type))
server_type = "wifi" if server.get("ExchangeToken") else "network"
self.list_.addItem(
self._add_listitem(server["Name"], server["Id"], server_type)
)
if self.user_image is not None:
self.getControl(USER_IMAGE).setImage(self.user_image)
@ -77,8 +79,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def _add_listitem(cls, label, server_id, server_type):
item = xbmcgui.ListItem(label)
item.setProperty('id', server_id)
item.setProperty('server_type', server_type)
item.setProperty("id", server_id)
item.setProperty("server_type", server_type)
return item
@ -87,14 +89,17 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
if (
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
and self.getFocusId() == LIST
):
server = self.list_.getSelectedItem()
selected_id = server.getProperty('id')
LOG.info('Server Id selected: %s', selected_id)
selected_id = server.getProperty("id")
LOG.info("Server Id selected: %s", selected_id)
if self._connect_server(selected_id):
self.message_box.setVisibleCondition('false')
self.message_box.setVisibleCondition("false")
self.close()
def onClick(self, control):
@ -109,19 +114,19 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def _connect_server(self, server_id):
server = self.connect_manager.get_server_info(server_id)
self.message.setLabel("%s %s..." % (translate(30610), server['Name']))
self.message.setLabel("%s %s..." % (translate(30610), server["Name"]))
self.message_box.setVisibleCondition('true')
self.busy.setVisibleCondition('true')
self.message_box.setVisibleCondition("true")
self.busy.setVisibleCondition("true")
result = self.connect_manager.connect_to_server(server)
if result['State'] == CONNECTION_STATE['Unavailable']:
self.busy.setVisibleCondition('false')
if result["State"] == CONNECTION_STATE["Unavailable"]:
self.busy.setVisibleCondition("false")
self.message.setLabel(translate(30609))
return False
else:
xbmc.sleep(1000)
self._selected_server = result['Servers'][0]
self._selected_server = result["Servers"][0]
return True

View File

@ -22,10 +22,7 @@ CONNECT = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {
'Invalid': 1,
'Empty': 2
}
ERROR = {"Invalid": 1, "Empty": 2}
# https://stackoverflow.com/a/17871737/1035647
_IPV6_PATTERN = r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
@ -77,7 +74,7 @@ class ServerManual(xbmcgui.WindowXMLDialog):
if not server:
# Display error
self._error(ERROR['Empty'], translate('empty_server'))
self._error(ERROR["Empty"], translate("empty_server"))
LOG.error("Server cannot be null")
elif self._connect_to_server(server):
@ -89,7 +86,7 @@ class ServerManual(xbmcgui.WindowXMLDialog):
def onAction(self, action):
if self.error == ERROR['Empty'] and self.host_field.getText():
if self.error == ERROR["Empty"] and self.host_field.getText():
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
@ -97,13 +94,18 @@ class ServerManual(xbmcgui.WindowXMLDialog):
def _add_editcontrol(self, x, y, height, width):
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="",
font="font13",
textColor="FF00A4DC",
disabledColor="FF888888",
focusTexture="-",
noFocusTexture="-")
control = xbmcgui.ControlEdit(
0,
0,
0,
0,
label="",
font="font13",
textColor="FF00A4DC",
disabledColor="FF888888",
focusTexture="-",
noFocusTexture="-",
)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
@ -118,25 +120,25 @@ class ServerManual(xbmcgui.WindowXMLDialog):
self._message("%s %s..." % (translate(30610), server))
result = self.connect_manager.connect_to_address(server)
if result['State'] == CONNECTION_STATE['Unavailable']:
if result["State"] == CONNECTION_STATE["Unavailable"]:
self._message(translate(30609))
return False
else:
self._server = result['Servers'][0]
self._server = result["Servers"][0]
return True
def _message(self, message):
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true')
self.error_toggle.setVisibleCondition("true")
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true')
self.error_toggle.setVisibleCondition("true")
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('false')
self.error_toggle.setVisibleCondition("false")

View File

@ -52,17 +52,20 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
self.list_ = self.getControl(LIST)
for user in self.users:
user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user
else self._get_user_artwork(user['Id'], 'Primary'))
self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image))
user_image = (
"items/logindefault.png"
if "PrimaryImageTag" not in user
else self._get_user_artwork(user["Id"], "Primary")
)
self.list_.addItem(self._add_listitem(user["Name"], user["Id"], user_image))
self.setFocus(self.list_)
def _add_listitem(self, label, user_id, user_image):
item = xbmcgui.ListItem(label)
item.setProperty('id', user_id)
item.setArt({'icon': user_image})
item.setProperty("id", user_id)
item.setArt({"icon": user_image})
return item
@ -71,14 +74,17 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
if (
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
and self.getFocusId() == LIST
):
user = self.list_.getSelectedItem()
selected_id = user.getProperty('id')
LOG.info('User Id selected: %s', selected_id)
selected_id = user.getProperty("id")
LOG.info("User Id selected: %s", selected_id)
for user in self.users:
if user['Id'] == selected_id:
if user["Id"] == selected_id:
self._user = user
break
@ -95,4 +101,8 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
def _get_user_artwork(self, user_id, item_type):
# Load user information set by UserClient
return "%s/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type)
return "%s/Users/%s/Images/%s?Format=original" % (
self.server,
user_id,
item_type,
)

View File

@ -25,7 +25,7 @@ LOG = LazyLogger(__name__)
def get_jellyfinserver_url(handler):
if handler.startswith('/'):
if handler.startswith("/"):
handler = handler[1:]
LOG.info("handler starts with /: %s", handler)
@ -38,47 +38,55 @@ def _http(action, url, request=None, server_id=None):
if request is None:
request = {}
request.update({'url': url, 'type': action})
request.update({"url": url, "type": action})
return Jellyfin(server_id).http.request(request)
def _get(handler, params=None, server_id=None):
return _http("GET", get_jellyfinserver_url(handler), {'params': params}, server_id)
return _http("GET", get_jellyfinserver_url(handler), {"params": params}, server_id)
def _post(handler, json=None, params=None, server_id=None):
return _http("POST", get_jellyfinserver_url(handler), {'params': params, 'json': json}, server_id)
return _http(
"POST",
get_jellyfinserver_url(handler),
{"params": params, "json": json},
server_id,
)
def _delete(handler, params=None, server_id=None):
return _http("DELETE", get_jellyfinserver_url(handler), {'params': params}, server_id)
return _http(
"DELETE", get_jellyfinserver_url(handler), {"params": params}, server_id
)
def validate_view(library_id, item_id):
''' This confirms a single item from the library matches the view it belongs to.
Used to detect grouped libraries.
'''
"""This confirms a single item from the library matches the view it belongs to.
Used to detect grouped libraries.
"""
try:
result = _get("Users/{UserId}/Items", {
'ParentId': library_id,
'Recursive': True,
'Ids': item_id
})
result = _get(
"Users/{UserId}/Items",
{"ParentId": library_id, "Recursive": True, "Ids": item_id},
)
except Exception as error:
LOG.exception(error)
return False
return bool(len(result['Items']))
return bool(len(result["Items"]))
def get_single_item(parent_id, media):
return _get("Users/{UserId}/Items", {
'ParentId': parent_id,
'Recursive': True,
'Limit': 1,
'IncludeItemTypes': media
})
return _get(
"Users/{UserId}/Items",
{
"ParentId": parent_id,
"Recursive": True,
"Limit": 1,
"IncludeItemTypes": media,
},
)
def get_movies_by_boxset(boxset_id):
@ -90,13 +98,13 @@ def get_movies_by_boxset(boxset_id):
def get_episode_by_show(show_id):
query = {
'url': "Shows/%s/Episodes" % show_id,
'params': {
'EnableUserData': True,
'EnableImages': True,
'UserId': "{UserId}",
'Fields': api.info()
}
"url": "Shows/%s/Episodes" % show_id,
"params": {
"EnableUserData": True,
"EnableImages": True,
"UserId": "{UserId}",
"Fields": api.info(),
},
}
for items in _get_items(query):
yield items
@ -105,14 +113,14 @@ def get_episode_by_show(show_id):
def get_episode_by_season(show_id, season_id):
query = {
'url': "Shows/%s/Episodes" % show_id,
'params': {
'SeasonId': season_id,
'EnableUserData': True,
'EnableImages': True,
'UserId': "{UserId}",
'Fields': api.info()
}
"url": "Shows/%s/Episodes" % show_id,
"params": {
"SeasonId": season_id,
"EnableUserData": True,
"EnableImages": True,
"UserId": "{UserId}",
"Fields": api.info(),
},
}
for items in _get_items(query):
yield items
@ -123,41 +131,41 @@ def get_item_count(parent_id, item_type=None, params=None):
url = "Users/{UserId}/Items"
query_params = {
'ParentId': parent_id,
'IncludeItemTypes': item_type,
'EnableTotalRecordCount': True,
'LocationTypes': "FileSystem,Remote,Offline",
'Recursive': True,
'Limit': 1
"ParentId": parent_id,
"IncludeItemTypes": item_type,
"EnableTotalRecordCount": True,
"LocationTypes": "FileSystem,Remote,Offline",
"Recursive": True,
"Limit": 1,
}
if params:
query_params['params'].update(params)
query_params["params"].update(params)
result = _get(url, query_params)
return result.get('TotalRecordCount', 1)
return result.get("TotalRecordCount", 1)
def get_items(parent_id, item_type=None, basic=False, params=None):
query = {
'url': "Users/{UserId}/Items",
'params': {
'ParentId': parent_id,
'IncludeItemTypes': item_type,
'SortBy': "SortName",
'SortOrder': "Ascending",
'Fields': api.basic_info() if basic else api.info(),
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'EnableTotalRecordCount': False,
'LocationTypes': "FileSystem,Remote,Offline",
'IsMissing': False,
'Recursive': True
}
"url": "Users/{UserId}/Items",
"params": {
"ParentId": parent_id,
"IncludeItemTypes": item_type,
"SortBy": "SortName",
"SortOrder": "Ascending",
"Fields": api.basic_info() if basic else api.info(),
"CollapseBoxSetItems": False,
"IsVirtualUnaired": False,
"EnableTotalRecordCount": False,
"LocationTypes": "FileSystem,Remote,Offline",
"IsMissing": False,
"Recursive": True,
},
}
if params:
query['params'].update(params)
query["params"].update(params)
for items in _get_items(query):
yield items
@ -166,20 +174,20 @@ def get_items(parent_id, item_type=None, basic=False, params=None):
def get_artists(parent_id=None):
query = {
'url': 'Artists',
'params': {
'UserId': "{UserId}",
'ParentId': parent_id,
'SortBy': "SortName",
'SortOrder': "Ascending",
'Fields': api.music_info(),
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
'EnableTotalRecordCount': False,
'LocationTypes': "FileSystem,Remote,Offline",
'IsMissing': False,
'Recursive': True
}
"url": "Artists",
"params": {
"UserId": "{UserId}",
"ParentId": parent_id,
"SortBy": "SortName",
"SortOrder": "Ascending",
"Fields": api.music_info(),
"CollapseBoxSetItems": False,
"IsVirtualUnaired": False,
"EnableTotalRecordCount": False,
"LocationTypes": "FileSystem,Remote,Offline",
"IsMissing": False,
"Recursive": True,
},
}
for items in _get_items(query):
@ -188,48 +196,49 @@ def get_artists(parent_id=None):
@stop
def _get_items(query, server_id=None):
''' query = {
'url': string,
'params': dict -- opt, include StartIndex to resume
}
'''
items = {
'Items': [],
'TotalRecordCount': 0,
'RestorePoint': {}
"""query = {
'url': string,
'params': dict -- opt, include StartIndex to resume
}
"""
items = {"Items": [], "TotalRecordCount": 0, "RestorePoint": {}}
limit = min(int(settings('limitIndex') or 50), 50)
dthreads = int(settings('limitThreads') or 3)
limit = min(int(settings("limitIndex") or 50), 50)
dthreads = int(settings("limitThreads") or 3)
url = query['url']
query.setdefault('params', {})
params = query['params']
url = query["url"]
query.setdefault("params", {})
params = query["params"]
try:
test_params = dict(params)
test_params['Limit'] = 1
test_params['EnableTotalRecordCount'] = True
test_params["Limit"] = 1
test_params["EnableTotalRecordCount"] = True
items['TotalRecordCount'] = _get(url, test_params, server_id=server_id)['TotalRecordCount']
items["TotalRecordCount"] = _get(url, test_params, server_id=server_id)[
"TotalRecordCount"
]
except Exception as error:
LOG.exception("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:
params.setdefault('StartIndex', 0)
params.setdefault("StartIndex", 0)
def get_query_params(params, start, count):
params_copy = dict(params)
params_copy['StartIndex'] = start
params_copy['Limit'] = count
params_copy["StartIndex"] = start
params_copy["Limit"] = count
return params_copy
query_params = [
get_query_params(params, offset, limit)
for offset
in range(params['StartIndex'], items['TotalRecordCount'], limit)
for offset in range(params["StartIndex"], items["TotalRecordCount"], limit)
]
# multiprocessing.dummy.Pool completes all requests in multiple threads but has to
@ -257,27 +266,29 @@ def _get_items(query, server_id=None):
# process complete jobs
for job in concurrent.futures.as_completed(jobs):
# get the result
result = job.result() or {'Items': []}
query['params'] = jobs[job]
result = job.result() or {"Items": []}
query["params"] = jobs[job]
# free job memory
del jobs[job]
del job
# Mitigates #216 till the server validates the date provided is valid
if result['Items'][0].get('ProductionYear'):
if result["Items"][0].get("ProductionYear"):
try:
date(result['Items'][0]['ProductionYear'], 1, 1)
date(result["Items"][0]["ProductionYear"], 1, 1)
except ValueError:
LOG.info('#216 mitigation triggered. Setting ProductionYear to None')
result['Items'][0]['ProductionYear'] = None
LOG.info(
"#216 mitigation triggered. Setting ProductionYear to None"
)
result["Items"][0]["ProductionYear"] = None
items['Items'].extend(result['Items'])
items["Items"].extend(result["Items"])
# Using items to return data and communicate a restore point back to the callee is
# a violation of the SRP. TODO: Separate responsibilities.
items['RestorePoint'] = query
items["RestorePoint"] = query
yield items
del items['Items'][:]
del items["Items"][:]
# release the semaphore again
thread_buffer.release()
@ -307,25 +318,25 @@ class GetItemWorker(threading.Thread):
return
request = {
'type': "GET",
'handler': "Users/{UserId}/Items",
'params': {
'Ids': ','.join(str(x) for x in item_ids),
'Fields': api.info()
}
"type": "GET",
"handler": "Users/{UserId}/Items",
"params": {
"Ids": ",".join(str(x) for x in item_ids),
"Fields": api.info(),
},
}
try:
result = self.server.http.request(request, s)
for item in result['Items']:
for item in result["Items"]:
if item['Type'] in self.output:
self.output[item['Type']].put(item)
if item["Type"] in self.output:
self.output[item["Type"]].put(item)
except HTTPException as error:
LOG.error("--[ http status: %s ]", error.status)
if error.status == 'ServerUnreachable':
if error.status == "ServerUnreachable":
self.is_done = True
break
@ -335,5 +346,5 @@ class GetItemWorker(threading.Thread):
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break

View File

@ -17,14 +17,18 @@ from ..jellyfin import Jellyfin
#################################################################################################
LOG = LazyLogger(__name__)
XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i")
XML_PATH = (
xbmcaddon.Addon("plugin.video.jellyfin").getAddonInfo("path"),
"default",
"1080i",
)
OPTIONS = {
'Refresh': translate(30410),
'Delete': translate(30409),
'Addon': translate(30408),
'AddFav': translate(30405),
'RemoveFav': translate(30406),
'Transcode': translate(30412)
"Refresh": translate(30410),
"Delete": translate(30409),
"Addon": translate(30408),
"AddFav": translate(30405),
"RemoveFav": translate(30406),
"Transcode": translate(30412),
}
#################################################################################################
@ -39,31 +43,33 @@ class Context(object):
try:
self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None
self.media = self.get_media_type()
self.server_id = sys.listitem.getProperty('jellyfinserver') or None
self.server_id = sys.listitem.getProperty("jellyfinserver") or None
self.api_client = Jellyfin(self.server_id).get_client().jellyfin
item_id = sys.listitem.getProperty('jellyfinid')
item_id = sys.listitem.getProperty("jellyfinid")
except AttributeError:
self.server_id = None
if xbmc.getInfoLabel('ListItem.Property(jellyfinid)'):
item_id = xbmc.getInfoLabel('ListItem.Property(jellyfinid)')
if xbmc.getInfoLabel("ListItem.Property(jellyfinid)"):
item_id = xbmc.getInfoLabel("ListItem.Property(jellyfinid)")
else:
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID')
self.media = xbmc.getInfoLabel('ListItem.DBTYPE')
self.kodi_id = xbmc.getInfoLabel("ListItem.DBID")
self.media = xbmc.getInfoLabel("ListItem.DBTYPE")
item_id = None
addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json")
with open(addon_data, 'rb') as infile:
addon_data = translate_path(
"special://profile/addon_data/plugin.video.jellyfin/data.json"
)
with open(addon_data, "rb") as infile:
data = json.load(infile)
try:
server_data = data['Servers'][0]
self.api_client.config.data['auth.server'] = server_data.get('address')
self.api_client.config.data['auth.server-name'] = server_data.get('Name')
self.api_client.config.data['auth.user_id'] = server_data.get('UserId')
self.api_client.config.data['auth.token'] = server_data.get('AccessToken')
server_data = data["Servers"][0]
self.api_client.config.data["auth.server"] = server_data.get("address")
self.api_client.config.data["auth.server-name"] = server_data.get("Name")
self.api_client.config.data["auth.user_id"] = server_data.get("UserId")
self.api_client.config.data["auth.token"] = server_data.get("AccessToken")
except Exception as e:
LOG.warning('Addon appears to not be configured yet: {}'.format(e))
LOG.warning("Addon appears to not be configured yet: {}".format(e))
if self.server_id or item_id:
self.item = self.api_client.get_item(item_id)
@ -81,26 +87,28 @@ class Context(object):
elif self.select_menu():
self.action_menu()
if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], OPTIONS['RemoveFav']):
if self._selected_option in (
OPTIONS["Delete"],
OPTIONS["AddFav"],
OPTIONS["RemoveFav"],
):
xbmc.sleep(500)
xbmc.executebuiltin('Container.Refresh')
xbmc.executebuiltin("Container.Refresh")
def get_media_type(self):
''' Get media type based on sys.listitem. If unfilled, base on visible window.
'''
"""Get media type based on sys.listitem. If unfilled, base on visible window."""
media = sys.listitem.getVideoInfoTag().getMediaType()
if not media:
if xbmc.getCondVisibility('Container.Content(albums)'):
if xbmc.getCondVisibility("Container.Content(albums)"):
media = "album"
elif xbmc.getCondVisibility('Container.Content(artists)'):
elif xbmc.getCondVisibility("Container.Content(artists)"):
media = "artist"
elif xbmc.getCondVisibility('Container.Content(songs)'):
elif xbmc.getCondVisibility("Container.Content(songs)"):
media = "song"
elif xbmc.getCondVisibility('Container.Content(pictures)'):
elif xbmc.getCondVisibility("Container.Content(pictures)"):
media = "picture"
else:
LOG.info("media is unknown")
@ -108,40 +116,37 @@ class Context(object):
return media
def get_item_id(self):
''' Get synced item from jellyfindb.
'''
"""Get synced item from jellyfindb."""
item = database.get_item(self.kodi_id, self.media)
if not item:
return
return {
'Id': item[0],
'UserData': json.loads(item[4]) if item[4] else {},
'Type': item[3]
"Id": item[0],
"UserData": json.loads(item[4]) if item[4] else {},
"Type": item[3],
}
def select_menu(self):
''' Display the select dialog.
Favorites, Refresh, Delete (opt), Settings.
'''
"""Display the select dialog.
Favorites, Refresh, Delete (opt), Settings.
"""
options = []
if self.item['Type'] != 'Season':
if self.item["Type"] != "Season":
if self.item['UserData'].get('IsFavorite'):
options.append(OPTIONS['RemoveFav'])
if self.item["UserData"].get("IsFavorite"):
options.append(OPTIONS["RemoveFav"])
else:
options.append(OPTIONS['AddFav'])
options.append(OPTIONS["AddFav"])
options.append(OPTIONS['Refresh'])
options.append(OPTIONS["Refresh"])
if settings('enableContextDelete.bool'):
options.append(OPTIONS['Delete'])
if settings("enableContextDelete.bool"):
options.append(OPTIONS["Delete"])
options.append(OPTIONS['Addon'])
options.append(OPTIONS["Addon"])
context_menu = context.ContextMenu("script-jellyfin-context.xml", *XML_PATH)
context_menu.set_options(options)
@ -156,24 +161,26 @@ class Context(object):
selected = self._selected_option
if selected == OPTIONS['Refresh']:
self.api_client.refresh_item(self.item['Id'])
if selected == OPTIONS["Refresh"]:
self.api_client.refresh_item(self.item["Id"])
elif selected == OPTIONS['AddFav']:
self.api_client.favorite(self.item['Id'], True)
elif selected == OPTIONS["AddFav"]:
self.api_client.favorite(self.item["Id"], True)
elif selected == OPTIONS['RemoveFav']:
self.api_client.favorite(self.item['Id'], False)
elif selected == OPTIONS["RemoveFav"]:
self.api_client.favorite(self.item["Id"], False)
elif selected == OPTIONS['Addon']:
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.jellyfin)')
elif selected == OPTIONS["Addon"]:
xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)")
elif selected == OPTIONS['Delete']:
elif selected == OPTIONS["Delete"]:
self.delete_item()
def delete_item(self):
if settings('skipContextMenu.bool') or dialog("yesno", "{jellyfin}", translate(33015)):
self.api_client.delete_item(self.item['Id'])
if settings("skipContextMenu.bool") or dialog(
"yesno", "{jellyfin}", translate(33015)
):
self.api_client.delete_item(self.item["Id"])
def transcode(self):
filename = xbmc.getInfoLabel("ListItem.Filenameandpath")

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,15 @@ from .. import client
from .. import library
from .. import monitor
from ..views import Views
from ..helper import translate, window, settings, event, dialog, set_addon_mode, LazyLogger
from ..helper import (
translate,
window,
settings,
event,
dialog,
set_addon_mode,
LazyLogger,
)
from ..helper.utils import JsonDebugPrinter, translate_path
from ..helper.xmls import verify_kodi_defaults
from ..jellyfin import Jellyfin
@ -37,83 +45,98 @@ class Service(xbmc.Monitor):
monitor = None
play_event = None
warn = True
settings = {'last_progress': datetime.today(), 'last_progress_report': datetime.today()}
settings = {
"last_progress": datetime.today(),
"last_progress_report": datetime.today(),
}
def __init__(self):
window('jellyfin_should_stop', clear=True)
window("jellyfin_should_stop", clear=True)
self.settings['addon_version'] = client.get_version()
self.settings['profile'] = translate_path('special://profile')
self.settings['mode'] = settings('useDirectPaths')
self.settings['log_level'] = settings('logLevel') or "1"
self.settings['auth_check'] = True
self.settings['enable_context'] = settings('enableContext.bool')
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool')
self.settings['kodi_companion'] = settings('kodiCompanion.bool')
window('jellyfin_kodiProfile', value=self.settings['profile'])
settings('platformDetected', client.get_platform())
self.settings["addon_version"] = client.get_version()
self.settings["profile"] = translate_path("special://profile")
self.settings["mode"] = settings("useDirectPaths")
self.settings["log_level"] = settings("logLevel") or "1"
self.settings["auth_check"] = True
self.settings["enable_context"] = settings("enableContext.bool")
self.settings["enable_context_transcode"] = settings(
"enableContextTranscode.bool"
)
self.settings["kodi_companion"] = settings("kodiCompanion.bool")
window("jellyfin_kodiProfile", value=self.settings["profile"])
settings("platformDetected", client.get_platform())
if self.settings['enable_context']:
window('jellyfin_context.bool', True)
if self.settings['enable_context_transcode']:
window('jellyfin_context_transcode.bool', True)
if self.settings["enable_context"]:
window("jellyfin_context.bool", True)
if self.settings["enable_context_transcode"]:
window("jellyfin_context_transcode.bool", True)
LOG.info("--->>>[ %s ]", client.get_addon_name())
LOG.info("Version: %s", client.get_version())
LOG.info("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion'))
LOG.info("Platform: %s", settings('platformDetected'))
LOG.info("KODI Version: %s", xbmc.getInfoLabel("System.BuildVersion"))
LOG.info("Platform: %s", settings("platformDetected"))
LOG.info("Python Version: %s", sys.version)
LOG.info("Using dynamic paths: %s", settings('useDirectPaths') == "0")
LOG.info("Log Level: %s", self.settings['log_level'])
LOG.info("Using dynamic paths: %s", settings("useDirectPaths") == "0")
LOG.info("Log Level: %s", self.settings["log_level"])
verify_kodi_defaults()
window('jellyfin.connected.bool', True)
settings('groupedSets.bool', objects.utils.get_grouped_set())
window("jellyfin.connected.bool", True)
settings("groupedSets.bool", objects.utils.get_grouped_set())
xbmc.Monitor.__init__(self)
def service(self):
"""Keeps the service monitor going.
Exit on Kodi shutdown or profile switch.
''' Keeps the service monitor going.
Exit on Kodi shutdown or profile switch.
if profile switch happens more than once,
Threads depending on abortRequest will not trigger.
'''
if profile switch happens more than once,
Threads depending on abortRequest will not trigger.
"""
self.monitor = monitor.Monitor()
player = self.monitor.player
self.connect = connect.Connect()
self.start_default()
self.settings['mode'] = settings('useDirectPaths')
self.settings["mode"] = settings("useDirectPaths")
while self.running:
if window('jellyfin_online.bool'):
if window("jellyfin_online.bool"):
if self.settings['profile'] != window('jellyfin_kodiProfile'):
LOG.info("[ profile switch ] %s", self.settings['profile'])
if self.settings["profile"] != window("jellyfin_kodiProfile"):
LOG.info("[ profile switch ] %s", self.settings["profile"])
break
if player.isPlaying() and player.is_playing_file(player.get_playing_file()):
difference = datetime.today() - self.settings['last_progress']
if player.isPlaying() and player.is_playing_file(
player.get_playing_file()
):
difference = datetime.today() - self.settings["last_progress"]
if difference.seconds > 10:
self.settings['last_progress'] = datetime.today()
self.settings["last_progress"] = datetime.today()
update = (datetime.today() - self.settings['last_progress_report']).seconds > 250
event('ReportProgressRequested', {'Report': update})
update = (
datetime.today() - self.settings["last_progress_report"]
).seconds > 250
event("ReportProgressRequested", {"Report": update})
if update:
self.settings['last_progress_report'] = datetime.today()
self.settings["last_progress_report"] = datetime.today()
if window('jellyfin.restart.bool'):
if window("jellyfin.restart.bool"):
window('jellyfin.restart', clear=True)
dialog("notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False)
window("jellyfin.restart", clear=True)
dialog(
"notification",
heading="{jellyfin}",
message=translate(33193),
icon="{jellyfin}",
time=1000,
sound=False,
)
raise Exception('RestartService')
raise Exception("RestartService")
if self.waitForAbort(1):
break
@ -126,14 +149,14 @@ class Service(xbmc.Monitor):
try:
self.connect.register()
if not settings('SyncInstallRunDone.bool'):
if not settings("SyncInstallRunDone.bool"):
set_addon_mode()
except Exception as error:
LOG.exception(error)
def stop_default(self):
window('jellyfin_online', clear=True)
window("jellyfin_online", clear=True)
Jellyfin().close()
if self.library_thread is not None:
@ -142,59 +165,93 @@ class Service(xbmc.Monitor):
self.library_thread = None
def onNotification(self, sender, method, data):
''' All notifications are sent via NotifyAll built-in or Kodi.
Central hub.
'''
if sender.lower() not in ('plugin.video.jellyfin', 'xbmc'):
"""All notifications are sent via NotifyAll built-in or Kodi.
Central hub.
"""
if sender.lower() not in ("plugin.video.jellyfin", "xbmc"):
return
if sender == 'plugin.video.jellyfin':
method = method.split('.')[1]
if sender == "plugin.video.jellyfin":
method = method.split(".")[1]
if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect',
'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary',
'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer',
'Unauthorized', 'UserConfigurationUpdated', 'ServerRestarting',
'RemoveServer', 'UpdatePassword', 'AddLibrarySelection', 'RemoveLibrarySelection'):
if method not in (
"ServerUnreachable",
"ServerShuttingDown",
"UserDataChanged",
"ServerConnect",
"LibraryChanged",
"ServerOnline",
"SyncLibrary",
"RepairLibrary",
"RemoveLibrary",
"SyncLibrarySelection",
"RepairLibrarySelection",
"AddServer",
"Unauthorized",
"UserConfigurationUpdated",
"ServerRestarting",
"RemoveServer",
"UpdatePassword",
"AddLibrarySelection",
"RemoveLibrarySelection",
):
return
data = json.loads(data)[0]
else:
if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'):
if method not in ("System.OnQuit", "System.OnSleep", "System.OnWake"):
return
data = json.loads(data)
LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data))
if method == 'ServerOnline':
if data.get('ServerId') is None:
if method == "ServerOnline":
if data.get("ServerId") is None:
window('jellyfin_online.bool', True)
self.settings['auth_check'] = True
window("jellyfin_online.bool", True)
self.settings["auth_check"] = True
self.warn = True
if settings('connectMsg.bool'):
if settings("connectMsg.bool"):
users = [user for user in (settings('additionalUsers') or "").split(',') if user]
users.insert(0, settings('username'))
dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33000), ", ".join(users)),
icon="{jellyfin}", time=1500, sound=False)
users = [
user
for user in (settings("additionalUsers") or "").split(",")
if user
]
users.insert(0, settings("username"))
dialog(
"notification",
heading="{jellyfin}",
message="%s %s" % (translate(33000), ", ".join(users)),
icon="{jellyfin}",
time=1500,
sound=False,
)
if self.library_thread is None:
self.library_thread = library.Library(self)
self.library_thread.start()
elif method in ('ServerUnreachable', 'ServerShuttingDown'):
elif method in ("ServerUnreachable", "ServerShuttingDown"):
if self.warn or data.get('ServerId'):
if self.warn or data.get("ServerId"):
self.warn = data.get('ServerId') is not None
dialog("notification", heading="{jellyfin}", message=translate(33146) if data.get('ServerId') is None else translate(33149), icon=xbmcgui.NOTIFICATION_ERROR)
self.warn = data.get("ServerId") is not None
dialog(
"notification",
heading="{jellyfin}",
message=(
translate(33146)
if data.get("ServerId") is None
else translate(33149)
),
icon=xbmcgui.NOTIFICATION_ERROR,
)
if data.get('ServerId') is None:
if data.get("ServerId") is None:
self.stop_default()
if self.waitForAbort(120):
@ -202,12 +259,19 @@ class Service(xbmc.Monitor):
self.start_default()
elif method == 'Unauthorized':
dialog("notification", heading="{jellyfin}", message=translate(33147) if data['ServerId'] is None else translate(33148), icon=xbmcgui.NOTIFICATION_ERROR)
elif method == "Unauthorized":
dialog(
"notification",
heading="{jellyfin}",
message=(
translate(33147) if data["ServerId"] is None else translate(33148)
),
icon=xbmcgui.NOTIFICATION_ERROR,
)
if data.get('ServerId') is None and self.settings['auth_check']:
if data.get("ServerId") is None and self.settings["auth_check"]:
self.settings['auth_check'] = False
self.settings["auth_check"] = False
self.stop_default()
if self.waitForAbort(5):
@ -215,12 +279,17 @@ class Service(xbmc.Monitor):
self.start_default()
elif method == 'ServerRestarting':
if data.get('ServerId'):
elif method == "ServerRestarting":
if data.get("ServerId"):
return
if settings('restartMsg.bool'):
dialog("notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}")
if settings("restartMsg.bool"):
dialog(
"notification",
heading="{jellyfin}",
message=translate(33006),
icon="{jellyfin}",
)
self.stop_default()
@ -229,67 +298,72 @@ class Service(xbmc.Monitor):
self.start_default()
elif method == 'ServerConnect':
self.connect.register(data['Id'])
elif method == "ServerConnect":
self.connect.register(data["Id"])
xbmc.executebuiltin("Container.Refresh")
elif method == 'AddServer':
elif method == "AddServer":
self.connect.setup_manual_server()
xbmc.executebuiltin("Container.Refresh")
elif method == 'RemoveServer':
elif method == "RemoveServer":
self.connect.remove_server(data['Id'])
self.connect.remove_server(data["Id"])
xbmc.executebuiltin("Container.Refresh")
elif method == 'UpdatePassword':
elif method == "UpdatePassword":
self.connect.setup_login_manual()
elif method == 'UserDataChanged' and self.library_thread:
if data.get('ServerId') or not window('jellyfin_startup.bool'):
elif method == "UserDataChanged" and self.library_thread:
if data.get("ServerId") or not window("jellyfin_startup.bool"):
return
LOG.info("[ UserDataChanged ] %s", data)
self.library_thread.userdata(data['UserDataList'])
self.library_thread.userdata(data["UserDataList"])
elif method == 'LibraryChanged' and self.library_thread:
if data.get('ServerId') or not window('jellyfin_startup.bool'):
elif method == "LibraryChanged" and self.library_thread:
if data.get("ServerId") or not window("jellyfin_startup.bool"):
return
LOG.info("[ LibraryChanged ] %s", data)
self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded'])
self.library_thread.removed(data['ItemsRemoved'])
self.library_thread.updated(data["ItemsUpdated"] + data["ItemsAdded"])
self.library_thread.removed(data["ItemsRemoved"])
elif method == 'System.OnQuit':
window('jellyfin_should_stop.bool', True)
elif method == "System.OnQuit":
window("jellyfin_should_stop.bool", True)
self.running = False
elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'):
elif method in (
"SyncLibrarySelection",
"RepairLibrarySelection",
"AddLibrarySelection",
"RemoveLibrarySelection",
):
self.library_thread.select_libraries(method)
elif method == 'SyncLibrary':
if not data.get('Id'):
elif method == "SyncLibrary":
if not data.get("Id"):
return
self.library_thread.add_library(data['Id'], data.get('Update', False))
self.library_thread.add_library(data["Id"], data.get("Update", False))
xbmc.executebuiltin("Container.Refresh")
elif method == 'RepairLibrary':
if not data.get('Id'):
elif method == "RepairLibrary":
if not data.get("Id"):
return
libraries = data['Id'].split(',')
libraries = data["Id"].split(",")
for lib in libraries:
if not self.library_thread.remove_library(lib):
return
self.library_thread.add_library(data['Id'])
self.library_thread.add_library(data["Id"])
xbmc.executebuiltin("Container.Refresh")
elif method == 'RemoveLibrary':
libraries = data['Id'].split(',')
elif method == "RemoveLibrary":
libraries = data["Id"].split(",")
for lib in libraries:
@ -298,10 +372,10 @@ class Service(xbmc.Monitor):
xbmc.executebuiltin("Container.Refresh")
elif method == 'System.OnSleep':
elif method == "System.OnSleep":
LOG.info("-->[ sleep ]")
window('jellyfin_should_stop.bool', True)
window("jellyfin_should_stop.bool", True)
if self.library_thread is not None:
@ -312,7 +386,7 @@ class Service(xbmc.Monitor):
self.monitor.server = []
self.monitor.sleep = True
elif method == 'System.OnWake':
elif method == "System.OnWake":
if not self.monitor.sleep:
LOG.warning("System.OnSleep was never called, skip System.OnWake")
@ -322,14 +396,14 @@ class Service(xbmc.Monitor):
LOG.info("--<[ sleep ]")
xbmc.sleep(10000) # Allow network to wake up
self.monitor.sleep = False
window('jellyfin_should_stop', clear=True)
window("jellyfin_should_stop", clear=True)
try:
self.connect.register()
except Exception as error:
LOG.exception(error)
elif method == 'GUI.OnScreensaverDeactivated':
elif method == "GUI.OnScreensaverDeactivated":
LOG.info("--<[ screensaver ]")
xbmc.sleep(5000)
@ -337,60 +411,80 @@ class Service(xbmc.Monitor):
if self.library_thread is not None:
self.library_thread.fast_sync()
elif method == 'UserConfigurationUpdated' and data.get('ServerId') is None:
elif method == "UserConfigurationUpdated" and data.get("ServerId") is None:
Views().get_views()
def onSettingsChanged(self):
''' React to setting changes that impact window values.
'''
if window('jellyfin_should_stop.bool'):
"""React to setting changes that impact window values."""
if window("jellyfin_should_stop.bool"):
return
if settings('logLevel') != self.settings['log_level']:
if settings("logLevel") != self.settings["log_level"]:
log_level = settings('logLevel')
self.settings['logLevel'] = log_level
log_level = settings("logLevel")
self.settings["logLevel"] = log_level
LOG.info("New log level: %s", log_level)
if settings('enableContext.bool') != self.settings['enable_context']:
if settings("enableContext.bool") != self.settings["enable_context"]:
window('jellyfin_context', settings('enableContext'))
self.settings['enable_context'] = settings('enableContext.bool')
LOG.info("New context setting: %s", self.settings['enable_context'])
window("jellyfin_context", settings("enableContext"))
self.settings["enable_context"] = settings("enableContext.bool")
LOG.info("New context setting: %s", self.settings["enable_context"])
if settings('enableContextTranscode.bool') != self.settings['enable_context_transcode']:
if (
settings("enableContextTranscode.bool")
!= self.settings["enable_context_transcode"]
):
window('jellyfin_context_transcode', settings('enableContextTranscode'))
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool')
LOG.info("New context transcode setting: %s", self.settings['enable_context_transcode'])
window("jellyfin_context_transcode", settings("enableContextTranscode"))
self.settings["enable_context_transcode"] = settings(
"enableContextTranscode.bool"
)
LOG.info(
"New context transcode setting: %s",
self.settings["enable_context_transcode"],
)
if settings('useDirectPaths') != self.settings['mode'] and self.library_thread.started:
if (
settings("useDirectPaths") != self.settings["mode"]
and self.library_thread.started
):
self.settings['mode'] = settings('useDirectPaths')
LOG.info("New playback mode setting: %s", self.settings['mode'])
self.settings["mode"] = settings("useDirectPaths")
LOG.info("New playback mode setting: %s", self.settings["mode"])
if not self.settings.get('mode_warn'):
if not self.settings.get("mode_warn"):
self.settings['mode_warn'] = True
self.settings["mode_warn"] = True
dialog("yesno", "{jellyfin}", translate(33118))
if settings('kodiCompanion.bool') != self.settings['kodi_companion']:
self.settings['kodi_companion'] = settings('kodiCompanion.bool')
if settings("kodiCompanion.bool") != self.settings["kodi_companion"]:
self.settings["kodi_companion"] = settings("kodiCompanion.bool")
if not self.settings['kodi_companion']:
if not self.settings["kodi_companion"]:
dialog("ok", "{jellyfin}", translate(33138))
def reload_objects(self):
''' Reload objects which depends on the patch module.
This allows to see the changes in code without restarting the python interpreter.
'''
reload_modules = ['objects.movies', 'objects.musicvideos', 'objects.tvshows',
'objects.music', 'objects.obj', 'objects.actions', 'objects.kodi.kodi',
'objects.kodi.movies', 'objects.kodi.musicvideos', 'objects.kodi.tvshows',
'objects.kodi.music', 'objects.kodi.artwork', 'objects.kodi.queries',
'objects.kodi.queries_music', 'objects.kodi.queries_texture']
"""Reload objects which depends on the patch module.
This allows to see the changes in code without restarting the python interpreter.
"""
reload_modules = [
"objects.movies",
"objects.musicvideos",
"objects.tvshows",
"objects.music",
"objects.obj",
"objects.actions",
"objects.kodi.kodi",
"objects.kodi.movies",
"objects.kodi.musicvideos",
"objects.kodi.tvshows",
"objects.kodi.music",
"objects.kodi.artwork",
"objects.kodi.queries",
"objects.kodi.queries_music",
"objects.kodi.queries_texture",
]
for mod in reload_modules:
del sys.modules[mod]
@ -407,14 +501,22 @@ class Service(xbmc.Monitor):
def shutdown(self):
LOG.info("---<[ EXITING ]")
window('jellyfin_should_stop.bool', True)
window("jellyfin_should_stop.bool", True)
properties = [ # TODO: review
"jellyfin_state", "jellyfin_serverStatus", "jellyfin_currUser",
"jellyfin_play", "jellyfin_online", "jellyfin.connected", "jellyfin_startup",
"jellyfin.external", "jellyfin.external_check", "jellyfin_deviceId", "jellyfin_db_check", "jellyfin_pathverified",
"jellyfin_sync"
"jellyfin_state",
"jellyfin_serverStatus",
"jellyfin_currUser",
"jellyfin_play",
"jellyfin_online",
"jellyfin.connected",
"jellyfin_startup",
"jellyfin.external",
"jellyfin.external_check",
"jellyfin_deviceId",
"jellyfin_db_check",
"jellyfin_pathverified",
"jellyfin_sync",
]
for prop in properties:
window(prop, clear=True)

View File

@ -23,11 +23,11 @@ LOG = LazyLogger(__name__)
class FullSync(object):
"""This should be called like a context.
i.e. with FullSync('jellyfin') as sync:
sync.libraries()
"""
''' This should be called like a context.
i.e. with FullSync('jellyfin') as sync:
sync.libraries()
'''
# Borg - multiple instances, shared state
_shared_state = {}
sync = None
@ -35,10 +35,9 @@ class FullSync(object):
screensaver = None
def __init__(self, library, server):
''' You can call all big syncing methods here.
Initial, update, repair, remove.
'''
"""You can call all big syncing methods here.
Initial, update, repair, remove.
"""
self.__dict__ = self._shared_state
if self.running:
@ -50,78 +49,81 @@ class FullSync(object):
self.server = server
def __enter__(self):
''' Do everything we need before the sync
'''
"""Do everything we need before the sync"""
LOG.info("-->[ fullsync ]")
if not settings('dbSyncScreensaver.bool'):
if not settings("dbSyncScreensaver.bool"):
xbmc.executebuiltin('InhibitIdleShutdown(true)')
xbmc.executebuiltin("InhibitIdleShutdown(true)")
self.screensaver = get_screensaver()
set_screensaver(value="")
self.running = True
window('jellyfin_sync.bool', True)
window("jellyfin_sync.bool", True)
return self
def libraries(self, libraries=None, update=False):
''' Map the syncing process and start the sync. Ensure only one sync is running.
'''
self.direct_path = settings('useDirectPaths') == "1"
"""Map the syncing process and start the sync. Ensure only one sync is running."""
self.direct_path = settings("useDirectPaths") == "1"
self.update_library = update
self.sync = get_sync()
if libraries:
# Can be a single ID or a comma separated list
libraries = libraries.split(',')
libraries = libraries.split(",")
for library_id in libraries:
# Look up library in local Jellyfin database
library = self.get_library(library_id)
if library:
if library.media_type == 'mixed':
self.sync['Libraries'].append("Mixed:%s" % library_id)
if library.media_type == "mixed":
self.sync["Libraries"].append("Mixed:%s" % library_id)
# Include boxsets library
libraries = self.get_libraries()
boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets']
boxsets = [
row.view_id
for row in libraries
if row.media_type == "boxsets"
]
if boxsets:
self.sync['Libraries'].append('Boxsets:%s' % boxsets[0])
elif library.media_type == 'movies':
self.sync['Libraries'].append(library_id)
self.sync["Libraries"].append("Boxsets:%s" % boxsets[0])
elif library.media_type == "movies":
self.sync["Libraries"].append(library_id)
# Include boxsets library
libraries = self.get_libraries()
boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets']
boxsets = [
row.view_id
for row in libraries
if row.media_type == "boxsets"
]
# Verify we're only trying to sync boxsets once
if boxsets and boxsets[0] not in self.sync['Libraries']:
self.sync['Libraries'].append('Boxsets:%s' % boxsets[0])
if boxsets and boxsets[0] not in self.sync["Libraries"]:
self.sync["Libraries"].append("Boxsets:%s" % boxsets[0])
else:
# Only called if the library isn't already known about
self.sync['Libraries'].append(library_id)
self.sync["Libraries"].append(library_id)
else:
self.sync['Libraries'].append(library_id)
self.sync["Libraries"].append(library_id)
else:
self.mapping()
if not xmls.advanced_settings() and self.sync['Libraries']:
if not xmls.advanced_settings() and self.sync["Libraries"]:
self.start()
def get_libraries(self):
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views()
def get_library(self, library_id):
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_view(library_id)
def mapping(self):
''' Load the mapping of the full sync.
This allows us to restore a previous sync.
'''
if self.sync['Libraries']:
"""Load the mapping of the full sync.
This allows us to restore a previous sync.
"""
if self.sync["Libraries"]:
if not dialog("yesno", "{jellyfin}", translate(33102)):
@ -130,38 +132,48 @@ class FullSync(object):
raise LibraryException("ProgressStopped")
else:
self.sync['Libraries'] = []
self.sync['RestorePoint'] = {}
self.sync["Libraries"] = []
self.sync["RestorePoint"] = {}
else:
LOG.info("generate full sync")
libraries = []
for library in self.get_libraries():
if library.media_type in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'):
libraries.append({'Id': library.view_id, 'Name': library.view_name, 'Media': library.media_type})
if library.media_type in (
"movies",
"tvshows",
"musicvideos",
"music",
"mixed",
):
libraries.append(
{
"Id": library.view_id,
"Name": library.view_name,
"Media": library.media_type,
}
)
libraries = self.select_libraries(libraries)
if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]:
self.sync['Libraries'].append("Boxsets:")
if [x["Media"] for x in libraries if x["Media"] in ("movies", "mixed")]:
self.sync["Libraries"].append("Boxsets:")
save_sync(self.sync)
def select_libraries(self, libraries):
"""Select all or certain libraries to be whitelisted."""
''' Select all or certain libraries to be whitelisted.
'''
choices = [x['Name'] for x in libraries]
choices = [x["Name"] for x in libraries]
choices.insert(0, translate(33121))
selection = dialog("multi", translate(33120), choices)
if selection is None:
raise LibraryException('LibrarySelection')
raise LibraryException("LibrarySelection")
elif not selection:
LOG.info("Nothing was selected.")
raise LibraryException('SyncLibraryLater')
raise LibraryException("SyncLibraryLater")
if 0 in selection:
selection = list(range(1, len(libraries) + 1))
@ -171,96 +183,100 @@ class FullSync(object):
for x in selection:
library = libraries[x - 1]
if library['Media'] != 'mixed':
selected_libraries.append(library['Id'])
if library["Media"] != "mixed":
selected_libraries.append(library["Id"])
else:
selected_libraries.append("Mixed:%s" % library['Id'])
selected_libraries.append("Mixed:%s" % library["Id"])
self.sync['Libraries'] = selected_libraries
self.sync["Libraries"] = selected_libraries
return [libraries[x - 1] for x in selection]
def start(self):
''' Main sync process.
'''
LOG.info("starting sync with %s", self.sync['Libraries'])
"""Main sync process."""
LOG.info("starting sync with %s", self.sync["Libraries"])
save_sync(self.sync)
start_time = datetime.datetime.now()
for library in list(self.sync['Libraries']):
for library in list(self.sync["Libraries"]):
self.process_library(library)
if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']:
self.sync['Whitelist'].append(library)
if (
not library.startswith("Boxsets:")
and library not in self.sync["Whitelist"]
):
self.sync["Whitelist"].append(library)
self.sync['Libraries'].pop(self.sync['Libraries'].index(library))
self.sync['RestorePoint'] = {}
self.sync["Libraries"].pop(self.sync["Libraries"].index(library))
self.sync["RestorePoint"] = {}
elapsed = datetime.datetime.now() - start_time
settings('SyncInstallRunDone.bool', True)
settings("SyncInstallRunDone.bool", True)
self.library.save_last_sync()
save_sync(self.sync)
xbmc.executebuiltin('UpdateLibrary(video)')
dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33025), str(elapsed).split('.')[0]),
icon="{jellyfin}", sound=False)
LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0])
xbmc.executebuiltin("UpdateLibrary(video)")
dialog(
"notification",
heading="{jellyfin}",
message="%s %s" % (translate(33025), str(elapsed).split(".")[0]),
icon="{jellyfin}",
sound=False,
)
LOG.info("Full sync completed in: %s", str(elapsed).split(".")[0])
def process_library(self, library_id):
''' Add a library by its id. Create a node and a playlist whenever appropriate.
'''
"""Add a library by its id. Create a node and a playlist whenever appropriate."""
media = {
'movies': self.movies,
'musicvideos': self.musicvideos,
'tvshows': self.tvshows,
'music': self.music
"movies": self.movies,
"musicvideos": self.musicvideos,
"tvshows": self.tvshows,
"music": self.music,
}
try:
if library_id.startswith('Boxsets:'):
if library_id.startswith("Boxsets:"):
boxset_library = {}
# Initial library sync is 'Boxsets:'
# Refresh from the addon menu is 'Boxsets:Refresh'
# Incremental syncs are 'Boxsets:$library_id'
sync_id = library_id.split(':')[1]
sync_id = library_id.split(":")[1]
if not sync_id or sync_id == 'Refresh':
if not sync_id or sync_id == "Refresh":
libraries = self.get_libraries()
else:
_lib = self.get_library(sync_id)
libraries = [_lib] if _lib else []
for entry in libraries:
if entry.media_type == 'boxsets':
boxset_library = {'Id': entry.view_id, 'Name': entry.view_name}
if entry.media_type == "boxsets":
boxset_library = {"Id": entry.view_id, "Name": entry.view_name}
break
if boxset_library:
if sync_id == 'Refresh':
if sync_id == "Refresh":
self.refresh_boxsets(boxset_library)
else:
self.boxsets(boxset_library)
return
library = self.server.jellyfin.get_item(library_id.replace('Mixed:', ""))
library = self.server.jellyfin.get_item(library_id.replace("Mixed:", ""))
if library_id.startswith('Mixed:'):
for mixed in ('movies', 'tvshows'):
if library_id.startswith("Mixed:"):
for mixed in ("movies", "tvshows"):
media[mixed](library)
self.sync['RestorePoint'] = {}
self.sync["RestorePoint"] = {}
else:
if library['CollectionType']:
settings('enableMusic.bool', True)
if library["CollectionType"]:
settings("enableMusic.bool", True)
media[library['CollectionType']](library)
media[library["CollectionType"]](library)
except LibraryException as error:
if error.status == 'StopCalled':
if error.status == "StopCalled":
save_sync(self.sync)
raise
@ -282,31 +298,41 @@ class FullSync(object):
def video_database_locks(self):
with self.library.database_lock:
with Database() as videodb:
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
yield videodb, jellyfindb
@progress()
def movies(self, library, dialog):
''' Process movies from a single library.
'''
"""Process movies from a single library."""
processed_ids = []
for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')):
for items in server.get_items(
library["Id"], "Movie", False, self.sync["RestorePoint"].get("params")
):
with self.video_database_locks() as (videodb, jellyfindb):
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
obj = Movies(
self.server, jellyfindb, videodb, self.direct_path, library
)
self.sync['RestorePoint'] = items['RestorePoint']
start_index = items['RestorePoint']['params']['StartIndex']
self.sync["RestorePoint"] = items["RestorePoint"]
start_index = items["RestorePoint"]["params"]["StartIndex"]
for index, movie in enumerate(items['Items']):
for index, movie in enumerate(items["Items"]):
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
heading="%s: %s" % (translate('addon_name'), library['Name']),
message=movie['Name'])
dialog.update(
int(
(
float(start_index + index)
/ float(items["TotalRecordCount"])
)
* 100
),
heading="%s: %s" % (translate("addon_name"), library["Name"]),
message=movie["Name"],
)
obj.movie(movie)
processed_ids.append(movie['Id'])
processed_ids.append(movie["Id"])
with self.video_database_locks() as (videodb, jellyfindb):
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
@ -316,158 +342,199 @@ class FullSync(object):
self.movies_compare(library, obj, jellyfindb)
def movies_compare(self, library, obj, jellyfinydb):
''' Compare entries from library to what's in the jellyfindb. Remove surplus
'''
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
db = jellyfin_db.JellyfinDatabase(jellyfinydb.cursor)
items = db.get_item_by_media_folder(library['Id'])
items = db.get_item_by_media_folder(library["Id"])
current = obj.item_ids
for x in items:
if x[0] not in current and x[1] == 'Movie':
if x[0] not in current and x[1] == "Movie":
obj.remove(x[0])
@progress()
def tvshows(self, library, dialog):
''' Process tvshows and episodes from a single library.
'''
"""Process tvshows and episodes from a single library."""
processed_ids = []
for items in server.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')):
for items in server.get_items(
library["Id"], "Series", False, self.sync["RestorePoint"].get("params")
):
with self.video_database_locks() as (videodb, jellyfindb):
obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True)
obj = TVShows(
self.server, jellyfindb, videodb, self.direct_path, library, True
)
self.sync['RestorePoint'] = items['RestorePoint']
start_index = items['RestorePoint']['params']['StartIndex']
self.sync["RestorePoint"] = items["RestorePoint"]
start_index = items["RestorePoint"]["params"]["StartIndex"]
for index, show in enumerate(items['Items']):
for index, show in enumerate(items["Items"]):
percent = int((float(start_index + index) / float(items['TotalRecordCount'])) * 100)
message = show['Name']
dialog.update(percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message)
percent = int(
(float(start_index + index) / float(items["TotalRecordCount"]))
* 100
)
message = show["Name"]
dialog.update(
percent,
heading="%s: %s" % (translate("addon_name"), library["Name"]),
message=message,
)
if obj.tvshow(show) is not False:
for episodes in server.get_episode_by_show(show['Id']):
for episode in episodes['Items']:
if episode.get('Path'):
dialog.update(percent, message="%s/%s" % (message, episode['Name'][:10]))
for episodes in server.get_episode_by_show(show["Id"]):
for episode in episodes["Items"]:
if episode.get("Path"):
dialog.update(
percent,
message="%s/%s"
% (message, episode["Name"][:10]),
)
obj.episode(episode)
processed_ids.append(show['Id'])
processed_ids.append(show["Id"])
with self.video_database_locks() as (videodb, jellyfindb):
obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True)
obj = TVShows(
self.server, jellyfindb, videodb, self.direct_path, library, True
)
obj.item_ids = processed_ids
if self.update_library:
self.tvshows_compare(library, obj, jellyfindb)
def tvshows_compare(self, library, obj, jellyfindb):
''' Compare entries from library to what's in the jellyfindb. Remove surplus
'''
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
items = db.get_item_by_media_folder(library['Id'])
items = db.get_item_by_media_folder(library["Id"])
for x in list(items):
items.extend(obj.get_child(x[0]))
current = obj.item_ids
for x in items:
if x[0] not in current and x[1] == 'Series':
if x[0] not in current and x[1] == "Series":
obj.remove(x[0])
@progress()
def musicvideos(self, library, dialog):
''' Process musicvideos from a single library.
'''
"""Process musicvideos from a single library."""
processed_ids = []
for items in server.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')):
for items in server.get_items(
library["Id"], "MusicVideo", False, self.sync["RestorePoint"].get("params")
):
with self.video_database_locks() as (videodb, jellyfindb):
obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library)
obj = MusicVideos(
self.server, jellyfindb, videodb, self.direct_path, library
)
self.sync['RestorePoint'] = items['RestorePoint']
start_index = items['RestorePoint']['params']['StartIndex']
self.sync["RestorePoint"] = items["RestorePoint"]
start_index = items["RestorePoint"]["params"]["StartIndex"]
for index, mvideo in enumerate(items['Items']):
for index, mvideo in enumerate(items["Items"]):
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
heading="%s: %s" % (translate('addon_name'), library['Name']),
message=mvideo['Name'])
dialog.update(
int(
(
float(start_index + index)
/ float(items["TotalRecordCount"])
)
* 100
),
heading="%s: %s" % (translate("addon_name"), library["Name"]),
message=mvideo["Name"],
)
obj.musicvideo(mvideo)
processed_ids.append(mvideo['Id'])
processed_ids.append(mvideo["Id"])
with self.video_database_locks() as (videodb, jellyfindb):
obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library)
obj = MusicVideos(
self.server, jellyfindb, videodb, self.direct_path, library
)
obj.item_ids = processed_ids
if self.update_library:
self.musicvideos_compare(library, obj, jellyfindb)
def musicvideos_compare(self, library, obj, jellyfindb):
''' Compare entries from library to what's in the jellyfindb. Remove surplus
'''
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
items = db.get_item_by_media_folder(library['Id'])
items = db.get_item_by_media_folder(library["Id"])
current = obj.item_ids
for x in items:
if x[0] not in current and x[1] == 'MusicVideo':
if x[0] not in current and x[1] == "MusicVideo":
obj.remove(x[0])
@progress()
def music(self, library, dialog):
''' Process artists, album, songs from a single library.
'''
"""Process artists, album, songs from a single library."""
with self.library.music_database_lock:
with Database('music') as musicdb:
with Database('jellyfin') as jellyfindb:
obj = Music(self.server, jellyfindb, musicdb, self.direct_path, library)
with Database("music") as musicdb:
with Database("jellyfin") as jellyfindb:
obj = Music(
self.server, jellyfindb, musicdb, self.direct_path, library
)
library_id = library['Id']
library_id = library["Id"]
total_items = server.get_item_count(library_id, 'MusicArtist,MusicAlbum,Audio')
total_items = server.get_item_count(
library_id, "MusicArtist,MusicAlbum,Audio"
)
count = 0
'''
"""
Music database syncing. Artists must be in the database
before albums, albums before songs. Pulls batches of items
in sizes of setting "Paging - Max items". 'artists',
'albums', and 'songs' are generators containing a dict of
api responses
'''
"""
artists = server.get_artists(library_id)
for batch in artists:
for item in batch['Items']:
LOG.debug('Artist: {}'.format(item.get('Name')))
for item in batch["Items"]:
LOG.debug("Artist: {}".format(item.get("Name")))
percent = int((float(count) / float(total_items)) * 100)
dialog.update(percent, message='Artist: {}'.format(item.get('Name')))
dialog.update(
percent, message="Artist: {}".format(item.get("Name"))
)
obj.artist(item)
count += 1
albums = server.get_items(library_id, item_type='MusicAlbum', params={'SortBy': 'AlbumArtist'})
albums = server.get_items(
library_id,
item_type="MusicAlbum",
params={"SortBy": "AlbumArtist"},
)
for batch in albums:
for item in batch['Items']:
LOG.debug('Album: {}'.format(item.get('Name')))
for item in batch["Items"]:
LOG.debug("Album: {}".format(item.get("Name")))
percent = int((float(count) / float(total_items)) * 100)
dialog.update(percent, message='Album: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name')))
dialog.update(
percent,
message="Album: {} - {}".format(
item.get("AlbumArtist", ""), item.get("Name")
),
)
obj.album(item)
count += 1
songs = server.get_items(library_id, item_type='Audio', params={'SortBy': 'AlbumArtist'})
songs = server.get_items(
library_id, item_type="Audio", params={"SortBy": "AlbumArtist"}
)
for batch in songs:
for item in batch['Items']:
LOG.debug('Song: {}'.format(item.get('Name')))
for item in batch["Items"]:
LOG.debug("Song: {}".format(item.get("Name")))
percent = int((float(count) / float(total_items)) * 100)
dialog.update(percent, message='Track: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name')))
dialog.update(
percent,
message="Track: {} - {}".format(
item.get("AlbumArtist", ""), item.get("Name")
),
)
obj.song(item)
count += 1
@ -475,45 +542,52 @@ class FullSync(object):
self.music_compare(library, obj, jellyfindb)
def music_compare(self, library, obj, jellyfindb):
''' Compare entries from library to what's in the jellyfindb. Remove surplus
'''
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
items = db.get_item_by_media_folder(library['Id'])
items = db.get_item_by_media_folder(library["Id"])
for x in list(items):
items.extend(obj.get_child(x[0]))
current = obj.item_ids
for x in items:
if x[0] not in current and x[1] == 'MusicArtist':
if x[0] not in current and x[1] == "MusicArtist":
obj.remove(x[0])
@progress(translate(33018))
def boxsets(self, library, dialog=None):
''' Process all boxsets.
'''
for items in server.get_items(library['Id'], "BoxSet", False, self.sync['RestorePoint'].get('params')):
"""Process all boxsets."""
for items in server.get_items(
library["Id"], "BoxSet", False, self.sync["RestorePoint"].get("params")
):
with self.video_database_locks() as (videodb, jellyfindb):
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
obj = Movies(
self.server, jellyfindb, videodb, self.direct_path, library
)
self.sync['RestorePoint'] = items['RestorePoint']
start_index = items['RestorePoint']['params']['StartIndex']
self.sync["RestorePoint"] = items["RestorePoint"]
start_index = items["RestorePoint"]["params"]["StartIndex"]
for index, boxset in enumerate(items['Items']):
for index, boxset in enumerate(items["Items"]):
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
heading="%s: %s" % (translate('addon_name'), translate('boxsets')),
message=boxset['Name'])
dialog.update(
int(
(
float(start_index + index)
/ float(items["TotalRecordCount"])
)
* 100
),
heading="%s: %s"
% (translate("addon_name"), translate("boxsets")),
message=boxset["Name"],
)
obj.boxset(boxset)
def refresh_boxsets(self, library):
''' Delete all existing boxsets and re-add.
'''
"""Delete all existing boxsets and re-add."""
with self.video_database_locks() as (videodb, jellyfindb):
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
obj.boxsets_reset()
@ -522,82 +596,108 @@ class FullSync(object):
@progress(translate(33144))
def remove_library(self, library_id, dialog):
''' Remove library by their id from the Kodi database.
'''
"""Remove library by their id from the Kodi database."""
direct_path = self.library.direct_path
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
library = db.get_view(library_id.replace('Mixed:', ""))
items = db.get_item_by_media_folder(library_id.replace('Mixed:', ""))
media = 'music' if library.media_type == 'music' else 'video'
library = db.get_view(library_id.replace("Mixed:", ""))
items = db.get_item_by_media_folder(library_id.replace("Mixed:", ""))
media = "music" if library.media_type == "music" else "video"
if media == 'music':
settings('MusicRescan.bool', False)
if media == "music":
settings("MusicRescan.bool", False)
if items:
with self.library.music_database_lock if media == 'music' else self.library.database_lock:
with (
self.library.music_database_lock
if media == "music"
else self.library.database_lock
):
with Database(media) as kodidb:
count = 0
if library.media_type == 'mixed':
if library.media_type == "mixed":
movies = [x for x in items if x[1] == 'Movie']
tvshows = [x for x in items if x[1] == 'Series']
movies = [x for x in items if x[1] == "Movie"]
tvshows = [x for x in items if x[1] == "Series"]
obj = Movies(self.server, jellyfindb, kodidb, direct_path, library).remove
obj = Movies(
self.server, jellyfindb, kodidb, direct_path, library
).remove
for item in movies:
obj(item[0])
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name))
dialog.update(
int((float(count) / float(len(items)) * 100)),
heading="%s: %s"
% (translate("addon_name"), library.view_name),
)
count += 1
obj = TVShows(self.server, jellyfindb, kodidb, direct_path, library).remove
obj = TVShows(
self.server, jellyfindb, kodidb, direct_path, library
).remove
for item in tvshows:
obj(item[0])
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name))
dialog.update(
int((float(count) / float(len(items)) * 100)),
heading="%s: %s"
% (translate("addon_name"), library.view_name),
)
count += 1
else:
default_args = (self.server, jellyfindb, kodidb, direct_path)
default_args = (
self.server,
jellyfindb,
kodidb,
direct_path,
)
for item in items:
if item[1] in ('Series', 'Season', 'Episode'):
if item[1] in ("Series", "Season", "Episode"):
TVShows(*default_args).remove(item[0])
elif item[1] in ('Movie', 'BoxSet'):
elif item[1] in ("Movie", "BoxSet"):
Movies(*default_args).remove(item[0])
elif item[1] in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'):
elif item[1] in (
"MusicAlbum",
"MusicArtist",
"AlbumArtist",
"Audio",
):
Music(*default_args).remove(item[0])
elif item[1] == 'MusicVideo':
elif item[1] == "MusicVideo":
MusicVideos(*default_args).remove(item[0])
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0]))
dialog.update(
int((float(count) / float(len(items)) * 100)),
heading="%s: %s"
% (translate("addon_name"), library[0]),
)
count += 1
self.sync = get_sync()
if library_id in self.sync['Whitelist']:
self.sync['Whitelist'].remove(library_id)
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)
elif "Mixed:%s" % library_id in self.sync["Whitelist"]:
self.sync["Whitelist"].remove("Mixed:%s" % library_id)
save_sync(self.sync)
def __exit__(self, exc_type, exc_val, exc_tb):
''' Exiting sync
'''
"""Exiting sync"""
self.running = False
window('jellyfin_sync', clear=True)
window("jellyfin_sync", clear=True)
if not settings('dbSyncScreensaver.bool') and self.screensaver is not None:
if not settings("dbSyncScreensaver.bool") and self.screensaver is not None:
xbmc.executebuiltin('InhibitIdleShutdown(false)')
xbmc.executebuiltin("InhibitIdleShutdown(false)")
set_screensaver(value=self.screensaver)
LOG.info("--<[ fullsync ]")

View File

@ -14,100 +14,102 @@ LOG = LazyLogger(__name__)
class API(object):
def __init__(self, item, server=None):
''' Get item information in special cases.
server is the server address, provide if your functions requires it.
'''
"""Get item information in special cases.
server is the server address, provide if your functions requires it.
"""
self.item = item
self.server = server
def get_playcount(self, played, playcount):
''' Convert Jellyfin played/playcount into
the Kodi equivalent. The playcount is tied to the watch status.
'''
"""Convert Jellyfin played/playcount into
the Kodi equivalent. The playcount is tied to the watch status.
"""
return (playcount or 1) if played else None
def get_naming(self):
if self.item['Type'] == 'Episode' and 'SeriesName' in self.item:
return "%s: %s" % (self.item['SeriesName'], self.item['Name'])
if self.item["Type"] == "Episode" and "SeriesName" in self.item:
return "%s: %s" % (self.item["SeriesName"], self.item["Name"])
elif self.item['Type'] == 'MusicAlbum' and 'AlbumArtist' in self.item:
return "%s: %s" % (self.item['AlbumArtist'], self.item['Name'])
elif self.item["Type"] == "MusicAlbum" and "AlbumArtist" in self.item:
return "%s: %s" % (self.item["AlbumArtist"], self.item["Name"])
elif self.item['Type'] == 'Audio' and self.item.get('Artists'):
return "%s: %s" % (self.item['Artists'][0], self.item['Name'])
elif self.item["Type"] == "Audio" and self.item.get("Artists"):
return "%s: %s" % (self.item["Artists"][0], self.item["Name"])
return self.item['Name']
return self.item["Name"]
def get_actors(self):
cast = []
if 'People' in self.item:
self.get_people_artwork(self.item['People'])
if "People" in self.item:
self.get_people_artwork(self.item["People"])
for person in self.item['People']:
for person in self.item["People"]:
if person['Type'] == "Actor":
cast.append({
'name': person['Name'],
'role': person.get('Role', "Unknown"),
'order': len(cast) + 1,
'thumbnail': person['imageurl']
})
if person["Type"] == "Actor":
cast.append(
{
"name": person["Name"],
"role": person.get("Role", "Unknown"),
"order": len(cast) + 1,
"thumbnail": person["imageurl"],
}
)
return cast
def media_streams(self, video, audio, subtitles):
return {
'video': video or [],
'audio': audio or [],
'subtitle': subtitles or []
}
return {"video": video or [], "audio": audio or [], "subtitle": subtitles or []}
def video_streams(self, tracks, container=None):
if container:
container = container.split(',')[0]
container = container.split(",")[0]
for track in tracks:
if "DvProfile" in track:
track['hdrtype'] = "dolbyvision"
elif track.get('VideoRangeType', '') in ["HDR10", "HDR10Plus"]:
track['hdrtype'] = "hdr10"
elif "HLG" in track.get('VideoRangeType', ''):
track['hdrtype'] = "hlg"
track["hdrtype"] = "dolbyvision"
elif track.get("VideoRangeType", "") in ["HDR10", "HDR10Plus"]:
track["hdrtype"] = "hdr10"
elif "HLG" in track.get("VideoRangeType", ""):
track["hdrtype"] = "hlg"
track.update({
'hdrtype': track.get('hdrtype', "").lower(),
'codec': track.get('Codec', "").lower(),
'profile': track.get('Profile', "").lower(),
'height': track.get('Height'),
'width': track.get('Width'),
'3d': self.item.get('Video3DFormat'),
'aspect': 1.85
})
track.update(
{
"hdrtype": track.get("hdrtype", "").lower(),
"codec": track.get("Codec", "").lower(),
"profile": track.get("Profile", "").lower(),
"height": track.get("Height"),
"width": track.get("Width"),
"3d": self.item.get("Video3DFormat"),
"aspect": 1.85,
}
)
if "msmpeg4" in track['codec']:
track['codec'] = "divx"
if "msmpeg4" in track["codec"]:
track["codec"] = "divx"
elif "mpeg4" in track['codec'] and ("simple profile" in track['profile'] or not track['profile']):
track['codec'] = "xvid"
elif "mpeg4" in track["codec"] and (
"simple profile" in track["profile"] or not track["profile"]
):
track["codec"] = "xvid"
elif "h264" in track['codec'] and container in ('mp4', 'mov', 'm4v'):
track['codec'] = "avc1"
elif "h264" in track["codec"] and container in ("mp4", "mov", "m4v"):
track["codec"] = "avc1"
try:
width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':')
track['aspect'] = round(float(width) / float(height), 6)
width, height = self.item.get(
"AspectRatio", track.get("AspectRatio", "0")
).split(":")
track["aspect"] = round(float(width) / float(height), 6)
except (ValueError, ZeroDivisionError):
if track['width'] and track['height']:
track['aspect'] = round(float(track['width'] / track['height']), 6)
if track["width"] and track["height"]:
track["aspect"] = round(float(track["width"] / track["height"]), 6)
track['duration'] = self.get_runtime()
track["duration"] = self.get_runtime()
return tracks
@ -115,28 +117,30 @@ class API(object):
for track in tracks:
track.update({
'codec': track.get('Codec', "").lower(),
'profile': track.get('Profile', "").lower(),
'channels': track.get('Channels'),
'language': track.get('Language')
})
track.update(
{
"codec": track.get("Codec", "").lower(),
"profile": track.get("Profile", "").lower(),
"channels": track.get("Channels"),
"language": track.get("Language"),
}
)
if "dts-hd ma" in track['profile']:
track['codec'] = "dtshd_ma"
if "dts-hd ma" in track["profile"]:
track["codec"] = "dtshd_ma"
elif "dts-hd hra" in track['profile']:
track['codec'] = "dtshd_hra"
elif "dts-hd hra" in track["profile"]:
track["codec"] = "dtshd_hra"
return tracks
def get_runtime(self):
try:
runtime = self.item['RunTimeTicks'] / 10000000.0
runtime = self.item["RunTimeTicks"] / 10000000.0
except KeyError:
runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0
runtime = self.item.get("CumulativeRunTimeTicks", 0) / 10000000.0
return runtime
@ -146,7 +150,7 @@ class API(object):
resume = 0
if resume_seconds:
resume = round(float(resume_seconds), 6)
jumpback = int(settings('resumeJumpBack'))
jumpback = int(settings("resumeJumpBack"))
if resume > jumpback:
# To avoid negative bookmark
resume = resume - jumpback
@ -156,25 +160,25 @@ class API(object):
def validate_studio(self, studio_name):
# Convert studio for Kodi to properly detect them
studios = {
'abc (us)': "ABC",
'fox (us)': "FOX",
'mtv (us)': "MTV",
'showcase (ca)': "Showcase",
'wgn america': "WGN",
'bravo (us)': "Bravo",
'tnt (us)': "TNT",
'comedy central': "Comedy Central (US)"
"abc (us)": "ABC",
"fox (us)": "FOX",
"mtv (us)": "MTV",
"showcase (ca)": "Showcase",
"wgn america": "WGN",
"bravo (us)": "Bravo",
"tnt (us)": "TNT",
"comedy central": "Comedy Central (US)",
}
return studios.get(studio_name.lower(), studio_name)
def get_overview(self, overview=None):
overview = overview or self.item.get('Overview')
overview = overview or self.item.get("Overview")
if not overview:
return
overview = overview.replace("\"", "\'")
overview = overview.replace('"', "'")
overview = overview.replace("\n", "[CR]")
overview = overview.replace("\r", " ")
overview = overview.replace("<br>", "[CR]")
@ -183,7 +187,7 @@ class API(object):
def get_mpaa(self, rating=None):
mpaa = rating or self.item.get('OfficialRating', "")
mpaa = rating or self.item.get("OfficialRating", "")
if mpaa in ("NR", "UR"):
# Kodi seems to not like NR, but will accept Not Rated
@ -197,112 +201,123 @@ class API(object):
def get_file_path(self, path=None):
if path is None:
path = self.item.get('Path')
path = self.item.get("Path")
if not path:
return ""
if path.startswith('\\\\'):
path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/")
if path.startswith("\\\\"):
path = (
path.replace("\\\\", "smb://", 1)
.replace("\\\\", "\\")
.replace("\\", "/")
)
if 'Container' in self.item:
if "Container" in self.item:
if self.item['Container'] == 'dvd':
if self.item["Container"] == "dvd":
path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path
elif self.item['Container'] == 'bluray':
elif self.item["Container"] == "bluray":
path = "%s/BDMV/index.bdmv" % path
path = path.replace('\\\\', "\\")
path = path.replace("\\\\", "\\")
if '\\' in path:
path = path.replace('/', "\\")
if "\\" in path:
path = path.replace("/", "\\")
if '://' in path:
protocol = path.split('://')[0]
if "://" in path:
protocol = path.split("://")[0]
path = path.replace(protocol, protocol.lower())
return path
def get_user_artwork(self, user_id):
''' Get jellyfin user profile picture.
'''
"""Get jellyfin user profile picture."""
return "%s/Users/%s/Images/Primary?Format=original" % (self.server, user_id)
def get_people_artwork(self, people):
''' Get people (actor, director, etc) artwork.
'''
"""Get people (actor, director, etc) artwork."""
for person in people:
if 'PrimaryImageTag' in person:
if "PrimaryImageTag" in person:
query = "&MaxWidth=400&MaxHeight=400&Index=0"
person['imageurl'] = self.get_artwork(person['Id'], "Primary", person['PrimaryImageTag'], query)
person["imageurl"] = self.get_artwork(
person["Id"], "Primary", person["PrimaryImageTag"], query
)
else:
person['imageurl'] = None
person["imageurl"] = None
return people
def get_all_artwork(self, obj, parent_info=False):
"""Get all artwork possible. If parent_info is True,
it will fill missing artwork with parent artwork.
''' Get all artwork possible. If parent_info is True,
it will fill missing artwork with parent artwork.
obj is from objects.Objects().map(item, 'Artwork')
'''
obj is from objects.Objects().map(item, 'Artwork')
"""
query = ""
all_artwork = {
'Primary': "",
'BoxRear': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
"Primary": "",
"BoxRear": "",
"Art": "",
"Banner": "",
"Logo": "",
"Thumb": "",
"Disc": "",
"Backdrop": [],
}
if settings('compressArt.bool'):
if settings("compressArt.bool"):
query = "&Quality=90"
if not settings('enableCoverArt.bool'):
if not settings("enableCoverArt.bool"):
query += "&EnableImageEnhancers=false"
art_maxheight = [360, 480, 600, 720, 1080, -1]
maxheight = art_maxheight[int(settings('maxArtResolution') or 5)]
maxheight = art_maxheight[int(settings("maxArtResolution") or 5)]
if maxheight != -1:
query += "&MaxHeight=%d" % maxheight
all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query)
all_artwork["Backdrop"] = self.get_backdrops(
obj["Id"], obj["BackdropTags"] or [], query
)
for artwork in (obj['Tags'] or []):
all_artwork[artwork] = self.get_artwork(obj['Id'], artwork, obj['Tags'][artwork], query)
for artwork in obj["Tags"] or []:
all_artwork[artwork] = self.get_artwork(
obj["Id"], artwork, obj["Tags"][artwork], query
)
if parent_info:
if not all_artwork['Backdrop'] and obj['ParentBackdropId']:
all_artwork['Backdrop'] = self.get_backdrops(obj['ParentBackdropId'], obj['ParentBackdropTags'], query)
if not all_artwork["Backdrop"] and obj["ParentBackdropId"]:
all_artwork["Backdrop"] = self.get_backdrops(
obj["ParentBackdropId"], obj["ParentBackdropTags"], query
)
for art in ('Logo', 'Art', 'Thumb'):
if not all_artwork[art] and obj['Parent%sId' % art]:
all_artwork[art] = self.get_artwork(obj['Parent%sId' % art], art, obj['Parent%sTag' % art], query)
for art in ("Logo", "Art", "Thumb"):
if not all_artwork[art] and obj["Parent%sId" % art]:
all_artwork[art] = self.get_artwork(
obj["Parent%sId" % art], art, obj["Parent%sTag" % art], query
)
if obj.get('SeriesTag'):
all_artwork['Series.Primary'] = self.get_artwork(obj['SeriesId'], "Primary", obj['SeriesTag'], query)
if obj.get("SeriesTag"):
all_artwork["Series.Primary"] = self.get_artwork(
obj["SeriesId"], "Primary", obj["SeriesTag"], query
)
if not all_artwork['Primary']:
all_artwork['Primary'] = all_artwork['Series.Primary']
if not all_artwork["Primary"]:
all_artwork["Primary"] = all_artwork["Series.Primary"]
elif not all_artwork['Primary'] and obj.get('AlbumId'):
all_artwork['Primary'] = self.get_artwork(obj['AlbumId'], "Primary", obj['AlbumTag'], query)
elif not all_artwork["Primary"] and obj.get("AlbumId"):
all_artwork["Primary"] = self.get_artwork(
obj["AlbumId"], "Primary", obj["AlbumTag"], query
)
return all_artwork
def get_backdrops(self, item_id, tags, query=None):
''' Get backdrops based of "BackdropImageTags" in the jellyfin object.
'''
"""Get backdrops based of "BackdropImageTags" in the jellyfin object."""
backdrops = []
if item_id is None:
@ -310,15 +325,19 @@ class API(object):
for index, tag in enumerate(tags):
artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, (query or ""))
artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (
self.server,
item_id,
index,
tag,
(query or ""),
)
backdrops.append(artwork)
return backdrops
def get_artwork(self, item_id, image, tag=None, query=None):
''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc
'''
"""Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc"""
if item_id is None:
return ""

View File

@ -9,7 +9,11 @@ import warnings
class HTTPException(Exception):
# Jellyfin HTTP exception
def __init__(self, status, message):
warnings.warn(f'{self.__class__.__name__} will be deprecated.', DeprecationWarning, stacklevel=2)
warnings.warn(
f"{self.__class__.__name__} will be deprecated.",
DeprecationWarning,
stacklevel=2,
)
self.status = status
self.message = message
@ -26,4 +30,5 @@ class PathValidationException(Exception):
TODO: Investigate the usage of this to see if it can be done better.
"""
pass

View File

@ -6,6 +6,7 @@ class LazyLogger(object):
"""`helper.loghandler.getLogger()` is used everywhere.
This class helps to avoid import errors.
"""
__logger = None
__logger_name = None
@ -15,5 +16,6 @@ class LazyLogger(object):
def __getattr__(self, name):
if self.__logger is None:
from .loghandler import getLogger
self.__logger = getLogger(self.__logger_name)
return getattr(self.__logger, name)

View File

@ -16,8 +16,8 @@ from .utils import translate_path
##################################################################################################
__addon__ = xbmcaddon.Addon(id='plugin.video.jellyfin')
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
__addon__ = xbmcaddon.Addon(id="plugin.video.jellyfin")
__pluginpath__ = translate_path(__addon__.getAddonInfo("path"))
##################################################################################################
@ -36,17 +36,17 @@ class LogHandler(logging.StreamHandler):
logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter())
self.sensitive = {'Token': [], 'Server': []}
self.sensitive = {"Token": [], "Server": []}
for server in database.get_credentials()['Servers']:
for server in database.get_credentials()["Servers"]:
if server.get('AccessToken'):
self.sensitive['Token'].append(server['AccessToken'])
if server.get("AccessToken"):
self.sensitive["Token"].append(server["AccessToken"])
if server.get('address'):
self.sensitive['Server'].append(server['address'].split('://')[1])
if server.get("address"):
self.sensitive["Server"].append(server["address"].split("://")[1])
self.mask_info = settings('maskInfo.bool')
self.mask_info = settings("maskInfo.bool")
if kodi_version() > 18:
self.level = xbmc.LOGINFO
@ -59,10 +59,10 @@ class LogHandler(logging.StreamHandler):
string = self.format(record)
if self.mask_info:
for server in self.sensitive['Server']:
for server in self.sensitive["Server"]:
string = string.replace(server or "{server}", "{jellyfin-server}")
for token in self.sensitive['Token']:
for token in self.sensitive["Token"]:
string = string.replace(token or "{token}", "{jellyfin-token}")
xbmc.log(string, level=self.level)
@ -74,10 +74,10 @@ class LogHandler(logging.StreamHandler):
logging.ERROR: 0,
logging.WARNING: 0,
logging.INFO: 1,
logging.DEBUG: 2
logging.DEBUG: 2,
}
try:
log_level = int(settings('logLevel'))
log_level = int(settings("logLevel"))
except ValueError:
log_level = 2 # If getting settings fail, we probably want debug logging.
@ -86,7 +86,9 @@ class LogHandler(logging.StreamHandler):
class MyFormatter(logging.Formatter):
def __init__(self, fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'):
def __init__(
self, fmt="%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s"
):
logging.Formatter.__init__(self, fmt)
def format(self, record):
@ -116,14 +118,14 @@ class MyFormatter(logging.Formatter):
res.append(o)
return ''.join(res)
return "".join(res)
def _gen_rel_path(self, record):
if record.pathname:
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
__LOGGER = logging.getLogger('JELLYFIN')
__LOGGER = logging.getLogger("JELLYFIN")
for handler in __LOGGER.handlers:
__LOGGER.removeHandler(handler)

View File

@ -26,81 +26,81 @@ class Transcode(object):
Disabled = 3
MediaDefault = 4
#################################################################################################
def set_properties(item, method, server_id=None):
"""Set all properties for playback detection."""
info = item.get("PlaybackInfo") or {}
''' Set all properties for playback detection.
'''
info = item.get('PlaybackInfo') or {}
current = window("jellyfin_play.json") or []
current.append(
{
"Type": item["Type"],
"Id": item["Id"],
"Path": info["Path"],
"PlayMethod": method,
"PlayOption": "Addon" if info.get("PlaySessionId") else "Native",
"MediaSourceId": info.get("MediaSourceId", item["Id"]),
"Runtime": item.get("RunTimeTicks"),
"PlaySessionId": info.get("PlaySessionId", str(uuid4()).replace("-", "")),
"ServerId": server_id,
"DeviceId": client.get_device_id(),
"SubsMapping": info.get("Subtitles"),
"AudioStreamIndex": info.get("AudioStreamIndex"),
"SubtitleStreamIndex": info.get("SubtitleStreamIndex"),
"CurrentPosition": info.get("CurrentPosition"),
"CurrentEpisode": info.get("CurrentEpisode"),
}
)
current = window('jellyfin_play.json') or []
current.append({
'Type': item['Type'],
'Id': item['Id'],
'Path': info['Path'],
'PlayMethod': method,
'PlayOption': 'Addon' if info.get('PlaySessionId') else 'Native',
'MediaSourceId': info.get('MediaSourceId', item['Id']),
'Runtime': item.get('RunTimeTicks'),
'PlaySessionId': info.get('PlaySessionId', str(uuid4()).replace("-", "")),
'ServerId': server_id,
'DeviceId': client.get_device_id(),
'SubsMapping': info.get('Subtitles'),
'AudioStreamIndex': info.get('AudioStreamIndex'),
'SubtitleStreamIndex': info.get('SubtitleStreamIndex'),
'CurrentPosition': info.get('CurrentPosition'),
'CurrentEpisode': info.get('CurrentEpisode')
})
window('jellyfin_play.json', current)
window("jellyfin_play.json", current)
class PlayUtils(object):
def __init__(self, item, force_transcode=False, server_id=None, server=None, api_client=None):
''' Item will be updated with the property PlaybackInfo, which
holds all the playback information.
'''
def __init__(
self, item, force_transcode=False, server_id=None, server=None, api_client=None
):
"""Item will be updated with the property PlaybackInfo, which
holds all the playback information.
"""
self.item = item
self.item['PlaybackInfo'] = {}
self.item["PlaybackInfo"] = {}
self.api_client = api_client
self.info = {
'ServerId': server_id,
'ServerAddress': server,
'ForceTranscode': force_transcode,
'Token': api_client.config.data['auth.token']
"ServerId": server_id,
"ServerAddress": server,
"ForceTranscode": force_transcode,
"Token": api_client.config.data["auth.token"],
}
def get_sources(self, source_id=None):
''' Return sources based on the optional source_id or the device profile.
'''
info = self.api_client.get_play_info(self.item['Id'], self.get_device_profile())
"""Return sources based on the optional source_id or the device profile."""
info = self.api_client.get_play_info(self.item["Id"], self.get_device_profile())
LOG.info(info)
self.info['PlaySessionId'] = info['PlaySessionId']
self.info["PlaySessionId"] = info["PlaySessionId"]
sources = []
if not info.get('MediaSources'):
if not info.get("MediaSources"):
LOG.info("No MediaSources found.")
elif source_id:
for source in info:
if source['Id'] == source_id:
if source["Id"] == source_id:
sources.append(source)
break
elif not self.is_selection(info) or len(info['MediaSources']) == 1:
elif not self.is_selection(info) or len(info["MediaSources"]) == 1:
LOG.info("Skip source selection.")
sources.append(info['MediaSources'][0])
sources.append(info["MediaSources"][0])
else:
sources.extend([x for x in info['MediaSources']])
sources.extend([x for x in info["MediaSources"]])
return sources
@ -110,7 +110,7 @@ class PlayUtils(object):
selection = []
for source in sources:
selection.append(source.get('Name', "na"))
selection.append(source.get("Name", "na"))
resp = dialog("select", translate(33130), selection)
@ -127,25 +127,23 @@ class PlayUtils(object):
return source
def is_selection(self, sources):
''' Do not allow source selection for.
'''
if self.item['MediaType'] != 'Video':
"""Do not allow source selection for."""
if self.item["MediaType"] != "Video":
LOG.debug("MediaType is not a video.")
return False
elif self.item['Type'] == 'TvChannel':
elif self.item["Type"] == "TvChannel":
LOG.debug("TvChannel detected.")
return False
elif len(sources) == 1 and sources[0]['Type'] == 'Placeholder':
elif len(sources) == 1 and sources[0]["Type"] == "Placeholder":
LOG.debug("Placeholder detected.")
return False
elif 'SourceType' in self.item and self.item['SourceType'] != 'Library':
elif "SourceType" in self.item and self.item["SourceType"] != "Library":
LOG.debug("SourceType not from library.")
return False
@ -156,7 +154,7 @@ class PlayUtils(object):
self.direct_play(source)
if xbmcvfs.exists(self.info['Path']):
if xbmcvfs.exists(self.info["Path"]):
LOG.info("Path exists.")
return True
@ -167,7 +165,7 @@ class PlayUtils(object):
def is_strm(self, source):
if source.get('Container') == 'strm' or self.item['Path'].endswith('.strm'):
if source.get("Container") == "strm" or self.item["Path"].endswith(".strm"):
LOG.info("strm detected")
return True
@ -175,31 +173,37 @@ class PlayUtils(object):
return False
def get(self, source, audio=None, subtitle=None):
"""The server returns sources based on the MaxStreamingBitrate value and other filters.
prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working?
"""
self.info["MediaSourceId"] = source["Id"]
''' The server returns sources based on the MaxStreamingBitrate value and other filters.
prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working?
'''
self.info['MediaSourceId'] = source['Id']
if source.get("RequiresClosing"):
if source.get('RequiresClosing'):
"""Server returning live tv stream for direct play is hardcoded with 127.0.0.1."""
self.info["LiveStreamId"] = source["LiveStreamId"]
source["SupportsDirectPlay"] = False
source["Protocol"] = "LiveTV"
''' Server returning live tv stream for direct play is hardcoded with 127.0.0.1.
'''
self.info['LiveStreamId'] = source['LiveStreamId']
source['SupportsDirectPlay'] = False
source['Protocol'] = "LiveTV"
if self.info["ForceTranscode"]:
if self.info['ForceTranscode']:
source["SupportsDirectPlay"] = False
source["SupportsDirectStream"] = False
source['SupportsDirectPlay'] = False
source['SupportsDirectStream'] = False
if source.get('Protocol') == 'Http' or source['SupportsDirectPlay'] and (self.is_strm(source) or not settings('playFromStream.bool') and self.is_file_exists(source)):
if (
source.get("Protocol") == "Http"
or source["SupportsDirectPlay"]
and (
self.is_strm(source)
or not settings("playFromStream.bool")
and self.is_file_exists(source)
)
):
LOG.info("--[ direct play ]")
self.direct_play(source)
elif source['SupportsDirectStream'] or source['SupportsDirectPlay']:
elif source["SupportsDirectStream"] or source["SupportsDirectPlay"]:
LOG.info("--[ direct stream ]")
self.direct_url(source)
@ -208,158 +212,209 @@ class PlayUtils(object):
LOG.info("--[ transcode ]")
self.transcode(source, audio, subtitle)
self.info['AudioStreamIndex'] = self.info.get('AudioStreamIndex') or source.get('DefaultAudioStreamIndex')
self.info['SubtitleStreamIndex'] = self.info.get('SubtitleStreamIndex') or source.get('DefaultSubtitleStreamIndex')
self.item['PlaybackInfo'].update(self.info)
self.info["AudioStreamIndex"] = self.info.get("AudioStreamIndex") or source.get(
"DefaultAudioStreamIndex"
)
self.info["SubtitleStreamIndex"] = self.info.get(
"SubtitleStreamIndex"
) or source.get("DefaultSubtitleStreamIndex")
self.item["PlaybackInfo"].update(self.info)
API = api.API(self.item, self.info['ServerAddress'])
window('jellyfinfilename', value=API.get_file_path(source.get('Path')))
API = api.API(self.item, self.info["ServerAddress"])
window("jellyfinfilename", value=API.get_file_path(source.get("Path")))
def live_stream(self, source):
''' Get live stream media info.
'''
info = self.api_client.get_live_stream(self.item['Id'], self.info['PlaySessionId'], source['OpenToken'], self.get_device_profile())
"""Get live stream media info."""
info = self.api_client.get_live_stream(
self.item["Id"],
self.info["PlaySessionId"],
source["OpenToken"],
self.get_device_profile(),
)
LOG.info(info)
if info['MediaSource'].get('RequiresClosing'):
self.info['LiveStreamId'] = source['LiveStreamId']
if info["MediaSource"].get("RequiresClosing"):
self.info["LiveStreamId"] = source["LiveStreamId"]
return info['MediaSource']
return info["MediaSource"]
def transcode(self, source, audio=None, subtitle=None):
if 'TranscodingUrl' not in source:
if "TranscodingUrl" not in source:
raise Exception("use get_sources to get transcoding url")
self.info['Method'] = "Transcode"
self.info["Method"] = "Transcode"
if self.item['MediaType'] == 'Video':
base, params = source['TranscodingUrl'].split('?')
url_parsed = params.split('&')
manual_tracks = ''
if self.item["MediaType"] == "Video":
base, params = source["TranscodingUrl"].split("?")
url_parsed = params.split("&")
manual_tracks = ""
# manual bitrate
url_parsed = [p for p in url_parsed if 'AudioBitrate' not in p and 'VideoBitrate' not in p]
url_parsed = [
p
for p in url_parsed
if "AudioBitrate" not in p and "VideoBitrate" not in p
]
if settings('skipDialogTranscode') != Transcode.Enabled and source.get('MediaStreams'):
if settings("skipDialogTranscode") != Transcode.Enabled and source.get(
"MediaStreams"
):
# manual tracks
url_parsed = [p for p in url_parsed if 'AudioStreamIndex' not in p and 'SubtitleStreamIndex' not in p]
url_parsed = [
p
for p in url_parsed
if "AudioStreamIndex" not in p and "SubtitleStreamIndex" not in p
]
manual_tracks = self.get_audio_subs(source, audio, subtitle)
audio_bitrate = self.get_transcoding_audio_bitrate()
video_bitrate = self.get_max_bitrate() - audio_bitrate
params = "%s%s" % ('&'.join(url_parsed), manual_tracks)
params += "&VideoBitrate=%s&AudioBitrate=%s" % (video_bitrate, audio_bitrate)
params = "%s%s" % ("&".join(url_parsed), manual_tracks)
params += "&VideoBitrate=%s&AudioBitrate=%s" % (
video_bitrate,
audio_bitrate,
)
video_type = 'live' if source['Protocol'] == 'LiveTV' else 'master'
base = base.replace('stream' if 'stream' in base else 'master', video_type, 1)
self.info['Path'] = "%s%s?%s" % (self.info['ServerAddress'], base, params)
self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
video_type = "live" if source["Protocol"] == "LiveTV" else "master"
base = base.replace(
"stream" if "stream" in base else "master", video_type, 1
)
self.info["Path"] = "%s%s?%s" % (self.info["ServerAddress"], base, params)
self.info["Path"] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
else:
self.info['Path'] = "%s/%s" % (self.info['ServerAddress'], source['TranscodingUrl'])
self.info["Path"] = "%s/%s" % (
self.info["ServerAddress"],
source["TranscodingUrl"],
)
return self.info['Path']
return self.info["Path"]
def direct_play(self, source):
API = api.API(self.item, self.info['ServerAddress'])
self.info['Method'] = "DirectPlay"
self.info['Path'] = API.get_file_path(source.get('Path'))
API = api.API(self.item, self.info["ServerAddress"])
self.info["Method"] = "DirectPlay"
self.info["Path"] = API.get_file_path(source.get("Path"))
return self.info['Path']
return self.info["Path"]
def direct_url(self, source):
self.info['Method'] = "DirectStream"
self.info["Method"] = "DirectStream"
if self.item['Type'] == "Audio":
self.info['Path'] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % (
self.info['ServerAddress'],
self.item['Id'],
source.get('Container', "mp4").split(',')[0],
self.info['Token']
if self.item["Type"] == "Audio":
self.info["Path"] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % (
self.info["ServerAddress"],
self.item["Id"],
source.get("Container", "mp4").split(",")[0],
self.info["Token"],
)
else:
self.info['Path'] = "%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" % (
self.info['ServerAddress'],
self.item['Id'],
source['Id'],
self.info['Token']
self.info["Path"] = (
"%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s"
% (
self.info["ServerAddress"],
self.item["Id"],
source["Id"],
self.info["Token"],
)
)
return self.info['Path']
return self.info["Path"]
def get_max_bitrate(self):
''' Get the video quality based on add-on settings.
Max bit rate supported by server: 2147483 (max signed 32bit integer)
'''
bitrate = [500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000,
7000, 8000, 9000, 10000, 12000, 14000, 16000, 18000,
20000, 25000, 30000, 35000, 40000, 100000, 1000000, 2147483]
return bitrate[int(settings('maxBitrate') or 24)] * 1000
"""Get the video quality based on add-on settings.
Max bit rate supported by server: 2147483 (max signed 32bit integer)
"""
bitrate = [
500,
1000,
1500,
2000,
2500,
3000,
4000,
5000,
6000,
7000,
8000,
9000,
10000,
12000,
14000,
16000,
18000,
20000,
25000,
30000,
35000,
40000,
100000,
1000000,
2147483,
]
return bitrate[int(settings("maxBitrate") or 24)] * 1000
def get_resolution(self):
return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight'))
return int(xbmc.getInfoLabel("System.ScreenWidth")), int(
xbmc.getInfoLabel("System.ScreenHeight")
)
def get_directplay_video_codec(self):
codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1', 'vp9', 'av1']
codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1", "vp9", "av1"]
if settings('transcode_h265.bool'):
codecs.remove('hevc')
codecs.remove('h265')
if settings("transcode_h265.bool"):
codecs.remove("hevc")
codecs.remove("h265")
if settings('transcode_mpeg2.bool'):
codecs.remove('mpeg2video')
if settings("transcode_mpeg2.bool"):
codecs.remove("mpeg2video")
if settings('transcode_vc1.bool'):
codecs.remove('vc1')
if settings("transcode_vc1.bool"):
codecs.remove("vc1")
if settings('transcode_vp9.bool'):
codecs.remove('vp9')
if settings("transcode_vp9.bool"):
codecs.remove("vp9")
if settings('transcode_av1.bool'):
codecs.remove('av1')
if settings("transcode_av1.bool"):
codecs.remove("av1")
return ','.join(codecs)
return ",".join(codecs)
def get_transcoding_video_codec(self):
codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1']
codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1"]
if settings('transcode_h265.bool'):
codecs.remove('hevc')
codecs.remove('h265')
if settings("transcode_h265.bool"):
codecs.remove("hevc")
codecs.remove("h265")
else:
if settings('videoPreferredCodec') == 'H265/HEVC':
codecs.insert(2, codecs.pop(codecs.index('h264')))
if settings("videoPreferredCodec") == "H265/HEVC":
codecs.insert(2, codecs.pop(codecs.index("h264")))
if settings('transcode_mpeg2.bool'):
codecs.remove('mpeg2video')
if settings("transcode_mpeg2.bool"):
codecs.remove("mpeg2video")
if settings('transcode_vc1.bool'):
codecs.remove('vc1')
if settings("transcode_vc1.bool"):
codecs.remove("vc1")
return ','.join(codecs)
return ",".join(codecs)
def get_transcoding_audio_codec(self):
codecs = ['aac', 'mp3', 'ac3', 'opus', 'flac', 'vorbis']
codecs = ["aac", "mp3", "ac3", "opus", "flac", "vorbis"]
preferred = settings('audioPreferredCodec').lower()
preferred = settings("audioPreferredCodec").lower()
if preferred in codecs:
codecs.insert(0, codecs.pop(codecs.index(preferred)))
return ','.join(codecs)
return ",".join(codecs)
def get_transcoding_audio_bitrate(self):
bitrate = [96, 128, 160, 192, 256, 320, 384]
return bitrate[int(settings('audioBitrate') or 6)] * 1000
return bitrate[int(settings("audioBitrate") or 6)] * 1000
def get_device_profile(self):
''' Get device profile based on the add-on settings.
'''
"""Get device profile based on the add-on settings."""
profile = {
"Name": "Kodi",
"MaxStaticBitrate": self.get_max_bitrate(),
@ -372,154 +427,96 @@ class PlayUtils(object):
"Container": "m3u8",
"AudioCodec": self.get_transcoding_audio_codec(),
"VideoCodec": self.get_transcoding_video_codec(),
"MaxAudioChannels": settings('audioMaxChannels')
"MaxAudioChannels": settings("audioMaxChannels"),
},
{
"Type": "Audio"
},
{
"Type": "Photo",
"Container": "jpeg"
}
{"Type": "Audio"},
{"Type": "Photo", "Container": "jpeg"},
],
"DirectPlayProfiles": [
{
"Type": "Video",
"VideoCodec": self.get_directplay_video_codec()
},
{
"Type": "Audio"
},
{
"Type": "Photo"
}
{"Type": "Video", "VideoCodec": self.get_directplay_video_codec()},
{"Type": "Audio"},
{"Type": "Photo"},
],
"ResponseProfiles": [],
"ContainerProfiles": [],
"CodecProfiles": [],
"SubtitleProfiles": [
{
"Format": "srt",
"Method": "External"
},
{
"Format": "srt",
"Method": "Embed"
},
{
"Format": "ass",
"Method": "External"
},
{
"Format": "ass",
"Method": "Embed"
},
{
"Format": "sub",
"Method": "Embed"
},
{
"Format": "sub",
"Method": "External"
},
{
"Format": "ssa",
"Method": "Embed"
},
{
"Format": "ssa",
"Method": "External"
},
{
"Format": "smi",
"Method": "Embed"
},
{
"Format": "smi",
"Method": "External"
},
{
"Format": "pgssub",
"Method": "Embed"
},
{
"Format": "pgssub",
"Method": "External"
},
{
"Format": "dvdsub",
"Method": "Embed"
},
{
"Format": "dvdsub",
"Method": "External"
},
{
"Format": "pgs",
"Method": "Embed"
},
{
"Format": "pgs",
"Method": "External"
}
]
{"Format": "srt", "Method": "External"},
{"Format": "srt", "Method": "Embed"},
{"Format": "ass", "Method": "External"},
{"Format": "ass", "Method": "Embed"},
{"Format": "sub", "Method": "Embed"},
{"Format": "sub", "Method": "External"},
{"Format": "ssa", "Method": "Embed"},
{"Format": "ssa", "Method": "External"},
{"Format": "smi", "Method": "Embed"},
{"Format": "smi", "Method": "External"},
{"Format": "pgssub", "Method": "Embed"},
{"Format": "pgssub", "Method": "External"},
{"Format": "dvdsub", "Method": "Embed"},
{"Format": "dvdsub", "Method": "External"},
{"Format": "pgs", "Method": "Embed"},
{"Format": "pgs", "Method": "External"},
],
}
if settings('transcodeHi10P.bool'):
profile['CodecProfiles'].append(
if settings("transcodeHi10P.bool"):
profile["CodecProfiles"].append(
{
'Type': 'Video',
'codec': 'h264',
'Conditions': [
"Type": "Video",
"codec": "h264",
"Conditions": [
{
'Condition': "LessThanEqual",
'Property': "VideoBitDepth",
'Value': "8"
"Condition": "LessThanEqual",
"Property": "VideoBitDepth",
"Value": "8",
}
]
],
}
)
if settings('transcode_h265_rext.bool'):
profile['CodecProfiles'].append(
if settings("transcode_h265_rext.bool"):
profile["CodecProfiles"].append(
{
'Type': 'Video',
'codec': 'h265,hevc',
'Conditions': [
"Type": "Video",
"codec": "h265,hevc",
"Conditions": [
{
'Condition': "EqualsAny",
'Property': "VideoProfile",
'Value': "main|main 10"
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": "main|main 10",
}
]
],
}
)
if self.info['ForceTranscode']:
profile['DirectPlayProfiles'] = []
if self.info["ForceTranscode"]:
profile["DirectPlayProfiles"] = []
if self.item['Type'] == 'TvChannel':
profile['TranscodingProfiles'].insert(0, {
"Container": "ts",
"Type": "Video",
"AudioCodec": "mp3,aac",
"VideoCodec": "h264",
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "1",
"BreakOnNonKeyFrames": True
})
if self.item["Type"] == "TvChannel":
profile["TranscodingProfiles"].insert(
0,
{
"Container": "ts",
"Type": "Video",
"AudioCodec": "mp3,aac",
"VideoCodec": "h264",
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "1",
"BreakOnNonKeyFrames": True,
},
)
return profile
def set_external_subs(self, source, listitem):
''' Try to download external subs locally, so we can label them.
Since Jellyfin returns all possible tracks together, sort them.
IsTextSubtitleStream if true, is available to download from server.
'''
if not settings('enableExternalSubs.bool') or not source['MediaStreams']:
"""Try to download external subs locally, so we can label them.
Since Jellyfin returns all possible tracks together, sort them.
IsTextSubtitleStream if true, is available to download from server.
"""
if not settings("enableExternalSubs.bool") or not source["MediaStreams"]:
return
subs = []
@ -528,12 +525,19 @@ class PlayUtils(object):
server_settings = self.api_client.get_transcode_settings()
for stream in source['MediaStreams']:
if stream['SupportsExternalStream'] and stream['Type'] == 'Subtitle' and stream['DeliveryMethod'] == 'External':
if not stream['IsExternal'] and not server_settings['EnableSubtitleExtraction']:
for stream in source["MediaStreams"]:
if (
stream["SupportsExternalStream"]
and stream["Type"] == "Subtitle"
and stream["DeliveryMethod"] == "External"
):
if (
not stream["IsExternal"]
and not server_settings["EnableSubtitleExtraction"]
):
continue
index = stream['Index']
index = stream["Index"]
url = self.get_subtitles(source, stream, index)
if url is None:
@ -541,8 +545,12 @@ class PlayUtils(object):
LOG.info("[ subtitles/%s ] %s", index, url)
if 'Language' in stream:
filename = "%s.%s.%s" % (source['Id'], stream['Language'], stream['Codec'])
if "Language" in stream:
filename = "%s.%s.%s" % (
source["Id"],
stream["Language"],
stream["Codec"],
)
try:
subs.append(self.download_external_subs(url, filename))
@ -556,15 +564,16 @@ class PlayUtils(object):
kodi += 1
listitem.setSubtitles(subs)
self.item['PlaybackInfo']['Subtitles'] = mapping
self.item["PlaybackInfo"]["Subtitles"] = mapping
@classmethod
def download_external_subs(cls, src, filename):
''' Download external subtitles to temp folder
to be able to have proper names to streams.
'''
temp = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/")
"""Download external subtitles to temp folder
to be able to have proper names to streams.
"""
temp = translate_path(
"special://profile/addon_data/plugin.video.jellyfin/temp/"
)
if not xbmcvfs.exists(temp):
xbmcvfs.mkdir(temp)
@ -572,59 +581,61 @@ class PlayUtils(object):
path = os.path.join(temp, filename)
try:
response = requests.get(src, stream=True, verify=settings('sslverify.bool'))
response = requests.get(src, stream=True, verify=settings("sslverify.bool"))
response.raise_for_status()
except Exception as error:
LOG.exception(error)
raise
else:
response.encoding = 'utf-8'
with open(path, 'wb') as f:
response.encoding = "utf-8"
with open(path, "wb") as f:
f.write(response.content)
del response
return path
def get_audio_subs(self, source, audio=None, subtitle=None):
"""For transcoding only
Present the list of audio/subs to select from, before playback starts.
''' For transcoding only
Present the list of audio/subs to select from, before playback starts.
Since Jellyfin returns all possible tracks together, sort them.
IsTextSubtitleStream if true, is available to download from server.
'''
Since Jellyfin returns all possible tracks together, sort them.
IsTextSubtitleStream if true, is available to download from server.
"""
prefs = ""
audio_streams = list()
subs_streams = list()
streams = source['MediaStreams']
streams = source["MediaStreams"]
server_settings = self.api_client.get_transcode_settings()
allow_burned_subs = settings('allowBurnedSubs.bool')
allow_burned_subs = settings("allowBurnedSubs.bool")
for stream in streams:
index = stream['Index']
stream_type = stream['Type']
index = stream["Index"]
stream_type = stream["Type"]
if stream_type == 'Audio':
if stream_type == "Audio":
audio_streams.append(index)
elif stream_type == 'Subtitle':
if stream['IsExternal']:
if not stream['SupportsExternalStream'] and not allow_burned_subs:
elif stream_type == "Subtitle":
if stream["IsExternal"]:
if not stream["SupportsExternalStream"] and not allow_burned_subs:
continue
else:
avail_for_extraction = stream['SupportsExternalStream'] and server_settings['EnableSubtitleExtraction']
avail_for_extraction = (
stream["SupportsExternalStream"]
and server_settings["EnableSubtitleExtraction"]
)
if not avail_for_extraction and not allow_burned_subs:
continue
subs_streams.append(index)
skip_dialog = int(settings('skipDialogTranscode') or 0)
skip_dialog = int(settings("skipDialogTranscode") or 0)
def get_track_title(track_index):
return streams[track_index]['DisplayTitle'] or ("Track %s" % track_index)
return streams[track_index]["DisplayTitle"] or ("Track %s" % track_index)
# Select audio stream
audio_selected = None
@ -633,7 +644,7 @@ class PlayUtils(object):
# NOTE: "DefaultAudioStreamIndex" is the default according to Jellyfin.
# The media's default is marked by the "IsDefault" value.
for track_index in audio_streams:
if streams[track_index]['IsDefault']:
if streams[track_index]["IsDefault"]:
audio = track_index
break
@ -648,16 +659,16 @@ class PlayUtils(object):
if resp > -1:
audio_selected = audio_streams[resp]
else:
audio_selected = source['DefaultAudioStreamIndex']
audio_selected = source["DefaultAudioStreamIndex"]
elif audio_streams:
# Only one choice
audio_selected = audio_streams[0]
else:
audio_selected = source['DefaultAudioStreamIndex']
audio_selected = source["DefaultAudioStreamIndex"]
if audio_selected is not None:
self.info['AudioStreamIndex'] = audio_selected
self.info["AudioStreamIndex"] = audio_selected
prefs += "&AudioStreamIndex=%s" % audio_selected
# Select audio stream
@ -665,7 +676,7 @@ class PlayUtils(object):
if skip_dialog == Transcode.MediaDefault:
for track_index in subs_streams:
if streams[track_index]['IsDefault']:
if streams[track_index]["IsDefault"]:
subtitle = track_index
break
@ -673,7 +684,9 @@ class PlayUtils(object):
subtitle_selected = subtitle
elif skip_dialog in (Transcode.Enabled, Transcode.Subtitle) and subs_streams:
selection = list(['No subtitles']) + list(map(get_track_title, subs_streams))
selection = list(["No subtitles"]) + list(
map(get_track_title, subs_streams)
)
resp = dialog("select", translate(33014), selection) - 1
# Possible responses:
# >=0 Subtitle track
@ -686,27 +699,36 @@ class PlayUtils(object):
if subtitle_selected is not None:
server_settings = self.api_client.get_transcode_settings()
stream = streams[track_index]
if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']:
self.info['SubtitleUrl'] = self.get_subtitles(source, stream, subtitle_selected)
self.info['SubtitleStreamIndex'] = subtitle_selected
if (
server_settings["EnableSubtitleExtraction"]
and stream["SupportsExternalStream"]
):
self.info["SubtitleUrl"] = self.get_subtitles(
source, stream, subtitle_selected
)
self.info["SubtitleStreamIndex"] = subtitle_selected
elif allow_burned_subs:
prefs += "&SubtitleStreamIndex=%s" % subtitle_selected
self.info['SubtitleStreamIndex'] = subtitle_selected
self.info["SubtitleStreamIndex"] = subtitle_selected
return prefs
def get_subtitles(self, source, stream, index):
if stream['IsTextSubtitleStream'] and 'DeliveryUrl' in stream and stream['DeliveryUrl'].lower().startswith('/videos'):
url = "%s%s" % (self.info['ServerAddress'], stream['DeliveryUrl'])
if (
stream["IsTextSubtitleStream"]
and "DeliveryUrl" in stream
and stream["DeliveryUrl"].lower().startswith("/videos")
):
url = "%s%s" % (self.info["ServerAddress"], stream["DeliveryUrl"])
else:
url = "%s/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % (
self.info['ServerAddress'],
self.item['Id'],
source['Id'],
self.info["ServerAddress"],
self.item["Id"],
source["Id"],
index,
stream['Codec'],
self.info['Token']
stream["Codec"],
self.info["Token"],
)
return url

View File

@ -15,13 +15,11 @@ LOG = LazyLogger(__name__)
def translate(string):
''' Get add-on string. Returns in unicode.
'''
"""Get add-on string. Returns in unicode."""
if type(string) != int:
string = STRINGS[string]
result = xbmcaddon.Addon('plugin.video.jellyfin').getLocalizedString(string)
result = xbmcaddon.Addon("plugin.video.jellyfin").getLocalizedString(string)
if not result:
result = xbmc.getLocalizedString(string)
@ -30,23 +28,23 @@ def translate(string):
STRINGS = {
'addon_name': 29999,
'playback_mode': 30511,
'empty_user': 30613,
'empty_user_pass': 30608,
'empty_server': 30617,
'network_credentials': 30517,
'invalid_auth': 33009,
'addon_mode': 33036,
'native_mode': 33037,
'cancel': 30606,
'username': 30024,
'password': 30602,
'gathering': 33021,
'boxsets': 30185,
'movies': 30302,
'tvshows': 30305,
'fav_movies': 30180,
'fav_tvshows': 30181,
'fav_episodes': 30182
"addon_name": 29999,
"playback_mode": 30511,
"empty_user": 30613,
"empty_user_pass": 30608,
"empty_server": 30617,
"network_credentials": 30517,
"invalid_auth": 33009,
"addon_mode": 33036,
"native_mode": 33037,
"cancel": 30606,
"username": 30024,
"password": 30602,
"gathering": 33021,
"boxsets": 30185,
"movies": 30302,
"tvshows": 30305,
"fav_movies": 30180,
"fav_tvshows": 30181,
"fav_episodes": 30182,
}

View File

@ -40,62 +40,59 @@ def kodi_version():
else:
default_versionstring = "19.1 (19.1.0) Git:20210509-85e05228b4"
version_string = xbmc.getInfoLabel('System.BuildVersion') or default_versionstring
return int(version_string.split(' ', 1)[0].split('.', 1)[0])
version_string = xbmc.getInfoLabel("System.BuildVersion") or default_versionstring
return int(version_string.split(" ", 1)[0].split(".", 1)[0])
def window(key, value=None, clear=False, window_id=10000):
''' Get or set window properties.
'''
"""Get or set window properties."""
window = xbmcgui.Window(window_id)
if clear:
LOG.debug("--[ window clear: %s ]", key)
window.clearProperty(key.replace('.json', "").replace('.bool', ""))
window.clearProperty(key.replace(".json", "").replace(".bool", ""))
elif value is not None:
if key.endswith('.json'):
if key.endswith(".json"):
key = key.replace('.json', "")
key = key.replace(".json", "")
value = json.dumps(value)
elif key.endswith('.bool'):
elif key.endswith(".bool"):
key = key.replace('.bool', "")
key = key.replace(".bool", "")
value = "true" if value else "false"
window.setProperty(key, value)
else:
result = window.getProperty(key.replace('.json', "").replace('.bool', ""))
result = window.getProperty(key.replace(".json", "").replace(".bool", ""))
if result:
if key.endswith('.json'):
if key.endswith(".json"):
result = json.loads(result)
elif key.endswith('.bool'):
elif key.endswith(".bool"):
result = result in ("true", "1")
return result
def settings(setting, value=None):
''' Get or add add-on settings.
getSetting returns unicode object.
'''
"""Get or add add-on settings.
getSetting returns unicode object.
"""
addon = xbmcaddon.Addon(addon_id())
if value is not None:
if setting.endswith('.bool'):
if setting.endswith(".bool"):
setting = setting.replace('.bool', "")
setting = setting.replace(".bool", "")
value = "true" if value else "false"
addon.setSetting(setting, value)
else:
result = addon.getSetting(setting.replace('.bool', ""))
result = addon.getSetting(setting.replace(".bool", ""))
if result and setting.endswith('.bool'):
if result and setting.endswith(".bool"):
result = result in ("true", "1")
return result
@ -106,9 +103,7 @@ def create_id():
def find(dict, item):
''' Find value in dictionary.
'''
"""Find value in dictionary."""
if item in dict:
return dict[item]
@ -119,9 +114,7 @@ def find(dict, item):
def event(method, data=None, sender=None, hexlify=False):
''' Data is a dictionary.
'''
"""Data is a dictionary."""
data = data or {}
sender = sender or "plugin.video.jellyfin"
@ -132,7 +125,7 @@ def event(method, data=None, sender=None, hexlify=False):
LOG.debug("---[ event: %s/%s ] %s", sender, method, data)
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
xbmc.executebuiltin("NotifyAll(%s, %s, %s)" % (sender, method, data))
def dialog(dialog_type, *args, **kwargs):
@ -140,63 +133,58 @@ def dialog(dialog_type, *args, **kwargs):
d = xbmcgui.Dialog()
if "icon" in kwargs:
kwargs['icon'] = kwargs['icon'].replace(
kwargs["icon"] = kwargs["icon"].replace(
"{jellyfin}",
"special://home/addons/plugin.video.jellyfin/resources/icon.png"
"special://home/addons/plugin.video.jellyfin/resources/icon.png",
)
if "heading" in kwargs:
kwargs['heading'] = kwargs['heading'].replace("{jellyfin}", translate('addon_name'))
kwargs["heading"] = kwargs["heading"].replace(
"{jellyfin}", translate("addon_name")
)
if args:
args = list(args)
args[0] = args[0].replace("{jellyfin}", translate('addon_name'))
args[0] = args[0].replace("{jellyfin}", translate("addon_name"))
types = {
'yesno': d.yesno,
'ok': d.ok,
'notification': d.notification,
'input': d.input,
'select': d.select,
'numeric': d.numeric,
'multi': d.multiselect
"yesno": d.yesno,
"ok": d.ok,
"notification": d.notification,
"input": d.input,
"select": d.select,
"numeric": d.numeric,
"multi": d.multiselect,
}
return types[dialog_type](*args, **kwargs)
def should_stop():
''' Checkpoint during the sync process.
'''
"""Checkpoint during the sync process."""
if xbmc.Monitor().waitForAbort(0.00001):
return True
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
LOG.info("exiiiiitttinggg")
return True
return not window('jellyfin_online.bool')
return not window("jellyfin_online.bool")
def get_screensaver():
''' Get the current screensaver value.
'''
result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"})
"""Get the current screensaver value."""
result = JSONRPC("Settings.getSettingValue").execute(
{"setting": "screensaver.mode"}
)
try:
return result['result']['value']
return result["result"]["value"]
except KeyError:
return ""
def set_screensaver(value):
''' Toggle the screensaver
'''
params = {
'setting': "screensaver.mode",
'value': value
}
result = JSONRPC('Settings.setSettingValue').execute(params)
"""Toggle the screensaver"""
params = {"setting": "screensaver.mode", "value": value}
result = JSONRPC("Settings.setSettingValue").execute(params)
LOG.info("---[ screensaver/%s ] %s", value, result)
@ -215,12 +203,12 @@ class JSONRPC(object):
def _query(self):
query = {
'jsonrpc': self.jsonrpc_version,
'id': self.id,
'method': self.method,
"jsonrpc": self.jsonrpc_version,
"id": self.id,
"method": self.method,
}
if self.params is not None:
query['params'] = self.params
query["params"] = self.params
return json.dumps(query)
@ -231,66 +219,68 @@ class JSONRPC(object):
def validate(path):
''' Verify if path is accessible.
'''
if window('jellyfin_pathverified.bool'):
"""Verify if path is accessible."""
if window("jellyfin_pathverified.bool"):
return True
if not xbmcvfs.exists(path):
LOG.info("Could not find %s", path)
if dialog("yesno", "{jellyfin}", "%s %s. %s" % (translate(33047), path, translate(33048))):
if dialog(
"yesno",
"{jellyfin}",
"%s %s. %s" % (translate(33047), path, translate(33048)),
):
return False
window('jellyfin_pathverified.bool', True)
window("jellyfin_pathverified.bool", True)
return True
def validate_bluray_dir(path):
"""Verify if path/BDMV/ is accessible."""
''' Verify if path/BDMV/ is accessible.
'''
path = path + '/BDMV/'
path = path + "/BDMV/"
if not xbmcvfs.exists(path):
return False
window('jellyfin_pathverified.bool', True)
window("jellyfin_pathverified.bool", True)
return True
def validate_dvd_dir(path):
"""Verify if path/VIDEO_TS/ is accessible."""
''' Verify if path/VIDEO_TS/ is accessible.
'''
path = path + '/VIDEO_TS/'
path = path + "/VIDEO_TS/"
if not xbmcvfs.exists(path):
return False
window('jellyfin_pathverified.bool', True)
window("jellyfin_pathverified.bool", True)
return True
def values(item, keys):
''' Grab the values in the item for a list of keys {key},{key1}....
If the key has no brackets, the key will be passed as is.
'''
return (item[key.replace('{', "").replace('}', "")] if isinstance(key, text_type) and key.startswith('{') else key for key in keys)
"""Grab the values in the item for a list of keys {key},{key1}....
If the key has no brackets, the key will be passed as is.
"""
return (
(
item[key.replace("{", "").replace("}", "")]
if isinstance(key, text_type) and key.startswith("{")
else key
)
for key in keys
)
def delete_folder(path):
''' Delete objects from kodi cache
'''
"""Delete objects from kodi cache"""
LOG.debug("--[ delete folder ]")
dirs, files = xbmcvfs.listdir(path)
@ -305,9 +295,7 @@ def delete_folder(path):
def delete_recursive(path, dirs):
''' Delete files and dirs recursively.
'''
"""Delete files and dirs recursively."""
for directory in dirs:
dirs2, files = xbmcvfs.listdir(os.path.join(path, directory))
@ -319,11 +307,9 @@ def delete_recursive(path, dirs):
def unzip(path, dest, folder=None):
''' Unzip file. zipfile module seems to fail on android with badziperror.
'''
"""Unzip file. zipfile module seems to fail on android with badziperror."""
path = quote_plus(path)
root = "zip://" + path + '/'
root = "zip://" + path + "/"
if folder:
@ -360,9 +346,7 @@ def unzip_recursive(path, dirs, dest):
def unzip_file(path, dest):
''' Unzip specific file. Path should start with zip://
'''
"""Unzip specific file. Path should start with zip://"""
xbmcvfs.copy(path, dest)
LOG.debug("unzip: %s to %s", path, dest)
@ -381,9 +365,7 @@ def get_zip_directory(path, folder):
def copytree(path, dest):
''' Copy folder content from one to another.
'''
"""Copy folder content from one to another."""
dirs, files = xbmcvfs.listdir(path)
if not xbmcvfs.exists(dest):
@ -416,10 +398,8 @@ def copy_recursive(path, dirs, dest):
def copy_file(path, dest):
''' Copy specific file.
'''
if path.endswith('.pyo'):
"""Copy specific file."""
if path.endswith(".pyo"):
return
xbmcvfs.copy(path, dest)
@ -427,11 +407,10 @@ def copy_file(path, dest):
def normalize_string(text):
''' For theme media, do not modify unless modified in TV Tunes.
Remove dots from the last character as windows can not have directories
with dots at the end
'''
"""For theme media, do not modify unless modified in TV Tunes.
Remove dots from the last character as windows can not have directories
with dots at the end
"""
text = text.replace(":", "")
text = text.replace("/", "-")
text = text.replace("\\", "-")
@ -439,26 +418,24 @@ def normalize_string(text):
text = text.replace(">", "")
text = text.replace("*", "")
text = text.replace("?", "")
text = text.replace('|', "")
text = text.replace("|", "")
text = text.strip()
text = text.rstrip('.')
text = unicodedata.normalize('NFKD', text_type(text, 'utf-8')).encode('ascii', 'ignore')
text = text.rstrip(".")
text = unicodedata.normalize("NFKD", text_type(text, "utf-8")).encode(
"ascii", "ignore"
)
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)]
"""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)]
def convert_to_local(date, timezone=tz.tzlocal()):
''' Convert the local datetime to local.
'''
"""Convert the local datetime to local."""
try:
date = parser.parse(date) if isinstance(date, string_types) else date
date = date.replace(tzinfo=tz.tzutc())
@ -475,9 +452,9 @@ def convert_to_local(date, timezone=tz.tzlocal()):
date.second,
)
else:
return date.strftime('%Y-%m-%dT%H:%M:%S')
return date.strftime("%Y-%m-%dT%H:%M:%S")
except Exception as error:
LOG.exception('Item date: {} --- {}'.format(str(date), error))
LOG.exception("Item date: {} --- {}".format(str(date), error))
return str(date)
@ -491,28 +468,27 @@ def has_attribute(obj, name):
def set_addon_mode():
"""Setup playback mode. If native mode selected, check network credentials."""
value = dialog(
"yesno",
translate("playback_mode"),
translate(33035),
nolabel=translate("addon_mode"),
yeslabel=translate("native_mode"),
)
''' Setup playback mode. If native mode selected, check network credentials.
'''
value = dialog("yesno",
translate('playback_mode'),
translate(33035),
nolabel=translate('addon_mode'),
yeslabel=translate('native_mode'))
settings('useDirectPaths', value="1" if value else "0")
settings("useDirectPaths", value="1" if value else "0")
if value:
dialog("ok", "{jellyfin}", translate(33145))
LOG.info("Add-on playback: %s", settings('useDirectPaths') == "0")
LOG.info("Add-on playback: %s", settings("useDirectPaths") == "0")
class JsonDebugPrinter(object):
''' Helper class to defer converting data to JSON until it is needed.
"""Helper class to defer converting data to JSON until it is needed.
See: https://github.com/jellyfin/jellyfin-kodi/pull/193
'''
"""
def __init__(self, data):
self.data = data
@ -527,8 +503,8 @@ def get_filesystem_encoding():
if not enc:
enc = sys.getdefaultencoding()
if not enc or enc == 'ascii':
enc = 'utf-8'
if not enc or enc == "ascii":
enc = "utf-8"
return enc
@ -537,20 +513,20 @@ def find_library(server, item):
from ..database import get_sync
sync = get_sync()
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
ancestors = server.jellyfin.get_ancestors(item['Id'])
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
ancestors = server.jellyfin.get_ancestors(item["Id"])
for ancestor in ancestors:
if ancestor['Id'] in whitelist:
if ancestor["Id"] in whitelist:
return ancestor
LOG.error('No ancestor found, not syncing item with ID: {}'.format(item['Id']))
LOG.error("No ancestor found, not syncing item with ID: {}".format(item["Id"]))
return {}
def translate_path(path):
'''
"""
Use new library location for translate path starting in Kodi 19
'''
"""
version = kodi_version()
if version > 18:

View File

@ -19,9 +19,8 @@ LOG = LazyLogger(__name__)
def progress(message=None):
"""Will start and close the progress dialog."""
''' Will start and close the progress dialog.
'''
def decorator(func):
def wrapper(self, item=None, *args, **kwargs):
@ -29,10 +28,13 @@ def progress(message=None):
if item and type(item) == dict:
dialog.create(translate('addon_name'), "%s %s" % (translate('gathering'), item['Name']))
LOG.info("Processing %s: %s", item['Name'], item['Id'])
dialog.create(
translate("addon_name"),
"%s %s" % (translate("gathering"), item["Name"]),
)
LOG.info("Processing %s: %s", item["Name"], item["Id"])
else:
dialog.create(translate('addon_name'), message)
dialog.create(translate("addon_name"), message)
LOG.info("Processing %s", message)
if item:
@ -44,13 +46,13 @@ def progress(message=None):
return result
return wrapper
return decorator
def stop(func):
"""Wrapper to catch exceptions and return using catch"""
''' Wrapper to catch exceptions and return using catch
'''
def wrapper(*args, **kwargs):
try:
@ -68,11 +70,12 @@ def stop(func):
def jellyfin_item(func):
"""Wrapper to retrieve the jellyfin_db item."""
''' Wrapper to retrieve the jellyfin_db item.
'''
def wrapper(self, item, *args, **kwargs):
e_item = self.jellyfin_db.get_item_by_id(item['Id'] if type(item) == dict else item)
e_item = self.jellyfin_db.get_item_by_id(
item["Id"] if type(item) == dict else item
)
return func(self, item, e_item=e_item, *args, **kwargs)

View File

@ -19,45 +19,42 @@ LOG = LazyLogger(__name__)
def tvtunes_nfo(path, urls):
''' Create tvtunes.nfo
'''
"""Create tvtunes.nfo"""
try:
xml = etree.parse(path).getroot()
except Exception:
xml = etree.Element('tvtunes')
xml = etree.Element("tvtunes")
for elem in xml.getiterator('tvtunes'):
for elem in xml.getiterator("tvtunes"):
for file in list(elem):
elem.remove(file)
for url in urls:
etree.SubElement(xml, 'file').text = url
etree.SubElement(xml, "file").text = url
tree = etree.ElementTree(xml)
tree.write(path)
def advanced_settings():
''' Track the existence of <cleanonupdate>true</cleanonupdate>
It is incompatible with plugin paths.
'''
if settings('useDirectPaths') != "0":
"""Track the existence of <cleanonupdate>true</cleanonupdate>
It is incompatible with plugin paths.
"""
if settings("useDirectPaths") != "0":
return
path = translate_path("special://profile/")
file = os.path.join(path, 'advancedsettings.xml')
file = os.path.join(path, "advancedsettings.xml")
try:
xml = etree.parse(file).getroot()
except Exception:
return
video = xml.find('videolibrary')
video = xml.find("videolibrary")
if video is not None:
cleanonupdate = video.find('cleanonupdate')
cleanonupdate = video.find("cleanonupdate")
if cleanonupdate is not None and cleanonupdate.text == "true":
@ -68,13 +65,13 @@ def advanced_settings():
tree.write(file)
dialog("ok", "{jellyfin}", translate(33097))
xbmc.executebuiltin('RestartApp')
xbmc.executebuiltin("RestartApp")
return True
def verify_kodi_defaults():
''' Make sure we have the kodi default folder in place.
'''
"""Make sure we have the kodi default folder in place."""
source_base_path = translate_path("special://xbmc/system/library/video")
dest_base_path = translate_path("special://profile/library/video")
@ -97,11 +94,15 @@ def verify_kodi_defaults():
if not os.path.exists(dest_file):
copy = True
elif os.path.splitext(file_name)[1].lower() == '.xml':
elif os.path.splitext(file_name)[1].lower() == ".xml":
try:
etree.parse(dest_file)
except etree.ParseError:
LOG.warning("Unable to parse `{}`, recovering from default.".format(dest_file))
LOG.warning(
"Unable to parse `{}`, recovering from default.".format(
dest_file
)
)
copy = True
if copy:
@ -112,7 +113,7 @@ def verify_kodi_defaults():
# This code seems to enforce a fixed ordering.
# Is it really desirable to force this on users?
# The default (system wide) order is [10, 20, 30] in Kodi 19.
for index, node in enumerate(['movies', 'tvshows', 'musicvideos']):
for index, node in enumerate(["movies", "tvshows", "musicvideos"]):
file_name = os.path.join(dest_base_path, node, "index.xml")
if xbmcvfs.exists(file_name):
@ -126,8 +127,8 @@ def verify_kodi_defaults():
tree = None
if tree is not None:
tree.getroot().set('order', str(17 + index))
with xbmcvfs.File(file_name, 'w') as f:
tree.getroot().set("order", str(17 + index))
with xbmcvfs.File(file_name, "w") as f:
f.write(etree.tostring(tree.getroot()))
playlist_path = translate_path("special://profile/playlists/video")

View File

@ -25,22 +25,22 @@ def ensure_client():
return func(self, *args, **kwargs)
return wrapper
return decorator
class Jellyfin(object):
"""This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing
to communicate with the JellyfinClient().
''' This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing
to communicate with the JellyfinClient().
from jellyfin_kodi.jellyfin import Jellyfin
from jellyfin_kodi.jellyfin import Jellyfin
Jellyfin('123456').config.data['app']
Jellyfin('123456').config.data['app']
# Permanent client reference
client = Jellyfin('123456').get_client()
client.config.data['app']
'''
# Permanent client reference
client = Jellyfin('123456').get_client()
client.config.data['app']
"""
# Borg - multiple instances, shared state
_shared_state = {}
@ -94,7 +94,7 @@ class Jellyfin(object):
self.client[self.server_id] = JellyfinClient()
if self.server_id == 'default':
if self.server_id == "default":
LOG.info("---[ START JELLYFINCLIENT ]---")
else:
LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id)

View File

@ -15,7 +15,7 @@ LOG = LazyLogger(__name__)
def jellyfin_url(client, handler):
return "%s/%s" % (client.config.data['auth.server'], handler)
return "%s/%s" % (client.config.data["auth.server"], handler)
def basic_info():
@ -42,9 +42,8 @@ def music_info():
class API(object):
"""All the api calls to the server."""
''' All the api calls to the server.
'''
def __init__(self, client, *args, **kwargs):
self.client = client
self.config = client.config
@ -54,18 +53,18 @@ class API(object):
if request is None:
request = {}
request.update({'type': action, 'handler': url})
request.update({"type": action, "handler": url})
return self.client.request(request)
def _get(self, handler, params=None):
return self._http("GET", handler, {'params': params})
return self._http("GET", handler, {"params": params})
def _post(self, handler, json=None, params=None):
return self._http("POST", handler, {'params': params, 'json': json})
return self._http("POST", handler, {"params": params, "json": json})
def _delete(self, handler, params=None):
return self._http("DELETE", handler, {'params': params})
return self._http("DELETE", handler, {"params": params})
#################################################################################################
@ -111,9 +110,17 @@ class API(object):
def artwork(self, item_id, art, max_width, ext="jpg", index=None):
if index is None:
return jellyfin_url(self.client, "Items/%s/Images/%s?MaxWidth=%s&format=%s" % (item_id, art, max_width, ext))
return jellyfin_url(
self.client,
"Items/%s/Images/%s?MaxWidth=%s&format=%s"
% (item_id, art, max_width, ext),
)
return jellyfin_url(self.client, "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (item_id, art, index, max_width, ext))
return jellyfin_url(
self.client,
"Items/%s/Images/%s/%s?MaxWidth=%s&format=%s"
% (item_id, art, index, max_width, ext),
)
#################################################################################################
@ -140,16 +147,16 @@ class API(object):
return self.users("/Items/%s" % item_id)
def get_items(self, item_ids):
return self.users("/Items", params={
'Ids': ','.join(str(x) for x in item_ids),
'Fields': info()
})
return self.users(
"/Items",
params={"Ids": ",".join(str(x) for x in item_ids), "Fields": info()},
)
def get_sessions(self):
return self.sessions(params={'ControllableByUserId': "{UserId}"})
return self.sessions(params={"ControllableByUserId": "{UserId}"})
def get_device(self, device_id):
return self.sessions(params={'DeviceId': device_id})
return self.sessions(params={"DeviceId": device_id})
def post_session(self, session_id, url, params=None, data=None):
return self.sessions("/%s/%s" % (session_id, url), "POST", params, data)
@ -158,64 +165,68 @@ class API(object):
return self.items("/%s/Images" % item_id)
def get_suggestion(self, media="Movie,Episode", limit=1):
return self.users("/Suggestions", params={
'Type': media,
'Limit': limit
})
return self.users("/Suggestions", params={"Type": media, "Limit": limit})
def get_recently_added(self, media=None, parent_id=None, limit=20):
return self.user_items("/Latest", {
'Limit': limit,
'UserId': "{UserId}",
'IncludeItemTypes': media,
'ParentId': parent_id,
'Fields': info()
})
return self.user_items(
"/Latest",
{
"Limit": limit,
"UserId": "{UserId}",
"IncludeItemTypes": media,
"ParentId": parent_id,
"Fields": info(),
},
)
def get_next(self, index=None, limit=1):
return self.shows("/NextUp", {
'Limit': limit,
'UserId': "{UserId}",
'StartIndex': None if index is None else int(index)
})
return self.shows(
"/NextUp",
{
"Limit": limit,
"UserId": "{UserId}",
"StartIndex": None if index is None else int(index),
},
)
def get_adjacent_episodes(self, show_id, item_id):
return self.shows("/%s/Episodes" % show_id, {
'UserId': "{UserId}",
'AdjacentTo': item_id,
'Fields': "Overview"
})
return self.shows(
"/%s/Episodes" % show_id,
{"UserId": "{UserId}", "AdjacentTo": item_id, "Fields": "Overview"},
)
def get_genres(self, parent_id=None):
return self._get("Genres", {
'ParentId': parent_id,
'UserId': "{UserId}",
'Fields': info()
})
return self._get(
"Genres", {"ParentId": parent_id, "UserId": "{UserId}", "Fields": info()}
)
def get_recommendation(self, parent_id=None, limit=20):
return self._get("Movies/Recommendations", {
'ParentId': parent_id,
'UserId': "{UserId}",
'Fields': info(),
'Limit': limit
})
return self._get(
"Movies/Recommendations",
{
"ParentId": parent_id,
"UserId": "{UserId}",
"Fields": info(),
"Limit": limit,
},
)
def get_items_by_letter(self, parent_id=None, media=None, letter=None):
return self.user_items(params={
'ParentId': parent_id,
'NameStartsWith': letter,
'Fields': info(),
'Recursive': True,
'IncludeItemTypes': media
})
return self.user_items(
params={
"ParentId": parent_id,
"NameStartsWith": letter,
"Fields": info(),
"Recursive": True,
"IncludeItemTypes": media,
}
)
def get_channels(self):
return self._get("LiveTv/Channels", {
'UserId': "{UserId}",
'EnableImages': True,
'EnableUserData': True
})
return self._get(
"LiveTv/Channels",
{"UserId": "{UserId}", "EnableImages": True, "EnableUserData": True},
)
def get_intros(self, item_id):
return self.user_items("/%s/Intros" % item_id)
@ -230,30 +241,26 @@ class API(object):
return self.user_items("/%s/LocalTrailers" % item_id)
def get_transcode_settings(self):
return self._get('System/Configuration/encoding')
return self._get("System/Configuration/encoding")
def get_ancestors(self, item_id):
return self.items("/%s/Ancestors" % item_id, params={
'UserId': "{UserId}"
})
return self.items("/%s/Ancestors" % item_id, params={"UserId": "{UserId}"})
def get_items_theme_video(self, parent_id):
return self.users("/Items", params={
'HasThemeVideo': True,
'ParentId': parent_id
})
return self.users(
"/Items", params={"HasThemeVideo": True, "ParentId": parent_id}
)
def get_themes(self, item_id):
return self.items("/%s/ThemeMedia" % item_id, params={
'UserId': "{UserId}",
'InheritFromParent': True
})
return self.items(
"/%s/ThemeMedia" % item_id,
params={"UserId": "{UserId}", "InheritFromParent": True},
)
def get_items_theme_song(self, parent_id):
return self.users("/Items", params={
'HasThemeSong': True,
'ParentId': parent_id
})
return self.users(
"/Items", params={"HasThemeSong": True, "ParentId": parent_id}
)
def check_companion_enabled(self):
"""
@ -262,8 +269,10 @@ class API(object):
None = Unknown
"""
try:
plugin_settings = self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {}
return plugin_settings.get('IsEnabled')
plugin_settings = (
self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {}
)
return plugin_settings.get("IsEnabled")
except requests.RequestException as e:
LOG.warning("Error checking companion installed state: %s", e)
@ -277,42 +286,51 @@ class API(object):
return None
def get_seasons(self, show_id):
return self.shows("/%s/Seasons" % show_id, params={
'UserId': "{UserId}",
'EnableImages': True,
'Fields': info()
})
return self.shows(
"/%s/Seasons" % show_id,
params={"UserId": "{UserId}", "EnableImages": True, "Fields": info()},
)
def get_date_modified(self, date, parent_id, media=None):
return self.users("/Items", params={
'ParentId': parent_id,
'Recursive': False,
'IsMissing': False,
'IsVirtualUnaired': False,
'IncludeItemTypes': media or None,
'MinDateLastSaved': date,
'Fields': info()
})
return self.users(
"/Items",
params={
"ParentId": parent_id,
"Recursive": False,
"IsMissing": False,
"IsVirtualUnaired": False,
"IncludeItemTypes": media or None,
"MinDateLastSaved": date,
"Fields": info(),
},
)
def get_userdata_date_modified(self, date, parent_id, media=None):
return self.users("/Items", params={
'ParentId': parent_id,
'Recursive': True,
'IsMissing': False,
'IsVirtualUnaired': False,
'IncludeItemTypes': media or None,
'MinDateLastSavedForUser': date,
'Fields': info()
})
return self.users(
"/Items",
params={
"ParentId": parent_id,
"Recursive": True,
"IsMissing": False,
"IsVirtualUnaired": False,
"IncludeItemTypes": media or None,
"MinDateLastSavedForUser": date,
"Fields": info(),
},
)
def refresh_item(self, item_id):
return self.items("/%s/Refresh" % item_id, "POST", json={
'Recursive': True,
'ImageRefreshMode': "FullRefresh",
'MetadataRefreshMode': "FullRefresh",
'ReplaceAllImages': False,
'ReplaceAllMetadata': True
})
return self.items(
"/%s/Refresh" % item_id,
"POST",
json={
"Recursive": True,
"ImageRefreshMode": "FullRefresh",
"MetadataRefreshMode": "FullRefresh",
"ReplaceAllImages": False,
"ReplaceAllMetadata": True,
},
)
def favorite(self, item_id, option=True):
return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE")
@ -324,7 +342,9 @@ class API(object):
return self.sessions("/Capabilities/Full", "POST", json=data)
def session_add_user(self, session_id, user_id, option=True):
return self.sessions("/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE")
return self.sessions(
"/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE"
)
def session_playing(self, data):
return self.sessions("/Playing", "POST", json=data)
@ -339,115 +359,132 @@ class API(object):
return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE")
def get_sync_queue(self, date, filters=None):
return self._get("Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems", params={
'LastUpdateDT': date,
'filter': filters or 'None'
})
return self._get(
"Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems",
params={"LastUpdateDT": date, "filter": filters or "None"},
)
def get_server_time(self):
return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime")
def get_play_info(self, item_id, profile):
return self.items("/%s/PlaybackInfo" % item_id, "POST", json={
'UserId': "{UserId}",
'DeviceProfile': profile,
'AutoOpenLiveStream': True
})
return self.items(
"/%s/PlaybackInfo" % item_id,
"POST",
json={
"UserId": "{UserId}",
"DeviceProfile": profile,
"AutoOpenLiveStream": True,
},
)
def get_live_stream(self, item_id, play_id, token, profile):
return self._post("LiveStreams/Open", json={
'UserId': "{UserId}",
'DeviceProfile': profile,
'OpenToken': token,
'PlaySessionId': play_id,
'ItemId': item_id
})
return self._post(
"LiveStreams/Open",
json={
"UserId": "{UserId}",
"DeviceProfile": profile,
"OpenToken": token,
"PlaySessionId": play_id,
"ItemId": item_id,
},
)
def close_live_stream(self, live_id):
return self._post("LiveStreams/Close", json={
'LiveStreamId': live_id
})
return self._post("LiveStreams/Close", json={"LiveStreamId": live_id})
def close_transcode(self, device_id, play_id):
return self._delete("Videos/ActiveEncodings", params={
'DeviceId': device_id,
'PlaySessionId': play_id
})
return self._delete(
"Videos/ActiveEncodings",
params={"DeviceId": device_id, "PlaySessionId": play_id},
)
def get_default_headers(self):
auth = "MediaBrowser "
auth += "Client=%s, " % self.config.data['app.name']
auth += "Device=%s, " % self.config.data['app.device_name']
auth += "DeviceId=%s, " % self.config.data['app.device_id']
auth += "Version=%s" % self.config.data['app.version']
auth += "Client=%s, " % self.config.data["app.name"]
auth += "Device=%s, " % self.config.data["app.device_name"]
auth += "DeviceId=%s, " % self.config.data["app.device_id"]
auth += "Version=%s" % self.config.data["app.version"]
return {
"Accept": "application/json",
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Application": "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']),
"X-Application": "%s/%s"
% (self.config.data["app.name"], self.config.data["app.version"]),
"Accept-Charset": "UTF-8,*",
"Accept-encoding": "gzip",
"User-Agent": self.config.data['http.user_agent'] or "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']),
"x-emby-authorization": ensure_str(auth, 'utf-8')
"User-Agent": self.config.data["http.user_agent"]
or "%s/%s"
% (self.config.data["app.name"], self.config.data["app.version"]),
"x-emby-authorization": ensure_str(auth, "utf-8"),
}
def send_request(self, url, path, method="get", timeout=None, headers=None, data=None):
def send_request(
self, url, path, method="get", timeout=None, headers=None, data=None
):
request_method = getattr(requests, method.lower())
url = "%s/%s" % (url, path)
request_settings = {
"timeout": timeout or self.default_timeout,
"headers": headers or self.get_default_headers(),
"data": data
"data": data,
}
request_settings["verify"] = settings('sslverify.bool')
request_settings["verify"] = settings("sslverify.bool")
LOG.info("Sending %s request to %s" % (method, path))
LOG.debug(request_settings['timeout'])
LOG.debug(request_settings['headers'])
LOG.debug(request_settings["timeout"])
LOG.debug(request_settings["headers"])
return request_method(url, **request_settings)
def login(self, server_url, username, password=""):
path = "Users/AuthenticateByName"
auth_data = {
"username": username,
"Pw": password
}
auth_data = {"username": username, "Pw": password}
headers = self.get_default_headers()
headers.update({'Content-type': "application/json"})
headers.update({"Content-type": "application/json"})
try:
LOG.info("Trying to login to %s/%s as %s" % (server_url, path, username))
response = self.send_request(server_url, path, method="post", timeout=10, headers=headers, data=json.dumps(auth_data))
response = self.send_request(
server_url,
path,
method="post",
timeout=10,
headers=headers,
data=json.dumps(auth_data),
)
if response.status_code == 200:
return response.json()
else:
LOG.error("Failed to login to server with status code: " + str(response.status_code))
LOG.error(
"Failed to login to server with status code: "
+ str(response.status_code)
)
LOG.error("Server Response:\n" + str(response.content))
LOG.debug(headers)
return {}
except Exception as e: # Find exceptions for likely cases i.e, server timeout, etc
except (
Exception
) as e: # Find exceptions for likely cases i.e, server timeout, etc
LOG.error(e)
return {}
def validate_authentication_token(self, server):
auth_token_header = {
'X-MediaBrowser-Token': server['AccessToken']
}
auth_token_header = {"X-MediaBrowser-Token": server["AccessToken"]}
headers = self.get_default_headers()
headers.update(auth_token_header)
response = self.send_request(server['address'], "system/info", headers=headers)
response = self.send_request(server["address"], "system/info", headers=headers)
if response.status_code == 200:
return response.json()
else:
return {'Status_Code': response.status_code}
return {"Status_Code": response.status_code}
def get_public_info(self, server_address):
response = self.send_request(server_address, "system/info/public")
@ -459,8 +496,8 @@ class API(object):
return {}
def check_redirect(self, server_address):
''' Checks if the server is redirecting traffic to a new URL and
"""Checks if the server is redirecting traffic to a new URL and
returns the URL the server prefers to use
'''
"""
response = self.send_request(server_address, "system/info/public")
return response.url.replace('/system/info/public', '')
return response.url.replace("/system/info/public", "")

View File

@ -19,11 +19,10 @@ LOG = LazyLogger(__name__)
def callback(message, data):
''' Callback function should receive message, data
message: string
data: json dictionary
'''
"""Callback function should receive message, data
message: string
data: json dictionary
"""
pass
@ -53,13 +52,13 @@ class JellyfinClient(object):
self.set_credentials(credentials or {})
state = self.auth.connect(options or {})
if state['State'] == CONNECTION_STATE['SignedIn']:
if state["State"] == CONNECTION_STATE["SignedIn"]:
LOG.info("User is authenticated.")
self.logged_in = True
self.callback("ServerOnline", {'Id': self.auth.server_id})
self.callback("ServerOnline", {"Id": self.auth.server_id})
state['Credentials'] = self.get_credentials()
state["Credentials"] = self.get_credentials()
return state

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
''' This will hold all configs from the client.
""" This will hold all configs from the client.
Configuration set here will be used for the HTTP client.
'''
"""
#################################################################################################
@ -26,28 +26,41 @@ class Config(object):
self.data = {}
self.http()
def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None):
def app(
self,
name,
version,
device_name,
device_id,
capabilities=None,
device_pixel_ratio=None,
):
LOG.debug("Begin app constructor.")
self.data['app.name'] = name
self.data['app.version'] = version
self.data['app.device_name'] = device_name
self.data['app.device_id'] = device_id
self.data['app.capabilities'] = capabilities
self.data['app.device_pixel_ratio'] = device_pixel_ratio
self.data['app.default'] = False
self.data["app.name"] = name
self.data["app.version"] = version
self.data["app.device_name"] = device_name
self.data["app.device_id"] = device_id
self.data["app.capabilities"] = capabilities
self.data["app.device_pixel_ratio"] = device_pixel_ratio
self.data["app.default"] = False
def auth(self, server, user_id, token=None, ssl=None):
LOG.debug("Begin auth constructor.")
self.data['auth.server'] = server
self.data['auth.user_id'] = user_id
self.data['auth.token'] = token
self.data['auth.ssl'] = ssl
self.data["auth.server"] = server
self.data["auth.user_id"] = user_id
self.data["auth.token"] = token
self.data["auth.ssl"] = ssl
def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT):
def http(
self,
user_agent=None,
max_retries=DEFAULT_HTTP_MAX_RETRIES,
timeout=DEFAULT_HTTP_TIMEOUT,
):
LOG.debug("Begin http constructor.")
self.data['http.max_retries'] = max_retries
self.data['http.timeout'] = timeout
self.data['http.user_agent'] = user_agent
self.data["http.max_retries"] = max_retries
self.data["http.timeout"] = timeout
self.data["http.user_agent"] = user_agent

View File

@ -20,10 +20,10 @@ from .api import API
LOG = LazyLogger(__name__)
CONNECTION_STATE = {
'Unavailable': 0,
'ServerSelection': 1,
'ServerSignIn': 2,
'SignedIn': 3
"Unavailable": 0,
"ServerSelection": 1,
"ServerSignIn": 2,
"SignedIn": 3,
}
#################################################################################################
@ -48,10 +48,10 @@ class ConnectionManager(object):
LOG.info("revoking token")
self['server']['AccessToken'] = None
self["server"]["AccessToken"] = None
self.credentials.set_credentials(self.credentials.get())
self.config.data['auth.token'] = None
self.config.data["auth.token"] = None
def get_available_servers(self):
@ -61,11 +61,13 @@ class ConnectionManager(object):
credentials = self.credentials.get()
found_servers = self.process_found_servers(self._server_discovery())
if not found_servers and not credentials['Servers']: # back out right away, no point in continuing
if (
not found_servers and not credentials["Servers"]
): # back out right away, no point in continuing
LOG.info("Found no servers")
return list()
servers = list(credentials['Servers'])
servers = list(credentials["Servers"])
# Merges servers we already knew with newly found ones
for found_server in found_servers:
@ -74,8 +76,8 @@ class ConnectionManager(object):
except KeyError:
continue
servers.sort(key=itemgetter('DateLastAccessed'), reverse=True)
credentials['Servers'] = servers
servers.sort(key=itemgetter("DateLastAccessed"), reverse=True)
credentials["Servers"] = servers
self.credentials.set(credentials)
return servers
@ -88,36 +90,35 @@ class ConnectionManager(object):
if not server_url:
raise AttributeError("server url cannot be empty")
data = self.API.login(server_url, username, password) # returns empty dict on failure
data = self.API.login(
server_url, username, password
) # returns empty dict on failure
if not data:
LOG.info("Failed to login as `"+username+"`")
LOG.info("Failed to login as `" + username + "`")
return {}
LOG.info("Successfully logged in as %s" % (username))
# TODO Change when moving to database storage of server details
credentials = self.credentials.get()
self.config.data['auth.user_id'] = data['User']['Id']
self.config.data['auth.token'] = data['AccessToken']
self.config.data["auth.user_id"] = data["User"]["Id"]
self.config.data["auth.token"] = data["AccessToken"]
for server in credentials['Servers']:
if server['Id'] == data['ServerId']:
for server in credentials["Servers"]:
if server["Id"] == data["ServerId"]:
found_server = server
break
else:
return {} # No server found
found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
found_server['UserId'] = data['User']['Id']
found_server['AccessToken'] = data['AccessToken']
found_server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
found_server["UserId"] = data["User"]["Id"]
found_server["AccessToken"] = data["AccessToken"]
self.credentials.add_update_server(credentials['Servers'], found_server)
self.credentials.add_update_server(credentials["Servers"], found_server)
info = {
'Id': data['User']['Id'],
'IsSignedInOffline': True
}
info = {"Id": data["User"]["Id"], "IsSignedInOffline": True}
self.credentials.add_update_user(server, info)
self.credentials.set_credentials(credentials)
@ -137,40 +138,44 @@ class ConnectionManager(object):
address = response_url
LOG.info("connectToAddress %s succeeded", address)
server = {
'address': address,
"address": address,
}
server = self.connect_to_server(server, options)
if server is False:
LOG.error("connectToAddress %s failed", address)
return {'State': CONNECTION_STATE['Unavailable']}
return {"State": CONNECTION_STATE["Unavailable"]}
return server
except Exception as error:
LOG.exception(error)
LOG.error("connectToAddress %s failed", address)
return {'State': CONNECTION_STATE['Unavailable']}
return {"State": CONNECTION_STATE["Unavailable"]}
def connect_to_server(self, server, options={}):
LOG.info("begin connectToServer")
try:
result = self.API.get_public_info(server.get('address'))
result = self.API.get_public_info(server.get("address"))
if not result:
LOG.error("Failed to connect to server: %s" % server.get('address'))
return {'State': CONNECTION_STATE['Unavailable']}
LOG.error("Failed to connect to server: %s" % server.get("address"))
return {"State": CONNECTION_STATE["Unavailable"]}
LOG.info("calling onSuccessfulConnection with server %s", server.get('Name'))
LOG.info(
"calling onSuccessfulConnection with server %s", server.get("Name")
)
self._update_server_info(server, result)
credentials = self.credentials.get()
return self._after_connect_validated(server, credentials, result, True, options)
return self._after_connect_validated(
server, credentials, result, True, options
)
except Exception as e:
LOG.error(traceback.format_exc())
LOG.error("Failing server connection. ERROR msg: {}".format(e))
return {'State': CONNECTION_STATE['Unavailable']}
return {"State": CONNECTION_STATE["Unavailable"]}
def connect(self, options={}):
@ -180,9 +185,7 @@ class ConnectionManager(object):
LOG.info("connect has %s servers", len(servers))
if not (len(servers)): # No servers provided
return {
'State': ['ServerSelection']
}
return {"State": ["ServerSelection"]}
result = self.connect_to_server(servers[0], options)
LOG.debug("resolving connect with result: %s", result)
@ -190,7 +193,7 @@ class ConnectionManager(object):
return result
def jellyfin_token(self): # Called once monitor.py#163
return self.get_server_info(self.server_id)['AccessToken']
return self.get_server_info(self.server_id)["AccessToken"]
def get_server_info(self, server_id):
@ -198,14 +201,14 @@ class ConnectionManager(object):
LOG.info("server_id is empty")
return {}
servers = self.credentials.get()['Servers']
servers = self.credentials.get()["Servers"]
for server in servers:
if server['Id'] == server_id:
if server["Id"] == server_id:
return server
def get_server_address(self, server_id):
return self.get_server_info(server_id or self.server_id).get('address')
return self.get_server_info(server_id or self.server_id).get("address")
def get_public_users(self):
return self.client.jellyfin.get_public_users()
@ -258,9 +261,9 @@ class ConnectionManager(object):
server = self._convert_endpoint_address_to_manual_address(found_server)
info = {
'Id': found_server['Id'],
'address': server or found_server['Address'],
'Name': found_server['Name']
"Id": found_server["Id"],
"address": server or found_server["Address"],
"Name": found_server["Name"],
}
servers.append(info)
@ -270,11 +273,11 @@ class ConnectionManager(object):
# TODO: Make IPv6 compatible
def _convert_endpoint_address_to_manual_address(self, info):
if info.get('Address') and info.get('EndpointAddress'):
address = info['EndpointAddress'].split(':')[0]
if info.get("Address") and info.get("EndpointAddress"):
address = info["EndpointAddress"].split(":")[0]
# Determine the port, if any
parts = info['Address'].split(':')
parts = info["Address"].split(":")
if len(parts) > 1:
port_string = parts[len(parts) - 1]
@ -288,64 +291,70 @@ class ConnectionManager(object):
def _normalize_address(self, address):
# TODO: Try HTTPS first, then HTTP if that fails.
if '://' not in address:
address = 'http://' + address
if "://" not in address:
address = "http://" + address
# Attempt to correct bad input
url = urllib3.util.parse_url(address.strip())
if url.scheme is None:
url = url._replace(scheme='http')
url = url._replace(scheme="http")
if url.scheme == 'http' and url.port == 80:
if url.scheme == "http" and url.port == 80:
url = url._replace(port=None)
if url.scheme == 'https' and url.port == 443:
if url.scheme == "https" and url.port == 443:
url = url._replace(port=None)
return url.url
def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options):
if options.get('enableAutoLogin') is False:
def _after_connect_validated(
self, server, credentials, system_info, verify_authentication, options
):
if options.get("enableAutoLogin") is False:
self.config.data['auth.user_id'] = server.pop('UserId', None)
self.config.data['auth.token'] = server.pop('AccessToken', None)
self.config.data["auth.user_id"] = server.pop("UserId", None)
self.config.data["auth.token"] = server.pop("AccessToken", None)
elif verify_authentication and server.get('AccessToken'):
elif verify_authentication and server.get("AccessToken"):
system_info = self.API.validate_authentication_token(server)
if 'Status_Code' not in system_info:
if "Status_Code" not in system_info:
self._update_server_info(server, system_info)
self.config.data['auth.user_id'] = server['UserId']
self.config.data['auth.token'] = server['AccessToken']
system_info['Status_Code'] = 200
self.config.data["auth.user_id"] = server["UserId"]
self.config.data["auth.token"] = server["AccessToken"]
system_info["Status_Code"] = 200
return self._after_connect_validated(server, credentials, system_info, False, options)
return self._after_connect_validated(
server, credentials, system_info, False, options
)
server['UserId'] = None
server['AccessToken'] = None
system_info['State'] = CONNECTION_STATE['Unavailable']
server["UserId"] = None
server["AccessToken"] = None
system_info["State"] = CONNECTION_STATE["Unavailable"]
return system_info
self._update_server_info(server, system_info)
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
self.credentials.add_update_server(credentials['Servers'], server)
server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
self.credentials.add_update_server(credentials["Servers"], server)
self.credentials.set(credentials)
self.server_id = server['Id']
self.server_id = server["Id"]
# Update configs
self.config.data['auth.server'] = server['address']
self.config.data['auth.server-name'] = server['Name']
self.config.data['auth.server=id'] = server['Id']
self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl'])
self.config.data["auth.server"] = server["address"]
self.config.data["auth.server-name"] = server["Name"]
self.config.data["auth.server=id"] = server["Id"]
self.config.data["auth.ssl"] = options.get("ssl", self.config.data["auth.ssl"])
# Connected
return {
'Servers': [server],
'State': CONNECTION_STATE['SignedIn']
if server.get('AccessToken')
else CONNECTION_STATE['ServerSignIn'],
"Servers": [server],
"State": (
CONNECTION_STATE["SignedIn"]
if server.get("AccessToken")
else CONNECTION_STATE["ServerSignIn"]
),
}
def _update_server_info(self, server, system_info):
@ -353,8 +362,8 @@ class ConnectionManager(object):
if server is None or system_info is None:
return
server['Name'] = system_info['ServerName']
server['Id'] = system_info['Id']
server["Name"] = system_info["ServerName"]
server["Id"] = system_info["Id"]
if system_info.get('address'):
server['address'] = system_info['address']
if system_info.get("address"):
server["address"] = system_info["address"]

View File

@ -41,7 +41,7 @@ class Credentials(object):
self.credentials = {}
LOG.debug("credentials initialized with: %s", self.credentials)
self.credentials['Servers'] = self.credentials.setdefault('Servers', [])
self.credentials["Servers"] = self.credentials.setdefault("Servers", [])
def get(self):
self._ensure()
@ -62,53 +62,55 @@ class Credentials(object):
def add_update_user(self, server, user):
for existing in server.setdefault('Users', []):
if existing['Id'] == user['Id']:
for existing in server.setdefault("Users", []):
if existing["Id"] == user["Id"]:
# Merge the data
existing['IsSignedInOffline'] = True
existing["IsSignedInOffline"] = True
break
else:
server['Users'].append(user)
server["Users"].append(user)
def add_update_server(self, servers, server):
if server.get('Id') is None:
if server.get("Id") is None:
raise KeyError("Server['Id'] cannot be null or empty")
# Add default DateLastAccessed if doesn't exist.
server.setdefault('DateLastAccessed', "1970-01-01T00:00:00Z")
server.setdefault("DateLastAccessed", "1970-01-01T00:00:00Z")
for existing in servers:
if existing['Id'] == server['Id']:
if existing["Id"] == server["Id"]:
# Merge the data
if server.get('DateLastAccessed') and self._date_object(server['DateLastAccessed']) > self._date_object(existing['DateLastAccessed']):
existing['DateLastAccessed'] = server['DateLastAccessed']
if server.get("DateLastAccessed") and self._date_object(
server["DateLastAccessed"]
) > self._date_object(existing["DateLastAccessed"]):
existing["DateLastAccessed"] = server["DateLastAccessed"]
if server.get('UserLinkType'):
existing['UserLinkType'] = server['UserLinkType']
if server.get("UserLinkType"):
existing["UserLinkType"] = server["UserLinkType"]
if server.get('AccessToken'):
existing['AccessToken'] = server['AccessToken']
existing['UserId'] = server['UserId']
if server.get("AccessToken"):
existing["AccessToken"] = server["AccessToken"]
existing["UserId"] = server["UserId"]
if server.get('ExchangeToken'):
existing['ExchangeToken'] = server['ExchangeToken']
if server.get("ExchangeToken"):
existing["ExchangeToken"] = server["ExchangeToken"]
if server.get('ManualAddress'):
existing['ManualAddress'] = server['ManualAddress']
if server.get("ManualAddress"):
existing["ManualAddress"] = server["ManualAddress"]
if server.get('LocalAddress'):
existing['LocalAddress'] = server['LocalAddress']
if server.get("LocalAddress"):
existing["LocalAddress"] = server["LocalAddress"]
if server.get('Name'):
existing['Name'] = server['Name']
if server.get("Name"):
existing["Name"] = server["Name"]
if server.get('LastConnectionMode') is not None:
existing['LastConnectionMode'] = server['LastConnectionMode']
if server.get("LastConnectionMode") is not None:
existing["LastConnectionMode"] = server["LastConnectionMode"]
if server.get('ConnectServerId'):
existing['ConnectServerId'] = server['ConnectServerId']
if server.get("ConnectServerId"):
existing["ConnectServerId"] = server["ConnectServerId"]
return existing

View File

@ -34,9 +34,13 @@ class HTTP(object):
self.session = requests.Session()
max_retries = self.config.data['http.max_retries']
self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries))
self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries))
max_retries = self.config.data["http.max_retries"]
self.session.mount(
"http://", requests.adapters.HTTPAdapter(max_retries=max_retries)
)
self.session.mount(
"https://", requests.adapters.HTTPAdapter(max_retries=max_retries)
)
def stop_session(self):
@ -51,43 +55,44 @@ class HTTP(object):
def _replace_user_info(self, string):
if '{server}' in string:
if self.config.data.get('auth.server', None):
string = string.replace("{server}", self.config.data['auth.server'])
if "{server}" in string:
if self.config.data.get("auth.server", None):
string = string.replace("{server}", self.config.data["auth.server"])
else:
LOG.debug("Server address not set")
if '{UserId}' in string:
if self.config.data.get('auth.user_id', None):
string = string.replace("{UserId}", self.config.data['auth.user_id'])
if "{UserId}" in string:
if self.config.data.get("auth.user_id", None):
string = string.replace("{UserId}", self.config.data["auth.user_id"])
else:
LOG.debug("UserId is not set.")
return string
def request(self, data, session=None):
''' Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
data dictionary can contain:
type: GET, POST, etc.
url: (optional)
handler: not considered when url is provided (optional)
params: request parameters (optional)
json: request body (optional)
headers: (optional),
verify: ssl certificate, True (verify using device built-in library) or False
'''
"""Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
data dictionary can contain:
type: GET, POST, etc.
url: (optional)
handler: not considered when url is provided (optional)
params: request parameters (optional)
json: request body (optional)
headers: (optional),
verify: ssl certificate, True (verify using device built-in library) or False
"""
if not data:
raise AttributeError("Request cannot be empty")
data = self._request(data)
LOG.debug("--->[ http ] %s", JsonDebugPrinter(data))
retry = data.pop('retry', 5)
retry = data.pop("retry", 5)
while True:
try:
r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data)
r = self._requests(
session or self.session or requests, data.pop("type", "GET"), **data
)
r.content # release the connection
if not self.keep_alive and self.session is not None:
@ -104,7 +109,10 @@ class HTTP(object):
continue
LOG.error(error)
self.client.callback("ServerUnreachable", {'ServerId': self.config.data['auth.server-id']})
self.client.callback(
"ServerUnreachable",
{"ServerId": self.config.data["auth.server-id"]},
)
raise HTTPException("ServerUnreachable", error)
@ -125,12 +133,18 @@ class HTTP(object):
if r.status_code == 401:
if 'X-Application-Error-Code' in r.headers:
self.client.callback("AccessRestricted", {'ServerId': self.config.data['auth.server-id']})
if "X-Application-Error-Code" in r.headers:
self.client.callback(
"AccessRestricted",
{"ServerId": self.config.data["auth.server-id"]},
)
raise HTTPException("AccessRestricted", error)
else:
self.client.callback("Unauthorized", {'ServerId': self.config.data['auth.server-id']})
self.client.callback(
"Unauthorized",
{"ServerId": self.config.data["auth.server-id"]},
)
self.client.auth.revoke_token()
raise HTTPException("Unauthorized", error)
@ -160,11 +174,13 @@ class HTTP(object):
except requests.exceptions.MissingSchema as error:
LOG.error("Request missing Schema. " + str(error))
raise HTTPException("MissingSchema", {'Id': self.config.data.get('auth.server', "None")})
raise HTTPException(
"MissingSchema", {"Id": self.config.data.get("auth.server", "None")}
)
else:
try:
self.config.data['server-time'] = r.headers['Date']
self.config.data["server-time"] = r.headers["Date"]
elapsed = int(r.elapsed.total_seconds() * 1000)
response = r.json()
LOG.debug("---<[ http ][%s ms]", elapsed)
@ -179,15 +195,18 @@ class HTTP(object):
def _request(self, data):
if 'url' not in data:
data['url'] = "%s/%s" % (self.config.data.get("auth.server", ""), data.pop('handler', ""))
if "url" not in data:
data["url"] = "%s/%s" % (
self.config.data.get("auth.server", ""),
data.pop("handler", ""),
)
self._get_header(data)
data['timeout'] = data.get('timeout') or self.config.data['http.timeout']
data['verify'] = data.get('verify') or self.config.data.get('auth.ssl', False)
data['url'] = self._replace_user_info(data['url'])
self._process_params(data.get('params') or {})
self._process_params(data.get('json') or {})
data["timeout"] = data.get("timeout") or self.config.data["http.timeout"]
data["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False)
data["url"] = self._replace_user_info(data["url"])
self._process_params(data.get("params") or {})
self._process_params(data.get("json") or {})
return data
@ -204,17 +223,24 @@ class HTTP(object):
def _get_header(self, data):
data['headers'] = data.setdefault('headers', {})
data["headers"] = data.setdefault("headers", {})
if not data['headers']:
data['headers'].update({
'Content-type': "application/json",
'Accept-Charset': "UTF-8,*",
'Accept-encoding': "gzip",
'User-Agent': self.config.data['http.user_agent'] or "%s/%s" % (self.config.data.get('app.name', 'Jellyfin for Kodi'), self.config.data.get('app.version', "0.0.0"))
})
if not data["headers"]:
data["headers"].update(
{
"Content-type": "application/json",
"Accept-Charset": "UTF-8,*",
"Accept-encoding": "gzip",
"User-Agent": self.config.data["http.user_agent"]
or "%s/%s"
% (
self.config.data.get("app.name", "Jellyfin for Kodi"),
self.config.data.get("app.version", "0.0.0"),
),
}
)
if 'x-emby-authorization' not in data['headers']:
if "x-emby-authorization" not in data["headers"]:
self._authorization(data)
return data
@ -222,19 +248,26 @@ class HTTP(object):
def _authorization(self, data):
auth = "MediaBrowser "
auth += "Client=%s, " % self.config.data.get('app.name', "Jellyfin for Kodi")
auth += "Device=%s, " % self.config.data.get('app.device_name', 'Unknown Device')
auth += "DeviceId=%s, " % self.config.data.get('app.device_id', 'Unknown Device id')
auth += "Version=%s" % self.config.data.get('app.version', '0.0.0')
auth += "Client=%s, " % self.config.data.get("app.name", "Jellyfin for Kodi")
auth += "Device=%s, " % self.config.data.get(
"app.device_name", "Unknown Device"
)
auth += "DeviceId=%s, " % self.config.data.get(
"app.device_id", "Unknown Device id"
)
auth += "Version=%s" % self.config.data.get("app.version", "0.0.0")
data['headers'].update({'x-emby-authorization': ensure_str(auth, 'utf-8')})
data["headers"].update({"x-emby-authorization": ensure_str(auth, "utf-8")})
if self.config.data.get('auth.token') and self.config.data.get('auth.user_id'):
if self.config.data.get("auth.token") and self.config.data.get("auth.user_id"):
auth += ', UserId=%s' % self.config.data.get('auth.user_id')
data['headers'].update({
'x-emby-authorization': ensure_str(auth, 'utf-8'),
'X-MediaBrowser-Token': self.config.data.get('auth.token')})
auth += ", UserId=%s" % self.config.data.get("auth.user_id")
data["headers"].update(
{
"x-emby-authorization": ensure_str(auth, "utf-8"),
"X-MediaBrowser-Token": self.config.data.get("auth.token"),
}
)
return data

View File

@ -14,7 +14,8 @@ from ..helper import LazyLogger, settings
# If numpy is installed, the websockets library tries to use it, and then
# kodi hard crashes for reasons I don't even want to pretend to understand
import sys # noqa: E402,I100
sys.modules['numpy'] = None
sys.modules["numpy"] = None
import websocket # noqa: E402,I201
##################################################################################################
@ -41,23 +42,29 @@ class WSClient(threading.Thread):
if self.wsc is None:
raise ValueError("The websocket client is not started.")
self.wsc.send(json.dumps({'MessageType': message, "Data": data}))
self.wsc.send(json.dumps({"MessageType": message, "Data": data}))
def run(self):
monitor = xbmc.Monitor()
token = self.client.config.data['auth.token']
device_id = self.client.config.data['app.device_id']
server = self.client.config.data['auth.server']
server = server.replace('https://', 'wss://') if server.startswith('https') else server.replace('http://', 'ws://')
token = self.client.config.data["auth.token"]
device_id = self.client.config.data["app.device_id"]
server = self.client.config.data["auth.server"]
server = (
server.replace("https://", "wss://")
if server.startswith("https")
else server.replace("http://", "ws://")
)
wsc_url = "%s/socket?api_key=%s&device_id=%s" % (server, token, device_id)
LOG.info("Websocket url: %s", wsc_url)
self.wsc = websocket.WebSocketApp(wsc_url,
on_open=lambda ws: self.on_open(ws),
on_message=lambda ws, message: self.on_message(ws, message),
on_error=lambda ws, error: self.on_error(ws, error))
self.wsc = websocket.WebSocketApp(
wsc_url,
on_open=lambda ws: self.on_open(ws),
on_message=lambda ws, message: self.on_message(ws, message),
on_error=lambda ws, error: self.on_error(ws, error),
)
while not self.stop:
@ -73,41 +80,42 @@ class WSClient(threading.Thread):
LOG.info("--->[ websocket opened ]")
# Avoid a timing issue where the capabilities are not correctly registered
time.sleep(5)
if settings('remoteControl.bool'):
self.client.jellyfin.post_capabilities({
'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True,
'SupportedCommands': (
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoHome,PageUp,NextLetter,GoToSearch,"
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
"SetRepeatMode,Mute,Unmute,SetVolume,"
"Play,Playstate,PlayNext,PlayMediaSource"
),
})
if settings("remoteControl.bool"):
self.client.jellyfin.post_capabilities(
{
"PlayableMediaTypes": "Audio,Video",
"SupportsMediaControl": True,
"SupportedCommands": (
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoHome,PageUp,NextLetter,GoToSearch,"
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
"SetRepeatMode,Mute,Unmute,SetVolume,"
"Play,Playstate,PlayNext,PlayMediaSource"
),
}
)
else:
self.client.jellyfin.post_capabilities({
"PlayableMediaTypes": "Audio, Video",
"SupportsMediaControl": False
})
self.client.jellyfin.post_capabilities(
{"PlayableMediaTypes": "Audio, Video", "SupportsMediaControl": False}
)
def on_message(self, ws, message):
message = json.loads(message)
data = message.get('Data', {})
data = message.get("Data", {})
if message['MessageType'] in ('RefreshProgress',):
if message["MessageType"] in ("RefreshProgress",):
LOG.debug("Ignoring %s", message)
return
if not self.client.config.data['app.default']:
data['ServerId'] = self.client.auth.server_id
if not self.client.config.data["app.default"]:
data["ServerId"] = self.client.auth.server_id
self.client.callback(message['MessageType'], data)
self.client.callback(message["MessageType"], data)
def stop_client(self):

View File

@ -24,8 +24,8 @@ from .jellyfin import Jellyfin
##################################################################################################
LOG = LazyLogger(__name__)
LIMIT = int(settings('limitIndex') or 15)
DTHREADS = int(settings('limitThreads') or 3)
LIMIT = int(settings("limitIndex") or 15)
DTHREADS = int(settings("limitThreads") or 3)
TARGET_DB_VERSION = 1
##################################################################################################
@ -43,8 +43,8 @@ class Library(threading.Thread):
def __init__(self, monitor):
self.direct_path = settings('useDirectPaths') == "1"
self.progress_display = int(settings('syncProgress') or 50)
self.direct_path = settings("useDirectPaths") == "1"
self.progress_display = int(settings("syncProgress") or 50)
self.monitor = monitor
self.player = monitor.monitor.player
self.server = Jellyfin().get_client()
@ -59,7 +59,7 @@ class Library(threading.Thread):
self.jellyfin_threads = []
self.download_threads = []
self.notify_threads = []
self.writer_threads = {'updated': [], 'userdata': [], 'removed': []}
self.writer_threads = {"updated": [], "userdata": [], "removed": []}
self.database_lock = threading.Lock()
self.music_database_lock = threading.Lock()
@ -67,16 +67,16 @@ class Library(threading.Thread):
def __new_queues__(self):
return {
'Movie': Queue.Queue(),
'BoxSet': Queue.Queue(),
'MusicVideo': Queue.Queue(),
'Series': Queue.Queue(),
'Season': Queue.Queue(),
'Episode': Queue.Queue(),
'MusicAlbum': Queue.Queue(),
'MusicArtist': Queue.Queue(),
'AlbumArtist': Queue.Queue(),
'Audio': Queue.Queue()
"Movie": Queue.Queue(),
"BoxSet": Queue.Queue(),
"MusicVideo": Queue.Queue(),
"Series": Queue.Queue(),
"Season": Queue.Queue(),
"Episode": Queue.Queue(),
"MusicAlbum": Queue.Queue(),
"MusicArtist": Queue.Queue(),
"AlbumArtist": Queue.Queue(),
"Audio": Queue.Queue(),
}
def run(self):
@ -86,7 +86,7 @@ class Library(threading.Thread):
if not self.startup():
self.stop_client()
window('jellyfin_startup.bool', True)
window("jellyfin_startup.bool", True)
while not self.stop_thread:
@ -105,17 +105,15 @@ class Library(threading.Thread):
LOG.info("---<[ library ]")
def test_databases(self):
''' Open the databases to test if the file exists.
'''
with Database('video'), Database('music'):
"""Open the databases to test if the file exists."""
with Database("video"), Database("music"):
pass
def check_version(self):
'''
"""
Checks database version and triggers any required data migrations
'''
with Database('jellyfin') as jellyfindb:
"""
with Database("jellyfin") as jellyfindb:
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
db_version = db.get_version()
@ -124,26 +122,37 @@ class Library(threading.Thread):
db.add_version((TARGET_DB_VERSION))
# Video Database Migrations
with Database('video') as videodb:
with Database("video") as videodb:
vid_db = KodiDb(videodb.cursor)
if vid_db.migrations():
LOG.info('changes detected, reloading skin')
xbmc.executebuiltin('UpdateLibrary(video)')
xbmc.executebuiltin('ReloadSkin()')
LOG.info("changes detected, reloading skin")
xbmc.executebuiltin("UpdateLibrary(video)")
xbmc.executebuiltin("ReloadSkin()")
@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)
"""
self.download_threads = [
thread for thread in self.download_threads if not thread.is_done
]
self.writer_threads["updated"] = [
thread for thread in self.writer_threads["updated"] if not thread.is_done
]
self.writer_threads["userdata"] = [
thread for thread in self.writer_threads["userdata"] if not thread.is_done
]
self.writer_threads["removed"] = [
thread for thread in self.writer_threads["removed"] if not thread.is_done
]
''' 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)
'''
self.download_threads = [thread for thread in self.download_threads if not thread.is_done]
self.writer_threads['updated'] = [thread for thread in self.writer_threads['updated'] if not thread.is_done]
self.writer_threads['userdata'] = [thread for thread in self.writer_threads['userdata'] if not thread.is_done]
self.writer_threads['removed'] = [thread for thread in self.writer_threads['removed'] if not thread.is_done]
if not self.player.isPlayingVideo() or settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'):
if (
not self.player.isPlayingVideo()
or settings("syncDuringPlay.bool")
or xbmc.getCondVisibility("VideoPlayer.Content(livetv)")
):
self.worker_downloads()
self.worker_sort()
@ -154,7 +163,7 @@ class Library(threading.Thread):
self.worker_notify()
if self.pending_refresh:
window('jellyfin_sync.bool', True)
window("jellyfin_sync.bool", True)
if self.total_updates > self.progress_display:
queue_size = self.worker_queue_size()
@ -162,58 +171,91 @@ class Library(threading.Thread):
if self.progress_updates is None:
self.progress_updates = xbmcgui.DialogProgressBG()
self.progress_updates.create(translate('addon_name'), translate(33178))
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size))
self.progress_updates.create(
translate("addon_name"), translate(33178)
)
self.progress_updates.update(
int(
(
float(self.total_updates - queue_size)
/ float(self.total_updates)
)
* 100
),
message="%s: %s" % (translate(33178), queue_size),
)
elif queue_size:
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size))
self.progress_updates.update(
int(
(
float(self.total_updates - queue_size)
/ float(self.total_updates)
)
* 100
),
message="%s: %s" % (translate(33178), queue_size),
)
else:
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message=translate(33178))
self.progress_updates.update(
int(
(
float(self.total_updates - queue_size)
/ float(self.total_updates)
)
* 100
),
message=translate(33178),
)
if not settings('dbSyncScreensaver.bool') and self.screensaver is None:
if not settings("dbSyncScreensaver.bool") and self.screensaver is None:
xbmc.executebuiltin('InhibitIdleShutdown(true)')
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']):
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
self.save_last_sync()
self.total_updates = 0
window('jellyfin_sync', clear=True)
window("jellyfin_sync", clear=True)
if self.progress_updates:
self.progress_updates.close()
self.progress_updates = None
if not settings('dbSyncScreensaver.bool') and self.screensaver is not None:
if not settings("dbSyncScreensaver.bool") and self.screensaver is not None:
xbmc.executebuiltin('InhibitIdleShutdown(false)')
xbmc.executebuiltin("InhibitIdleShutdown(false)")
set_screensaver(value=self.screensaver)
self.screensaver = None
if xbmc.getCondVisibility('Container.Content(musicvideos)'): # Prevent cursor from moving
xbmc.executebuiltin('Container.Refresh')
if xbmc.getCondVisibility(
"Container.Content(musicvideos)"
): # Prevent cursor from moving
xbmc.executebuiltin("Container.Refresh")
else: # Update widgets
xbmc.executebuiltin('UpdateLibrary(video)')
xbmc.executebuiltin("UpdateLibrary(video)")
if xbmc.getCondVisibility('Window.IsMedia'):
xbmc.executebuiltin('Container.Refresh')
if xbmc.getCondVisibility("Window.IsMedia"):
xbmc.executebuiltin("Container.Refresh")
def stop_client(self):
self.stop_thread = True
def enable_pending_refresh(self):
''' When there's an active thread. Let the main thread know.
'''
"""When there's an active thread. Let the main thread know."""
self.pending_refresh = True
window('jellyfin_sync.bool', True)
window("jellyfin_sync.bool", True)
def worker_queue_size(self):
''' Get how many items are queued up for worker threads.
'''
"""Get how many items are queued up for worker threads."""
total = 0
for queues in self.updated_output:
@ -228,10 +270,11 @@ class Library(threading.Thread):
return total
def worker_downloads(self):
''' Get items from jellyfin and place them in the appropriate queues.
'''
for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)):
"""Get items from jellyfin and place them in the appropriate queues."""
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])
@ -240,9 +283,7 @@ class Library(threading.Thread):
self.download_threads.append(new_thread)
def worker_sort(self):
''' Get items based on the local jellyfin database and place item in appropriate queues.
'''
"""Get items based on the local jellyfin database and place item in appropriate queues."""
if self.removed_queue.qsize() and len(self.jellyfin_threads) < 2:
new_thread = SortWorker(self.removed_queue, self.removed_output)
@ -250,66 +291,96 @@ class Library(threading.Thread):
LOG.info("-->[ q:sort/%s ]", id(new_thread))
def worker_updates(self):
''' Update items in the Kodi database.
'''
"""Update items in the Kodi database."""
for queues in self.updated_output:
queue = self.updated_output[queues]
if queue.qsize() and not len(self.writer_threads['updated']):
if queue.qsize() and not len(self.writer_threads["updated"]):
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
new_thread = UpdateWorker(queue, self.notify_output, self.music_database_lock, "music", self.server, self.direct_path)
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
new_thread = UpdateWorker(
queue,
self.notify_output,
self.music_database_lock,
"music",
self.server,
self.direct_path,
)
else:
new_thread = UpdateWorker(queue, self.notify_output, self.database_lock, "video", self.server, self.direct_path)
new_thread = UpdateWorker(
queue,
self.notify_output,
self.database_lock,
"video",
self.server,
self.direct_path,
)
new_thread.start()
LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread))
self.writer_threads['updated'].append(new_thread)
self.writer_threads["updated"].append(new_thread)
self.enable_pending_refresh()
def worker_userdata(self):
''' Update userdata in the Kodi database.
'''
"""Update userdata in the Kodi database."""
for queues in self.userdata_output:
queue = self.userdata_output[queues]
if queue.qsize() and not len(self.writer_threads['userdata']):
if queue.qsize() and not len(self.writer_threads["userdata"]):
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
new_thread = UserDataWorker(queue, self.music_database_lock, "music", self.server, self.direct_path)
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
new_thread = UserDataWorker(
queue,
self.music_database_lock,
"music",
self.server,
self.direct_path,
)
else:
new_thread = UserDataWorker(queue, self.database_lock, "video", self.server, self.direct_path)
new_thread = UserDataWorker(
queue,
self.database_lock,
"video",
self.server,
self.direct_path,
)
new_thread.start()
LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread))
self.writer_threads['userdata'].append(new_thread)
self.writer_threads["userdata"].append(new_thread)
self.enable_pending_refresh()
def worker_remove(self):
''' Remove items from the Kodi database.
'''
"""Remove items from the Kodi database."""
for queues in self.removed_output:
queue = self.removed_output[queues]
if queue.qsize() and not len(self.writer_threads['removed']):
if queue.qsize() and not len(self.writer_threads["removed"]):
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
new_thread = RemovedWorker(queue, self.music_database_lock, "music", self.server, self.direct_path)
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
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 = 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)
self.writer_threads["removed"].append(new_thread)
self.enable_pending_refresh()
def worker_notify(self):
''' Notify the user of new additions.
'''
"""Notify the user of new additions."""
if self.notify_output.qsize() and not len(self.notify_threads):
new_thread = NotifyWorker(self.notify_output, self.player)
@ -318,11 +389,10 @@ class Library(threading.Thread):
self.notify_threads.append(new_thread)
def startup(self):
''' Run at startup.
Check databases.
Check for the server plugin.
'''
"""Run at startup.
Check databases.
Check for the server plugin.
"""
self.test_databases()
self.check_version()
@ -330,7 +400,7 @@ class Library(threading.Thread):
Views().get_nodes()
try:
if get_sync()['Libraries']:
if get_sync()["Libraries"]:
try:
with FullSync(self, self.server) as sync:
@ -340,7 +410,7 @@ class Library(threading.Thread):
except Exception as error:
LOG.exception(error)
elif not settings('SyncInstallRunDone.bool'):
elif not settings("SyncInstallRunDone.bool"):
with FullSync(self, self.server) as sync:
sync.libraries()
@ -349,9 +419,7 @@ class Library(threading.Thread):
return True
if settings('SyncInstallRunDone.bool') and settings(
'kodiCompanion.bool'
):
if settings("SyncInstallRunDone.bool") and settings("kodiCompanion.bool"):
# None == Unknown
if self.server.jellyfin.check_companion_enabled() is not False:
@ -372,12 +440,12 @@ class Library(threading.Thread):
except LibraryException as error:
LOG.error(error.status)
if error.status in 'SyncLibraryLater':
if error.status in "SyncLibraryLater":
dialog("ok", "{jellyfin}", translate(33129))
settings('SyncInstallRunDone.bool', True)
settings("SyncInstallRunDone.bool", True)
sync = get_sync()
sync['Libraries'] = []
sync["Libraries"] = []
save_sync(sync)
return True
@ -388,33 +456,33 @@ class Library(threading.Thread):
return False
def fast_sync(self):
''' Movie and userdata not provided by server yet.
'''
last_sync = settings('LastIncrementalSync')
"""Movie and userdata not provided by server yet."""
last_sync = settings("LastIncrementalSync")
include = []
filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"]
sync = get_sync()
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
LOG.info("--[ retrieve changes ] %s", last_sync)
# Get the item type of each synced library and build list of types to request
for item_id in whitelist:
library = self.server.jellyfin.get_item(item_id)
library_type = library.get('CollectionType')
library_type = library.get("CollectionType")
if library_type in filters:
include.append(library_type)
# Include boxsets if movies are synced
if 'movies' in include:
include.append('boxsets')
if "movies" in include:
include.append("boxsets")
# Filter down to the list of library types we want to exclude
query_filter = list(set(filters) - set(include))
try:
# Get list of updates from server for synced library types and populate work queues
result = self.server.jellyfin.get_sync_queue(last_sync, ",".join([x for x in query_filter]))
result = self.server.jellyfin.get_sync_queue(
last_sync, ",".join([x for x in query_filter])
)
if result is None:
return True
@ -423,18 +491,23 @@ class Library(threading.Thread):
userdata = []
removed = []
updated.extend(result['ItemsAdded'])
updated.extend(result['ItemsUpdated'])
userdata.extend(result['UserDataChanged'])
removed.extend(result['ItemsRemoved'])
updated.extend(result["ItemsAdded"])
updated.extend(result["ItemsUpdated"])
userdata.extend(result["UserDataChanged"])
removed.extend(result["ItemsRemoved"])
total = len(updated) + len(userdata)
if total > int(settings('syncIndicator') or 99):
if total > int(settings("syncIndicator") or 99):
''' Inverse yes no, in case the dialog is forced closed by Kodi.
'''
if dialog("yesno", "{jellyfin}", translate(33172).replace('{number}', str(total)), nolabel=translate(107), yeslabel=translate(106)):
"""Inverse yes no, in case the dialog is forced closed by Kodi."""
if dialog(
"yesno",
"{jellyfin}",
translate(33172).replace("{number}", str(total)),
nolabel=translate(107),
yeslabel=translate(106),
):
LOG.warning("Large updates skipped.")
return True
@ -453,56 +526,68 @@ class Library(threading.Thread):
def save_last_sync(self):
try:
time_now = datetime.strptime(self.server.config.data['server-time'].split(', ', 1)[1], '%d %b %Y %H:%M:%S GMT') - timedelta(minutes=2)
time_now = datetime.strptime(
self.server.config.data["server-time"].split(", ", 1)[1],
"%d %b %Y %H:%M:%S GMT",
) - timedelta(minutes=2)
except Exception as error:
LOG.exception(error)
time_now = datetime.utcnow() - timedelta(minutes=2)
last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz')
settings('LastIncrementalSync', value=last_sync)
last_sync = time_now.strftime("%Y-%m-%dT%H:%M:%Sz")
settings("LastIncrementalSync", value=last_sync)
LOG.info("--[ sync/%s ]", last_sync)
def select_libraries(self, mode=None):
''' Select from libraries synced. Either update or repair libraries.
Send event back to service.py
'''
"""Select from libraries synced. Either update or repair libraries.
Send event back to service.py
"""
modes = {
'SyncLibrarySelection': 'SyncLibrary',
'RepairLibrarySelection': 'RepairLibrary',
'AddLibrarySelection': 'SyncLibrary',
'RemoveLibrarySelection': 'RemoveLibrary'
"SyncLibrarySelection": "SyncLibrary",
"RepairLibrarySelection": "RepairLibrary",
"AddLibrarySelection": "SyncLibrary",
"RemoveLibrarySelection": "RemoveLibrary",
}
sync = get_sync()
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
libraries = []
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'):
for library in sync['Whitelist']:
if mode in (
"SyncLibrarySelection",
"RepairLibrarySelection",
"RemoveLibrarySelection",
):
for library in sync["Whitelist"]:
name = db.get_view_name(library.replace('Mixed:', ""))
libraries.append({'Id': library, 'Name': name})
name = db.get_view_name(library.replace("Mixed:", ""))
libraries.append({"Id": library, "Name": name})
else:
available = [x for x in sync['SortedViews'] if x not in whitelist]
available = [x for x in sync["SortedViews"] if x not in whitelist]
for library in available:
view = db.get_view(library)
if view.media_type in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'):
libraries.append({'Id': view.view_id, 'Name': view.view_name})
if view.media_type in (
"movies",
"tvshows",
"musicvideos",
"mixed",
"music",
):
libraries.append({"Id": view.view_id, "Name": view.view_name})
choices = [x['Name'] for x in libraries]
choices = [x["Name"] for x in libraries]
choices.insert(0, translate(33121))
titles = {
"RepairLibrarySelection": 33199,
"SyncLibrarySelection": 33198,
"RemoveLibrarySelection": 33200,
"AddLibrarySelection": 33120
"AddLibrarySelection": 33120,
}
title = titles.get(mode, "Failed to get title {}".format(mode))
@ -519,9 +604,15 @@ class Library(threading.Thread):
for x in selection:
library = libraries[x - 1]
selected_libraries.append(library['Id'])
selected_libraries.append(library["Id"])
event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'})
event(
modes[mode],
{
"Id": ",".join([libraries[x - 1]["Id"] for x in selection]),
"Update": mode == "SyncLibrarySelection",
},
)
def add_library(self, library_id, update=False):
@ -555,13 +646,11 @@ class Library(threading.Thread):
return True
def userdata(self, data):
''' Add item_id to userdata queue.
'''
"""Add item_id to userdata queue."""
if not data:
return
items = [x['ItemId'] for x in data]
items = [x["ItemId"] for x in data]
for item in split_list(items, LIMIT):
self.userdata_queue.put(item)
@ -570,9 +659,7 @@ class Library(threading.Thread):
LOG.info("---[ userdata:%s ]", len(items))
def updated(self, data):
''' Add item_id to updated queue.
'''
"""Add item_id to updated queue."""
if not data:
return
@ -583,9 +670,7 @@ class Library(threading.Thread):
LOG.info("---[ updated:%s ]", len(data))
def removed(self, data):
''' Add item_id to removed queue.
'''
"""Add item_id to removed queue."""
if not data:
return
@ -604,10 +689,12 @@ class UpdateWorker(threading.Thread):
is_done = False
def __init__(self, queue, notify, lock, database, server=None, direct_path=None, *args):
def __init__(
self, queue, notify, lock, database, server=None, direct_path=None, *args
):
self.queue = queue
self.notify_output = notify
self.notify = settings('newContent.bool')
self.notify = settings("newContent.bool")
self.lock = lock
self.database = Database(database)
self.args = args
@ -616,7 +703,7 @@ class UpdateWorker(threading.Thread):
threading.Thread.__init__(self)
def run(self):
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
if kodidb.db_file == "video":
movies = Movies(*default_args)
@ -626,7 +713,9 @@ class UpdateWorker(threading.Thread):
music = Music(*default_args)
else:
# this should not happen
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
LOG.error(
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
)
return
while True:
@ -637,39 +726,41 @@ class UpdateWorker(threading.Thread):
break
try:
LOG.debug('{} - {}'.format(item['Type'], item['Name']))
if item['Type'] == 'Movie':
LOG.debug("{} - {}".format(item["Type"], item["Name"]))
if item["Type"] == "Movie":
movies.movie(item)
elif item['Type'] == 'BoxSet':
elif item["Type"] == "BoxSet":
movies.boxset(item)
elif item['Type'] == 'Series':
elif item["Type"] == "Series":
tvshows.tvshow(item)
elif item['Type'] == 'Season':
elif item["Type"] == "Season":
tvshows.season(item)
elif item['Type'] == 'Episode':
elif item["Type"] == "Episode":
tvshows.episode(item)
elif item['Type'] == 'MusicVideo':
elif item["Type"] == "MusicVideo":
musicvideos.musicvideo(item)
elif item['Type'] == 'MusicAlbum':
elif item["Type"] == "MusicAlbum":
music.album(item)
elif item['Type'] == 'MusicArtist':
elif item["Type"] == "MusicArtist":
music.artist(item)
elif item['Type'] == 'AlbumArtist':
elif item["Type"] == "AlbumArtist":
music.albumartist(item)
elif item['Type'] == 'Audio':
elif item["Type"] == "Audio":
music.song(item)
if self.notify:
self.notify_output.put((item['Type'], api.API(item).get_naming()))
self.notify_output.put(
(item["Type"], api.API(item).get_naming())
)
except LibraryException as error:
if error.status == 'StopCalled':
if error.status == "StopCalled":
break
except Exception as error:
LOG.exception(error)
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break
LOG.info("--<[ q:updated/%s ]", id(self))
@ -692,7 +783,7 @@ class UserDataWorker(threading.Thread):
def run(self):
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
if kodidb.db_file == "video":
movies = Movies(*default_args)
@ -701,7 +792,9 @@ class UserDataWorker(threading.Thread):
music = Music(*default_args)
else:
# this should not happen
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
LOG.error(
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
)
return
while True:
@ -712,27 +805,27 @@ class UserDataWorker(threading.Thread):
break
try:
if item['Type'] == 'Movie':
if item["Type"] == "Movie":
movies.userdata(item)
elif item['Type'] in ['Series', 'Season', 'Episode']:
elif item["Type"] in ["Series", "Season", "Episode"]:
tvshows.userdata(item)
elif item['Type'] == 'MusicAlbum':
elif item["Type"] == "MusicAlbum":
music.album(item)
elif item['Type'] == 'MusicArtist':
elif item["Type"] == "MusicArtist":
music.artist(item)
elif item['Type'] == 'AlbumArtist':
elif item["Type"] == "AlbumArtist":
music.albumartist(item)
elif item['Type'] == 'Audio':
elif item["Type"] == "Audio":
music.userdata(item)
except LibraryException as error:
if error.status == 'StopCalled':
if error.status == "StopCalled":
break
except Exception as error:
LOG.exception(error)
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break
LOG.info("--<[ q:userdata/%s ]", id(self))
@ -752,7 +845,7 @@ class SortWorker(threading.Thread):
def run(self):
with Database('jellyfin') as jellyfindb:
with Database("jellyfin") as jellyfindb:
database = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
while True:
@ -765,21 +858,26 @@ class SortWorker(threading.Thread):
try:
media = database.get_media_by_id(item_id)
if media:
self.output[media].put({'Id': item_id, 'Type': media})
self.output[media].put({"Id": item_id, "Type": media})
else:
items = database.get_media_by_parent_id(item_id)
if not items:
LOG.info("Could not find media %s in the jellyfin database.", item_id)
LOG.info(
"Could not find media %s in the jellyfin database.",
item_id,
)
else:
for item in items:
self.output[item[1]].put({'Id': item[0], 'Type': item[1]})
self.output[item[1]].put(
{"Id": item[0], "Type": item[1]}
)
except Exception as error:
LOG.exception(error)
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break
LOG.info("--<[ q:sort/%s ]", id(self))
@ -801,7 +899,7 @@ class RemovedWorker(threading.Thread):
def run(self):
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
if kodidb.db_file == "video":
movies = Movies(*default_args)
@ -811,7 +909,9 @@ class RemovedWorker(threading.Thread):
music = Music(*default_args)
else:
# this should not happen
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
LOG.error(
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
)
return
while True:
@ -821,26 +921,31 @@ class RemovedWorker(threading.Thread):
except Queue.Empty:
break
if item['Type'] == 'Movie':
if item["Type"] == "Movie":
obj = movies.remove
elif item['Type'] in ['Series', 'Season', 'Episode']:
elif item["Type"] in ["Series", "Season", "Episode"]:
obj = tvshows.remove
elif item['Type'] in ['MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio']:
elif item["Type"] in [
"MusicAlbum",
"MusicArtist",
"AlbumArtist",
"Audio",
]:
obj = music.remove
elif item['Type'] == 'MusicVideo':
elif item["Type"] == "MusicVideo":
obj = musicvideos.remove
try:
obj(item['Id'])
obj(item["Id"])
except LibraryException as error:
if error.status == 'StopCalled':
if error.status == "StopCalled":
break
except Exception as error:
LOG.exception(error)
finally:
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break
LOG.info("--<[ q:removed/%s ]", id(self))
@ -854,8 +959,8 @@ class NotifyWorker(threading.Thread):
def __init__(self, queue, player):
self.queue = queue
self.video_time = int(settings('newvideotime')) * 1000
self.music_time = int(settings('newmusictime')) * 1000
self.video_time = int(settings("newvideotime")) * 1000
self.music_time = int(settings("newmusictime")) * 1000
self.player = player
threading.Thread.__init__(self)
@ -868,15 +973,24 @@ class NotifyWorker(threading.Thread):
except Queue.Empty:
break
time = self.music_time if item[0] == 'Audio' else self.video_time
time = self.music_time if item[0] == "Audio" else self.video_time
if time and (not self.player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')):
dialog("notification", heading="%s %s" % (translate(33049), item[0]), message=item[1],
icon="{jellyfin}", time=time, sound=False)
if time and (
not self.player.isPlayingVideo()
or xbmc.getCondVisibility("VideoPlayer.Content(livetv)")
):
dialog(
"notification",
heading="%s %s" % (translate(33049), item[0]),
message=item[1],
icon="{jellyfin}",
time=time,
sound=False,
)
self.queue.task_done()
if window('jellyfin_should_stop.bool'):
if window("jellyfin_should_stop.bool"):
break
LOG.info("--<[ q:notify/%s ]", id(self))

View File

@ -46,23 +46,35 @@ class Monitor(xbmc.Monitor):
def onNotification(self, sender, method, data):
if sender.lower() not in ('plugin.video.jellyfin', 'xbmc', 'upnextprovider.signal'):
if sender.lower() not in (
"plugin.video.jellyfin",
"xbmc",
"upnextprovider.signal",
):
return
if sender == 'plugin.video.jellyfin':
method = method.split('.')[1]
if sender == "plugin.video.jellyfin":
method = method.split(".")[1]
if method not in ('ReportProgressRequested', 'LoadServer', 'AddUser', 'PlayPlaylist', 'Play', 'Playstate', 'GeneralCommand'):
if method not in (
"ReportProgressRequested",
"LoadServer",
"AddUser",
"PlayPlaylist",
"Play",
"Playstate",
"GeneralCommand",
):
return
data = json.loads(data)[0]
elif sender.startswith('upnextprovider'):
LOG.info('Attempting to play the next episode via upnext')
method = method.split('.', 1)[1]
elif sender.startswith("upnextprovider"):
LOG.info("Attempting to play the next episode via upnext")
method = method.split(".", 1)[1]
if method not in ('plugin.video.jellyfin_play_action',):
LOG.info('Received invalid upnext method: %s', method)
if method not in ("plugin.video.jellyfin_play_action",):
LOG.info("Received invalid upnext method: %s", method)
return
data = json.loads(data)
@ -71,15 +83,23 @@ class Monitor(xbmc.Monitor):
if data:
data = json.loads(binascii.unhexlify(data[0]))
else:
if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'):
if method not in (
"Player.OnPlay",
"VideoLibrary.OnUpdate",
"Player.OnAVChange",
):
''' We have to clear the playlist if it was stopped before it has been played completely.
Otherwise the next played item will be added the previous queue.
'''
"""We have to clear the playlist if it was stopped before it has been played completely.
Otherwise the next played item will be added the previous queue.
"""
if method == "Player.OnStop":
xbmc.sleep(3000) # let's wait for the player, so we don't clear the canceled playlist by mistake.
xbmc.sleep(
3000
) # let's wait for the player, so we don't clear the canceled playlist by mistake.
if xbmc.getCondVisibility("!Player.HasMedia + !Window.IsVisible(busydialog)"):
if xbmc.getCondVisibility(
"!Player.HasMedia + !Window.IsVisible(busydialog)"
):
xbmc.executebuiltin("Playlist.Clear")
LOG.info("[ playlist ] cleared")
@ -96,14 +116,14 @@ class Monitor(xbmc.Monitor):
return
try:
if not data.get('ServerId'):
if not data.get("ServerId"):
server = Jellyfin()
else:
if method != 'LoadServer' and data['ServerId'] not in self.servers:
if method != "LoadServer" and data["ServerId"] not in self.servers:
try:
connect.Connect().register(data['ServerId'])
self.server_instance(data['ServerId'])
connect.Connect().register(data["ServerId"])
self.server_instance(data["ServerId"])
except Exception as error:
LOG.exception(error)
@ -111,80 +131,90 @@ class Monitor(xbmc.Monitor):
return
server = Jellyfin(data['ServerId'])
server = Jellyfin(data["ServerId"])
except Exception as error:
LOG.exception(error)
server = Jellyfin()
server = server.get_client()
if method == 'Play':
if method == "Play":
items = server.jellyfin.get_items(data['ItemIds'])
items = server.jellyfin.get_items(data["ItemIds"])
PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow',
data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'),
data.get('SubtitleStreamIndex')).start()
PlaylistWorker(
data.get("ServerId"),
items,
data["PlayCommand"] == "PlayNow",
data.get("StartPositionTicks", 0),
data.get("AudioStreamIndex"),
data.get("SubtitleStreamIndex"),
).start()
# TODO no clue if this is called by anything
elif method == 'PlayPlaylist':
elif method == "PlayPlaylist":
server.jellyfin.post_session(server.config.data['app.session'], "Playing", {
'PlayCommand': "PlayNow",
'ItemIds': data['Id'],
'StartPositionTicks': 0
})
server.jellyfin.post_session(
server.config.data["app.session"],
"Playing",
{
"PlayCommand": "PlayNow",
"ItemIds": data["Id"],
"StartPositionTicks": 0,
},
)
elif method in ('ReportProgressRequested', 'Player.OnAVChange'):
self.player.report_playback(data.get('Report', True))
elif method in ("ReportProgressRequested", "Player.OnAVChange"):
self.player.report_playback(data.get("Report", True))
elif method == 'Playstate':
elif method == "Playstate":
self.playstate(data)
elif method == 'GeneralCommand':
elif method == "GeneralCommand":
self.general_commands(data)
elif method == 'LoadServer':
self.server_instance(data['ServerId'])
elif method == "LoadServer":
self.server_instance(data["ServerId"])
elif method == 'AddUser':
server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add'])
elif method == "AddUser":
server.jellyfin.session_add_user(
server.config.data["app.session"], data["Id"], data["Add"]
)
self.additional_users(server)
elif method == 'Player.OnPlay':
elif method == "Player.OnPlay":
on_play(data, server)
elif method == 'VideoLibrary.OnUpdate':
elif method == "VideoLibrary.OnUpdate":
on_update(data, server)
def server_instance(self, server_id=None):
server = Jellyfin(server_id).get_client()
session = server.jellyfin.get_device(self.device_id)
server.config.data['app.session'] = session[0]['Id']
server.config.data["app.session"] = session[0]["Id"]
if server_id is not None:
self.servers.append(server_id)
elif settings('additionalUsers'):
elif settings("additionalUsers"):
users = settings('additionalUsers').split(',')
users = settings("additionalUsers").split(",")
all_users = server.jellyfin.get_users()
for additional in users:
for user in all_users:
if user['Name'].lower() in additional.lower():
server.jellyfin.session_add_user(server.config.data['app.session'], user['Id'], True)
if user["Name"].lower() in additional.lower():
server.jellyfin.session_add_user(
server.config.data["app.session"], user["Id"], True
)
self.additional_users(server)
def additional_users(self, server):
''' Setup additional users images.
'''
"""Setup additional users images."""
for i in range(10):
window('JellyfinAdditionalUserImage.%s' % i, clear=True)
window("JellyfinAdditionalUserImage.%s" % i, clear=True)
try:
session = server.jellyfin.get_device(self.device_id)
@ -193,31 +223,31 @@ class Monitor(xbmc.Monitor):
return
for index, user in enumerate(session[0]['AdditionalUsers']):
for index, user in enumerate(session[0]["AdditionalUsers"]):
info = server.jellyfin.get_user(user['UserId'])
image = api.API(info, server.config.data['auth.server']).get_user_artwork(user['UserId'])
window('JellyfinAdditionalUserImage.%s' % index, image)
window('JellyfinAdditionalUserPosition.%s' % user['UserId'], str(index))
info = server.jellyfin.get_user(user["UserId"])
image = api.API(info, server.config.data["auth.server"]).get_user_artwork(
user["UserId"]
)
window("JellyfinAdditionalUserImage.%s" % index, image)
window("JellyfinAdditionalUserPosition.%s" % user["UserId"], str(index))
def playstate(self, data):
''' Jellyfin playstate updates.
'''
command = data['Command']
"""Jellyfin playstate updates."""
command = data["Command"]
actions = {
'Stop': self.player.stop,
'Unpause': self.player.pause,
'Pause': self.player.pause,
'PlayPause': self.player.pause,
'NextTrack': self.player.playnext,
'PreviousTrack': self.player.playprevious
"Stop": self.player.stop,
"Unpause": self.player.pause,
"Pause": self.player.pause,
"PlayPause": self.player.pause,
"NextTrack": self.player.playnext,
"PreviousTrack": self.player.playprevious,
}
if command == 'Seek':
if command == "Seek":
if self.player.isPlaying():
seektime = data['SeekPositionTicks'] / 10000000.0
seektime = data["SeekPositionTicks"] / 10000000.0
self.player.seekTime(seektime)
LOG.info("[ seek/%s ]", seektime)
@ -227,69 +257,78 @@ class Monitor(xbmc.Monitor):
LOG.info("[ command/%s ]", command)
def general_commands(self, data):
"""General commands from Jellyfin to control the Kodi interface."""
command = data["Name"]
args = data["Arguments"]
''' General commands from Jellyfin to control the Kodi interface.
'''
command = data['Name']
args = data['Arguments']
if command in (
"Mute",
"Unmute",
"SetVolume",
"SetSubtitleStreamIndex",
"SetAudioStreamIndex",
"SetRepeatMode",
):
if command in ('Mute', 'Unmute', 'SetVolume',
'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'):
if command in ["Mute", "Unmute"]:
xbmc.executebuiltin("Mute")
elif command == "SetAudioStreamIndex":
self.player.set_audio_subs(args["Index"])
elif command == "SetRepeatMode":
xbmc.executebuiltin("xbmc.PlayerControl(%s)" % args["RepeatMode"])
elif command == "SetSubtitleStreamIndex":
self.player.set_audio_subs(None, args["Index"])
if command in ['Mute', 'Unmute']:
xbmc.executebuiltin('Mute')
elif command == 'SetAudioStreamIndex':
self.player.set_audio_subs(args['Index'])
elif command == 'SetRepeatMode':
xbmc.executebuiltin('xbmc.PlayerControl(%s)' % args['RepeatMode'])
elif command == 'SetSubtitleStreamIndex':
self.player.set_audio_subs(None, args['Index'])
elif command == 'SetVolume':
xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume'])
elif command == "SetVolume":
xbmc.executebuiltin("SetVolume(%s[,showvolumebar])" % args["Volume"])
# Kodi needs a bit of time to update its current status
xbmc.sleep(500)
self.player.report_playback()
elif command == 'DisplayMessage':
dialog("notification", heading=args['Header'], message=args['Text'],
icon="{jellyfin}", time=int(settings('displayMessage')) * 1000)
elif command == "DisplayMessage":
dialog(
"notification",
heading=args["Header"],
message=args["Text"],
icon="{jellyfin}",
time=int(settings("displayMessage")) * 1000,
)
elif command == 'SendString':
JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False})
elif command == "SendString":
JSONRPC("Input.SendText").execute({"text": args["String"], "done": False})
elif command == 'GoHome':
JSONRPC('GUI.ActivateWindow').execute({'window': "home"})
elif command == "GoHome":
JSONRPC("GUI.ActivateWindow").execute({"window": "home"})
elif command == 'Guide':
JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"})
elif command == "Guide":
JSONRPC("GUI.ActivateWindow").execute({"window": "tvguide"})
elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'):
elif command in ("MoveUp", "MoveDown", "MoveRight", "MoveLeft"):
actions = {
'MoveUp': "Input.Up",
'MoveDown': "Input.Down",
'MoveRight': "Input.Right",
'MoveLeft': "Input.Left"
"MoveUp": "Input.Up",
"MoveDown": "Input.Down",
"MoveRight": "Input.Right",
"MoveLeft": "Input.Left",
}
JSONRPC(actions[command]).execute()
else:
builtin = {
'ToggleFullscreen': 'Action(FullScreen)',
'ToggleOsdMenu': 'Action(OSD)',
'ToggleContextMenu': 'Action(ContextMenu)',
'Select': 'Action(Select)',
'Back': 'Action(back)',
'PageUp': 'Action(PageUp)',
'NextLetter': 'Action(NextLetter)',
'GoToSearch': 'VideoLibrary.Search',
'GoToSettings': 'ActivateWindow(Settings)',
'PageDown': 'Action(PageDown)',
'PreviousLetter': 'Action(PrevLetter)',
'TakeScreenshot': 'TakeScreenshot',
'ToggleMute': 'Mute',
'VolumeUp': 'Action(VolumeUp)',
'VolumeDown': 'Action(VolumeDown)',
"ToggleFullscreen": "Action(FullScreen)",
"ToggleOsdMenu": "Action(OSD)",
"ToggleContextMenu": "Action(ContextMenu)",
"Select": "Action(Select)",
"Back": "Action(back)",
"PageUp": "Action(PageUp)",
"NextLetter": "Action(NextLetter)",
"GoToSearch": "VideoLibrary.Search",
"GoToSettings": "ActivateWindow(Settings)",
"PageDown": "Action(PageDown)",
"PreviousLetter": "Action(PrevLetter)",
"TakeScreenshot": "TakeScreenshot",
"ToggleMute": "Mute",
"VolumeUp": "Action(VolumeUp)",
"VolumeDown": "Action(VolumeDown)",
}
if command in builtin:
xbmc.executebuiltin(builtin[command])
@ -305,10 +344,9 @@ class Listener(threading.Thread):
threading.Thread.__init__(self)
def run(self):
''' Detect the resume dialog for widgets.
Detect external players.
'''
"""Detect the resume dialog for widgets.
Detect external players.
"""
LOG.info("--->[ listener ]")
while not self.stop_thread:

File diff suppressed because it is too large Load Diff

View File

@ -21,15 +21,21 @@ class Artwork(object):
self.cursor = cursor
def update(self, image_url, kodi_id, media, image):
''' Update artwork in the video database.
Delete current entry before updating with the new one.
'''
if not image_url or image == 'poster' and media in ('song', 'artist', 'album'):
"""Update artwork in the video database.
Delete current entry before updating with the new one.
"""
if not image_url or image == "poster" and media in ("song", "artist", "album"):
return
try:
self.cursor.execute(QU.get_art, (kodi_id, media, image,))
self.cursor.execute(
QU.get_art,
(
kodi_id,
media,
image,
),
)
url = self.cursor.fetchone()[0]
except TypeError:
@ -41,43 +47,39 @@ class Artwork(object):
self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image))
def add(self, artwork, *args):
''' Add all artworks.
'''
"""Add all artworks."""
KODI = {
'Primary': ['thumb', 'poster'],
'Banner': "banner",
'Logo': "clearlogo",
'Art': "clearart",
'Thumb': "landscape",
'Disc': "discart",
'Backdrop': "fanart"
"Primary": ["thumb", "poster"],
"Banner": "banner",
"Logo": "clearlogo",
"Art": "clearart",
"Thumb": "landscape",
"Disc": "discart",
"Backdrop": "fanart",
}
for art in KODI:
if art == 'Backdrop':
if art == "Backdrop":
self.cursor.execute(QU.get_backdrops, args + ("fanart%",))
if len(self.cursor.fetchall()) > len(artwork['Backdrop']):
if len(self.cursor.fetchall()) > len(artwork["Backdrop"]):
self.cursor.execute(QU.delete_backdrops, args + ("fanart_",))
for index, backdrop in enumerate(artwork['Backdrop']):
for index, backdrop in enumerate(artwork["Backdrop"]):
if index:
self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),))
else:
self.update(*(backdrop,) + args + ("fanart",))
elif art == 'Primary':
for kodi_image in KODI['Primary']:
self.update(*(artwork['Primary'],) + args + (kodi_image,))
elif art == "Primary":
for kodi_image in KODI["Primary"]:
self.update(*(artwork["Primary"],) + args + (kodi_image,))
elif artwork.get(art):
self.update(*(artwork[art],) + args + (KODI[art],))
def delete(self, *args):
''' Delete artwork from kodi database
'''
"""Delete artwork from kodi database"""
self.cursor.execute(QU.delete_art, args)

View File

@ -89,7 +89,13 @@ class Kodi(object):
def add_file(self, filename, path_id):
try:
self.cursor.execute(QU.get_file, (filename, path_id,))
self.cursor.execute(
QU.get_file,
(
filename,
path_id,
),
)
file_id = self.cursor.fetchone()[0]
except TypeError:
@ -120,40 +126,49 @@ class Kodi(object):
def add_thumbnail(person_id, person, person_type):
if person['imageurl']:
if person["imageurl"]:
art = person_type.lower()
if "writing" in art:
art = "writer"
self.artwork.update(person['imageurl'], person_id, art, "thumb")
self.artwork.update(person["imageurl"], person_id, art, "thumb")
cast_order = 1
bulk_updates = {}
for person in people:
person_id = self.get_person(person['Name'])
person_id = self.get_person(person["Name"])
if person['Type'] == 'Actor':
if person["Type"] == "Actor":
sql = QU.update_actor
role = person.get('Role')
bulk_updates.setdefault(sql, []).append((person_id,) + args + (role, cast_order,))
role = person.get("Role")
bulk_updates.setdefault(sql, []).append(
(person_id,)
+ args
+ (
role,
cast_order,
)
)
cast_order += 1
elif person['Type'] == 'Director':
sql = QU.update_link.replace("{LinkType}", 'director_link')
elif person["Type"] == "Director":
sql = QU.update_link.replace("{LinkType}", "director_link")
bulk_updates.setdefault(sql, []).append((person_id,) + args)
elif person['Type'] == 'Writer':
sql = QU.update_link.replace("{LinkType}", 'writer_link')
elif person["Type"] == "Writer":
sql = QU.update_link.replace("{LinkType}", "writer_link")
bulk_updates.setdefault(sql, []).append((person_id,) + args)
elif person['Type'] == 'Artist':
sql = QU.insert_link_if_not_exists.replace("{LinkType}", 'actor_link')
bulk_updates.setdefault(sql, []).append((person_id,) + args + (person_id,) + args)
elif person["Type"] == "Artist":
sql = QU.insert_link_if_not_exists.replace("{LinkType}", "actor_link")
bulk_updates.setdefault(sql, []).append(
(person_id,) + args + (person_id,) + args
)
add_thumbnail(person_id, person, person['Type'])
add_thumbnail(person_id, person, person["Type"])
for sql, parameters in bulk_updates.items():
self.cursor.executemany(sql, parameters)
@ -163,8 +178,7 @@ class Kodi(object):
return self.cursor.lastrowid
def _get_person(self, name):
'''Retrieve person from the database, or add them if they don't exist
'''
"""Retrieve person from the database, or add them if they don't exist"""
resp = self.cursor.execute(QU.get_person, (name,)).fetchone()
if resp is not None:
return resp[0]
@ -172,8 +186,7 @@ class Kodi(object):
return self.add_person(name)
def get_person(self, name):
'''Retrieve person from cache, else forward to db query
'''
"""Retrieve person from cache, else forward to db query"""
if name in self._people_cache:
return self._people_cache[name]
else:
@ -182,9 +195,7 @@ class Kodi(object):
return person_id
def add_genres(self, genres, *args):
''' Delete current genres first for clean slate.
'''
"""Delete current genres first for clean slate."""
self.cursor.execute(QU.delete_genres, args)
for genre in genres:
@ -230,29 +241,32 @@ class Kodi(object):
return self.add_studio(*args)
def add_streams(self, file_id, streams, runtime):
''' First remove any existing entries
Then re-add video, audio and subtitles.
'''
"""First remove any existing entries
Then re-add video, audio and subtitles.
"""
self.cursor.execute(QU.delete_streams, (file_id,))
if streams:
for track in streams['video']:
for track in streams["video"]:
track['FileId'] = file_id
track['Runtime'] = runtime
track["FileId"] = file_id
track["Runtime"] = runtime
if kodi_version() < 20:
self.add_stream_video(*values(track, QU.add_stream_video_obj_19))
else:
self.add_stream_video(*values(track, QU.add_stream_video_obj))
for track in streams['audio']:
for track in streams["audio"]:
track['FileId'] = file_id
track["FileId"] = file_id
self.add_stream_audio(*values(track, QU.add_stream_audio_obj))
for track in streams['subtitle']:
self.add_stream_sub(*values({'language': track, 'FileId': file_id}, QU.add_stream_sub_obj))
for track in streams["subtitle"]:
self.add_stream_sub(
*values(
{"language": track, "FileId": file_id}, QU.add_stream_sub_obj
)
)
def add_stream_video(self, *args):
if kodi_version() < 20:
@ -267,17 +281,24 @@ class Kodi(object):
self.cursor.execute(QU.add_stream_sub, args)
def add_playstate(self, file_id, playcount, date_played, resume, *args):
''' Delete the existing resume point.
Set the watched count.
'''
"""Delete the existing resume point.
Set the watched count.
"""
self.cursor.execute(QU.delete_bookmark, (file_id,))
self.set_playcount(playcount, date_played, file_id)
if resume:
bookmark_id = self.create_entry_bookmark()
self.cursor.execute(QU.add_bookmark, (bookmark_id, file_id, resume,) + args)
self.cursor.execute(
QU.add_bookmark,
(
bookmark_id,
file_id,
resume,
)
+ args,
)
def set_playcount(self, *args):
self.cursor.execute(QU.update_playcount, args)

View File

@ -74,15 +74,11 @@ class Movies(Kodi):
return None
def add_ratings(self, *args):
''' Add ratings, rating type and votes.
'''
"""Add ratings, rating type and votes."""
self.cursor.execute(QU.add_rating, args)
def update_ratings(self, *args):
''' Update rating by rating_id.
'''
"""Update rating by rating_id."""
self.cursor.execute(QU.update_rating, args)
def get_unique_id(self, *args):
@ -95,15 +91,11 @@ class Movies(Kodi):
return
def add_unique_id(self, *args):
''' Add the provider id, imdb, tvdb.
'''
"""Add the provider id, imdb, tvdb."""
self.cursor.execute(QU.add_unique_id, args)
def update_unique_id(self, *args):
''' Update the provider id, imdb, tvdb.
'''
"""Update the provider id, imdb, tvdb."""
self.cursor.execute(QU.update_unique_id, args)
def add_countries(self, countries, *args):
@ -141,9 +133,9 @@ class Movies(Kodi):
self.cursor.execute(QU.delete_set, args)
def migrations(self):
'''
"""
Used to trigger required database migrations for new versions
'''
"""
self.cursor.execute(QU.get_version)
version_id = self.cursor.fetchone()[0]
changes = False
@ -156,10 +148,10 @@ class Movies(Kodi):
return changes
def omega_migration(self):
'''
"""
Adds a video version for all existing movies
'''
LOG.info('Starting migration for Omega database changes')
"""
LOG.info("Starting migration for Omega database changes")
# Tracks if this migration made any changes
changes = False
self.cursor.execute(QU.get_missing_versions)
@ -169,5 +161,5 @@ class Movies(Kodi):
self.add_videoversion(entry[0], entry[1], "movie", "0", 40400)
changes = True
LOG.info('Omega database migration is complete')
LOG.info("Omega database migration is complete")
return changes

View File

@ -25,10 +25,9 @@ class Music(Kodi):
Kodi.__init__(self)
def create_entry(self):
''' Krypton has a dummy first entry
idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
'''
"""Krypton has a dummy first entry
idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
"""
self.cursor.execute(QU.create_artist)
return self.cursor.fetchone()[0] + 1
@ -55,9 +54,7 @@ class Music(Kodi):
self.cursor.execute(QU.update_role, args)
def get(self, artist_id, name, musicbrainz):
''' Get artist or create the entry.
'''
"""Get artist or create the entry."""
try:
self.cursor.execute(QU.get_artist, (musicbrainz,))
result = self.cursor.fetchone()
@ -72,15 +69,20 @@ class Music(Kodi):
return artist_id_res
def add_artist(self, artist_id, name, *args):
''' Safety check, when musicbrainz does not exist
'''
"""Safety check, when musicbrainz does not exist"""
try:
self.cursor.execute(QU.get_artist_by_name, (name,))
artist_id_res = self.cursor.fetchone()[0]
except TypeError:
artist_id_res = artist_id or self.create_entry()
self.cursor.execute(QU.add_artist, (artist_id, name,) + args)
self.cursor.execute(
QU.add_artist,
(
artist_id,
name,
)
+ args,
)
return artist_id_res
@ -141,7 +143,7 @@ class Music(Kodi):
self.cursor.execute(QU.get_album_by_name72, (name,))
album = self.cursor.fetchone()
if album[1] and album[1].split(' / ')[0] not in artists.split(' / '):
if album[1] and album[1].split(" / ")[0] not in artists.split(" / "):
LOG.info("Album found, but artist doesn't match?")
LOG.info("Album [ %s/%s ] %s", name, album[1], artists)
@ -149,7 +151,14 @@ class Music(Kodi):
album_id = (album or self.cursor.fetchone())[0]
except TypeError:
album_id = self.add_album(*(album_id, name, musicbrainz,) + args)
album_id = self.add_album(
*(
album_id,
name,
musicbrainz,
)
+ args
)
return album_id
@ -225,11 +234,10 @@ class Music(Kodi):
self.cursor.execute(QU.update_song_rating, args)
def add_genres(self, kodi_id, genres, media):
''' Add genres, but delete current genres first.
Album_genres was removed in kodi 18
'''
if media == 'album' and self.version_id < 72:
"""Add genres, but delete current genres first.
Album_genres was removed in kodi 18
"""
if media == "album" and self.version_id < 72:
self.cursor.execute(QU.delete_genres_album, (kodi_id,))
for genre in genres:
@ -237,7 +245,7 @@ class Music(Kodi):
genre_id = self.get_genre(genre)
self.cursor.execute(QU.update_genre_album, (genre_id, kodi_id))
if media == 'song':
if media == "song":
self.cursor.execute(QU.delete_genres_song, (kodi_id,))
for genre in genres:

View File

@ -1,9 +1,9 @@
from __future__ import division, absolute_import, print_function, unicode_literals
''' Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items.
""" Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items.
Some functions require additional information, therefore obj do not always reflect
the Kodi database query values.
'''
"""
create_path = """
SELECT coalesce(max(idPath), 0)
FROM path
@ -254,21 +254,48 @@ add_bookmark = """
INSERT INTO bookmark(idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type)
VALUES (?, ?, ?, ?, ?, ?)
"""
add_bookmark_obj = ["{FileId}", "{PlayCount}", "{DatePlayed}", "{Resume}", "{Runtime}", "DVDPlayer", 1]
add_bookmark_obj = [
"{FileId}",
"{PlayCount}",
"{DatePlayed}",
"{Resume}",
"{Runtime}",
"DVDPlayer",
1,
]
add_streams_obj = ["{FileId}", "{Streams}", "{Runtime}"]
add_stream_video = """
INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth,
iVideoHeight, iVideoDuration, strStereoMode, strHdrType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_stream_video_obj = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}", "{hdrtype}"]
add_stream_video_obj = [
"{FileId}",
0,
"{codec}",
"{aspect}",
"{width}",
"{height}",
"{Runtime}",
"{3d}",
"{hdrtype}",
]
# strHdrType is new to Kodi 20
add_stream_video_19 = """
INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth,
iVideoHeight, iVideoDuration, strStereoMode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""
add_stream_video_obj_19 = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}"]
add_stream_video_obj_19 = [
"{FileId}",
0,
"{codec}",
"{aspect}",
"{width}",
"{height}",
"{Runtime}",
"{3d}",
]
add_stream_audio = """
INSERT INTO streamdetails(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
VALUES (?, ?, ?, ?, ?)
@ -295,24 +322,82 @@ INSERT INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_movie_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}",
"{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}",
"{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}",
"{Trailer}", "{Country}", "{Premiere}"]
add_movie_obj = [
"{MovieId}",
"{FileId}",
"{Title}",
"{Plot}",
"{ShortPlot}",
"{Tagline}",
"{Votes}",
"{RatingId}",
"{Writers}",
"{Year}",
"{Unique}",
"{SortTitle}",
"{Runtime}",
"{Mpaa}",
"{Genre}",
"{Directors}",
"{Title}",
"{Studio}",
"{Trailer}",
"{Country}",
"{Premiere}",
]
add_rating = """
INSERT INTO rating(rating_id, media_id, media_type, rating_type, rating, votes)
VALUES (?, ?, ?, ?, ?, ?)
"""
add_rating_movie_obj = ["{RatingId}", "{MovieId}", "movie", "default", "{Rating}", "{Votes}"]
add_rating_tvshow_obj = ["{RatingId}", "{ShowId}", "tvshow", "default", "{Rating}", "{Votes}"]
add_rating_episode_obj = ["{RatingId}", "{EpisodeId}", "episode", "default", "{Rating}", "{Votes}"]
add_rating_movie_obj = [
"{RatingId}",
"{MovieId}",
"movie",
"default",
"{Rating}",
"{Votes}",
]
add_rating_tvshow_obj = [
"{RatingId}",
"{ShowId}",
"tvshow",
"default",
"{Rating}",
"{Votes}",
]
add_rating_episode_obj = [
"{RatingId}",
"{EpisodeId}",
"episode",
"default",
"{Rating}",
"{Votes}",
]
add_unique_id = """
INSERT INTO uniqueid(uniqueid_id, media_id, media_type, value, type)
VALUES (?, ?, ?, ?, ?)
"""
add_unique_id_movie_obj = ["{Unique}", "{MovieId}", "movie", "{UniqueId}", "{ProviderName}"]
add_unique_id_tvshow_obj = ["{Unique}", "{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}"]
add_unique_id_episode_obj = ["{Unique}", "{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}"]
add_unique_id_movie_obj = [
"{Unique}",
"{MovieId}",
"movie",
"{UniqueId}",
"{ProviderName}",
]
add_unique_id_tvshow_obj = [
"{Unique}",
"{ShowId}",
"tvshow",
"{UniqueId}",
"{ProviderName}",
]
add_unique_id_episode_obj = [
"{Unique}",
"{EpisodeId}",
"episode",
"{UniqueId}",
"{ProviderName}",
]
add_country = """
INSERT INTO country(name)
VALUES (?)
@ -335,14 +420,40 @@ INSERT INTO musicvideo(idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09,
c11, c12, premiered)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_musicvideo_obj = ["{MvideoId}", "{FileId}", "{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}",
"{Plot}", "{Album}", "{Artists}", "{Genre}", "{Index}", "{Premiere}"]
add_musicvideo_obj = [
"{MvideoId}",
"{FileId}",
"{Title}",
"{Runtime}",
"{Directors}",
"{Studio}",
"{Year}",
"{Plot}",
"{Album}",
"{Artists}",
"{Genre}",
"{Index}",
"{Premiere}",
]
add_tvshow = """
INSERT INTO tvshow(idShow, c00, c01, c02, c04, c05, c08, c09, c10, c12, c13, c14, c15)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_tvshow_obj = ["{ShowId}", "{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}",
"disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}"]
add_tvshow_obj = [
"{ShowId}",
"{Title}",
"{Plot}",
"{Status}",
"{RatingId}",
"{Premiere}",
"{Genre}",
"{Title}",
"disintegrate browse bug",
"{Unique}",
"{Mpaa}",
"{Studio}",
"{SortTitle}",
]
add_season = """
INSERT INTO seasons(idSeason, idShow, season)
VALUES (?, ?, ?)
@ -352,9 +463,27 @@ INSERT INTO episode(idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c1
idShow, c15, c16, idSeason, c18, c19, c20)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_episode_obj = ["{EpisodeId}", "{FileId}", "{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}",
"{Directors}", "{Season}", "{Index}", "{Title}", "{ShowId}", "{AirsBeforeSeason}",
"{AirsBeforeEpisode}", "{SeasonId}", "{FullFilePath}", "{PathId}", "{Unique}"]
add_episode_obj = [
"{EpisodeId}",
"{FileId}",
"{Title}",
"{Plot}",
"{RatingId}",
"{Writers}",
"{Premiere}",
"{Runtime}",
"{Directors}",
"{Season}",
"{Index}",
"{Title}",
"{ShowId}",
"{AirsBeforeSeason}",
"{AirsBeforeEpisode}",
"{SeasonId}",
"{FullFilePath}",
"{PathId}",
"{Unique}",
]
add_art = """
INSERT INTO art(media_id, media_type, type, url)
VALUES (?, ?, ?, ?)
@ -372,7 +501,13 @@ SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?
WHERE idPath = ?
"""
update_path_movie_obj = ["{Path}", "movies", "metadata.local", 1, "{PathId}"]
update_path_toptvshow_obj = ["{TopLevel}", "tvshows", "metadata.local", 1, "{TopPathId}"]
update_path_toptvshow_obj = [
"{TopLevel}",
"tvshows",
"metadata.local",
1,
"{TopPathId}",
]
update_path_toptvshow_addon_obj = ["{TopLevel}", None, None, 1, "{TopPathId}"]
update_path_tvshow_obj = ["{Path}", None, None, 1, "{PathId}"]
update_path_episode_obj = ["{Path}", None, None, 1, "{PathId}"]
@ -431,26 +566,83 @@ SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,
c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ?
WHERE idMovie = ?
"""
update_movie_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}",
"{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}",
"{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}", "{Trailer}",
"{Country}", "{Premiere}", "{MovieId}"]
update_movie_obj = [
"{Title}",
"{Plot}",
"{ShortPlot}",
"{Tagline}",
"{Votes}",
"{RatingId}",
"{Writers}",
"{Year}",
"{Unique}",
"{SortTitle}",
"{Runtime}",
"{Mpaa}",
"{Genre}",
"{Directors}",
"{Title}",
"{Studio}",
"{Trailer}",
"{Country}",
"{Premiere}",
"{MovieId}",
]
update_rating = """
UPDATE rating
SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ?
WHERE rating_id = ?
"""
update_rating_movie_obj = ["{MovieId}", "movie", "default", "{Rating}", "{Votes}", "{RatingId}"]
update_rating_tvshow_obj = ["{ShowId}", "tvshow", "default", "{Rating}", "{Votes}", "{RatingId}"]
update_rating_episode_obj = ["{EpisodeId}", "episode", "default", "{Rating}", "{Votes}", "{RatingId}"]
update_rating_movie_obj = [
"{MovieId}",
"movie",
"default",
"{Rating}",
"{Votes}",
"{RatingId}",
]
update_rating_tvshow_obj = [
"{ShowId}",
"tvshow",
"default",
"{Rating}",
"{Votes}",
"{RatingId}",
]
update_rating_episode_obj = [
"{EpisodeId}",
"episode",
"default",
"{Rating}",
"{Votes}",
"{RatingId}",
]
update_unique_id = """
UPDATE uniqueid
SET media_id = ?, media_type = ?, value = ?, type = ?
WHERE uniqueid_id = ?
"""
update_unique_id_movie_obj = ["{MovieId}", "movie", "{UniqueId}", "{ProviderName}", "{Unique}"]
update_unique_id_tvshow_obj = ["{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}", "{Unique}"]
update_unique_id_episode_obj = ["{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}", "{Unique}"]
update_unique_id_movie_obj = [
"{MovieId}",
"movie",
"{UniqueId}",
"{ProviderName}",
"{Unique}",
]
update_unique_id_tvshow_obj = [
"{ShowId}",
"tvshow",
"{UniqueId}",
"{ProviderName}",
"{Unique}",
]
update_unique_id_episode_obj = [
"{EpisodeId}",
"episode",
"{UniqueId}",
"{ProviderName}",
"{Unique}",
]
update_country = """
INSERT OR REPLACE INTO country_link(country_id, media_id, media_type)
VALUES (?, ?, ?)
@ -474,16 +666,41 @@ SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 =
c11 = ?, c12 = ?, premiered = ?
WHERE idMVideo = ?
"""
update_musicvideo_obj = ["{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", "{Plot}", "{Album}",
"{Artists}", "{Genre}", "{Index}", "{Premiere}", "{MvideoId}"]
update_musicvideo_obj = [
"{Title}",
"{Runtime}",
"{Directors}",
"{Studio}",
"{Year}",
"{Plot}",
"{Album}",
"{Artists}",
"{Genre}",
"{Index}",
"{Premiere}",
"{MvideoId}",
]
update_tvshow = """
UPDATE tvshow
SET c00 = ?, c01 = ?, c02 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?, c10 = ?,
c12 = ?, c13 = ?, c14 = ?, c15 = ?
WHERE idShow = ?
"""
update_tvshow_obj = ["{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}",
"disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}", "{ShowId}"]
update_tvshow_obj = [
"{Title}",
"{Plot}",
"{Status}",
"{RatingId}",
"{Premiere}",
"{Genre}",
"{Title}",
"disintegrate browse bug",
"{Unique}",
"{Mpaa}",
"{Studio}",
"{SortTitle}",
"{ShowId}",
]
update_tvshow_link = """
INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath)
VALUES (?, ?)
@ -501,9 +718,26 @@ SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,
c18 = ?, c19 = ?, c20 = ?
WHERE idEpisode = ?
"""
update_episode_obj = ["{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", "{Directors}",
"{Season}", "{Index}", "{Title}", "{AirsBeforeSeason}", "{AirsBeforeEpisode}", "{SeasonId}",
"{ShowId}", "{FullFilePath}", "{PathId}", "{Unique}", "{EpisodeId}"]
update_episode_obj = [
"{Title}",
"{Plot}",
"{RatingId}",
"{Writers}",
"{Premiere}",
"{Runtime}",
"{Directors}",
"{Season}",
"{Index}",
"{Title}",
"{AirsBeforeSeason}",
"{AirsBeforeEpisode}",
"{SeasonId}",
"{ShowId}",
"{FullFilePath}",
"{PathId}",
"{Unique}",
"{EpisodeId}",
]
delete_path = """

View File

@ -54,7 +54,14 @@ FROM album
WHERE strMusicBrainzAlbumID = ?
"""
get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album"]
get_album_obj82 = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album", "{DateAdded}"]
get_album_obj82 = [
"{AlbumId}",
"{Title}",
"{UniqueId}",
"{Artists}",
"album",
"{DateAdded}",
]
get_album_by_name = """
SELECT idAlbum, strArtists
FROM album
@ -132,9 +139,24 @@ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle
rating, comment, dateAdded)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
add_song_obj = ["{SongId}", "{AlbumId}", "{PathId}", "{Artists}", "{Genre}", "{Title}", "{Index}",
"{Runtime}", "{Year}", "{Filename}", "{UniqueId}", "{PlayCount}", "{DatePlayed}", "{Rating}",
"{Comment}", "{DateAdded}"]
add_song_obj = [
"{SongId}",
"{AlbumId}",
"{PathId}",
"{Artists}",
"{Genre}",
"{Title}",
"{Index}",
"{Runtime}",
"{Year}",
"{Filename}",
"{UniqueId}",
"{PlayCount}",
"{DatePlayed}",
"{Rating}",
"{Comment}",
"{DateAdded}",
]
add_genre = """
INSERT INTO genre(idGenre, strGenre)
VALUES (?, ?)
@ -197,7 +219,17 @@ SET strArtistDisp = ?, strReleaseDate = ?, strGenres = ?, strReview = ?,
iUserrating = ?, lastScraped = ?, bScrapedMBID = 1, strReleaseType = ?
WHERE idAlbum = ?
"""
update_album_obj = ["{Artists}", "{Year}", "{Genre}", "{Bio}", "{Thumb}", "{Rating}", "{LastScraped}", "album", "{AlbumId}"]
update_album_obj = [
"{Artists}",
"{Year}",
"{Genre}",
"{Bio}",
"{Thumb}",
"{Rating}",
"{LastScraped}",
"album",
"{AlbumId}",
]
update_album_artist = """
UPDATE album
SET strArtists = ?
@ -229,9 +261,22 @@ SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack
rating = ?, comment = ?, dateAdded = ?
WHERE idSong = ?
"""
update_song_obj = ["{AlbumId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}",
"{Filename}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}",
"{DateAdded}", "{SongId}"]
update_song_obj = [
"{AlbumId}",
"{Artists}",
"{Genre}",
"{Title}",
"{Index}",
"{Runtime}",
"{Year}",
"{Filename}",
"{PlayCount}",
"{DatePlayed}",
"{Rating}",
"{Comment}",
"{DateAdded}",
"{SongId}",
]
update_song_artist = """
INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
VALUES (?, ?, ?, ?, ?)

View File

@ -8,7 +8,16 @@ from kodi_six.utils import py2_encode
from .. import downloader as server
from ..database import jellyfin_db, queries as QUEM
from ..helper import api, stop, validate, validate_bluray_dir, validate_dvd_dir, jellyfin_item, values, Local
from ..helper import (
api,
stop,
validate,
validate_bluray_dir,
validate_dvd_dir,
jellyfin_item,
values,
Local,
)
from ..helper import LazyLogger
from ..helper.utils import find_library
from ..helper.exceptions import PathValidationException
@ -42,74 +51,83 @@ class Movies(KodiDb):
@stop
@jellyfin_item
def movie(self, item, e_item):
''' If item does not exist, entry will be added.
If item exists, entry will be updated.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""If item does not exist, entry will be added.
If item exists, entry will be updated.
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'Movie')
obj = self.objects.map(item, "Movie")
update = True
try:
obj['MovieId'] = e_item[0]
obj['FileId'] = e_item[1]
obj['PathId'] = e_item[2]
obj['LibraryId'] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
obj["MovieId"] = e_item[0]
obj["FileId"] = e_item[1]
obj["PathId"] = e_item[2]
obj["LibraryId"] = e_item[6]
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError:
update = False
LOG.debug("MovieId %s not found", obj['Id'])
LOG.debug("MovieId %s not found", obj["Id"])
library = self.library or find_library(self.server, item)
if not library:
# This item doesn't belong to a whitelisted library
return
obj['MovieId'] = self.create_entry()
obj['LibraryId'] = library['Id']
obj['LibraryName'] = library['Name']
obj["MovieId"] = self.create_entry()
obj["LibraryId"] = library["Id"]
obj["LibraryName"] = library["Name"]
else:
if self.get(*values(obj, QU.get_movie_obj)) is None:
update = False
LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId'])
LOG.info(
"MovieId %s missing from kodi. repairing the entry.", obj["MovieId"]
)
obj['Path'] = API.get_file_path(obj['Path'])
obj['Genres'] = obj['Genres'] or []
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])]
obj['People'] = obj['People'] or []
obj['Genre'] = " / ".join(obj['Genres'])
obj['Writers'] = " / ".join(obj['Writers'] or [])
obj['Directors'] = " / ".join(obj['Directors'] or [])
obj['Plot'] = API.get_overview(obj['Plot'])
obj['Mpaa'] = API.get_mpaa(obj['Mpaa'])
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
obj['People'] = API.get_people_artwork(obj['People'])
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container'])
obj['Audio'] = API.audio_streams(obj['Audio'] or [])
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles'])
if obj['Premiere'] is not None:
obj['Premiere'] = str(obj['Premiere']).split('T')[0]
obj["Path"] = API.get_file_path(obj["Path"])
obj["Genres"] = obj["Genres"] or []
obj["Studios"] = [
API.validate_studio(studio) for studio in (obj["Studios"] or [])
]
obj["People"] = obj["People"] or []
obj["Genre"] = " / ".join(obj["Genres"])
obj["Writers"] = " / ".join(obj["Writers"] or [])
obj["Directors"] = " / ".join(obj["Directors"] or [])
obj["Plot"] = API.get_overview(obj["Plot"])
obj["Mpaa"] = API.get_mpaa(obj["Mpaa"])
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj["People"] = API.get_people_artwork(obj["People"])
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
obj["DatePlayed"] = (
None
if not obj["DatePlayed"]
else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
)
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"])
obj["Audio"] = API.audio_streams(obj["Audio"] or [])
obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"])
if obj["Premiere"] is not None:
obj["Premiere"] = str(obj["Premiere"]).split("T")[0]
self.get_path_filename(obj)
self.trailer(obj)
if obj['Countries']:
if obj["Countries"]:
self.add_countries(*values(obj, QU.update_country_obj))
tags = list(obj['Tags'] or [])
tags.append(obj['LibraryName'])
tags = list(obj["Tags"] or [])
tags.append(obj["LibraryName"])
if obj['Favorite']:
tags.append('Favorite movies')
if obj["Favorite"]:
tags.append("Favorite movies")
obj['Tags'] = tags
obj["Tags"] = tags
if update:
self.movie_update(obj)
@ -124,239 +142,289 @@ class Movies(KodiDb):
self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.add_people(*values(obj, QU.add_people_movie_obj))
self.add_streams(*values(obj, QU.add_streams_obj))
self.artwork.add(obj['Artwork'], obj['MovieId'], "movie")
self.item_ids.append(obj['Id'])
self.artwork.add(obj["Artwork"], obj["MovieId"], "movie")
self.item_ids.append(obj["Id"])
return not update
def movie_add(self, obj):
''' Add object to kodi.
'''
obj['RatingId'] = self.create_entry_rating()
"""Add object to kodi."""
obj["RatingId"] = self.create_entry_rating()
self.add_ratings(*values(obj, QU.add_rating_movie_obj))
obj['Unique'] = self.create_entry_unique_id()
obj["Unique"] = self.create_entry_unique_id()
self.add_unique_id(*values(obj, QU.add_unique_id_movie_obj))
obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj))
obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj))
obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj))
obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj))
self.add(*values(obj, QU.add_movie_obj))
self.add_videoversion(*values(obj, QU.add_video_version_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_movie_obj))
LOG.debug("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
LOG.debug(
"ADD movie [%s/%s/%s] %s: %s",
obj["PathId"],
obj["FileId"],
obj["MovieId"],
obj["Id"],
obj["Title"],
)
def movie_update(self, obj):
''' Update object to kodi.
'''
obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
"""Update object to kodi."""
obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
self.update_ratings(*values(obj, QU.update_rating_movie_obj))
obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj))
obj["Unique"] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj))
self.update_unique_id(*values(obj, QU.update_unique_id_movie_obj))
self.update(*values(obj, QU.update_movie_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
LOG.debug(
"UPDATE movie [%s/%s/%s] %s: %s",
obj["PathId"],
obj["FileId"],
obj["MovieId"],
obj["Id"],
obj["Title"],
)
def trailer(self, obj):
try:
if obj['LocalTrailer']:
if obj["LocalTrailer"]:
trailer = self.server.jellyfin.get_local_trailers(obj['Id'])
obj['Trailer'] = "plugin://plugin.video.jellyfin/trailer?id=%s&mode=play" % trailer[0]['Id']
trailer = self.server.jellyfin.get_local_trailers(obj["Id"])
obj["Trailer"] = (
"plugin://plugin.video.jellyfin/trailer?id=%s&mode=play"
% trailer[0]["Id"]
)
elif obj['Trailer']:
obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1]
elif obj["Trailer"]:
obj["Trailer"] = (
"plugin://plugin.video.youtube/play/?video_id=%s"
% obj["Trailer"].rsplit("=", 1)[1]
)
except Exception as error:
LOG.exception("Failed to get trailer: %s", error)
obj['Trailer'] = None
obj["Trailer"] = None
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]
"""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]
)
if self.direct_path:
if not validate(obj['Path']):
if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.")
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
'''check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO'''
if validate_dvd_dir(obj['Path'] + obj['Filename']):
obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/'
obj['Filename'] = 'VIDEO_TS.IFO'
LOG.debug("DVD directory %s", obj['Path'])
"""check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO"""
if validate_dvd_dir(obj["Path"] + obj["Filename"]):
obj["Path"] = obj["Path"] + obj["Filename"] + "/VIDEO_TS/"
obj["Filename"] = "VIDEO_TS.IFO"
LOG.debug("DVD directory %s", obj["Path"])
'''check bluray directories and point it to ./BDMV/index.bdmv'''
if validate_bluray_dir(obj['Path'] + obj['Filename']):
obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/'
obj['Filename'] = 'index.bdmv'
LOG.debug("Bluray directory %s", obj['Path'])
"""check bluray directories and point it to ./BDMV/index.bdmv"""
if validate_bluray_dir(obj["Path"] + obj["Filename"]):
obj["Path"] = obj["Path"] + obj["Filename"] + "/BDMV/"
obj["Filename"] = "index.bdmv"
LOG.debug("Bluray directory %s", obj["Path"])
else:
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId']
obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
params = {
'filename': py2_encode(obj['Filename'], 'utf-8'),
'id': obj['Id'],
'dbid': obj['MovieId'],
'mode': "play"
"filename": py2_encode(obj["Filename"], "utf-8"),
"id": obj["Id"],
"dbid": obj["MovieId"],
"mode": "play",
}
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params))
obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
@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.
''' If item does not exist, entry will be added.
If item exists, entry will be updated.
Process movies inside boxset.
Process removals from boxset.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
Process movies inside boxset.
Process removals from boxset.
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'Boxset')
obj = self.objects.map(item, "Boxset")
obj['Overview'] = API.get_overview(obj['Overview'])
obj["Overview"] = API.get_overview(obj["Overview"])
try:
obj['SetId'] = e_item[0]
obj["SetId"] = e_item[0]
self.update_boxset(*values(obj, QU.update_set_obj))
except TypeError:
LOG.debug("SetId %s not found", obj['Id'])
obj['SetId'] = self.add_boxset(*values(obj, QU.add_set_obj))
LOG.debug("SetId %s not found", obj["Id"])
obj["SetId"] = self.add_boxset(*values(obj, QU.add_set_obj))
self.boxset_current(obj)
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
for movie in obj['Current']:
for movie in obj["Current"]:
temp_obj = dict(obj)
temp_obj['Movie'] = movie
temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']]
temp_obj["Movie"] = movie
temp_obj["MovieId"] = obj["Current"][temp_obj["Movie"]]
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj))
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj))
LOG.debug("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId'])
self.jellyfin_db.update_parent_id(
*values(temp_obj, QUEM.delete_parent_boxset_obj)
)
LOG.debug(
"DELETE from boxset [%s] %s: %s",
temp_obj["SetId"],
temp_obj["Title"],
temp_obj["MovieId"],
)
self.artwork.add(obj['Artwork'], obj['SetId'], "set")
self.artwork.add(obj["Artwork"], obj["SetId"], "set")
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_boxset_obj))
LOG.debug("UPDATE boxset [%s] %s", obj['SetId'], obj['Title'])
LOG.debug("UPDATE boxset [%s] %s", obj["SetId"], obj["Title"])
def boxset_current(self, obj):
''' Add or removes movies based on the current movies found in the boxset.
'''
"""Add or removes movies based on the current movies found in the boxset."""
try:
current = self.jellyfin_db.get_item_id_by_parent_id(*values(obj, QUEM.get_item_id_by_parent_boxset_obj))
current = self.jellyfin_db.get_item_id_by_parent_id(
*values(obj, QUEM.get_item_id_by_parent_boxset_obj)
)
movies = dict(current)
except ValueError:
movies = {}
obj['Current'] = movies
obj["Current"] = movies
for all_movies in server.get_movies_by_boxset(obj['Id']):
for movie in all_movies['Items']:
for all_movies in server.get_movies_by_boxset(obj["Id"]):
for movie in all_movies["Items"]:
temp_obj = dict(obj)
temp_obj['Title'] = movie['Name']
temp_obj['Id'] = movie['Id']
temp_obj["Title"] = movie["Name"]
temp_obj["Id"] = movie["Id"]
try:
temp_obj['MovieId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
temp_obj["MovieId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except TypeError:
LOG.info("Failed to process %s to boxset.", temp_obj['Title'])
LOG.info("Failed to process %s to boxset.", temp_obj["Title"])
continue
if temp_obj['Id'] not in obj['Current']:
if temp_obj["Id"] not in obj["Current"]:
self.set_boxset(*values(temp_obj, QU.update_movie_set_obj))
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_movie_obj))
LOG.debug("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id'])
self.jellyfin_db.update_parent_id(
*values(temp_obj, QUEM.update_parent_movie_obj)
)
LOG.debug(
"ADD to boxset [%s/%s] %s: %s to boxset",
temp_obj["SetId"],
temp_obj["MovieId"],
temp_obj["Title"],
temp_obj["Id"],
)
else:
obj['Current'].pop(temp_obj['Id'])
obj["Current"].pop(temp_obj["Id"])
def boxsets_reset(self):
''' Special function to remove all existing boxsets.
'''
boxsets = self.jellyfin_db.get_items_by_media('set')
"""Special function to remove all existing boxsets."""
boxsets = self.jellyfin_db.get_items_by_media("set")
for boxset in boxsets:
self.remove(boxset[0])
@stop
@jellyfin_item
def userdata(self, item, e_item):
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'MovieUserData')
obj = self.objects.map(item, "MovieUserData")
try:
obj['MovieId'] = e_item[0]
obj['FileId'] = e_item[1]
obj["MovieId"] = e_item[0]
obj["FileId"] = e_item[1]
except TypeError:
return
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'])
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"])
if obj['DatePlayed']:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
if obj["DatePlayed"]:
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
if obj['Favorite']:
if obj["Favorite"]:
self.get_tag(*values(obj, QU.get_tag_movie_obj))
else:
self.remove_tag(*values(obj, QU.delete_tag_movie_obj))
LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume'])
LOG.debug("New resume point %s: %s", obj["Id"], obj["Resume"])
self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
LOG.debug(
"USERDATA movie [%s/%s] %s: %s",
obj["FileId"],
obj["MovieId"],
obj["Id"],
obj["Title"],
)
@stop
@jellyfin_item
def remove(self, item_id, e_item):
''' Remove movieid, fileid, jellyfin reference.
Remove artwork, boxset
'''
obj = {'Id': item_id}
"""Remove movieid, fileid, jellyfin reference.
Remove artwork, boxset
"""
obj = {"Id": item_id}
try:
obj['KodiId'] = e_item[0]
obj['FileId'] = e_item[1]
obj['Media'] = e_item[4]
obj["KodiId"] = e_item[0]
obj["FileId"] = e_item[1]
obj["Media"] = e_item[4]
except TypeError:
return
self.artwork.delete(obj['KodiId'], obj['Media'])
self.artwork.delete(obj["KodiId"], obj["Media"])
if obj['Media'] == 'movie':
if obj["Media"] == "movie":
self.delete(*values(obj, QU.delete_movie_obj))
elif obj['Media'] == 'set':
elif obj["Media"] == "set":
for movie in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_movie_obj)):
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]
temp_obj["MovieId"] = movie[1]
temp_obj["Movie"] = movie[0]
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj))
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj))
self.jellyfin_db.update_parent_id(
*values(temp_obj, QUEM.delete_parent_boxset_obj)
)
self.delete_boxset(*values(obj, QU.delete_set_obj))
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
LOG.debug("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'])
LOG.debug(
"DELETE %s [%s/%s] %s",
obj["Media"],
obj["FileId"],
obj["KodiId"],
obj["Id"],
)

View File

@ -39,19 +39,20 @@ class Music(KodiDb):
@stop
@jellyfin_item
def artist(self, item, e_item):
''' If item does not exist, entry will be added.
If item exists, entry will be updated.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""If item does not exist, entry will be added.
If item exists, entry will be updated.
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'Artist')
obj = self.objects.map(item, "Artist")
update = True
try:
obj['ArtistId'] = e_item[0]
obj['LibraryId'] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
obj["ArtistId"] = e_item[0]
obj["LibraryId"] = e_item[6]
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError:
update = False
@ -60,72 +61,81 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library
return
obj['ArtistId'] = None
obj['LibraryId'] = library['Id']
obj['LibraryName'] = library['Name']
LOG.debug("ArtistId %s not found", obj['Id'])
obj["ArtistId"] = None
obj["LibraryId"] = library["Id"]
obj["LibraryName"] = library["Name"]
LOG.debug("ArtistId %s not found", obj["Id"])
else:
if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None:
update = False
LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId'])
LOG.info(
"ArtistId %s missing from kodi. repairing the entry.",
obj["ArtistId"],
)
obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
obj['ArtistType'] = "MusicArtist"
obj['Genre'] = " / ".join(obj['Genres'] or [])
obj['Bio'] = API.get_overview(obj['Bio'])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
obj['Thumb'] = obj['Artwork']['Primary']
obj['Backdrops'] = obj['Artwork']['Backdrop'] or ""
obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
obj["ArtistType"] = "MusicArtist"
obj["Genre"] = " / ".join(obj["Genres"] or [])
obj["Bio"] = API.get_overview(obj["Bio"])
obj["Artwork"] = API.get_all_artwork(
self.objects.map(item, "ArtworkMusic"), True
)
obj["Thumb"] = obj["Artwork"]["Primary"]
obj["Backdrops"] = obj["Artwork"]["Backdrop"] or ""
if obj['Thumb']:
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb']
if obj["Thumb"]:
obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
if obj['Backdrops']:
obj['Backdrops'] = "<fanart>%s</fanart>" % obj['Backdrops'][0]
if obj["Backdrops"]:
obj["Backdrops"] = "<fanart>%s</fanart>" % obj["Backdrops"][0]
if update:
self.artist_update(obj)
else:
self.artist_add(obj)
self.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['ArtistId'])
self.artwork.add(obj['Artwork'], obj['ArtistId'], "artist")
self.item_ids.append(obj['Id'])
self.update(
obj["Genre"],
obj["Bio"],
obj["Thumb"],
obj["Backdrops"],
obj["LastScraped"],
obj["ArtistId"],
)
self.artwork.add(obj["Artwork"], obj["ArtistId"], "artist")
self.item_ids.append(obj["Id"])
def artist_add(self, obj):
"""Add object to kodi.
''' Add object to kodi.
safety checks: It looks like Jellyfin supports the same artist multiple times.
Kodi doesn't allow that. In case that happens we just merge the artist entries.
'''
obj['ArtistId'] = self.get(*values(obj, QU.get_artist_obj))
safety checks: It looks like Jellyfin supports the same artist multiple times.
Kodi doesn't allow that. In case that happens we just merge the artist entries.
"""
obj["ArtistId"] = self.get(*values(obj, QU.get_artist_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_artist_obj))
LOG.debug("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id'])
LOG.debug("ADD artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"])
def artist_update(self, obj):
''' Update object to kodi.
'''
"""Update object to kodi."""
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id'])
LOG.debug("UPDATE artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"])
@stop
@jellyfin_item
def album(self, item, e_item):
''' Update object to kodi.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""Update object to kodi."""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'Album')
obj = self.objects.map(item, "Album")
update = True
try:
obj['AlbumId'] = e_item[0]
obj['LibraryId'] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
obj["AlbumId"] = e_item[0]
obj["LibraryId"] = e_item[6]
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError:
update = False
@ -134,31 +144,35 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library
return
obj['AlbumId'] = None
obj['LibraryId'] = library['Id']
obj['LibraryName'] = library['Name']
LOG.debug("AlbumId %s not found", obj['Id'])
obj["AlbumId"] = None
obj["LibraryId"] = library["Id"]
obj["LibraryName"] = library["Name"]
LOG.debug("AlbumId %s not found", obj["Id"])
else:
if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None:
update = False
LOG.info("AlbumId %s missing from kodi. repairing the entry.", obj['AlbumId'])
LOG.info(
"AlbumId %s missing from kodi. repairing the entry.", obj["AlbumId"]
)
obj['Rating'] = 0
obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
obj['Genres'] = obj['Genres'] or []
obj['Genre'] = " / ".join(obj['Genres'])
obj['Bio'] = API.get_overview(obj['Bio'])
obj['Artists'] = " / ".join(obj['Artists'] or [])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
obj['Thumb'] = obj['Artwork']['Primary']
obj['DateAdded'] = item.get('DateCreated')
obj["Rating"] = 0
obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
obj["Genres"] = obj["Genres"] or []
obj["Genre"] = " / ".join(obj["Genres"])
obj["Bio"] = API.get_overview(obj["Bio"])
obj["Artists"] = " / ".join(obj["Artists"] or [])
obj["Artwork"] = API.get_all_artwork(
self.objects.map(item, "ArtworkMusic"), True
)
obj["Thumb"] = obj["Artwork"]["Primary"]
obj["DateAdded"] = item.get("DateCreated")
if obj['DateAdded']:
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
if obj["DateAdded"]:
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
if obj['Thumb']:
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb']
if obj["Thumb"]:
obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
if update:
self.album_update(obj)
@ -169,89 +183,90 @@ class Music(KodiDb):
self.artist_discography(obj)
self.update_album(*values(obj, QU.update_album_obj))
self.add_genres(*values(obj, QU.add_genres_obj))
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album")
self.item_ids.append(obj['Id'])
self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
self.item_ids.append(obj["Id"])
def album_add(self, obj):
''' Add object to kodi.
'''
"""Add object to kodi."""
if self.version_id >= 82:
obj_values = values(obj, QU.get_album_obj82)
else:
obj_values = values(obj, QU.get_album_obj)
obj['AlbumId'] = self.get_album(*obj_values)
obj["AlbumId"] = self.get_album(*obj_values)
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_album_obj))
LOG.debug("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id'])
LOG.debug("ADD album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"])
def album_update(self, obj):
''' Update object to kodi.
'''
"""Update object to kodi."""
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id'])
LOG.debug("UPDATE album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"])
def artist_discography(self, obj):
''' Update the artist's discography.
'''
for artist in (obj['ArtistItems'] or []):
"""Update the artist's discography."""
for artist in obj["ArtistItems"] or []:
temp_obj = dict(obj)
temp_obj['Id'] = artist['Id']
temp_obj['AlbumId'] = obj['Id']
temp_obj["Id"] = artist["Id"]
temp_obj["AlbumId"] = obj["Id"]
try:
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except TypeError:
continue
self.add_discography(*values(temp_obj, QU.update_discography_obj))
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_album_obj))
self.jellyfin_db.update_parent_id(
*values(temp_obj, QUEM.update_parent_album_obj)
)
def artist_link(self, obj):
''' Assign main artists to album.
Artist does not exist in jellyfin database, create the reference.
'''
for artist in (obj['AlbumArtists'] or []):
"""Assign main artists to album.
Artist does not exist in jellyfin database, create the reference.
"""
for artist in obj["AlbumArtists"] or []:
temp_obj = dict(obj)
temp_obj['Name'] = artist['Name']
temp_obj['Id'] = artist['Id']
temp_obj["Name"] = artist["Name"]
temp_obj["Id"] = artist["Id"]
try:
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except TypeError:
try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error:
LOG.exception(error)
continue
self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj))
self.link(*values(temp_obj, QU.update_link_obj))
self.item_ids.append(temp_obj['Id'])
self.item_ids.append(temp_obj["Id"])
@stop
@jellyfin_item
def song(self, item, e_item):
''' Update object to kodi.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""Update object to kodi."""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'Song')
obj = self.objects.map(item, "Song")
update = True
try:
obj['SongId'] = e_item[0]
obj['PathId'] = e_item[2]
obj['AlbumId'] = e_item[3]
obj['LibraryId'] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
obj["SongId"] = e_item[0]
obj["PathId"] = e_item[2]
obj["AlbumId"] = e_item[3]
obj["LibraryId"] = e_item[6]
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError:
update = False
@ -260,38 +275,42 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library
return
obj['SongId'] = self.create_entry_song()
obj['LibraryId'] = library['Id']
obj['LibraryName'] = library['Name']
LOG.debug("SongId %s not found", obj['Id'])
obj["SongId"] = self.create_entry_song()
obj["LibraryId"] = library["Id"]
obj["LibraryName"] = library["Name"]
LOG.debug("SongId %s not found", obj["Id"])
else:
if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None:
update = False
LOG.info("SongId %s missing from kodi. repairing the entry.", obj['SongId'])
LOG.info(
"SongId %s missing from kodi. repairing the entry.", obj["SongId"]
)
self.get_song_path_filename(obj, API)
obj['Rating'] = 0
obj['Genres'] = obj['Genres'] or []
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0
obj['Genre'] = " / ".join(obj['Genres'])
obj['Artists'] = " / ".join(obj['Artists'] or [])
obj['AlbumArtists'] = obj['AlbumArtists'] or []
obj['Index'] = obj['Index'] or 0
obj['Disc'] = obj['Disc'] or 1
obj['EmbedCover'] = False
obj['Comment'] = API.get_overview(obj['Comment'])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
obj["Rating"] = 0
obj["Genres"] = obj["Genres"] or []
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
obj["Runtime"] = (obj["Runtime"] or 0) / 10000000.0
obj["Genre"] = " / ".join(obj["Genres"])
obj["Artists"] = " / ".join(obj["Artists"] or [])
obj["AlbumArtists"] = obj["AlbumArtists"] or []
obj["Index"] = obj["Index"] or 0
obj["Disc"] = obj["Disc"] or 1
obj["EmbedCover"] = False
obj["Comment"] = API.get_overview(obj["Comment"])
obj["Artwork"] = API.get_all_artwork(
self.objects.map(item, "ArtworkMusic"), True
)
if obj['DateAdded']:
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
if obj["DateAdded"]:
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
if obj['DatePlayed']:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
if obj["DatePlayed"]:
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index']
obj["Index"] = obj["Disc"] * 2**16 + obj["Index"]
if update:
self.song_update(obj)
@ -303,227 +322,275 @@ class Music(KodiDb):
self.song_artist_link(obj)
self.song_artist_discography(obj)
obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists'])
obj["strAlbumArtists"] = " / ".join(obj["AlbumArtists"])
self.get_album_artist(*values(obj, QU.get_album_artist_obj))
self.add_genres(*values(obj, QU.update_genre_song_obj))
self.artwork.add(obj['Artwork'], obj['SongId'], "song")
self.item_ids.append(obj['Id'])
self.artwork.add(obj["Artwork"], obj["SongId"], "song")
self.item_ids.append(obj["Id"])
if obj['SongAlbumId'] is None:
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album")
if obj["SongAlbumId"] is None:
self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
return not update
def song_add(self, obj):
"""Add object to kodi.
''' Add object to kodi.
Verify if there's an album associated.
If no album found, create a single's album
'''
obj['PathId'] = self.add_path(obj['Path'])
Verify if there's an album associated.
If no album found, create a single's album
"""
obj["PathId"] = self.add_path(obj["Path"])
try:
obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0]
obj["AlbumId"] = self.jellyfin_db.get_item_by_id(
*values(obj, QUEM.get_item_song_obj)
)[0]
except TypeError:
try:
if obj['SongAlbumId'] is None:
if obj["SongAlbumId"] is None:
raise TypeError("No album id found associated?")
self.album(self.server.jellyfin.get_item(obj['SongAlbumId']))
obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0]
self.album(self.server.jellyfin.get_item(obj["SongAlbumId"]))
obj["AlbumId"] = self.jellyfin_db.get_item_by_id(
*values(obj, QUEM.get_item_song_obj)
)[0]
except TypeError:
self.single(obj)
self.add_song(*values(obj, QU.add_song_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_song_obj))
LOG.debug("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])
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.
'''
"""Update object to kodi."""
self.update_path(*values(obj, QU.update_path_obj))
self.update_song(*values(obj, QU.update_song_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])
LOG.debug(
"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'])
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1]
"""Get the path and filename and build it into protocol://path"""
obj["Path"] = api.get_file_path(obj["Path"])
obj["Filename"] = (
obj["Path"].rsplit("\\", 1)[1]
if "\\" in obj["Path"]
else obj["Path"].rsplit("/", 1)[1]
)
if self.direct_path:
if not validate(obj['Path']):
if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.")
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
else:
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
obj['Path'] = "%s/Audio/%s/" % (server_address, obj['Id'])
obj['Filename'] = "stream.%s?static=true" % obj['Container']
server_address = self.server.auth.get_server_info(
self.server.auth.server_id
)["address"]
obj["Path"] = "%s/Audio/%s/" % (server_address, obj["Id"])
obj["Filename"] = "stream.%s?static=true" % obj["Container"]
def song_artist_discography(self, obj):
''' Update the artist's discography.
'''
"""Update the artist's discography."""
artists = []
for artist in (obj['AlbumArtists'] or []):
for artist in obj["AlbumArtists"] or []:
temp_obj = dict(obj)
temp_obj['Name'] = artist['Name']
temp_obj['Id'] = artist['Id']
temp_obj["Name"] = artist["Name"]
temp_obj["Id"] = artist["Id"]
artists.append(temp_obj['Name'])
artists.append(temp_obj["Name"])
try:
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except TypeError:
try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error:
LOG.exception(error)
continue
self.link(*values(temp_obj, QU.update_link_obj))
self.item_ids.append(temp_obj['Id'])
self.item_ids.append(temp_obj["Id"])
if obj['Album']:
if obj["Album"]:
temp_obj['Title'] = obj['Album']
temp_obj['Year'] = 0
temp_obj["Title"] = obj["Album"]
temp_obj["Year"] = 0
self.add_discography(*values(temp_obj, QU.update_discography_obj))
obj['AlbumArtists'] = artists
obj["AlbumArtists"] = artists
def song_artist_link(self, obj):
''' Assign main artists to song.
Artist does not exist in jellyfin database, create the reference.
'''
for index, artist in enumerate(obj['ArtistItems'] or []):
"""Assign main artists to song.
Artist does not exist in jellyfin database, create the reference.
"""
for index, artist in enumerate(obj["ArtistItems"] or []):
temp_obj = dict(obj)
temp_obj['Name'] = artist['Name']
temp_obj['Id'] = artist['Id']
temp_obj['Index'] = index
temp_obj["Name"] = artist["Name"]
temp_obj["Id"] = artist["Id"]
temp_obj["Index"] = index
try:
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except TypeError:
try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error:
LOG.exception(error)
continue
self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj))
self.item_ids.append(temp_obj['Id'])
self.item_ids.append(temp_obj["Id"])
def single(self, obj):
obj['AlbumId'] = self.create_entry_album()
obj["AlbumId"] = self.create_entry_album()
self.add_single(*values(obj, QU.add_single_obj))
@stop
@jellyfin_item
def userdata(self, item, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
"""
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
'''
obj = self.objects.map(item, 'SongUserData')
obj = self.objects.map(item, "SongUserData")
try:
obj['KodiId'] = e_item[0]
obj['Media'] = e_item[4]
obj["KodiId"] = e_item[0]
obj["Media"] = e_item[4]
except TypeError:
return
obj['Rating'] = 0
obj["Rating"] = 0
if obj['Media'] == 'song':
if obj["Media"] == "song":
if obj['DatePlayed']:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
if obj["DatePlayed"]:
obj["DatePlayed"] = (
Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
)
self.rate_song(*values(obj, QU.update_song_rating_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title'])
LOG.debug(
"USERDATA %s [%s] %s: %s",
obj["Media"],
obj["KodiId"],
obj["Id"],
obj["Title"],
)
@stop
@jellyfin_item
def remove(self, item_id, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
This should address single song scenario, where server doesn't actually
create an album for the song.
'''
obj = {'Id': item_id}
This should address single song scenario, where server doesn't actually
create an album for the song.
"""
obj = {"Id": item_id}
try:
obj['KodiId'] = e_item[0]
obj['Media'] = e_item[4]
obj["KodiId"] = e_item[0]
obj["Media"] = e_item[4]
except TypeError:
return
if obj['Media'] == 'song':
if obj["Media"] == "song":
self.remove_song(obj['KodiId'], obj['Id'])
self.jellyfin_db.remove_wild_item(obj['Id'])
self.remove_song(obj["KodiId"], obj["Id"])
self.jellyfin_db.remove_wild_item(obj["Id"])
for item in self.jellyfin_db.get_item_by_wild_id(*values(obj, QUEM.get_item_by_wild_obj)):
if item[1] == 'album':
for item in self.jellyfin_db.get_item_by_wild_id(
*values(obj, QUEM.get_item_by_wild_obj)
):
if item[1] == "album":
temp_obj = dict(obj)
temp_obj['ParentId'] = item[0]
temp_obj["ParentId"] = item[0]
if not self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
self.remove_album(temp_obj['ParentId'], obj['Id'])
if not self.jellyfin_db.get_item_by_parent_id(
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_album(temp_obj["ParentId"], obj["Id"])
elif obj['Media'] == 'album':
obj['ParentId'] = obj['KodiId']
elif obj["Media"] == "album":
obj["ParentId"] = obj["KodiId"]
for song in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_song_obj)):
self.remove_song(song[1], obj['Id'])
for song in self.jellyfin_db.get_item_by_parent_id(
*values(obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_song(song[1], obj["Id"])
else:
self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_song_obj))
self.jellyfin_db.remove_items_by_parent_id(
*values(obj, QUEM.delete_item_by_parent_song_obj)
)
self.remove_album(obj['KodiId'], obj['Id'])
self.remove_album(obj["KodiId"], obj["Id"])
elif obj['Media'] == 'artist':
obj['ParentId'] = obj['KodiId']
elif obj["Media"] == "artist":
obj["ParentId"] = obj["KodiId"]
for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)):
for album in self.jellyfin_db.get_item_by_parent_id(
*values(obj, QUEM.get_item_by_parent_album_obj)
):
temp_obj = dict(obj)
temp_obj['ParentId'] = album[1]
temp_obj["ParentId"] = album[1]
for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
self.remove_song(song[1], obj['Id'])
for song in self.jellyfin_db.get_item_by_parent_id(
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_song(song[1], obj["Id"])
else:
self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_song_obj))
self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_artist_obj))
self.remove_album(temp_obj['ParentId'], obj['Id'])
self.jellyfin_db.remove_items_by_parent_id(
*values(temp_obj, QUEM.delete_item_by_parent_song_obj)
)
self.jellyfin_db.remove_items_by_parent_id(
*values(temp_obj, QUEM.delete_item_by_parent_artist_obj)
)
self.remove_album(temp_obj["ParentId"], obj["Id"])
else:
self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_album_obj))
self.jellyfin_db.remove_items_by_parent_id(
*values(obj, QUEM.delete_item_by_parent_album_obj)
)
self.remove_artist(obj['KodiId'], obj['Id'])
self.remove_artist(obj["KodiId"], obj["Id"])
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
@ -547,29 +614,31 @@ class Music(KodiDb):
@jellyfin_item
def get_child(self, item_id, e_item):
''' Get all child elements from tv show jellyfin id.
'''
obj = {'Id': item_id}
"""Get all child elements from tv show jellyfin id."""
obj = {"Id": item_id}
child = []
try:
obj['KodiId'] = e_item[0]
obj['FileId'] = e_item[1]
obj['ParentId'] = e_item[3]
obj['Media'] = e_item[4]
obj["KodiId"] = e_item[0]
obj["FileId"] = e_item[1]
obj["ParentId"] = e_item[3]
obj["Media"] = e_item[4]
except TypeError:
return child
obj['ParentId'] = obj['KodiId']
obj["ParentId"] = obj["KodiId"]
for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)):
for album in self.jellyfin_db.get_item_by_parent_id(
*values(obj, QUEM.get_item_by_parent_album_obj)
):
temp_obj = dict(obj)
temp_obj['ParentId'] = album[1]
temp_obj["ParentId"] = album[1]
child.append((album[0],))
for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
for song in self.jellyfin_db.get_item_by_parent_id(
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
):
child.append((song[0],))
return child

View File

@ -43,24 +43,25 @@ class MusicVideos(KodiDb):
@stop
@jellyfin_item
def musicvideo(self, item, e_item):
"""If item does not exist, entry will be added.
If item exists, entry will be updated.
''' If item does not exist, entry will be added.
If item exists, entry will be updated.
If we don't get the track number from Jellyfin, see if we can infer it
from the sortname attribute.
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
If we don't get the track number from Jellyfin, see if we can infer it
from the sortname attribute.
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'MusicVideo')
obj = self.objects.map(item, "MusicVideo")
update = True
try:
obj['MvideoId'] = e_item[0]
obj['FileId'] = e_item[1]
obj['PathId'] = e_item[2]
obj['LibraryId'] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
obj["MvideoId"] = e_item[0]
obj["FileId"] = e_item[1]
obj["PathId"] = e_item[2]
obj["LibraryId"] = e_item[6]
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError:
update = False
@ -69,67 +70,80 @@ class MusicVideos(KodiDb):
# This item doesn't belong to a whitelisted library
return
LOG.debug("MvideoId for %s not found", obj['Id'])
obj['MvideoId'] = self.create_entry()
obj['LibraryId'] = library['Id']
obj['LibraryName'] = library['Name']
LOG.debug("MvideoId for %s not found", obj["Id"])
obj["MvideoId"] = self.create_entry()
obj["LibraryId"] = library["Id"]
obj["LibraryName"] = library["Name"]
else:
if self.get(*values(obj, QU.get_musicvideo_obj)) is None:
update = False
LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId'])
LOG.info(
"MvideoId %s missing from kodi. repairing the entry.",
obj["MvideoId"],
)
if (obj.get('ProductionYear') or 0) > 9999:
obj['ProductionYear'] = int(str(obj['ProductionYear'])[:4])
if (obj.get("ProductionYear") or 0) > 9999:
obj["ProductionYear"] = int(str(obj["ProductionYear"])[:4])
if (obj.get('Year') or 0) > 9999:
obj['Year'] = int(str(obj['Year'])[:4])
if (obj.get("Year") or 0) > 9999:
obj["Year"] = int(str(obj["Year"])[:4])
obj['Path'] = API.get_file_path(obj['Path'])
obj['Genres'] = obj['Genres'] or []
obj['ArtistItems'] = obj['ArtistItems'] or []
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])]
obj['Plot'] = API.get_overview(obj['Plot'])
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
obj['Premiere'] = Local(obj['Premiere']) if obj['Premiere'] else datetime.date(obj['Year'] or 1970, 1, 1)
obj['Genre'] = " / ".join(obj['Genres'])
obj['Studio'] = " / ".join(obj['Studios'])
obj['Artists'] = " / ".join(obj['Artists'] or [])
obj['Directors'] = " / ".join(obj['Directors'] or [])
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container'])
obj['Audio'] = API.audio_streams(obj['Audio'] or [])
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles'])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
obj["Path"] = API.get_file_path(obj["Path"])
obj["Genres"] = obj["Genres"] or []
obj["ArtistItems"] = obj["ArtistItems"] or []
obj["Studios"] = [
API.validate_studio(studio) for studio in (obj["Studios"] or [])
]
obj["Plot"] = API.get_overview(obj["Plot"])
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
obj["DatePlayed"] = (
None
if not obj["DatePlayed"]
else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
)
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj["Premiere"] = (
Local(obj["Premiere"])
if obj["Premiere"]
else datetime.date(obj["Year"] or 1970, 1, 1)
)
obj["Genre"] = " / ".join(obj["Genres"])
obj["Studio"] = " / ".join(obj["Studios"])
obj["Artists"] = " / ".join(obj["Artists"] or [])
obj["Directors"] = " / ".join(obj["Directors"] or [])
obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"])
obj["Audio"] = API.audio_streams(obj["Audio"] or [])
obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"])
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
self.get_path_filename(obj)
if obj['Premiere']:
obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ")
if obj["Premiere"]:
obj["Premiere"] = str(obj["Premiere"]).split(".")[0].replace("T", " ")
for artist in obj['ArtistItems']:
artist['Type'] = "Artist"
for artist in obj["ArtistItems"]:
artist["Type"] = "Artist"
obj['People'] = obj['People'] or [] + obj['ArtistItems']
obj['People'] = API.get_people_artwork(obj['People'])
obj["People"] = obj["People"] or [] + obj["ArtistItems"]
obj["People"] = API.get_people_artwork(obj["People"])
if obj['Index'] is None and obj['SortTitle'] is not None:
search = re.search(r'^\d+\s?', obj['SortTitle'])
if obj["Index"] is None and obj["SortTitle"] is not None:
search = re.search(r"^\d+\s?", obj["SortTitle"])
if search:
obj['Index'] = search.group()
obj["Index"] = search.group()
tags = []
tags.extend(obj['Tags'] or [])
tags.append(obj['LibraryName'])
tags.extend(obj["Tags"] or [])
tags.append(obj["LibraryName"])
if obj['Favorite']:
tags.append('Favorite musicvideos')
if obj["Favorite"]:
tags.append("Favorite musicvideos")
obj['Tags'] = tags
obj["Tags"] = tags
if update:
self.musicvideo_update(obj)
@ -144,106 +158,129 @@ class MusicVideos(KodiDb):
self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.add_people(*values(obj, QU.add_people_mvideo_obj))
self.add_streams(*values(obj, QU.add_streams_obj))
self.artwork.add(obj['Artwork'], obj['MvideoId'], "musicvideo")
self.item_ids.append(obj['Id'])
self.artwork.add(obj["Artwork"], obj["MvideoId"], "musicvideo")
self.item_ids.append(obj["Id"])
return not update
def musicvideo_add(self, obj):
''' Add object to kodi.
'''
obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj))
obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj))
"""Add object to kodi."""
obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj))
obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj))
self.add(*values(obj, QU.add_musicvideo_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_mvideo_obj))
LOG.debug("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
LOG.debug(
"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.
'''
"""Update object to kodi."""
self.update(*values(obj, QU.update_musicvideo_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
LOG.debug(
"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]
"""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]
)
if self.direct_path:
if not validate(obj['Path']):
if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.")
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
else:
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId']
obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
params = {
'filename': py2_encode(obj['Filename'], 'utf-8'),
'id': obj['Id'],
'dbid': obj['MvideoId'],
'mode': "play"
"filename": py2_encode(obj["Filename"], "utf-8"),
"id": obj["Id"],
"dbid": obj["MvideoId"],
"mode": "play",
}
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params))
obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
@stop
@jellyfin_item
def userdata(self, item, e_item):
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
'''
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
"""
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
"address"
]
API = api.API(item, server_address)
obj = self.objects.map(item, 'MusicVideoUserData')
obj = self.objects.map(item, "MusicVideoUserData")
try:
obj['MvideoId'] = e_item[0]
obj['FileId'] = e_item[1]
obj["MvideoId"] = e_item[0]
obj["FileId"] = e_item[1]
except TypeError:
return
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'])
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"])
if obj['DatePlayed']:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
if obj["DatePlayed"]:
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
if obj['Favorite']:
if obj["Favorite"]:
self.get_tag(*values(obj, QU.get_tag_mvideo_obj))
else:
self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj))
self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
LOG.debug("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
LOG.debug(
"USERDATA mvideo [%s/%s] %s: %s",
obj["FileId"],
obj["MvideoId"],
obj["Id"],
obj["Title"],
)
@stop
@jellyfin_item
def remove(self, item_id, e_item):
''' Remove mvideoid, fileid, pathid, jellyfin reference.
'''
obj = {'Id': item_id}
"""Remove mvideoid, fileid, pathid, jellyfin reference."""
obj = {"Id": item_id}
try:
obj['MvideoId'] = e_item[0]
obj['FileId'] = e_item[1]
obj['PathId'] = e_item[2]
obj["MvideoId"] = e_item[0]
obj["FileId"] = e_item[1]
obj["PathId"] = e_item[2]
except TypeError:
return
self.artwork.delete(obj['MvideoId'], "musicvideo")
self.artwork.delete(obj["MvideoId"], "musicvideo")
self.delete(*values(obj, QU.delete_musicvideo_obj))
if self.direct_path:
self.remove_path(*values(obj, QU.delete_path_obj))
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
LOG.debug("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id'])
LOG.debug(
"DELETE musicvideo %s [%s/%s] %s",
obj["MvideoId"],
obj["PathId"],
obj["FileId"],
obj["Id"],
)

View File

@ -23,35 +23,30 @@ class Objects(object):
_shared_state = {}
def __init__(self):
''' Hold all persistent data here.
'''
"""Hold all persistent data here."""
self.__dict__ = self._shared_state
def mapping(self):
''' Load objects mapping.
'''
"""Load objects mapping."""
file_dir = os.path.dirname(ensure_text(__file__, get_filesystem_encoding()))
with open(os.path.join(file_dir, 'obj_map.json')) as infile:
with open(os.path.join(file_dir, "obj_map.json")) as infile:
self.objects = json.load(infile)
def map(self, item, mapping_name):
"""Syntax to traverse the item dictionary.
This of the query almost as a url.
''' Syntax to traverse the item dictionary.
This of the query almost as a url.
Item is the Jellyfin item json object structure
Item is the Jellyfin item json object structure
",": each element will be used as a fallback until a value is found.
"?": split filters and key name from the query part, i.e. MediaSources/0?$Name
"$": lead the key name with $. Only one key value can be requested per element.
":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name
MediaStreams is a list.
"/": indicates where to go directly
'''
",": each element will be used as a fallback until a value is found.
"?": split filters and key name from the query part, i.e. MediaSources/0?$Name
"$": lead the key name with $. Only one key value can be requested per element.
":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name
MediaStreams is a list.
"/": indicates where to go directly
"""
self.mapped_item = {}
if not mapping_name:
@ -62,7 +57,7 @@ class Objects(object):
for key, value in iteritems(mapping):
self.mapped_item[key] = None
params = value.split(',')
params = value.split(",")
for param in params:
@ -71,19 +66,19 @@ class Objects(object):
obj_key = ""
obj_filters = {}
if '?' in obj_param:
if "?" in obj_param:
if '$' in obj_param:
obj_param, obj_key = obj_param.rsplit('$', 1)
if "$" in obj_param:
obj_param, obj_key = obj_param.rsplit("$", 1)
obj_param, filters = obj_param.rsplit('?', 1)
obj_param, filters = obj_param.rsplit("?", 1)
if filters:
for filter in filters.split('&'):
filter_key, filter_value = filter.split('=')
for filter in filters.split("&"):
filter_key, filter_value = filter.split("=")
obj_filters[filter_key] = filter_value
if ':' in obj_param:
if ":" in obj_param:
result = []
for d in self.__recursiveloop__(obj, obj_param):
@ -94,7 +89,7 @@ class Objects(object):
obj = result
obj_filters = {}
elif '/' in obj_param:
elif "/" in obj_param:
obj = self.__recursive__(obj, obj_param)
elif obj is item and obj is not None:
@ -107,21 +102,31 @@ class Objects(object):
continue
if obj_key:
obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key)
obj = (
[d[obj_key] for d in obj if d.get(obj_key)]
if type(obj) == list
else obj.get(obj_key)
)
self.mapped_item[key] = obj
break
if not mapping_name.startswith('Browse') and not mapping_name.startswith('Artwork') and not mapping_name.startswith('UpNext'):
if (
not mapping_name.startswith("Browse")
and not mapping_name.startswith("Artwork")
and not mapping_name.startswith("UpNext")
):
self.mapped_item['ProviderName'] = self.objects.get('%sProviderName' % mapping_name)
self.mapped_item['Checksum'] = json.dumps(item['UserData'])
self.mapped_item["ProviderName"] = self.objects.get(
"%sProviderName" % mapping_name
)
self.mapped_item["Checksum"] = json.dumps(item["UserData"])
return self.mapped_item
def __recursiveloop__(self, obj, keys):
first, rest = keys.split(':', 1)
first, rest = keys.split(":", 1)
obj = self.__recursive__(obj, first)
if obj:
@ -133,7 +138,7 @@ class Objects(object):
def __recursive__(self, obj, keys):
for string in keys.split('/'):
for string in keys.split("/"):
if not obj:
return
@ -150,10 +155,10 @@ class Objects(object):
inverse = False
if value.startswith('!'):
if value.startswith("!"):
inverse = True
value = value.split('!', 1)[1]
value = value.split("!", 1)[1]
if value.lower() == "null":
value = None

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@ LOG = LazyLogger(__name__)
def get_grouped_set():
''' Get if boxsets should be grouped
'''
result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"})
return result.get('result', {}).get('value', False)
"""Get if boxsets should be grouped"""
result = JSONRPC("Settings.GetSettingValue").execute(
{"setting": "videolibrary.groupmoviesets"}
)
return result.get("result", {}).get("value", False)

View File

@ -44,11 +44,10 @@ class Player(xbmc.Player):
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.
'''
"""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
@ -69,11 +68,11 @@ class Player(xbmc.Player):
if monitor.waitForAbort(1):
return
else:
LOG.info('Cancel playback report')
LOG.info("Cancel playback report")
return
items = window('jellyfin_play.json')
items = window("jellyfin_play.json")
item = None
while not items:
@ -81,7 +80,7 @@ class Player(xbmc.Player):
if monitor.waitForAbort(2):
return
items = window('jellyfin_play.json')
items = window("jellyfin_play.json")
count += 1
if count == 20:
@ -90,51 +89,49 @@ class Player(xbmc.Player):
return
for item in items:
if item['Path'] == current_file:
if item["Path"] == current_file:
items.pop(items.index(item))
break
else:
item = items.pop(0)
window('jellyfin_play.json', items)
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']
"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'].jellyfin.session_playing(data)
window('jellyfin.skip.%s.bool' % item['Id'], True)
item["Server"].jellyfin.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'])
if item["PlayOption"] == "Addon":
self.set_audio_subs(item["AudioStreamIndex"], item["SubtitleStreamIndex"])
def set_item(self, file, item):
''' Set playback information.
'''
"""Set playback information."""
try:
item['Runtime'] = int(item['Runtime'])
item["Runtime"] = int(item["Runtime"])
except (TypeError, ValueError):
try:
item['Runtime'] = int(self.getTotalTime())
LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime'])
item["Runtime"] = int(self.getTotalTime())
LOG.info("Runtime is missing, Kodi runtime: %s" % item["Runtime"])
except Exception:
item['Runtime'] = 0
item["Runtime"] = 0
LOG.info("Runtime is missing, Using Zero")
try:
@ -142,22 +139,26 @@ class Player(xbmc.Player):
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')
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
})
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)
LOG.info("-->[ play/%s ] %s", item["Id"], item)
def set_audio_subs(self, audio=None, subtitle=None):
if audio:
@ -165,15 +166,15 @@ class Player(xbmc.Player):
if subtitle:
subtitle = int(subtitle)
''' Only for after playback started
'''
""" 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']
mapping = item["SubsMapping"]
if audio and len(self.getAvailableAudioStreams()) > 1:
self.setAudioStream(audio - 1)
@ -200,82 +201,90 @@ class Player(xbmc.Player):
def detect_audio_subs(self, item):
params = {
'playerid': 1,
'properties': ["currentsubtitle", "currentaudiostream", "subtitleenabled"]
"playerid": 1,
"properties": ["currentsubtitle", "currentaudiostream", "subtitleenabled"],
}
result = JSONRPC('Player.GetProperties').execute(params)
result = result.get('result')
result = JSONRPC("Player.GetProperties").execute(params)
result = result.get("result")
try: # Audio tracks
audio = result['currentaudiostream']['index']
audio = result["currentaudiostream"]["index"]
except (KeyError, TypeError):
audio = 0
try: # Subtitles tracks
subs = result['currentsubtitle']['index']
subs = result["currentsubtitle"]["index"]
except (KeyError, TypeError):
subs = 0
try: # If subtitles are enabled
subs_enabled = result['subtitleenabled']
subs_enabled = result["subtitleenabled"]
except (KeyError, TypeError):
subs_enabled = False
item['AudioStreamIndex'] = audio + 1
item["AudioStreamIndex"] = audio + 1
if not subs_enabled or not len(self.getAvailableSubtitleStreams()):
item['SubtitleStreamIndex'] = None
item["SubtitleStreamIndex"] = None
return
mapping = item['SubsMapping']
mapping = item["SubsMapping"]
tracks = len(self.getAvailableAudioStreams())
if mapping:
if str(subs) in mapping:
item['SubtitleStreamIndex'] = mapping[str(subs)]
item["SubtitleStreamIndex"] = mapping[str(subs)]
else:
item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1
item["SubtitleStreamIndex"] = subs - len(mapping) + tracks + 1
else:
item['SubtitleStreamIndex'] = subs + tracks + 1
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'):
if item["Type"] != "Episode" or not item.get("CurrentEpisode"):
return
next_items = item['Server'].jellyfin.get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id'])
next_items = item["Server"].jellyfin.get_adjacent_episodes(
item["CurrentEpisode"]["tvshowid"], item["Id"]
)
for index, next_item in enumerate(next_items['Items']):
if next_item['Id'] == 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]
next_item = next_items["Items"][index + 1]
except IndexError:
LOG.warning("No next up episode.")
return
break
server_address = item['Server'].auth.get_server_info(item['Server'].auth.server_id)['address']
server_address = item["Server"].auth.get_server_info(
item["Server"].auth.server_id
)["address"]
API = api.API(next_item, 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')
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]
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
"play_info": {
"ItemIds": [data["episodeid"]],
"ServerId": item["ServerId"],
"PlayCommand": "PlayNow",
},
"current_episode": item["CurrentEpisode"],
"next_episode": data,
}
LOG.info("--[ next up ] %s", next_info)
@ -286,7 +295,7 @@ class Player(xbmc.Player):
if self.is_playing_file(current_file):
self.get_file_info(current_file)['Paused'] = True
self.get_file_info(current_file)["Paused"] = True
self.report_playback()
LOG.debug("-->[ paused ]")
@ -295,24 +304,21 @@ class Player(xbmc.Player):
if self.is_playing_file(current_file):
self.get_file_info(current_file)['Paused'] = False
self.get_file_info(current_file)["Paused"] = False
self.report_playback()
LOG.debug("--<[ paused ]")
def onPlayBackSeek(self, time, seek_offset):
''' Does not seem to work in Leia??
'''
"""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.
'''
"""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):
@ -320,25 +326,29 @@ class Player(xbmc.Player):
item = self.get_file_info(current_file)
if window('jellyfin.external.bool'):
if window("jellyfin.external.bool"):
return
if not report:
previous = item['CurrentPosition']
previous = item["CurrentPosition"]
try:
item['CurrentPosition'] = int(self.getTime())
item["CurrentPosition"] = int(self.getTime())
except Exception as e:
# getTime() raises RuntimeError if nothing is playing
LOG.debug("Failed to get playback position: %s", e)
return
if int(item['CurrentPosition']) == 1:
if int(item["CurrentPosition"]) == 1:
return
try:
played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100
played = (
float(item["CurrentPosition"] * 10000000)
/ int(item["Runtime"])
* 100
)
except ZeroDivisionError: # Runtime is 0.
played = 0
@ -347,52 +357,48 @@ class Player(xbmc.Player):
self.up_next = True
self.next_up()
if (item['CurrentPosition'] - previous) < 30:
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())
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']
"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'].jellyfin.session_progress(data)
item["Server"].jellyfin.session_progress(data)
def onPlayBackStopped(self):
''' Will be called when user stops playing a file.
'''
window('jellyfin_play', clear=True)
"""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.
'''
"""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.
'''
"""Stop all playback. Check for external player for positionticks."""
if not self.played:
return
@ -401,61 +407,67 @@ class Player(xbmc.Player):
for file in self.played:
item = self.get_file_info(file)
window('jellyfin.skip.%s.bool' % item['Id'], True)
window("jellyfin.skip.%s.bool" % item["Id"], True)
if window('jellyfin.external.bool'):
window('jellyfin.external', clear=True)
if window("jellyfin.external.bool"):
window("jellyfin.external", clear=True)
if int(item['CurrentPosition']) == 1:
item['CurrentPosition'] = int(item['Runtime'])
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']
"ItemId": item["Id"],
"MediaSourceId": item["MediaSourceId"],
"PositionTicks": int(item["CurrentPosition"] * 10000000),
"PlaySessionId": item["PlaySessionId"],
}
item['Server'].jellyfin.session_stop(data)
item["Server"].jellyfin.session_stop(data)
if item.get('LiveStreamId'):
if item.get("LiveStreamId"):
LOG.info("<[ livestream/%s ]", item['LiveStreamId'])
item['Server'].jellyfin.close_live_stream(item['LiveStreamId'])
LOG.info("<[ livestream/%s ]", item["LiveStreamId"])
item["Server"].jellyfin.close_live_stream(item["LiveStreamId"])
elif item['PlayMethod'] == 'Transcode':
elif item["PlayMethod"] == "Transcode":
LOG.info("<[ transcode/%s ]", item['Id'])
item['Server'].jellyfin.close_transcode(item['DeviceId'], item['PlaySessionId'])
LOG.info("<[ transcode/%s ]", item["Id"])
item["Server"].jellyfin.close_transcode(
item["DeviceId"], item["PlaySessionId"]
)
path = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/")
path = translate_path(
"special://profile/addon_data/plugin.video.jellyfin/temp/"
)
if xbmcvfs.exists(path):
dirs, files = xbmcvfs.listdir(path)
for file in files:
# Only delete the cached files for the previous play session
if item['Id'] in file:
if item["Id"] in file:
xbmcvfs.delete(os.path.join(path, file))
result = item['Server'].jellyfin.get_item(item['Id']) or {}
result = item["Server"].jellyfin.get_item(item["Id"]) or {}
if 'UserData' in result and result['UserData']['Played']:
if "UserData" in result and result["UserData"]["Played"]:
delete = False
if result['Type'] == 'Episode' and settings('deleteTV.bool'):
if result["Type"] == "Episode" and settings("deleteTV.bool"):
delete = True
elif result['Type'] == 'Movie' and settings('deleteMovies.bool'):
elif result["Type"] == "Movie" and settings("deleteMovies.bool"):
delete = True
if not settings('offerDelete.bool'):
if not settings("offerDelete.bool"):
delete = False
if delete:
LOG.info("Offer delete option")
if dialog("yesno", translate(30091), translate(33015), autoclose=120000):
item['Server'].jellyfin.delete_item(item['Id'])
if dialog(
"yesno", translate(30091), translate(33015), autoclose=120000
):
item["Server"].jellyfin.delete_item(item["Id"])
window('jellyfin.external_check', clear=True)
window("jellyfin.external_check", clear=True)
self.played.clear()

File diff suppressed because it is too large Load Diff

View File

@ -14,16 +14,16 @@ from jellyfin_kodi.helper import LazyLogger
#################################################################################################
LOG = LazyLogger(__name__)
DELAY = int(settings('startupDelay') if settings('SyncInstallRunDone.bool') else 4)
DELAY = int(settings("startupDelay") if settings("SyncInstallRunDone.bool") else 4)
#################################################################################################
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
def __init__(self):
@ -44,10 +44,10 @@ class ServiceManager(threading.Thread):
if service is not None:
# TODO: fix this properly as to not match on str()
if 'ExitService' not in str(error):
if "ExitService" not in str(error):
service.shutdown()
if 'RestartService' in str(error):
if "RestartService" in str(error):
service.reload_objects()
self.exception = error
@ -58,7 +58,7 @@ if __name__ == "__main__":
LOG.info("Delay startup by %s seconds.", DELAY)
while True:
if not settings('enableAddon.bool'):
if not settings("enableAddon.bool"):
LOG.warning("Jellyfin for Kodi is not enabled.")
break
@ -68,12 +68,11 @@ if __name__ == "__main__":
session.start()
session.join() # Block until the thread exits.
if 'RestartService' in str(session.exception):
if "RestartService" in str(session.exception):
continue
except Exception as error:
''' Issue initializing the service.
'''
"""Issue initializing the service."""
LOG.exception(error)
break

View File

@ -3,45 +3,51 @@ import pytest
from jellyfin_kodi.jellyfin.utils import clean_none_dict_values
@pytest.mark.parametrize("obj,expected", [
(None, None),
([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]),
({'foo': None, 'bar': 123}, {'bar': 123}),
({
'dict': {
'empty': None,
'string': "Hello, Woorld!",
},
'number': 123,
'list': [
None,
123,
"foo",
@pytest.mark.parametrize(
"obj,expected",
[
(None, None),
([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]),
({"foo": None, "bar": 123}, {"bar": 123}),
(
{
'empty': None,
'number': 123,
'string': "foo",
'list': [],
'dict': {},
}
]
}, {
'dict': {
'string': "Hello, Woorld!",
},
'number': 123,
'list': [
None,
123,
"foo",
"dict": {
"empty": None,
"string": "Hello, Woorld!",
},
"number": 123,
"list": [
None,
123,
"foo",
{
"empty": None,
"number": 123,
"string": "foo",
"list": [],
"dict": {},
},
],
},
{
'number': 123,
'string': "foo",
'list': [],
'dict': {},
}
]
}),
])
"dict": {
"string": "Hello, Woorld!",
},
"number": 123,
"list": [
None,
123,
"foo",
{
"number": 123,
"string": "foo",
"list": [],
"dict": {},
},
],
},
),
],
)
def test_clean_none_dict_values(obj, expected):
assert clean_none_dict_values(obj) == expected

View File

@ -37,6 +37,7 @@ def test_import_downloader():
def test_import_entrypoint():
import jellyfin_kodi.entrypoint
import jellyfin_kodi.entrypoint.context
# import jellyfin_kodi.entrypoint.default # FIXME: Messes with sys.argv
import jellyfin_kodi.entrypoint.service # noqa: F401

View File

@ -1,13 +1,11 @@
from sqlite3 import Cursor
from typing import Any, List, Optional, NamedTuple
class ViewRow(NamedTuple):
view_id: str
view_name: str
media_type: str
class JellyfinDatabase:
cursor: Cursor = ...
def __init__(self, cursor: Cursor) -> None: ...