Merge pull request #869 from oddstr13/black

Run the black formatter
This commit is contained in:
Odd Stråbø 2024-06-10 12:41:58 +02:00 committed by GitHub
commit 94e44d4c10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 6547 additions and 4723 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Tool: black
77637622125a187c5b9cbe72b78c8bd3b26f754a

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals from __future__ import division, absolute_import, print_function, unicode_literals
################################################################################################# #################################################################################################
from . import queries as QU from . import queries as QU
@ -14,7 +15,7 @@ LOG = LazyLogger(__name__)
################################################################################################## ##################################################################################################
class JellyfinDatabase(): class JellyfinDatabase:
def __init__(self, cursor): def __init__(self, cursor):
self.cursor = cursor self.cursor = cursor
@ -32,9 +33,7 @@ class JellyfinDatabase():
self.cursor.execute(QU.update_reference, args) self.cursor.execute(QU.update_reference, args)
def update_parent_id(self, *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) self.cursor.execute(QU.update_parent, args)
def get_item_id_by_parent_id(self, *args): def get_item_id_by_parent_id(self, *args):
@ -160,8 +159,8 @@ class JellyfinDatabase():
return self.cursor.fetchone() return self.cursor.fetchone()
def add_version(self, *args): def add_version(self, *args):
''' """
We only ever want one value here, so erase the existing contents first We only ever want one value here, so erase the existing contents first
''' """
self.cursor.execute(QU.delete_version) self.cursor.execute(QU.delete_version)
self.cursor.execute(QU.add_version, args) 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) media_type, parent_id, checksum, media_folder, jellyfin_parent_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""" """
add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] add_reference_movie_obj = [
add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None] "{Id}",
add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] "{MovieId}",
add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, None, None] "{FileId}",
add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None] "{PathId}",
add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", None, "{JellyfinParentId}"] "Movie",
add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] "movie",
add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] None,
add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] "{Checksum}",
add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{JellyfinParentId}"] "{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 = """ add_view = """
INSERT OR REPLACE INTO view(view_id, view_name, media_type) INSERT OR REPLACE INTO view(view_id, view_name, media_type)
VALUES (?, ?, ?) VALUES (?, ?, ?)

View File

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

View File

@ -18,7 +18,7 @@ SIGN_IN = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
ERROR = {'Invalid': 1, 'Empty': 2} ERROR = {"Invalid": 1, "Empty": 2}
################################################################################################## ##################################################################################################
@ -76,7 +76,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
if not user: if not user:
# Display error # Display error
self._error(ERROR['Empty'], translate('empty_user')) self._error(ERROR["Empty"], translate("empty_user"))
LOG.error("Username cannot be null") LOG.error("Username cannot be null")
elif self._login(user, password): elif self._login(user, password):
@ -88,7 +88,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
def onAction(self, action): 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() self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
@ -102,12 +102,12 @@ class LoginManual(xbmcgui.WindowXMLDialog):
textColor="FF00A4DC", textColor="FF00A4DC",
disabledColor="FF888888", disabledColor="FF888888",
focusTexture="-", focusTexture="-",
noFocusTexture="-" noFocusTexture="-",
) )
# TODO: Kodi 17 compat removal cleanup # TODO: Kodi 17 compat removal cleanup
if kodi_version() < 18: if kodi_version() < 18:
kwargs['isPassword'] = password kwargs["isPassword"] = password
control = xbmcgui.ControlEdit(0, 0, 0, 0, **kwargs) control = xbmcgui.ControlEdit(0, 0, 0, 0, **kwargs)
@ -126,11 +126,13 @@ class LoginManual(xbmcgui.WindowXMLDialog):
def _login(self, username, password): 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) result = self.connect_manager.login(server, username, password)
if not result: if not result:
self._error(ERROR['Invalid'], translate('invalid_auth')) self._error(ERROR["Invalid"], translate("invalid_auth"))
return False return False
else: else:
self._user = result self._user = result
@ -140,9 +142,9 @@ class LoginManual(xbmcgui.WindowXMLDialog):
self.error = state self.error = state
self.error_msg.setLabel(message) self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true') self.error_toggle.setVisibleCondition("true")
def _disable_error(self): def _disable_error(self):
self.error = None 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) self.list_ = self.getControl(LIST)
for server in self.servers: for server in self.servers:
server_type = "wifi" if server.get('ExchangeToken') else "network" server_type = "wifi" if server.get("ExchangeToken") else "network"
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type)) self.list_.addItem(
self._add_listitem(server["Name"], server["Id"], server_type)
)
if self.user_image is not None: if self.user_image is not None:
self.getControl(USER_IMAGE).setImage(self.user_image) 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): def _add_listitem(cls, label, server_id, server_type):
item = xbmcgui.ListItem(label) item = xbmcgui.ListItem(label)
item.setProperty('id', server_id) item.setProperty("id", server_id)
item.setProperty('server_type', server_type) item.setProperty("server_type", server_type)
return item return item
@ -87,14 +89,17 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close() 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() server = self.list_.getSelectedItem()
selected_id = server.getProperty('id') selected_id = server.getProperty("id")
LOG.info('Server Id selected: %s', selected_id) LOG.info("Server Id selected: %s", selected_id)
if self._connect_server(selected_id): if self._connect_server(selected_id):
self.message_box.setVisibleCondition('false') self.message_box.setVisibleCondition("false")
self.close() self.close()
def onClick(self, control): def onClick(self, control):
@ -109,19 +114,19 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
def _connect_server(self, server_id): def _connect_server(self, server_id):
server = self.connect_manager.get_server_info(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.message_box.setVisibleCondition("true")
self.busy.setVisibleCondition('true') self.busy.setVisibleCondition("true")
result = self.connect_manager.connect_to_server(server) result = self.connect_manager.connect_to_server(server)
if result['State'] == CONNECTION_STATE['Unavailable']: if result["State"] == CONNECTION_STATE["Unavailable"]:
self.busy.setVisibleCondition('false') self.busy.setVisibleCondition("false")
self.message.setLabel(translate(30609)) self.message.setLabel(translate(30609))
return False return False
else: else:
xbmc.sleep(1000) xbmc.sleep(1000)
self._selected_server = result['Servers'][0] self._selected_server = result["Servers"][0]
return True return True

View File

@ -22,10 +22,7 @@ CONNECT = 200
CANCEL = 201 CANCEL = 201
ERROR_TOGGLE = 202 ERROR_TOGGLE = 202
ERROR_MSG = 203 ERROR_MSG = 203
ERROR = { ERROR = {"Invalid": 1, "Empty": 2}
'Invalid': 1,
'Empty': 2
}
# https://stackoverflow.com/a/17871737/1035647 # 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]))$" _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: if not server:
# Display error # Display error
self._error(ERROR['Empty'], translate('empty_server')) self._error(ERROR["Empty"], translate("empty_server"))
LOG.error("Server cannot be null") LOG.error("Server cannot be null")
elif self._connect_to_server(server): elif self._connect_to_server(server):
@ -89,7 +86,7 @@ class ServerManual(xbmcgui.WindowXMLDialog):
def onAction(self, action): 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() self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): 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): def _add_editcontrol(self, x, y, height, width):
control = xbmcgui.ControlEdit(0, 0, 0, 0, control = xbmcgui.ControlEdit(
label="", 0,
font="font13", 0,
textColor="FF00A4DC", 0,
disabledColor="FF888888", 0,
focusTexture="-", label="",
noFocusTexture="-") font="font13",
textColor="FF00A4DC",
disabledColor="FF888888",
focusTexture="-",
noFocusTexture="-",
)
control.setPosition(x, y) control.setPosition(x, y)
control.setHeight(height) control.setHeight(height)
control.setWidth(width) control.setWidth(width)
@ -118,25 +120,25 @@ class ServerManual(xbmcgui.WindowXMLDialog):
self._message("%s %s..." % (translate(30610), server)) self._message("%s %s..." % (translate(30610), server))
result = self.connect_manager.connect_to_address(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)) self._message(translate(30609))
return False return False
else: else:
self._server = result['Servers'][0] self._server = result["Servers"][0]
return True return True
def _message(self, message): def _message(self, message):
self.error_msg.setLabel(message) self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true') self.error_toggle.setVisibleCondition("true")
def _error(self, state, message): def _error(self, state, message):
self.error = state self.error = state
self.error_msg.setLabel(message) self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('true') self.error_toggle.setVisibleCondition("true")
def _disable_error(self): def _disable_error(self):
self.error = None 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) self.list_ = self.getControl(LIST)
for user in self.users: for user in self.users:
user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user user_image = (
else self._get_user_artwork(user['Id'], 'Primary')) "items/logindefault.png"
self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image)) 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_) self.setFocus(self.list_)
def _add_listitem(self, label, user_id, user_image): def _add_listitem(self, label, user_id, user_image):
item = xbmcgui.ListItem(label) item = xbmcgui.ListItem(label)
item.setProperty('id', user_id) item.setProperty("id", user_id)
item.setArt({'icon': user_image}) item.setArt({"icon": user_image})
return item return item
@ -71,14 +74,17 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR): if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close() 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() user = self.list_.getSelectedItem()
selected_id = user.getProperty('id') selected_id = user.getProperty("id")
LOG.info('User Id selected: %s', selected_id) LOG.info("User Id selected: %s", selected_id)
for user in self.users: for user in self.users:
if user['Id'] == selected_id: if user["Id"] == selected_id:
self._user = user self._user = user
break break
@ -95,4 +101,8 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
def _get_user_artwork(self, user_id, item_type): def _get_user_artwork(self, user_id, item_type):
# Load user information set by UserClient # 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): def get_jellyfinserver_url(handler):
if handler.startswith('/'): if handler.startswith("/"):
handler = handler[1:] handler = handler[1:]
LOG.info("handler starts with /: %s", handler) LOG.info("handler starts with /: %s", handler)
@ -38,47 +38,55 @@ def _http(action, url, request=None, server_id=None):
if request is None: if request is None:
request = {} request = {}
request.update({'url': url, 'type': action}) request.update({"url": url, "type": action})
return Jellyfin(server_id).http.request(request) return Jellyfin(server_id).http.request(request)
def _get(handler, params=None, server_id=None): 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): 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): 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): def validate_view(library_id, item_id):
"""This confirms a single item from the library matches the view it belongs to.
''' This confirms a single item from the library matches the view it belongs to. Used to detect grouped libraries.
Used to detect grouped libraries. """
'''
try: try:
result = _get("Users/{UserId}/Items", { result = _get(
'ParentId': library_id, "Users/{UserId}/Items",
'Recursive': True, {"ParentId": library_id, "Recursive": True, "Ids": item_id},
'Ids': item_id )
})
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
return False return False
return bool(len(result['Items'])) return bool(len(result["Items"]))
def get_single_item(parent_id, media): def get_single_item(parent_id, media):
return _get("Users/{UserId}/Items", { return _get(
'ParentId': parent_id, "Users/{UserId}/Items",
'Recursive': True, {
'Limit': 1, "ParentId": parent_id,
'IncludeItemTypes': media "Recursive": True,
}) "Limit": 1,
"IncludeItemTypes": media,
},
)
def get_movies_by_boxset(boxset_id): 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): def get_episode_by_show(show_id):
query = { query = {
'url': "Shows/%s/Episodes" % show_id, "url": "Shows/%s/Episodes" % show_id,
'params': { "params": {
'EnableUserData': True, "EnableUserData": True,
'EnableImages': True, "EnableImages": True,
'UserId': "{UserId}", "UserId": "{UserId}",
'Fields': api.info() "Fields": api.info(),
} },
} }
for items in _get_items(query): for items in _get_items(query):
yield items yield items
@ -105,14 +113,14 @@ def get_episode_by_show(show_id):
def get_episode_by_season(show_id, season_id): def get_episode_by_season(show_id, season_id):
query = { query = {
'url': "Shows/%s/Episodes" % show_id, "url": "Shows/%s/Episodes" % show_id,
'params': { "params": {
'SeasonId': season_id, "SeasonId": season_id,
'EnableUserData': True, "EnableUserData": True,
'EnableImages': True, "EnableImages": True,
'UserId': "{UserId}", "UserId": "{UserId}",
'Fields': api.info() "Fields": api.info(),
} },
} }
for items in _get_items(query): for items in _get_items(query):
yield items yield items
@ -123,41 +131,41 @@ def get_item_count(parent_id, item_type=None, params=None):
url = "Users/{UserId}/Items" url = "Users/{UserId}/Items"
query_params = { query_params = {
'ParentId': parent_id, "ParentId": parent_id,
'IncludeItemTypes': item_type, "IncludeItemTypes": item_type,
'EnableTotalRecordCount': True, "EnableTotalRecordCount": True,
'LocationTypes': "FileSystem,Remote,Offline", "LocationTypes": "FileSystem,Remote,Offline",
'Recursive': True, "Recursive": True,
'Limit': 1 "Limit": 1,
} }
if params: if params:
query_params['params'].update(params) query_params["params"].update(params)
result = _get(url, query_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): def get_items(parent_id, item_type=None, basic=False, params=None):
query = { query = {
'url': "Users/{UserId}/Items", "url": "Users/{UserId}/Items",
'params': { "params": {
'ParentId': parent_id, "ParentId": parent_id,
'IncludeItemTypes': item_type, "IncludeItemTypes": item_type,
'SortBy': "SortName", "SortBy": "SortName",
'SortOrder': "Ascending", "SortOrder": "Ascending",
'Fields': api.basic_info() if basic else api.info(), "Fields": api.basic_info() if basic else api.info(),
'CollapseBoxSetItems': False, "CollapseBoxSetItems": False,
'IsVirtualUnaired': False, "IsVirtualUnaired": False,
'EnableTotalRecordCount': False, "EnableTotalRecordCount": False,
'LocationTypes': "FileSystem,Remote,Offline", "LocationTypes": "FileSystem,Remote,Offline",
'IsMissing': False, "IsMissing": False,
'Recursive': True "Recursive": True,
} },
} }
if params: if params:
query['params'].update(params) query["params"].update(params)
for items in _get_items(query): for items in _get_items(query):
yield items yield items
@ -166,20 +174,20 @@ def get_items(parent_id, item_type=None, basic=False, params=None):
def get_artists(parent_id=None): def get_artists(parent_id=None):
query = { query = {
'url': 'Artists', "url": "Artists",
'params': { "params": {
'UserId': "{UserId}", "UserId": "{UserId}",
'ParentId': parent_id, "ParentId": parent_id,
'SortBy': "SortName", "SortBy": "SortName",
'SortOrder': "Ascending", "SortOrder": "Ascending",
'Fields': api.music_info(), "Fields": api.music_info(),
'CollapseBoxSetItems': False, "CollapseBoxSetItems": False,
'IsVirtualUnaired': False, "IsVirtualUnaired": False,
'EnableTotalRecordCount': False, "EnableTotalRecordCount": False,
'LocationTypes': "FileSystem,Remote,Offline", "LocationTypes": "FileSystem,Remote,Offline",
'IsMissing': False, "IsMissing": False,
'Recursive': True "Recursive": True,
} },
} }
for items in _get_items(query): for items in _get_items(query):
@ -188,48 +196,49 @@ def get_artists(parent_id=None):
@stop @stop
def _get_items(query, server_id=None): def _get_items(query, server_id=None):
"""query = {
''' query = { 'url': string,
'url': string, 'params': dict -- opt, include StartIndex to resume
'params': dict -- opt, include StartIndex to resume
}
'''
items = {
'Items': [],
'TotalRecordCount': 0,
'RestorePoint': {}
} }
"""
items = {"Items": [], "TotalRecordCount": 0, "RestorePoint": {}}
limit = min(int(settings('limitIndex') or 50), 50) limit = min(int(settings("limitIndex") or 50), 50)
dthreads = int(settings('limitThreads') or 3) dthreads = int(settings("limitThreads") or 3)
url = query['url'] url = query["url"]
query.setdefault('params', {}) query.setdefault("params", {})
params = query['params'] params = query["params"]
try: try:
test_params = dict(params) test_params = dict(params)
test_params['Limit'] = 1 test_params["Limit"] = 1
test_params['EnableTotalRecordCount'] = True 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: 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: else:
params.setdefault('StartIndex', 0) params.setdefault("StartIndex", 0)
def get_query_params(params, start, count): def get_query_params(params, start, count):
params_copy = dict(params) params_copy = dict(params)
params_copy['StartIndex'] = start params_copy["StartIndex"] = start
params_copy['Limit'] = count params_copy["Limit"] = count
return params_copy return params_copy
query_params = [ query_params = [
get_query_params(params, offset, limit) get_query_params(params, offset, limit)
for offset for offset in range(params["StartIndex"], items["TotalRecordCount"], limit)
in range(params['StartIndex'], items['TotalRecordCount'], limit)
] ]
# multiprocessing.dummy.Pool completes all requests in multiple threads but has to # 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 # process complete jobs
for job in concurrent.futures.as_completed(jobs): for job in concurrent.futures.as_completed(jobs):
# get the result # get the result
result = job.result() or {'Items': []} result = job.result() or {"Items": []}
query['params'] = jobs[job] query["params"] = jobs[job]
# free job memory # free job memory
del jobs[job] del jobs[job]
del job del job
# Mitigates #216 till the server validates the date provided is valid # Mitigates #216 till the server validates the date provided is valid
if result['Items'][0].get('ProductionYear'): if result["Items"][0].get("ProductionYear"):
try: try:
date(result['Items'][0]['ProductionYear'], 1, 1) date(result["Items"][0]["ProductionYear"], 1, 1)
except ValueError: except ValueError:
LOG.info('#216 mitigation triggered. Setting ProductionYear to None') LOG.info(
result['Items'][0]['ProductionYear'] = None "#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 # Using items to return data and communicate a restore point back to the callee is
# a violation of the SRP. TODO: Separate responsibilities. # a violation of the SRP. TODO: Separate responsibilities.
items['RestorePoint'] = query items["RestorePoint"] = query
yield items yield items
del items['Items'][:] del items["Items"][:]
# release the semaphore again # release the semaphore again
thread_buffer.release() thread_buffer.release()
@ -307,25 +318,25 @@ class GetItemWorker(threading.Thread):
return return
request = { request = {
'type': "GET", "type": "GET",
'handler': "Users/{UserId}/Items", "handler": "Users/{UserId}/Items",
'params': { "params": {
'Ids': ','.join(str(x) for x in item_ids), "Ids": ",".join(str(x) for x in item_ids),
'Fields': api.info() "Fields": api.info(),
} },
} }
try: try:
result = self.server.http.request(request, s) result = self.server.http.request(request, s)
for item in result['Items']: for item in result["Items"]:
if item['Type'] in self.output: if item["Type"] in self.output:
self.output[item['Type']].put(item) self.output[item["Type"]].put(item)
except HTTPException as error: except HTTPException as error:
LOG.error("--[ http status: %s ]", error.status) LOG.error("--[ http status: %s ]", error.status)
if error.status == 'ServerUnreachable': if error.status == "ServerUnreachable":
self.is_done = True self.is_done = True
break break
@ -335,5 +346,5 @@ class GetItemWorker(threading.Thread):
self.queue.task_done() self.queue.task_done()
if window('jellyfin_should_stop.bool'): if window("jellyfin_should_stop.bool"):
break break

View File

@ -17,14 +17,18 @@ from ..jellyfin import Jellyfin
################################################################################################# #################################################################################################
LOG = LazyLogger(__name__) 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 = { OPTIONS = {
'Refresh': translate(30410), "Refresh": translate(30410),
'Delete': translate(30409), "Delete": translate(30409),
'Addon': translate(30408), "Addon": translate(30408),
'AddFav': translate(30405), "AddFav": translate(30405),
'RemoveFav': translate(30406), "RemoveFav": translate(30406),
'Transcode': translate(30412) "Transcode": translate(30412),
} }
################################################################################################# #################################################################################################
@ -39,31 +43,33 @@ class Context(object):
try: try:
self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None
self.media = self.get_media_type() 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 self.api_client = Jellyfin(self.server_id).get_client().jellyfin
item_id = sys.listitem.getProperty('jellyfinid') item_id = sys.listitem.getProperty("jellyfinid")
except AttributeError: except AttributeError:
self.server_id = None self.server_id = None
if xbmc.getInfoLabel('ListItem.Property(jellyfinid)'): if xbmc.getInfoLabel("ListItem.Property(jellyfinid)"):
item_id = xbmc.getInfoLabel('ListItem.Property(jellyfinid)') item_id = xbmc.getInfoLabel("ListItem.Property(jellyfinid)")
else: else:
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID') self.kodi_id = xbmc.getInfoLabel("ListItem.DBID")
self.media = xbmc.getInfoLabel('ListItem.DBTYPE') self.media = xbmc.getInfoLabel("ListItem.DBTYPE")
item_id = None item_id = None
addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json") addon_data = translate_path(
with open(addon_data, 'rb') as infile: "special://profile/addon_data/plugin.video.jellyfin/data.json"
)
with open(addon_data, "rb") as infile:
data = json.load(infile) data = json.load(infile)
try: try:
server_data = data['Servers'][0] server_data = data["Servers"][0]
self.api_client.config.data['auth.server'] = server_data.get('address') 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.server-name"] = server_data.get("Name")
self.api_client.config.data['auth.user_id'] = server_data.get('UserId') self.api_client.config.data["auth.user_id"] = server_data.get("UserId")
self.api_client.config.data['auth.token'] = server_data.get('AccessToken') self.api_client.config.data["auth.token"] = server_data.get("AccessToken")
except Exception as e: 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: if self.server_id or item_id:
self.item = self.api_client.get_item(item_id) self.item = self.api_client.get_item(item_id)
@ -81,26 +87,28 @@ class Context(object):
elif self.select_menu(): elif self.select_menu():
self.action_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.sleep(500)
xbmc.executebuiltin('Container.Refresh') xbmc.executebuiltin("Container.Refresh")
def get_media_type(self): 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() media = sys.listitem.getVideoInfoTag().getMediaType()
if not media: if not media:
if xbmc.getCondVisibility('Container.Content(albums)'): if xbmc.getCondVisibility("Container.Content(albums)"):
media = "album" media = "album"
elif xbmc.getCondVisibility('Container.Content(artists)'): elif xbmc.getCondVisibility("Container.Content(artists)"):
media = "artist" media = "artist"
elif xbmc.getCondVisibility('Container.Content(songs)'): elif xbmc.getCondVisibility("Container.Content(songs)"):
media = "song" media = "song"
elif xbmc.getCondVisibility('Container.Content(pictures)'): elif xbmc.getCondVisibility("Container.Content(pictures)"):
media = "picture" media = "picture"
else: else:
LOG.info("media is unknown") LOG.info("media is unknown")
@ -108,40 +116,37 @@ class Context(object):
return media return media
def get_item_id(self): def get_item_id(self):
"""Get synced item from jellyfindb."""
''' Get synced item from jellyfindb.
'''
item = database.get_item(self.kodi_id, self.media) item = database.get_item(self.kodi_id, self.media)
if not item: if not item:
return return
return { return {
'Id': item[0], "Id": item[0],
'UserData': json.loads(item[4]) if item[4] else {}, "UserData": json.loads(item[4]) if item[4] else {},
'Type': item[3] "Type": item[3],
} }
def select_menu(self): def select_menu(self):
"""Display the select dialog.
''' Display the select dialog. Favorites, Refresh, Delete (opt), Settings.
Favorites, Refresh, Delete (opt), Settings. """
'''
options = [] options = []
if self.item['Type'] != 'Season': if self.item["Type"] != "Season":
if self.item['UserData'].get('IsFavorite'): if self.item["UserData"].get("IsFavorite"):
options.append(OPTIONS['RemoveFav']) options.append(OPTIONS["RemoveFav"])
else: else:
options.append(OPTIONS['AddFav']) options.append(OPTIONS["AddFav"])
options.append(OPTIONS['Refresh']) options.append(OPTIONS["Refresh"])
if settings('enableContextDelete.bool'): if settings("enableContextDelete.bool"):
options.append(OPTIONS['Delete']) options.append(OPTIONS["Delete"])
options.append(OPTIONS['Addon']) options.append(OPTIONS["Addon"])
context_menu = context.ContextMenu("script-jellyfin-context.xml", *XML_PATH) context_menu = context.ContextMenu("script-jellyfin-context.xml", *XML_PATH)
context_menu.set_options(options) context_menu.set_options(options)
@ -156,24 +161,26 @@ class Context(object):
selected = self._selected_option selected = self._selected_option
if selected == OPTIONS['Refresh']: if selected == OPTIONS["Refresh"]:
self.api_client.refresh_item(self.item['Id']) self.api_client.refresh_item(self.item["Id"])
elif selected == OPTIONS['AddFav']: elif selected == OPTIONS["AddFav"]:
self.api_client.favorite(self.item['Id'], True) self.api_client.favorite(self.item["Id"], True)
elif selected == OPTIONS['RemoveFav']: elif selected == OPTIONS["RemoveFav"]:
self.api_client.favorite(self.item['Id'], False) self.api_client.favorite(self.item["Id"], False)
elif selected == OPTIONS['Addon']: elif selected == OPTIONS["Addon"]:
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.jellyfin)') xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)")
elif selected == OPTIONS['Delete']: elif selected == OPTIONS["Delete"]:
self.delete_item() self.delete_item()
def delete_item(self): def delete_item(self):
if settings('skipContextMenu.bool') or dialog("yesno", "{jellyfin}", translate(33015)): if settings("skipContextMenu.bool") or dialog(
self.api_client.delete_item(self.item['Id']) "yesno", "{jellyfin}", translate(33015)
):
self.api_client.delete_item(self.item["Id"])
def transcode(self): def transcode(self):
filename = xbmc.getInfoLabel("ListItem.Filenameandpath") 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 library
from .. import monitor from .. import monitor
from ..views import Views 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.utils import JsonDebugPrinter, translate_path
from ..helper.xmls import verify_kodi_defaults from ..helper.xmls import verify_kodi_defaults
from ..jellyfin import Jellyfin from ..jellyfin import Jellyfin
@ -37,83 +45,98 @@ class Service(xbmc.Monitor):
monitor = None monitor = None
play_event = None play_event = None
warn = True 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): def __init__(self):
window('jellyfin_should_stop', clear=True) window("jellyfin_should_stop", clear=True)
self.settings['addon_version'] = client.get_version() self.settings["addon_version"] = client.get_version()
self.settings['profile'] = translate_path('special://profile') self.settings["profile"] = translate_path("special://profile")
self.settings['mode'] = settings('useDirectPaths') self.settings["mode"] = settings("useDirectPaths")
self.settings['log_level'] = settings('logLevel') or "1" self.settings["log_level"] = settings("logLevel") or "1"
self.settings['auth_check'] = True self.settings["auth_check"] = True
self.settings['enable_context'] = settings('enableContext.bool') self.settings["enable_context"] = settings("enableContext.bool")
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') self.settings["enable_context_transcode"] = settings(
self.settings['kodi_companion'] = settings('kodiCompanion.bool') "enableContextTranscode.bool"
window('jellyfin_kodiProfile', value=self.settings['profile']) )
settings('platformDetected', client.get_platform()) self.settings["kodi_companion"] = settings("kodiCompanion.bool")
window("jellyfin_kodiProfile", value=self.settings["profile"])
settings("platformDetected", client.get_platform())
if self.settings['enable_context']: if self.settings["enable_context"]:
window('jellyfin_context.bool', True) window("jellyfin_context.bool", True)
if self.settings['enable_context_transcode']: if self.settings["enable_context_transcode"]:
window('jellyfin_context_transcode.bool', True) window("jellyfin_context_transcode.bool", True)
LOG.info("--->>>[ %s ]", client.get_addon_name()) LOG.info("--->>>[ %s ]", client.get_addon_name())
LOG.info("Version: %s", client.get_version()) LOG.info("Version: %s", client.get_version())
LOG.info("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion')) LOG.info("KODI Version: %s", xbmc.getInfoLabel("System.BuildVersion"))
LOG.info("Platform: %s", settings('platformDetected')) LOG.info("Platform: %s", settings("platformDetected"))
LOG.info("Python Version: %s", sys.version) LOG.info("Python Version: %s", sys.version)
LOG.info("Using dynamic paths: %s", settings('useDirectPaths') == "0") LOG.info("Using dynamic paths: %s", settings("useDirectPaths") == "0")
LOG.info("Log Level: %s", self.settings['log_level']) LOG.info("Log Level: %s", self.settings["log_level"])
verify_kodi_defaults() verify_kodi_defaults()
window('jellyfin.connected.bool', True) window("jellyfin.connected.bool", True)
settings('groupedSets.bool', objects.utils.get_grouped_set()) settings("groupedSets.bool", objects.utils.get_grouped_set())
xbmc.Monitor.__init__(self) xbmc.Monitor.__init__(self)
def service(self): def service(self):
"""Keeps the service monitor going.
Exit on Kodi shutdown or profile switch.
''' Keeps the service monitor going. if profile switch happens more than once,
Exit on Kodi shutdown or profile switch. 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() self.monitor = monitor.Monitor()
player = self.monitor.player player = self.monitor.player
self.connect = connect.Connect() self.connect = connect.Connect()
self.start_default() self.start_default()
self.settings['mode'] = settings('useDirectPaths') self.settings["mode"] = settings("useDirectPaths")
while self.running: while self.running:
if window('jellyfin_online.bool'): if window("jellyfin_online.bool"):
if self.settings['profile'] != window('jellyfin_kodiProfile'): if self.settings["profile"] != window("jellyfin_kodiProfile"):
LOG.info("[ profile switch ] %s", self.settings['profile']) LOG.info("[ profile switch ] %s", self.settings["profile"])
break break
if player.isPlaying() and player.is_playing_file(player.get_playing_file()): if player.isPlaying() and player.is_playing_file(
difference = datetime.today() - self.settings['last_progress'] player.get_playing_file()
):
difference = datetime.today() - self.settings["last_progress"]
if difference.seconds > 10: 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 update = (
event('ReportProgressRequested', {'Report': update}) datetime.today() - self.settings["last_progress_report"]
).seconds > 250
event("ReportProgressRequested", {"Report": update})
if 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) window("jellyfin.restart", clear=True)
dialog("notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False) dialog(
"notification",
heading="{jellyfin}",
message=translate(33193),
icon="{jellyfin}",
time=1000,
sound=False,
)
raise Exception('RestartService') raise Exception("RestartService")
if self.waitForAbort(1): if self.waitForAbort(1):
break break
@ -126,14 +149,14 @@ class Service(xbmc.Monitor):
try: try:
self.connect.register() self.connect.register()
if not settings('SyncInstallRunDone.bool'): if not settings("SyncInstallRunDone.bool"):
set_addon_mode() set_addon_mode()
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
def stop_default(self): def stop_default(self):
window('jellyfin_online', clear=True) window("jellyfin_online", clear=True)
Jellyfin().close() Jellyfin().close()
if self.library_thread is not None: if self.library_thread is not None:
@ -142,59 +165,93 @@ class Service(xbmc.Monitor):
self.library_thread = None self.library_thread = None
def onNotification(self, sender, method, data): def onNotification(self, sender, method, data):
"""All notifications are sent via NotifyAll built-in or Kodi.
''' All notifications are sent via NotifyAll built-in or Kodi. Central hub.
Central hub. """
''' if sender.lower() not in ("plugin.video.jellyfin", "xbmc"):
if sender.lower() not in ('plugin.video.jellyfin', 'xbmc'):
return return
if sender == 'plugin.video.jellyfin': if sender == "plugin.video.jellyfin":
method = method.split('.')[1] method = method.split(".")[1]
if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect', if method not in (
'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', "ServerUnreachable",
'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer', "ServerShuttingDown",
'Unauthorized', 'UserConfigurationUpdated', 'ServerRestarting', "UserDataChanged",
'RemoveServer', 'UpdatePassword', 'AddLibrarySelection', 'RemoveLibrarySelection'): "ServerConnect",
"LibraryChanged",
"ServerOnline",
"SyncLibrary",
"RepairLibrary",
"RemoveLibrary",
"SyncLibrarySelection",
"RepairLibrarySelection",
"AddServer",
"Unauthorized",
"UserConfigurationUpdated",
"ServerRestarting",
"RemoveServer",
"UpdatePassword",
"AddLibrarySelection",
"RemoveLibrarySelection",
):
return return
data = json.loads(data)[0] data = json.loads(data)[0]
else: else:
if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'): if method not in ("System.OnQuit", "System.OnSleep", "System.OnWake"):
return return
data = json.loads(data) data = json.loads(data)
LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data)) LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data))
if method == 'ServerOnline': if method == "ServerOnline":
if data.get('ServerId') is None: if data.get("ServerId") is None:
window('jellyfin_online.bool', True) window("jellyfin_online.bool", True)
self.settings['auth_check'] = True self.settings["auth_check"] = True
self.warn = True self.warn = True
if settings('connectMsg.bool'): if settings("connectMsg.bool"):
users = [user for user in (settings('additionalUsers') or "").split(',') if user] users = [
users.insert(0, settings('username')) user
dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33000), ", ".join(users)), for user in (settings("additionalUsers") or "").split(",")
icon="{jellyfin}", time=1500, sound=False) 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: if self.library_thread is None:
self.library_thread = library.Library(self) self.library_thread = library.Library(self)
self.library_thread.start() 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 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) 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() self.stop_default()
if self.waitForAbort(120): if self.waitForAbort(120):
@ -202,12 +259,19 @@ class Service(xbmc.Monitor):
self.start_default() self.start_default()
elif method == 'Unauthorized': elif method == "Unauthorized":
dialog("notification", heading="{jellyfin}", message=translate(33147) if data['ServerId'] is None else translate(33148), icon=xbmcgui.NOTIFICATION_ERROR) 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() self.stop_default()
if self.waitForAbort(5): if self.waitForAbort(5):
@ -215,12 +279,17 @@ class Service(xbmc.Monitor):
self.start_default() self.start_default()
elif method == 'ServerRestarting': elif method == "ServerRestarting":
if data.get('ServerId'): if data.get("ServerId"):
return return
if settings('restartMsg.bool'): if settings("restartMsg.bool"):
dialog("notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}") dialog(
"notification",
heading="{jellyfin}",
message=translate(33006),
icon="{jellyfin}",
)
self.stop_default() self.stop_default()
@ -229,67 +298,72 @@ class Service(xbmc.Monitor):
self.start_default() self.start_default()
elif method == 'ServerConnect': elif method == "ServerConnect":
self.connect.register(data['Id']) self.connect.register(data["Id"])
xbmc.executebuiltin("Container.Refresh") xbmc.executebuiltin("Container.Refresh")
elif method == 'AddServer': elif method == "AddServer":
self.connect.setup_manual_server() self.connect.setup_manual_server()
xbmc.executebuiltin("Container.Refresh") 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") xbmc.executebuiltin("Container.Refresh")
elif method == 'UpdatePassword': elif method == "UpdatePassword":
self.connect.setup_login_manual() self.connect.setup_login_manual()
elif method == 'UserDataChanged' and self.library_thread: elif method == "UserDataChanged" and self.library_thread:
if data.get('ServerId') or not window('jellyfin_startup.bool'): if data.get("ServerId") or not window("jellyfin_startup.bool"):
return return
LOG.info("[ UserDataChanged ] %s", data) LOG.info("[ UserDataChanged ] %s", data)
self.library_thread.userdata(data['UserDataList']) self.library_thread.userdata(data["UserDataList"])
elif method == 'LibraryChanged' and self.library_thread: elif method == "LibraryChanged" and self.library_thread:
if data.get('ServerId') or not window('jellyfin_startup.bool'): if data.get("ServerId") or not window("jellyfin_startup.bool"):
return return
LOG.info("[ LibraryChanged ] %s", data) LOG.info("[ LibraryChanged ] %s", data)
self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded']) self.library_thread.updated(data["ItemsUpdated"] + data["ItemsAdded"])
self.library_thread.removed(data['ItemsRemoved']) self.library_thread.removed(data["ItemsRemoved"])
elif method == 'System.OnQuit': elif method == "System.OnQuit":
window('jellyfin_should_stop.bool', True) window("jellyfin_should_stop.bool", True)
self.running = False self.running = False
elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'): elif method in (
"SyncLibrarySelection",
"RepairLibrarySelection",
"AddLibrarySelection",
"RemoveLibrarySelection",
):
self.library_thread.select_libraries(method) self.library_thread.select_libraries(method)
elif method == 'SyncLibrary': elif method == "SyncLibrary":
if not data.get('Id'): if not data.get("Id"):
return 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") xbmc.executebuiltin("Container.Refresh")
elif method == 'RepairLibrary': elif method == "RepairLibrary":
if not data.get('Id'): if not data.get("Id"):
return return
libraries = data['Id'].split(',') libraries = data["Id"].split(",")
for lib in libraries: for lib in libraries:
if not self.library_thread.remove_library(lib): if not self.library_thread.remove_library(lib):
return return
self.library_thread.add_library(data['Id']) self.library_thread.add_library(data["Id"])
xbmc.executebuiltin("Container.Refresh") xbmc.executebuiltin("Container.Refresh")
elif method == 'RemoveLibrary': elif method == "RemoveLibrary":
libraries = data['Id'].split(',') libraries = data["Id"].split(",")
for lib in libraries: for lib in libraries:
@ -298,10 +372,10 @@ class Service(xbmc.Monitor):
xbmc.executebuiltin("Container.Refresh") xbmc.executebuiltin("Container.Refresh")
elif method == 'System.OnSleep': elif method == "System.OnSleep":
LOG.info("-->[ sleep ]") LOG.info("-->[ sleep ]")
window('jellyfin_should_stop.bool', True) window("jellyfin_should_stop.bool", True)
if self.library_thread is not None: if self.library_thread is not None:
@ -312,7 +386,7 @@ class Service(xbmc.Monitor):
self.monitor.server = [] self.monitor.server = []
self.monitor.sleep = True self.monitor.sleep = True
elif method == 'System.OnWake': elif method == "System.OnWake":
if not self.monitor.sleep: if not self.monitor.sleep:
LOG.warning("System.OnSleep was never called, skip System.OnWake") LOG.warning("System.OnSleep was never called, skip System.OnWake")
@ -322,14 +396,14 @@ class Service(xbmc.Monitor):
LOG.info("--<[ sleep ]") LOG.info("--<[ sleep ]")
xbmc.sleep(10000) # Allow network to wake up xbmc.sleep(10000) # Allow network to wake up
self.monitor.sleep = False self.monitor.sleep = False
window('jellyfin_should_stop', clear=True) window("jellyfin_should_stop", clear=True)
try: try:
self.connect.register() self.connect.register()
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
elif method == 'GUI.OnScreensaverDeactivated': elif method == "GUI.OnScreensaverDeactivated":
LOG.info("--<[ screensaver ]") LOG.info("--<[ screensaver ]")
xbmc.sleep(5000) xbmc.sleep(5000)
@ -337,60 +411,80 @@ class Service(xbmc.Monitor):
if self.library_thread is not None: if self.library_thread is not None:
self.library_thread.fast_sync() 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() Views().get_views()
def onSettingsChanged(self): def onSettingsChanged(self):
"""React to setting changes that impact window values."""
''' React to setting changes that impact window values. if window("jellyfin_should_stop.bool"):
'''
if window('jellyfin_should_stop.bool'):
return return
if settings('logLevel') != self.settings['log_level']: if settings("logLevel") != self.settings["log_level"]:
log_level = settings('logLevel') log_level = settings("logLevel")
self.settings['logLevel'] = log_level self.settings["logLevel"] = log_level
LOG.info("New log level: %s", 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')) window("jellyfin_context", settings("enableContext"))
self.settings['enable_context'] = settings('enableContext.bool') self.settings["enable_context"] = settings("enableContext.bool")
LOG.info("New context setting: %s", self.settings['enable_context']) 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')) window("jellyfin_context_transcode", settings("enableContextTranscode"))
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool') self.settings["enable_context_transcode"] = settings(
LOG.info("New context transcode setting: %s", self.settings['enable_context_transcode']) "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') self.settings["mode"] = settings("useDirectPaths")
LOG.info("New playback mode setting: %s", self.settings['mode']) 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)) dialog("yesno", "{jellyfin}", translate(33118))
if settings('kodiCompanion.bool') != self.settings['kodi_companion']: if settings("kodiCompanion.bool") != self.settings["kodi_companion"]:
self.settings['kodi_companion'] = settings('kodiCompanion.bool') self.settings["kodi_companion"] = settings("kodiCompanion.bool")
if not self.settings['kodi_companion']: if not self.settings["kodi_companion"]:
dialog("ok", "{jellyfin}", translate(33138)) dialog("ok", "{jellyfin}", translate(33138))
def reload_objects(self): def reload_objects(self):
"""Reload objects which depends on the patch module.
''' Reload objects which depends on the patch module. This allows to see the changes in code without restarting the python interpreter.
This allows to see the changes in code without restarting the python interpreter. """
''' reload_modules = [
reload_modules = ['objects.movies', 'objects.musicvideos', 'objects.tvshows', "objects.movies",
'objects.music', 'objects.obj', 'objects.actions', 'objects.kodi.kodi', "objects.musicvideos",
'objects.kodi.movies', 'objects.kodi.musicvideos', 'objects.kodi.tvshows', "objects.tvshows",
'objects.kodi.music', 'objects.kodi.artwork', 'objects.kodi.queries', "objects.music",
'objects.kodi.queries_music', 'objects.kodi.queries_texture'] "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: for mod in reload_modules:
del sys.modules[mod] del sys.modules[mod]
@ -407,14 +501,22 @@ class Service(xbmc.Monitor):
def shutdown(self): def shutdown(self):
LOG.info("---<[ EXITING ]") LOG.info("---<[ EXITING ]")
window('jellyfin_should_stop.bool', True) window("jellyfin_should_stop.bool", True)
properties = [ # TODO: review properties = [ # TODO: review
"jellyfin_state", "jellyfin_serverStatus", "jellyfin_currUser", "jellyfin_state",
"jellyfin_serverStatus",
"jellyfin_play", "jellyfin_online", "jellyfin.connected", "jellyfin_startup", "jellyfin_currUser",
"jellyfin.external", "jellyfin.external_check", "jellyfin_deviceId", "jellyfin_db_check", "jellyfin_pathverified", "jellyfin_play",
"jellyfin_sync" "jellyfin_online",
"jellyfin.connected",
"jellyfin_startup",
"jellyfin.external",
"jellyfin.external_check",
"jellyfin_deviceId",
"jellyfin_db_check",
"jellyfin_pathverified",
"jellyfin_sync",
] ]
for prop in properties: for prop in properties:
window(prop, clear=True) window(prop, clear=True)

View File

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

View File

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

View File

@ -9,7 +9,11 @@ import warnings
class HTTPException(Exception): class HTTPException(Exception):
# Jellyfin HTTP exception # Jellyfin HTTP exception
def __init__(self, status, message): 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.status = status
self.message = message self.message = message
@ -26,4 +30,5 @@ class PathValidationException(Exception):
TODO: Investigate the usage of this to see if it can be done better. TODO: Investigate the usage of this to see if it can be done better.
""" """
pass pass

View File

@ -6,6 +6,7 @@ class LazyLogger(object):
"""`helper.loghandler.getLogger()` is used everywhere. """`helper.loghandler.getLogger()` is used everywhere.
This class helps to avoid import errors. This class helps to avoid import errors.
""" """
__logger = None __logger = None
__logger_name = None __logger_name = None
@ -15,5 +16,6 @@ class LazyLogger(object):
def __getattr__(self, name): def __getattr__(self, name):
if self.__logger is None: if self.__logger is None:
from .loghandler import getLogger from .loghandler import getLogger
self.__logger = getLogger(self.__logger_name) self.__logger = getLogger(self.__logger_name)
return getattr(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') __addon__ = xbmcaddon.Addon(id="plugin.video.jellyfin")
__pluginpath__ = translate_path(__addon__.getAddonInfo('path')) __pluginpath__ = translate_path(__addon__.getAddonInfo("path"))
################################################################################################## ##################################################################################################
@ -36,17 +36,17 @@ class LogHandler(logging.StreamHandler):
logging.StreamHandler.__init__(self) logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter()) 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'): if server.get("AccessToken"):
self.sensitive['Token'].append(server['AccessToken']) self.sensitive["Token"].append(server["AccessToken"])
if server.get('address'): if server.get("address"):
self.sensitive['Server'].append(server['address'].split('://')[1]) self.sensitive["Server"].append(server["address"].split("://")[1])
self.mask_info = settings('maskInfo.bool') self.mask_info = settings("maskInfo.bool")
if kodi_version() > 18: if kodi_version() > 18:
self.level = xbmc.LOGINFO self.level = xbmc.LOGINFO
@ -59,10 +59,10 @@ class LogHandler(logging.StreamHandler):
string = self.format(record) string = self.format(record)
if self.mask_info: if self.mask_info:
for server in self.sensitive['Server']: for server in self.sensitive["Server"]:
string = string.replace(server or "{server}", "{jellyfin-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}") string = string.replace(token or "{token}", "{jellyfin-token}")
xbmc.log(string, level=self.level) xbmc.log(string, level=self.level)
@ -74,10 +74,10 @@ class LogHandler(logging.StreamHandler):
logging.ERROR: 0, logging.ERROR: 0,
logging.WARNING: 0, logging.WARNING: 0,
logging.INFO: 1, logging.INFO: 1,
logging.DEBUG: 2 logging.DEBUG: 2,
} }
try: try:
log_level = int(settings('logLevel')) log_level = int(settings("logLevel"))
except ValueError: except ValueError:
log_level = 2 # If getting settings fail, we probably want debug logging. log_level = 2 # If getting settings fail, we probably want debug logging.
@ -86,7 +86,9 @@ class LogHandler(logging.StreamHandler):
class MyFormatter(logging.Formatter): 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) logging.Formatter.__init__(self, fmt)
def format(self, record): def format(self, record):
@ -116,14 +118,14 @@ class MyFormatter(logging.Formatter):
res.append(o) res.append(o)
return ''.join(res) return "".join(res)
def _gen_rel_path(self, record): def _gen_rel_path(self, record):
if record.pathname: if record.pathname:
record.relpath = os.path.relpath(record.pathname, __pluginpath__) record.relpath = os.path.relpath(record.pathname, __pluginpath__)
__LOGGER = logging.getLogger('JELLYFIN') __LOGGER = logging.getLogger("JELLYFIN")
for handler in __LOGGER.handlers: for handler in __LOGGER.handlers:
__LOGGER.removeHandler(handler) __LOGGER.removeHandler(handler)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,22 +25,22 @@ def ensure_client():
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return wrapper return wrapper
return decorator return decorator
class Jellyfin(object): 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 from jellyfin_kodi.jellyfin import Jellyfin
to communicate with the JellyfinClient().
from jellyfin_kodi.jellyfin import Jellyfin Jellyfin('123456').config.data['app']
Jellyfin('123456').config.data['app'] # Permanent client reference
client = Jellyfin('123456').get_client()
# Permanent client reference client.config.data['app']
client = Jellyfin('123456').get_client() """
client.config.data['app']
'''
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
_shared_state = {} _shared_state = {}
@ -94,7 +94,7 @@ class Jellyfin(object):
self.client[self.server_id] = JellyfinClient() self.client[self.server_id] = JellyfinClient()
if self.server_id == 'default': if self.server_id == "default":
LOG.info("---[ START JELLYFINCLIENT ]---") LOG.info("---[ START JELLYFINCLIENT ]---")
else: else:
LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id) LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id)

View File

@ -15,7 +15,7 @@ LOG = LazyLogger(__name__)
def jellyfin_url(client, handler): 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(): def basic_info():
@ -42,9 +42,8 @@ def music_info():
class API(object): class API(object):
"""All the api calls to the server."""
''' All the api calls to the server.
'''
def __init__(self, client, *args, **kwargs): def __init__(self, client, *args, **kwargs):
self.client = client self.client = client
self.config = client.config self.config = client.config
@ -54,18 +53,18 @@ class API(object):
if request is None: if request is None:
request = {} request = {}
request.update({'type': action, 'handler': url}) request.update({"type": action, "handler": url})
return self.client.request(request) return self.client.request(request)
def _get(self, handler, params=None): 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): 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): 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): def artwork(self, item_id, art, max_width, ext="jpg", index=None):
if index is 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) return self.users("/Items/%s" % item_id)
def get_items(self, item_ids): def get_items(self, item_ids):
return self.users("/Items", params={ return self.users(
'Ids': ','.join(str(x) for x in item_ids), "/Items",
'Fields': info() params={"Ids": ",".join(str(x) for x in item_ids), "Fields": info()},
}) )
def get_sessions(self): def get_sessions(self):
return self.sessions(params={'ControllableByUserId': "{UserId}"}) return self.sessions(params={"ControllableByUserId": "{UserId}"})
def get_device(self, device_id): 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): def post_session(self, session_id, url, params=None, data=None):
return self.sessions("/%s/%s" % (session_id, url), "POST", params, data) 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) return self.items("/%s/Images" % item_id)
def get_suggestion(self, media="Movie,Episode", limit=1): def get_suggestion(self, media="Movie,Episode", limit=1):
return self.users("/Suggestions", params={ return self.users("/Suggestions", params={"Type": media, "Limit": limit})
'Type': media,
'Limit': limit
})
def get_recently_added(self, media=None, parent_id=None, limit=20): def get_recently_added(self, media=None, parent_id=None, limit=20):
return self.user_items("/Latest", { return self.user_items(
'Limit': limit, "/Latest",
'UserId': "{UserId}", {
'IncludeItemTypes': media, "Limit": limit,
'ParentId': parent_id, "UserId": "{UserId}",
'Fields': info() "IncludeItemTypes": media,
}) "ParentId": parent_id,
"Fields": info(),
},
)
def get_next(self, index=None, limit=1): def get_next(self, index=None, limit=1):
return self.shows("/NextUp", { return self.shows(
'Limit': limit, "/NextUp",
'UserId': "{UserId}", {
'StartIndex': None if index is None else int(index) "Limit": limit,
}) "UserId": "{UserId}",
"StartIndex": None if index is None else int(index),
},
)
def get_adjacent_episodes(self, show_id, item_id): def get_adjacent_episodes(self, show_id, item_id):
return self.shows("/%s/Episodes" % show_id, { return self.shows(
'UserId': "{UserId}", "/%s/Episodes" % show_id,
'AdjacentTo': item_id, {"UserId": "{UserId}", "AdjacentTo": item_id, "Fields": "Overview"},
'Fields': "Overview" )
})
def get_genres(self, parent_id=None): def get_genres(self, parent_id=None):
return self._get("Genres", { return self._get(
'ParentId': parent_id, "Genres", {"ParentId": parent_id, "UserId": "{UserId}", "Fields": info()}
'UserId': "{UserId}", )
'Fields': info()
})
def get_recommendation(self, parent_id=None, limit=20): def get_recommendation(self, parent_id=None, limit=20):
return self._get("Movies/Recommendations", { return self._get(
'ParentId': parent_id, "Movies/Recommendations",
'UserId': "{UserId}", {
'Fields': info(), "ParentId": parent_id,
'Limit': limit "UserId": "{UserId}",
}) "Fields": info(),
"Limit": limit,
},
)
def get_items_by_letter(self, parent_id=None, media=None, letter=None): def get_items_by_letter(self, parent_id=None, media=None, letter=None):
return self.user_items(params={ return self.user_items(
'ParentId': parent_id, params={
'NameStartsWith': letter, "ParentId": parent_id,
'Fields': info(), "NameStartsWith": letter,
'Recursive': True, "Fields": info(),
'IncludeItemTypes': media "Recursive": True,
}) "IncludeItemTypes": media,
}
)
def get_channels(self): def get_channels(self):
return self._get("LiveTv/Channels", { return self._get(
'UserId': "{UserId}", "LiveTv/Channels",
'EnableImages': True, {"UserId": "{UserId}", "EnableImages": True, "EnableUserData": True},
'EnableUserData': True )
})
def get_intros(self, item_id): def get_intros(self, item_id):
return self.user_items("/%s/Intros" % 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) return self.user_items("/%s/LocalTrailers" % item_id)
def get_transcode_settings(self): def get_transcode_settings(self):
return self._get('System/Configuration/encoding') return self._get("System/Configuration/encoding")
def get_ancestors(self, item_id): def get_ancestors(self, item_id):
return self.items("/%s/Ancestors" % item_id, params={ return self.items("/%s/Ancestors" % item_id, params={"UserId": "{UserId}"})
'UserId': "{UserId}"
})
def get_items_theme_video(self, parent_id): def get_items_theme_video(self, parent_id):
return self.users("/Items", params={ return self.users(
'HasThemeVideo': True, "/Items", params={"HasThemeVideo": True, "ParentId": parent_id}
'ParentId': parent_id )
})
def get_themes(self, item_id): def get_themes(self, item_id):
return self.items("/%s/ThemeMedia" % item_id, params={ return self.items(
'UserId': "{UserId}", "/%s/ThemeMedia" % item_id,
'InheritFromParent': True params={"UserId": "{UserId}", "InheritFromParent": True},
}) )
def get_items_theme_song(self, parent_id): def get_items_theme_song(self, parent_id):
return self.users("/Items", params={ return self.users(
'HasThemeSong': True, "/Items", params={"HasThemeSong": True, "ParentId": parent_id}
'ParentId': parent_id )
})
def check_companion_enabled(self): def check_companion_enabled(self):
""" """
@ -262,8 +269,10 @@ class API(object):
None = Unknown None = Unknown
""" """
try: try:
plugin_settings = self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {} plugin_settings = (
return plugin_settings.get('IsEnabled') self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {}
)
return plugin_settings.get("IsEnabled")
except requests.RequestException as e: except requests.RequestException as e:
LOG.warning("Error checking companion installed state: %s", e) LOG.warning("Error checking companion installed state: %s", e)
@ -277,42 +286,51 @@ class API(object):
return None return None
def get_seasons(self, show_id): def get_seasons(self, show_id):
return self.shows("/%s/Seasons" % show_id, params={ return self.shows(
'UserId': "{UserId}", "/%s/Seasons" % show_id,
'EnableImages': True, params={"UserId": "{UserId}", "EnableImages": True, "Fields": info()},
'Fields': info() )
})
def get_date_modified(self, date, parent_id, media=None): def get_date_modified(self, date, parent_id, media=None):
return self.users("/Items", params={ return self.users(
'ParentId': parent_id, "/Items",
'Recursive': False, params={
'IsMissing': False, "ParentId": parent_id,
'IsVirtualUnaired': False, "Recursive": False,
'IncludeItemTypes': media or None, "IsMissing": False,
'MinDateLastSaved': date, "IsVirtualUnaired": False,
'Fields': info() "IncludeItemTypes": media or None,
}) "MinDateLastSaved": date,
"Fields": info(),
},
)
def get_userdata_date_modified(self, date, parent_id, media=None): def get_userdata_date_modified(self, date, parent_id, media=None):
return self.users("/Items", params={ return self.users(
'ParentId': parent_id, "/Items",
'Recursive': True, params={
'IsMissing': False, "ParentId": parent_id,
'IsVirtualUnaired': False, "Recursive": True,
'IncludeItemTypes': media or None, "IsMissing": False,
'MinDateLastSavedForUser': date, "IsVirtualUnaired": False,
'Fields': info() "IncludeItemTypes": media or None,
}) "MinDateLastSavedForUser": date,
"Fields": info(),
},
)
def refresh_item(self, item_id): def refresh_item(self, item_id):
return self.items("/%s/Refresh" % item_id, "POST", json={ return self.items(
'Recursive': True, "/%s/Refresh" % item_id,
'ImageRefreshMode': "FullRefresh", "POST",
'MetadataRefreshMode': "FullRefresh", json={
'ReplaceAllImages': False, "Recursive": True,
'ReplaceAllMetadata': True "ImageRefreshMode": "FullRefresh",
}) "MetadataRefreshMode": "FullRefresh",
"ReplaceAllImages": False,
"ReplaceAllMetadata": True,
},
)
def favorite(self, item_id, option=True): def favorite(self, item_id, option=True):
return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE") 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) return self.sessions("/Capabilities/Full", "POST", json=data)
def session_add_user(self, session_id, user_id, option=True): 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): def session_playing(self, data):
return self.sessions("/Playing", "POST", json=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") return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE")
def get_sync_queue(self, date, filters=None): def get_sync_queue(self, date, filters=None):
return self._get("Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems", params={ return self._get(
'LastUpdateDT': date, "Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems",
'filter': filters or 'None' params={"LastUpdateDT": date, "filter": filters or "None"},
}) )
def get_server_time(self): def get_server_time(self):
return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime") return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime")
def get_play_info(self, item_id, profile): def get_play_info(self, item_id, profile):
return self.items("/%s/PlaybackInfo" % item_id, "POST", json={ return self.items(
'UserId': "{UserId}", "/%s/PlaybackInfo" % item_id,
'DeviceProfile': profile, "POST",
'AutoOpenLiveStream': True json={
}) "UserId": "{UserId}",
"DeviceProfile": profile,
"AutoOpenLiveStream": True,
},
)
def get_live_stream(self, item_id, play_id, token, profile): def get_live_stream(self, item_id, play_id, token, profile):
return self._post("LiveStreams/Open", json={ return self._post(
'UserId': "{UserId}", "LiveStreams/Open",
'DeviceProfile': profile, json={
'OpenToken': token, "UserId": "{UserId}",
'PlaySessionId': play_id, "DeviceProfile": profile,
'ItemId': item_id "OpenToken": token,
}) "PlaySessionId": play_id,
"ItemId": item_id,
},
)
def close_live_stream(self, live_id): def close_live_stream(self, live_id):
return self._post("LiveStreams/Close", json={ return self._post("LiveStreams/Close", json={"LiveStreamId": live_id})
'LiveStreamId': live_id
})
def close_transcode(self, device_id, play_id): def close_transcode(self, device_id, play_id):
return self._delete("Videos/ActiveEncodings", params={ return self._delete(
'DeviceId': device_id, "Videos/ActiveEncodings",
'PlaySessionId': play_id params={"DeviceId": device_id, "PlaySessionId": play_id},
}) )
def get_default_headers(self): def get_default_headers(self):
auth = "MediaBrowser " auth = "MediaBrowser "
auth += "Client=%s, " % self.config.data['app.name'] auth += "Client=%s, " % self.config.data["app.name"]
auth += "Device=%s, " % self.config.data['app.device_name'] auth += "Device=%s, " % self.config.data["app.device_name"]
auth += "DeviceId=%s, " % self.config.data['app.device_id'] auth += "DeviceId=%s, " % self.config.data["app.device_id"]
auth += "Version=%s" % self.config.data['app.version'] auth += "Version=%s" % self.config.data["app.version"]
return { return {
"Accept": "application/json", "Accept": "application/json",
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8", "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-Charset": "UTF-8,*",
"Accept-encoding": "gzip", "Accept-encoding": "gzip",
"User-Agent": self.config.data['http.user_agent'] or "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']), "User-Agent": self.config.data["http.user_agent"]
"x-emby-authorization": ensure_str(auth, 'utf-8') 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()) request_method = getattr(requests, method.lower())
url = "%s/%s" % (url, path) url = "%s/%s" % (url, path)
request_settings = { request_settings = {
"timeout": timeout or self.default_timeout, "timeout": timeout or self.default_timeout,
"headers": headers or self.get_default_headers(), "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.info("Sending %s request to %s" % (method, path))
LOG.debug(request_settings['timeout']) LOG.debug(request_settings["timeout"])
LOG.debug(request_settings['headers']) LOG.debug(request_settings["headers"])
return request_method(url, **request_settings) return request_method(url, **request_settings)
def login(self, server_url, username, password=""): def login(self, server_url, username, password=""):
path = "Users/AuthenticateByName" path = "Users/AuthenticateByName"
auth_data = { auth_data = {"username": username, "Pw": password}
"username": username,
"Pw": password
}
headers = self.get_default_headers() headers = self.get_default_headers()
headers.update({'Content-type': "application/json"}) headers.update({"Content-type": "application/json"})
try: try:
LOG.info("Trying to login to %s/%s as %s" % (server_url, path, username)) 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: if response.status_code == 200:
return response.json() return response.json()
else: 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.error("Server Response:\n" + str(response.content))
LOG.debug(headers) LOG.debug(headers)
return {} 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) LOG.error(e)
return {} return {}
def validate_authentication_token(self, server): def validate_authentication_token(self, server):
auth_token_header = { auth_token_header = {"X-MediaBrowser-Token": server["AccessToken"]}
'X-MediaBrowser-Token': server['AccessToken']
}
headers = self.get_default_headers() headers = self.get_default_headers()
headers.update(auth_token_header) 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: if response.status_code == 200:
return response.json() return response.json()
else: else:
return {'Status_Code': response.status_code} return {"Status_Code": response.status_code}
def get_public_info(self, server_address): def get_public_info(self, server_address):
response = self.send_request(server_address, "system/info/public") response = self.send_request(server_address, "system/info/public")
@ -459,8 +496,8 @@ class API(object):
return {} return {}
def check_redirect(self, server_address): 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 returns the URL the server prefers to use
''' """
response = self.send_request(server_address, "system/info/public") 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): def callback(message, data):
"""Callback function should receive message, data
''' Callback function should receive message, data message: string
message: string data: json dictionary
data: json dictionary """
'''
pass pass
@ -53,13 +52,13 @@ class JellyfinClient(object):
self.set_credentials(credentials or {}) self.set_credentials(credentials or {})
state = self.auth.connect(options or {}) state = self.auth.connect(options or {})
if state['State'] == CONNECTION_STATE['SignedIn']: if state["State"] == CONNECTION_STATE["SignedIn"]:
LOG.info("User is authenticated.") LOG.info("User is authenticated.")
self.logged_in = True 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 return state

View File

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

View File

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

View File

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

View File

@ -34,9 +34,13 @@ class HTTP(object):
self.session = requests.Session() self.session = requests.Session()
max_retries = self.config.data['http.max_retries'] max_retries = self.config.data["http.max_retries"]
self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries)) self.session.mount(
self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries)) "http://", requests.adapters.HTTPAdapter(max_retries=max_retries)
)
self.session.mount(
"https://", requests.adapters.HTTPAdapter(max_retries=max_retries)
)
def stop_session(self): def stop_session(self):
@ -51,43 +55,44 @@ class HTTP(object):
def _replace_user_info(self, string): def _replace_user_info(self, string):
if '{server}' in string: if "{server}" in string:
if self.config.data.get('auth.server', None): if self.config.data.get("auth.server", None):
string = string.replace("{server}", self.config.data['auth.server']) string = string.replace("{server}", self.config.data["auth.server"])
else: else:
LOG.debug("Server address not set") LOG.debug("Server address not set")
if '{UserId}' in string: if "{UserId}" in string:
if self.config.data.get('auth.user_id', None): if self.config.data.get("auth.user_id", None):
string = string.replace("{UserId}", self.config.data['auth.user_id']) string = string.replace("{UserId}", self.config.data["auth.user_id"])
else: else:
LOG.debug("UserId is not set.") LOG.debug("UserId is not set.")
return string return string
def request(self, data, session=None): def request(self, data, session=None):
"""Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
''' Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back data dictionary can contain:
data dictionary can contain: type: GET, POST, etc.
type: GET, POST, etc. url: (optional)
url: (optional) handler: not considered when url is provided (optional)
handler: not considered when url is provided (optional) params: request parameters (optional)
params: request parameters (optional) json: request body (optional)
json: request body (optional) headers: (optional),
headers: (optional), verify: ssl certificate, True (verify using device built-in library) or False
verify: ssl certificate, True (verify using device built-in library) or False """
'''
if not data: if not data:
raise AttributeError("Request cannot be empty") raise AttributeError("Request cannot be empty")
data = self._request(data) data = self._request(data)
LOG.debug("--->[ http ] %s", JsonDebugPrinter(data)) LOG.debug("--->[ http ] %s", JsonDebugPrinter(data))
retry = data.pop('retry', 5) retry = data.pop("retry", 5)
while True: while True:
try: 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 r.content # release the connection
if not self.keep_alive and self.session is not None: if not self.keep_alive and self.session is not None:
@ -104,7 +109,10 @@ class HTTP(object):
continue continue
LOG.error(error) 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) raise HTTPException("ServerUnreachable", error)
@ -125,12 +133,18 @@ class HTTP(object):
if r.status_code == 401: if r.status_code == 401:
if 'X-Application-Error-Code' in r.headers: if "X-Application-Error-Code" in r.headers:
self.client.callback("AccessRestricted", {'ServerId': self.config.data['auth.server-id']}) self.client.callback(
"AccessRestricted",
{"ServerId": self.config.data["auth.server-id"]},
)
raise HTTPException("AccessRestricted", error) raise HTTPException("AccessRestricted", error)
else: 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() self.client.auth.revoke_token()
raise HTTPException("Unauthorized", error) raise HTTPException("Unauthorized", error)
@ -160,11 +174,13 @@ class HTTP(object):
except requests.exceptions.MissingSchema as error: except requests.exceptions.MissingSchema as error:
LOG.error("Request missing Schema. " + str(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: else:
try: try:
self.config.data['server-time'] = r.headers['Date'] self.config.data["server-time"] = r.headers["Date"]
elapsed = int(r.elapsed.total_seconds() * 1000) elapsed = int(r.elapsed.total_seconds() * 1000)
response = r.json() response = r.json()
LOG.debug("---<[ http ][%s ms]", elapsed) LOG.debug("---<[ http ][%s ms]", elapsed)
@ -179,15 +195,18 @@ class HTTP(object):
def _request(self, data): def _request(self, data):
if 'url' not in data: if "url" not in data:
data['url'] = "%s/%s" % (self.config.data.get("auth.server", ""), data.pop('handler', "")) data["url"] = "%s/%s" % (
self.config.data.get("auth.server", ""),
data.pop("handler", ""),
)
self._get_header(data) self._get_header(data)
data['timeout'] = data.get('timeout') or self.config.data['http.timeout'] 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["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False)
data['url'] = self._replace_user_info(data['url']) data["url"] = self._replace_user_info(data["url"])
self._process_params(data.get('params') or {}) self._process_params(data.get("params") or {})
self._process_params(data.get('json') or {}) self._process_params(data.get("json") or {})
return data return data
@ -204,17 +223,24 @@ class HTTP(object):
def _get_header(self, data): def _get_header(self, data):
data['headers'] = data.setdefault('headers', {}) data["headers"] = data.setdefault("headers", {})
if not data['headers']: if not data["headers"]:
data['headers'].update({ data["headers"].update(
'Content-type': "application/json", {
'Accept-Charset': "UTF-8,*", "Content-type": "application/json",
'Accept-encoding': "gzip", "Accept-Charset": "UTF-8,*",
'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")) "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) self._authorization(data)
return data return data
@ -222,19 +248,26 @@ class HTTP(object):
def _authorization(self, data): def _authorization(self, data):
auth = "MediaBrowser " auth = "MediaBrowser "
auth += "Client=%s, " % self.config.data.get('app.name', "Jellyfin for Kodi") 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 += "Device=%s, " % self.config.data.get(
auth += "DeviceId=%s, " % self.config.data.get('app.device_id', 'Unknown Device id') "app.device_name", "Unknown Device"
auth += "Version=%s" % self.config.data.get('app.version', '0.0.0') )
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') auth += ", UserId=%s" % self.config.data.get("auth.user_id")
data['headers'].update({ data["headers"].update(
'x-emby-authorization': ensure_str(auth, 'utf-8'), {
'X-MediaBrowser-Token': self.config.data.get('auth.token')}) "x-emby-authorization": ensure_str(auth, "utf-8"),
"X-MediaBrowser-Token": self.config.data.get("auth.token"),
}
)
return data 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 # 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 # kodi hard crashes for reasons I don't even want to pretend to understand
import sys # noqa: E402,I100 import sys # noqa: E402,I100
sys.modules['numpy'] = None
sys.modules["numpy"] = None
import websocket # noqa: E402,I201 import websocket # noqa: E402,I201
################################################################################################## ##################################################################################################
@ -41,23 +42,29 @@ class WSClient(threading.Thread):
if self.wsc is None: if self.wsc is None:
raise ValueError("The websocket client is not started.") 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): def run(self):
monitor = xbmc.Monitor() monitor = xbmc.Monitor()
token = self.client.config.data['auth.token'] token = self.client.config.data["auth.token"]
device_id = self.client.config.data['app.device_id'] device_id = self.client.config.data["app.device_id"]
server = self.client.config.data['auth.server'] server = self.client.config.data["auth.server"]
server = server.replace('https://', 'wss://') if server.startswith('https') else server.replace('http://', 'ws://') 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) wsc_url = "%s/socket?api_key=%s&device_id=%s" % (server, token, device_id)
LOG.info("Websocket url: %s", wsc_url) LOG.info("Websocket url: %s", wsc_url)
self.wsc = websocket.WebSocketApp(wsc_url, self.wsc = websocket.WebSocketApp(
on_open=lambda ws: self.on_open(ws), wsc_url,
on_message=lambda ws, message: self.on_message(ws, message), on_open=lambda ws: self.on_open(ws),
on_error=lambda ws, error: self.on_error(ws, error)) on_message=lambda ws, message: self.on_message(ws, message),
on_error=lambda ws, error: self.on_error(ws, error),
)
while not self.stop: while not self.stop:
@ -73,41 +80,42 @@ class WSClient(threading.Thread):
LOG.info("--->[ websocket opened ]") LOG.info("--->[ websocket opened ]")
# Avoid a timing issue where the capabilities are not correctly registered # Avoid a timing issue where the capabilities are not correctly registered
time.sleep(5) time.sleep(5)
if settings('remoteControl.bool'): if settings("remoteControl.bool"):
self.client.jellyfin.post_capabilities({ self.client.jellyfin.post_capabilities(
'PlayableMediaTypes': "Audio,Video", {
'SupportsMediaControl': True, "PlayableMediaTypes": "Audio,Video",
'SupportedCommands': ( "SupportsMediaControl": True,
"MoveUp,MoveDown,MoveLeft,MoveRight,Select," "SupportedCommands": (
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu," "MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"GoHome,PageUp,NextLetter,GoToSearch," "Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot," "GoHome,PageUp,NextLetter,GoToSearch,"
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage," "GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
"SetAudioStreamIndex,SetSubtitleStreamIndex," "VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
"SetRepeatMode,Mute,Unmute,SetVolume," "SetAudioStreamIndex,SetSubtitleStreamIndex,"
"Play,Playstate,PlayNext,PlayMediaSource" "SetRepeatMode,Mute,Unmute,SetVolume,"
), "Play,Playstate,PlayNext,PlayMediaSource"
}) ),
}
)
else: else:
self.client.jellyfin.post_capabilities({ self.client.jellyfin.post_capabilities(
"PlayableMediaTypes": "Audio, Video", {"PlayableMediaTypes": "Audio, Video", "SupportsMediaControl": False}
"SupportsMediaControl": False )
})
def on_message(self, ws, message): def on_message(self, ws, message):
message = json.loads(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) LOG.debug("Ignoring %s", message)
return return
if not self.client.config.data['app.default']: if not self.client.config.data["app.default"]:
data['ServerId'] = self.client.auth.server_id data["ServerId"] = self.client.auth.server_id
self.client.callback(message['MessageType'], data) self.client.callback(message["MessageType"], data)
def stop_client(self): def stop_client(self):

View File

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

View File

@ -46,23 +46,35 @@ class Monitor(xbmc.Monitor):
def onNotification(self, sender, method, data): 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 return
if sender == 'plugin.video.jellyfin': if sender == "plugin.video.jellyfin":
method = method.split('.')[1] 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 return
data = json.loads(data)[0] data = json.loads(data)[0]
elif sender.startswith('upnextprovider'): elif sender.startswith("upnextprovider"):
LOG.info('Attempting to play the next episode via upnext') LOG.info("Attempting to play the next episode via upnext")
method = method.split('.', 1)[1] method = method.split(".", 1)[1]
if method not in ('plugin.video.jellyfin_play_action',): if method not in ("plugin.video.jellyfin_play_action",):
LOG.info('Received invalid upnext method: %s', method) LOG.info("Received invalid upnext method: %s", method)
return return
data = json.loads(data) data = json.loads(data)
@ -71,15 +83,23 @@ class Monitor(xbmc.Monitor):
if data: if data:
data = json.loads(binascii.unhexlify(data[0])) data = json.loads(binascii.unhexlify(data[0]))
else: 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. """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. Otherwise the next played item will be added the previous queue.
''' """
if method == "Player.OnStop": 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") xbmc.executebuiltin("Playlist.Clear")
LOG.info("[ playlist ] cleared") LOG.info("[ playlist ] cleared")
@ -96,14 +116,14 @@ class Monitor(xbmc.Monitor):
return return
try: try:
if not data.get('ServerId'): if not data.get("ServerId"):
server = Jellyfin() server = Jellyfin()
else: else:
if method != 'LoadServer' and data['ServerId'] not in self.servers: if method != "LoadServer" and data["ServerId"] not in self.servers:
try: try:
connect.Connect().register(data['ServerId']) connect.Connect().register(data["ServerId"])
self.server_instance(data['ServerId']) self.server_instance(data["ServerId"])
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
@ -111,80 +131,90 @@ class Monitor(xbmc.Monitor):
return return
server = Jellyfin(data['ServerId']) server = Jellyfin(data["ServerId"])
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
server = Jellyfin() server = Jellyfin()
server = server.get_client() 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', PlaylistWorker(
data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), data.get("ServerId"),
data.get('SubtitleStreamIndex')).start() items,
data["PlayCommand"] == "PlayNow",
data.get("StartPositionTicks", 0),
data.get("AudioStreamIndex"),
data.get("SubtitleStreamIndex"),
).start()
# TODO no clue if this is called by anything # 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", { server.jellyfin.post_session(
'PlayCommand': "PlayNow", server.config.data["app.session"],
'ItemIds': data['Id'], "Playing",
'StartPositionTicks': 0 {
}) "PlayCommand": "PlayNow",
"ItemIds": data["Id"],
"StartPositionTicks": 0,
},
)
elif method in ('ReportProgressRequested', 'Player.OnAVChange'): elif method in ("ReportProgressRequested", "Player.OnAVChange"):
self.player.report_playback(data.get('Report', True)) self.player.report_playback(data.get("Report", True))
elif method == 'Playstate': elif method == "Playstate":
self.playstate(data) self.playstate(data)
elif method == 'GeneralCommand': elif method == "GeneralCommand":
self.general_commands(data) self.general_commands(data)
elif method == 'LoadServer': elif method == "LoadServer":
self.server_instance(data['ServerId']) self.server_instance(data["ServerId"])
elif method == 'AddUser': elif method == "AddUser":
server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add']) server.jellyfin.session_add_user(
server.config.data["app.session"], data["Id"], data["Add"]
)
self.additional_users(server) self.additional_users(server)
elif method == 'Player.OnPlay': elif method == "Player.OnPlay":
on_play(data, server) on_play(data, server)
elif method == 'VideoLibrary.OnUpdate': elif method == "VideoLibrary.OnUpdate":
on_update(data, server) on_update(data, server)
def server_instance(self, server_id=None): def server_instance(self, server_id=None):
server = Jellyfin(server_id).get_client() server = Jellyfin(server_id).get_client()
session = server.jellyfin.get_device(self.device_id) 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: if server_id is not None:
self.servers.append(server_id) 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() all_users = server.jellyfin.get_users()
for additional in users: for additional in users:
for user in all_users: for user in all_users:
if user['Name'].lower() in additional.lower(): if user["Name"].lower() in additional.lower():
server.jellyfin.session_add_user(server.config.data['app.session'], user['Id'], True) server.jellyfin.session_add_user(
server.config.data["app.session"], user["Id"], True
)
self.additional_users(server) self.additional_users(server)
def additional_users(self, server): def additional_users(self, server):
"""Setup additional users images."""
''' Setup additional users images.
'''
for i in range(10): for i in range(10):
window('JellyfinAdditionalUserImage.%s' % i, clear=True) window("JellyfinAdditionalUserImage.%s" % i, clear=True)
try: try:
session = server.jellyfin.get_device(self.device_id) session = server.jellyfin.get_device(self.device_id)
@ -193,31 +223,31 @@ class Monitor(xbmc.Monitor):
return return
for index, user in enumerate(session[0]['AdditionalUsers']): for index, user in enumerate(session[0]["AdditionalUsers"]):
info = server.jellyfin.get_user(user['UserId']) info = server.jellyfin.get_user(user["UserId"])
image = api.API(info, server.config.data['auth.server']).get_user_artwork(user['UserId']) image = api.API(info, server.config.data["auth.server"]).get_user_artwork(
window('JellyfinAdditionalUserImage.%s' % index, image) user["UserId"]
window('JellyfinAdditionalUserPosition.%s' % user['UserId'], str(index)) )
window("JellyfinAdditionalUserImage.%s" % index, image)
window("JellyfinAdditionalUserPosition.%s" % user["UserId"], str(index))
def playstate(self, data): def playstate(self, data):
"""Jellyfin playstate updates."""
''' Jellyfin playstate updates. command = data["Command"]
'''
command = data['Command']
actions = { actions = {
'Stop': self.player.stop, "Stop": self.player.stop,
'Unpause': self.player.pause, "Unpause": self.player.pause,
'Pause': self.player.pause, "Pause": self.player.pause,
'PlayPause': self.player.pause, "PlayPause": self.player.pause,
'NextTrack': self.player.playnext, "NextTrack": self.player.playnext,
'PreviousTrack': self.player.playprevious "PreviousTrack": self.player.playprevious,
} }
if command == 'Seek': if command == "Seek":
if self.player.isPlaying(): if self.player.isPlaying():
seektime = data['SeekPositionTicks'] / 10000000.0 seektime = data["SeekPositionTicks"] / 10000000.0
self.player.seekTime(seektime) self.player.seekTime(seektime)
LOG.info("[ seek/%s ]", seektime) LOG.info("[ seek/%s ]", seektime)
@ -227,69 +257,78 @@ class Monitor(xbmc.Monitor):
LOG.info("[ command/%s ]", command) LOG.info("[ command/%s ]", command)
def general_commands(self, data): 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. if command in (
''' "Mute",
command = data['Name'] "Unmute",
args = data['Arguments'] "SetVolume",
"SetSubtitleStreamIndex",
"SetAudioStreamIndex",
"SetRepeatMode",
):
if command in ('Mute', 'Unmute', 'SetVolume', if command in ["Mute", "Unmute"]:
'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'): 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']: elif command == "SetVolume":
xbmc.executebuiltin('Mute') xbmc.executebuiltin("SetVolume(%s[,showvolumebar])" % args["Volume"])
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'])
# Kodi needs a bit of time to update its current status # Kodi needs a bit of time to update its current status
xbmc.sleep(500) xbmc.sleep(500)
self.player.report_playback() self.player.report_playback()
elif command == 'DisplayMessage': elif command == "DisplayMessage":
dialog("notification", heading=args['Header'], message=args['Text'], dialog(
icon="{jellyfin}", time=int(settings('displayMessage')) * 1000) "notification",
heading=args["Header"],
message=args["Text"],
icon="{jellyfin}",
time=int(settings("displayMessage")) * 1000,
)
elif command == 'SendString': elif command == "SendString":
JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) JSONRPC("Input.SendText").execute({"text": args["String"], "done": False})
elif command == 'GoHome': elif command == "GoHome":
JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) JSONRPC("GUI.ActivateWindow").execute({"window": "home"})
elif command == 'Guide': elif command == "Guide":
JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) JSONRPC("GUI.ActivateWindow").execute({"window": "tvguide"})
elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): elif command in ("MoveUp", "MoveDown", "MoveRight", "MoveLeft"):
actions = { actions = {
'MoveUp': "Input.Up", "MoveUp": "Input.Up",
'MoveDown': "Input.Down", "MoveDown": "Input.Down",
'MoveRight': "Input.Right", "MoveRight": "Input.Right",
'MoveLeft': "Input.Left" "MoveLeft": "Input.Left",
} }
JSONRPC(actions[command]).execute() JSONRPC(actions[command]).execute()
else: else:
builtin = { builtin = {
'ToggleFullscreen': 'Action(FullScreen)', "ToggleFullscreen": "Action(FullScreen)",
'ToggleOsdMenu': 'Action(OSD)', "ToggleOsdMenu": "Action(OSD)",
'ToggleContextMenu': 'Action(ContextMenu)', "ToggleContextMenu": "Action(ContextMenu)",
'Select': 'Action(Select)', "Select": "Action(Select)",
'Back': 'Action(back)', "Back": "Action(back)",
'PageUp': 'Action(PageUp)', "PageUp": "Action(PageUp)",
'NextLetter': 'Action(NextLetter)', "NextLetter": "Action(NextLetter)",
'GoToSearch': 'VideoLibrary.Search', "GoToSearch": "VideoLibrary.Search",
'GoToSettings': 'ActivateWindow(Settings)', "GoToSettings": "ActivateWindow(Settings)",
'PageDown': 'Action(PageDown)', "PageDown": "Action(PageDown)",
'PreviousLetter': 'Action(PrevLetter)', "PreviousLetter": "Action(PrevLetter)",
'TakeScreenshot': 'TakeScreenshot', "TakeScreenshot": "TakeScreenshot",
'ToggleMute': 'Mute', "ToggleMute": "Mute",
'VolumeUp': 'Action(VolumeUp)', "VolumeUp": "Action(VolumeUp)",
'VolumeDown': 'Action(VolumeDown)', "VolumeDown": "Action(VolumeDown)",
} }
if command in builtin: if command in builtin:
xbmc.executebuiltin(builtin[command]) xbmc.executebuiltin(builtin[command])
@ -305,10 +344,9 @@ class Listener(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
def run(self): def run(self):
"""Detect the resume dialog for widgets.
''' Detect the resume dialog for widgets. Detect external players.
Detect external players. """
'''
LOG.info("--->[ listener ]") LOG.info("--->[ listener ]")
while not self.stop_thread: 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 self.cursor = cursor
def update(self, image_url, kodi_id, media, image): def update(self, image_url, kodi_id, media, image):
"""Update artwork in the video database.
''' Update artwork in the video database. Delete current entry before updating with the new one.
Delete current entry before updating with the new one. """
''' if not image_url or image == "poster" and media in ("song", "artist", "album"):
if not image_url or image == 'poster' and media in ('song', 'artist', 'album'):
return return
try: 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] url = self.cursor.fetchone()[0]
except TypeError: except TypeError:
@ -41,43 +47,39 @@ class Artwork(object):
self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image)) self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image))
def add(self, artwork, *args): def add(self, artwork, *args):
"""Add all artworks."""
''' Add all artworks.
'''
KODI = { KODI = {
'Primary': ['thumb', 'poster'], "Primary": ["thumb", "poster"],
'Banner': "banner", "Banner": "banner",
'Logo': "clearlogo", "Logo": "clearlogo",
'Art': "clearart", "Art": "clearart",
'Thumb': "landscape", "Thumb": "landscape",
'Disc': "discart", "Disc": "discart",
'Backdrop': "fanart" "Backdrop": "fanart",
} }
for art in KODI: for art in KODI:
if art == 'Backdrop': if art == "Backdrop":
self.cursor.execute(QU.get_backdrops, args + ("fanart%",)) 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_",)) self.cursor.execute(QU.delete_backdrops, args + ("fanart_",))
for index, backdrop in enumerate(artwork['Backdrop']): for index, backdrop in enumerate(artwork["Backdrop"]):
if index: if index:
self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),)) self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),))
else: else:
self.update(*(backdrop,) + args + ("fanart",)) self.update(*(backdrop,) + args + ("fanart",))
elif art == 'Primary': elif art == "Primary":
for kodi_image in KODI['Primary']: for kodi_image in KODI["Primary"]:
self.update(*(artwork['Primary'],) + args + (kodi_image,)) self.update(*(artwork["Primary"],) + args + (kodi_image,))
elif artwork.get(art): elif artwork.get(art):
self.update(*(artwork[art],) + args + (KODI[art],)) self.update(*(artwork[art],) + args + (KODI[art],))
def delete(self, *args): def delete(self, *args):
"""Delete artwork from kodi database"""
''' Delete artwork from kodi database
'''
self.cursor.execute(QU.delete_art, args) self.cursor.execute(QU.delete_art, args)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,16 @@ from kodi_six.utils import py2_encode
from .. import downloader as server from .. import downloader as server
from ..database import jellyfin_db, queries as QUEM 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 import LazyLogger
from ..helper.utils import find_library from ..helper.utils import find_library
from ..helper.exceptions import PathValidationException from ..helper.exceptions import PathValidationException
@ -42,74 +51,83 @@ class Movies(KodiDb):
@stop @stop
@jellyfin_item @jellyfin_item
def movie(self, item, e_item): def movie(self, item, e_item):
"""If item does not exist, entry will be added.
''' If item does not exist, entry will be added. If item exists, entry will be updated.
If item exists, entry will be updated. """
''' server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] "address"
]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'Movie') obj = self.objects.map(item, "Movie")
update = True update = True
try: try:
obj['MovieId'] = e_item[0] obj["MovieId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
obj['PathId'] = e_item[2] obj["PathId"] = e_item[2]
obj['LibraryId'] = e_item[6] obj["LibraryId"] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError: except TypeError:
update = False 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) library = self.library or find_library(self.server, item)
if not library: if not library:
# This item doesn't belong to a whitelisted library # This item doesn't belong to a whitelisted library
return return
obj['MovieId'] = self.create_entry() obj["MovieId"] = self.create_entry()
obj['LibraryId'] = library['Id'] obj["LibraryId"] = library["Id"]
obj['LibraryName'] = library['Name'] obj["LibraryName"] = library["Name"]
else: else:
if self.get(*values(obj, QU.get_movie_obj)) is None: if self.get(*values(obj, QU.get_movie_obj)) is None:
update = False 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["Path"] = API.get_file_path(obj["Path"])
obj['Genres'] = obj['Genres'] or [] obj["Genres"] = obj["Genres"] or []
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] obj["Studios"] = [
obj['People'] = obj['People'] or [] API.validate_studio(studio) for studio in (obj["Studios"] or [])
obj['Genre'] = " / ".join(obj['Genres']) ]
obj['Writers'] = " / ".join(obj['Writers'] or []) obj["People"] = obj["People"] or []
obj['Directors'] = " / ".join(obj['Directors'] or []) obj["Genre"] = " / ".join(obj["Genres"])
obj['Plot'] = API.get_overview(obj['Plot']) obj["Writers"] = " / ".join(obj["Writers"] or [])
obj['Mpaa'] = API.get_mpaa(obj['Mpaa']) obj["Directors"] = " / ".join(obj["Directors"] or [])
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj["Plot"] = API.get_overview(obj["Plot"])
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) obj["Mpaa"] = API.get_mpaa(obj["Mpaa"])
obj['People'] = API.get_people_artwork(obj['People']) obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ") obj["People"] = API.get_people_artwork(obj["People"])
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) obj["DatePlayed"] = (
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) None
obj['Audio'] = API.audio_streams(obj['Audio'] or []) if not obj["DatePlayed"]
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
if obj['Premiere'] is not None: )
obj['Premiere'] = str(obj['Premiere']).split('T')[0] 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.get_path_filename(obj)
self.trailer(obj) self.trailer(obj)
if obj['Countries']: if obj["Countries"]:
self.add_countries(*values(obj, QU.update_country_obj)) self.add_countries(*values(obj, QU.update_country_obj))
tags = list(obj['Tags'] or []) tags = list(obj["Tags"] or [])
tags.append(obj['LibraryName']) tags.append(obj["LibraryName"])
if obj['Favorite']: if obj["Favorite"]:
tags.append('Favorite movies') tags.append("Favorite movies")
obj['Tags'] = tags obj["Tags"] = tags
if update: if update:
self.movie_update(obj) self.movie_update(obj)
@ -124,239 +142,289 @@ class Movies(KodiDb):
self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.add_people(*values(obj, QU.add_people_movie_obj)) self.add_people(*values(obj, QU.add_people_movie_obj))
self.add_streams(*values(obj, QU.add_streams_obj)) self.add_streams(*values(obj, QU.add_streams_obj))
self.artwork.add(obj['Artwork'], obj['MovieId'], "movie") self.artwork.add(obj["Artwork"], obj["MovieId"], "movie")
self.item_ids.append(obj['Id']) self.item_ids.append(obj["Id"])
return not update return not update
def movie_add(self, obj): def movie_add(self, obj):
"""Add object to kodi."""
''' Add object to kodi. obj["RatingId"] = self.create_entry_rating()
'''
obj['RatingId'] = self.create_entry_rating()
self.add_ratings(*values(obj, QU.add_rating_movie_obj)) 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)) self.add_unique_id(*values(obj, QU.add_unique_id_movie_obj))
obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj)) obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj))
obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj)) obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj))
self.add(*values(obj, QU.add_movie_obj)) self.add(*values(obj, QU.add_movie_obj))
self.add_videoversion(*values(obj, QU.add_video_version_obj)) self.add_videoversion(*values(obj, QU.add_video_version_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_movie_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): def movie_update(self, obj):
"""Update object to kodi."""
''' Update object to kodi. obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
'''
obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
self.update_ratings(*values(obj, QU.update_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_unique_id(*values(obj, QU.update_unique_id_movie_obj))
self.update(*values(obj, QU.update_movie_obj)) self.update(*values(obj, QU.update_movie_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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): def trailer(self, obj):
try: try:
if obj['LocalTrailer']: if obj["LocalTrailer"]:
trailer = self.server.jellyfin.get_local_trailers(obj['Id']) trailer = self.server.jellyfin.get_local_trailers(obj["Id"])
obj['Trailer'] = "plugin://plugin.video.jellyfin/trailer?id=%s&mode=play" % trailer[0]['Id'] obj["Trailer"] = (
"plugin://plugin.video.jellyfin/trailer?id=%s&mode=play"
% trailer[0]["Id"]
)
elif obj['Trailer']: elif obj["Trailer"]:
obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1] obj["Trailer"] = (
"plugin://plugin.video.youtube/play/?video_id=%s"
% obj["Trailer"].rsplit("=", 1)[1]
)
except Exception as error: except Exception as error:
LOG.exception("Failed to get trailer: %s", error) LOG.exception("Failed to get trailer: %s", error)
obj['Trailer'] = None obj["Trailer"] = None
def get_path_filename(self, obj): def get_path_filename(self, obj):
"""Get the path and filename and build it into protocol://path"""
''' Get the path and filename and build it into protocol://path obj["Filename"] = (
''' obj["Path"].rsplit("\\", 1)[1]
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] if "\\" in obj["Path"]
else obj["Path"].rsplit("/", 1)[1]
)
if self.direct_path: if self.direct_path:
if not validate(obj['Path']): if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.") 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''' """check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO"""
if validate_dvd_dir(obj['Path'] + obj['Filename']): if validate_dvd_dir(obj["Path"] + obj["Filename"]):
obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/' obj["Path"] = obj["Path"] + obj["Filename"] + "/VIDEO_TS/"
obj['Filename'] = 'VIDEO_TS.IFO' obj["Filename"] = "VIDEO_TS.IFO"
LOG.debug("DVD directory %s", obj['Path']) LOG.debug("DVD directory %s", obj["Path"])
'''check bluray directories and point it to ./BDMV/index.bdmv''' """check bluray directories and point it to ./BDMV/index.bdmv"""
if validate_bluray_dir(obj['Path'] + obj['Filename']): if validate_bluray_dir(obj["Path"] + obj["Filename"]):
obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/' obj["Path"] = obj["Path"] + obj["Filename"] + "/BDMV/"
obj['Filename'] = 'index.bdmv' obj["Filename"] = "index.bdmv"
LOG.debug("Bluray directory %s", obj['Path']) LOG.debug("Bluray directory %s", obj["Path"])
else: else:
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
params = { params = {
'filename': py2_encode(obj['Filename'], 'utf-8'), "filename": py2_encode(obj["Filename"], "utf-8"),
'id': obj['Id'], "id": obj["Id"],
'dbid': obj['MovieId'], "dbid": obj["MovieId"],
'mode': "play" "mode": "play",
} }
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params)) obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
@stop @stop
@jellyfin_item @jellyfin_item
def boxset(self, item, e_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. Process movies inside boxset.
If item exists, entry will be updated. Process removals from boxset.
"""
Process movies inside boxset. server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
Process removals from boxset. "address"
''' ]
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
API = api.API(item, server_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: try:
obj['SetId'] = e_item[0] obj["SetId"] = e_item[0]
self.update_boxset(*values(obj, QU.update_set_obj)) self.update_boxset(*values(obj, QU.update_set_obj))
except TypeError: except TypeError:
LOG.debug("SetId %s not found", obj['Id']) LOG.debug("SetId %s not found", obj["Id"])
obj['SetId'] = self.add_boxset(*values(obj, QU.add_set_obj)) obj["SetId"] = self.add_boxset(*values(obj, QU.add_set_obj))
self.boxset_current(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 = dict(obj)
temp_obj['Movie'] = movie temp_obj["Movie"] = movie
temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']] temp_obj["MovieId"] = obj["Current"][temp_obj["Movie"]]
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) 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(
LOG.debug("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId']) *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)) 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): 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: 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) movies = dict(current)
except ValueError: except ValueError:
movies = {} movies = {}
obj['Current'] = movies obj["Current"] = movies
for all_movies in server.get_movies_by_boxset(obj['Id']): for all_movies in server.get_movies_by_boxset(obj["Id"]):
for movie in all_movies['Items']: for movie in all_movies["Items"]:
temp_obj = dict(obj) temp_obj = dict(obj)
temp_obj['Title'] = movie['Name'] temp_obj["Title"] = movie["Name"]
temp_obj['Id'] = movie['Id'] temp_obj["Id"] = movie["Id"]
try: 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: 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 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.set_boxset(*values(temp_obj, QU.update_movie_set_obj))
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_movie_obj)) self.jellyfin_db.update_parent_id(
LOG.debug("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['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: else:
obj['Current'].pop(temp_obj['Id']) obj["Current"].pop(temp_obj["Id"])
def boxsets_reset(self): def boxsets_reset(self):
"""Special function to remove all existing boxsets."""
''' Special function to remove all existing boxsets. boxsets = self.jellyfin_db.get_items_by_media("set")
'''
boxsets = self.jellyfin_db.get_items_by_media('set')
for boxset in boxsets: for boxset in boxsets:
self.remove(boxset[0]) self.remove(boxset[0])
@stop @stop
@jellyfin_item @jellyfin_item
def userdata(self, item, e_item): def userdata(self, item, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar
Poster with progress bar """
''' server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] "address"
]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'MovieUserData') obj = self.objects.map(item, "MovieUserData")
try: try:
obj['MovieId'] = e_item[0] obj["MovieId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
except TypeError: except TypeError:
return return
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
if obj['DatePlayed']: if obj["DatePlayed"]:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") 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)) self.get_tag(*values(obj, QU.get_tag_movie_obj))
else: else:
self.remove_tag(*values(obj, QU.delete_tag_movie_obj)) 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.add_playstate(*values(obj, QU.add_bookmark_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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 @stop
@jellyfin_item @jellyfin_item
def remove(self, item_id, e_item): def remove(self, item_id, e_item):
"""Remove movieid, fileid, jellyfin reference.
''' Remove movieid, fileid, jellyfin reference. Remove artwork, boxset
Remove artwork, boxset """
''' obj = {"Id": item_id}
obj = {'Id': item_id}
try: try:
obj['KodiId'] = e_item[0] obj["KodiId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
obj['Media'] = e_item[4] obj["Media"] = e_item[4]
except TypeError: except TypeError:
return 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)) 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 = dict(obj)
temp_obj['MovieId'] = movie[1] temp_obj["MovieId"] = movie[1]
temp_obj['Movie'] = movie[0] temp_obj["Movie"] = movie[0]
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj)) 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.delete_boxset(*values(obj, QU.delete_set_obj))
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_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 @stop
@jellyfin_item @jellyfin_item
def artist(self, item, e_item): def artist(self, item, e_item):
"""If item does not exist, entry will be added.
''' If item does not exist, entry will be added. If item exists, entry will be updated.
If item exists, entry will be updated. """
''' server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] "address"
]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'Artist') obj = self.objects.map(item, "Artist")
update = True update = True
try: try:
obj['ArtistId'] = e_item[0] obj["ArtistId"] = e_item[0]
obj['LibraryId'] = e_item[6] obj["LibraryId"] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError: except TypeError:
update = False update = False
@ -60,72 +61,81 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library # This item doesn't belong to a whitelisted library
return return
obj['ArtistId'] = None obj["ArtistId"] = None
obj['LibraryId'] = library['Id'] obj["LibraryId"] = library["Id"]
obj['LibraryName'] = library['Name'] obj["LibraryName"] = library["Name"]
LOG.debug("ArtistId %s not found", obj['Id']) LOG.debug("ArtistId %s not found", obj["Id"])
else: else:
if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None: if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None:
update = False 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["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
obj['ArtistType'] = "MusicArtist" obj["ArtistType"] = "MusicArtist"
obj['Genre'] = " / ".join(obj['Genres'] or []) obj["Genre"] = " / ".join(obj["Genres"] or [])
obj['Bio'] = API.get_overview(obj['Bio']) obj["Bio"] = API.get_overview(obj["Bio"])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj["Artwork"] = API.get_all_artwork(
obj['Thumb'] = obj['Artwork']['Primary'] self.objects.map(item, "ArtworkMusic"), True
obj['Backdrops'] = obj['Artwork']['Backdrop'] or "" )
obj["Thumb"] = obj["Artwork"]["Primary"]
obj["Backdrops"] = obj["Artwork"]["Backdrop"] or ""
if obj['Thumb']: if obj["Thumb"]:
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb'] obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
if obj['Backdrops']: if obj["Backdrops"]:
obj['Backdrops'] = "<fanart>%s</fanart>" % obj['Backdrops'][0] obj["Backdrops"] = "<fanart>%s</fanart>" % obj["Backdrops"][0]
if update: if update:
self.artist_update(obj) self.artist_update(obj)
else: else:
self.artist_add(obj) self.artist_add(obj)
self.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['ArtistId']) self.update(
self.artwork.add(obj['Artwork'], obj['ArtistId'], "artist") obj["Genre"],
self.item_ids.append(obj['Id']) 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): 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.
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))
'''
obj['ArtistId'] = self.get(*values(obj, QU.get_artist_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_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): def artist_update(self, obj):
"""Update object to kodi."""
''' Update object to kodi.
'''
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) 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 @stop
@jellyfin_item @jellyfin_item
def album(self, item, e_item): def album(self, item, e_item):
"""Update object to kodi."""
''' Update object to kodi. server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
''' "address"
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] ]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'Album') obj = self.objects.map(item, "Album")
update = True update = True
try: try:
obj['AlbumId'] = e_item[0] obj["AlbumId"] = e_item[0]
obj['LibraryId'] = e_item[6] obj["LibraryId"] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError: except TypeError:
update = False update = False
@ -134,31 +144,35 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library # This item doesn't belong to a whitelisted library
return return
obj['AlbumId'] = None obj["AlbumId"] = None
obj['LibraryId'] = library['Id'] obj["LibraryId"] = library["Id"]
obj['LibraryName'] = library['Name'] obj["LibraryName"] = library["Name"]
LOG.debug("AlbumId %s not found", obj['Id']) LOG.debug("AlbumId %s not found", obj["Id"])
else: else:
if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None: if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None:
update = False 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["Rating"] = 0
obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
obj['Genres'] = obj['Genres'] or [] obj["Genres"] = obj["Genres"] or []
obj['Genre'] = " / ".join(obj['Genres']) obj["Genre"] = " / ".join(obj["Genres"])
obj['Bio'] = API.get_overview(obj['Bio']) obj["Bio"] = API.get_overview(obj["Bio"])
obj['Artists'] = " / ".join(obj['Artists'] or []) obj["Artists"] = " / ".join(obj["Artists"] or [])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj["Artwork"] = API.get_all_artwork(
obj['Thumb'] = obj['Artwork']['Primary'] self.objects.map(item, "ArtworkMusic"), True
obj['DateAdded'] = item.get('DateCreated') )
obj["Thumb"] = obj["Artwork"]["Primary"]
obj["DateAdded"] = item.get("DateCreated")
if obj['DateAdded']: if obj["DateAdded"]:
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
if obj['Thumb']: if obj["Thumb"]:
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb'] obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
if update: if update:
self.album_update(obj) self.album_update(obj)
@ -169,89 +183,90 @@ class Music(KodiDb):
self.artist_discography(obj) self.artist_discography(obj)
self.update_album(*values(obj, QU.update_album_obj)) self.update_album(*values(obj, QU.update_album_obj))
self.add_genres(*values(obj, QU.add_genres_obj)) self.add_genres(*values(obj, QU.add_genres_obj))
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
self.item_ids.append(obj['Id']) self.item_ids.append(obj["Id"])
def album_add(self, obj): def album_add(self, obj):
"""Add object to kodi."""
''' Add object to kodi.
'''
if self.version_id >= 82: if self.version_id >= 82:
obj_values = values(obj, QU.get_album_obj82) obj_values = values(obj, QU.get_album_obj82)
else: else:
obj_values = values(obj, QU.get_album_obj) 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)) 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): def album_update(self, obj):
"""Update object to kodi."""
''' Update object to kodi.
'''
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj)) 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): def artist_discography(self, obj):
"""Update the artist's discography."""
''' Update the artist's discography. for artist in obj["ArtistItems"] or []:
'''
for artist in (obj['ArtistItems'] or []):
temp_obj = dict(obj) temp_obj = dict(obj)
temp_obj['Id'] = artist['Id'] temp_obj["Id"] = artist["Id"]
temp_obj['AlbumId'] = obj['Id'] temp_obj["AlbumId"] = obj["Id"]
try: 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: except TypeError:
continue continue
self.add_discography(*values(temp_obj, QU.update_discography_obj)) 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): def artist_link(self, obj):
"""Assign main artists to album.
''' Assign main artists to album. Artist does not exist in jellyfin database, create the reference.
Artist does not exist in jellyfin database, create the reference. """
''' for artist in obj["AlbumArtists"] or []:
for artist in (obj['AlbumArtists'] or []):
temp_obj = dict(obj) temp_obj = dict(obj)
temp_obj['Name'] = artist['Name'] temp_obj["Name"] = artist["Name"]
temp_obj['Id'] = artist['Id'] temp_obj["Id"] = artist["Id"]
try: 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: except TypeError:
try: try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) 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] temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
continue continue
self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj)) self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj))
self.link(*values(temp_obj, QU.update_link_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 @stop
@jellyfin_item @jellyfin_item
def song(self, item, e_item): def song(self, item, e_item):
"""Update object to kodi."""
''' Update object to kodi. server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
''' "address"
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] ]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'Song') obj = self.objects.map(item, "Song")
update = True update = True
try: try:
obj['SongId'] = e_item[0] obj["SongId"] = e_item[0]
obj['PathId'] = e_item[2] obj["PathId"] = e_item[2]
obj['AlbumId'] = e_item[3] obj["AlbumId"] = e_item[3]
obj['LibraryId'] = e_item[6] obj["LibraryId"] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError: except TypeError:
update = False update = False
@ -260,38 +275,42 @@ class Music(KodiDb):
# This item doesn't belong to a whitelisted library # This item doesn't belong to a whitelisted library
return return
obj['SongId'] = self.create_entry_song() obj["SongId"] = self.create_entry_song()
obj['LibraryId'] = library['Id'] obj["LibraryId"] = library["Id"]
obj['LibraryName'] = library['Name'] obj["LibraryName"] = library["Name"]
LOG.debug("SongId %s not found", obj['Id']) LOG.debug("SongId %s not found", obj["Id"])
else: else:
if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None: if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None:
update = False 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) self.get_song_path_filename(obj, API)
obj['Rating'] = 0 obj["Rating"] = 0
obj['Genres'] = obj['Genres'] or [] obj["Genres"] = obj["Genres"] or []
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0 obj["Runtime"] = (obj["Runtime"] or 0) / 10000000.0
obj['Genre'] = " / ".join(obj['Genres']) obj["Genre"] = " / ".join(obj["Genres"])
obj['Artists'] = " / ".join(obj['Artists'] or []) obj["Artists"] = " / ".join(obj["Artists"] or [])
obj['AlbumArtists'] = obj['AlbumArtists'] or [] obj["AlbumArtists"] = obj["AlbumArtists"] or []
obj['Index'] = obj['Index'] or 0 obj["Index"] = obj["Index"] or 0
obj['Disc'] = obj['Disc'] or 1 obj["Disc"] = obj["Disc"] or 1
obj['EmbedCover'] = False obj["EmbedCover"] = False
obj['Comment'] = API.get_overview(obj['Comment']) obj["Comment"] = API.get_overview(obj["Comment"])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True) obj["Artwork"] = API.get_all_artwork(
self.objects.map(item, "ArtworkMusic"), True
)
if obj['DateAdded']: if obj["DateAdded"]:
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ") obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
if obj['DatePlayed']: if obj["DatePlayed"]:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") 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: if update:
self.song_update(obj) self.song_update(obj)
@ -303,227 +322,275 @@ class Music(KodiDb):
self.song_artist_link(obj) self.song_artist_link(obj)
self.song_artist_discography(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.get_album_artist(*values(obj, QU.get_album_artist_obj))
self.add_genres(*values(obj, QU.update_genre_song_obj)) self.add_genres(*values(obj, QU.update_genre_song_obj))
self.artwork.add(obj['Artwork'], obj['SongId'], "song") self.artwork.add(obj["Artwork"], obj["SongId"], "song")
self.item_ids.append(obj['Id']) self.item_ids.append(obj["Id"])
if obj['SongAlbumId'] is None: if obj["SongAlbumId"] is None:
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album") self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
return not update return not update
def song_add(self, obj): 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
Verify if there's an album associated. """
If no album found, create a single's album obj["PathId"] = self.add_path(obj["Path"])
'''
obj['PathId'] = self.add_path(obj['Path'])
try: 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: except TypeError:
try: try:
if obj['SongAlbumId'] is None: if obj["SongAlbumId"] is None:
raise TypeError("No album id found associated?") raise TypeError("No album id found associated?")
self.album(self.server.jellyfin.get_item(obj['SongAlbumId'])) 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] obj["AlbumId"] = self.jellyfin_db.get_item_by_id(
*values(obj, QUEM.get_item_song_obj)
)[0]
except TypeError: except TypeError:
self.single(obj) self.single(obj)
self.add_song(*values(obj, QU.add_song_obj)) self.add_song(*values(obj, QU.add_song_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_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): def song_update(self, obj):
"""Update object to kodi."""
''' Update object to kodi.
'''
self.update_path(*values(obj, QU.update_path_obj)) self.update_path(*values(obj, QU.update_path_obj))
self.update_song(*values(obj, QU.update_song_obj)) self.update_song(*values(obj, QU.update_song_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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): def get_song_path_filename(self, obj, api):
"""Get the path and filename and build it into protocol://path"""
''' Get the path and filename and build it into protocol://path obj["Path"] = api.get_file_path(obj["Path"])
''' obj["Filename"] = (
obj['Path'] = api.get_file_path(obj['Path']) obj["Path"].rsplit("\\", 1)[1]
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] if "\\" in obj["Path"]
else obj["Path"].rsplit("/", 1)[1]
)
if self.direct_path: if self.direct_path:
if not validate(obj['Path']): if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.") raise PathValidationException("Failed to validate path. User stopped.")
obj['Path'] = obj['Path'].replace(obj['Filename'], "") obj["Path"] = obj["Path"].replace(obj["Filename"], "")
else: else:
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] server_address = self.server.auth.get_server_info(
obj['Path'] = "%s/Audio/%s/" % (server_address, obj['Id']) self.server.auth.server_id
obj['Filename'] = "stream.%s?static=true" % obj['Container'] )["address"]
obj["Path"] = "%s/Audio/%s/" % (server_address, obj["Id"])
obj["Filename"] = "stream.%s?static=true" % obj["Container"]
def song_artist_discography(self, obj): def song_artist_discography(self, obj):
"""Update the artist's discography."""
''' Update the artist's discography.
'''
artists = [] artists = []
for artist in (obj['AlbumArtists'] or []): for artist in obj["AlbumArtists"] or []:
temp_obj = dict(obj) temp_obj = dict(obj)
temp_obj['Name'] = artist['Name'] temp_obj["Name"] = artist["Name"]
temp_obj['Id'] = artist['Id'] temp_obj["Id"] = artist["Id"]
artists.append(temp_obj['Name']) artists.append(temp_obj["Name"])
try: 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: except TypeError:
try: try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) 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] temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
continue continue
self.link(*values(temp_obj, QU.update_link_obj)) 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["Title"] = obj["Album"]
temp_obj['Year'] = 0 temp_obj["Year"] = 0
self.add_discography(*values(temp_obj, QU.update_discography_obj)) self.add_discography(*values(temp_obj, QU.update_discography_obj))
obj['AlbumArtists'] = artists obj["AlbumArtists"] = artists
def song_artist_link(self, obj): def song_artist_link(self, obj):
"""Assign main artists to song.
''' Assign main artists to song. Artist does not exist in jellyfin database, create the reference.
Artist does not exist in jellyfin database, create the reference. """
''' for index, artist in enumerate(obj["ArtistItems"] or []):
for index, artist in enumerate(obj['ArtistItems'] or []):
temp_obj = dict(obj) temp_obj = dict(obj)
temp_obj['Name'] = artist['Name'] temp_obj["Name"] = artist["Name"]
temp_obj['Id'] = artist['Id'] temp_obj["Id"] = artist["Id"]
temp_obj['Index'] = index temp_obj["Index"] = index
try: 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: except TypeError:
try: try:
self.artist(self.server.jellyfin.get_item(temp_obj['Id'])) 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] temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
*values(temp_obj, QUEM.get_item_obj)
)[0]
except Exception as error: except Exception as error:
LOG.exception(error) LOG.exception(error)
continue continue
self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj)) 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): 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)) self.add_single(*values(obj, QU.add_single_obj))
@stop @stop
@jellyfin_item @jellyfin_item
def userdata(self, item, e_item): def userdata(self, item, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
"""
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks obj = self.objects.map(item, "SongUserData")
Poster with progress bar
'''
obj = self.objects.map(item, 'SongUserData')
try: try:
obj['KodiId'] = e_item[0] obj["KodiId"] = e_item[0]
obj['Media'] = e_item[4] obj["Media"] = e_item[4]
except TypeError: except TypeError:
return return
obj['Rating'] = 0 obj["Rating"] = 0
if obj['Media'] == 'song': if obj["Media"] == "song":
if obj['DatePlayed']: if obj["DatePlayed"]:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") obj["DatePlayed"] = (
Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
)
self.rate_song(*values(obj, QU.update_song_rating_obj)) self.rate_song(*values(obj, QU.update_song_rating_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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 @stop
@jellyfin_item @jellyfin_item
def remove(self, item_id, e_item): def remove(self, item_id, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
Poster with progress bar
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks This should address single song scenario, where server doesn't actually
Poster with progress bar create an album for the song.
"""
This should address single song scenario, where server doesn't actually obj = {"Id": item_id}
create an album for the song.
'''
obj = {'Id': item_id}
try: try:
obj['KodiId'] = e_item[0] obj["KodiId"] = e_item[0]
obj['Media'] = e_item[4] obj["Media"] = e_item[4]
except TypeError: except TypeError:
return return
if obj['Media'] == 'song': if obj["Media"] == "song":
self.remove_song(obj['KodiId'], obj['Id']) self.remove_song(obj["KodiId"], obj["Id"])
self.jellyfin_db.remove_wild_item(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)): for item in self.jellyfin_db.get_item_by_wild_id(
if item[1] == 'album': *values(obj, QUEM.get_item_by_wild_obj)
):
if item[1] == "album":
temp_obj = dict(obj) 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)): if not self.jellyfin_db.get_item_by_parent_id(
self.remove_album(temp_obj['ParentId'], obj['Id']) *values(temp_obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_album(temp_obj["ParentId"], obj["Id"])
elif obj['Media'] == 'album': elif obj["Media"] == "album":
obj['ParentId'] = obj['KodiId'] obj["ParentId"] = obj["KodiId"]
for song in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_song_obj)): for song in self.jellyfin_db.get_item_by_parent_id(
self.remove_song(song[1], obj['Id']) *values(obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_song(song[1], obj["Id"])
else: 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': elif obj["Media"] == "artist":
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 = 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)): for song in self.jellyfin_db.get_item_by_parent_id(
self.remove_song(song[1], obj['Id']) *values(temp_obj, QUEM.get_item_by_parent_song_obj)
):
self.remove_song(song[1], obj["Id"])
else: 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(
self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_artist_obj)) *values(temp_obj, QUEM.delete_item_by_parent_song_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_artist_obj)
)
self.remove_album(temp_obj["ParentId"], obj["Id"])
else: 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)) self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
@ -547,29 +614,31 @@ class Music(KodiDb):
@jellyfin_item @jellyfin_item
def get_child(self, item_id, e_item): def get_child(self, item_id, e_item):
"""Get all child elements from tv show jellyfin id."""
''' Get all child elements from tv show jellyfin id. obj = {"Id": item_id}
'''
obj = {'Id': item_id}
child = [] child = []
try: try:
obj['KodiId'] = e_item[0] obj["KodiId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
obj['ParentId'] = e_item[3] obj["ParentId"] = e_item[3]
obj['Media'] = e_item[4] obj["Media"] = e_item[4]
except TypeError: except TypeError:
return child 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 = dict(obj)
temp_obj['ParentId'] = album[1] temp_obj["ParentId"] = album[1]
child.append((album[0],)) 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],)) child.append((song[0],))
return child return child

View File

@ -43,24 +43,25 @@ class MusicVideos(KodiDb):
@stop @stop
@jellyfin_item @jellyfin_item
def musicvideo(self, item, e_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 we don't get the track number from Jellyfin, see if we can infer it
If item exists, entry will be updated. from the sortname attribute.
"""
If we don't get the track number from Jellyfin, see if we can infer it server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
from the sortname attribute. "address"
''' ]
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'MusicVideo') obj = self.objects.map(item, "MusicVideo")
update = True update = True
try: try:
obj['MvideoId'] = e_item[0] obj["MvideoId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
obj['PathId'] = e_item[2] obj["PathId"] = e_item[2]
obj['LibraryId'] = e_item[6] obj["LibraryId"] = e_item[6]
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId']) obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
except TypeError: except TypeError:
update = False update = False
@ -69,67 +70,80 @@ class MusicVideos(KodiDb):
# This item doesn't belong to a whitelisted library # This item doesn't belong to a whitelisted library
return return
LOG.debug("MvideoId for %s not found", obj['Id']) LOG.debug("MvideoId for %s not found", obj["Id"])
obj['MvideoId'] = self.create_entry() obj["MvideoId"] = self.create_entry()
obj['LibraryId'] = library['Id'] obj["LibraryId"] = library["Id"]
obj['LibraryName'] = library['Name'] obj["LibraryName"] = library["Name"]
else: else:
if self.get(*values(obj, QU.get_musicvideo_obj)) is None: if self.get(*values(obj, QU.get_musicvideo_obj)) is None:
update = False 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: if (obj.get("ProductionYear") or 0) > 9999:
obj['ProductionYear'] = int(str(obj['ProductionYear'])[:4]) obj["ProductionYear"] = int(str(obj["ProductionYear"])[:4])
if (obj.get('Year') or 0) > 9999: if (obj.get("Year") or 0) > 9999:
obj['Year'] = int(str(obj['Year'])[:4]) obj["Year"] = int(str(obj["Year"])[:4])
obj['Path'] = API.get_file_path(obj['Path']) obj["Path"] = API.get_file_path(obj["Path"])
obj['Genres'] = obj['Genres'] or [] obj["Genres"] = obj["Genres"] or []
obj['ArtistItems'] = obj['ArtistItems'] or [] obj["ArtistItems"] = obj["ArtistItems"] or []
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])] obj["Studios"] = [
obj['Plot'] = API.get_overview(obj['Plot']) API.validate_studio(studio) for studio in (obj["Studios"] or [])
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["Plot"] = API.get_overview(obj["Plot"])
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj["DatePlayed"] = (
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) None
obj['Premiere'] = Local(obj['Premiere']) if obj['Premiere'] else datetime.date(obj['Year'] or 1970, 1, 1) if not obj["DatePlayed"]
obj['Genre'] = " / ".join(obj['Genres']) else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
obj['Studio'] = " / ".join(obj['Studios']) )
obj['Artists'] = " / ".join(obj['Artists'] or []) obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
obj['Directors'] = " / ".join(obj['Directors'] or []) obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container']) obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj['Audio'] = API.audio_streams(obj['Audio'] or []) obj["Premiere"] = (
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles']) Local(obj["Premiere"])
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork')) 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) self.get_path_filename(obj)
if obj['Premiere']: if obj["Premiere"]:
obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ") obj["Premiere"] = str(obj["Premiere"]).split(".")[0].replace("T", " ")
for artist in obj['ArtistItems']: for artist in obj["ArtistItems"]:
artist['Type'] = "Artist" artist["Type"] = "Artist"
obj['People'] = obj['People'] or [] + obj['ArtistItems'] obj["People"] = obj["People"] or [] + obj["ArtistItems"]
obj['People'] = API.get_people_artwork(obj['People']) obj["People"] = API.get_people_artwork(obj["People"])
if obj['Index'] is None and obj['SortTitle'] is not None: if obj["Index"] is None and obj["SortTitle"] is not None:
search = re.search(r'^\d+\s?', obj['SortTitle']) search = re.search(r"^\d+\s?", obj["SortTitle"])
if search: if search:
obj['Index'] = search.group() obj["Index"] = search.group()
tags = [] tags = []
tags.extend(obj['Tags'] or []) tags.extend(obj["Tags"] or [])
tags.append(obj['LibraryName']) tags.append(obj["LibraryName"])
if obj['Favorite']: if obj["Favorite"]:
tags.append('Favorite musicvideos') tags.append("Favorite musicvideos")
obj['Tags'] = tags obj["Tags"] = tags
if update: if update:
self.musicvideo_update(obj) self.musicvideo_update(obj)
@ -144,106 +158,129 @@ class MusicVideos(KodiDb):
self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.add_people(*values(obj, QU.add_people_mvideo_obj)) self.add_people(*values(obj, QU.add_people_mvideo_obj))
self.add_streams(*values(obj, QU.add_streams_obj)) self.add_streams(*values(obj, QU.add_streams_obj))
self.artwork.add(obj['Artwork'], obj['MvideoId'], "musicvideo") self.artwork.add(obj["Artwork"], obj["MvideoId"], "musicvideo")
self.item_ids.append(obj['Id']) self.item_ids.append(obj["Id"])
return not update return not update
def musicvideo_add(self, obj): def musicvideo_add(self, obj):
"""Add object to kodi."""
''' 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))
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.add(*values(obj, QU.add_musicvideo_obj))
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_mvideo_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): def musicvideo_update(self, obj):
"""Update object to kodi."""
''' Update object to kodi.
'''
self.update(*values(obj, QU.update_musicvideo_obj)) self.update(*values(obj, QU.update_musicvideo_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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): def get_path_filename(self, obj):
"""Get the path and filename and build it into protocol://path"""
''' Get the path and filename and build it into protocol://path obj["Filename"] = (
''' obj["Path"].rsplit("\\", 1)[1]
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1] if "\\" in obj["Path"]
else obj["Path"].rsplit("/", 1)[1]
)
if self.direct_path: if self.direct_path:
if not validate(obj['Path']): if not validate(obj["Path"]):
raise PathValidationException("Failed to validate path. User stopped.") raise PathValidationException("Failed to validate path. User stopped.")
obj['Path'] = obj['Path'].replace(obj['Filename'], "") obj["Path"] = obj["Path"].replace(obj["Filename"], "")
else: else:
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId'] obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
params = { params = {
'filename': py2_encode(obj['Filename'], 'utf-8'), "filename": py2_encode(obj["Filename"], "utf-8"),
'id': obj['Id'], "id": obj["Id"],
'dbid': obj['MvideoId'], "dbid": obj["MvideoId"],
'mode': "play" "mode": "play",
} }
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params)) obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
@stop @stop
@jellyfin_item @jellyfin_item
def userdata(self, item, e_item): def userdata(self, item, e_item):
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks Poster with progress bar
Poster with progress bar """
''' server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] "address"
]
API = api.API(item, server_address) API = api.API(item, server_address)
obj = self.objects.map(item, 'MusicVideoUserData') obj = self.objects.map(item, "MusicVideoUserData")
try: try:
obj['MvideoId'] = e_item[0] obj["MvideoId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
except TypeError: except TypeError:
return return
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0) obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6) obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount']) obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
if obj['DatePlayed']: if obj["DatePlayed"]:
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ") 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)) self.get_tag(*values(obj, QU.get_tag_mvideo_obj))
else: else:
self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj)) self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj))
self.add_playstate(*values(obj, QU.add_bookmark_obj)) self.add_playstate(*values(obj, QU.add_bookmark_obj))
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_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 @stop
@jellyfin_item @jellyfin_item
def remove(self, item_id, e_item): def remove(self, item_id, e_item):
"""Remove mvideoid, fileid, pathid, jellyfin reference."""
''' Remove mvideoid, fileid, pathid, jellyfin reference. obj = {"Id": item_id}
'''
obj = {'Id': item_id}
try: try:
obj['MvideoId'] = e_item[0] obj["MvideoId"] = e_item[0]
obj['FileId'] = e_item[1] obj["FileId"] = e_item[1]
obj['PathId'] = e_item[2] obj["PathId"] = e_item[2]
except TypeError: except TypeError:
return return
self.artwork.delete(obj['MvideoId'], "musicvideo") self.artwork.delete(obj["MvideoId"], "musicvideo")
self.delete(*values(obj, QU.delete_musicvideo_obj)) self.delete(*values(obj, QU.delete_musicvideo_obj))
if self.direct_path: if self.direct_path:
self.remove_path(*values(obj, QU.delete_path_obj)) self.remove_path(*values(obj, QU.delete_path_obj))
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_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 = {} _shared_state = {}
def __init__(self): def __init__(self):
"""Hold all persistent data here."""
''' Hold all persistent data here.
'''
self.__dict__ = self._shared_state self.__dict__ = self._shared_state
def mapping(self): def mapping(self):
"""Load objects mapping."""
''' Load objects mapping.
'''
file_dir = os.path.dirname(ensure_text(__file__, get_filesystem_encoding())) 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) self.objects = json.load(infile)
def map(self, item, mapping_name): 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. Item is the Jellyfin item json object structure
This of the query almost as a url.
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
",": each element will be used as a fallback until a value is found. "$": lead the key name with $. Only one key value can be requested per element.
"?": split filters and key name from the query part, i.e. MediaSources/0?$Name ":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name
"$": lead the key name with $. Only one key value can be requested per element. MediaStreams is a list.
":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name "/": indicates where to go directly
MediaStreams is a list. """
"/": indicates where to go directly
'''
self.mapped_item = {} self.mapped_item = {}
if not mapping_name: if not mapping_name:
@ -62,7 +57,7 @@ class Objects(object):
for key, value in iteritems(mapping): for key, value in iteritems(mapping):
self.mapped_item[key] = None self.mapped_item[key] = None
params = value.split(',') params = value.split(",")
for param in params: for param in params:
@ -71,19 +66,19 @@ class Objects(object):
obj_key = "" obj_key = ""
obj_filters = {} obj_filters = {}
if '?' in obj_param: if "?" in obj_param:
if '$' in obj_param: if "$" in obj_param:
obj_param, obj_key = obj_param.rsplit('$', 1) obj_param, obj_key = obj_param.rsplit("$", 1)
obj_param, filters = obj_param.rsplit('?', 1) obj_param, filters = obj_param.rsplit("?", 1)
if filters: if filters:
for filter in filters.split('&'): for filter in filters.split("&"):
filter_key, filter_value = filter.split('=') filter_key, filter_value = filter.split("=")
obj_filters[filter_key] = filter_value obj_filters[filter_key] = filter_value
if ':' in obj_param: if ":" in obj_param:
result = [] result = []
for d in self.__recursiveloop__(obj, obj_param): for d in self.__recursiveloop__(obj, obj_param):
@ -94,7 +89,7 @@ class Objects(object):
obj = result obj = result
obj_filters = {} obj_filters = {}
elif '/' in obj_param: elif "/" in obj_param:
obj = self.__recursive__(obj, obj_param) obj = self.__recursive__(obj, obj_param)
elif obj is item and obj is not None: elif obj is item and obj is not None:
@ -107,21 +102,31 @@ class Objects(object):
continue continue
if obj_key: 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 self.mapped_item[key] = obj
break 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["ProviderName"] = self.objects.get(
self.mapped_item['Checksum'] = json.dumps(item['UserData']) "%sProviderName" % mapping_name
)
self.mapped_item["Checksum"] = json.dumps(item["UserData"])
return self.mapped_item return self.mapped_item
def __recursiveloop__(self, obj, keys): def __recursiveloop__(self, obj, keys):
first, rest = keys.split(':', 1) first, rest = keys.split(":", 1)
obj = self.__recursive__(obj, first) obj = self.__recursive__(obj, first)
if obj: if obj:
@ -133,7 +138,7 @@ class Objects(object):
def __recursive__(self, obj, keys): def __recursive__(self, obj, keys):
for string in keys.split('/'): for string in keys.split("/"):
if not obj: if not obj:
return return
@ -150,10 +155,10 @@ class Objects(object):
inverse = False inverse = False
if value.startswith('!'): if value.startswith("!"):
inverse = True inverse = True
value = value.split('!', 1)[1] value = value.split("!", 1)[1]
if value.lower() == "null": if value.lower() == "null":
value = None 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(): def get_grouped_set():
"""Get if boxsets should be grouped"""
''' Get if boxsets should be grouped result = JSONRPC("Settings.GetSettingValue").execute(
''' {"setting": "videolibrary.groupmoviesets"}
result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"}) )
return result.get('result', {}).get('value', False) return result.get("result", {}).get("value", False)

View File

@ -44,11 +44,10 @@ class Player(xbmc.Player):
return file in self.played return file in self.played
def onPlayBackStarted(self): def onPlayBackStarted(self):
"""We may need to wait for info to be set in kodi monitor.
''' We may need to wait for info to be set in kodi monitor. Accounts for scenario where Kodi starts playback and exits immediately.
Accounts for scenario where Kodi starts playback and exits immediately. First, ensure previous playback terminated correctly in Jellyfin.
First, ensure previous playback terminated correctly in Jellyfin. """
'''
self.stop_playback() self.stop_playback()
self.up_next = False self.up_next = False
count = 0 count = 0
@ -69,11 +68,11 @@ class Player(xbmc.Player):
if monitor.waitForAbort(1): if monitor.waitForAbort(1):
return return
else: else:
LOG.info('Cancel playback report') LOG.info("Cancel playback report")
return return
items = window('jellyfin_play.json') items = window("jellyfin_play.json")
item = None item = None
while not items: while not items:
@ -81,7 +80,7 @@ class Player(xbmc.Player):
if monitor.waitForAbort(2): if monitor.waitForAbort(2):
return return
items = window('jellyfin_play.json') items = window("jellyfin_play.json")
count += 1 count += 1
if count == 20: if count == 20:
@ -90,51 +89,49 @@ class Player(xbmc.Player):
return return
for item in items: for item in items:
if item['Path'] == current_file: if item["Path"] == current_file:
items.pop(items.index(item)) items.pop(items.index(item))
break break
else: else:
item = items.pop(0) item = items.pop(0)
window('jellyfin_play.json', items) window("jellyfin_play.json", items)
self.set_item(current_file, item) self.set_item(current_file, item)
data = { data = {
'QueueableMediaTypes': "Video,Audio", "QueueableMediaTypes": "Video,Audio",
'CanSeek': True, "CanSeek": True,
'ItemId': item['Id'], "ItemId": item["Id"],
'MediaSourceId': item['MediaSourceId'], "MediaSourceId": item["MediaSourceId"],
'PlayMethod': item['PlayMethod'], "PlayMethod": item["PlayMethod"],
'VolumeLevel': item['Volume'], "VolumeLevel": item["Volume"],
'PositionTicks': int(item['CurrentPosition'] * 10000000), "PositionTicks": int(item["CurrentPosition"] * 10000000),
'IsPaused': item['Paused'], "IsPaused": item["Paused"],
'IsMuted': item['Muted'], "IsMuted": item["Muted"],
'PlaySessionId': item['PlaySessionId'], "PlaySessionId": item["PlaySessionId"],
'AudioStreamIndex': item['AudioStreamIndex'], "AudioStreamIndex": item["AudioStreamIndex"],
'SubtitleStreamIndex': item['SubtitleStreamIndex'] "SubtitleStreamIndex": item["SubtitleStreamIndex"],
} }
item['Server'].jellyfin.session_playing(data) item["Server"].jellyfin.session_playing(data)
window('jellyfin.skip.%s.bool' % item['Id'], True) window("jellyfin.skip.%s.bool" % item["Id"], True)
if monitor.waitForAbort(2): if monitor.waitForAbort(2):
return return
if item['PlayOption'] == 'Addon': if item["PlayOption"] == "Addon":
self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex']) self.set_audio_subs(item["AudioStreamIndex"], item["SubtitleStreamIndex"])
def set_item(self, file, item): def set_item(self, file, item):
"""Set playback information."""
''' Set playback information.
'''
try: try:
item['Runtime'] = int(item['Runtime']) item["Runtime"] = int(item["Runtime"])
except (TypeError, ValueError): except (TypeError, ValueError):
try: try:
item['Runtime'] = int(self.getTotalTime()) item["Runtime"] = int(self.getTotalTime())
LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime']) LOG.info("Runtime is missing, Kodi runtime: %s" % item["Runtime"])
except Exception: except Exception:
item['Runtime'] = 0 item["Runtime"] = 0
LOG.info("Runtime is missing, Using Zero") LOG.info("Runtime is missing, Using Zero")
try: try:
@ -142,22 +139,26 @@ class Player(xbmc.Player):
except Exception: # at this point we should be playing and if not then bail out except Exception: # at this point we should be playing and if not then bail out
return return
result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) result = JSONRPC("Application.GetProperties").execute(
result = result.get('result', {}) {"properties": ["volume", "muted"]}
volume = result.get('volume') )
muted = result.get('muted') result = result.get("result", {})
volume = result.get("volume")
muted = result.get("muted")
item.update({ item.update(
'File': file, {
'CurrentPosition': item.get('CurrentPosition') or int(seektime), "File": file,
'Muted': muted, "CurrentPosition": item.get("CurrentPosition") or int(seektime),
'Volume': volume, "Muted": muted,
'Server': Jellyfin(item['ServerId']).get_client(), "Volume": volume,
'Paused': False "Server": Jellyfin(item["ServerId"]).get_client(),
}) "Paused": False,
}
)
self.played[file] = item 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): def set_audio_subs(self, audio=None, subtitle=None):
if audio: if audio:
@ -165,15 +166,15 @@ class Player(xbmc.Player):
if subtitle: if subtitle:
subtitle = int(subtitle) subtitle = int(subtitle)
''' Only for after playback started """ Only for after playback started
''' """
LOG.info("Setting audio: %s subs: %s", audio, subtitle) LOG.info("Setting audio: %s subs: %s", audio, subtitle)
current_file = self.get_playing_file() current_file = self.get_playing_file()
if self.is_playing_file(current_file): if self.is_playing_file(current_file):
item = self.get_file_info(current_file) item = self.get_file_info(current_file)
mapping = item['SubsMapping'] mapping = item["SubsMapping"]
if audio and len(self.getAvailableAudioStreams()) > 1: if audio and len(self.getAvailableAudioStreams()) > 1:
self.setAudioStream(audio - 1) self.setAudioStream(audio - 1)
@ -200,82 +201,90 @@ class Player(xbmc.Player):
def detect_audio_subs(self, item): def detect_audio_subs(self, item):
params = { params = {
'playerid': 1, "playerid": 1,
'properties': ["currentsubtitle", "currentaudiostream", "subtitleenabled"] "properties": ["currentsubtitle", "currentaudiostream", "subtitleenabled"],
} }
result = JSONRPC('Player.GetProperties').execute(params) result = JSONRPC("Player.GetProperties").execute(params)
result = result.get('result') result = result.get("result")
try: # Audio tracks try: # Audio tracks
audio = result['currentaudiostream']['index'] audio = result["currentaudiostream"]["index"]
except (KeyError, TypeError): except (KeyError, TypeError):
audio = 0 audio = 0
try: # Subtitles tracks try: # Subtitles tracks
subs = result['currentsubtitle']['index'] subs = result["currentsubtitle"]["index"]
except (KeyError, TypeError): except (KeyError, TypeError):
subs = 0 subs = 0
try: # If subtitles are enabled try: # If subtitles are enabled
subs_enabled = result['subtitleenabled'] subs_enabled = result["subtitleenabled"]
except (KeyError, TypeError): except (KeyError, TypeError):
subs_enabled = False subs_enabled = False
item['AudioStreamIndex'] = audio + 1 item["AudioStreamIndex"] = audio + 1
if not subs_enabled or not len(self.getAvailableSubtitleStreams()): if not subs_enabled or not len(self.getAvailableSubtitleStreams()):
item['SubtitleStreamIndex'] = None item["SubtitleStreamIndex"] = None
return return
mapping = item['SubsMapping'] mapping = item["SubsMapping"]
tracks = len(self.getAvailableAudioStreams()) tracks = len(self.getAvailableAudioStreams())
if mapping: if mapping:
if str(subs) in mapping: if str(subs) in mapping:
item['SubtitleStreamIndex'] = mapping[str(subs)] item["SubtitleStreamIndex"] = mapping[str(subs)]
else: else:
item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1 item["SubtitleStreamIndex"] = subs - len(mapping) + tracks + 1
else: else:
item['SubtitleStreamIndex'] = subs + tracks + 1 item["SubtitleStreamIndex"] = subs + tracks + 1
def next_up(self): def next_up(self):
item = self.get_file_info(self.get_playing_file()) item = self.get_file_info(self.get_playing_file())
objects = Objects() objects = Objects()
if item['Type'] != 'Episode' or not item.get('CurrentEpisode'): if item["Type"] != "Episode" or not item.get("CurrentEpisode"):
return 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']): for index, next_item in enumerate(next_items["Items"]):
if next_item['Id'] == item['Id']: if next_item["Id"] == item["Id"]:
try: try:
next_item = next_items['Items'][index + 1] next_item = next_items["Items"][index + 1]
except IndexError: except IndexError:
LOG.warning("No next up episode.") LOG.warning("No next up episode.")
return return
break 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) API = api.API(next_item, server_address)
data = objects.map(next_item, "UpNext") data = objects.map(next_item, "UpNext")
artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True) artwork = API.get_all_artwork(objects.map(next_item, "ArtworkParent"), True)
data['art'] = { data["art"] = {
'tvshow.poster': artwork.get('Series.Primary'), "tvshow.poster": artwork.get("Series.Primary"),
'tvshow.fanart': None, "tvshow.fanart": None,
'thumb': artwork.get('Primary') "thumb": artwork.get("Primary"),
} }
if artwork['Backdrop']: if artwork["Backdrop"]:
data['art']['tvshow.fanart'] = artwork['Backdrop'][0] data["art"]["tvshow.fanart"] = artwork["Backdrop"][0]
next_info = { next_info = {
'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'}, "play_info": {
'current_episode': item['CurrentEpisode'], "ItemIds": [data["episodeid"]],
'next_episode': data "ServerId": item["ServerId"],
"PlayCommand": "PlayNow",
},
"current_episode": item["CurrentEpisode"],
"next_episode": data,
} }
LOG.info("--[ next up ] %s", next_info) LOG.info("--[ next up ] %s", next_info)
@ -286,7 +295,7 @@ class Player(xbmc.Player):
if self.is_playing_file(current_file): 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() self.report_playback()
LOG.debug("-->[ paused ]") LOG.debug("-->[ paused ]")
@ -295,24 +304,21 @@ class Player(xbmc.Player):
if self.is_playing_file(current_file): 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() self.report_playback()
LOG.debug("--<[ paused ]") LOG.debug("--<[ paused ]")
def onPlayBackSeek(self, time, seek_offset): 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()): if self.is_playing_file(self.get_playing_file()):
self.report_playback() self.report_playback()
LOG.info("--[ seek ]") LOG.info("--[ seek ]")
def report_playback(self, report=True): def report_playback(self, report=True):
"""Report playback progress to jellyfin server.
''' Report playback progress to jellyfin server. Check if the user seek.
Check if the user seek. """
'''
current_file = self.get_playing_file() current_file = self.get_playing_file()
if not self.is_playing_file(current_file): if not self.is_playing_file(current_file):
@ -320,25 +326,29 @@ class Player(xbmc.Player):
item = self.get_file_info(current_file) item = self.get_file_info(current_file)
if window('jellyfin.external.bool'): if window("jellyfin.external.bool"):
return return
if not report: if not report:
previous = item['CurrentPosition'] previous = item["CurrentPosition"]
try: try:
item['CurrentPosition'] = int(self.getTime()) item["CurrentPosition"] = int(self.getTime())
except Exception as e: except Exception as e:
# getTime() raises RuntimeError if nothing is playing # getTime() raises RuntimeError if nothing is playing
LOG.debug("Failed to get playback position: %s", e) LOG.debug("Failed to get playback position: %s", e)
return return
if int(item['CurrentPosition']) == 1: if int(item["CurrentPosition"]) == 1:
return return
try: try:
played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100 played = (
float(item["CurrentPosition"] * 10000000)
/ int(item["Runtime"])
* 100
)
except ZeroDivisionError: # Runtime is 0. except ZeroDivisionError: # Runtime is 0.
played = 0 played = 0
@ -347,52 +357,48 @@ class Player(xbmc.Player):
self.up_next = True self.up_next = True
self.next_up() self.next_up()
if (item['CurrentPosition'] - previous) < 30: if (item["CurrentPosition"] - previous) < 30:
return return
result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]}) result = JSONRPC("Application.GetProperties").execute(
result = result.get('result', {}) {"properties": ["volume", "muted"]}
item['Volume'] = result.get('volume') )
item['Muted'] = result.get('muted') result = result.get("result", {})
item['CurrentPosition'] = int(self.getTime()) item["Volume"] = result.get("volume")
item["Muted"] = result.get("muted")
item["CurrentPosition"] = int(self.getTime())
self.detect_audio_subs(item) self.detect_audio_subs(item)
data = { data = {
'QueueableMediaTypes': "Video,Audio", "QueueableMediaTypes": "Video,Audio",
'CanSeek': True, "CanSeek": True,
'ItemId': item['Id'], "ItemId": item["Id"],
'MediaSourceId': item['MediaSourceId'], "MediaSourceId": item["MediaSourceId"],
'PlayMethod': item['PlayMethod'], "PlayMethod": item["PlayMethod"],
'VolumeLevel': item['Volume'], "VolumeLevel": item["Volume"],
'PositionTicks': int(item['CurrentPosition'] * 10000000), "PositionTicks": int(item["CurrentPosition"] * 10000000),
'IsPaused': item['Paused'], "IsPaused": item["Paused"],
'IsMuted': item['Muted'], "IsMuted": item["Muted"],
'PlaySessionId': item['PlaySessionId'], "PlaySessionId": item["PlaySessionId"],
'AudioStreamIndex': item['AudioStreamIndex'], "AudioStreamIndex": item["AudioStreamIndex"],
'SubtitleStreamIndex': item['SubtitleStreamIndex'] "SubtitleStreamIndex": item["SubtitleStreamIndex"],
} }
item['Server'].jellyfin.session_progress(data) item["Server"].jellyfin.session_progress(data)
def onPlayBackStopped(self): def onPlayBackStopped(self):
"""Will be called when user stops playing a file."""
''' Will be called when user stops playing a file. window("jellyfin_play", clear=True)
'''
window('jellyfin_play', clear=True)
self.stop_playback() self.stop_playback()
LOG.info("--<[ playback ]") LOG.info("--<[ playback ]")
def onPlayBackEnded(self): def onPlayBackEnded(self):
"""Will be called when kodi stops playing a file."""
''' Will be called when kodi stops playing a file.
'''
self.stop_playback() self.stop_playback()
LOG.info("--<<[ playback ]") LOG.info("--<<[ playback ]")
def stop_playback(self): 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: if not self.played:
return return
@ -401,61 +407,67 @@ class Player(xbmc.Player):
for file in self.played: for file in self.played:
item = self.get_file_info(file) 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'): if window("jellyfin.external.bool"):
window('jellyfin.external', clear=True) window("jellyfin.external", clear=True)
if int(item['CurrentPosition']) == 1: if int(item["CurrentPosition"]) == 1:
item['CurrentPosition'] = int(item['Runtime']) item["CurrentPosition"] = int(item["Runtime"])
data = { data = {
'ItemId': item['Id'], "ItemId": item["Id"],
'MediaSourceId': item['MediaSourceId'], "MediaSourceId": item["MediaSourceId"],
'PositionTicks': int(item['CurrentPosition'] * 10000000), "PositionTicks": int(item["CurrentPosition"] * 10000000),
'PlaySessionId': item['PlaySessionId'] "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']) LOG.info("<[ livestream/%s ]", item["LiveStreamId"])
item['Server'].jellyfin.close_live_stream(item['LiveStreamId']) item["Server"].jellyfin.close_live_stream(item["LiveStreamId"])
elif item['PlayMethod'] == 'Transcode': elif item["PlayMethod"] == "Transcode":
LOG.info("<[ transcode/%s ]", item['Id']) LOG.info("<[ transcode/%s ]", item["Id"])
item['Server'].jellyfin.close_transcode(item['DeviceId'], item['PlaySessionId']) 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): if xbmcvfs.exists(path):
dirs, files = xbmcvfs.listdir(path) dirs, files = xbmcvfs.listdir(path)
for file in files: for file in files:
# Only delete the cached files for the previous play session # 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)) 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 delete = False
if result['Type'] == 'Episode' and settings('deleteTV.bool'): if result["Type"] == "Episode" and settings("deleteTV.bool"):
delete = True delete = True
elif result['Type'] == 'Movie' and settings('deleteMovies.bool'): elif result["Type"] == "Movie" and settings("deleteMovies.bool"):
delete = True delete = True
if not settings('offerDelete.bool'): if not settings("offerDelete.bool"):
delete = False delete = False
if delete: if delete:
LOG.info("Offer delete option") LOG.info("Offer delete option")
if dialog("yesno", translate(30091), translate(33015), autoclose=120000): if dialog(
item['Server'].jellyfin.delete_item(item['Id']) "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() 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__) 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): 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 exception = None
def __init__(self): def __init__(self):
@ -44,10 +44,10 @@ class ServiceManager(threading.Thread):
if service is not None: if service is not None:
# TODO: fix this properly as to not match on str() # TODO: fix this properly as to not match on str()
if 'ExitService' not in str(error): if "ExitService" not in str(error):
service.shutdown() service.shutdown()
if 'RestartService' in str(error): if "RestartService" in str(error):
service.reload_objects() service.reload_objects()
self.exception = error self.exception = error
@ -58,7 +58,7 @@ if __name__ == "__main__":
LOG.info("Delay startup by %s seconds.", DELAY) LOG.info("Delay startup by %s seconds.", DELAY)
while True: while True:
if not settings('enableAddon.bool'): if not settings("enableAddon.bool"):
LOG.warning("Jellyfin for Kodi is not enabled.") LOG.warning("Jellyfin for Kodi is not enabled.")
break break
@ -68,12 +68,11 @@ if __name__ == "__main__":
session.start() session.start()
session.join() # Block until the thread exits. session.join() # Block until the thread exits.
if 'RestartService' in str(session.exception): if "RestartService" in str(session.exception):
continue continue
except Exception as error: except Exception as error:
''' Issue initializing the service. """Issue initializing the service."""
'''
LOG.exception(error) LOG.exception(error)
break break

View File

@ -3,45 +3,51 @@ import pytest
from jellyfin_kodi.jellyfin.utils import clean_none_dict_values from jellyfin_kodi.jellyfin.utils import clean_none_dict_values
@pytest.mark.parametrize("obj,expected", [ @pytest.mark.parametrize(
(None, None), "obj,expected",
([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]), [
({'foo': None, 'bar': 123}, {'bar': 123}), (None, None),
({ ([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]),
'dict': { ({"foo": None, "bar": 123}, {"bar": 123}),
'empty': None, (
'string': "Hello, Woorld!",
},
'number': 123,
'list': [
None,
123,
"foo",
{ {
'empty': None, "dict": {
'number': 123, "empty": None,
'string': "foo", "string": "Hello, Woorld!",
'list': [], },
'dict': {}, "number": 123,
} "list": [
] None,
}, { 123,
'dict': { "foo",
'string': "Hello, Woorld!", {
}, "empty": None,
'number': 123, "number": 123,
'list': [ "string": "foo",
None, "list": [],
123, "dict": {},
"foo", },
],
},
{ {
'number': 123, "dict": {
'string': "foo", "string": "Hello, Woorld!",
'list': [], },
'dict': {}, "number": 123,
} "list": [
] None,
}), 123,
]) "foo",
{
"number": 123,
"string": "foo",
"list": [],
"dict": {},
},
],
},
),
],
)
def test_clean_none_dict_values(obj, expected): def test_clean_none_dict_values(obj, expected):
assert 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(): def test_import_entrypoint():
import jellyfin_kodi.entrypoint import jellyfin_kodi.entrypoint
import jellyfin_kodi.entrypoint.context import jellyfin_kodi.entrypoint.context
# import jellyfin_kodi.entrypoint.default # FIXME: Messes with sys.argv # import jellyfin_kodi.entrypoint.default # FIXME: Messes with sys.argv
import jellyfin_kodi.entrypoint.service # noqa: F401 import jellyfin_kodi.entrypoint.service # noqa: F401

View File

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