mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-11-23 05:19:48 +00:00
commit
94e44d4c10
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
||||
# Tool: black
|
||||
77637622125a187c5b9cbe72b78c8bd3b26f754a
|
81
build.py
81
build.py
@ -35,46 +35,48 @@ def create_addon_xml(config: dict, source: str, py_version: str) -> None:
|
||||
Create addon.xml from template file
|
||||
"""
|
||||
# Load template file
|
||||
with open('{}/.build/template.xml'.format(source), 'r') as f:
|
||||
with open("{}/.build/template.xml".format(source), "r") as f:
|
||||
tree = ET.parse(f)
|
||||
root = tree.getroot()
|
||||
|
||||
# Populate dependencies in template
|
||||
dependencies = config['dependencies'].get(py_version)
|
||||
dependencies = config["dependencies"].get(py_version)
|
||||
for dep in dependencies:
|
||||
ET.SubElement(root.find('requires'), 'import', attrib=dep)
|
||||
ET.SubElement(root.find("requires"), "import", attrib=dep)
|
||||
|
||||
# Populate version string
|
||||
addon_version = config.get('version')
|
||||
root.attrib['version'] = '{}+{}'.format(addon_version, py_version)
|
||||
addon_version = config.get("version")
|
||||
root.attrib["version"] = "{}+{}".format(addon_version, py_version)
|
||||
|
||||
# Populate Changelog
|
||||
date = datetime.today().strftime('%Y-%m-%d')
|
||||
changelog = config.get('changelog')
|
||||
for section in root.findall('extension'):
|
||||
news = section.findall('news')
|
||||
date = datetime.today().strftime("%Y-%m-%d")
|
||||
changelog = config.get("changelog")
|
||||
for section in root.findall("extension"):
|
||||
news = section.findall("news")
|
||||
if news:
|
||||
news[0].text = 'v{} ({}):\n{}'.format(addon_version, date, changelog)
|
||||
news[0].text = "v{} ({}):\n{}".format(addon_version, date, changelog)
|
||||
|
||||
# Format xml tree
|
||||
indent(root)
|
||||
|
||||
# Write addon.xml
|
||||
tree.write('{}/addon.xml'.format(source), encoding='utf-8', xml_declaration=True)
|
||||
tree.write("{}/addon.xml".format(source), encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
def zip_files(py_version: str, source: str, target: str, dev: bool) -> None:
|
||||
"""
|
||||
Create installable addon zip archive
|
||||
"""
|
||||
archive_name = 'plugin.video.jellyfin+{}.zip'.format(py_version)
|
||||
archive_name = "plugin.video.jellyfin+{}.zip".format(py_version)
|
||||
|
||||
with zipfile.ZipFile('{}/{}'.format(target, archive_name), 'w') as z:
|
||||
with zipfile.ZipFile("{}/{}".format(target, archive_name), "w") as z:
|
||||
for root, dirs, files in os.walk(args.source):
|
||||
for filename in filter(file_filter, files):
|
||||
file_path = os.path.join(root, filename)
|
||||
if dev or folder_filter(file_path):
|
||||
relative_path = os.path.join('plugin.video.jellyfin', os.path.relpath(file_path, source))
|
||||
relative_path = os.path.join(
|
||||
"plugin.video.jellyfin", os.path.relpath(file_path, source)
|
||||
)
|
||||
z.write(file_path, relative_path)
|
||||
|
||||
|
||||
@ -83,10 +85,12 @@ def file_filter(file_name: str) -> bool:
|
||||
True if file_name is meant to be included
|
||||
"""
|
||||
return (
|
||||
not (file_name.startswith('plugin.video.jellyfin') and file_name.endswith('.zip'))
|
||||
and not file_name.endswith('.pyo')
|
||||
and not file_name.endswith('.pyc')
|
||||
and not file_name.endswith('.pyd')
|
||||
not (
|
||||
file_name.startswith("plugin.video.jellyfin") and file_name.endswith(".zip")
|
||||
)
|
||||
and not file_name.endswith(".pyo")
|
||||
and not file_name.endswith(".pyc")
|
||||
and not file_name.endswith(".pyd")
|
||||
)
|
||||
|
||||
|
||||
@ -95,13 +99,13 @@ def folder_filter(folder_name: str) -> bool:
|
||||
True if folder_name is meant to be included
|
||||
"""
|
||||
filters = [
|
||||
'.ci',
|
||||
'.git',
|
||||
'.github',
|
||||
'.build',
|
||||
'.mypy_cache',
|
||||
'.pytest_cache',
|
||||
'__pycache__',
|
||||
".ci",
|
||||
".git",
|
||||
".github",
|
||||
".build",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
"__pycache__",
|
||||
]
|
||||
for f in filters:
|
||||
if f in folder_name.split(os.path.sep):
|
||||
@ -110,33 +114,22 @@ def folder_filter(folder_name: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Build flags:")
|
||||
parser.add_argument("--version", type=str, choices=("py2", "py3"), default="py3")
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Build flags:')
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
type=str,
|
||||
choices=('py2', 'py3'),
|
||||
default='py3')
|
||||
parser.add_argument("--source", type=Path, default=Path(__file__).absolute().parent)
|
||||
|
||||
parser.add_argument(
|
||||
'--source',
|
||||
type=Path,
|
||||
default=Path(__file__).absolute().parent)
|
||||
parser.add_argument("--target", type=Path, default=Path(__file__).absolute().parent)
|
||||
|
||||
parser.add_argument(
|
||||
'--target',
|
||||
type=Path,
|
||||
default=Path(__file__).absolute().parent)
|
||||
|
||||
parser.add_argument('--dev', dest='dev', action='store_true')
|
||||
parser.add_argument("--dev", dest="dev", action="store_true")
|
||||
parser.set_defaults(dev=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config file
|
||||
config_path = os.path.join(args.source, 'release.yaml')
|
||||
with open(config_path, 'r') as fh:
|
||||
config_path = os.path.join(args.source, "release.yaml")
|
||||
with open(config_path, "r") as fh:
|
||||
release_config = yaml.safe_load(fh)
|
||||
|
||||
create_addon_xml(release_config, args.source, args.version)
|
||||
|
@ -18,73 +18,69 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_addon_name():
|
||||
|
||||
''' Used for logging.
|
||||
'''
|
||||
return xbmcaddon.Addon(addon_id()).getAddonInfo('name').upper()
|
||||
"""Used for logging."""
|
||||
return xbmcaddon.Addon(addon_id()).getAddonInfo("name").upper()
|
||||
|
||||
|
||||
def get_version():
|
||||
return xbmcaddon.Addon(addon_id()).getAddonInfo('version')
|
||||
return xbmcaddon.Addon(addon_id()).getAddonInfo("version")
|
||||
|
||||
|
||||
def get_platform():
|
||||
|
||||
if xbmc.getCondVisibility('system.platform.osx'):
|
||||
if xbmc.getCondVisibility("system.platform.osx"):
|
||||
return "OSX"
|
||||
elif xbmc.getCondVisibility('System.HasAddon(service.coreelec.settings)'):
|
||||
elif xbmc.getCondVisibility("System.HasAddon(service.coreelec.settings)"):
|
||||
return "CoreElec"
|
||||
elif xbmc.getCondVisibility('System.HasAddon(service.libreelec.settings)'):
|
||||
elif xbmc.getCondVisibility("System.HasAddon(service.libreelec.settings)"):
|
||||
return "LibreElec"
|
||||
elif xbmc.getCondVisibility('System.HasAddon(service.osmc.settings)'):
|
||||
elif xbmc.getCondVisibility("System.HasAddon(service.osmc.settings)"):
|
||||
return "OSMC"
|
||||
elif xbmc.getCondVisibility('system.platform.atv2'):
|
||||
elif xbmc.getCondVisibility("system.platform.atv2"):
|
||||
return "ATV2"
|
||||
elif xbmc.getCondVisibility('system.platform.ios'):
|
||||
elif xbmc.getCondVisibility("system.platform.ios"):
|
||||
return "iOS"
|
||||
elif xbmc.getCondVisibility('system.platform.windows'):
|
||||
elif xbmc.getCondVisibility("system.platform.windows"):
|
||||
return "Windows"
|
||||
elif xbmc.getCondVisibility('system.platform.android'):
|
||||
elif xbmc.getCondVisibility("system.platform.android"):
|
||||
return "Linux/Android"
|
||||
elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'):
|
||||
elif xbmc.getCondVisibility("system.platform.linux.raspberrypi"):
|
||||
return "Linux/RPi"
|
||||
elif xbmc.getCondVisibility('system.platform.linux'):
|
||||
elif xbmc.getCondVisibility("system.platform.linux"):
|
||||
return "Linux"
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def get_device_name():
|
||||
|
||||
''' Detect the device name. If deviceNameOpt, then
|
||||
use the device name in the add-on settings.
|
||||
Otherwise, fallback to the Kodi device name.
|
||||
'''
|
||||
if not settings('deviceNameOpt.bool'):
|
||||
device_name = xbmc.getInfoLabel('System.FriendlyName')
|
||||
"""Detect the device name. If deviceNameOpt, then
|
||||
use the device name in the add-on settings.
|
||||
Otherwise, fallback to the Kodi device name.
|
||||
"""
|
||||
if not settings("deviceNameOpt.bool"):
|
||||
device_name = xbmc.getInfoLabel("System.FriendlyName")
|
||||
else:
|
||||
device_name = settings('deviceName')
|
||||
device_name = device_name.replace("\"", "_")
|
||||
device_name = settings("deviceName")
|
||||
device_name = device_name.replace('"', "_")
|
||||
device_name = device_name.replace("/", "_")
|
||||
|
||||
return device_name
|
||||
|
||||
|
||||
def get_device_id(reset=False):
|
||||
"""Return the device_id if already loaded.
|
||||
It will load from jellyfin_guid file. If it's a fresh
|
||||
setup, it will generate a new GUID to uniquely
|
||||
identify the setup for all users.
|
||||
|
||||
''' Return the device_id if already loaded.
|
||||
It will load from jellyfin_guid file. If it's a fresh
|
||||
setup, it will generate a new GUID to uniquely
|
||||
identify the setup for all users.
|
||||
|
||||
window prop: jellyfin_deviceId
|
||||
'''
|
||||
client_id = window('jellyfin_deviceId')
|
||||
window prop: jellyfin_deviceId
|
||||
"""
|
||||
client_id = window("jellyfin_deviceId")
|
||||
|
||||
if client_id:
|
||||
return client_id
|
||||
|
||||
directory = translate_path('special://profile/addon_data/plugin.video.jellyfin/')
|
||||
directory = translate_path("special://profile/addon_data/plugin.video.jellyfin/")
|
||||
|
||||
if not xbmcvfs.exists(directory):
|
||||
xbmcvfs.mkdir(directory)
|
||||
@ -97,27 +93,27 @@ def get_device_id(reset=False):
|
||||
LOG.debug("Generating a new GUID.")
|
||||
|
||||
client_id = str(create_id())
|
||||
file_guid = xbmcvfs.File(jellyfin_guid, 'w')
|
||||
file_guid = xbmcvfs.File(jellyfin_guid, "w")
|
||||
file_guid.write(client_id)
|
||||
|
||||
file_guid.close()
|
||||
LOG.debug("DeviceId loaded: %s", client_id)
|
||||
window('jellyfin_deviceId', value=client_id)
|
||||
window("jellyfin_deviceId", value=client_id)
|
||||
|
||||
return client_id
|
||||
|
||||
|
||||
def reset_device_id():
|
||||
|
||||
window('jellyfin_deviceId', clear=True)
|
||||
window("jellyfin_deviceId", clear=True)
|
||||
get_device_id(True)
|
||||
dialog("ok", "{jellyfin}", translate(33033))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
xbmc.executebuiltin("RestartApp")
|
||||
|
||||
|
||||
def get_info():
|
||||
return {
|
||||
'DeviceName': get_device_name(),
|
||||
'Version': get_version(),
|
||||
'DeviceId': get_device_id()
|
||||
"DeviceName": get_device_name(),
|
||||
"Version": get_version(),
|
||||
"DeviceId": get_device_id(),
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ from .helper.exceptions import HTTPException
|
||||
##################################################################################################
|
||||
|
||||
LOG = LazyLogger(__name__)
|
||||
XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo('path'), "default", "1080i")
|
||||
XML_PATH = (xbmcaddon.Addon(addon_id()).getAddonInfo("path"), "default", "1080i")
|
||||
|
||||
##################################################################################################
|
||||
|
||||
@ -27,36 +27,37 @@ class Connect(object):
|
||||
self.info = client.get_info()
|
||||
|
||||
def register(self, server_id=None, options={}):
|
||||
|
||||
''' Login into server. If server is None, then it will show the proper prompts to login, etc.
|
||||
If a server id is specified then only a login dialog will be shown for that server.
|
||||
'''
|
||||
LOG.info("--[ server/%s ]", server_id or 'default')
|
||||
"""Login into server. If server is None, then it will show the proper prompts to login, etc.
|
||||
If a server id is specified then only a login dialog will be shown for that server.
|
||||
"""
|
||||
LOG.info("--[ server/%s ]", server_id or "default")
|
||||
credentials = dict(get_credentials())
|
||||
servers = credentials['Servers']
|
||||
servers = credentials["Servers"]
|
||||
|
||||
if server_id is None and credentials['Servers']:
|
||||
credentials['Servers'] = [credentials['Servers'][0]]
|
||||
if server_id is None and credentials["Servers"]:
|
||||
credentials["Servers"] = [credentials["Servers"][0]]
|
||||
|
||||
elif credentials['Servers']:
|
||||
elif credentials["Servers"]:
|
||||
|
||||
for server in credentials['Servers']:
|
||||
for server in credentials["Servers"]:
|
||||
|
||||
if server['Id'] == server_id:
|
||||
credentials['Servers'] = [server]
|
||||
if server["Id"] == server_id:
|
||||
credentials["Servers"] = [server]
|
||||
|
||||
server_select = server_id is None and not settings('SyncInstallRunDone.bool')
|
||||
new_credentials = self.register_client(credentials, options, server_id, server_select)
|
||||
server_select = server_id is None and not settings("SyncInstallRunDone.bool")
|
||||
new_credentials = self.register_client(
|
||||
credentials, options, server_id, server_select
|
||||
)
|
||||
|
||||
for server in servers:
|
||||
if server['Id'] == new_credentials['Servers'][0]['Id']:
|
||||
server = new_credentials['Servers'][0]
|
||||
if server["Id"] == new_credentials["Servers"][0]["Id"]:
|
||||
server = new_credentials["Servers"][0]
|
||||
|
||||
break
|
||||
else:
|
||||
servers = new_credentials['Servers']
|
||||
servers = new_credentials["Servers"]
|
||||
|
||||
credentials['Servers'] = servers
|
||||
credentials["Servers"] = servers
|
||||
save_credentials(credentials)
|
||||
|
||||
try:
|
||||
@ -65,36 +66,39 @@ class Connect(object):
|
||||
LOG.error(error)
|
||||
|
||||
def get_ssl(self):
|
||||
|
||||
''' Returns boolean value.
|
||||
True: verify connection.
|
||||
'''
|
||||
return settings('sslverify.bool')
|
||||
"""Returns boolean value.
|
||||
True: verify connection.
|
||||
"""
|
||||
return settings("sslverify.bool")
|
||||
|
||||
def get_client(self, server_id=None):
|
||||
|
||||
''' Get Jellyfin client.
|
||||
'''
|
||||
"""Get Jellyfin client."""
|
||||
client = Jellyfin(server_id)
|
||||
client.config.app("Kodi", self.info['Version'], self.info['DeviceName'], self.info['DeviceId'])
|
||||
client.config.data['http.user_agent'] = "Jellyfin-Kodi/%s" % self.info['Version']
|
||||
client.config.data['auth.ssl'] = self.get_ssl()
|
||||
client.config.app(
|
||||
"Kodi", self.info["Version"], self.info["DeviceName"], self.info["DeviceId"]
|
||||
)
|
||||
client.config.data["http.user_agent"] = (
|
||||
"Jellyfin-Kodi/%s" % self.info["Version"]
|
||||
)
|
||||
client.config.data["auth.ssl"] = self.get_ssl()
|
||||
|
||||
return client
|
||||
|
||||
def register_client(self, credentials=None, options=None, server_id=None, server_selection=False):
|
||||
def register_client(
|
||||
self, credentials=None, options=None, server_id=None, server_selection=False
|
||||
):
|
||||
|
||||
client = self.get_client(server_id)
|
||||
self.client = client
|
||||
self.connect_manager = client.auth
|
||||
|
||||
if server_id is None:
|
||||
client.config.data['app.default'] = True
|
||||
client.config.data["app.default"] = True
|
||||
|
||||
try:
|
||||
state = client.authenticate(credentials or {}, options or {})
|
||||
|
||||
if state['State'] == CONNECTION_STATE['SignedIn']:
|
||||
if state["State"] == CONNECTION_STATE["SignedIn"]:
|
||||
client.callback_ws = event
|
||||
|
||||
if server_id is None: # Only assign for default server
|
||||
@ -102,66 +106,77 @@ class Connect(object):
|
||||
client.callback = event
|
||||
self.get_user(client)
|
||||
|
||||
settings('serverName', client.config.data['auth.server-name'])
|
||||
settings('server', client.config.data['auth.server'])
|
||||
settings("serverName", client.config.data["auth.server-name"])
|
||||
settings("server", client.config.data["auth.server"])
|
||||
|
||||
event('ServerOnline', {'ServerId': server_id})
|
||||
event('LoadServer', {'ServerId': server_id})
|
||||
event("ServerOnline", {"ServerId": server_id})
|
||||
event("LoadServer", {"ServerId": server_id})
|
||||
|
||||
return state['Credentials']
|
||||
return state["Credentials"]
|
||||
|
||||
elif (server_selection or state['State'] == CONNECTION_STATE['ServerSelection'] or state['State'] == CONNECTION_STATE['Unavailable'] and not settings('SyncInstallRunDone.bool')):
|
||||
state['Credentials']['Servers'] = [self.select_servers(state)]
|
||||
elif (
|
||||
server_selection
|
||||
or state["State"] == CONNECTION_STATE["ServerSelection"]
|
||||
or state["State"] == CONNECTION_STATE["Unavailable"]
|
||||
and not settings("SyncInstallRunDone.bool")
|
||||
):
|
||||
state["Credentials"]["Servers"] = [self.select_servers(state)]
|
||||
|
||||
elif state['State'] == CONNECTION_STATE['ServerSignIn']:
|
||||
if 'ExchangeToken' not in state['Servers'][0]:
|
||||
elif state["State"] == CONNECTION_STATE["ServerSignIn"]:
|
||||
if "ExchangeToken" not in state["Servers"][0]:
|
||||
self.login()
|
||||
|
||||
elif state['State'] == CONNECTION_STATE['Unavailable'] and state.get('Status_Code', 0) == 401:
|
||||
elif (
|
||||
state["State"] == CONNECTION_STATE["Unavailable"]
|
||||
and state.get("Status_Code", 0) == 401
|
||||
):
|
||||
# If the saved credentials don't work, restart the addon to force the password dialog to open
|
||||
window('jellyfin.restart', clear=True)
|
||||
window("jellyfin.restart", clear=True)
|
||||
|
||||
elif state['State'] == CONNECTION_STATE['Unavailable']:
|
||||
raise HTTPException('ServerUnreachable', {})
|
||||
elif state["State"] == CONNECTION_STATE["Unavailable"]:
|
||||
raise HTTPException("ServerUnreachable", {})
|
||||
|
||||
return self.register_client(state['Credentials'], options, server_id, False)
|
||||
return self.register_client(state["Credentials"], options, server_id, False)
|
||||
|
||||
except RuntimeError as error:
|
||||
|
||||
LOG.exception(error)
|
||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addon_id())
|
||||
xbmc.executebuiltin("Addon.OpenSettings(%s)" % addon_id())
|
||||
|
||||
raise Exception('User sign in interrupted')
|
||||
raise Exception("User sign in interrupted")
|
||||
|
||||
except HTTPException as error:
|
||||
|
||||
if error.status == 'ServerUnreachable':
|
||||
event('ServerUnreachable', {'ServerId': server_id})
|
||||
if error.status == "ServerUnreachable":
|
||||
event("ServerUnreachable", {"ServerId": server_id})
|
||||
|
||||
return client.get_credentials()
|
||||
|
||||
def get_user(self, client):
|
||||
|
||||
''' Save user info.
|
||||
'''
|
||||
"""Save user info."""
|
||||
self.user = client.jellyfin.get_user()
|
||||
settings('username', self.user['Name'])
|
||||
settings("username", self.user["Name"])
|
||||
|
||||
if 'PrimaryImageTag' in self.user:
|
||||
server_address = client.auth.get_server_info(client.auth.server_id)['address']
|
||||
window('JellyfinUserImage', api.API(self.user, server_address).get_user_artwork(self.user['Id']))
|
||||
if "PrimaryImageTag" in self.user:
|
||||
server_address = client.auth.get_server_info(client.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
window(
|
||||
"JellyfinUserImage",
|
||||
api.API(self.user, server_address).get_user_artwork(self.user["Id"]),
|
||||
)
|
||||
|
||||
def select_servers(self, state=None):
|
||||
|
||||
state = state or self.connect_manager.connect({'enableAutoLogin': False})
|
||||
state = state or self.connect_manager.connect({"enableAutoLogin": False})
|
||||
user = {}
|
||||
|
||||
dialog = ServerConnect("script-jellyfin-connect-server.xml", *XML_PATH)
|
||||
dialog.set_args(
|
||||
connect_manager=self.connect_manager,
|
||||
username=user.get('DisplayName', ""),
|
||||
user_image=user.get('ImageUrl'),
|
||||
servers=self.connect_manager.get_available_servers()
|
||||
username=user.get("DisplayName", ""),
|
||||
user_image=user.get("ImageUrl"),
|
||||
servers=self.connect_manager.get_available_servers(),
|
||||
)
|
||||
|
||||
dialog.doModal()
|
||||
@ -182,9 +197,7 @@ class Connect(object):
|
||||
return self.select_servers()
|
||||
|
||||
def setup_manual_server(self):
|
||||
|
||||
''' Setup manual servers
|
||||
'''
|
||||
"""Setup manual servers"""
|
||||
client = self.get_client()
|
||||
client.set_credentials(get_credentials())
|
||||
manager = client.auth
|
||||
@ -198,11 +211,9 @@ class Connect(object):
|
||||
save_credentials(credentials)
|
||||
|
||||
def manual_server(self, manager=None):
|
||||
|
||||
''' Return server or raise error.
|
||||
'''
|
||||
"""Return server or raise error."""
|
||||
dialog = ServerManual("script-jellyfin-connect-server-manual.xml", *XML_PATH)
|
||||
dialog.set_args(**{'connect_manager': manager or self.connect_manager})
|
||||
dialog.set_args(**{"connect_manager": manager or self.connect_manager})
|
||||
dialog.doModal()
|
||||
|
||||
if dialog.is_connected():
|
||||
@ -213,7 +224,9 @@ class Connect(object):
|
||||
def login(self):
|
||||
|
||||
users = self.connect_manager.get_public_users()
|
||||
server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address']
|
||||
server = self.connect_manager.get_server_info(self.connect_manager.server_id)[
|
||||
"address"
|
||||
]
|
||||
|
||||
if not users:
|
||||
try:
|
||||
@ -222,14 +235,14 @@ class Connect(object):
|
||||
raise RuntimeError("No user selected")
|
||||
|
||||
dialog = UsersConnect("script-jellyfin-connect-users.xml", *XML_PATH)
|
||||
dialog.set_args(**{'server': server, 'users': users})
|
||||
dialog.set_args(**{"server": server, "users": users})
|
||||
dialog.doModal()
|
||||
|
||||
if dialog.is_user_selected():
|
||||
user = dialog.get_user()
|
||||
username = user['Name']
|
||||
username = user["Name"]
|
||||
|
||||
if user['HasPassword']:
|
||||
if user["HasPassword"]:
|
||||
LOG.debug("User has password, present manual login")
|
||||
try:
|
||||
return self.login_manual(username)
|
||||
@ -249,14 +262,12 @@ class Connect(object):
|
||||
return self.login()
|
||||
|
||||
def setup_login_manual(self):
|
||||
|
||||
''' Setup manual login by itself for default server.
|
||||
'''
|
||||
"""Setup manual login by itself for default server."""
|
||||
client = self.get_client()
|
||||
client.set_credentials(get_credentials())
|
||||
manager = client.auth
|
||||
|
||||
username = settings('username')
|
||||
username = settings("username")
|
||||
try:
|
||||
self.login_manual(user=username, manager=manager)
|
||||
except RuntimeError:
|
||||
@ -266,11 +277,14 @@ class Connect(object):
|
||||
save_credentials(credentials)
|
||||
|
||||
def login_manual(self, user=None, manager=None):
|
||||
|
||||
''' Return manual login user authenticated or raise error.
|
||||
'''
|
||||
"""Return manual login user authenticated or raise error."""
|
||||
dialog = LoginManual("script-jellyfin-connect-login-manual.xml", *XML_PATH)
|
||||
dialog.set_args(**{'connect_manager': manager or self.connect_manager, 'username': user or {}})
|
||||
dialog.set_args(
|
||||
**{
|
||||
"connect_manager": manager or self.connect_manager,
|
||||
"username": user or {},
|
||||
}
|
||||
)
|
||||
dialog.doModal()
|
||||
|
||||
if dialog.is_logged_in():
|
||||
@ -279,15 +293,13 @@ class Connect(object):
|
||||
raise RuntimeError("User is not authenticated")
|
||||
|
||||
def remove_server(self, server_id):
|
||||
|
||||
''' Stop client and remove server.
|
||||
'''
|
||||
"""Stop client and remove server."""
|
||||
Jellyfin(server_id).close()
|
||||
credentials = get_credentials()
|
||||
|
||||
for server in credentials['Servers']:
|
||||
if server['Id'] == server_id:
|
||||
credentials['Servers'].remove(server)
|
||||
for server in credentials["Servers"]:
|
||||
if server["Id"] == server_id:
|
||||
credentials["Servers"].remove(server)
|
||||
|
||||
break
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import datetime
|
||||
@ -28,51 +29,56 @@ ADDON_DATA = translate_path("special://profile/addon_data/plugin.video.jellyfin/
|
||||
|
||||
|
||||
class Database(object):
|
||||
"""This should be called like a context.
|
||||
i.e. with Database('jellyfin') as db:
|
||||
db.cursor
|
||||
db.conn.commit()
|
||||
"""
|
||||
|
||||
''' This should be called like a context.
|
||||
i.e. with Database('jellyfin') as db:
|
||||
db.cursor
|
||||
db.conn.commit()
|
||||
'''
|
||||
timeout = 120
|
||||
discovered = False
|
||||
discovered_file = None
|
||||
|
||||
def __init__(self, db_file=None, commit_close=True):
|
||||
|
||||
''' file: jellyfin, texture, music, video, :memory: or path to file
|
||||
'''
|
||||
"""file: jellyfin, texture, music, video, :memory: or path to file"""
|
||||
self.db_file = db_file or "video"
|
||||
self.commit_close = commit_close
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
''' Open the connection and return the Database class.
|
||||
This is to allow for the cursor, conn and others to be accessible.
|
||||
'''
|
||||
"""Open the connection and return the Database class.
|
||||
This is to allow for the cursor, conn and others to be accessible.
|
||||
"""
|
||||
self.path = self._sql(self.db_file)
|
||||
self.conn = sqlite3.connect(self.path, timeout=self.timeout)
|
||||
self.cursor = self.conn.cursor()
|
||||
|
||||
if self.db_file in ('video', 'music', 'texture', 'jellyfin'):
|
||||
self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi
|
||||
if self.db_file in ("video", "music", "texture", "jellyfin"):
|
||||
self.conn.execute(
|
||||
"PRAGMA journal_mode=WAL"
|
||||
) # to avoid writing conflict with kodi
|
||||
|
||||
LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn))
|
||||
|
||||
if not window('jellyfin_db_check.bool') and self.db_file == 'jellyfin':
|
||||
if not window("jellyfin_db_check.bool") and self.db_file == "jellyfin":
|
||||
|
||||
window('jellyfin_db_check.bool', True)
|
||||
window("jellyfin_db_check.bool", True)
|
||||
jellyfin_tables(self.cursor)
|
||||
self.conn.commit()
|
||||
|
||||
# Migration for #162
|
||||
if self.db_file == 'music':
|
||||
query = self.conn.execute('SELECT * FROM path WHERE strPath LIKE "%/emby/%"')
|
||||
if self.db_file == "music":
|
||||
query = self.conn.execute(
|
||||
'SELECT * FROM path WHERE strPath LIKE "%/emby/%"'
|
||||
)
|
||||
contents = query.fetchall()
|
||||
if contents:
|
||||
for item in contents:
|
||||
new_path = item[1].replace('/emby/', '/')
|
||||
self.conn.execute('UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(new_path, item[0]))
|
||||
new_path = item[1].replace("/emby/", "/")
|
||||
self.conn.execute(
|
||||
'UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(
|
||||
new_path, item[0]
|
||||
)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
@ -97,68 +103,68 @@ class Database(object):
|
||||
return path
|
||||
|
||||
def _discover_database(self, database):
|
||||
"""Use UpdateLibrary(video) to update the date modified
|
||||
on the database file used by Kodi.
|
||||
"""
|
||||
if database == "video":
|
||||
|
||||
''' Use UpdateLibrary(video) to update the date modified
|
||||
on the database file used by Kodi.
|
||||
'''
|
||||
if database == 'video':
|
||||
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
xbmc.executebuiltin("UpdateLibrary(video)")
|
||||
xbmc.sleep(200)
|
||||
|
||||
databases = translate_path("special://database/")
|
||||
types = {
|
||||
'video': "MyVideos",
|
||||
'music': "MyMusic",
|
||||
'texture': "Textures"
|
||||
}
|
||||
types = {"video": "MyVideos", "music": "MyMusic", "texture": "Textures"}
|
||||
database = types[database]
|
||||
dirs, files = xbmcvfs.listdir(databases)
|
||||
target = {'db_file': '', 'version': 0}
|
||||
target = {"db_file": "", "version": 0}
|
||||
|
||||
for db_file in reversed(files):
|
||||
if (db_file.startswith(database)
|
||||
and not db_file.endswith('-wal')
|
||||
and not db_file.endswith('-shm')
|
||||
and not db_file.endswith('db-journal')):
|
||||
if (
|
||||
db_file.startswith(database)
|
||||
and not db_file.endswith("-wal")
|
||||
and not db_file.endswith("-shm")
|
||||
and not db_file.endswith("db-journal")
|
||||
):
|
||||
|
||||
version_string = re.search('{}(.*).db'.format(database), db_file)
|
||||
version_string = re.search("{}(.*).db".format(database), db_file)
|
||||
version = int(version_string.group(1))
|
||||
|
||||
if version > target['version']:
|
||||
target['db_file'] = db_file
|
||||
target['version'] = version
|
||||
if version > target["version"]:
|
||||
target["db_file"] = db_file
|
||||
target["version"] = version
|
||||
|
||||
LOG.debug("Discovered database: %s", target)
|
||||
self.discovered_file = target['db_file']
|
||||
self.discovered_file = target["db_file"]
|
||||
|
||||
return translate_path("special://database/%s" % target['db_file'])
|
||||
return translate_path("special://database/%s" % target["db_file"])
|
||||
|
||||
def _sql(self, db_file):
|
||||
|
||||
''' Get the database path based on the file objects/obj_map.json
|
||||
Compatible check, in the event multiple db version are supported with the same Kodi version.
|
||||
Discover by file as a last resort.
|
||||
'''
|
||||
"""Get the database path based on the file objects/obj_map.json
|
||||
Compatible check, in the event multiple db version are supported with the same Kodi version.
|
||||
Discover by file as a last resort.
|
||||
"""
|
||||
databases = obj.Objects().objects
|
||||
|
||||
if db_file not in ('video', 'music', 'texture') or databases.get('database_set%s' % db_file):
|
||||
if db_file not in ("video", "music", "texture") or databases.get(
|
||||
"database_set%s" % db_file
|
||||
):
|
||||
return self._get_database(databases[db_file], True)
|
||||
|
||||
discovered = self._discover_database(db_file) if not databases.get('database_set%s' % db_file) else None
|
||||
discovered = (
|
||||
self._discover_database(db_file)
|
||||
if not databases.get("database_set%s" % db_file)
|
||||
else None
|
||||
)
|
||||
|
||||
databases[db_file] = discovered
|
||||
self.discovered = True
|
||||
|
||||
databases['database_set%s' % db_file] = True
|
||||
databases["database_set%s" % db_file] = True
|
||||
LOG.info("Database locked in: %s", databases[db_file])
|
||||
|
||||
return databases[db_file]
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
''' Close the connection and cursor.
|
||||
'''
|
||||
"""Close the connection and cursor."""
|
||||
changes = self.conn.total_changes
|
||||
|
||||
if exc_type is not None: # errors raised
|
||||
@ -175,41 +181,43 @@ class Database(object):
|
||||
|
||||
|
||||
def jellyfin_tables(cursor):
|
||||
|
||||
''' Create the tables for the jellyfin database.
|
||||
jellyfin, view, version
|
||||
'''
|
||||
"""Create the tables for the jellyfin database.
|
||||
jellyfin, view, version
|
||||
"""
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS jellyfin(
|
||||
jellyfin_id TEXT UNIQUE, media_folder TEXT, jellyfin_type TEXT, media_type TEXT,
|
||||
kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER,
|
||||
checksum INTEGER, jellyfin_parent_id TEXT)""")
|
||||
checksum INTEGER, jellyfin_parent_id TEXT)"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS view(
|
||||
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""")
|
||||
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)"""
|
||||
)
|
||||
cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
|
||||
|
||||
columns = cursor.execute("SELECT * FROM jellyfin")
|
||||
if 'jellyfin_parent_id' not in [description[0] for description in columns.description]:
|
||||
if "jellyfin_parent_id" not in [
|
||||
description[0] for description in columns.description
|
||||
]:
|
||||
|
||||
LOG.debug("Add missing column jellyfin_parent_id")
|
||||
cursor.execute("ALTER TABLE jellyfin ADD COLUMN jellyfin_parent_id 'TEXT'")
|
||||
|
||||
|
||||
def reset():
|
||||
|
||||
''' Reset both the jellyfin database and the kodi database.
|
||||
'''
|
||||
"""Reset both the jellyfin database and the kodi database."""
|
||||
from ..views import Views
|
||||
|
||||
views = Views()
|
||||
|
||||
if not dialog("yesno", "{jellyfin}", translate(33074)):
|
||||
return
|
||||
|
||||
window('jellyfin_should_stop.bool', True)
|
||||
window("jellyfin_should_stop.bool", True)
|
||||
count = 10
|
||||
|
||||
while window('jellyfin_sync.bool'):
|
||||
while window("jellyfin_sync.bool"):
|
||||
|
||||
LOG.info("Sync is running...")
|
||||
count -= 1
|
||||
@ -239,12 +247,12 @@ def reset():
|
||||
if xbmcvfs.exists(os.path.join(ADDON_DATA, "sync.json")):
|
||||
xbmcvfs.delete(os.path.join(ADDON_DATA, "sync.json"))
|
||||
|
||||
settings('enableMusic.bool', False)
|
||||
settings('MinimumSetup', "")
|
||||
settings('MusicRescan.bool', False)
|
||||
settings('SyncInstallRunDone.bool', False)
|
||||
settings("enableMusic.bool", False)
|
||||
settings("MinimumSetup", "")
|
||||
settings("MusicRescan.bool", False)
|
||||
settings("SyncInstallRunDone.bool", False)
|
||||
dialog("ok", "{jellyfin}", translate(33088))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
xbmc.executebuiltin("RestartApp")
|
||||
|
||||
|
||||
def reset_kodi():
|
||||
@ -256,18 +264,20 @@ def reset_kodi():
|
||||
name = table[0]
|
||||
|
||||
# These tables are populated by Kodi and we shouldn't wipe them
|
||||
if name not in ['version', 'videoversiontype']:
|
||||
if name not in ["version", "videoversiontype"]:
|
||||
videodb.cursor.execute("DELETE FROM " + name)
|
||||
|
||||
if settings('enableMusic.bool') or dialog("yesno", "{jellyfin}", translate(33162)):
|
||||
if settings("enableMusic.bool") or dialog("yesno", "{jellyfin}", translate(33162)):
|
||||
|
||||
with Database('music') as musicdb:
|
||||
musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
||||
with Database("music") as musicdb:
|
||||
musicdb.cursor.execute(
|
||||
"SELECT tbl_name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
|
||||
for table in musicdb.cursor.fetchall():
|
||||
name = table[0]
|
||||
|
||||
if name != 'version':
|
||||
if name != "version":
|
||||
musicdb.cursor.execute("DELETE FROM " + name)
|
||||
|
||||
LOG.info("[ reset kodi ]")
|
||||
@ -275,13 +285,15 @@ def reset_kodi():
|
||||
|
||||
def reset_jellyfin():
|
||||
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
jellyfindb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
jellyfindb.cursor.execute(
|
||||
"SELECT tbl_name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
|
||||
for table in jellyfindb.cursor.fetchall():
|
||||
name = table[0]
|
||||
|
||||
if name not in ('version', 'view'):
|
||||
if name not in ("version", "view"):
|
||||
jellyfindb.cursor.execute("DELETE FROM " + name)
|
||||
|
||||
jellyfindb.cursor.execute("DROP table IF EXISTS jellyfin")
|
||||
@ -292,10 +304,8 @@ def reset_jellyfin():
|
||||
|
||||
|
||||
def reset_artwork():
|
||||
|
||||
''' Remove all existing texture.
|
||||
'''
|
||||
thumbnails = translate_path('special://thumbnails/')
|
||||
"""Remove all existing texture."""
|
||||
thumbnails = translate_path("special://thumbnails/")
|
||||
|
||||
if xbmcvfs.exists(thumbnails):
|
||||
dirs, ignore = xbmcvfs.listdir(thumbnails)
|
||||
@ -307,13 +317,13 @@ def reset_artwork():
|
||||
LOG.debug("DELETE thumbnail %s", thumb)
|
||||
xbmcvfs.delete(os.path.join(thumbnails, directory, thumb))
|
||||
|
||||
with Database('texture') as texdb:
|
||||
with Database("texture") as texdb:
|
||||
texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
||||
|
||||
for table in texdb.cursor.fetchall():
|
||||
name = table[0]
|
||||
|
||||
if name != 'version':
|
||||
if name != "version":
|
||||
texdb.cursor.execute("DELETE FROM " + name)
|
||||
|
||||
LOG.info("[ reset artwork ]")
|
||||
@ -327,18 +337,18 @@ def get_sync():
|
||||
xbmcvfs.mkdirs(ADDON_DATA)
|
||||
|
||||
try:
|
||||
with open(os.path.join(ADDON_DATA, 'sync.json'), 'rb') as infile:
|
||||
with open(os.path.join(ADDON_DATA, "sync.json"), "rb") as infile:
|
||||
sync = json.load(infile)
|
||||
except Exception:
|
||||
sync = {}
|
||||
|
||||
sync['Libraries'] = sync.get('Libraries', [])
|
||||
sync['RestorePoint'] = sync.get('RestorePoint', {})
|
||||
sync['Whitelist'] = list(set(sync.get('Whitelist', [])))
|
||||
sync['SortedViews'] = sync.get('SortedViews', [])
|
||||
sync["Libraries"] = sync.get("Libraries", [])
|
||||
sync["RestorePoint"] = sync.get("RestorePoint", {})
|
||||
sync["Whitelist"] = list(set(sync.get("Whitelist", [])))
|
||||
sync["SortedViews"] = sync.get("SortedViews", [])
|
||||
|
||||
# Temporary cleanup from #494/#511, remove in a future version
|
||||
sync['Libraries'] = [lib_id for lib_id in sync['Libraries'] if ',' not in lib_id]
|
||||
sync["Libraries"] = [lib_id for lib_id in sync["Libraries"] if "," not in lib_id]
|
||||
|
||||
return sync
|
||||
|
||||
@ -348,12 +358,12 @@ def save_sync(sync):
|
||||
if not xbmcvfs.exists(ADDON_DATA):
|
||||
xbmcvfs.mkdirs(ADDON_DATA)
|
||||
|
||||
sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
sync["Date"] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
with open(os.path.join(ADDON_DATA, 'sync.json'), 'wb') as outfile:
|
||||
with open(os.path.join(ADDON_DATA, "sync.json"), "wb") as outfile:
|
||||
data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False)
|
||||
if isinstance(data, text_type):
|
||||
data = data.encode('utf-8')
|
||||
data = data.encode("utf-8")
|
||||
outfile.write(data)
|
||||
|
||||
|
||||
@ -365,30 +375,30 @@ def get_credentials():
|
||||
xbmcvfs.mkdirs(ADDON_DATA)
|
||||
|
||||
try:
|
||||
with open(os.path.join(ADDON_DATA, 'data.json'), 'rb') as infile:
|
||||
with open(os.path.join(ADDON_DATA, "data.json"), "rb") as infile:
|
||||
credentials = json.load(infile)
|
||||
except IOError:
|
||||
credentials = {}
|
||||
|
||||
credentials['Servers'] = credentials.get('Servers', [])
|
||||
credentials["Servers"] = credentials.get("Servers", [])
|
||||
|
||||
# Migration for #145
|
||||
# TODO: CLEANUP for 1.0.0 release
|
||||
for server in credentials['Servers']:
|
||||
for server in credentials["Servers"]:
|
||||
# Functionality removed in #60
|
||||
if 'RemoteAddress' in server:
|
||||
del server['RemoteAddress']
|
||||
if 'ManualAddress' in server:
|
||||
server['address'] = server['ManualAddress']
|
||||
del server['ManualAddress']
|
||||
if "RemoteAddress" in server:
|
||||
del server["RemoteAddress"]
|
||||
if "ManualAddress" in server:
|
||||
server["address"] = server["ManualAddress"]
|
||||
del server["ManualAddress"]
|
||||
# If manual is present, local should always be here, but better to be safe
|
||||
if 'LocalAddress' in server:
|
||||
del server['LocalAddress']
|
||||
elif 'LocalAddress' in server:
|
||||
server['address'] = server['LocalAddress']
|
||||
del server['LocalAddress']
|
||||
if 'LastConnectionMode' in server:
|
||||
del server['LastConnectionMode']
|
||||
if "LocalAddress" in server:
|
||||
del server["LocalAddress"]
|
||||
elif "LocalAddress" in server:
|
||||
server["address"] = server["LocalAddress"]
|
||||
del server["LocalAddress"]
|
||||
if "LastConnectionMode" in server:
|
||||
del server["LastConnectionMode"]
|
||||
|
||||
return credentials
|
||||
|
||||
@ -399,21 +409,21 @@ def save_credentials(credentials):
|
||||
if not xbmcvfs.exists(ADDON_DATA):
|
||||
xbmcvfs.mkdirs(ADDON_DATA)
|
||||
try:
|
||||
with open(os.path.join(ADDON_DATA, 'data.json'), 'wb') as outfile:
|
||||
with open(os.path.join(ADDON_DATA, "data.json"), "wb") as outfile:
|
||||
data = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False)
|
||||
if isinstance(data, text_type):
|
||||
data = data.encode('utf-8')
|
||||
data = data.encode("utf-8")
|
||||
outfile.write(data)
|
||||
except Exception:
|
||||
LOG.exception("Failed to save credentials:")
|
||||
|
||||
|
||||
def get_item(kodi_id, media):
|
||||
|
||||
''' Get jellyfin item based on kodi id and media.
|
||||
'''
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(kodi_id, media)
|
||||
"""Get jellyfin item based on kodi id and media."""
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(
|
||||
kodi_id, media
|
||||
)
|
||||
|
||||
if not item:
|
||||
LOG.debug("Not an jellyfin item")
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
#################################################################################################
|
||||
|
||||
from . import queries as QU
|
||||
@ -14,7 +15,7 @@ LOG = LazyLogger(__name__)
|
||||
##################################################################################################
|
||||
|
||||
|
||||
class JellyfinDatabase():
|
||||
class JellyfinDatabase:
|
||||
|
||||
def __init__(self, cursor):
|
||||
self.cursor = cursor
|
||||
@ -32,9 +33,7 @@ class JellyfinDatabase():
|
||||
self.cursor.execute(QU.update_reference, args)
|
||||
|
||||
def update_parent_id(self, *args):
|
||||
|
||||
''' Parent_id is the parent Kodi id.
|
||||
'''
|
||||
"""Parent_id is the parent Kodi id."""
|
||||
self.cursor.execute(QU.update_parent, args)
|
||||
|
||||
def get_item_id_by_parent_id(self, *args):
|
||||
@ -160,8 +159,8 @@ class JellyfinDatabase():
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def add_version(self, *args):
|
||||
'''
|
||||
"""
|
||||
We only ever want one value here, so erase the existing contents first
|
||||
'''
|
||||
"""
|
||||
self.cursor.execute(QU.delete_version)
|
||||
self.cursor.execute(QU.add_version, args)
|
||||
|
@ -94,16 +94,126 @@ INSERT OR REPLACE INTO jellyfin(jellyfin_id, kodi_id, kodi_fileid, kodi_pat
|
||||
media_type, parent_id, checksum, media_folder, jellyfin_parent_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_reference_movie_obj = ["{Id}", "{MovieId}", "{FileId}", "{PathId}", "Movie", "movie", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_boxset_obj = ["{Id}", "{SetId}", None, None, "BoxSet", "set", None, "{Checksum}", None, None]
|
||||
add_reference_tvshow_obj = ["{Id}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_season_obj = ["{Id}", "{SeasonId}", None, None, "Season", "season", "{ShowId}", None, None, None]
|
||||
add_reference_pool_obj = ["{SeriesId}", "{ShowId}", None, "{PathId}", "Series", "tvshow", None, "{Checksum}", "{LibraryId}", None]
|
||||
add_reference_episode_obj = ["{Id}", "{EpisodeId}", "{FileId}", "{PathId}", "Episode", "episode", "{SeasonId}", "{Checksum}", None, "{JellyfinParentId}"]
|
||||
add_reference_mvideo_obj = ["{Id}", "{MvideoId}", "{FileId}", "{PathId}", "MusicVideo", "musicvideo", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_artist_obj = ["{Id}", "{ArtistId}", None, None, "{ArtistType}", "artist", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_album_obj = ["{Id}", "{AlbumId}", None, None, "MusicAlbum", "album", None, "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_song_obj = ["{Id}", "{SongId}", None, "{PathId}", "Audio", "song", "{AlbumId}", "{Checksum}", "{LibraryId}", "{JellyfinParentId}"]
|
||||
add_reference_movie_obj = [
|
||||
"{Id}",
|
||||
"{MovieId}",
|
||||
"{FileId}",
|
||||
"{PathId}",
|
||||
"Movie",
|
||||
"movie",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_boxset_obj = [
|
||||
"{Id}",
|
||||
"{SetId}",
|
||||
None,
|
||||
None,
|
||||
"BoxSet",
|
||||
"set",
|
||||
None,
|
||||
"{Checksum}",
|
||||
None,
|
||||
None,
|
||||
]
|
||||
add_reference_tvshow_obj = [
|
||||
"{Id}",
|
||||
"{ShowId}",
|
||||
None,
|
||||
"{PathId}",
|
||||
"Series",
|
||||
"tvshow",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_season_obj = [
|
||||
"{Id}",
|
||||
"{SeasonId}",
|
||||
None,
|
||||
None,
|
||||
"Season",
|
||||
"season",
|
||||
"{ShowId}",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
add_reference_pool_obj = [
|
||||
"{SeriesId}",
|
||||
"{ShowId}",
|
||||
None,
|
||||
"{PathId}",
|
||||
"Series",
|
||||
"tvshow",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
None,
|
||||
]
|
||||
add_reference_episode_obj = [
|
||||
"{Id}",
|
||||
"{EpisodeId}",
|
||||
"{FileId}",
|
||||
"{PathId}",
|
||||
"Episode",
|
||||
"episode",
|
||||
"{SeasonId}",
|
||||
"{Checksum}",
|
||||
None,
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_mvideo_obj = [
|
||||
"{Id}",
|
||||
"{MvideoId}",
|
||||
"{FileId}",
|
||||
"{PathId}",
|
||||
"MusicVideo",
|
||||
"musicvideo",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_artist_obj = [
|
||||
"{Id}",
|
||||
"{ArtistId}",
|
||||
None,
|
||||
None,
|
||||
"{ArtistType}",
|
||||
"artist",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_album_obj = [
|
||||
"{Id}",
|
||||
"{AlbumId}",
|
||||
None,
|
||||
None,
|
||||
"MusicAlbum",
|
||||
"album",
|
||||
None,
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_reference_song_obj = [
|
||||
"{Id}",
|
||||
"{SongId}",
|
||||
None,
|
||||
"{PathId}",
|
||||
"Audio",
|
||||
"song",
|
||||
"{AlbumId}",
|
||||
"{Checksum}",
|
||||
"{LibraryId}",
|
||||
"{JellyfinParentId}",
|
||||
]
|
||||
add_view = """
|
||||
INSERT OR REPLACE INTO view(view_id, view_name, media_type)
|
||||
VALUES (?, ?, ?)
|
||||
|
@ -47,8 +47,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def onInit(self):
|
||||
|
||||
if window('JellyfinUserImage'):
|
||||
self.getControl(USER_IMAGE).setImage(window('JellyfinUserImage'))
|
||||
if window("JellyfinUserImage"):
|
||||
self.getControl(USER_IMAGE).setImage(window("JellyfinUserImage"))
|
||||
|
||||
LOG.info("options: %s", self._options)
|
||||
self.list_ = self.getControl(LIST)
|
||||
@ -63,21 +63,35 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||
self.close()
|
||||
|
||||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
|
||||
if (
|
||||
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
|
||||
and self.getFocusId() == LIST
|
||||
):
|
||||
|
||||
option = self.list_.getSelectedItem()
|
||||
self.selected_option = ensure_text(option.getLabel())
|
||||
LOG.info('option selected: %s', self.selected_option)
|
||||
LOG.info("option selected: %s", self.selected_option)
|
||||
|
||||
self.close()
|
||||
|
||||
def _add_editcontrol(self, x, y, height, width, password=0):
|
||||
|
||||
media = os.path.join(xbmcaddon.Addon(addon_id()).getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
|
||||
control = xbmcgui.ControlImage(0, 0, 0, 0,
|
||||
filename=os.path.join(media, "white.png"),
|
||||
aspectRatio=0,
|
||||
colorDiffuse="ff111111")
|
||||
media = os.path.join(
|
||||
xbmcaddon.Addon(addon_id()).getAddonInfo("path"),
|
||||
"resources",
|
||||
"skins",
|
||||
"default",
|
||||
"media",
|
||||
)
|
||||
control = xbmcgui.ControlImage(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
filename=os.path.join(media, "white.png"),
|
||||
aspectRatio=0,
|
||||
colorDiffuse="ff111111",
|
||||
)
|
||||
control.setPosition(x, y)
|
||||
control.setHeight(height)
|
||||
control.setWidth(width)
|
||||
|
@ -18,7 +18,7 @@ SIGN_IN = 200
|
||||
CANCEL = 201
|
||||
ERROR_TOGGLE = 202
|
||||
ERROR_MSG = 203
|
||||
ERROR = {'Invalid': 1, 'Empty': 2}
|
||||
ERROR = {"Invalid": 1, "Empty": 2}
|
||||
|
||||
##################################################################################################
|
||||
|
||||
@ -76,7 +76,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
if not user:
|
||||
# Display error
|
||||
self._error(ERROR['Empty'], translate('empty_user'))
|
||||
self._error(ERROR["Empty"], translate("empty_user"))
|
||||
LOG.error("Username cannot be null")
|
||||
|
||||
elif self._login(user, password):
|
||||
@ -88,7 +88,7 @@ class LoginManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if self.error == ERROR['Empty'] and self.user_field.getText():
|
||||
if self.error == ERROR["Empty"] and self.user_field.getText():
|
||||
self._disable_error()
|
||||
|
||||
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||
@ -102,12 +102,12 @@ class LoginManual(xbmcgui.WindowXMLDialog):
|
||||
textColor="FF00A4DC",
|
||||
disabledColor="FF888888",
|
||||
focusTexture="-",
|
||||
noFocusTexture="-"
|
||||
noFocusTexture="-",
|
||||
)
|
||||
|
||||
# TODO: Kodi 17 compat removal cleanup
|
||||
if kodi_version() < 18:
|
||||
kwargs['isPassword'] = password
|
||||
kwargs["isPassword"] = password
|
||||
|
||||
control = xbmcgui.ControlEdit(0, 0, 0, 0, **kwargs)
|
||||
|
||||
@ -126,11 +126,13 @@ class LoginManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def _login(self, username, password):
|
||||
|
||||
server = self.connect_manager.get_server_info(self.connect_manager.server_id)['address']
|
||||
server = self.connect_manager.get_server_info(self.connect_manager.server_id)[
|
||||
"address"
|
||||
]
|
||||
result = self.connect_manager.login(server, username, password)
|
||||
|
||||
if not result:
|
||||
self._error(ERROR['Invalid'], translate('invalid_auth'))
|
||||
self._error(ERROR["Invalid"], translate("invalid_auth"))
|
||||
return False
|
||||
else:
|
||||
self._user = result
|
||||
@ -140,9 +142,9 @@ class LoginManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
self.error = state
|
||||
self.error_msg.setLabel(message)
|
||||
self.error_toggle.setVisibleCondition('true')
|
||||
self.error_toggle.setVisibleCondition("true")
|
||||
|
||||
def _disable_error(self):
|
||||
|
||||
self.error = None
|
||||
self.error_toggle.setVisibleCondition('false')
|
||||
self.error_toggle.setVisibleCondition("false")
|
||||
|
@ -64,8 +64,10 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||
self.list_ = self.getControl(LIST)
|
||||
|
||||
for server in self.servers:
|
||||
server_type = "wifi" if server.get('ExchangeToken') else "network"
|
||||
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type))
|
||||
server_type = "wifi" if server.get("ExchangeToken") else "network"
|
||||
self.list_.addItem(
|
||||
self._add_listitem(server["Name"], server["Id"], server_type)
|
||||
)
|
||||
|
||||
if self.user_image is not None:
|
||||
self.getControl(USER_IMAGE).setImage(self.user_image)
|
||||
@ -77,8 +79,8 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||
def _add_listitem(cls, label, server_id, server_type):
|
||||
|
||||
item = xbmcgui.ListItem(label)
|
||||
item.setProperty('id', server_id)
|
||||
item.setProperty('server_type', server_type)
|
||||
item.setProperty("id", server_id)
|
||||
item.setProperty("server_type", server_type)
|
||||
|
||||
return item
|
||||
|
||||
@ -87,14 +89,17 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
|
||||
self.close()
|
||||
|
||||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
|
||||
if (
|
||||
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
|
||||
and self.getFocusId() == LIST
|
||||
):
|
||||
|
||||
server = self.list_.getSelectedItem()
|
||||
selected_id = server.getProperty('id')
|
||||
LOG.info('Server Id selected: %s', selected_id)
|
||||
selected_id = server.getProperty("id")
|
||||
LOG.info("Server Id selected: %s", selected_id)
|
||||
|
||||
if self._connect_server(selected_id):
|
||||
self.message_box.setVisibleCondition('false')
|
||||
self.message_box.setVisibleCondition("false")
|
||||
self.close()
|
||||
|
||||
def onClick(self, control):
|
||||
@ -109,19 +114,19 @@ class ServerConnect(xbmcgui.WindowXMLDialog):
|
||||
def _connect_server(self, server_id):
|
||||
|
||||
server = self.connect_manager.get_server_info(server_id)
|
||||
self.message.setLabel("%s %s..." % (translate(30610), server['Name']))
|
||||
self.message.setLabel("%s %s..." % (translate(30610), server["Name"]))
|
||||
|
||||
self.message_box.setVisibleCondition('true')
|
||||
self.busy.setVisibleCondition('true')
|
||||
self.message_box.setVisibleCondition("true")
|
||||
self.busy.setVisibleCondition("true")
|
||||
|
||||
result = self.connect_manager.connect_to_server(server)
|
||||
|
||||
if result['State'] == CONNECTION_STATE['Unavailable']:
|
||||
self.busy.setVisibleCondition('false')
|
||||
if result["State"] == CONNECTION_STATE["Unavailable"]:
|
||||
self.busy.setVisibleCondition("false")
|
||||
|
||||
self.message.setLabel(translate(30609))
|
||||
return False
|
||||
else:
|
||||
xbmc.sleep(1000)
|
||||
self._selected_server = result['Servers'][0]
|
||||
self._selected_server = result["Servers"][0]
|
||||
return True
|
||||
|
@ -22,10 +22,7 @@ CONNECT = 200
|
||||
CANCEL = 201
|
||||
ERROR_TOGGLE = 202
|
||||
ERROR_MSG = 203
|
||||
ERROR = {
|
||||
'Invalid': 1,
|
||||
'Empty': 2
|
||||
}
|
||||
ERROR = {"Invalid": 1, "Empty": 2}
|
||||
|
||||
# https://stackoverflow.com/a/17871737/1035647
|
||||
_IPV6_PATTERN = r"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
|
||||
@ -77,7 +74,7 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
if not server:
|
||||
# Display error
|
||||
self._error(ERROR['Empty'], translate('empty_server'))
|
||||
self._error(ERROR["Empty"], translate("empty_server"))
|
||||
LOG.error("Server cannot be null")
|
||||
|
||||
elif self._connect_to_server(server):
|
||||
@ -89,7 +86,7 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if self.error == ERROR['Empty'] and self.host_field.getText():
|
||||
if self.error == ERROR["Empty"] and self.host_field.getText():
|
||||
self._disable_error()
|
||||
|
||||
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
|
||||
@ -97,13 +94,18 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def _add_editcontrol(self, x, y, height, width):
|
||||
|
||||
control = xbmcgui.ControlEdit(0, 0, 0, 0,
|
||||
label="",
|
||||
font="font13",
|
||||
textColor="FF00A4DC",
|
||||
disabledColor="FF888888",
|
||||
focusTexture="-",
|
||||
noFocusTexture="-")
|
||||
control = xbmcgui.ControlEdit(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
label="",
|
||||
font="font13",
|
||||
textColor="FF00A4DC",
|
||||
disabledColor="FF888888",
|
||||
focusTexture="-",
|
||||
noFocusTexture="-",
|
||||
)
|
||||
control.setPosition(x, y)
|
||||
control.setHeight(height)
|
||||
control.setWidth(width)
|
||||
@ -118,25 +120,25 @@ class ServerManual(xbmcgui.WindowXMLDialog):
|
||||
self._message("%s %s..." % (translate(30610), server))
|
||||
result = self.connect_manager.connect_to_address(server)
|
||||
|
||||
if result['State'] == CONNECTION_STATE['Unavailable']:
|
||||
if result["State"] == CONNECTION_STATE["Unavailable"]:
|
||||
self._message(translate(30609))
|
||||
return False
|
||||
else:
|
||||
self._server = result['Servers'][0]
|
||||
self._server = result["Servers"][0]
|
||||
return True
|
||||
|
||||
def _message(self, message):
|
||||
|
||||
self.error_msg.setLabel(message)
|
||||
self.error_toggle.setVisibleCondition('true')
|
||||
self.error_toggle.setVisibleCondition("true")
|
||||
|
||||
def _error(self, state, message):
|
||||
|
||||
self.error = state
|
||||
self.error_msg.setLabel(message)
|
||||
self.error_toggle.setVisibleCondition('true')
|
||||
self.error_toggle.setVisibleCondition("true")
|
||||
|
||||
def _disable_error(self):
|
||||
|
||||
self.error = None
|
||||
self.error_toggle.setVisibleCondition('false')
|
||||
self.error_toggle.setVisibleCondition("false")
|
||||
|
@ -52,17 +52,20 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
|
||||
|
||||
self.list_ = self.getControl(LIST)
|
||||
for user in self.users:
|
||||
user_image = ("items/logindefault.png" if 'PrimaryImageTag' not in user
|
||||
else self._get_user_artwork(user['Id'], 'Primary'))
|
||||
self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image))
|
||||
user_image = (
|
||||
"items/logindefault.png"
|
||||
if "PrimaryImageTag" not in user
|
||||
else self._get_user_artwork(user["Id"], "Primary")
|
||||
)
|
||||
self.list_.addItem(self._add_listitem(user["Name"], user["Id"], user_image))
|
||||
|
||||
self.setFocus(self.list_)
|
||||
|
||||
def _add_listitem(self, label, user_id, user_image):
|
||||
|
||||
item = xbmcgui.ListItem(label)
|
||||
item.setProperty('id', user_id)
|
||||
item.setArt({'icon': user_image})
|
||||
item.setProperty("id", user_id)
|
||||
item.setArt({"icon": user_image})
|
||||
|
||||
return item
|
||||
|
||||
@ -71,14 +74,17 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
|
||||
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
|
||||
self.close()
|
||||
|
||||
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK) and self.getFocusId() == LIST:
|
||||
if (
|
||||
action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK)
|
||||
and self.getFocusId() == LIST
|
||||
):
|
||||
|
||||
user = self.list_.getSelectedItem()
|
||||
selected_id = user.getProperty('id')
|
||||
LOG.info('User Id selected: %s', selected_id)
|
||||
selected_id = user.getProperty("id")
|
||||
LOG.info("User Id selected: %s", selected_id)
|
||||
|
||||
for user in self.users:
|
||||
if user['Id'] == selected_id:
|
||||
if user["Id"] == selected_id:
|
||||
self._user = user
|
||||
break
|
||||
|
||||
@ -95,4 +101,8 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
|
||||
|
||||
def _get_user_artwork(self, user_id, item_type):
|
||||
# Load user information set by UserClient
|
||||
return "%s/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type)
|
||||
return "%s/Users/%s/Images/%s?Format=original" % (
|
||||
self.server,
|
||||
user_id,
|
||||
item_type,
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
def get_jellyfinserver_url(handler):
|
||||
|
||||
if handler.startswith('/'):
|
||||
if handler.startswith("/"):
|
||||
|
||||
handler = handler[1:]
|
||||
LOG.info("handler starts with /: %s", handler)
|
||||
@ -38,47 +38,55 @@ def _http(action, url, request=None, server_id=None):
|
||||
if request is None:
|
||||
request = {}
|
||||
|
||||
request.update({'url': url, 'type': action})
|
||||
request.update({"url": url, "type": action})
|
||||
return Jellyfin(server_id).http.request(request)
|
||||
|
||||
|
||||
def _get(handler, params=None, server_id=None):
|
||||
return _http("GET", get_jellyfinserver_url(handler), {'params': params}, server_id)
|
||||
return _http("GET", get_jellyfinserver_url(handler), {"params": params}, server_id)
|
||||
|
||||
|
||||
def _post(handler, json=None, params=None, server_id=None):
|
||||
return _http("POST", get_jellyfinserver_url(handler), {'params': params, 'json': json}, server_id)
|
||||
return _http(
|
||||
"POST",
|
||||
get_jellyfinserver_url(handler),
|
||||
{"params": params, "json": json},
|
||||
server_id,
|
||||
)
|
||||
|
||||
|
||||
def _delete(handler, params=None, server_id=None):
|
||||
return _http("DELETE", get_jellyfinserver_url(handler), {'params': params}, server_id)
|
||||
return _http(
|
||||
"DELETE", get_jellyfinserver_url(handler), {"params": params}, server_id
|
||||
)
|
||||
|
||||
|
||||
def validate_view(library_id, item_id):
|
||||
|
||||
''' This confirms a single item from the library matches the view it belongs to.
|
||||
Used to detect grouped libraries.
|
||||
'''
|
||||
"""This confirms a single item from the library matches the view it belongs to.
|
||||
Used to detect grouped libraries.
|
||||
"""
|
||||
try:
|
||||
result = _get("Users/{UserId}/Items", {
|
||||
'ParentId': library_id,
|
||||
'Recursive': True,
|
||||
'Ids': item_id
|
||||
})
|
||||
result = _get(
|
||||
"Users/{UserId}/Items",
|
||||
{"ParentId": library_id, "Recursive": True, "Ids": item_id},
|
||||
)
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
return False
|
||||
|
||||
return bool(len(result['Items']))
|
||||
return bool(len(result["Items"]))
|
||||
|
||||
|
||||
def get_single_item(parent_id, media):
|
||||
return _get("Users/{UserId}/Items", {
|
||||
'ParentId': parent_id,
|
||||
'Recursive': True,
|
||||
'Limit': 1,
|
||||
'IncludeItemTypes': media
|
||||
})
|
||||
return _get(
|
||||
"Users/{UserId}/Items",
|
||||
{
|
||||
"ParentId": parent_id,
|
||||
"Recursive": True,
|
||||
"Limit": 1,
|
||||
"IncludeItemTypes": media,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_movies_by_boxset(boxset_id):
|
||||
@ -90,13 +98,13 @@ def get_movies_by_boxset(boxset_id):
|
||||
def get_episode_by_show(show_id):
|
||||
|
||||
query = {
|
||||
'url': "Shows/%s/Episodes" % show_id,
|
||||
'params': {
|
||||
'EnableUserData': True,
|
||||
'EnableImages': True,
|
||||
'UserId': "{UserId}",
|
||||
'Fields': api.info()
|
||||
}
|
||||
"url": "Shows/%s/Episodes" % show_id,
|
||||
"params": {
|
||||
"EnableUserData": True,
|
||||
"EnableImages": True,
|
||||
"UserId": "{UserId}",
|
||||
"Fields": api.info(),
|
||||
},
|
||||
}
|
||||
for items in _get_items(query):
|
||||
yield items
|
||||
@ -105,14 +113,14 @@ def get_episode_by_show(show_id):
|
||||
def get_episode_by_season(show_id, season_id):
|
||||
|
||||
query = {
|
||||
'url': "Shows/%s/Episodes" % show_id,
|
||||
'params': {
|
||||
'SeasonId': season_id,
|
||||
'EnableUserData': True,
|
||||
'EnableImages': True,
|
||||
'UserId': "{UserId}",
|
||||
'Fields': api.info()
|
||||
}
|
||||
"url": "Shows/%s/Episodes" % show_id,
|
||||
"params": {
|
||||
"SeasonId": season_id,
|
||||
"EnableUserData": True,
|
||||
"EnableImages": True,
|
||||
"UserId": "{UserId}",
|
||||
"Fields": api.info(),
|
||||
},
|
||||
}
|
||||
for items in _get_items(query):
|
||||
yield items
|
||||
@ -123,41 +131,41 @@ def get_item_count(parent_id, item_type=None, params=None):
|
||||
url = "Users/{UserId}/Items"
|
||||
|
||||
query_params = {
|
||||
'ParentId': parent_id,
|
||||
'IncludeItemTypes': item_type,
|
||||
'EnableTotalRecordCount': True,
|
||||
'LocationTypes': "FileSystem,Remote,Offline",
|
||||
'Recursive': True,
|
||||
'Limit': 1
|
||||
"ParentId": parent_id,
|
||||
"IncludeItemTypes": item_type,
|
||||
"EnableTotalRecordCount": True,
|
||||
"LocationTypes": "FileSystem,Remote,Offline",
|
||||
"Recursive": True,
|
||||
"Limit": 1,
|
||||
}
|
||||
if params:
|
||||
query_params['params'].update(params)
|
||||
query_params["params"].update(params)
|
||||
|
||||
result = _get(url, query_params)
|
||||
|
||||
return result.get('TotalRecordCount', 1)
|
||||
return result.get("TotalRecordCount", 1)
|
||||
|
||||
|
||||
def get_items(parent_id, item_type=None, basic=False, params=None):
|
||||
|
||||
query = {
|
||||
'url': "Users/{UserId}/Items",
|
||||
'params': {
|
||||
'ParentId': parent_id,
|
||||
'IncludeItemTypes': item_type,
|
||||
'SortBy': "SortName",
|
||||
'SortOrder': "Ascending",
|
||||
'Fields': api.basic_info() if basic else api.info(),
|
||||
'CollapseBoxSetItems': False,
|
||||
'IsVirtualUnaired': False,
|
||||
'EnableTotalRecordCount': False,
|
||||
'LocationTypes': "FileSystem,Remote,Offline",
|
||||
'IsMissing': False,
|
||||
'Recursive': True
|
||||
}
|
||||
"url": "Users/{UserId}/Items",
|
||||
"params": {
|
||||
"ParentId": parent_id,
|
||||
"IncludeItemTypes": item_type,
|
||||
"SortBy": "SortName",
|
||||
"SortOrder": "Ascending",
|
||||
"Fields": api.basic_info() if basic else api.info(),
|
||||
"CollapseBoxSetItems": False,
|
||||
"IsVirtualUnaired": False,
|
||||
"EnableTotalRecordCount": False,
|
||||
"LocationTypes": "FileSystem,Remote,Offline",
|
||||
"IsMissing": False,
|
||||
"Recursive": True,
|
||||
},
|
||||
}
|
||||
if params:
|
||||
query['params'].update(params)
|
||||
query["params"].update(params)
|
||||
|
||||
for items in _get_items(query):
|
||||
yield items
|
||||
@ -166,20 +174,20 @@ def get_items(parent_id, item_type=None, basic=False, params=None):
|
||||
def get_artists(parent_id=None):
|
||||
|
||||
query = {
|
||||
'url': 'Artists',
|
||||
'params': {
|
||||
'UserId': "{UserId}",
|
||||
'ParentId': parent_id,
|
||||
'SortBy': "SortName",
|
||||
'SortOrder': "Ascending",
|
||||
'Fields': api.music_info(),
|
||||
'CollapseBoxSetItems': False,
|
||||
'IsVirtualUnaired': False,
|
||||
'EnableTotalRecordCount': False,
|
||||
'LocationTypes': "FileSystem,Remote,Offline",
|
||||
'IsMissing': False,
|
||||
'Recursive': True
|
||||
}
|
||||
"url": "Artists",
|
||||
"params": {
|
||||
"UserId": "{UserId}",
|
||||
"ParentId": parent_id,
|
||||
"SortBy": "SortName",
|
||||
"SortOrder": "Ascending",
|
||||
"Fields": api.music_info(),
|
||||
"CollapseBoxSetItems": False,
|
||||
"IsVirtualUnaired": False,
|
||||
"EnableTotalRecordCount": False,
|
||||
"LocationTypes": "FileSystem,Remote,Offline",
|
||||
"IsMissing": False,
|
||||
"Recursive": True,
|
||||
},
|
||||
}
|
||||
|
||||
for items in _get_items(query):
|
||||
@ -188,48 +196,49 @@ def get_artists(parent_id=None):
|
||||
|
||||
@stop
|
||||
def _get_items(query, server_id=None):
|
||||
|
||||
''' query = {
|
||||
'url': string,
|
||||
'params': dict -- opt, include StartIndex to resume
|
||||
}
|
||||
'''
|
||||
items = {
|
||||
'Items': [],
|
||||
'TotalRecordCount': 0,
|
||||
'RestorePoint': {}
|
||||
"""query = {
|
||||
'url': string,
|
||||
'params': dict -- opt, include StartIndex to resume
|
||||
}
|
||||
"""
|
||||
items = {"Items": [], "TotalRecordCount": 0, "RestorePoint": {}}
|
||||
|
||||
limit = min(int(settings('limitIndex') or 50), 50)
|
||||
dthreads = int(settings('limitThreads') or 3)
|
||||
limit = min(int(settings("limitIndex") or 50), 50)
|
||||
dthreads = int(settings("limitThreads") or 3)
|
||||
|
||||
url = query['url']
|
||||
query.setdefault('params', {})
|
||||
params = query['params']
|
||||
url = query["url"]
|
||||
query.setdefault("params", {})
|
||||
params = query["params"]
|
||||
|
||||
try:
|
||||
test_params = dict(params)
|
||||
test_params['Limit'] = 1
|
||||
test_params['EnableTotalRecordCount'] = True
|
||||
test_params["Limit"] = 1
|
||||
test_params["EnableTotalRecordCount"] = True
|
||||
|
||||
items['TotalRecordCount'] = _get(url, test_params, server_id=server_id)['TotalRecordCount']
|
||||
items["TotalRecordCount"] = _get(url, test_params, server_id=server_id)[
|
||||
"TotalRecordCount"
|
||||
]
|
||||
|
||||
except Exception as error:
|
||||
LOG.exception("Failed to retrieve the server response %s: %s params:%s", url, error, params)
|
||||
LOG.exception(
|
||||
"Failed to retrieve the server response %s: %s params:%s",
|
||||
url,
|
||||
error,
|
||||
params,
|
||||
)
|
||||
|
||||
else:
|
||||
params.setdefault('StartIndex', 0)
|
||||
params.setdefault("StartIndex", 0)
|
||||
|
||||
def get_query_params(params, start, count):
|
||||
params_copy = dict(params)
|
||||
params_copy['StartIndex'] = start
|
||||
params_copy['Limit'] = count
|
||||
params_copy["StartIndex"] = start
|
||||
params_copy["Limit"] = count
|
||||
return params_copy
|
||||
|
||||
query_params = [
|
||||
get_query_params(params, offset, limit)
|
||||
for offset
|
||||
in range(params['StartIndex'], items['TotalRecordCount'], limit)
|
||||
for offset in range(params["StartIndex"], items["TotalRecordCount"], limit)
|
||||
]
|
||||
|
||||
# multiprocessing.dummy.Pool completes all requests in multiple threads but has to
|
||||
@ -257,27 +266,29 @@ def _get_items(query, server_id=None):
|
||||
# process complete jobs
|
||||
for job in concurrent.futures.as_completed(jobs):
|
||||
# get the result
|
||||
result = job.result() or {'Items': []}
|
||||
query['params'] = jobs[job]
|
||||
result = job.result() or {"Items": []}
|
||||
query["params"] = jobs[job]
|
||||
|
||||
# free job memory
|
||||
del jobs[job]
|
||||
del job
|
||||
|
||||
# Mitigates #216 till the server validates the date provided is valid
|
||||
if result['Items'][0].get('ProductionYear'):
|
||||
if result["Items"][0].get("ProductionYear"):
|
||||
try:
|
||||
date(result['Items'][0]['ProductionYear'], 1, 1)
|
||||
date(result["Items"][0]["ProductionYear"], 1, 1)
|
||||
except ValueError:
|
||||
LOG.info('#216 mitigation triggered. Setting ProductionYear to None')
|
||||
result['Items'][0]['ProductionYear'] = None
|
||||
LOG.info(
|
||||
"#216 mitigation triggered. Setting ProductionYear to None"
|
||||
)
|
||||
result["Items"][0]["ProductionYear"] = None
|
||||
|
||||
items['Items'].extend(result['Items'])
|
||||
items["Items"].extend(result["Items"])
|
||||
# Using items to return data and communicate a restore point back to the callee is
|
||||
# a violation of the SRP. TODO: Separate responsibilities.
|
||||
items['RestorePoint'] = query
|
||||
items["RestorePoint"] = query
|
||||
yield items
|
||||
del items['Items'][:]
|
||||
del items["Items"][:]
|
||||
|
||||
# release the semaphore again
|
||||
thread_buffer.release()
|
||||
@ -307,25 +318,25 @@ class GetItemWorker(threading.Thread):
|
||||
return
|
||||
|
||||
request = {
|
||||
'type': "GET",
|
||||
'handler': "Users/{UserId}/Items",
|
||||
'params': {
|
||||
'Ids': ','.join(str(x) for x in item_ids),
|
||||
'Fields': api.info()
|
||||
}
|
||||
"type": "GET",
|
||||
"handler": "Users/{UserId}/Items",
|
||||
"params": {
|
||||
"Ids": ",".join(str(x) for x in item_ids),
|
||||
"Fields": api.info(),
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
result = self.server.http.request(request, s)
|
||||
|
||||
for item in result['Items']:
|
||||
for item in result["Items"]:
|
||||
|
||||
if item['Type'] in self.output:
|
||||
self.output[item['Type']].put(item)
|
||||
if item["Type"] in self.output:
|
||||
self.output[item["Type"]].put(item)
|
||||
except HTTPException as error:
|
||||
LOG.error("--[ http status: %s ]", error.status)
|
||||
|
||||
if error.status == 'ServerUnreachable':
|
||||
if error.status == "ServerUnreachable":
|
||||
self.is_done = True
|
||||
|
||||
break
|
||||
@ -335,5 +346,5 @@ class GetItemWorker(threading.Thread):
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
@ -17,14 +17,18 @@ from ..jellyfin import Jellyfin
|
||||
#################################################################################################
|
||||
|
||||
LOG = LazyLogger(__name__)
|
||||
XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i")
|
||||
XML_PATH = (
|
||||
xbmcaddon.Addon("plugin.video.jellyfin").getAddonInfo("path"),
|
||||
"default",
|
||||
"1080i",
|
||||
)
|
||||
OPTIONS = {
|
||||
'Refresh': translate(30410),
|
||||
'Delete': translate(30409),
|
||||
'Addon': translate(30408),
|
||||
'AddFav': translate(30405),
|
||||
'RemoveFav': translate(30406),
|
||||
'Transcode': translate(30412)
|
||||
"Refresh": translate(30410),
|
||||
"Delete": translate(30409),
|
||||
"Addon": translate(30408),
|
||||
"AddFav": translate(30405),
|
||||
"RemoveFav": translate(30406),
|
||||
"Transcode": translate(30412),
|
||||
}
|
||||
|
||||
#################################################################################################
|
||||
@ -39,31 +43,33 @@ class Context(object):
|
||||
try:
|
||||
self.kodi_id = sys.listitem.getVideoInfoTag().getDbId() or None
|
||||
self.media = self.get_media_type()
|
||||
self.server_id = sys.listitem.getProperty('jellyfinserver') or None
|
||||
self.server_id = sys.listitem.getProperty("jellyfinserver") or None
|
||||
self.api_client = Jellyfin(self.server_id).get_client().jellyfin
|
||||
item_id = sys.listitem.getProperty('jellyfinid')
|
||||
item_id = sys.listitem.getProperty("jellyfinid")
|
||||
except AttributeError:
|
||||
self.server_id = None
|
||||
|
||||
if xbmc.getInfoLabel('ListItem.Property(jellyfinid)'):
|
||||
item_id = xbmc.getInfoLabel('ListItem.Property(jellyfinid)')
|
||||
if xbmc.getInfoLabel("ListItem.Property(jellyfinid)"):
|
||||
item_id = xbmc.getInfoLabel("ListItem.Property(jellyfinid)")
|
||||
else:
|
||||
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID')
|
||||
self.media = xbmc.getInfoLabel('ListItem.DBTYPE')
|
||||
self.kodi_id = xbmc.getInfoLabel("ListItem.DBID")
|
||||
self.media = xbmc.getInfoLabel("ListItem.DBTYPE")
|
||||
item_id = None
|
||||
|
||||
addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin/data.json")
|
||||
with open(addon_data, 'rb') as infile:
|
||||
addon_data = translate_path(
|
||||
"special://profile/addon_data/plugin.video.jellyfin/data.json"
|
||||
)
|
||||
with open(addon_data, "rb") as infile:
|
||||
data = json.load(infile)
|
||||
|
||||
try:
|
||||
server_data = data['Servers'][0]
|
||||
self.api_client.config.data['auth.server'] = server_data.get('address')
|
||||
self.api_client.config.data['auth.server-name'] = server_data.get('Name')
|
||||
self.api_client.config.data['auth.user_id'] = server_data.get('UserId')
|
||||
self.api_client.config.data['auth.token'] = server_data.get('AccessToken')
|
||||
server_data = data["Servers"][0]
|
||||
self.api_client.config.data["auth.server"] = server_data.get("address")
|
||||
self.api_client.config.data["auth.server-name"] = server_data.get("Name")
|
||||
self.api_client.config.data["auth.user_id"] = server_data.get("UserId")
|
||||
self.api_client.config.data["auth.token"] = server_data.get("AccessToken")
|
||||
except Exception as e:
|
||||
LOG.warning('Addon appears to not be configured yet: {}'.format(e))
|
||||
LOG.warning("Addon appears to not be configured yet: {}".format(e))
|
||||
|
||||
if self.server_id or item_id:
|
||||
self.item = self.api_client.get_item(item_id)
|
||||
@ -81,26 +87,28 @@ class Context(object):
|
||||
elif self.select_menu():
|
||||
self.action_menu()
|
||||
|
||||
if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'], OPTIONS['RemoveFav']):
|
||||
if self._selected_option in (
|
||||
OPTIONS["Delete"],
|
||||
OPTIONS["AddFav"],
|
||||
OPTIONS["RemoveFav"],
|
||||
):
|
||||
|
||||
xbmc.sleep(500)
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
def get_media_type(self):
|
||||
|
||||
''' Get media type based on sys.listitem. If unfilled, base on visible window.
|
||||
'''
|
||||
"""Get media type based on sys.listitem. If unfilled, base on visible window."""
|
||||
media = sys.listitem.getVideoInfoTag().getMediaType()
|
||||
|
||||
if not media:
|
||||
|
||||
if xbmc.getCondVisibility('Container.Content(albums)'):
|
||||
if xbmc.getCondVisibility("Container.Content(albums)"):
|
||||
media = "album"
|
||||
elif xbmc.getCondVisibility('Container.Content(artists)'):
|
||||
elif xbmc.getCondVisibility("Container.Content(artists)"):
|
||||
media = "artist"
|
||||
elif xbmc.getCondVisibility('Container.Content(songs)'):
|
||||
elif xbmc.getCondVisibility("Container.Content(songs)"):
|
||||
media = "song"
|
||||
elif xbmc.getCondVisibility('Container.Content(pictures)'):
|
||||
elif xbmc.getCondVisibility("Container.Content(pictures)"):
|
||||
media = "picture"
|
||||
else:
|
||||
LOG.info("media is unknown")
|
||||
@ -108,40 +116,37 @@ class Context(object):
|
||||
return media
|
||||
|
||||
def get_item_id(self):
|
||||
|
||||
''' Get synced item from jellyfindb.
|
||||
'''
|
||||
"""Get synced item from jellyfindb."""
|
||||
item = database.get_item(self.kodi_id, self.media)
|
||||
|
||||
if not item:
|
||||
return
|
||||
|
||||
return {
|
||||
'Id': item[0],
|
||||
'UserData': json.loads(item[4]) if item[4] else {},
|
||||
'Type': item[3]
|
||||
"Id": item[0],
|
||||
"UserData": json.loads(item[4]) if item[4] else {},
|
||||
"Type": item[3],
|
||||
}
|
||||
|
||||
def select_menu(self):
|
||||
|
||||
''' Display the select dialog.
|
||||
Favorites, Refresh, Delete (opt), Settings.
|
||||
'''
|
||||
"""Display the select dialog.
|
||||
Favorites, Refresh, Delete (opt), Settings.
|
||||
"""
|
||||
options = []
|
||||
|
||||
if self.item['Type'] != 'Season':
|
||||
if self.item["Type"] != "Season":
|
||||
|
||||
if self.item['UserData'].get('IsFavorite'):
|
||||
options.append(OPTIONS['RemoveFav'])
|
||||
if self.item["UserData"].get("IsFavorite"):
|
||||
options.append(OPTIONS["RemoveFav"])
|
||||
else:
|
||||
options.append(OPTIONS['AddFav'])
|
||||
options.append(OPTIONS["AddFav"])
|
||||
|
||||
options.append(OPTIONS['Refresh'])
|
||||
options.append(OPTIONS["Refresh"])
|
||||
|
||||
if settings('enableContextDelete.bool'):
|
||||
options.append(OPTIONS['Delete'])
|
||||
if settings("enableContextDelete.bool"):
|
||||
options.append(OPTIONS["Delete"])
|
||||
|
||||
options.append(OPTIONS['Addon'])
|
||||
options.append(OPTIONS["Addon"])
|
||||
|
||||
context_menu = context.ContextMenu("script-jellyfin-context.xml", *XML_PATH)
|
||||
context_menu.set_options(options)
|
||||
@ -156,24 +161,26 @@ class Context(object):
|
||||
|
||||
selected = self._selected_option
|
||||
|
||||
if selected == OPTIONS['Refresh']:
|
||||
self.api_client.refresh_item(self.item['Id'])
|
||||
if selected == OPTIONS["Refresh"]:
|
||||
self.api_client.refresh_item(self.item["Id"])
|
||||
|
||||
elif selected == OPTIONS['AddFav']:
|
||||
self.api_client.favorite(self.item['Id'], True)
|
||||
elif selected == OPTIONS["AddFav"]:
|
||||
self.api_client.favorite(self.item["Id"], True)
|
||||
|
||||
elif selected == OPTIONS['RemoveFav']:
|
||||
self.api_client.favorite(self.item['Id'], False)
|
||||
elif selected == OPTIONS["RemoveFav"]:
|
||||
self.api_client.favorite(self.item["Id"], False)
|
||||
|
||||
elif selected == OPTIONS['Addon']:
|
||||
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.jellyfin)')
|
||||
elif selected == OPTIONS["Addon"]:
|
||||
xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)")
|
||||
|
||||
elif selected == OPTIONS['Delete']:
|
||||
elif selected == OPTIONS["Delete"]:
|
||||
self.delete_item()
|
||||
|
||||
def delete_item(self):
|
||||
if settings('skipContextMenu.bool') or dialog("yesno", "{jellyfin}", translate(33015)):
|
||||
self.api_client.delete_item(self.item['Id'])
|
||||
if settings("skipContextMenu.bool") or dialog(
|
||||
"yesno", "{jellyfin}", translate(33015)
|
||||
):
|
||||
self.api_client.delete_item(self.item["Id"])
|
||||
|
||||
def transcode(self):
|
||||
filename = xbmc.getInfoLabel("ListItem.Filenameandpath")
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,15 @@ from .. import client
|
||||
from .. import library
|
||||
from .. import monitor
|
||||
from ..views import Views
|
||||
from ..helper import translate, window, settings, event, dialog, set_addon_mode, LazyLogger
|
||||
from ..helper import (
|
||||
translate,
|
||||
window,
|
||||
settings,
|
||||
event,
|
||||
dialog,
|
||||
set_addon_mode,
|
||||
LazyLogger,
|
||||
)
|
||||
from ..helper.utils import JsonDebugPrinter, translate_path
|
||||
from ..helper.xmls import verify_kodi_defaults
|
||||
from ..jellyfin import Jellyfin
|
||||
@ -37,83 +45,98 @@ class Service(xbmc.Monitor):
|
||||
monitor = None
|
||||
play_event = None
|
||||
warn = True
|
||||
settings = {'last_progress': datetime.today(), 'last_progress_report': datetime.today()}
|
||||
settings = {
|
||||
"last_progress": datetime.today(),
|
||||
"last_progress_report": datetime.today(),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
window('jellyfin_should_stop', clear=True)
|
||||
window("jellyfin_should_stop", clear=True)
|
||||
|
||||
self.settings['addon_version'] = client.get_version()
|
||||
self.settings['profile'] = translate_path('special://profile')
|
||||
self.settings['mode'] = settings('useDirectPaths')
|
||||
self.settings['log_level'] = settings('logLevel') or "1"
|
||||
self.settings['auth_check'] = True
|
||||
self.settings['enable_context'] = settings('enableContext.bool')
|
||||
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool')
|
||||
self.settings['kodi_companion'] = settings('kodiCompanion.bool')
|
||||
window('jellyfin_kodiProfile', value=self.settings['profile'])
|
||||
settings('platformDetected', client.get_platform())
|
||||
self.settings["addon_version"] = client.get_version()
|
||||
self.settings["profile"] = translate_path("special://profile")
|
||||
self.settings["mode"] = settings("useDirectPaths")
|
||||
self.settings["log_level"] = settings("logLevel") or "1"
|
||||
self.settings["auth_check"] = True
|
||||
self.settings["enable_context"] = settings("enableContext.bool")
|
||||
self.settings["enable_context_transcode"] = settings(
|
||||
"enableContextTranscode.bool"
|
||||
)
|
||||
self.settings["kodi_companion"] = settings("kodiCompanion.bool")
|
||||
window("jellyfin_kodiProfile", value=self.settings["profile"])
|
||||
settings("platformDetected", client.get_platform())
|
||||
|
||||
if self.settings['enable_context']:
|
||||
window('jellyfin_context.bool', True)
|
||||
if self.settings['enable_context_transcode']:
|
||||
window('jellyfin_context_transcode.bool', True)
|
||||
if self.settings["enable_context"]:
|
||||
window("jellyfin_context.bool", True)
|
||||
if self.settings["enable_context_transcode"]:
|
||||
window("jellyfin_context_transcode.bool", True)
|
||||
|
||||
LOG.info("--->>>[ %s ]", client.get_addon_name())
|
||||
LOG.info("Version: %s", client.get_version())
|
||||
LOG.info("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion'))
|
||||
LOG.info("Platform: %s", settings('platformDetected'))
|
||||
LOG.info("KODI Version: %s", xbmc.getInfoLabel("System.BuildVersion"))
|
||||
LOG.info("Platform: %s", settings("platformDetected"))
|
||||
LOG.info("Python Version: %s", sys.version)
|
||||
LOG.info("Using dynamic paths: %s", settings('useDirectPaths') == "0")
|
||||
LOG.info("Log Level: %s", self.settings['log_level'])
|
||||
LOG.info("Using dynamic paths: %s", settings("useDirectPaths") == "0")
|
||||
LOG.info("Log Level: %s", self.settings["log_level"])
|
||||
|
||||
verify_kodi_defaults()
|
||||
|
||||
window('jellyfin.connected.bool', True)
|
||||
settings('groupedSets.bool', objects.utils.get_grouped_set())
|
||||
window("jellyfin.connected.bool", True)
|
||||
settings("groupedSets.bool", objects.utils.get_grouped_set())
|
||||
xbmc.Monitor.__init__(self)
|
||||
|
||||
def service(self):
|
||||
"""Keeps the service monitor going.
|
||||
Exit on Kodi shutdown or profile switch.
|
||||
|
||||
''' Keeps the service monitor going.
|
||||
Exit on Kodi shutdown or profile switch.
|
||||
|
||||
if profile switch happens more than once,
|
||||
Threads depending on abortRequest will not trigger.
|
||||
'''
|
||||
if profile switch happens more than once,
|
||||
Threads depending on abortRequest will not trigger.
|
||||
"""
|
||||
self.monitor = monitor.Monitor()
|
||||
player = self.monitor.player
|
||||
self.connect = connect.Connect()
|
||||
self.start_default()
|
||||
|
||||
self.settings['mode'] = settings('useDirectPaths')
|
||||
self.settings["mode"] = settings("useDirectPaths")
|
||||
|
||||
while self.running:
|
||||
if window('jellyfin_online.bool'):
|
||||
if window("jellyfin_online.bool"):
|
||||
|
||||
if self.settings['profile'] != window('jellyfin_kodiProfile'):
|
||||
LOG.info("[ profile switch ] %s", self.settings['profile'])
|
||||
if self.settings["profile"] != window("jellyfin_kodiProfile"):
|
||||
LOG.info("[ profile switch ] %s", self.settings["profile"])
|
||||
|
||||
break
|
||||
|
||||
if player.isPlaying() and player.is_playing_file(player.get_playing_file()):
|
||||
difference = datetime.today() - self.settings['last_progress']
|
||||
if player.isPlaying() and player.is_playing_file(
|
||||
player.get_playing_file()
|
||||
):
|
||||
difference = datetime.today() - self.settings["last_progress"]
|
||||
|
||||
if difference.seconds > 10:
|
||||
self.settings['last_progress'] = datetime.today()
|
||||
self.settings["last_progress"] = datetime.today()
|
||||
|
||||
update = (datetime.today() - self.settings['last_progress_report']).seconds > 250
|
||||
event('ReportProgressRequested', {'Report': update})
|
||||
update = (
|
||||
datetime.today() - self.settings["last_progress_report"]
|
||||
).seconds > 250
|
||||
event("ReportProgressRequested", {"Report": update})
|
||||
|
||||
if update:
|
||||
self.settings['last_progress_report'] = datetime.today()
|
||||
self.settings["last_progress_report"] = datetime.today()
|
||||
|
||||
if window('jellyfin.restart.bool'):
|
||||
if window("jellyfin.restart.bool"):
|
||||
|
||||
window('jellyfin.restart', clear=True)
|
||||
dialog("notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False)
|
||||
window("jellyfin.restart", clear=True)
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message=translate(33193),
|
||||
icon="{jellyfin}",
|
||||
time=1000,
|
||||
sound=False,
|
||||
)
|
||||
|
||||
raise Exception('RestartService')
|
||||
raise Exception("RestartService")
|
||||
|
||||
if self.waitForAbort(1):
|
||||
break
|
||||
@ -126,14 +149,14 @@ class Service(xbmc.Monitor):
|
||||
|
||||
try:
|
||||
self.connect.register()
|
||||
if not settings('SyncInstallRunDone.bool'):
|
||||
if not settings("SyncInstallRunDone.bool"):
|
||||
set_addon_mode()
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
def stop_default(self):
|
||||
|
||||
window('jellyfin_online', clear=True)
|
||||
window("jellyfin_online", clear=True)
|
||||
Jellyfin().close()
|
||||
|
||||
if self.library_thread is not None:
|
||||
@ -142,59 +165,93 @@ class Service(xbmc.Monitor):
|
||||
self.library_thread = None
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
|
||||
''' All notifications are sent via NotifyAll built-in or Kodi.
|
||||
Central hub.
|
||||
'''
|
||||
if sender.lower() not in ('plugin.video.jellyfin', 'xbmc'):
|
||||
"""All notifications are sent via NotifyAll built-in or Kodi.
|
||||
Central hub.
|
||||
"""
|
||||
if sender.lower() not in ("plugin.video.jellyfin", "xbmc"):
|
||||
return
|
||||
|
||||
if sender == 'plugin.video.jellyfin':
|
||||
method = method.split('.')[1]
|
||||
if sender == "plugin.video.jellyfin":
|
||||
method = method.split(".")[1]
|
||||
|
||||
if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect',
|
||||
'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary',
|
||||
'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer',
|
||||
'Unauthorized', 'UserConfigurationUpdated', 'ServerRestarting',
|
||||
'RemoveServer', 'UpdatePassword', 'AddLibrarySelection', 'RemoveLibrarySelection'):
|
||||
if method not in (
|
||||
"ServerUnreachable",
|
||||
"ServerShuttingDown",
|
||||
"UserDataChanged",
|
||||
"ServerConnect",
|
||||
"LibraryChanged",
|
||||
"ServerOnline",
|
||||
"SyncLibrary",
|
||||
"RepairLibrary",
|
||||
"RemoveLibrary",
|
||||
"SyncLibrarySelection",
|
||||
"RepairLibrarySelection",
|
||||
"AddServer",
|
||||
"Unauthorized",
|
||||
"UserConfigurationUpdated",
|
||||
"ServerRestarting",
|
||||
"RemoveServer",
|
||||
"UpdatePassword",
|
||||
"AddLibrarySelection",
|
||||
"RemoveLibrarySelection",
|
||||
):
|
||||
return
|
||||
|
||||
data = json.loads(data)[0]
|
||||
else:
|
||||
if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'):
|
||||
if method not in ("System.OnQuit", "System.OnSleep", "System.OnWake"):
|
||||
return
|
||||
|
||||
data = json.loads(data)
|
||||
|
||||
LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data))
|
||||
|
||||
if method == 'ServerOnline':
|
||||
if data.get('ServerId') is None:
|
||||
if method == "ServerOnline":
|
||||
if data.get("ServerId") is None:
|
||||
|
||||
window('jellyfin_online.bool', True)
|
||||
self.settings['auth_check'] = True
|
||||
window("jellyfin_online.bool", True)
|
||||
self.settings["auth_check"] = True
|
||||
self.warn = True
|
||||
|
||||
if settings('connectMsg.bool'):
|
||||
if settings("connectMsg.bool"):
|
||||
|
||||
users = [user for user in (settings('additionalUsers') or "").split(',') if user]
|
||||
users.insert(0, settings('username'))
|
||||
dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33000), ", ".join(users)),
|
||||
icon="{jellyfin}", time=1500, sound=False)
|
||||
users = [
|
||||
user
|
||||
for user in (settings("additionalUsers") or "").split(",")
|
||||
if user
|
||||
]
|
||||
users.insert(0, settings("username"))
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message="%s %s" % (translate(33000), ", ".join(users)),
|
||||
icon="{jellyfin}",
|
||||
time=1500,
|
||||
sound=False,
|
||||
)
|
||||
|
||||
if self.library_thread is None:
|
||||
|
||||
self.library_thread = library.Library(self)
|
||||
self.library_thread.start()
|
||||
|
||||
elif method in ('ServerUnreachable', 'ServerShuttingDown'):
|
||||
elif method in ("ServerUnreachable", "ServerShuttingDown"):
|
||||
|
||||
if self.warn or data.get('ServerId'):
|
||||
if self.warn or data.get("ServerId"):
|
||||
|
||||
self.warn = data.get('ServerId') is not None
|
||||
dialog("notification", heading="{jellyfin}", message=translate(33146) if data.get('ServerId') is None else translate(33149), icon=xbmcgui.NOTIFICATION_ERROR)
|
||||
self.warn = data.get("ServerId") is not None
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message=(
|
||||
translate(33146)
|
||||
if data.get("ServerId") is None
|
||||
else translate(33149)
|
||||
),
|
||||
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||
)
|
||||
|
||||
if data.get('ServerId') is None:
|
||||
if data.get("ServerId") is None:
|
||||
self.stop_default()
|
||||
|
||||
if self.waitForAbort(120):
|
||||
@ -202,12 +259,19 @@ class Service(xbmc.Monitor):
|
||||
|
||||
self.start_default()
|
||||
|
||||
elif method == 'Unauthorized':
|
||||
dialog("notification", heading="{jellyfin}", message=translate(33147) if data['ServerId'] is None else translate(33148), icon=xbmcgui.NOTIFICATION_ERROR)
|
||||
elif method == "Unauthorized":
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message=(
|
||||
translate(33147) if data["ServerId"] is None else translate(33148)
|
||||
),
|
||||
icon=xbmcgui.NOTIFICATION_ERROR,
|
||||
)
|
||||
|
||||
if data.get('ServerId') is None and self.settings['auth_check']:
|
||||
if data.get("ServerId") is None and self.settings["auth_check"]:
|
||||
|
||||
self.settings['auth_check'] = False
|
||||
self.settings["auth_check"] = False
|
||||
self.stop_default()
|
||||
|
||||
if self.waitForAbort(5):
|
||||
@ -215,12 +279,17 @@ class Service(xbmc.Monitor):
|
||||
|
||||
self.start_default()
|
||||
|
||||
elif method == 'ServerRestarting':
|
||||
if data.get('ServerId'):
|
||||
elif method == "ServerRestarting":
|
||||
if data.get("ServerId"):
|
||||
return
|
||||
|
||||
if settings('restartMsg.bool'):
|
||||
dialog("notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}")
|
||||
if settings("restartMsg.bool"):
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message=translate(33006),
|
||||
icon="{jellyfin}",
|
||||
)
|
||||
|
||||
self.stop_default()
|
||||
|
||||
@ -229,67 +298,72 @@ class Service(xbmc.Monitor):
|
||||
|
||||
self.start_default()
|
||||
|
||||
elif method == 'ServerConnect':
|
||||
self.connect.register(data['Id'])
|
||||
elif method == "ServerConnect":
|
||||
self.connect.register(data["Id"])
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'AddServer':
|
||||
elif method == "AddServer":
|
||||
|
||||
self.connect.setup_manual_server()
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'RemoveServer':
|
||||
elif method == "RemoveServer":
|
||||
|
||||
self.connect.remove_server(data['Id'])
|
||||
self.connect.remove_server(data["Id"])
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'UpdatePassword':
|
||||
elif method == "UpdatePassword":
|
||||
self.connect.setup_login_manual()
|
||||
elif method == 'UserDataChanged' and self.library_thread:
|
||||
if data.get('ServerId') or not window('jellyfin_startup.bool'):
|
||||
elif method == "UserDataChanged" and self.library_thread:
|
||||
if data.get("ServerId") or not window("jellyfin_startup.bool"):
|
||||
return
|
||||
|
||||
LOG.info("[ UserDataChanged ] %s", data)
|
||||
self.library_thread.userdata(data['UserDataList'])
|
||||
self.library_thread.userdata(data["UserDataList"])
|
||||
|
||||
elif method == 'LibraryChanged' and self.library_thread:
|
||||
if data.get('ServerId') or not window('jellyfin_startup.bool'):
|
||||
elif method == "LibraryChanged" and self.library_thread:
|
||||
if data.get("ServerId") or not window("jellyfin_startup.bool"):
|
||||
return
|
||||
|
||||
LOG.info("[ LibraryChanged ] %s", data)
|
||||
self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded'])
|
||||
self.library_thread.removed(data['ItemsRemoved'])
|
||||
self.library_thread.updated(data["ItemsUpdated"] + data["ItemsAdded"])
|
||||
self.library_thread.removed(data["ItemsRemoved"])
|
||||
|
||||
elif method == 'System.OnQuit':
|
||||
window('jellyfin_should_stop.bool', True)
|
||||
elif method == "System.OnQuit":
|
||||
window("jellyfin_should_stop.bool", True)
|
||||
self.running = False
|
||||
|
||||
elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'):
|
||||
elif method in (
|
||||
"SyncLibrarySelection",
|
||||
"RepairLibrarySelection",
|
||||
"AddLibrarySelection",
|
||||
"RemoveLibrarySelection",
|
||||
):
|
||||
self.library_thread.select_libraries(method)
|
||||
|
||||
elif method == 'SyncLibrary':
|
||||
if not data.get('Id'):
|
||||
elif method == "SyncLibrary":
|
||||
if not data.get("Id"):
|
||||
return
|
||||
|
||||
self.library_thread.add_library(data['Id'], data.get('Update', False))
|
||||
self.library_thread.add_library(data["Id"], data.get("Update", False))
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'RepairLibrary':
|
||||
if not data.get('Id'):
|
||||
elif method == "RepairLibrary":
|
||||
if not data.get("Id"):
|
||||
return
|
||||
|
||||
libraries = data['Id'].split(',')
|
||||
libraries = data["Id"].split(",")
|
||||
|
||||
for lib in libraries:
|
||||
|
||||
if not self.library_thread.remove_library(lib):
|
||||
return
|
||||
|
||||
self.library_thread.add_library(data['Id'])
|
||||
self.library_thread.add_library(data["Id"])
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'RemoveLibrary':
|
||||
libraries = data['Id'].split(',')
|
||||
elif method == "RemoveLibrary":
|
||||
libraries = data["Id"].split(",")
|
||||
|
||||
for lib in libraries:
|
||||
|
||||
@ -298,10 +372,10 @@ class Service(xbmc.Monitor):
|
||||
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
elif method == 'System.OnSleep':
|
||||
elif method == "System.OnSleep":
|
||||
|
||||
LOG.info("-->[ sleep ]")
|
||||
window('jellyfin_should_stop.bool', True)
|
||||
window("jellyfin_should_stop.bool", True)
|
||||
|
||||
if self.library_thread is not None:
|
||||
|
||||
@ -312,7 +386,7 @@ class Service(xbmc.Monitor):
|
||||
self.monitor.server = []
|
||||
self.monitor.sleep = True
|
||||
|
||||
elif method == 'System.OnWake':
|
||||
elif method == "System.OnWake":
|
||||
|
||||
if not self.monitor.sleep:
|
||||
LOG.warning("System.OnSleep was never called, skip System.OnWake")
|
||||
@ -322,14 +396,14 @@ class Service(xbmc.Monitor):
|
||||
LOG.info("--<[ sleep ]")
|
||||
xbmc.sleep(10000) # Allow network to wake up
|
||||
self.monitor.sleep = False
|
||||
window('jellyfin_should_stop', clear=True)
|
||||
window("jellyfin_should_stop", clear=True)
|
||||
|
||||
try:
|
||||
self.connect.register()
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
elif method == 'GUI.OnScreensaverDeactivated':
|
||||
elif method == "GUI.OnScreensaverDeactivated":
|
||||
|
||||
LOG.info("--<[ screensaver ]")
|
||||
xbmc.sleep(5000)
|
||||
@ -337,60 +411,80 @@ class Service(xbmc.Monitor):
|
||||
if self.library_thread is not None:
|
||||
self.library_thread.fast_sync()
|
||||
|
||||
elif method == 'UserConfigurationUpdated' and data.get('ServerId') is None:
|
||||
elif method == "UserConfigurationUpdated" and data.get("ServerId") is None:
|
||||
Views().get_views()
|
||||
|
||||
def onSettingsChanged(self):
|
||||
|
||||
''' React to setting changes that impact window values.
|
||||
'''
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
"""React to setting changes that impact window values."""
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
return
|
||||
|
||||
if settings('logLevel') != self.settings['log_level']:
|
||||
if settings("logLevel") != self.settings["log_level"]:
|
||||
|
||||
log_level = settings('logLevel')
|
||||
self.settings['logLevel'] = log_level
|
||||
log_level = settings("logLevel")
|
||||
self.settings["logLevel"] = log_level
|
||||
LOG.info("New log level: %s", log_level)
|
||||
|
||||
if settings('enableContext.bool') != self.settings['enable_context']:
|
||||
if settings("enableContext.bool") != self.settings["enable_context"]:
|
||||
|
||||
window('jellyfin_context', settings('enableContext'))
|
||||
self.settings['enable_context'] = settings('enableContext.bool')
|
||||
LOG.info("New context setting: %s", self.settings['enable_context'])
|
||||
window("jellyfin_context", settings("enableContext"))
|
||||
self.settings["enable_context"] = settings("enableContext.bool")
|
||||
LOG.info("New context setting: %s", self.settings["enable_context"])
|
||||
|
||||
if settings('enableContextTranscode.bool') != self.settings['enable_context_transcode']:
|
||||
if (
|
||||
settings("enableContextTranscode.bool")
|
||||
!= self.settings["enable_context_transcode"]
|
||||
):
|
||||
|
||||
window('jellyfin_context_transcode', settings('enableContextTranscode'))
|
||||
self.settings['enable_context_transcode'] = settings('enableContextTranscode.bool')
|
||||
LOG.info("New context transcode setting: %s", self.settings['enable_context_transcode'])
|
||||
window("jellyfin_context_transcode", settings("enableContextTranscode"))
|
||||
self.settings["enable_context_transcode"] = settings(
|
||||
"enableContextTranscode.bool"
|
||||
)
|
||||
LOG.info(
|
||||
"New context transcode setting: %s",
|
||||
self.settings["enable_context_transcode"],
|
||||
)
|
||||
|
||||
if settings('useDirectPaths') != self.settings['mode'] and self.library_thread.started:
|
||||
if (
|
||||
settings("useDirectPaths") != self.settings["mode"]
|
||||
and self.library_thread.started
|
||||
):
|
||||
|
||||
self.settings['mode'] = settings('useDirectPaths')
|
||||
LOG.info("New playback mode setting: %s", self.settings['mode'])
|
||||
self.settings["mode"] = settings("useDirectPaths")
|
||||
LOG.info("New playback mode setting: %s", self.settings["mode"])
|
||||
|
||||
if not self.settings.get('mode_warn'):
|
||||
if not self.settings.get("mode_warn"):
|
||||
|
||||
self.settings['mode_warn'] = True
|
||||
self.settings["mode_warn"] = True
|
||||
dialog("yesno", "{jellyfin}", translate(33118))
|
||||
|
||||
if settings('kodiCompanion.bool') != self.settings['kodi_companion']:
|
||||
self.settings['kodi_companion'] = settings('kodiCompanion.bool')
|
||||
if settings("kodiCompanion.bool") != self.settings["kodi_companion"]:
|
||||
self.settings["kodi_companion"] = settings("kodiCompanion.bool")
|
||||
|
||||
if not self.settings['kodi_companion']:
|
||||
if not self.settings["kodi_companion"]:
|
||||
dialog("ok", "{jellyfin}", translate(33138))
|
||||
|
||||
def reload_objects(self):
|
||||
|
||||
''' Reload objects which depends on the patch module.
|
||||
This allows to see the changes in code without restarting the python interpreter.
|
||||
'''
|
||||
reload_modules = ['objects.movies', 'objects.musicvideos', 'objects.tvshows',
|
||||
'objects.music', 'objects.obj', 'objects.actions', 'objects.kodi.kodi',
|
||||
'objects.kodi.movies', 'objects.kodi.musicvideos', 'objects.kodi.tvshows',
|
||||
'objects.kodi.music', 'objects.kodi.artwork', 'objects.kodi.queries',
|
||||
'objects.kodi.queries_music', 'objects.kodi.queries_texture']
|
||||
"""Reload objects which depends on the patch module.
|
||||
This allows to see the changes in code without restarting the python interpreter.
|
||||
"""
|
||||
reload_modules = [
|
||||
"objects.movies",
|
||||
"objects.musicvideos",
|
||||
"objects.tvshows",
|
||||
"objects.music",
|
||||
"objects.obj",
|
||||
"objects.actions",
|
||||
"objects.kodi.kodi",
|
||||
"objects.kodi.movies",
|
||||
"objects.kodi.musicvideos",
|
||||
"objects.kodi.tvshows",
|
||||
"objects.kodi.music",
|
||||
"objects.kodi.artwork",
|
||||
"objects.kodi.queries",
|
||||
"objects.kodi.queries_music",
|
||||
"objects.kodi.queries_texture",
|
||||
]
|
||||
|
||||
for mod in reload_modules:
|
||||
del sys.modules[mod]
|
||||
@ -407,14 +501,22 @@ class Service(xbmc.Monitor):
|
||||
def shutdown(self):
|
||||
|
||||
LOG.info("---<[ EXITING ]")
|
||||
window('jellyfin_should_stop.bool', True)
|
||||
window("jellyfin_should_stop.bool", True)
|
||||
|
||||
properties = [ # TODO: review
|
||||
"jellyfin_state", "jellyfin_serverStatus", "jellyfin_currUser",
|
||||
|
||||
"jellyfin_play", "jellyfin_online", "jellyfin.connected", "jellyfin_startup",
|
||||
"jellyfin.external", "jellyfin.external_check", "jellyfin_deviceId", "jellyfin_db_check", "jellyfin_pathverified",
|
||||
"jellyfin_sync"
|
||||
"jellyfin_state",
|
||||
"jellyfin_serverStatus",
|
||||
"jellyfin_currUser",
|
||||
"jellyfin_play",
|
||||
"jellyfin_online",
|
||||
"jellyfin.connected",
|
||||
"jellyfin_startup",
|
||||
"jellyfin.external",
|
||||
"jellyfin.external_check",
|
||||
"jellyfin_deviceId",
|
||||
"jellyfin_db_check",
|
||||
"jellyfin_pathverified",
|
||||
"jellyfin_sync",
|
||||
]
|
||||
for prop in properties:
|
||||
window(prop, clear=True)
|
||||
|
@ -23,11 +23,11 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
class FullSync(object):
|
||||
"""This should be called like a context.
|
||||
i.e. with FullSync('jellyfin') as sync:
|
||||
sync.libraries()
|
||||
"""
|
||||
|
||||
''' This should be called like a context.
|
||||
i.e. with FullSync('jellyfin') as sync:
|
||||
sync.libraries()
|
||||
'''
|
||||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
sync = None
|
||||
@ -35,10 +35,9 @@ class FullSync(object):
|
||||
screensaver = None
|
||||
|
||||
def __init__(self, library, server):
|
||||
|
||||
''' You can call all big syncing methods here.
|
||||
Initial, update, repair, remove.
|
||||
'''
|
||||
"""You can call all big syncing methods here.
|
||||
Initial, update, repair, remove.
|
||||
"""
|
||||
self.__dict__ = self._shared_state
|
||||
|
||||
if self.running:
|
||||
@ -50,78 +49,81 @@ class FullSync(object):
|
||||
self.server = server
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
''' Do everything we need before the sync
|
||||
'''
|
||||
"""Do everything we need before the sync"""
|
||||
LOG.info("-->[ fullsync ]")
|
||||
|
||||
if not settings('dbSyncScreensaver.bool'):
|
||||
if not settings("dbSyncScreensaver.bool"):
|
||||
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
||||
xbmc.executebuiltin("InhibitIdleShutdown(true)")
|
||||
self.screensaver = get_screensaver()
|
||||
set_screensaver(value="")
|
||||
|
||||
self.running = True
|
||||
window('jellyfin_sync.bool', True)
|
||||
window("jellyfin_sync.bool", True)
|
||||
|
||||
return self
|
||||
|
||||
def libraries(self, libraries=None, update=False):
|
||||
|
||||
''' Map the syncing process and start the sync. Ensure only one sync is running.
|
||||
'''
|
||||
self.direct_path = settings('useDirectPaths') == "1"
|
||||
"""Map the syncing process and start the sync. Ensure only one sync is running."""
|
||||
self.direct_path = settings("useDirectPaths") == "1"
|
||||
self.update_library = update
|
||||
self.sync = get_sync()
|
||||
|
||||
if libraries:
|
||||
# Can be a single ID or a comma separated list
|
||||
libraries = libraries.split(',')
|
||||
libraries = libraries.split(",")
|
||||
for library_id in libraries:
|
||||
# Look up library in local Jellyfin database
|
||||
library = self.get_library(library_id)
|
||||
|
||||
if library:
|
||||
if library.media_type == 'mixed':
|
||||
self.sync['Libraries'].append("Mixed:%s" % library_id)
|
||||
if library.media_type == "mixed":
|
||||
self.sync["Libraries"].append("Mixed:%s" % library_id)
|
||||
# Include boxsets library
|
||||
libraries = self.get_libraries()
|
||||
boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets']
|
||||
boxsets = [
|
||||
row.view_id
|
||||
for row in libraries
|
||||
if row.media_type == "boxsets"
|
||||
]
|
||||
if boxsets:
|
||||
self.sync['Libraries'].append('Boxsets:%s' % boxsets[0])
|
||||
elif library.media_type == 'movies':
|
||||
self.sync['Libraries'].append(library_id)
|
||||
self.sync["Libraries"].append("Boxsets:%s" % boxsets[0])
|
||||
elif library.media_type == "movies":
|
||||
self.sync["Libraries"].append(library_id)
|
||||
# Include boxsets library
|
||||
libraries = self.get_libraries()
|
||||
boxsets = [row.view_id for row in libraries if row.media_type == 'boxsets']
|
||||
boxsets = [
|
||||
row.view_id
|
||||
for row in libraries
|
||||
if row.media_type == "boxsets"
|
||||
]
|
||||
# Verify we're only trying to sync boxsets once
|
||||
if boxsets and boxsets[0] not in self.sync['Libraries']:
|
||||
self.sync['Libraries'].append('Boxsets:%s' % boxsets[0])
|
||||
if boxsets and boxsets[0] not in self.sync["Libraries"]:
|
||||
self.sync["Libraries"].append("Boxsets:%s" % boxsets[0])
|
||||
else:
|
||||
# Only called if the library isn't already known about
|
||||
self.sync['Libraries'].append(library_id)
|
||||
self.sync["Libraries"].append(library_id)
|
||||
else:
|
||||
self.sync['Libraries'].append(library_id)
|
||||
self.sync["Libraries"].append(library_id)
|
||||
else:
|
||||
self.mapping()
|
||||
|
||||
if not xmls.advanced_settings() and self.sync['Libraries']:
|
||||
if not xmls.advanced_settings() and self.sync["Libraries"]:
|
||||
self.start()
|
||||
|
||||
def get_libraries(self):
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views()
|
||||
|
||||
def get_library(self, library_id):
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
return jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_view(library_id)
|
||||
|
||||
def mapping(self):
|
||||
|
||||
''' Load the mapping of the full sync.
|
||||
This allows us to restore a previous sync.
|
||||
'''
|
||||
if self.sync['Libraries']:
|
||||
"""Load the mapping of the full sync.
|
||||
This allows us to restore a previous sync.
|
||||
"""
|
||||
if self.sync["Libraries"]:
|
||||
|
||||
if not dialog("yesno", "{jellyfin}", translate(33102)):
|
||||
|
||||
@ -130,38 +132,48 @@ class FullSync(object):
|
||||
|
||||
raise LibraryException("ProgressStopped")
|
||||
else:
|
||||
self.sync['Libraries'] = []
|
||||
self.sync['RestorePoint'] = {}
|
||||
self.sync["Libraries"] = []
|
||||
self.sync["RestorePoint"] = {}
|
||||
else:
|
||||
LOG.info("generate full sync")
|
||||
libraries = []
|
||||
|
||||
for library in self.get_libraries():
|
||||
if library.media_type in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'):
|
||||
libraries.append({'Id': library.view_id, 'Name': library.view_name, 'Media': library.media_type})
|
||||
if library.media_type in (
|
||||
"movies",
|
||||
"tvshows",
|
||||
"musicvideos",
|
||||
"music",
|
||||
"mixed",
|
||||
):
|
||||
libraries.append(
|
||||
{
|
||||
"Id": library.view_id,
|
||||
"Name": library.view_name,
|
||||
"Media": library.media_type,
|
||||
}
|
||||
)
|
||||
|
||||
libraries = self.select_libraries(libraries)
|
||||
|
||||
if [x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed')]:
|
||||
self.sync['Libraries'].append("Boxsets:")
|
||||
if [x["Media"] for x in libraries if x["Media"] in ("movies", "mixed")]:
|
||||
self.sync["Libraries"].append("Boxsets:")
|
||||
|
||||
save_sync(self.sync)
|
||||
|
||||
def select_libraries(self, libraries):
|
||||
"""Select all or certain libraries to be whitelisted."""
|
||||
|
||||
''' Select all or certain libraries to be whitelisted.
|
||||
'''
|
||||
|
||||
choices = [x['Name'] for x in libraries]
|
||||
choices = [x["Name"] for x in libraries]
|
||||
choices.insert(0, translate(33121))
|
||||
selection = dialog("multi", translate(33120), choices)
|
||||
|
||||
if selection is None:
|
||||
raise LibraryException('LibrarySelection')
|
||||
raise LibraryException("LibrarySelection")
|
||||
elif not selection:
|
||||
LOG.info("Nothing was selected.")
|
||||
|
||||
raise LibraryException('SyncLibraryLater')
|
||||
raise LibraryException("SyncLibraryLater")
|
||||
|
||||
if 0 in selection:
|
||||
selection = list(range(1, len(libraries) + 1))
|
||||
@ -171,96 +183,100 @@ class FullSync(object):
|
||||
for x in selection:
|
||||
library = libraries[x - 1]
|
||||
|
||||
if library['Media'] != 'mixed':
|
||||
selected_libraries.append(library['Id'])
|
||||
if library["Media"] != "mixed":
|
||||
selected_libraries.append(library["Id"])
|
||||
else:
|
||||
selected_libraries.append("Mixed:%s" % library['Id'])
|
||||
selected_libraries.append("Mixed:%s" % library["Id"])
|
||||
|
||||
self.sync['Libraries'] = selected_libraries
|
||||
self.sync["Libraries"] = selected_libraries
|
||||
|
||||
return [libraries[x - 1] for x in selection]
|
||||
|
||||
def start(self):
|
||||
|
||||
''' Main sync process.
|
||||
'''
|
||||
LOG.info("starting sync with %s", self.sync['Libraries'])
|
||||
"""Main sync process."""
|
||||
LOG.info("starting sync with %s", self.sync["Libraries"])
|
||||
save_sync(self.sync)
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
for library in list(self.sync['Libraries']):
|
||||
for library in list(self.sync["Libraries"]):
|
||||
|
||||
self.process_library(library)
|
||||
|
||||
if not library.startswith('Boxsets:') and library not in self.sync['Whitelist']:
|
||||
self.sync['Whitelist'].append(library)
|
||||
if (
|
||||
not library.startswith("Boxsets:")
|
||||
and library not in self.sync["Whitelist"]
|
||||
):
|
||||
self.sync["Whitelist"].append(library)
|
||||
|
||||
self.sync['Libraries'].pop(self.sync['Libraries'].index(library))
|
||||
self.sync['RestorePoint'] = {}
|
||||
self.sync["Libraries"].pop(self.sync["Libraries"].index(library))
|
||||
self.sync["RestorePoint"] = {}
|
||||
|
||||
elapsed = datetime.datetime.now() - start_time
|
||||
settings('SyncInstallRunDone.bool', True)
|
||||
settings("SyncInstallRunDone.bool", True)
|
||||
self.library.save_last_sync()
|
||||
save_sync(self.sync)
|
||||
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33025), str(elapsed).split('.')[0]),
|
||||
icon="{jellyfin}", sound=False)
|
||||
LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0])
|
||||
xbmc.executebuiltin("UpdateLibrary(video)")
|
||||
dialog(
|
||||
"notification",
|
||||
heading="{jellyfin}",
|
||||
message="%s %s" % (translate(33025), str(elapsed).split(".")[0]),
|
||||
icon="{jellyfin}",
|
||||
sound=False,
|
||||
)
|
||||
LOG.info("Full sync completed in: %s", str(elapsed).split(".")[0])
|
||||
|
||||
def process_library(self, library_id):
|
||||
|
||||
''' Add a library by its id. Create a node and a playlist whenever appropriate.
|
||||
'''
|
||||
"""Add a library by its id. Create a node and a playlist whenever appropriate."""
|
||||
media = {
|
||||
'movies': self.movies,
|
||||
'musicvideos': self.musicvideos,
|
||||
'tvshows': self.tvshows,
|
||||
'music': self.music
|
||||
"movies": self.movies,
|
||||
"musicvideos": self.musicvideos,
|
||||
"tvshows": self.tvshows,
|
||||
"music": self.music,
|
||||
}
|
||||
try:
|
||||
if library_id.startswith('Boxsets:'):
|
||||
if library_id.startswith("Boxsets:"):
|
||||
boxset_library = {}
|
||||
|
||||
# Initial library sync is 'Boxsets:'
|
||||
# Refresh from the addon menu is 'Boxsets:Refresh'
|
||||
# Incremental syncs are 'Boxsets:$library_id'
|
||||
sync_id = library_id.split(':')[1]
|
||||
sync_id = library_id.split(":")[1]
|
||||
|
||||
if not sync_id or sync_id == 'Refresh':
|
||||
if not sync_id or sync_id == "Refresh":
|
||||
libraries = self.get_libraries()
|
||||
else:
|
||||
_lib = self.get_library(sync_id)
|
||||
libraries = [_lib] if _lib else []
|
||||
|
||||
for entry in libraries:
|
||||
if entry.media_type == 'boxsets':
|
||||
boxset_library = {'Id': entry.view_id, 'Name': entry.view_name}
|
||||
if entry.media_type == "boxsets":
|
||||
boxset_library = {"Id": entry.view_id, "Name": entry.view_name}
|
||||
break
|
||||
|
||||
if boxset_library:
|
||||
if sync_id == 'Refresh':
|
||||
if sync_id == "Refresh":
|
||||
self.refresh_boxsets(boxset_library)
|
||||
else:
|
||||
self.boxsets(boxset_library)
|
||||
|
||||
return
|
||||
|
||||
library = self.server.jellyfin.get_item(library_id.replace('Mixed:', ""))
|
||||
library = self.server.jellyfin.get_item(library_id.replace("Mixed:", ""))
|
||||
|
||||
if library_id.startswith('Mixed:'):
|
||||
for mixed in ('movies', 'tvshows'):
|
||||
if library_id.startswith("Mixed:"):
|
||||
for mixed in ("movies", "tvshows"):
|
||||
|
||||
media[mixed](library)
|
||||
self.sync['RestorePoint'] = {}
|
||||
self.sync["RestorePoint"] = {}
|
||||
else:
|
||||
if library['CollectionType']:
|
||||
settings('enableMusic.bool', True)
|
||||
if library["CollectionType"]:
|
||||
settings("enableMusic.bool", True)
|
||||
|
||||
media[library['CollectionType']](library)
|
||||
media[library["CollectionType"]](library)
|
||||
except LibraryException as error:
|
||||
|
||||
if error.status == 'StopCalled':
|
||||
if error.status == "StopCalled":
|
||||
save_sync(self.sync)
|
||||
|
||||
raise
|
||||
@ -282,31 +298,41 @@ class FullSync(object):
|
||||
def video_database_locks(self):
|
||||
with self.library.database_lock:
|
||||
with Database() as videodb:
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
yield videodb, jellyfindb
|
||||
|
||||
@progress()
|
||||
def movies(self, library, dialog):
|
||||
|
||||
''' Process movies from a single library.
|
||||
'''
|
||||
"""Process movies from a single library."""
|
||||
processed_ids = []
|
||||
|
||||
for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')):
|
||||
for items in server.get_items(
|
||||
library["Id"], "Movie", False, self.sync["RestorePoint"].get("params")
|
||||
):
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
obj = Movies(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library
|
||||
)
|
||||
|
||||
self.sync['RestorePoint'] = items['RestorePoint']
|
||||
start_index = items['RestorePoint']['params']['StartIndex']
|
||||
self.sync["RestorePoint"] = items["RestorePoint"]
|
||||
start_index = items["RestorePoint"]["params"]["StartIndex"]
|
||||
|
||||
for index, movie in enumerate(items['Items']):
|
||||
for index, movie in enumerate(items["Items"]):
|
||||
|
||||
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
|
||||
heading="%s: %s" % (translate('addon_name'), library['Name']),
|
||||
message=movie['Name'])
|
||||
dialog.update(
|
||||
int(
|
||||
(
|
||||
float(start_index + index)
|
||||
/ float(items["TotalRecordCount"])
|
||||
)
|
||||
* 100
|
||||
),
|
||||
heading="%s: %s" % (translate("addon_name"), library["Name"]),
|
||||
message=movie["Name"],
|
||||
)
|
||||
obj.movie(movie)
|
||||
processed_ids.append(movie['Id'])
|
||||
processed_ids.append(movie["Id"])
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
@ -316,158 +342,199 @@ class FullSync(object):
|
||||
self.movies_compare(library, obj, jellyfindb)
|
||||
|
||||
def movies_compare(self, library, obj, jellyfinydb):
|
||||
|
||||
''' Compare entries from library to what's in the jellyfindb. Remove surplus
|
||||
'''
|
||||
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfinydb.cursor)
|
||||
|
||||
items = db.get_item_by_media_folder(library['Id'])
|
||||
items = db.get_item_by_media_folder(library["Id"])
|
||||
current = obj.item_ids
|
||||
|
||||
for x in items:
|
||||
if x[0] not in current and x[1] == 'Movie':
|
||||
if x[0] not in current and x[1] == "Movie":
|
||||
obj.remove(x[0])
|
||||
|
||||
@progress()
|
||||
def tvshows(self, library, dialog):
|
||||
|
||||
''' Process tvshows and episodes from a single library.
|
||||
'''
|
||||
"""Process tvshows and episodes from a single library."""
|
||||
processed_ids = []
|
||||
|
||||
for items in server.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')):
|
||||
for items in server.get_items(
|
||||
library["Id"], "Series", False, self.sync["RestorePoint"].get("params")
|
||||
):
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True)
|
||||
obj = TVShows(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library, True
|
||||
)
|
||||
|
||||
self.sync['RestorePoint'] = items['RestorePoint']
|
||||
start_index = items['RestorePoint']['params']['StartIndex']
|
||||
self.sync["RestorePoint"] = items["RestorePoint"]
|
||||
start_index = items["RestorePoint"]["params"]["StartIndex"]
|
||||
|
||||
for index, show in enumerate(items['Items']):
|
||||
for index, show in enumerate(items["Items"]):
|
||||
|
||||
percent = int((float(start_index + index) / float(items['TotalRecordCount'])) * 100)
|
||||
message = show['Name']
|
||||
dialog.update(percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message)
|
||||
percent = int(
|
||||
(float(start_index + index) / float(items["TotalRecordCount"]))
|
||||
* 100
|
||||
)
|
||||
message = show["Name"]
|
||||
dialog.update(
|
||||
percent,
|
||||
heading="%s: %s" % (translate("addon_name"), library["Name"]),
|
||||
message=message,
|
||||
)
|
||||
|
||||
if obj.tvshow(show) is not False:
|
||||
|
||||
for episodes in server.get_episode_by_show(show['Id']):
|
||||
for episode in episodes['Items']:
|
||||
if episode.get('Path'):
|
||||
dialog.update(percent, message="%s/%s" % (message, episode['Name'][:10]))
|
||||
for episodes in server.get_episode_by_show(show["Id"]):
|
||||
for episode in episodes["Items"]:
|
||||
if episode.get("Path"):
|
||||
dialog.update(
|
||||
percent,
|
||||
message="%s/%s"
|
||||
% (message, episode["Name"][:10]),
|
||||
)
|
||||
obj.episode(episode)
|
||||
processed_ids.append(show['Id'])
|
||||
processed_ids.append(show["Id"])
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, library, True)
|
||||
obj = TVShows(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library, True
|
||||
)
|
||||
obj.item_ids = processed_ids
|
||||
if self.update_library:
|
||||
self.tvshows_compare(library, obj, jellyfindb)
|
||||
|
||||
def tvshows_compare(self, library, obj, jellyfindb):
|
||||
|
||||
''' Compare entries from library to what's in the jellyfindb. Remove surplus
|
||||
'''
|
||||
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
|
||||
items = db.get_item_by_media_folder(library['Id'])
|
||||
items = db.get_item_by_media_folder(library["Id"])
|
||||
for x in list(items):
|
||||
items.extend(obj.get_child(x[0]))
|
||||
|
||||
current = obj.item_ids
|
||||
|
||||
for x in items:
|
||||
if x[0] not in current and x[1] == 'Series':
|
||||
if x[0] not in current and x[1] == "Series":
|
||||
obj.remove(x[0])
|
||||
|
||||
@progress()
|
||||
def musicvideos(self, library, dialog):
|
||||
|
||||
''' Process musicvideos from a single library.
|
||||
'''
|
||||
"""Process musicvideos from a single library."""
|
||||
processed_ids = []
|
||||
|
||||
for items in server.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')):
|
||||
for items in server.get_items(
|
||||
library["Id"], "MusicVideo", False, self.sync["RestorePoint"].get("params")
|
||||
):
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
obj = MusicVideos(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library
|
||||
)
|
||||
|
||||
self.sync['RestorePoint'] = items['RestorePoint']
|
||||
start_index = items['RestorePoint']['params']['StartIndex']
|
||||
self.sync["RestorePoint"] = items["RestorePoint"]
|
||||
start_index = items["RestorePoint"]["params"]["StartIndex"]
|
||||
|
||||
for index, mvideo in enumerate(items['Items']):
|
||||
for index, mvideo in enumerate(items["Items"]):
|
||||
|
||||
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
|
||||
heading="%s: %s" % (translate('addon_name'), library['Name']),
|
||||
message=mvideo['Name'])
|
||||
dialog.update(
|
||||
int(
|
||||
(
|
||||
float(start_index + index)
|
||||
/ float(items["TotalRecordCount"])
|
||||
)
|
||||
* 100
|
||||
),
|
||||
heading="%s: %s" % (translate("addon_name"), library["Name"]),
|
||||
message=mvideo["Name"],
|
||||
)
|
||||
obj.musicvideo(mvideo)
|
||||
processed_ids.append(mvideo['Id'])
|
||||
processed_ids.append(mvideo["Id"])
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
obj = MusicVideos(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library
|
||||
)
|
||||
obj.item_ids = processed_ids
|
||||
if self.update_library:
|
||||
self.musicvideos_compare(library, obj, jellyfindb)
|
||||
|
||||
def musicvideos_compare(self, library, obj, jellyfindb):
|
||||
|
||||
''' Compare entries from library to what's in the jellyfindb. Remove surplus
|
||||
'''
|
||||
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
|
||||
items = db.get_item_by_media_folder(library['Id'])
|
||||
items = db.get_item_by_media_folder(library["Id"])
|
||||
current = obj.item_ids
|
||||
|
||||
for x in items:
|
||||
if x[0] not in current and x[1] == 'MusicVideo':
|
||||
if x[0] not in current and x[1] == "MusicVideo":
|
||||
obj.remove(x[0])
|
||||
|
||||
@progress()
|
||||
def music(self, library, dialog):
|
||||
|
||||
''' Process artists, album, songs from a single library.
|
||||
'''
|
||||
"""Process artists, album, songs from a single library."""
|
||||
with self.library.music_database_lock:
|
||||
with Database('music') as musicdb:
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
obj = Music(self.server, jellyfindb, musicdb, self.direct_path, library)
|
||||
with Database("music") as musicdb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
obj = Music(
|
||||
self.server, jellyfindb, musicdb, self.direct_path, library
|
||||
)
|
||||
|
||||
library_id = library['Id']
|
||||
library_id = library["Id"]
|
||||
|
||||
total_items = server.get_item_count(library_id, 'MusicArtist,MusicAlbum,Audio')
|
||||
total_items = server.get_item_count(
|
||||
library_id, "MusicArtist,MusicAlbum,Audio"
|
||||
)
|
||||
count = 0
|
||||
|
||||
'''
|
||||
"""
|
||||
Music database syncing. Artists must be in the database
|
||||
before albums, albums before songs. Pulls batches of items
|
||||
in sizes of setting "Paging - Max items". 'artists',
|
||||
'albums', and 'songs' are generators containing a dict of
|
||||
api responses
|
||||
'''
|
||||
"""
|
||||
artists = server.get_artists(library_id)
|
||||
for batch in artists:
|
||||
for item in batch['Items']:
|
||||
LOG.debug('Artist: {}'.format(item.get('Name')))
|
||||
for item in batch["Items"]:
|
||||
LOG.debug("Artist: {}".format(item.get("Name")))
|
||||
percent = int((float(count) / float(total_items)) * 100)
|
||||
dialog.update(percent, message='Artist: {}'.format(item.get('Name')))
|
||||
dialog.update(
|
||||
percent, message="Artist: {}".format(item.get("Name"))
|
||||
)
|
||||
obj.artist(item)
|
||||
count += 1
|
||||
|
||||
albums = server.get_items(library_id, item_type='MusicAlbum', params={'SortBy': 'AlbumArtist'})
|
||||
albums = server.get_items(
|
||||
library_id,
|
||||
item_type="MusicAlbum",
|
||||
params={"SortBy": "AlbumArtist"},
|
||||
)
|
||||
for batch in albums:
|
||||
for item in batch['Items']:
|
||||
LOG.debug('Album: {}'.format(item.get('Name')))
|
||||
for item in batch["Items"]:
|
||||
LOG.debug("Album: {}".format(item.get("Name")))
|
||||
percent = int((float(count) / float(total_items)) * 100)
|
||||
dialog.update(percent, message='Album: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name')))
|
||||
dialog.update(
|
||||
percent,
|
||||
message="Album: {} - {}".format(
|
||||
item.get("AlbumArtist", ""), item.get("Name")
|
||||
),
|
||||
)
|
||||
obj.album(item)
|
||||
count += 1
|
||||
|
||||
songs = server.get_items(library_id, item_type='Audio', params={'SortBy': 'AlbumArtist'})
|
||||
songs = server.get_items(
|
||||
library_id, item_type="Audio", params={"SortBy": "AlbumArtist"}
|
||||
)
|
||||
for batch in songs:
|
||||
for item in batch['Items']:
|
||||
LOG.debug('Song: {}'.format(item.get('Name')))
|
||||
for item in batch["Items"]:
|
||||
LOG.debug("Song: {}".format(item.get("Name")))
|
||||
percent = int((float(count) / float(total_items)) * 100)
|
||||
dialog.update(percent, message='Track: {} - {}'.format(item.get('AlbumArtist', ''), item.get('Name')))
|
||||
dialog.update(
|
||||
percent,
|
||||
message="Track: {} - {}".format(
|
||||
item.get("AlbumArtist", ""), item.get("Name")
|
||||
),
|
||||
)
|
||||
obj.song(item)
|
||||
count += 1
|
||||
|
||||
@ -475,45 +542,52 @@ class FullSync(object):
|
||||
self.music_compare(library, obj, jellyfindb)
|
||||
|
||||
def music_compare(self, library, obj, jellyfindb):
|
||||
|
||||
''' Compare entries from library to what's in the jellyfindb. Remove surplus
|
||||
'''
|
||||
"""Compare entries from library to what's in the jellyfindb. Remove surplus"""
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
|
||||
items = db.get_item_by_media_folder(library['Id'])
|
||||
items = db.get_item_by_media_folder(library["Id"])
|
||||
for x in list(items):
|
||||
items.extend(obj.get_child(x[0]))
|
||||
|
||||
current = obj.item_ids
|
||||
|
||||
for x in items:
|
||||
if x[0] not in current and x[1] == 'MusicArtist':
|
||||
if x[0] not in current and x[1] == "MusicArtist":
|
||||
obj.remove(x[0])
|
||||
|
||||
@progress(translate(33018))
|
||||
def boxsets(self, library, dialog=None):
|
||||
|
||||
''' Process all boxsets.
|
||||
'''
|
||||
for items in server.get_items(library['Id'], "BoxSet", False, self.sync['RestorePoint'].get('params')):
|
||||
"""Process all boxsets."""
|
||||
for items in server.get_items(
|
||||
library["Id"], "BoxSet", False, self.sync["RestorePoint"].get("params")
|
||||
):
|
||||
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
obj = Movies(
|
||||
self.server, jellyfindb, videodb, self.direct_path, library
|
||||
)
|
||||
|
||||
self.sync['RestorePoint'] = items['RestorePoint']
|
||||
start_index = items['RestorePoint']['params']['StartIndex']
|
||||
self.sync["RestorePoint"] = items["RestorePoint"]
|
||||
start_index = items["RestorePoint"]["params"]["StartIndex"]
|
||||
|
||||
for index, boxset in enumerate(items['Items']):
|
||||
for index, boxset in enumerate(items["Items"]):
|
||||
|
||||
dialog.update(int((float(start_index + index) / float(items['TotalRecordCount'])) * 100),
|
||||
heading="%s: %s" % (translate('addon_name'), translate('boxsets')),
|
||||
message=boxset['Name'])
|
||||
dialog.update(
|
||||
int(
|
||||
(
|
||||
float(start_index + index)
|
||||
/ float(items["TotalRecordCount"])
|
||||
)
|
||||
* 100
|
||||
),
|
||||
heading="%s: %s"
|
||||
% (translate("addon_name"), translate("boxsets")),
|
||||
message=boxset["Name"],
|
||||
)
|
||||
obj.boxset(boxset)
|
||||
|
||||
def refresh_boxsets(self, library):
|
||||
|
||||
''' Delete all existing boxsets and re-add.
|
||||
'''
|
||||
"""Delete all existing boxsets and re-add."""
|
||||
with self.video_database_locks() as (videodb, jellyfindb):
|
||||
obj = Movies(self.server, jellyfindb, videodb, self.direct_path, library)
|
||||
obj.boxsets_reset()
|
||||
@ -522,82 +596,108 @@ class FullSync(object):
|
||||
|
||||
@progress(translate(33144))
|
||||
def remove_library(self, library_id, dialog):
|
||||
|
||||
''' Remove library by their id from the Kodi database.
|
||||
'''
|
||||
"""Remove library by their id from the Kodi database."""
|
||||
direct_path = self.library.direct_path
|
||||
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
library = db.get_view(library_id.replace('Mixed:', ""))
|
||||
items = db.get_item_by_media_folder(library_id.replace('Mixed:', ""))
|
||||
media = 'music' if library.media_type == 'music' else 'video'
|
||||
library = db.get_view(library_id.replace("Mixed:", ""))
|
||||
items = db.get_item_by_media_folder(library_id.replace("Mixed:", ""))
|
||||
media = "music" if library.media_type == "music" else "video"
|
||||
|
||||
if media == 'music':
|
||||
settings('MusicRescan.bool', False)
|
||||
if media == "music":
|
||||
settings("MusicRescan.bool", False)
|
||||
|
||||
if items:
|
||||
with self.library.music_database_lock if media == 'music' else self.library.database_lock:
|
||||
with (
|
||||
self.library.music_database_lock
|
||||
if media == "music"
|
||||
else self.library.database_lock
|
||||
):
|
||||
with Database(media) as kodidb:
|
||||
|
||||
count = 0
|
||||
|
||||
if library.media_type == 'mixed':
|
||||
if library.media_type == "mixed":
|
||||
|
||||
movies = [x for x in items if x[1] == 'Movie']
|
||||
tvshows = [x for x in items if x[1] == 'Series']
|
||||
movies = [x for x in items if x[1] == "Movie"]
|
||||
tvshows = [x for x in items if x[1] == "Series"]
|
||||
|
||||
obj = Movies(self.server, jellyfindb, kodidb, direct_path, library).remove
|
||||
obj = Movies(
|
||||
self.server, jellyfindb, kodidb, direct_path, library
|
||||
).remove
|
||||
|
||||
for item in movies:
|
||||
|
||||
obj(item[0])
|
||||
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name))
|
||||
dialog.update(
|
||||
int((float(count) / float(len(items)) * 100)),
|
||||
heading="%s: %s"
|
||||
% (translate("addon_name"), library.view_name),
|
||||
)
|
||||
count += 1
|
||||
|
||||
obj = TVShows(self.server, jellyfindb, kodidb, direct_path, library).remove
|
||||
obj = TVShows(
|
||||
self.server, jellyfindb, kodidb, direct_path, library
|
||||
).remove
|
||||
|
||||
for item in tvshows:
|
||||
|
||||
obj(item[0])
|
||||
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library.view_name))
|
||||
dialog.update(
|
||||
int((float(count) / float(len(items)) * 100)),
|
||||
heading="%s: %s"
|
||||
% (translate("addon_name"), library.view_name),
|
||||
)
|
||||
count += 1
|
||||
else:
|
||||
default_args = (self.server, jellyfindb, kodidb, direct_path)
|
||||
default_args = (
|
||||
self.server,
|
||||
jellyfindb,
|
||||
kodidb,
|
||||
direct_path,
|
||||
)
|
||||
for item in items:
|
||||
if item[1] in ('Series', 'Season', 'Episode'):
|
||||
if item[1] in ("Series", "Season", "Episode"):
|
||||
TVShows(*default_args).remove(item[0])
|
||||
elif item[1] in ('Movie', 'BoxSet'):
|
||||
elif item[1] in ("Movie", "BoxSet"):
|
||||
Movies(*default_args).remove(item[0])
|
||||
elif item[1] in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'):
|
||||
elif item[1] in (
|
||||
"MusicAlbum",
|
||||
"MusicArtist",
|
||||
"AlbumArtist",
|
||||
"Audio",
|
||||
):
|
||||
Music(*default_args).remove(item[0])
|
||||
elif item[1] == 'MusicVideo':
|
||||
elif item[1] == "MusicVideo":
|
||||
MusicVideos(*default_args).remove(item[0])
|
||||
|
||||
dialog.update(int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0]))
|
||||
dialog.update(
|
||||
int((float(count) / float(len(items)) * 100)),
|
||||
heading="%s: %s"
|
||||
% (translate("addon_name"), library[0]),
|
||||
)
|
||||
count += 1
|
||||
|
||||
self.sync = get_sync()
|
||||
|
||||
if library_id in self.sync['Whitelist']:
|
||||
self.sync['Whitelist'].remove(library_id)
|
||||
if library_id in self.sync["Whitelist"]:
|
||||
self.sync["Whitelist"].remove(library_id)
|
||||
|
||||
elif 'Mixed:%s' % library_id in self.sync['Whitelist']:
|
||||
self.sync['Whitelist'].remove('Mixed:%s' % library_id)
|
||||
elif "Mixed:%s" % library_id in self.sync["Whitelist"]:
|
||||
self.sync["Whitelist"].remove("Mixed:%s" % library_id)
|
||||
|
||||
save_sync(self.sync)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
''' Exiting sync
|
||||
'''
|
||||
"""Exiting sync"""
|
||||
self.running = False
|
||||
window('jellyfin_sync', clear=True)
|
||||
window("jellyfin_sync", clear=True)
|
||||
|
||||
if not settings('dbSyncScreensaver.bool') and self.screensaver is not None:
|
||||
if not settings("dbSyncScreensaver.bool") and self.screensaver is not None:
|
||||
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||
xbmc.executebuiltin("InhibitIdleShutdown(false)")
|
||||
set_screensaver(value=self.screensaver)
|
||||
|
||||
LOG.info("--<[ fullsync ]")
|
||||
|
@ -14,100 +14,102 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
class API(object):
|
||||
def __init__(self, item, server=None):
|
||||
|
||||
''' Get item information in special cases.
|
||||
server is the server address, provide if your functions requires it.
|
||||
'''
|
||||
"""Get item information in special cases.
|
||||
server is the server address, provide if your functions requires it.
|
||||
"""
|
||||
self.item = item
|
||||
self.server = server
|
||||
|
||||
def get_playcount(self, played, playcount):
|
||||
|
||||
''' Convert Jellyfin played/playcount into
|
||||
the Kodi equivalent. The playcount is tied to the watch status.
|
||||
'''
|
||||
"""Convert Jellyfin played/playcount into
|
||||
the Kodi equivalent. The playcount is tied to the watch status.
|
||||
"""
|
||||
return (playcount or 1) if played else None
|
||||
|
||||
def get_naming(self):
|
||||
|
||||
if self.item['Type'] == 'Episode' and 'SeriesName' in self.item:
|
||||
return "%s: %s" % (self.item['SeriesName'], self.item['Name'])
|
||||
if self.item["Type"] == "Episode" and "SeriesName" in self.item:
|
||||
return "%s: %s" % (self.item["SeriesName"], self.item["Name"])
|
||||
|
||||
elif self.item['Type'] == 'MusicAlbum' and 'AlbumArtist' in self.item:
|
||||
return "%s: %s" % (self.item['AlbumArtist'], self.item['Name'])
|
||||
elif self.item["Type"] == "MusicAlbum" and "AlbumArtist" in self.item:
|
||||
return "%s: %s" % (self.item["AlbumArtist"], self.item["Name"])
|
||||
|
||||
elif self.item['Type'] == 'Audio' and self.item.get('Artists'):
|
||||
return "%s: %s" % (self.item['Artists'][0], self.item['Name'])
|
||||
elif self.item["Type"] == "Audio" and self.item.get("Artists"):
|
||||
return "%s: %s" % (self.item["Artists"][0], self.item["Name"])
|
||||
|
||||
return self.item['Name']
|
||||
return self.item["Name"]
|
||||
|
||||
def get_actors(self):
|
||||
cast = []
|
||||
|
||||
if 'People' in self.item:
|
||||
self.get_people_artwork(self.item['People'])
|
||||
if "People" in self.item:
|
||||
self.get_people_artwork(self.item["People"])
|
||||
|
||||
for person in self.item['People']:
|
||||
for person in self.item["People"]:
|
||||
|
||||
if person['Type'] == "Actor":
|
||||
cast.append({
|
||||
'name': person['Name'],
|
||||
'role': person.get('Role', "Unknown"),
|
||||
'order': len(cast) + 1,
|
||||
'thumbnail': person['imageurl']
|
||||
})
|
||||
if person["Type"] == "Actor":
|
||||
cast.append(
|
||||
{
|
||||
"name": person["Name"],
|
||||
"role": person.get("Role", "Unknown"),
|
||||
"order": len(cast) + 1,
|
||||
"thumbnail": person["imageurl"],
|
||||
}
|
||||
)
|
||||
|
||||
return cast
|
||||
|
||||
def media_streams(self, video, audio, subtitles):
|
||||
return {
|
||||
'video': video or [],
|
||||
'audio': audio or [],
|
||||
'subtitle': subtitles or []
|
||||
}
|
||||
return {"video": video or [], "audio": audio or [], "subtitle": subtitles or []}
|
||||
|
||||
def video_streams(self, tracks, container=None):
|
||||
|
||||
if container:
|
||||
container = container.split(',')[0]
|
||||
container = container.split(",")[0]
|
||||
|
||||
for track in tracks:
|
||||
|
||||
if "DvProfile" in track:
|
||||
track['hdrtype'] = "dolbyvision"
|
||||
elif track.get('VideoRangeType', '') in ["HDR10", "HDR10Plus"]:
|
||||
track['hdrtype'] = "hdr10"
|
||||
elif "HLG" in track.get('VideoRangeType', ''):
|
||||
track['hdrtype'] = "hlg"
|
||||
track["hdrtype"] = "dolbyvision"
|
||||
elif track.get("VideoRangeType", "") in ["HDR10", "HDR10Plus"]:
|
||||
track["hdrtype"] = "hdr10"
|
||||
elif "HLG" in track.get("VideoRangeType", ""):
|
||||
track["hdrtype"] = "hlg"
|
||||
|
||||
track.update({
|
||||
'hdrtype': track.get('hdrtype', "").lower(),
|
||||
'codec': track.get('Codec', "").lower(),
|
||||
'profile': track.get('Profile', "").lower(),
|
||||
'height': track.get('Height'),
|
||||
'width': track.get('Width'),
|
||||
'3d': self.item.get('Video3DFormat'),
|
||||
'aspect': 1.85
|
||||
})
|
||||
track.update(
|
||||
{
|
||||
"hdrtype": track.get("hdrtype", "").lower(),
|
||||
"codec": track.get("Codec", "").lower(),
|
||||
"profile": track.get("Profile", "").lower(),
|
||||
"height": track.get("Height"),
|
||||
"width": track.get("Width"),
|
||||
"3d": self.item.get("Video3DFormat"),
|
||||
"aspect": 1.85,
|
||||
}
|
||||
)
|
||||
|
||||
if "msmpeg4" in track['codec']:
|
||||
track['codec'] = "divx"
|
||||
if "msmpeg4" in track["codec"]:
|
||||
track["codec"] = "divx"
|
||||
|
||||
elif "mpeg4" in track['codec'] and ("simple profile" in track['profile'] or not track['profile']):
|
||||
track['codec'] = "xvid"
|
||||
elif "mpeg4" in track["codec"] and (
|
||||
"simple profile" in track["profile"] or not track["profile"]
|
||||
):
|
||||
track["codec"] = "xvid"
|
||||
|
||||
elif "h264" in track['codec'] and container in ('mp4', 'mov', 'm4v'):
|
||||
track['codec'] = "avc1"
|
||||
elif "h264" in track["codec"] and container in ("mp4", "mov", "m4v"):
|
||||
track["codec"] = "avc1"
|
||||
|
||||
try:
|
||||
width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':')
|
||||
track['aspect'] = round(float(width) / float(height), 6)
|
||||
width, height = self.item.get(
|
||||
"AspectRatio", track.get("AspectRatio", "0")
|
||||
).split(":")
|
||||
track["aspect"] = round(float(width) / float(height), 6)
|
||||
except (ValueError, ZeroDivisionError):
|
||||
|
||||
if track['width'] and track['height']:
|
||||
track['aspect'] = round(float(track['width'] / track['height']), 6)
|
||||
if track["width"] and track["height"]:
|
||||
track["aspect"] = round(float(track["width"] / track["height"]), 6)
|
||||
|
||||
track['duration'] = self.get_runtime()
|
||||
track["duration"] = self.get_runtime()
|
||||
|
||||
return tracks
|
||||
|
||||
@ -115,28 +117,30 @@ class API(object):
|
||||
|
||||
for track in tracks:
|
||||
|
||||
track.update({
|
||||
'codec': track.get('Codec', "").lower(),
|
||||
'profile': track.get('Profile', "").lower(),
|
||||
'channels': track.get('Channels'),
|
||||
'language': track.get('Language')
|
||||
})
|
||||
track.update(
|
||||
{
|
||||
"codec": track.get("Codec", "").lower(),
|
||||
"profile": track.get("Profile", "").lower(),
|
||||
"channels": track.get("Channels"),
|
||||
"language": track.get("Language"),
|
||||
}
|
||||
)
|
||||
|
||||
if "dts-hd ma" in track['profile']:
|
||||
track['codec'] = "dtshd_ma"
|
||||
if "dts-hd ma" in track["profile"]:
|
||||
track["codec"] = "dtshd_ma"
|
||||
|
||||
elif "dts-hd hra" in track['profile']:
|
||||
track['codec'] = "dtshd_hra"
|
||||
elif "dts-hd hra" in track["profile"]:
|
||||
track["codec"] = "dtshd_hra"
|
||||
|
||||
return tracks
|
||||
|
||||
def get_runtime(self):
|
||||
|
||||
try:
|
||||
runtime = self.item['RunTimeTicks'] / 10000000.0
|
||||
runtime = self.item["RunTimeTicks"] / 10000000.0
|
||||
|
||||
except KeyError:
|
||||
runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0
|
||||
runtime = self.item.get("CumulativeRunTimeTicks", 0) / 10000000.0
|
||||
|
||||
return runtime
|
||||
|
||||
@ -146,7 +150,7 @@ class API(object):
|
||||
resume = 0
|
||||
if resume_seconds:
|
||||
resume = round(float(resume_seconds), 6)
|
||||
jumpback = int(settings('resumeJumpBack'))
|
||||
jumpback = int(settings("resumeJumpBack"))
|
||||
if resume > jumpback:
|
||||
# To avoid negative bookmark
|
||||
resume = resume - jumpback
|
||||
@ -156,25 +160,25 @@ class API(object):
|
||||
def validate_studio(self, studio_name):
|
||||
# Convert studio for Kodi to properly detect them
|
||||
studios = {
|
||||
'abc (us)': "ABC",
|
||||
'fox (us)': "FOX",
|
||||
'mtv (us)': "MTV",
|
||||
'showcase (ca)': "Showcase",
|
||||
'wgn america': "WGN",
|
||||
'bravo (us)': "Bravo",
|
||||
'tnt (us)': "TNT",
|
||||
'comedy central': "Comedy Central (US)"
|
||||
"abc (us)": "ABC",
|
||||
"fox (us)": "FOX",
|
||||
"mtv (us)": "MTV",
|
||||
"showcase (ca)": "Showcase",
|
||||
"wgn america": "WGN",
|
||||
"bravo (us)": "Bravo",
|
||||
"tnt (us)": "TNT",
|
||||
"comedy central": "Comedy Central (US)",
|
||||
}
|
||||
return studios.get(studio_name.lower(), studio_name)
|
||||
|
||||
def get_overview(self, overview=None):
|
||||
|
||||
overview = overview or self.item.get('Overview')
|
||||
overview = overview or self.item.get("Overview")
|
||||
|
||||
if not overview:
|
||||
return
|
||||
|
||||
overview = overview.replace("\"", "\'")
|
||||
overview = overview.replace('"', "'")
|
||||
overview = overview.replace("\n", "[CR]")
|
||||
overview = overview.replace("\r", " ")
|
||||
overview = overview.replace("<br>", "[CR]")
|
||||
@ -183,7 +187,7 @@ class API(object):
|
||||
|
||||
def get_mpaa(self, rating=None):
|
||||
|
||||
mpaa = rating or self.item.get('OfficialRating', "")
|
||||
mpaa = rating or self.item.get("OfficialRating", "")
|
||||
|
||||
if mpaa in ("NR", "UR"):
|
||||
# Kodi seems to not like NR, but will accept Not Rated
|
||||
@ -197,112 +201,123 @@ class API(object):
|
||||
def get_file_path(self, path=None):
|
||||
|
||||
if path is None:
|
||||
path = self.item.get('Path')
|
||||
path = self.item.get("Path")
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
if path.startswith('\\\\'):
|
||||
path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/")
|
||||
if path.startswith("\\\\"):
|
||||
path = (
|
||||
path.replace("\\\\", "smb://", 1)
|
||||
.replace("\\\\", "\\")
|
||||
.replace("\\", "/")
|
||||
)
|
||||
|
||||
if 'Container' in self.item:
|
||||
if "Container" in self.item:
|
||||
|
||||
if self.item['Container'] == 'dvd':
|
||||
if self.item["Container"] == "dvd":
|
||||
path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path
|
||||
elif self.item['Container'] == 'bluray':
|
||||
elif self.item["Container"] == "bluray":
|
||||
path = "%s/BDMV/index.bdmv" % path
|
||||
|
||||
path = path.replace('\\\\', "\\")
|
||||
path = path.replace("\\\\", "\\")
|
||||
|
||||
if '\\' in path:
|
||||
path = path.replace('/', "\\")
|
||||
if "\\" in path:
|
||||
path = path.replace("/", "\\")
|
||||
|
||||
if '://' in path:
|
||||
protocol = path.split('://')[0]
|
||||
if "://" in path:
|
||||
protocol = path.split("://")[0]
|
||||
path = path.replace(protocol, protocol.lower())
|
||||
|
||||
return path
|
||||
|
||||
def get_user_artwork(self, user_id):
|
||||
|
||||
''' Get jellyfin user profile picture.
|
||||
'''
|
||||
"""Get jellyfin user profile picture."""
|
||||
return "%s/Users/%s/Images/Primary?Format=original" % (self.server, user_id)
|
||||
|
||||
def get_people_artwork(self, people):
|
||||
|
||||
''' Get people (actor, director, etc) artwork.
|
||||
'''
|
||||
"""Get people (actor, director, etc) artwork."""
|
||||
for person in people:
|
||||
|
||||
if 'PrimaryImageTag' in person:
|
||||
if "PrimaryImageTag" in person:
|
||||
|
||||
query = "&MaxWidth=400&MaxHeight=400&Index=0"
|
||||
person['imageurl'] = self.get_artwork(person['Id'], "Primary", person['PrimaryImageTag'], query)
|
||||
person["imageurl"] = self.get_artwork(
|
||||
person["Id"], "Primary", person["PrimaryImageTag"], query
|
||||
)
|
||||
else:
|
||||
person['imageurl'] = None
|
||||
person["imageurl"] = None
|
||||
|
||||
return people
|
||||
|
||||
def get_all_artwork(self, obj, parent_info=False):
|
||||
"""Get all artwork possible. If parent_info is True,
|
||||
it will fill missing artwork with parent artwork.
|
||||
|
||||
''' Get all artwork possible. If parent_info is True,
|
||||
it will fill missing artwork with parent artwork.
|
||||
|
||||
obj is from objects.Objects().map(item, 'Artwork')
|
||||
'''
|
||||
obj is from objects.Objects().map(item, 'Artwork')
|
||||
"""
|
||||
query = ""
|
||||
all_artwork = {
|
||||
'Primary': "",
|
||||
'BoxRear': "",
|
||||
'Art': "",
|
||||
'Banner': "",
|
||||
'Logo': "",
|
||||
'Thumb': "",
|
||||
'Disc': "",
|
||||
'Backdrop': []
|
||||
"Primary": "",
|
||||
"BoxRear": "",
|
||||
"Art": "",
|
||||
"Banner": "",
|
||||
"Logo": "",
|
||||
"Thumb": "",
|
||||
"Disc": "",
|
||||
"Backdrop": [],
|
||||
}
|
||||
|
||||
if settings('compressArt.bool'):
|
||||
if settings("compressArt.bool"):
|
||||
query = "&Quality=90"
|
||||
|
||||
if not settings('enableCoverArt.bool'):
|
||||
if not settings("enableCoverArt.bool"):
|
||||
query += "&EnableImageEnhancers=false"
|
||||
|
||||
art_maxheight = [360, 480, 600, 720, 1080, -1]
|
||||
maxheight = art_maxheight[int(settings('maxArtResolution') or 5)]
|
||||
maxheight = art_maxheight[int(settings("maxArtResolution") or 5)]
|
||||
if maxheight != -1:
|
||||
query += "&MaxHeight=%d" % maxheight
|
||||
|
||||
all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query)
|
||||
all_artwork["Backdrop"] = self.get_backdrops(
|
||||
obj["Id"], obj["BackdropTags"] or [], query
|
||||
)
|
||||
|
||||
for artwork in (obj['Tags'] or []):
|
||||
all_artwork[artwork] = self.get_artwork(obj['Id'], artwork, obj['Tags'][artwork], query)
|
||||
for artwork in obj["Tags"] or []:
|
||||
all_artwork[artwork] = self.get_artwork(
|
||||
obj["Id"], artwork, obj["Tags"][artwork], query
|
||||
)
|
||||
|
||||
if parent_info:
|
||||
|
||||
if not all_artwork['Backdrop'] and obj['ParentBackdropId']:
|
||||
all_artwork['Backdrop'] = self.get_backdrops(obj['ParentBackdropId'], obj['ParentBackdropTags'], query)
|
||||
if not all_artwork["Backdrop"] and obj["ParentBackdropId"]:
|
||||
all_artwork["Backdrop"] = self.get_backdrops(
|
||||
obj["ParentBackdropId"], obj["ParentBackdropTags"], query
|
||||
)
|
||||
|
||||
for art in ('Logo', 'Art', 'Thumb'):
|
||||
if not all_artwork[art] and obj['Parent%sId' % art]:
|
||||
all_artwork[art] = self.get_artwork(obj['Parent%sId' % art], art, obj['Parent%sTag' % art], query)
|
||||
for art in ("Logo", "Art", "Thumb"):
|
||||
if not all_artwork[art] and obj["Parent%sId" % art]:
|
||||
all_artwork[art] = self.get_artwork(
|
||||
obj["Parent%sId" % art], art, obj["Parent%sTag" % art], query
|
||||
)
|
||||
|
||||
if obj.get('SeriesTag'):
|
||||
all_artwork['Series.Primary'] = self.get_artwork(obj['SeriesId'], "Primary", obj['SeriesTag'], query)
|
||||
if obj.get("SeriesTag"):
|
||||
all_artwork["Series.Primary"] = self.get_artwork(
|
||||
obj["SeriesId"], "Primary", obj["SeriesTag"], query
|
||||
)
|
||||
|
||||
if not all_artwork['Primary']:
|
||||
all_artwork['Primary'] = all_artwork['Series.Primary']
|
||||
if not all_artwork["Primary"]:
|
||||
all_artwork["Primary"] = all_artwork["Series.Primary"]
|
||||
|
||||
elif not all_artwork['Primary'] and obj.get('AlbumId'):
|
||||
all_artwork['Primary'] = self.get_artwork(obj['AlbumId'], "Primary", obj['AlbumTag'], query)
|
||||
elif not all_artwork["Primary"] and obj.get("AlbumId"):
|
||||
all_artwork["Primary"] = self.get_artwork(
|
||||
obj["AlbumId"], "Primary", obj["AlbumTag"], query
|
||||
)
|
||||
|
||||
return all_artwork
|
||||
|
||||
def get_backdrops(self, item_id, tags, query=None):
|
||||
|
||||
''' Get backdrops based of "BackdropImageTags" in the jellyfin object.
|
||||
'''
|
||||
"""Get backdrops based of "BackdropImageTags" in the jellyfin object."""
|
||||
backdrops = []
|
||||
|
||||
if item_id is None:
|
||||
@ -310,15 +325,19 @@ class API(object):
|
||||
|
||||
for index, tag in enumerate(tags):
|
||||
|
||||
artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, (query or ""))
|
||||
artwork = "%s/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (
|
||||
self.server,
|
||||
item_id,
|
||||
index,
|
||||
tag,
|
||||
(query or ""),
|
||||
)
|
||||
backdrops.append(artwork)
|
||||
|
||||
return backdrops
|
||||
|
||||
def get_artwork(self, item_id, image, tag=None, query=None):
|
||||
|
||||
''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc
|
||||
'''
|
||||
"""Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc"""
|
||||
if item_id is None:
|
||||
return ""
|
||||
|
||||
|
@ -9,7 +9,11 @@ import warnings
|
||||
class HTTPException(Exception):
|
||||
# Jellyfin HTTP exception
|
||||
def __init__(self, status, message):
|
||||
warnings.warn(f'{self.__class__.__name__} will be deprecated.', DeprecationWarning, stacklevel=2)
|
||||
warnings.warn(
|
||||
f"{self.__class__.__name__} will be deprecated.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
@ -26,4 +30,5 @@ class PathValidationException(Exception):
|
||||
|
||||
TODO: Investigate the usage of this to see if it can be done better.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
@ -6,6 +6,7 @@ class LazyLogger(object):
|
||||
"""`helper.loghandler.getLogger()` is used everywhere.
|
||||
This class helps to avoid import errors.
|
||||
"""
|
||||
|
||||
__logger = None
|
||||
__logger_name = None
|
||||
|
||||
@ -15,5 +16,6 @@ class LazyLogger(object):
|
||||
def __getattr__(self, name):
|
||||
if self.__logger is None:
|
||||
from .loghandler import getLogger
|
||||
|
||||
self.__logger = getLogger(self.__logger_name)
|
||||
return getattr(self.__logger, name)
|
||||
|
@ -16,8 +16,8 @@ from .utils import translate_path
|
||||
|
||||
##################################################################################################
|
||||
|
||||
__addon__ = xbmcaddon.Addon(id='plugin.video.jellyfin')
|
||||
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
|
||||
__addon__ = xbmcaddon.Addon(id="plugin.video.jellyfin")
|
||||
__pluginpath__ = translate_path(__addon__.getAddonInfo("path"))
|
||||
|
||||
##################################################################################################
|
||||
|
||||
@ -36,17 +36,17 @@ class LogHandler(logging.StreamHandler):
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
|
||||
self.sensitive = {'Token': [], 'Server': []}
|
||||
self.sensitive = {"Token": [], "Server": []}
|
||||
|
||||
for server in database.get_credentials()['Servers']:
|
||||
for server in database.get_credentials()["Servers"]:
|
||||
|
||||
if server.get('AccessToken'):
|
||||
self.sensitive['Token'].append(server['AccessToken'])
|
||||
if server.get("AccessToken"):
|
||||
self.sensitive["Token"].append(server["AccessToken"])
|
||||
|
||||
if server.get('address'):
|
||||
self.sensitive['Server'].append(server['address'].split('://')[1])
|
||||
if server.get("address"):
|
||||
self.sensitive["Server"].append(server["address"].split("://")[1])
|
||||
|
||||
self.mask_info = settings('maskInfo.bool')
|
||||
self.mask_info = settings("maskInfo.bool")
|
||||
|
||||
if kodi_version() > 18:
|
||||
self.level = xbmc.LOGINFO
|
||||
@ -59,10 +59,10 @@ class LogHandler(logging.StreamHandler):
|
||||
string = self.format(record)
|
||||
|
||||
if self.mask_info:
|
||||
for server in self.sensitive['Server']:
|
||||
for server in self.sensitive["Server"]:
|
||||
string = string.replace(server or "{server}", "{jellyfin-server}")
|
||||
|
||||
for token in self.sensitive['Token']:
|
||||
for token in self.sensitive["Token"]:
|
||||
string = string.replace(token or "{token}", "{jellyfin-token}")
|
||||
|
||||
xbmc.log(string, level=self.level)
|
||||
@ -74,10 +74,10 @@ class LogHandler(logging.StreamHandler):
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
logging.DEBUG: 2,
|
||||
}
|
||||
try:
|
||||
log_level = int(settings('logLevel'))
|
||||
log_level = int(settings("logLevel"))
|
||||
except ValueError:
|
||||
log_level = 2 # If getting settings fail, we probably want debug logging.
|
||||
|
||||
@ -86,7 +86,9 @@ class LogHandler(logging.StreamHandler):
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'):
|
||||
def __init__(
|
||||
self, fmt="%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s"
|
||||
):
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
@ -116,14 +118,14 @@ class MyFormatter(logging.Formatter):
|
||||
|
||||
res.append(o)
|
||||
|
||||
return ''.join(res)
|
||||
return "".join(res)
|
||||
|
||||
def _gen_rel_path(self, record):
|
||||
if record.pathname:
|
||||
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
|
||||
|
||||
|
||||
__LOGGER = logging.getLogger('JELLYFIN')
|
||||
__LOGGER = logging.getLogger("JELLYFIN")
|
||||
for handler in __LOGGER.handlers:
|
||||
__LOGGER.removeHandler(handler)
|
||||
|
||||
|
@ -26,81 +26,81 @@ class Transcode(object):
|
||||
Disabled = 3
|
||||
MediaDefault = 4
|
||||
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def set_properties(item, method, server_id=None):
|
||||
"""Set all properties for playback detection."""
|
||||
info = item.get("PlaybackInfo") or {}
|
||||
|
||||
''' Set all properties for playback detection.
|
||||
'''
|
||||
info = item.get('PlaybackInfo') or {}
|
||||
current = window("jellyfin_play.json") or []
|
||||
current.append(
|
||||
{
|
||||
"Type": item["Type"],
|
||||
"Id": item["Id"],
|
||||
"Path": info["Path"],
|
||||
"PlayMethod": method,
|
||||
"PlayOption": "Addon" if info.get("PlaySessionId") else "Native",
|
||||
"MediaSourceId": info.get("MediaSourceId", item["Id"]),
|
||||
"Runtime": item.get("RunTimeTicks"),
|
||||
"PlaySessionId": info.get("PlaySessionId", str(uuid4()).replace("-", "")),
|
||||
"ServerId": server_id,
|
||||
"DeviceId": client.get_device_id(),
|
||||
"SubsMapping": info.get("Subtitles"),
|
||||
"AudioStreamIndex": info.get("AudioStreamIndex"),
|
||||
"SubtitleStreamIndex": info.get("SubtitleStreamIndex"),
|
||||
"CurrentPosition": info.get("CurrentPosition"),
|
||||
"CurrentEpisode": info.get("CurrentEpisode"),
|
||||
}
|
||||
)
|
||||
|
||||
current = window('jellyfin_play.json') or []
|
||||
current.append({
|
||||
'Type': item['Type'],
|
||||
'Id': item['Id'],
|
||||
'Path': info['Path'],
|
||||
'PlayMethod': method,
|
||||
'PlayOption': 'Addon' if info.get('PlaySessionId') else 'Native',
|
||||
'MediaSourceId': info.get('MediaSourceId', item['Id']),
|
||||
'Runtime': item.get('RunTimeTicks'),
|
||||
'PlaySessionId': info.get('PlaySessionId', str(uuid4()).replace("-", "")),
|
||||
'ServerId': server_id,
|
||||
'DeviceId': client.get_device_id(),
|
||||
'SubsMapping': info.get('Subtitles'),
|
||||
'AudioStreamIndex': info.get('AudioStreamIndex'),
|
||||
'SubtitleStreamIndex': info.get('SubtitleStreamIndex'),
|
||||
'CurrentPosition': info.get('CurrentPosition'),
|
||||
'CurrentEpisode': info.get('CurrentEpisode')
|
||||
})
|
||||
|
||||
window('jellyfin_play.json', current)
|
||||
window("jellyfin_play.json", current)
|
||||
|
||||
|
||||
class PlayUtils(object):
|
||||
|
||||
def __init__(self, item, force_transcode=False, server_id=None, server=None, api_client=None):
|
||||
|
||||
''' Item will be updated with the property PlaybackInfo, which
|
||||
holds all the playback information.
|
||||
'''
|
||||
def __init__(
|
||||
self, item, force_transcode=False, server_id=None, server=None, api_client=None
|
||||
):
|
||||
"""Item will be updated with the property PlaybackInfo, which
|
||||
holds all the playback information.
|
||||
"""
|
||||
self.item = item
|
||||
self.item['PlaybackInfo'] = {}
|
||||
self.item["PlaybackInfo"] = {}
|
||||
self.api_client = api_client
|
||||
self.info = {
|
||||
'ServerId': server_id,
|
||||
'ServerAddress': server,
|
||||
'ForceTranscode': force_transcode,
|
||||
'Token': api_client.config.data['auth.token']
|
||||
"ServerId": server_id,
|
||||
"ServerAddress": server,
|
||||
"ForceTranscode": force_transcode,
|
||||
"Token": api_client.config.data["auth.token"],
|
||||
}
|
||||
|
||||
def get_sources(self, source_id=None):
|
||||
|
||||
''' Return sources based on the optional source_id or the device profile.
|
||||
'''
|
||||
info = self.api_client.get_play_info(self.item['Id'], self.get_device_profile())
|
||||
"""Return sources based on the optional source_id or the device profile."""
|
||||
info = self.api_client.get_play_info(self.item["Id"], self.get_device_profile())
|
||||
LOG.info(info)
|
||||
self.info['PlaySessionId'] = info['PlaySessionId']
|
||||
self.info["PlaySessionId"] = info["PlaySessionId"]
|
||||
sources = []
|
||||
|
||||
if not info.get('MediaSources'):
|
||||
if not info.get("MediaSources"):
|
||||
LOG.info("No MediaSources found.")
|
||||
|
||||
elif source_id:
|
||||
for source in info:
|
||||
|
||||
if source['Id'] == source_id:
|
||||
if source["Id"] == source_id:
|
||||
sources.append(source)
|
||||
|
||||
break
|
||||
|
||||
elif not self.is_selection(info) or len(info['MediaSources']) == 1:
|
||||
elif not self.is_selection(info) or len(info["MediaSources"]) == 1:
|
||||
|
||||
LOG.info("Skip source selection.")
|
||||
sources.append(info['MediaSources'][0])
|
||||
sources.append(info["MediaSources"][0])
|
||||
|
||||
else:
|
||||
sources.extend([x for x in info['MediaSources']])
|
||||
sources.extend([x for x in info["MediaSources"]])
|
||||
|
||||
return sources
|
||||
|
||||
@ -110,7 +110,7 @@ class PlayUtils(object):
|
||||
selection = []
|
||||
|
||||
for source in sources:
|
||||
selection.append(source.get('Name', "na"))
|
||||
selection.append(source.get("Name", "na"))
|
||||
|
||||
resp = dialog("select", translate(33130), selection)
|
||||
|
||||
@ -127,25 +127,23 @@ class PlayUtils(object):
|
||||
return source
|
||||
|
||||
def is_selection(self, sources):
|
||||
|
||||
''' Do not allow source selection for.
|
||||
'''
|
||||
if self.item['MediaType'] != 'Video':
|
||||
"""Do not allow source selection for."""
|
||||
if self.item["MediaType"] != "Video":
|
||||
LOG.debug("MediaType is not a video.")
|
||||
|
||||
return False
|
||||
|
||||
elif self.item['Type'] == 'TvChannel':
|
||||
elif self.item["Type"] == "TvChannel":
|
||||
LOG.debug("TvChannel detected.")
|
||||
|
||||
return False
|
||||
|
||||
elif len(sources) == 1 and sources[0]['Type'] == 'Placeholder':
|
||||
elif len(sources) == 1 and sources[0]["Type"] == "Placeholder":
|
||||
LOG.debug("Placeholder detected.")
|
||||
|
||||
return False
|
||||
|
||||
elif 'SourceType' in self.item and self.item['SourceType'] != 'Library':
|
||||
elif "SourceType" in self.item and self.item["SourceType"] != "Library":
|
||||
LOG.debug("SourceType not from library.")
|
||||
|
||||
return False
|
||||
@ -156,7 +154,7 @@ class PlayUtils(object):
|
||||
|
||||
self.direct_play(source)
|
||||
|
||||
if xbmcvfs.exists(self.info['Path']):
|
||||
if xbmcvfs.exists(self.info["Path"]):
|
||||
LOG.info("Path exists.")
|
||||
|
||||
return True
|
||||
@ -167,7 +165,7 @@ class PlayUtils(object):
|
||||
|
||||
def is_strm(self, source):
|
||||
|
||||
if source.get('Container') == 'strm' or self.item['Path'].endswith('.strm'):
|
||||
if source.get("Container") == "strm" or self.item["Path"].endswith(".strm"):
|
||||
LOG.info("strm detected")
|
||||
|
||||
return True
|
||||
@ -175,31 +173,37 @@ class PlayUtils(object):
|
||||
return False
|
||||
|
||||
def get(self, source, audio=None, subtitle=None):
|
||||
"""The server returns sources based on the MaxStreamingBitrate value and other filters.
|
||||
prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working?
|
||||
"""
|
||||
self.info["MediaSourceId"] = source["Id"]
|
||||
|
||||
''' The server returns sources based on the MaxStreamingBitrate value and other filters.
|
||||
prop: jellyfinfilename for ?? I thought it was to pass the real path to subtitle add-ons but it's not working?
|
||||
'''
|
||||
self.info['MediaSourceId'] = source['Id']
|
||||
if source.get("RequiresClosing"):
|
||||
|
||||
if source.get('RequiresClosing'):
|
||||
"""Server returning live tv stream for direct play is hardcoded with 127.0.0.1."""
|
||||
self.info["LiveStreamId"] = source["LiveStreamId"]
|
||||
source["SupportsDirectPlay"] = False
|
||||
source["Protocol"] = "LiveTV"
|
||||
|
||||
''' Server returning live tv stream for direct play is hardcoded with 127.0.0.1.
|
||||
'''
|
||||
self.info['LiveStreamId'] = source['LiveStreamId']
|
||||
source['SupportsDirectPlay'] = False
|
||||
source['Protocol'] = "LiveTV"
|
||||
if self.info["ForceTranscode"]:
|
||||
|
||||
if self.info['ForceTranscode']:
|
||||
source["SupportsDirectPlay"] = False
|
||||
source["SupportsDirectStream"] = False
|
||||
|
||||
source['SupportsDirectPlay'] = False
|
||||
source['SupportsDirectStream'] = False
|
||||
|
||||
if source.get('Protocol') == 'Http' or source['SupportsDirectPlay'] and (self.is_strm(source) or not settings('playFromStream.bool') and self.is_file_exists(source)):
|
||||
if (
|
||||
source.get("Protocol") == "Http"
|
||||
or source["SupportsDirectPlay"]
|
||||
and (
|
||||
self.is_strm(source)
|
||||
or not settings("playFromStream.bool")
|
||||
and self.is_file_exists(source)
|
||||
)
|
||||
):
|
||||
|
||||
LOG.info("--[ direct play ]")
|
||||
self.direct_play(source)
|
||||
|
||||
elif source['SupportsDirectStream'] or source['SupportsDirectPlay']:
|
||||
elif source["SupportsDirectStream"] or source["SupportsDirectPlay"]:
|
||||
|
||||
LOG.info("--[ direct stream ]")
|
||||
self.direct_url(source)
|
||||
@ -208,158 +212,209 @@ class PlayUtils(object):
|
||||
LOG.info("--[ transcode ]")
|
||||
self.transcode(source, audio, subtitle)
|
||||
|
||||
self.info['AudioStreamIndex'] = self.info.get('AudioStreamIndex') or source.get('DefaultAudioStreamIndex')
|
||||
self.info['SubtitleStreamIndex'] = self.info.get('SubtitleStreamIndex') or source.get('DefaultSubtitleStreamIndex')
|
||||
self.item['PlaybackInfo'].update(self.info)
|
||||
self.info["AudioStreamIndex"] = self.info.get("AudioStreamIndex") or source.get(
|
||||
"DefaultAudioStreamIndex"
|
||||
)
|
||||
self.info["SubtitleStreamIndex"] = self.info.get(
|
||||
"SubtitleStreamIndex"
|
||||
) or source.get("DefaultSubtitleStreamIndex")
|
||||
self.item["PlaybackInfo"].update(self.info)
|
||||
|
||||
API = api.API(self.item, self.info['ServerAddress'])
|
||||
window('jellyfinfilename', value=API.get_file_path(source.get('Path')))
|
||||
API = api.API(self.item, self.info["ServerAddress"])
|
||||
window("jellyfinfilename", value=API.get_file_path(source.get("Path")))
|
||||
|
||||
def live_stream(self, source):
|
||||
|
||||
''' Get live stream media info.
|
||||
'''
|
||||
info = self.api_client.get_live_stream(self.item['Id'], self.info['PlaySessionId'], source['OpenToken'], self.get_device_profile())
|
||||
"""Get live stream media info."""
|
||||
info = self.api_client.get_live_stream(
|
||||
self.item["Id"],
|
||||
self.info["PlaySessionId"],
|
||||
source["OpenToken"],
|
||||
self.get_device_profile(),
|
||||
)
|
||||
LOG.info(info)
|
||||
|
||||
if info['MediaSource'].get('RequiresClosing'):
|
||||
self.info['LiveStreamId'] = source['LiveStreamId']
|
||||
if info["MediaSource"].get("RequiresClosing"):
|
||||
self.info["LiveStreamId"] = source["LiveStreamId"]
|
||||
|
||||
return info['MediaSource']
|
||||
return info["MediaSource"]
|
||||
|
||||
def transcode(self, source, audio=None, subtitle=None):
|
||||
|
||||
if 'TranscodingUrl' not in source:
|
||||
if "TranscodingUrl" not in source:
|
||||
raise Exception("use get_sources to get transcoding url")
|
||||
|
||||
self.info['Method'] = "Transcode"
|
||||
self.info["Method"] = "Transcode"
|
||||
|
||||
if self.item['MediaType'] == 'Video':
|
||||
base, params = source['TranscodingUrl'].split('?')
|
||||
url_parsed = params.split('&')
|
||||
manual_tracks = ''
|
||||
if self.item["MediaType"] == "Video":
|
||||
base, params = source["TranscodingUrl"].split("?")
|
||||
url_parsed = params.split("&")
|
||||
manual_tracks = ""
|
||||
|
||||
# manual bitrate
|
||||
url_parsed = [p for p in url_parsed if 'AudioBitrate' not in p and 'VideoBitrate' not in p]
|
||||
url_parsed = [
|
||||
p
|
||||
for p in url_parsed
|
||||
if "AudioBitrate" not in p and "VideoBitrate" not in p
|
||||
]
|
||||
|
||||
if settings('skipDialogTranscode') != Transcode.Enabled and source.get('MediaStreams'):
|
||||
if settings("skipDialogTranscode") != Transcode.Enabled and source.get(
|
||||
"MediaStreams"
|
||||
):
|
||||
# manual tracks
|
||||
url_parsed = [p for p in url_parsed if 'AudioStreamIndex' not in p and 'SubtitleStreamIndex' not in p]
|
||||
url_parsed = [
|
||||
p
|
||||
for p in url_parsed
|
||||
if "AudioStreamIndex" not in p and "SubtitleStreamIndex" not in p
|
||||
]
|
||||
manual_tracks = self.get_audio_subs(source, audio, subtitle)
|
||||
|
||||
audio_bitrate = self.get_transcoding_audio_bitrate()
|
||||
video_bitrate = self.get_max_bitrate() - audio_bitrate
|
||||
|
||||
params = "%s%s" % ('&'.join(url_parsed), manual_tracks)
|
||||
params += "&VideoBitrate=%s&AudioBitrate=%s" % (video_bitrate, audio_bitrate)
|
||||
params = "%s%s" % ("&".join(url_parsed), manual_tracks)
|
||||
params += "&VideoBitrate=%s&AudioBitrate=%s" % (
|
||||
video_bitrate,
|
||||
audio_bitrate,
|
||||
)
|
||||
|
||||
video_type = 'live' if source['Protocol'] == 'LiveTV' else 'master'
|
||||
base = base.replace('stream' if 'stream' in base else 'master', video_type, 1)
|
||||
self.info['Path'] = "%s%s?%s" % (self.info['ServerAddress'], base, params)
|
||||
self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
|
||||
video_type = "live" if source["Protocol"] == "LiveTV" else "master"
|
||||
base = base.replace(
|
||||
"stream" if "stream" in base else "master", video_type, 1
|
||||
)
|
||||
self.info["Path"] = "%s%s?%s" % (self.info["ServerAddress"], base, params)
|
||||
self.info["Path"] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
|
||||
else:
|
||||
self.info['Path'] = "%s/%s" % (self.info['ServerAddress'], source['TranscodingUrl'])
|
||||
self.info["Path"] = "%s/%s" % (
|
||||
self.info["ServerAddress"],
|
||||
source["TranscodingUrl"],
|
||||
)
|
||||
|
||||
return self.info['Path']
|
||||
return self.info["Path"]
|
||||
|
||||
def direct_play(self, source):
|
||||
|
||||
API = api.API(self.item, self.info['ServerAddress'])
|
||||
self.info['Method'] = "DirectPlay"
|
||||
self.info['Path'] = API.get_file_path(source.get('Path'))
|
||||
API = api.API(self.item, self.info["ServerAddress"])
|
||||
self.info["Method"] = "DirectPlay"
|
||||
self.info["Path"] = API.get_file_path(source.get("Path"))
|
||||
|
||||
return self.info['Path']
|
||||
return self.info["Path"]
|
||||
|
||||
def direct_url(self, source):
|
||||
|
||||
self.info['Method'] = "DirectStream"
|
||||
self.info["Method"] = "DirectStream"
|
||||
|
||||
if self.item['Type'] == "Audio":
|
||||
self.info['Path'] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % (
|
||||
self.info['ServerAddress'],
|
||||
self.item['Id'],
|
||||
source.get('Container', "mp4").split(',')[0],
|
||||
self.info['Token']
|
||||
if self.item["Type"] == "Audio":
|
||||
self.info["Path"] = "%s/Audio/%s/stream.%s?static=true&api_key=%s" % (
|
||||
self.info["ServerAddress"],
|
||||
self.item["Id"],
|
||||
source.get("Container", "mp4").split(",")[0],
|
||||
self.info["Token"],
|
||||
)
|
||||
else:
|
||||
self.info['Path'] = "%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s" % (
|
||||
self.info['ServerAddress'],
|
||||
self.item['Id'],
|
||||
source['Id'],
|
||||
self.info['Token']
|
||||
self.info["Path"] = (
|
||||
"%s/Videos/%s/stream?static=true&MediaSourceId=%s&api_key=%s"
|
||||
% (
|
||||
self.info["ServerAddress"],
|
||||
self.item["Id"],
|
||||
source["Id"],
|
||||
self.info["Token"],
|
||||
)
|
||||
)
|
||||
|
||||
return self.info['Path']
|
||||
return self.info["Path"]
|
||||
|
||||
def get_max_bitrate(self):
|
||||
|
||||
''' Get the video quality based on add-on settings.
|
||||
Max bit rate supported by server: 2147483 (max signed 32bit integer)
|
||||
'''
|
||||
bitrate = [500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000,
|
||||
7000, 8000, 9000, 10000, 12000, 14000, 16000, 18000,
|
||||
20000, 25000, 30000, 35000, 40000, 100000, 1000000, 2147483]
|
||||
return bitrate[int(settings('maxBitrate') or 24)] * 1000
|
||||
"""Get the video quality based on add-on settings.
|
||||
Max bit rate supported by server: 2147483 (max signed 32bit integer)
|
||||
"""
|
||||
bitrate = [
|
||||
500,
|
||||
1000,
|
||||
1500,
|
||||
2000,
|
||||
2500,
|
||||
3000,
|
||||
4000,
|
||||
5000,
|
||||
6000,
|
||||
7000,
|
||||
8000,
|
||||
9000,
|
||||
10000,
|
||||
12000,
|
||||
14000,
|
||||
16000,
|
||||
18000,
|
||||
20000,
|
||||
25000,
|
||||
30000,
|
||||
35000,
|
||||
40000,
|
||||
100000,
|
||||
1000000,
|
||||
2147483,
|
||||
]
|
||||
return bitrate[int(settings("maxBitrate") or 24)] * 1000
|
||||
|
||||
def get_resolution(self):
|
||||
return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight'))
|
||||
return int(xbmc.getInfoLabel("System.ScreenWidth")), int(
|
||||
xbmc.getInfoLabel("System.ScreenHeight")
|
||||
)
|
||||
|
||||
def get_directplay_video_codec(self):
|
||||
codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1', 'vp9', 'av1']
|
||||
codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1", "vp9", "av1"]
|
||||
|
||||
if settings('transcode_h265.bool'):
|
||||
codecs.remove('hevc')
|
||||
codecs.remove('h265')
|
||||
if settings("transcode_h265.bool"):
|
||||
codecs.remove("hevc")
|
||||
codecs.remove("h265")
|
||||
|
||||
if settings('transcode_mpeg2.bool'):
|
||||
codecs.remove('mpeg2video')
|
||||
if settings("transcode_mpeg2.bool"):
|
||||
codecs.remove("mpeg2video")
|
||||
|
||||
if settings('transcode_vc1.bool'):
|
||||
codecs.remove('vc1')
|
||||
if settings("transcode_vc1.bool"):
|
||||
codecs.remove("vc1")
|
||||
|
||||
if settings('transcode_vp9.bool'):
|
||||
codecs.remove('vp9')
|
||||
if settings("transcode_vp9.bool"):
|
||||
codecs.remove("vp9")
|
||||
|
||||
if settings('transcode_av1.bool'):
|
||||
codecs.remove('av1')
|
||||
if settings("transcode_av1.bool"):
|
||||
codecs.remove("av1")
|
||||
|
||||
return ','.join(codecs)
|
||||
return ",".join(codecs)
|
||||
|
||||
def get_transcoding_video_codec(self):
|
||||
codecs = ['h264', 'hevc', 'h265', 'mpeg4', 'mpeg2video', 'vc1']
|
||||
codecs = ["h264", "hevc", "h265", "mpeg4", "mpeg2video", "vc1"]
|
||||
|
||||
if settings('transcode_h265.bool'):
|
||||
codecs.remove('hevc')
|
||||
codecs.remove('h265')
|
||||
if settings("transcode_h265.bool"):
|
||||
codecs.remove("hevc")
|
||||
codecs.remove("h265")
|
||||
else:
|
||||
if settings('videoPreferredCodec') == 'H265/HEVC':
|
||||
codecs.insert(2, codecs.pop(codecs.index('h264')))
|
||||
if settings("videoPreferredCodec") == "H265/HEVC":
|
||||
codecs.insert(2, codecs.pop(codecs.index("h264")))
|
||||
|
||||
if settings('transcode_mpeg2.bool'):
|
||||
codecs.remove('mpeg2video')
|
||||
if settings("transcode_mpeg2.bool"):
|
||||
codecs.remove("mpeg2video")
|
||||
|
||||
if settings('transcode_vc1.bool'):
|
||||
codecs.remove('vc1')
|
||||
if settings("transcode_vc1.bool"):
|
||||
codecs.remove("vc1")
|
||||
|
||||
return ','.join(codecs)
|
||||
return ",".join(codecs)
|
||||
|
||||
def get_transcoding_audio_codec(self):
|
||||
codecs = ['aac', 'mp3', 'ac3', 'opus', 'flac', 'vorbis']
|
||||
codecs = ["aac", "mp3", "ac3", "opus", "flac", "vorbis"]
|
||||
|
||||
preferred = settings('audioPreferredCodec').lower()
|
||||
preferred = settings("audioPreferredCodec").lower()
|
||||
if preferred in codecs:
|
||||
codecs.insert(0, codecs.pop(codecs.index(preferred)))
|
||||
|
||||
return ','.join(codecs)
|
||||
return ",".join(codecs)
|
||||
|
||||
def get_transcoding_audio_bitrate(self):
|
||||
bitrate = [96, 128, 160, 192, 256, 320, 384]
|
||||
return bitrate[int(settings('audioBitrate') or 6)] * 1000
|
||||
return bitrate[int(settings("audioBitrate") or 6)] * 1000
|
||||
|
||||
def get_device_profile(self):
|
||||
|
||||
''' Get device profile based on the add-on settings.
|
||||
'''
|
||||
"""Get device profile based on the add-on settings."""
|
||||
profile = {
|
||||
"Name": "Kodi",
|
||||
"MaxStaticBitrate": self.get_max_bitrate(),
|
||||
@ -372,154 +427,96 @@ class PlayUtils(object):
|
||||
"Container": "m3u8",
|
||||
"AudioCodec": self.get_transcoding_audio_codec(),
|
||||
"VideoCodec": self.get_transcoding_video_codec(),
|
||||
"MaxAudioChannels": settings('audioMaxChannels')
|
||||
"MaxAudioChannels": settings("audioMaxChannels"),
|
||||
},
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Type": "Photo",
|
||||
"Container": "jpeg"
|
||||
}
|
||||
{"Type": "Audio"},
|
||||
{"Type": "Photo", "Container": "jpeg"},
|
||||
],
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Type": "Video",
|
||||
"VideoCodec": self.get_directplay_video_codec()
|
||||
},
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Type": "Photo"
|
||||
}
|
||||
{"Type": "Video", "VideoCodec": self.get_directplay_video_codec()},
|
||||
{"Type": "Audio"},
|
||||
{"Type": "Photo"},
|
||||
],
|
||||
"ResponseProfiles": [],
|
||||
"ContainerProfiles": [],
|
||||
"CodecProfiles": [],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "External"
|
||||
}
|
||||
]
|
||||
{"Format": "srt", "Method": "External"},
|
||||
{"Format": "srt", "Method": "Embed"},
|
||||
{"Format": "ass", "Method": "External"},
|
||||
{"Format": "ass", "Method": "Embed"},
|
||||
{"Format": "sub", "Method": "Embed"},
|
||||
{"Format": "sub", "Method": "External"},
|
||||
{"Format": "ssa", "Method": "Embed"},
|
||||
{"Format": "ssa", "Method": "External"},
|
||||
{"Format": "smi", "Method": "Embed"},
|
||||
{"Format": "smi", "Method": "External"},
|
||||
{"Format": "pgssub", "Method": "Embed"},
|
||||
{"Format": "pgssub", "Method": "External"},
|
||||
{"Format": "dvdsub", "Method": "Embed"},
|
||||
{"Format": "dvdsub", "Method": "External"},
|
||||
{"Format": "pgs", "Method": "Embed"},
|
||||
{"Format": "pgs", "Method": "External"},
|
||||
],
|
||||
}
|
||||
|
||||
if settings('transcodeHi10P.bool'):
|
||||
profile['CodecProfiles'].append(
|
||||
if settings("transcodeHi10P.bool"):
|
||||
profile["CodecProfiles"].append(
|
||||
{
|
||||
'Type': 'Video',
|
||||
'codec': 'h264',
|
||||
'Conditions': [
|
||||
"Type": "Video",
|
||||
"codec": "h264",
|
||||
"Conditions": [
|
||||
{
|
||||
'Condition': "LessThanEqual",
|
||||
'Property': "VideoBitDepth",
|
||||
'Value': "8"
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoBitDepth",
|
||||
"Value": "8",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if settings('transcode_h265_rext.bool'):
|
||||
profile['CodecProfiles'].append(
|
||||
if settings("transcode_h265_rext.bool"):
|
||||
profile["CodecProfiles"].append(
|
||||
{
|
||||
'Type': 'Video',
|
||||
'codec': 'h265,hevc',
|
||||
'Conditions': [
|
||||
"Type": "Video",
|
||||
"codec": "h265,hevc",
|
||||
"Conditions": [
|
||||
{
|
||||
'Condition': "EqualsAny",
|
||||
'Property': "VideoProfile",
|
||||
'Value': "main|main 10"
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "main|main 10",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if self.info['ForceTranscode']:
|
||||
profile['DirectPlayProfiles'] = []
|
||||
if self.info["ForceTranscode"]:
|
||||
profile["DirectPlayProfiles"] = []
|
||||
|
||||
if self.item['Type'] == 'TvChannel':
|
||||
profile['TranscodingProfiles'].insert(0, {
|
||||
"Container": "ts",
|
||||
"Type": "Video",
|
||||
"AudioCodec": "mp3,aac",
|
||||
"VideoCodec": "h264",
|
||||
"Context": "Streaming",
|
||||
"Protocol": "hls",
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": "1",
|
||||
"BreakOnNonKeyFrames": True
|
||||
})
|
||||
if self.item["Type"] == "TvChannel":
|
||||
profile["TranscodingProfiles"].insert(
|
||||
0,
|
||||
{
|
||||
"Container": "ts",
|
||||
"Type": "Video",
|
||||
"AudioCodec": "mp3,aac",
|
||||
"VideoCodec": "h264",
|
||||
"Context": "Streaming",
|
||||
"Protocol": "hls",
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": "1",
|
||||
"BreakOnNonKeyFrames": True,
|
||||
},
|
||||
)
|
||||
|
||||
return profile
|
||||
|
||||
def set_external_subs(self, source, listitem):
|
||||
|
||||
''' Try to download external subs locally, so we can label them.
|
||||
Since Jellyfin returns all possible tracks together, sort them.
|
||||
IsTextSubtitleStream if true, is available to download from server.
|
||||
'''
|
||||
if not settings('enableExternalSubs.bool') or not source['MediaStreams']:
|
||||
"""Try to download external subs locally, so we can label them.
|
||||
Since Jellyfin returns all possible tracks together, sort them.
|
||||
IsTextSubtitleStream if true, is available to download from server.
|
||||
"""
|
||||
if not settings("enableExternalSubs.bool") or not source["MediaStreams"]:
|
||||
return
|
||||
|
||||
subs = []
|
||||
@ -528,12 +525,19 @@ class PlayUtils(object):
|
||||
|
||||
server_settings = self.api_client.get_transcode_settings()
|
||||
|
||||
for stream in source['MediaStreams']:
|
||||
if stream['SupportsExternalStream'] and stream['Type'] == 'Subtitle' and stream['DeliveryMethod'] == 'External':
|
||||
if not stream['IsExternal'] and not server_settings['EnableSubtitleExtraction']:
|
||||
for stream in source["MediaStreams"]:
|
||||
if (
|
||||
stream["SupportsExternalStream"]
|
||||
and stream["Type"] == "Subtitle"
|
||||
and stream["DeliveryMethod"] == "External"
|
||||
):
|
||||
if (
|
||||
not stream["IsExternal"]
|
||||
and not server_settings["EnableSubtitleExtraction"]
|
||||
):
|
||||
continue
|
||||
|
||||
index = stream['Index']
|
||||
index = stream["Index"]
|
||||
url = self.get_subtitles(source, stream, index)
|
||||
|
||||
if url is None:
|
||||
@ -541,8 +545,12 @@ class PlayUtils(object):
|
||||
|
||||
LOG.info("[ subtitles/%s ] %s", index, url)
|
||||
|
||||
if 'Language' in stream:
|
||||
filename = "%s.%s.%s" % (source['Id'], stream['Language'], stream['Codec'])
|
||||
if "Language" in stream:
|
||||
filename = "%s.%s.%s" % (
|
||||
source["Id"],
|
||||
stream["Language"],
|
||||
stream["Codec"],
|
||||
)
|
||||
|
||||
try:
|
||||
subs.append(self.download_external_subs(url, filename))
|
||||
@ -556,15 +564,16 @@ class PlayUtils(object):
|
||||
kodi += 1
|
||||
|
||||
listitem.setSubtitles(subs)
|
||||
self.item['PlaybackInfo']['Subtitles'] = mapping
|
||||
self.item["PlaybackInfo"]["Subtitles"] = mapping
|
||||
|
||||
@classmethod
|
||||
def download_external_subs(cls, src, filename):
|
||||
|
||||
''' Download external subtitles to temp folder
|
||||
to be able to have proper names to streams.
|
||||
'''
|
||||
temp = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/")
|
||||
"""Download external subtitles to temp folder
|
||||
to be able to have proper names to streams.
|
||||
"""
|
||||
temp = translate_path(
|
||||
"special://profile/addon_data/plugin.video.jellyfin/temp/"
|
||||
)
|
||||
|
||||
if not xbmcvfs.exists(temp):
|
||||
xbmcvfs.mkdir(temp)
|
||||
@ -572,59 +581,61 @@ class PlayUtils(object):
|
||||
path = os.path.join(temp, filename)
|
||||
|
||||
try:
|
||||
response = requests.get(src, stream=True, verify=settings('sslverify.bool'))
|
||||
response = requests.get(src, stream=True, verify=settings("sslverify.bool"))
|
||||
response.raise_for_status()
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
raise
|
||||
else:
|
||||
response.encoding = 'utf-8'
|
||||
with open(path, 'wb') as f:
|
||||
response.encoding = "utf-8"
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.content)
|
||||
del response
|
||||
|
||||
return path
|
||||
|
||||
def get_audio_subs(self, source, audio=None, subtitle=None):
|
||||
"""For transcoding only
|
||||
Present the list of audio/subs to select from, before playback starts.
|
||||
|
||||
''' For transcoding only
|
||||
Present the list of audio/subs to select from, before playback starts.
|
||||
|
||||
Since Jellyfin returns all possible tracks together, sort them.
|
||||
IsTextSubtitleStream if true, is available to download from server.
|
||||
'''
|
||||
Since Jellyfin returns all possible tracks together, sort them.
|
||||
IsTextSubtitleStream if true, is available to download from server.
|
||||
"""
|
||||
prefs = ""
|
||||
audio_streams = list()
|
||||
subs_streams = list()
|
||||
streams = source['MediaStreams']
|
||||
streams = source["MediaStreams"]
|
||||
|
||||
server_settings = self.api_client.get_transcode_settings()
|
||||
allow_burned_subs = settings('allowBurnedSubs.bool')
|
||||
allow_burned_subs = settings("allowBurnedSubs.bool")
|
||||
|
||||
for stream in streams:
|
||||
|
||||
index = stream['Index']
|
||||
stream_type = stream['Type']
|
||||
index = stream["Index"]
|
||||
stream_type = stream["Type"]
|
||||
|
||||
if stream_type == 'Audio':
|
||||
if stream_type == "Audio":
|
||||
|
||||
audio_streams.append(index)
|
||||
|
||||
elif stream_type == 'Subtitle':
|
||||
if stream['IsExternal']:
|
||||
if not stream['SupportsExternalStream'] and not allow_burned_subs:
|
||||
elif stream_type == "Subtitle":
|
||||
if stream["IsExternal"]:
|
||||
if not stream["SupportsExternalStream"] and not allow_burned_subs:
|
||||
continue
|
||||
else:
|
||||
avail_for_extraction = stream['SupportsExternalStream'] and server_settings['EnableSubtitleExtraction']
|
||||
avail_for_extraction = (
|
||||
stream["SupportsExternalStream"]
|
||||
and server_settings["EnableSubtitleExtraction"]
|
||||
)
|
||||
if not avail_for_extraction and not allow_burned_subs:
|
||||
continue
|
||||
|
||||
subs_streams.append(index)
|
||||
|
||||
skip_dialog = int(settings('skipDialogTranscode') or 0)
|
||||
skip_dialog = int(settings("skipDialogTranscode") or 0)
|
||||
|
||||
def get_track_title(track_index):
|
||||
return streams[track_index]['DisplayTitle'] or ("Track %s" % track_index)
|
||||
return streams[track_index]["DisplayTitle"] or ("Track %s" % track_index)
|
||||
|
||||
# Select audio stream
|
||||
audio_selected = None
|
||||
@ -633,7 +644,7 @@ class PlayUtils(object):
|
||||
# NOTE: "DefaultAudioStreamIndex" is the default according to Jellyfin.
|
||||
# The media's default is marked by the "IsDefault" value.
|
||||
for track_index in audio_streams:
|
||||
if streams[track_index]['IsDefault']:
|
||||
if streams[track_index]["IsDefault"]:
|
||||
audio = track_index
|
||||
break
|
||||
|
||||
@ -648,16 +659,16 @@ class PlayUtils(object):
|
||||
if resp > -1:
|
||||
audio_selected = audio_streams[resp]
|
||||
else:
|
||||
audio_selected = source['DefaultAudioStreamIndex']
|
||||
audio_selected = source["DefaultAudioStreamIndex"]
|
||||
elif audio_streams:
|
||||
# Only one choice
|
||||
audio_selected = audio_streams[0]
|
||||
|
||||
else:
|
||||
audio_selected = source['DefaultAudioStreamIndex']
|
||||
audio_selected = source["DefaultAudioStreamIndex"]
|
||||
|
||||
if audio_selected is not None:
|
||||
self.info['AudioStreamIndex'] = audio_selected
|
||||
self.info["AudioStreamIndex"] = audio_selected
|
||||
prefs += "&AudioStreamIndex=%s" % audio_selected
|
||||
|
||||
# Select audio stream
|
||||
@ -665,7 +676,7 @@ class PlayUtils(object):
|
||||
|
||||
if skip_dialog == Transcode.MediaDefault:
|
||||
for track_index in subs_streams:
|
||||
if streams[track_index]['IsDefault']:
|
||||
if streams[track_index]["IsDefault"]:
|
||||
subtitle = track_index
|
||||
break
|
||||
|
||||
@ -673,7 +684,9 @@ class PlayUtils(object):
|
||||
subtitle_selected = subtitle
|
||||
|
||||
elif skip_dialog in (Transcode.Enabled, Transcode.Subtitle) and subs_streams:
|
||||
selection = list(['No subtitles']) + list(map(get_track_title, subs_streams))
|
||||
selection = list(["No subtitles"]) + list(
|
||||
map(get_track_title, subs_streams)
|
||||
)
|
||||
resp = dialog("select", translate(33014), selection) - 1
|
||||
# Possible responses:
|
||||
# >=0 Subtitle track
|
||||
@ -686,27 +699,36 @@ class PlayUtils(object):
|
||||
if subtitle_selected is not None:
|
||||
server_settings = self.api_client.get_transcode_settings()
|
||||
stream = streams[track_index]
|
||||
if server_settings['EnableSubtitleExtraction'] and stream['SupportsExternalStream']:
|
||||
self.info['SubtitleUrl'] = self.get_subtitles(source, stream, subtitle_selected)
|
||||
self.info['SubtitleStreamIndex'] = subtitle_selected
|
||||
if (
|
||||
server_settings["EnableSubtitleExtraction"]
|
||||
and stream["SupportsExternalStream"]
|
||||
):
|
||||
self.info["SubtitleUrl"] = self.get_subtitles(
|
||||
source, stream, subtitle_selected
|
||||
)
|
||||
self.info["SubtitleStreamIndex"] = subtitle_selected
|
||||
elif allow_burned_subs:
|
||||
prefs += "&SubtitleStreamIndex=%s" % subtitle_selected
|
||||
self.info['SubtitleStreamIndex'] = subtitle_selected
|
||||
self.info["SubtitleStreamIndex"] = subtitle_selected
|
||||
|
||||
return prefs
|
||||
|
||||
def get_subtitles(self, source, stream, index):
|
||||
|
||||
if stream['IsTextSubtitleStream'] and 'DeliveryUrl' in stream and stream['DeliveryUrl'].lower().startswith('/videos'):
|
||||
url = "%s%s" % (self.info['ServerAddress'], stream['DeliveryUrl'])
|
||||
if (
|
||||
stream["IsTextSubtitleStream"]
|
||||
and "DeliveryUrl" in stream
|
||||
and stream["DeliveryUrl"].lower().startswith("/videos")
|
||||
):
|
||||
url = "%s%s" % (self.info["ServerAddress"], stream["DeliveryUrl"])
|
||||
else:
|
||||
url = "%s/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" % (
|
||||
self.info['ServerAddress'],
|
||||
self.item['Id'],
|
||||
source['Id'],
|
||||
self.info["ServerAddress"],
|
||||
self.item["Id"],
|
||||
source["Id"],
|
||||
index,
|
||||
stream['Codec'],
|
||||
self.info['Token']
|
||||
stream["Codec"],
|
||||
self.info["Token"],
|
||||
)
|
||||
|
||||
return url
|
||||
|
@ -15,13 +15,11 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def translate(string):
|
||||
|
||||
''' Get add-on string. Returns in unicode.
|
||||
'''
|
||||
"""Get add-on string. Returns in unicode."""
|
||||
if type(string) != int:
|
||||
string = STRINGS[string]
|
||||
|
||||
result = xbmcaddon.Addon('plugin.video.jellyfin').getLocalizedString(string)
|
||||
result = xbmcaddon.Addon("plugin.video.jellyfin").getLocalizedString(string)
|
||||
|
||||
if not result:
|
||||
result = xbmc.getLocalizedString(string)
|
||||
@ -30,23 +28,23 @@ def translate(string):
|
||||
|
||||
|
||||
STRINGS = {
|
||||
'addon_name': 29999,
|
||||
'playback_mode': 30511,
|
||||
'empty_user': 30613,
|
||||
'empty_user_pass': 30608,
|
||||
'empty_server': 30617,
|
||||
'network_credentials': 30517,
|
||||
'invalid_auth': 33009,
|
||||
'addon_mode': 33036,
|
||||
'native_mode': 33037,
|
||||
'cancel': 30606,
|
||||
'username': 30024,
|
||||
'password': 30602,
|
||||
'gathering': 33021,
|
||||
'boxsets': 30185,
|
||||
'movies': 30302,
|
||||
'tvshows': 30305,
|
||||
'fav_movies': 30180,
|
||||
'fav_tvshows': 30181,
|
||||
'fav_episodes': 30182
|
||||
"addon_name": 29999,
|
||||
"playback_mode": 30511,
|
||||
"empty_user": 30613,
|
||||
"empty_user_pass": 30608,
|
||||
"empty_server": 30617,
|
||||
"network_credentials": 30517,
|
||||
"invalid_auth": 33009,
|
||||
"addon_mode": 33036,
|
||||
"native_mode": 33037,
|
||||
"cancel": 30606,
|
||||
"username": 30024,
|
||||
"password": 30602,
|
||||
"gathering": 33021,
|
||||
"boxsets": 30185,
|
||||
"movies": 30302,
|
||||
"tvshows": 30305,
|
||||
"fav_movies": 30180,
|
||||
"fav_tvshows": 30181,
|
||||
"fav_episodes": 30182,
|
||||
}
|
||||
|
@ -40,62 +40,59 @@ def kodi_version():
|
||||
else:
|
||||
default_versionstring = "19.1 (19.1.0) Git:20210509-85e05228b4"
|
||||
|
||||
version_string = xbmc.getInfoLabel('System.BuildVersion') or default_versionstring
|
||||
return int(version_string.split(' ', 1)[0].split('.', 1)[0])
|
||||
version_string = xbmc.getInfoLabel("System.BuildVersion") or default_versionstring
|
||||
return int(version_string.split(" ", 1)[0].split(".", 1)[0])
|
||||
|
||||
|
||||
def window(key, value=None, clear=False, window_id=10000):
|
||||
|
||||
''' Get or set window properties.
|
||||
'''
|
||||
"""Get or set window properties."""
|
||||
window = xbmcgui.Window(window_id)
|
||||
|
||||
if clear:
|
||||
|
||||
LOG.debug("--[ window clear: %s ]", key)
|
||||
window.clearProperty(key.replace('.json', "").replace('.bool', ""))
|
||||
window.clearProperty(key.replace(".json", "").replace(".bool", ""))
|
||||
elif value is not None:
|
||||
if key.endswith('.json'):
|
||||
if key.endswith(".json"):
|
||||
|
||||
key = key.replace('.json', "")
|
||||
key = key.replace(".json", "")
|
||||
value = json.dumps(value)
|
||||
|
||||
elif key.endswith('.bool'):
|
||||
elif key.endswith(".bool"):
|
||||
|
||||
key = key.replace('.bool', "")
|
||||
key = key.replace(".bool", "")
|
||||
value = "true" if value else "false"
|
||||
|
||||
window.setProperty(key, value)
|
||||
else:
|
||||
result = window.getProperty(key.replace('.json', "").replace('.bool', ""))
|
||||
result = window.getProperty(key.replace(".json", "").replace(".bool", ""))
|
||||
|
||||
if result:
|
||||
if key.endswith('.json'):
|
||||
if key.endswith(".json"):
|
||||
result = json.loads(result)
|
||||
elif key.endswith('.bool'):
|
||||
elif key.endswith(".bool"):
|
||||
result = result in ("true", "1")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def settings(setting, value=None):
|
||||
|
||||
''' Get or add add-on settings.
|
||||
getSetting returns unicode object.
|
||||
'''
|
||||
"""Get or add add-on settings.
|
||||
getSetting returns unicode object.
|
||||
"""
|
||||
addon = xbmcaddon.Addon(addon_id())
|
||||
|
||||
if value is not None:
|
||||
if setting.endswith('.bool'):
|
||||
if setting.endswith(".bool"):
|
||||
|
||||
setting = setting.replace('.bool', "")
|
||||
setting = setting.replace(".bool", "")
|
||||
value = "true" if value else "false"
|
||||
|
||||
addon.setSetting(setting, value)
|
||||
else:
|
||||
result = addon.getSetting(setting.replace('.bool', ""))
|
||||
result = addon.getSetting(setting.replace(".bool", ""))
|
||||
|
||||
if result and setting.endswith('.bool'):
|
||||
if result and setting.endswith(".bool"):
|
||||
result = result in ("true", "1")
|
||||
|
||||
return result
|
||||
@ -106,9 +103,7 @@ def create_id():
|
||||
|
||||
|
||||
def find(dict, item):
|
||||
|
||||
''' Find value in dictionary.
|
||||
'''
|
||||
"""Find value in dictionary."""
|
||||
if item in dict:
|
||||
return dict[item]
|
||||
|
||||
@ -119,9 +114,7 @@ def find(dict, item):
|
||||
|
||||
|
||||
def event(method, data=None, sender=None, hexlify=False):
|
||||
|
||||
''' Data is a dictionary.
|
||||
'''
|
||||
"""Data is a dictionary."""
|
||||
data = data or {}
|
||||
sender = sender or "plugin.video.jellyfin"
|
||||
|
||||
@ -132,7 +125,7 @@ def event(method, data=None, sender=None, hexlify=False):
|
||||
|
||||
LOG.debug("---[ event: %s/%s ] %s", sender, method, data)
|
||||
|
||||
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
|
||||
xbmc.executebuiltin("NotifyAll(%s, %s, %s)" % (sender, method, data))
|
||||
|
||||
|
||||
def dialog(dialog_type, *args, **kwargs):
|
||||
@ -140,63 +133,58 @@ def dialog(dialog_type, *args, **kwargs):
|
||||
d = xbmcgui.Dialog()
|
||||
|
||||
if "icon" in kwargs:
|
||||
kwargs['icon'] = kwargs['icon'].replace(
|
||||
kwargs["icon"] = kwargs["icon"].replace(
|
||||
"{jellyfin}",
|
||||
"special://home/addons/plugin.video.jellyfin/resources/icon.png"
|
||||
"special://home/addons/plugin.video.jellyfin/resources/icon.png",
|
||||
)
|
||||
if "heading" in kwargs:
|
||||
kwargs['heading'] = kwargs['heading'].replace("{jellyfin}", translate('addon_name'))
|
||||
kwargs["heading"] = kwargs["heading"].replace(
|
||||
"{jellyfin}", translate("addon_name")
|
||||
)
|
||||
|
||||
if args:
|
||||
args = list(args)
|
||||
args[0] = args[0].replace("{jellyfin}", translate('addon_name'))
|
||||
args[0] = args[0].replace("{jellyfin}", translate("addon_name"))
|
||||
|
||||
types = {
|
||||
'yesno': d.yesno,
|
||||
'ok': d.ok,
|
||||
'notification': d.notification,
|
||||
'input': d.input,
|
||||
'select': d.select,
|
||||
'numeric': d.numeric,
|
||||
'multi': d.multiselect
|
||||
"yesno": d.yesno,
|
||||
"ok": d.ok,
|
||||
"notification": d.notification,
|
||||
"input": d.input,
|
||||
"select": d.select,
|
||||
"numeric": d.numeric,
|
||||
"multi": d.multiselect,
|
||||
}
|
||||
return types[dialog_type](*args, **kwargs)
|
||||
|
||||
|
||||
def should_stop():
|
||||
|
||||
''' Checkpoint during the sync process.
|
||||
'''
|
||||
"""Checkpoint during the sync process."""
|
||||
if xbmc.Monitor().waitForAbort(0.00001):
|
||||
return True
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
LOG.info("exiiiiitttinggg")
|
||||
return True
|
||||
|
||||
return not window('jellyfin_online.bool')
|
||||
return not window("jellyfin_online.bool")
|
||||
|
||||
|
||||
def get_screensaver():
|
||||
|
||||
''' Get the current screensaver value.
|
||||
'''
|
||||
result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"})
|
||||
"""Get the current screensaver value."""
|
||||
result = JSONRPC("Settings.getSettingValue").execute(
|
||||
{"setting": "screensaver.mode"}
|
||||
)
|
||||
try:
|
||||
return result['result']['value']
|
||||
return result["result"]["value"]
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
|
||||
def set_screensaver(value):
|
||||
|
||||
''' Toggle the screensaver
|
||||
'''
|
||||
params = {
|
||||
'setting': "screensaver.mode",
|
||||
'value': value
|
||||
}
|
||||
result = JSONRPC('Settings.setSettingValue').execute(params)
|
||||
"""Toggle the screensaver"""
|
||||
params = {"setting": "screensaver.mode", "value": value}
|
||||
result = JSONRPC("Settings.setSettingValue").execute(params)
|
||||
LOG.info("---[ screensaver/%s ] %s", value, result)
|
||||
|
||||
|
||||
@ -215,12 +203,12 @@ class JSONRPC(object):
|
||||
def _query(self):
|
||||
|
||||
query = {
|
||||
'jsonrpc': self.jsonrpc_version,
|
||||
'id': self.id,
|
||||
'method': self.method,
|
||||
"jsonrpc": self.jsonrpc_version,
|
||||
"id": self.id,
|
||||
"method": self.method,
|
||||
}
|
||||
if self.params is not None:
|
||||
query['params'] = self.params
|
||||
query["params"] = self.params
|
||||
|
||||
return json.dumps(query)
|
||||
|
||||
@ -231,66 +219,68 @@ class JSONRPC(object):
|
||||
|
||||
|
||||
def validate(path):
|
||||
|
||||
''' Verify if path is accessible.
|
||||
'''
|
||||
if window('jellyfin_pathverified.bool'):
|
||||
"""Verify if path is accessible."""
|
||||
if window("jellyfin_pathverified.bool"):
|
||||
return True
|
||||
|
||||
if not xbmcvfs.exists(path):
|
||||
LOG.info("Could not find %s", path)
|
||||
|
||||
if dialog("yesno", "{jellyfin}", "%s %s. %s" % (translate(33047), path, translate(33048))):
|
||||
if dialog(
|
||||
"yesno",
|
||||
"{jellyfin}",
|
||||
"%s %s. %s" % (translate(33047), path, translate(33048)),
|
||||
):
|
||||
|
||||
return False
|
||||
|
||||
window('jellyfin_pathverified.bool', True)
|
||||
window("jellyfin_pathverified.bool", True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_bluray_dir(path):
|
||||
"""Verify if path/BDMV/ is accessible."""
|
||||
|
||||
''' Verify if path/BDMV/ is accessible.
|
||||
'''
|
||||
|
||||
path = path + '/BDMV/'
|
||||
path = path + "/BDMV/"
|
||||
|
||||
if not xbmcvfs.exists(path):
|
||||
return False
|
||||
|
||||
window('jellyfin_pathverified.bool', True)
|
||||
window("jellyfin_pathverified.bool", True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_dvd_dir(path):
|
||||
"""Verify if path/VIDEO_TS/ is accessible."""
|
||||
|
||||
''' Verify if path/VIDEO_TS/ is accessible.
|
||||
'''
|
||||
|
||||
path = path + '/VIDEO_TS/'
|
||||
path = path + "/VIDEO_TS/"
|
||||
|
||||
if not xbmcvfs.exists(path):
|
||||
return False
|
||||
|
||||
window('jellyfin_pathverified.bool', True)
|
||||
window("jellyfin_pathverified.bool", True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def values(item, keys):
|
||||
|
||||
''' Grab the values in the item for a list of keys {key},{key1}....
|
||||
If the key has no brackets, the key will be passed as is.
|
||||
'''
|
||||
return (item[key.replace('{', "").replace('}', "")] if isinstance(key, text_type) and key.startswith('{') else key for key in keys)
|
||||
"""Grab the values in the item for a list of keys {key},{key1}....
|
||||
If the key has no brackets, the key will be passed as is.
|
||||
"""
|
||||
return (
|
||||
(
|
||||
item[key.replace("{", "").replace("}", "")]
|
||||
if isinstance(key, text_type) and key.startswith("{")
|
||||
else key
|
||||
)
|
||||
for key in keys
|
||||
)
|
||||
|
||||
|
||||
def delete_folder(path):
|
||||
|
||||
''' Delete objects from kodi cache
|
||||
'''
|
||||
"""Delete objects from kodi cache"""
|
||||
LOG.debug("--[ delete folder ]")
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
|
||||
@ -305,9 +295,7 @@ def delete_folder(path):
|
||||
|
||||
|
||||
def delete_recursive(path, dirs):
|
||||
|
||||
''' Delete files and dirs recursively.
|
||||
'''
|
||||
"""Delete files and dirs recursively."""
|
||||
for directory in dirs:
|
||||
dirs2, files = xbmcvfs.listdir(os.path.join(path, directory))
|
||||
|
||||
@ -319,11 +307,9 @@ def delete_recursive(path, dirs):
|
||||
|
||||
|
||||
def unzip(path, dest, folder=None):
|
||||
|
||||
''' Unzip file. zipfile module seems to fail on android with badziperror.
|
||||
'''
|
||||
"""Unzip file. zipfile module seems to fail on android with badziperror."""
|
||||
path = quote_plus(path)
|
||||
root = "zip://" + path + '/'
|
||||
root = "zip://" + path + "/"
|
||||
|
||||
if folder:
|
||||
|
||||
@ -360,9 +346,7 @@ def unzip_recursive(path, dirs, dest):
|
||||
|
||||
|
||||
def unzip_file(path, dest):
|
||||
|
||||
''' Unzip specific file. Path should start with zip://
|
||||
'''
|
||||
"""Unzip specific file. Path should start with zip://"""
|
||||
xbmcvfs.copy(path, dest)
|
||||
LOG.debug("unzip: %s to %s", path, dest)
|
||||
|
||||
@ -381,9 +365,7 @@ def get_zip_directory(path, folder):
|
||||
|
||||
|
||||
def copytree(path, dest):
|
||||
|
||||
''' Copy folder content from one to another.
|
||||
'''
|
||||
"""Copy folder content from one to another."""
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
|
||||
if not xbmcvfs.exists(dest):
|
||||
@ -416,10 +398,8 @@ def copy_recursive(path, dirs, dest):
|
||||
|
||||
|
||||
def copy_file(path, dest):
|
||||
|
||||
''' Copy specific file.
|
||||
'''
|
||||
if path.endswith('.pyo'):
|
||||
"""Copy specific file."""
|
||||
if path.endswith(".pyo"):
|
||||
return
|
||||
|
||||
xbmcvfs.copy(path, dest)
|
||||
@ -427,11 +407,10 @@ def copy_file(path, dest):
|
||||
|
||||
|
||||
def normalize_string(text):
|
||||
|
||||
''' For theme media, do not modify unless modified in TV Tunes.
|
||||
Remove dots from the last character as windows can not have directories
|
||||
with dots at the end
|
||||
'''
|
||||
"""For theme media, do not modify unless modified in TV Tunes.
|
||||
Remove dots from the last character as windows can not have directories
|
||||
with dots at the end
|
||||
"""
|
||||
text = text.replace(":", "")
|
||||
text = text.replace("/", "-")
|
||||
text = text.replace("\\", "-")
|
||||
@ -439,26 +418,24 @@ def normalize_string(text):
|
||||
text = text.replace(">", "")
|
||||
text = text.replace("*", "")
|
||||
text = text.replace("?", "")
|
||||
text = text.replace('|', "")
|
||||
text = text.replace("|", "")
|
||||
text = text.strip()
|
||||
|
||||
text = text.rstrip('.')
|
||||
text = unicodedata.normalize('NFKD', text_type(text, 'utf-8')).encode('ascii', 'ignore')
|
||||
text = text.rstrip(".")
|
||||
text = unicodedata.normalize("NFKD", text_type(text, "utf-8")).encode(
|
||||
"ascii", "ignore"
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def split_list(itemlist, size):
|
||||
|
||||
''' Split up list in pieces of size. Will generate a list of lists
|
||||
'''
|
||||
return [itemlist[i:i + size] for i in range(0, len(itemlist), size)]
|
||||
"""Split up list in pieces of size. Will generate a list of lists"""
|
||||
return [itemlist[i : i + size] for i in range(0, len(itemlist), size)]
|
||||
|
||||
|
||||
def convert_to_local(date, timezone=tz.tzlocal()):
|
||||
|
||||
''' Convert the local datetime to local.
|
||||
'''
|
||||
"""Convert the local datetime to local."""
|
||||
try:
|
||||
date = parser.parse(date) if isinstance(date, string_types) else date
|
||||
date = date.replace(tzinfo=tz.tzutc())
|
||||
@ -475,9 +452,9 @@ def convert_to_local(date, timezone=tz.tzlocal()):
|
||||
date.second,
|
||||
)
|
||||
else:
|
||||
return date.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
return date.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
except Exception as error:
|
||||
LOG.exception('Item date: {} --- {}'.format(str(date), error))
|
||||
LOG.exception("Item date: {} --- {}".format(str(date), error))
|
||||
|
||||
return str(date)
|
||||
|
||||
@ -491,28 +468,27 @@ def has_attribute(obj, name):
|
||||
|
||||
|
||||
def set_addon_mode():
|
||||
"""Setup playback mode. If native mode selected, check network credentials."""
|
||||
value = dialog(
|
||||
"yesno",
|
||||
translate("playback_mode"),
|
||||
translate(33035),
|
||||
nolabel=translate("addon_mode"),
|
||||
yeslabel=translate("native_mode"),
|
||||
)
|
||||
|
||||
''' Setup playback mode. If native mode selected, check network credentials.
|
||||
'''
|
||||
value = dialog("yesno",
|
||||
translate('playback_mode'),
|
||||
translate(33035),
|
||||
nolabel=translate('addon_mode'),
|
||||
yeslabel=translate('native_mode'))
|
||||
|
||||
settings('useDirectPaths', value="1" if value else "0")
|
||||
settings("useDirectPaths", value="1" if value else "0")
|
||||
|
||||
if value:
|
||||
dialog("ok", "{jellyfin}", translate(33145))
|
||||
|
||||
LOG.info("Add-on playback: %s", settings('useDirectPaths') == "0")
|
||||
LOG.info("Add-on playback: %s", settings("useDirectPaths") == "0")
|
||||
|
||||
|
||||
class JsonDebugPrinter(object):
|
||||
|
||||
''' Helper class to defer converting data to JSON until it is needed.
|
||||
"""Helper class to defer converting data to JSON until it is needed.
|
||||
See: https://github.com/jellyfin/jellyfin-kodi/pull/193
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
@ -527,8 +503,8 @@ def get_filesystem_encoding():
|
||||
if not enc:
|
||||
enc = sys.getdefaultencoding()
|
||||
|
||||
if not enc or enc == 'ascii':
|
||||
enc = 'utf-8'
|
||||
if not enc or enc == "ascii":
|
||||
enc = "utf-8"
|
||||
|
||||
return enc
|
||||
|
||||
@ -537,20 +513,20 @@ def find_library(server, item):
|
||||
from ..database import get_sync
|
||||
|
||||
sync = get_sync()
|
||||
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
|
||||
ancestors = server.jellyfin.get_ancestors(item['Id'])
|
||||
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
|
||||
ancestors = server.jellyfin.get_ancestors(item["Id"])
|
||||
for ancestor in ancestors:
|
||||
if ancestor['Id'] in whitelist:
|
||||
if ancestor["Id"] in whitelist:
|
||||
return ancestor
|
||||
|
||||
LOG.error('No ancestor found, not syncing item with ID: {}'.format(item['Id']))
|
||||
LOG.error("No ancestor found, not syncing item with ID: {}".format(item["Id"]))
|
||||
return {}
|
||||
|
||||
|
||||
def translate_path(path):
|
||||
'''
|
||||
"""
|
||||
Use new library location for translate path starting in Kodi 19
|
||||
'''
|
||||
"""
|
||||
version = kodi_version()
|
||||
|
||||
if version > 18:
|
||||
|
@ -19,9 +19,8 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def progress(message=None):
|
||||
"""Will start and close the progress dialog."""
|
||||
|
||||
''' Will start and close the progress dialog.
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(self, item=None, *args, **kwargs):
|
||||
|
||||
@ -29,10 +28,13 @@ def progress(message=None):
|
||||
|
||||
if item and type(item) == dict:
|
||||
|
||||
dialog.create(translate('addon_name'), "%s %s" % (translate('gathering'), item['Name']))
|
||||
LOG.info("Processing %s: %s", item['Name'], item['Id'])
|
||||
dialog.create(
|
||||
translate("addon_name"),
|
||||
"%s %s" % (translate("gathering"), item["Name"]),
|
||||
)
|
||||
LOG.info("Processing %s: %s", item["Name"], item["Id"])
|
||||
else:
|
||||
dialog.create(translate('addon_name'), message)
|
||||
dialog.create(translate("addon_name"), message)
|
||||
LOG.info("Processing %s", message)
|
||||
|
||||
if item:
|
||||
@ -44,13 +46,13 @@ def progress(message=None):
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def stop(func):
|
||||
"""Wrapper to catch exceptions and return using catch"""
|
||||
|
||||
''' Wrapper to catch exceptions and return using catch
|
||||
'''
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
try:
|
||||
@ -68,11 +70,12 @@ def stop(func):
|
||||
|
||||
|
||||
def jellyfin_item(func):
|
||||
"""Wrapper to retrieve the jellyfin_db item."""
|
||||
|
||||
''' Wrapper to retrieve the jellyfin_db item.
|
||||
'''
|
||||
def wrapper(self, item, *args, **kwargs):
|
||||
e_item = self.jellyfin_db.get_item_by_id(item['Id'] if type(item) == dict else item)
|
||||
e_item = self.jellyfin_db.get_item_by_id(
|
||||
item["Id"] if type(item) == dict else item
|
||||
)
|
||||
|
||||
return func(self, item, e_item=e_item, *args, **kwargs)
|
||||
|
||||
|
@ -19,45 +19,42 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def tvtunes_nfo(path, urls):
|
||||
|
||||
''' Create tvtunes.nfo
|
||||
'''
|
||||
"""Create tvtunes.nfo"""
|
||||
try:
|
||||
xml = etree.parse(path).getroot()
|
||||
except Exception:
|
||||
xml = etree.Element('tvtunes')
|
||||
xml = etree.Element("tvtunes")
|
||||
|
||||
for elem in xml.getiterator('tvtunes'):
|
||||
for elem in xml.getiterator("tvtunes"):
|
||||
for file in list(elem):
|
||||
elem.remove(file)
|
||||
|
||||
for url in urls:
|
||||
etree.SubElement(xml, 'file').text = url
|
||||
etree.SubElement(xml, "file").text = url
|
||||
|
||||
tree = etree.ElementTree(xml)
|
||||
tree.write(path)
|
||||
|
||||
|
||||
def advanced_settings():
|
||||
|
||||
''' Track the existence of <cleanonupdate>true</cleanonupdate>
|
||||
It is incompatible with plugin paths.
|
||||
'''
|
||||
if settings('useDirectPaths') != "0":
|
||||
"""Track the existence of <cleanonupdate>true</cleanonupdate>
|
||||
It is incompatible with plugin paths.
|
||||
"""
|
||||
if settings("useDirectPaths") != "0":
|
||||
return
|
||||
|
||||
path = translate_path("special://profile/")
|
||||
file = os.path.join(path, 'advancedsettings.xml')
|
||||
file = os.path.join(path, "advancedsettings.xml")
|
||||
|
||||
try:
|
||||
xml = etree.parse(file).getroot()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
video = xml.find('videolibrary')
|
||||
video = xml.find("videolibrary")
|
||||
|
||||
if video is not None:
|
||||
cleanonupdate = video.find('cleanonupdate')
|
||||
cleanonupdate = video.find("cleanonupdate")
|
||||
|
||||
if cleanonupdate is not None and cleanonupdate.text == "true":
|
||||
|
||||
@ -68,13 +65,13 @@ def advanced_settings():
|
||||
tree.write(file)
|
||||
|
||||
dialog("ok", "{jellyfin}", translate(33097))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
xbmc.executebuiltin("RestartApp")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verify_kodi_defaults():
|
||||
''' Make sure we have the kodi default folder in place.
|
||||
'''
|
||||
"""Make sure we have the kodi default folder in place."""
|
||||
|
||||
source_base_path = translate_path("special://xbmc/system/library/video")
|
||||
dest_base_path = translate_path("special://profile/library/video")
|
||||
@ -97,11 +94,15 @@ def verify_kodi_defaults():
|
||||
|
||||
if not os.path.exists(dest_file):
|
||||
copy = True
|
||||
elif os.path.splitext(file_name)[1].lower() == '.xml':
|
||||
elif os.path.splitext(file_name)[1].lower() == ".xml":
|
||||
try:
|
||||
etree.parse(dest_file)
|
||||
except etree.ParseError:
|
||||
LOG.warning("Unable to parse `{}`, recovering from default.".format(dest_file))
|
||||
LOG.warning(
|
||||
"Unable to parse `{}`, recovering from default.".format(
|
||||
dest_file
|
||||
)
|
||||
)
|
||||
copy = True
|
||||
|
||||
if copy:
|
||||
@ -112,7 +113,7 @@ def verify_kodi_defaults():
|
||||
# This code seems to enforce a fixed ordering.
|
||||
# Is it really desirable to force this on users?
|
||||
# The default (system wide) order is [10, 20, 30] in Kodi 19.
|
||||
for index, node in enumerate(['movies', 'tvshows', 'musicvideos']):
|
||||
for index, node in enumerate(["movies", "tvshows", "musicvideos"]):
|
||||
file_name = os.path.join(dest_base_path, node, "index.xml")
|
||||
|
||||
if xbmcvfs.exists(file_name):
|
||||
@ -126,8 +127,8 @@ def verify_kodi_defaults():
|
||||
tree = None
|
||||
|
||||
if tree is not None:
|
||||
tree.getroot().set('order', str(17 + index))
|
||||
with xbmcvfs.File(file_name, 'w') as f:
|
||||
tree.getroot().set("order", str(17 + index))
|
||||
with xbmcvfs.File(file_name, "w") as f:
|
||||
f.write(etree.tostring(tree.getroot()))
|
||||
|
||||
playlist_path = translate_path("special://profile/playlists/video")
|
||||
|
@ -25,22 +25,22 @@ def ensure_client():
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class Jellyfin(object):
|
||||
"""This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing
|
||||
to communicate with the JellyfinClient().
|
||||
|
||||
''' This is your Jellyfinclient, you can create more than one. The server_id is only a temporary thing
|
||||
to communicate with the JellyfinClient().
|
||||
from jellyfin_kodi.jellyfin import Jellyfin
|
||||
|
||||
from jellyfin_kodi.jellyfin import Jellyfin
|
||||
Jellyfin('123456').config.data['app']
|
||||
|
||||
Jellyfin('123456').config.data['app']
|
||||
|
||||
# Permanent client reference
|
||||
client = Jellyfin('123456').get_client()
|
||||
client.config.data['app']
|
||||
'''
|
||||
# Permanent client reference
|
||||
client = Jellyfin('123456').get_client()
|
||||
client.config.data['app']
|
||||
"""
|
||||
|
||||
# Borg - multiple instances, shared state
|
||||
_shared_state = {}
|
||||
@ -94,7 +94,7 @@ class Jellyfin(object):
|
||||
|
||||
self.client[self.server_id] = JellyfinClient()
|
||||
|
||||
if self.server_id == 'default':
|
||||
if self.server_id == "default":
|
||||
LOG.info("---[ START JELLYFINCLIENT ]---")
|
||||
else:
|
||||
LOG.info("---[ START JELLYFINCLIENT: %s ]---", self.server_id)
|
||||
|
@ -15,7 +15,7 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def jellyfin_url(client, handler):
|
||||
return "%s/%s" % (client.config.data['auth.server'], handler)
|
||||
return "%s/%s" % (client.config.data["auth.server"], handler)
|
||||
|
||||
|
||||
def basic_info():
|
||||
@ -42,9 +42,8 @@ def music_info():
|
||||
|
||||
|
||||
class API(object):
|
||||
"""All the api calls to the server."""
|
||||
|
||||
''' All the api calls to the server.
|
||||
'''
|
||||
def __init__(self, client, *args, **kwargs):
|
||||
self.client = client
|
||||
self.config = client.config
|
||||
@ -54,18 +53,18 @@ class API(object):
|
||||
if request is None:
|
||||
request = {}
|
||||
|
||||
request.update({'type': action, 'handler': url})
|
||||
request.update({"type": action, "handler": url})
|
||||
|
||||
return self.client.request(request)
|
||||
|
||||
def _get(self, handler, params=None):
|
||||
return self._http("GET", handler, {'params': params})
|
||||
return self._http("GET", handler, {"params": params})
|
||||
|
||||
def _post(self, handler, json=None, params=None):
|
||||
return self._http("POST", handler, {'params': params, 'json': json})
|
||||
return self._http("POST", handler, {"params": params, "json": json})
|
||||
|
||||
def _delete(self, handler, params=None):
|
||||
return self._http("DELETE", handler, {'params': params})
|
||||
return self._http("DELETE", handler, {"params": params})
|
||||
|
||||
#################################################################################################
|
||||
|
||||
@ -111,9 +110,17 @@ class API(object):
|
||||
|
||||
def artwork(self, item_id, art, max_width, ext="jpg", index=None):
|
||||
if index is None:
|
||||
return jellyfin_url(self.client, "Items/%s/Images/%s?MaxWidth=%s&format=%s" % (item_id, art, max_width, ext))
|
||||
return jellyfin_url(
|
||||
self.client,
|
||||
"Items/%s/Images/%s?MaxWidth=%s&format=%s"
|
||||
% (item_id, art, max_width, ext),
|
||||
)
|
||||
|
||||
return jellyfin_url(self.client, "Items/%s/Images/%s/%s?MaxWidth=%s&format=%s" % (item_id, art, index, max_width, ext))
|
||||
return jellyfin_url(
|
||||
self.client,
|
||||
"Items/%s/Images/%s/%s?MaxWidth=%s&format=%s"
|
||||
% (item_id, art, index, max_width, ext),
|
||||
)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
@ -140,16 +147,16 @@ class API(object):
|
||||
return self.users("/Items/%s" % item_id)
|
||||
|
||||
def get_items(self, item_ids):
|
||||
return self.users("/Items", params={
|
||||
'Ids': ','.join(str(x) for x in item_ids),
|
||||
'Fields': info()
|
||||
})
|
||||
return self.users(
|
||||
"/Items",
|
||||
params={"Ids": ",".join(str(x) for x in item_ids), "Fields": info()},
|
||||
)
|
||||
|
||||
def get_sessions(self):
|
||||
return self.sessions(params={'ControllableByUserId': "{UserId}"})
|
||||
return self.sessions(params={"ControllableByUserId": "{UserId}"})
|
||||
|
||||
def get_device(self, device_id):
|
||||
return self.sessions(params={'DeviceId': device_id})
|
||||
return self.sessions(params={"DeviceId": device_id})
|
||||
|
||||
def post_session(self, session_id, url, params=None, data=None):
|
||||
return self.sessions("/%s/%s" % (session_id, url), "POST", params, data)
|
||||
@ -158,64 +165,68 @@ class API(object):
|
||||
return self.items("/%s/Images" % item_id)
|
||||
|
||||
def get_suggestion(self, media="Movie,Episode", limit=1):
|
||||
return self.users("/Suggestions", params={
|
||||
'Type': media,
|
||||
'Limit': limit
|
||||
})
|
||||
return self.users("/Suggestions", params={"Type": media, "Limit": limit})
|
||||
|
||||
def get_recently_added(self, media=None, parent_id=None, limit=20):
|
||||
return self.user_items("/Latest", {
|
||||
'Limit': limit,
|
||||
'UserId': "{UserId}",
|
||||
'IncludeItemTypes': media,
|
||||
'ParentId': parent_id,
|
||||
'Fields': info()
|
||||
})
|
||||
return self.user_items(
|
||||
"/Latest",
|
||||
{
|
||||
"Limit": limit,
|
||||
"UserId": "{UserId}",
|
||||
"IncludeItemTypes": media,
|
||||
"ParentId": parent_id,
|
||||
"Fields": info(),
|
||||
},
|
||||
)
|
||||
|
||||
def get_next(self, index=None, limit=1):
|
||||
return self.shows("/NextUp", {
|
||||
'Limit': limit,
|
||||
'UserId': "{UserId}",
|
||||
'StartIndex': None if index is None else int(index)
|
||||
})
|
||||
return self.shows(
|
||||
"/NextUp",
|
||||
{
|
||||
"Limit": limit,
|
||||
"UserId": "{UserId}",
|
||||
"StartIndex": None if index is None else int(index),
|
||||
},
|
||||
)
|
||||
|
||||
def get_adjacent_episodes(self, show_id, item_id):
|
||||
return self.shows("/%s/Episodes" % show_id, {
|
||||
'UserId': "{UserId}",
|
||||
'AdjacentTo': item_id,
|
||||
'Fields': "Overview"
|
||||
})
|
||||
return self.shows(
|
||||
"/%s/Episodes" % show_id,
|
||||
{"UserId": "{UserId}", "AdjacentTo": item_id, "Fields": "Overview"},
|
||||
)
|
||||
|
||||
def get_genres(self, parent_id=None):
|
||||
return self._get("Genres", {
|
||||
'ParentId': parent_id,
|
||||
'UserId': "{UserId}",
|
||||
'Fields': info()
|
||||
})
|
||||
return self._get(
|
||||
"Genres", {"ParentId": parent_id, "UserId": "{UserId}", "Fields": info()}
|
||||
)
|
||||
|
||||
def get_recommendation(self, parent_id=None, limit=20):
|
||||
return self._get("Movies/Recommendations", {
|
||||
'ParentId': parent_id,
|
||||
'UserId': "{UserId}",
|
||||
'Fields': info(),
|
||||
'Limit': limit
|
||||
})
|
||||
return self._get(
|
||||
"Movies/Recommendations",
|
||||
{
|
||||
"ParentId": parent_id,
|
||||
"UserId": "{UserId}",
|
||||
"Fields": info(),
|
||||
"Limit": limit,
|
||||
},
|
||||
)
|
||||
|
||||
def get_items_by_letter(self, parent_id=None, media=None, letter=None):
|
||||
return self.user_items(params={
|
||||
'ParentId': parent_id,
|
||||
'NameStartsWith': letter,
|
||||
'Fields': info(),
|
||||
'Recursive': True,
|
||||
'IncludeItemTypes': media
|
||||
})
|
||||
return self.user_items(
|
||||
params={
|
||||
"ParentId": parent_id,
|
||||
"NameStartsWith": letter,
|
||||
"Fields": info(),
|
||||
"Recursive": True,
|
||||
"IncludeItemTypes": media,
|
||||
}
|
||||
)
|
||||
|
||||
def get_channels(self):
|
||||
return self._get("LiveTv/Channels", {
|
||||
'UserId': "{UserId}",
|
||||
'EnableImages': True,
|
||||
'EnableUserData': True
|
||||
})
|
||||
return self._get(
|
||||
"LiveTv/Channels",
|
||||
{"UserId": "{UserId}", "EnableImages": True, "EnableUserData": True},
|
||||
)
|
||||
|
||||
def get_intros(self, item_id):
|
||||
return self.user_items("/%s/Intros" % item_id)
|
||||
@ -230,30 +241,26 @@ class API(object):
|
||||
return self.user_items("/%s/LocalTrailers" % item_id)
|
||||
|
||||
def get_transcode_settings(self):
|
||||
return self._get('System/Configuration/encoding')
|
||||
return self._get("System/Configuration/encoding")
|
||||
|
||||
def get_ancestors(self, item_id):
|
||||
return self.items("/%s/Ancestors" % item_id, params={
|
||||
'UserId': "{UserId}"
|
||||
})
|
||||
return self.items("/%s/Ancestors" % item_id, params={"UserId": "{UserId}"})
|
||||
|
||||
def get_items_theme_video(self, parent_id):
|
||||
return self.users("/Items", params={
|
||||
'HasThemeVideo': True,
|
||||
'ParentId': parent_id
|
||||
})
|
||||
return self.users(
|
||||
"/Items", params={"HasThemeVideo": True, "ParentId": parent_id}
|
||||
)
|
||||
|
||||
def get_themes(self, item_id):
|
||||
return self.items("/%s/ThemeMedia" % item_id, params={
|
||||
'UserId': "{UserId}",
|
||||
'InheritFromParent': True
|
||||
})
|
||||
return self.items(
|
||||
"/%s/ThemeMedia" % item_id,
|
||||
params={"UserId": "{UserId}", "InheritFromParent": True},
|
||||
)
|
||||
|
||||
def get_items_theme_song(self, parent_id):
|
||||
return self.users("/Items", params={
|
||||
'HasThemeSong': True,
|
||||
'ParentId': parent_id
|
||||
})
|
||||
return self.users(
|
||||
"/Items", params={"HasThemeSong": True, "ParentId": parent_id}
|
||||
)
|
||||
|
||||
def check_companion_enabled(self):
|
||||
"""
|
||||
@ -262,8 +269,10 @@ class API(object):
|
||||
None = Unknown
|
||||
"""
|
||||
try:
|
||||
plugin_settings = self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {}
|
||||
return plugin_settings.get('IsEnabled')
|
||||
plugin_settings = (
|
||||
self._get("Jellyfin.Plugin.KodiSyncQueue/GetPluginSettings") or {}
|
||||
)
|
||||
return plugin_settings.get("IsEnabled")
|
||||
|
||||
except requests.RequestException as e:
|
||||
LOG.warning("Error checking companion installed state: %s", e)
|
||||
@ -277,42 +286,51 @@ class API(object):
|
||||
return None
|
||||
|
||||
def get_seasons(self, show_id):
|
||||
return self.shows("/%s/Seasons" % show_id, params={
|
||||
'UserId': "{UserId}",
|
||||
'EnableImages': True,
|
||||
'Fields': info()
|
||||
})
|
||||
return self.shows(
|
||||
"/%s/Seasons" % show_id,
|
||||
params={"UserId": "{UserId}", "EnableImages": True, "Fields": info()},
|
||||
)
|
||||
|
||||
def get_date_modified(self, date, parent_id, media=None):
|
||||
return self.users("/Items", params={
|
||||
'ParentId': parent_id,
|
||||
'Recursive': False,
|
||||
'IsMissing': False,
|
||||
'IsVirtualUnaired': False,
|
||||
'IncludeItemTypes': media or None,
|
||||
'MinDateLastSaved': date,
|
||||
'Fields': info()
|
||||
})
|
||||
return self.users(
|
||||
"/Items",
|
||||
params={
|
||||
"ParentId": parent_id,
|
||||
"Recursive": False,
|
||||
"IsMissing": False,
|
||||
"IsVirtualUnaired": False,
|
||||
"IncludeItemTypes": media or None,
|
||||
"MinDateLastSaved": date,
|
||||
"Fields": info(),
|
||||
},
|
||||
)
|
||||
|
||||
def get_userdata_date_modified(self, date, parent_id, media=None):
|
||||
return self.users("/Items", params={
|
||||
'ParentId': parent_id,
|
||||
'Recursive': True,
|
||||
'IsMissing': False,
|
||||
'IsVirtualUnaired': False,
|
||||
'IncludeItemTypes': media or None,
|
||||
'MinDateLastSavedForUser': date,
|
||||
'Fields': info()
|
||||
})
|
||||
return self.users(
|
||||
"/Items",
|
||||
params={
|
||||
"ParentId": parent_id,
|
||||
"Recursive": True,
|
||||
"IsMissing": False,
|
||||
"IsVirtualUnaired": False,
|
||||
"IncludeItemTypes": media or None,
|
||||
"MinDateLastSavedForUser": date,
|
||||
"Fields": info(),
|
||||
},
|
||||
)
|
||||
|
||||
def refresh_item(self, item_id):
|
||||
return self.items("/%s/Refresh" % item_id, "POST", json={
|
||||
'Recursive': True,
|
||||
'ImageRefreshMode': "FullRefresh",
|
||||
'MetadataRefreshMode': "FullRefresh",
|
||||
'ReplaceAllImages': False,
|
||||
'ReplaceAllMetadata': True
|
||||
})
|
||||
return self.items(
|
||||
"/%s/Refresh" % item_id,
|
||||
"POST",
|
||||
json={
|
||||
"Recursive": True,
|
||||
"ImageRefreshMode": "FullRefresh",
|
||||
"MetadataRefreshMode": "FullRefresh",
|
||||
"ReplaceAllImages": False,
|
||||
"ReplaceAllMetadata": True,
|
||||
},
|
||||
)
|
||||
|
||||
def favorite(self, item_id, option=True):
|
||||
return self.users("/FavoriteItems/%s" % item_id, "POST" if option else "DELETE")
|
||||
@ -324,7 +342,9 @@ class API(object):
|
||||
return self.sessions("/Capabilities/Full", "POST", json=data)
|
||||
|
||||
def session_add_user(self, session_id, user_id, option=True):
|
||||
return self.sessions("/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE")
|
||||
return self.sessions(
|
||||
"/%s/User/%s" % (session_id, user_id), "POST" if option else "DELETE"
|
||||
)
|
||||
|
||||
def session_playing(self, data):
|
||||
return self.sessions("/Playing", "POST", json=data)
|
||||
@ -339,115 +359,132 @@ class API(object):
|
||||
return self.users("/PlayedItems/%s" % item_id, "POST" if watched else "DELETE")
|
||||
|
||||
def get_sync_queue(self, date, filters=None):
|
||||
return self._get("Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems", params={
|
||||
'LastUpdateDT': date,
|
||||
'filter': filters or 'None'
|
||||
})
|
||||
return self._get(
|
||||
"Jellyfin.Plugin.KodiSyncQueue/{UserId}/GetItems",
|
||||
params={"LastUpdateDT": date, "filter": filters or "None"},
|
||||
)
|
||||
|
||||
def get_server_time(self):
|
||||
return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime")
|
||||
|
||||
def get_play_info(self, item_id, profile):
|
||||
return self.items("/%s/PlaybackInfo" % item_id, "POST", json={
|
||||
'UserId': "{UserId}",
|
||||
'DeviceProfile': profile,
|
||||
'AutoOpenLiveStream': True
|
||||
})
|
||||
return self.items(
|
||||
"/%s/PlaybackInfo" % item_id,
|
||||
"POST",
|
||||
json={
|
||||
"UserId": "{UserId}",
|
||||
"DeviceProfile": profile,
|
||||
"AutoOpenLiveStream": True,
|
||||
},
|
||||
)
|
||||
|
||||
def get_live_stream(self, item_id, play_id, token, profile):
|
||||
return self._post("LiveStreams/Open", json={
|
||||
'UserId': "{UserId}",
|
||||
'DeviceProfile': profile,
|
||||
'OpenToken': token,
|
||||
'PlaySessionId': play_id,
|
||||
'ItemId': item_id
|
||||
})
|
||||
return self._post(
|
||||
"LiveStreams/Open",
|
||||
json={
|
||||
"UserId": "{UserId}",
|
||||
"DeviceProfile": profile,
|
||||
"OpenToken": token,
|
||||
"PlaySessionId": play_id,
|
||||
"ItemId": item_id,
|
||||
},
|
||||
)
|
||||
|
||||
def close_live_stream(self, live_id):
|
||||
return self._post("LiveStreams/Close", json={
|
||||
'LiveStreamId': live_id
|
||||
})
|
||||
return self._post("LiveStreams/Close", json={"LiveStreamId": live_id})
|
||||
|
||||
def close_transcode(self, device_id, play_id):
|
||||
return self._delete("Videos/ActiveEncodings", params={
|
||||
'DeviceId': device_id,
|
||||
'PlaySessionId': play_id
|
||||
})
|
||||
return self._delete(
|
||||
"Videos/ActiveEncodings",
|
||||
params={"DeviceId": device_id, "PlaySessionId": play_id},
|
||||
)
|
||||
|
||||
def get_default_headers(self):
|
||||
auth = "MediaBrowser "
|
||||
auth += "Client=%s, " % self.config.data['app.name']
|
||||
auth += "Device=%s, " % self.config.data['app.device_name']
|
||||
auth += "DeviceId=%s, " % self.config.data['app.device_id']
|
||||
auth += "Version=%s" % self.config.data['app.version']
|
||||
auth += "Client=%s, " % self.config.data["app.name"]
|
||||
auth += "Device=%s, " % self.config.data["app.device_name"]
|
||||
auth += "DeviceId=%s, " % self.config.data["app.device_id"]
|
||||
auth += "Version=%s" % self.config.data["app.version"]
|
||||
|
||||
return {
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Application": "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']),
|
||||
"X-Application": "%s/%s"
|
||||
% (self.config.data["app.name"], self.config.data["app.version"]),
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-encoding": "gzip",
|
||||
"User-Agent": self.config.data['http.user_agent'] or "%s/%s" % (self.config.data['app.name'], self.config.data['app.version']),
|
||||
"x-emby-authorization": ensure_str(auth, 'utf-8')
|
||||
"User-Agent": self.config.data["http.user_agent"]
|
||||
or "%s/%s"
|
||||
% (self.config.data["app.name"], self.config.data["app.version"]),
|
||||
"x-emby-authorization": ensure_str(auth, "utf-8"),
|
||||
}
|
||||
|
||||
def send_request(self, url, path, method="get", timeout=None, headers=None, data=None):
|
||||
def send_request(
|
||||
self, url, path, method="get", timeout=None, headers=None, data=None
|
||||
):
|
||||
request_method = getattr(requests, method.lower())
|
||||
url = "%s/%s" % (url, path)
|
||||
request_settings = {
|
||||
"timeout": timeout or self.default_timeout,
|
||||
"headers": headers or self.get_default_headers(),
|
||||
"data": data
|
||||
"data": data,
|
||||
}
|
||||
|
||||
request_settings["verify"] = settings('sslverify.bool')
|
||||
request_settings["verify"] = settings("sslverify.bool")
|
||||
|
||||
LOG.info("Sending %s request to %s" % (method, path))
|
||||
LOG.debug(request_settings['timeout'])
|
||||
LOG.debug(request_settings['headers'])
|
||||
LOG.debug(request_settings["timeout"])
|
||||
LOG.debug(request_settings["headers"])
|
||||
|
||||
return request_method(url, **request_settings)
|
||||
|
||||
def login(self, server_url, username, password=""):
|
||||
path = "Users/AuthenticateByName"
|
||||
auth_data = {
|
||||
"username": username,
|
||||
"Pw": password
|
||||
}
|
||||
auth_data = {"username": username, "Pw": password}
|
||||
|
||||
headers = self.get_default_headers()
|
||||
headers.update({'Content-type': "application/json"})
|
||||
headers.update({"Content-type": "application/json"})
|
||||
|
||||
try:
|
||||
LOG.info("Trying to login to %s/%s as %s" % (server_url, path, username))
|
||||
response = self.send_request(server_url, path, method="post", timeout=10, headers=headers, data=json.dumps(auth_data))
|
||||
response = self.send_request(
|
||||
server_url,
|
||||
path,
|
||||
method="post",
|
||||
timeout=10,
|
||||
headers=headers,
|
||||
data=json.dumps(auth_data),
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
LOG.error("Failed to login to server with status code: " + str(response.status_code))
|
||||
LOG.error(
|
||||
"Failed to login to server with status code: "
|
||||
+ str(response.status_code)
|
||||
)
|
||||
LOG.error("Server Response:\n" + str(response.content))
|
||||
LOG.debug(headers)
|
||||
|
||||
return {}
|
||||
except Exception as e: # Find exceptions for likely cases i.e, server timeout, etc
|
||||
except (
|
||||
Exception
|
||||
) as e: # Find exceptions for likely cases i.e, server timeout, etc
|
||||
LOG.error(e)
|
||||
|
||||
return {}
|
||||
|
||||
def validate_authentication_token(self, server):
|
||||
auth_token_header = {
|
||||
'X-MediaBrowser-Token': server['AccessToken']
|
||||
}
|
||||
auth_token_header = {"X-MediaBrowser-Token": server["AccessToken"]}
|
||||
headers = self.get_default_headers()
|
||||
headers.update(auth_token_header)
|
||||
|
||||
response = self.send_request(server['address'], "system/info", headers=headers)
|
||||
response = self.send_request(server["address"], "system/info", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return {'Status_Code': response.status_code}
|
||||
return {"Status_Code": response.status_code}
|
||||
|
||||
def get_public_info(self, server_address):
|
||||
response = self.send_request(server_address, "system/info/public")
|
||||
@ -459,8 +496,8 @@ class API(object):
|
||||
return {}
|
||||
|
||||
def check_redirect(self, server_address):
|
||||
''' Checks if the server is redirecting traffic to a new URL and
|
||||
"""Checks if the server is redirecting traffic to a new URL and
|
||||
returns the URL the server prefers to use
|
||||
'''
|
||||
"""
|
||||
response = self.send_request(server_address, "system/info/public")
|
||||
return response.url.replace('/system/info/public', '')
|
||||
return response.url.replace("/system/info/public", "")
|
||||
|
@ -19,11 +19,10 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def callback(message, data):
|
||||
|
||||
''' Callback function should receive message, data
|
||||
message: string
|
||||
data: json dictionary
|
||||
'''
|
||||
"""Callback function should receive message, data
|
||||
message: string
|
||||
data: json dictionary
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@ -53,13 +52,13 @@ class JellyfinClient(object):
|
||||
self.set_credentials(credentials or {})
|
||||
state = self.auth.connect(options or {})
|
||||
|
||||
if state['State'] == CONNECTION_STATE['SignedIn']:
|
||||
if state["State"] == CONNECTION_STATE["SignedIn"]:
|
||||
|
||||
LOG.info("User is authenticated.")
|
||||
self.logged_in = True
|
||||
self.callback("ServerOnline", {'Id': self.auth.server_id})
|
||||
self.callback("ServerOnline", {"Id": self.auth.server_id})
|
||||
|
||||
state['Credentials'] = self.get_credentials()
|
||||
state["Credentials"] = self.get_credentials()
|
||||
|
||||
return state
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
''' This will hold all configs from the client.
|
||||
""" This will hold all configs from the client.
|
||||
Configuration set here will be used for the HTTP client.
|
||||
'''
|
||||
"""
|
||||
|
||||
#################################################################################################
|
||||
|
||||
@ -26,28 +26,41 @@ class Config(object):
|
||||
self.data = {}
|
||||
self.http()
|
||||
|
||||
def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None):
|
||||
def app(
|
||||
self,
|
||||
name,
|
||||
version,
|
||||
device_name,
|
||||
device_id,
|
||||
capabilities=None,
|
||||
device_pixel_ratio=None,
|
||||
):
|
||||
|
||||
LOG.debug("Begin app constructor.")
|
||||
self.data['app.name'] = name
|
||||
self.data['app.version'] = version
|
||||
self.data['app.device_name'] = device_name
|
||||
self.data['app.device_id'] = device_id
|
||||
self.data['app.capabilities'] = capabilities
|
||||
self.data['app.device_pixel_ratio'] = device_pixel_ratio
|
||||
self.data['app.default'] = False
|
||||
self.data["app.name"] = name
|
||||
self.data["app.version"] = version
|
||||
self.data["app.device_name"] = device_name
|
||||
self.data["app.device_id"] = device_id
|
||||
self.data["app.capabilities"] = capabilities
|
||||
self.data["app.device_pixel_ratio"] = device_pixel_ratio
|
||||
self.data["app.default"] = False
|
||||
|
||||
def auth(self, server, user_id, token=None, ssl=None):
|
||||
|
||||
LOG.debug("Begin auth constructor.")
|
||||
self.data['auth.server'] = server
|
||||
self.data['auth.user_id'] = user_id
|
||||
self.data['auth.token'] = token
|
||||
self.data['auth.ssl'] = ssl
|
||||
self.data["auth.server"] = server
|
||||
self.data["auth.user_id"] = user_id
|
||||
self.data["auth.token"] = token
|
||||
self.data["auth.ssl"] = ssl
|
||||
|
||||
def http(self, user_agent=None, max_retries=DEFAULT_HTTP_MAX_RETRIES, timeout=DEFAULT_HTTP_TIMEOUT):
|
||||
def http(
|
||||
self,
|
||||
user_agent=None,
|
||||
max_retries=DEFAULT_HTTP_MAX_RETRIES,
|
||||
timeout=DEFAULT_HTTP_TIMEOUT,
|
||||
):
|
||||
|
||||
LOG.debug("Begin http constructor.")
|
||||
self.data['http.max_retries'] = max_retries
|
||||
self.data['http.timeout'] = timeout
|
||||
self.data['http.user_agent'] = user_agent
|
||||
self.data["http.max_retries"] = max_retries
|
||||
self.data["http.timeout"] = timeout
|
||||
self.data["http.user_agent"] = user_agent
|
||||
|
@ -20,10 +20,10 @@ from .api import API
|
||||
|
||||
LOG = LazyLogger(__name__)
|
||||
CONNECTION_STATE = {
|
||||
'Unavailable': 0,
|
||||
'ServerSelection': 1,
|
||||
'ServerSignIn': 2,
|
||||
'SignedIn': 3
|
||||
"Unavailable": 0,
|
||||
"ServerSelection": 1,
|
||||
"ServerSignIn": 2,
|
||||
"SignedIn": 3,
|
||||
}
|
||||
|
||||
#################################################################################################
|
||||
@ -48,10 +48,10 @@ class ConnectionManager(object):
|
||||
|
||||
LOG.info("revoking token")
|
||||
|
||||
self['server']['AccessToken'] = None
|
||||
self["server"]["AccessToken"] = None
|
||||
self.credentials.set_credentials(self.credentials.get())
|
||||
|
||||
self.config.data['auth.token'] = None
|
||||
self.config.data["auth.token"] = None
|
||||
|
||||
def get_available_servers(self):
|
||||
|
||||
@ -61,11 +61,13 @@ class ConnectionManager(object):
|
||||
credentials = self.credentials.get()
|
||||
found_servers = self.process_found_servers(self._server_discovery())
|
||||
|
||||
if not found_servers and not credentials['Servers']: # back out right away, no point in continuing
|
||||
if (
|
||||
not found_servers and not credentials["Servers"]
|
||||
): # back out right away, no point in continuing
|
||||
LOG.info("Found no servers")
|
||||
return list()
|
||||
|
||||
servers = list(credentials['Servers'])
|
||||
servers = list(credentials["Servers"])
|
||||
|
||||
# Merges servers we already knew with newly found ones
|
||||
for found_server in found_servers:
|
||||
@ -74,8 +76,8 @@ class ConnectionManager(object):
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
servers.sort(key=itemgetter('DateLastAccessed'), reverse=True)
|
||||
credentials['Servers'] = servers
|
||||
servers.sort(key=itemgetter("DateLastAccessed"), reverse=True)
|
||||
credentials["Servers"] = servers
|
||||
self.credentials.set(credentials)
|
||||
|
||||
return servers
|
||||
@ -88,36 +90,35 @@ class ConnectionManager(object):
|
||||
if not server_url:
|
||||
raise AttributeError("server url cannot be empty")
|
||||
|
||||
data = self.API.login(server_url, username, password) # returns empty dict on failure
|
||||
data = self.API.login(
|
||||
server_url, username, password
|
||||
) # returns empty dict on failure
|
||||
|
||||
if not data:
|
||||
LOG.info("Failed to login as `"+username+"`")
|
||||
LOG.info("Failed to login as `" + username + "`")
|
||||
return {}
|
||||
|
||||
LOG.info("Successfully logged in as %s" % (username))
|
||||
# TODO Change when moving to database storage of server details
|
||||
credentials = self.credentials.get()
|
||||
|
||||
self.config.data['auth.user_id'] = data['User']['Id']
|
||||
self.config.data['auth.token'] = data['AccessToken']
|
||||
self.config.data["auth.user_id"] = data["User"]["Id"]
|
||||
self.config.data["auth.token"] = data["AccessToken"]
|
||||
|
||||
for server in credentials['Servers']:
|
||||
if server['Id'] == data['ServerId']:
|
||||
for server in credentials["Servers"]:
|
||||
if server["Id"] == data["ServerId"]:
|
||||
found_server = server
|
||||
break
|
||||
else:
|
||||
return {} # No server found
|
||||
|
||||
found_server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
found_server['UserId'] = data['User']['Id']
|
||||
found_server['AccessToken'] = data['AccessToken']
|
||||
found_server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
found_server["UserId"] = data["User"]["Id"]
|
||||
found_server["AccessToken"] = data["AccessToken"]
|
||||
|
||||
self.credentials.add_update_server(credentials['Servers'], found_server)
|
||||
self.credentials.add_update_server(credentials["Servers"], found_server)
|
||||
|
||||
info = {
|
||||
'Id': data['User']['Id'],
|
||||
'IsSignedInOffline': True
|
||||
}
|
||||
info = {"Id": data["User"]["Id"], "IsSignedInOffline": True}
|
||||
self.credentials.add_update_user(server, info)
|
||||
|
||||
self.credentials.set_credentials(credentials)
|
||||
@ -137,40 +138,44 @@ class ConnectionManager(object):
|
||||
address = response_url
|
||||
LOG.info("connectToAddress %s succeeded", address)
|
||||
server = {
|
||||
'address': address,
|
||||
"address": address,
|
||||
}
|
||||
server = self.connect_to_server(server, options)
|
||||
if server is False:
|
||||
LOG.error("connectToAddress %s failed", address)
|
||||
return {'State': CONNECTION_STATE['Unavailable']}
|
||||
return {"State": CONNECTION_STATE["Unavailable"]}
|
||||
|
||||
return server
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
LOG.error("connectToAddress %s failed", address)
|
||||
return {'State': CONNECTION_STATE['Unavailable']}
|
||||
return {"State": CONNECTION_STATE["Unavailable"]}
|
||||
|
||||
def connect_to_server(self, server, options={}):
|
||||
|
||||
LOG.info("begin connectToServer")
|
||||
|
||||
try:
|
||||
result = self.API.get_public_info(server.get('address'))
|
||||
result = self.API.get_public_info(server.get("address"))
|
||||
|
||||
if not result:
|
||||
LOG.error("Failed to connect to server: %s" % server.get('address'))
|
||||
return {'State': CONNECTION_STATE['Unavailable']}
|
||||
LOG.error("Failed to connect to server: %s" % server.get("address"))
|
||||
return {"State": CONNECTION_STATE["Unavailable"]}
|
||||
|
||||
LOG.info("calling onSuccessfulConnection with server %s", server.get('Name'))
|
||||
LOG.info(
|
||||
"calling onSuccessfulConnection with server %s", server.get("Name")
|
||||
)
|
||||
|
||||
self._update_server_info(server, result)
|
||||
credentials = self.credentials.get()
|
||||
return self._after_connect_validated(server, credentials, result, True, options)
|
||||
return self._after_connect_validated(
|
||||
server, credentials, result, True, options
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(traceback.format_exc())
|
||||
LOG.error("Failing server connection. ERROR msg: {}".format(e))
|
||||
return {'State': CONNECTION_STATE['Unavailable']}
|
||||
return {"State": CONNECTION_STATE["Unavailable"]}
|
||||
|
||||
def connect(self, options={}):
|
||||
|
||||
@ -180,9 +185,7 @@ class ConnectionManager(object):
|
||||
LOG.info("connect has %s servers", len(servers))
|
||||
|
||||
if not (len(servers)): # No servers provided
|
||||
return {
|
||||
'State': ['ServerSelection']
|
||||
}
|
||||
return {"State": ["ServerSelection"]}
|
||||
|
||||
result = self.connect_to_server(servers[0], options)
|
||||
LOG.debug("resolving connect with result: %s", result)
|
||||
@ -190,7 +193,7 @@ class ConnectionManager(object):
|
||||
return result
|
||||
|
||||
def jellyfin_token(self): # Called once monitor.py#163
|
||||
return self.get_server_info(self.server_id)['AccessToken']
|
||||
return self.get_server_info(self.server_id)["AccessToken"]
|
||||
|
||||
def get_server_info(self, server_id):
|
||||
|
||||
@ -198,14 +201,14 @@ class ConnectionManager(object):
|
||||
LOG.info("server_id is empty")
|
||||
return {}
|
||||
|
||||
servers = self.credentials.get()['Servers']
|
||||
servers = self.credentials.get()["Servers"]
|
||||
|
||||
for server in servers:
|
||||
if server['Id'] == server_id:
|
||||
if server["Id"] == server_id:
|
||||
return server
|
||||
|
||||
def get_server_address(self, server_id):
|
||||
return self.get_server_info(server_id or self.server_id).get('address')
|
||||
return self.get_server_info(server_id or self.server_id).get("address")
|
||||
|
||||
def get_public_users(self):
|
||||
return self.client.jellyfin.get_public_users()
|
||||
@ -258,9 +261,9 @@ class ConnectionManager(object):
|
||||
server = self._convert_endpoint_address_to_manual_address(found_server)
|
||||
|
||||
info = {
|
||||
'Id': found_server['Id'],
|
||||
'address': server or found_server['Address'],
|
||||
'Name': found_server['Name']
|
||||
"Id": found_server["Id"],
|
||||
"address": server or found_server["Address"],
|
||||
"Name": found_server["Name"],
|
||||
}
|
||||
|
||||
servers.append(info)
|
||||
@ -270,11 +273,11 @@ class ConnectionManager(object):
|
||||
# TODO: Make IPv6 compatible
|
||||
def _convert_endpoint_address_to_manual_address(self, info):
|
||||
|
||||
if info.get('Address') and info.get('EndpointAddress'):
|
||||
address = info['EndpointAddress'].split(':')[0]
|
||||
if info.get("Address") and info.get("EndpointAddress"):
|
||||
address = info["EndpointAddress"].split(":")[0]
|
||||
|
||||
# Determine the port, if any
|
||||
parts = info['Address'].split(':')
|
||||
parts = info["Address"].split(":")
|
||||
if len(parts) > 1:
|
||||
port_string = parts[len(parts) - 1]
|
||||
|
||||
@ -288,64 +291,70 @@ class ConnectionManager(object):
|
||||
|
||||
def _normalize_address(self, address):
|
||||
# TODO: Try HTTPS first, then HTTP if that fails.
|
||||
if '://' not in address:
|
||||
address = 'http://' + address
|
||||
if "://" not in address:
|
||||
address = "http://" + address
|
||||
|
||||
# Attempt to correct bad input
|
||||
url = urllib3.util.parse_url(address.strip())
|
||||
|
||||
if url.scheme is None:
|
||||
url = url._replace(scheme='http')
|
||||
url = url._replace(scheme="http")
|
||||
|
||||
if url.scheme == 'http' and url.port == 80:
|
||||
if url.scheme == "http" and url.port == 80:
|
||||
url = url._replace(port=None)
|
||||
|
||||
if url.scheme == 'https' and url.port == 443:
|
||||
if url.scheme == "https" and url.port == 443:
|
||||
url = url._replace(port=None)
|
||||
|
||||
return url.url
|
||||
|
||||
def _after_connect_validated(self, server, credentials, system_info, verify_authentication, options):
|
||||
if options.get('enableAutoLogin') is False:
|
||||
def _after_connect_validated(
|
||||
self, server, credentials, system_info, verify_authentication, options
|
||||
):
|
||||
if options.get("enableAutoLogin") is False:
|
||||
|
||||
self.config.data['auth.user_id'] = server.pop('UserId', None)
|
||||
self.config.data['auth.token'] = server.pop('AccessToken', None)
|
||||
self.config.data["auth.user_id"] = server.pop("UserId", None)
|
||||
self.config.data["auth.token"] = server.pop("AccessToken", None)
|
||||
|
||||
elif verify_authentication and server.get('AccessToken'):
|
||||
elif verify_authentication and server.get("AccessToken"):
|
||||
system_info = self.API.validate_authentication_token(server)
|
||||
if 'Status_Code' not in system_info:
|
||||
if "Status_Code" not in system_info:
|
||||
|
||||
self._update_server_info(server, system_info)
|
||||
self.config.data['auth.user_id'] = server['UserId']
|
||||
self.config.data['auth.token'] = server['AccessToken']
|
||||
system_info['Status_Code'] = 200
|
||||
self.config.data["auth.user_id"] = server["UserId"]
|
||||
self.config.data["auth.token"] = server["AccessToken"]
|
||||
system_info["Status_Code"] = 200
|
||||
|
||||
return self._after_connect_validated(server, credentials, system_info, False, options)
|
||||
return self._after_connect_validated(
|
||||
server, credentials, system_info, False, options
|
||||
)
|
||||
|
||||
server['UserId'] = None
|
||||
server['AccessToken'] = None
|
||||
system_info['State'] = CONNECTION_STATE['Unavailable']
|
||||
server["UserId"] = None
|
||||
server["AccessToken"] = None
|
||||
system_info["State"] = CONNECTION_STATE["Unavailable"]
|
||||
return system_info
|
||||
|
||||
self._update_server_info(server, system_info)
|
||||
|
||||
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
self.credentials.add_update_server(credentials['Servers'], server)
|
||||
server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
self.credentials.add_update_server(credentials["Servers"], server)
|
||||
self.credentials.set(credentials)
|
||||
self.server_id = server['Id']
|
||||
self.server_id = server["Id"]
|
||||
|
||||
# Update configs
|
||||
self.config.data['auth.server'] = server['address']
|
||||
self.config.data['auth.server-name'] = server['Name']
|
||||
self.config.data['auth.server=id'] = server['Id']
|
||||
self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl'])
|
||||
self.config.data["auth.server"] = server["address"]
|
||||
self.config.data["auth.server-name"] = server["Name"]
|
||||
self.config.data["auth.server=id"] = server["Id"]
|
||||
self.config.data["auth.ssl"] = options.get("ssl", self.config.data["auth.ssl"])
|
||||
|
||||
# Connected
|
||||
return {
|
||||
'Servers': [server],
|
||||
'State': CONNECTION_STATE['SignedIn']
|
||||
if server.get('AccessToken')
|
||||
else CONNECTION_STATE['ServerSignIn'],
|
||||
"Servers": [server],
|
||||
"State": (
|
||||
CONNECTION_STATE["SignedIn"]
|
||||
if server.get("AccessToken")
|
||||
else CONNECTION_STATE["ServerSignIn"]
|
||||
),
|
||||
}
|
||||
|
||||
def _update_server_info(self, server, system_info):
|
||||
@ -353,8 +362,8 @@ class ConnectionManager(object):
|
||||
if server is None or system_info is None:
|
||||
return
|
||||
|
||||
server['Name'] = system_info['ServerName']
|
||||
server['Id'] = system_info['Id']
|
||||
server["Name"] = system_info["ServerName"]
|
||||
server["Id"] = system_info["Id"]
|
||||
|
||||
if system_info.get('address'):
|
||||
server['address'] = system_info['address']
|
||||
if system_info.get("address"):
|
||||
server["address"] = system_info["address"]
|
||||
|
@ -41,7 +41,7 @@ class Credentials(object):
|
||||
self.credentials = {}
|
||||
|
||||
LOG.debug("credentials initialized with: %s", self.credentials)
|
||||
self.credentials['Servers'] = self.credentials.setdefault('Servers', [])
|
||||
self.credentials["Servers"] = self.credentials.setdefault("Servers", [])
|
||||
|
||||
def get(self):
|
||||
self._ensure()
|
||||
@ -62,53 +62,55 @@ class Credentials(object):
|
||||
|
||||
def add_update_user(self, server, user):
|
||||
|
||||
for existing in server.setdefault('Users', []):
|
||||
if existing['Id'] == user['Id']:
|
||||
for existing in server.setdefault("Users", []):
|
||||
if existing["Id"] == user["Id"]:
|
||||
# Merge the data
|
||||
existing['IsSignedInOffline'] = True
|
||||
existing["IsSignedInOffline"] = True
|
||||
break
|
||||
else:
|
||||
server['Users'].append(user)
|
||||
server["Users"].append(user)
|
||||
|
||||
def add_update_server(self, servers, server):
|
||||
|
||||
if server.get('Id') is None:
|
||||
if server.get("Id") is None:
|
||||
raise KeyError("Server['Id'] cannot be null or empty")
|
||||
|
||||
# Add default DateLastAccessed if doesn't exist.
|
||||
server.setdefault('DateLastAccessed', "1970-01-01T00:00:00Z")
|
||||
server.setdefault("DateLastAccessed", "1970-01-01T00:00:00Z")
|
||||
|
||||
for existing in servers:
|
||||
if existing['Id'] == server['Id']:
|
||||
if existing["Id"] == server["Id"]:
|
||||
|
||||
# Merge the data
|
||||
if server.get('DateLastAccessed') and self._date_object(server['DateLastAccessed']) > self._date_object(existing['DateLastAccessed']):
|
||||
existing['DateLastAccessed'] = server['DateLastAccessed']
|
||||
if server.get("DateLastAccessed") and self._date_object(
|
||||
server["DateLastAccessed"]
|
||||
) > self._date_object(existing["DateLastAccessed"]):
|
||||
existing["DateLastAccessed"] = server["DateLastAccessed"]
|
||||
|
||||
if server.get('UserLinkType'):
|
||||
existing['UserLinkType'] = server['UserLinkType']
|
||||
if server.get("UserLinkType"):
|
||||
existing["UserLinkType"] = server["UserLinkType"]
|
||||
|
||||
if server.get('AccessToken'):
|
||||
existing['AccessToken'] = server['AccessToken']
|
||||
existing['UserId'] = server['UserId']
|
||||
if server.get("AccessToken"):
|
||||
existing["AccessToken"] = server["AccessToken"]
|
||||
existing["UserId"] = server["UserId"]
|
||||
|
||||
if server.get('ExchangeToken'):
|
||||
existing['ExchangeToken'] = server['ExchangeToken']
|
||||
if server.get("ExchangeToken"):
|
||||
existing["ExchangeToken"] = server["ExchangeToken"]
|
||||
|
||||
if server.get('ManualAddress'):
|
||||
existing['ManualAddress'] = server['ManualAddress']
|
||||
if server.get("ManualAddress"):
|
||||
existing["ManualAddress"] = server["ManualAddress"]
|
||||
|
||||
if server.get('LocalAddress'):
|
||||
existing['LocalAddress'] = server['LocalAddress']
|
||||
if server.get("LocalAddress"):
|
||||
existing["LocalAddress"] = server["LocalAddress"]
|
||||
|
||||
if server.get('Name'):
|
||||
existing['Name'] = server['Name']
|
||||
if server.get("Name"):
|
||||
existing["Name"] = server["Name"]
|
||||
|
||||
if server.get('LastConnectionMode') is not None:
|
||||
existing['LastConnectionMode'] = server['LastConnectionMode']
|
||||
if server.get("LastConnectionMode") is not None:
|
||||
existing["LastConnectionMode"] = server["LastConnectionMode"]
|
||||
|
||||
if server.get('ConnectServerId'):
|
||||
existing['ConnectServerId'] = server['ConnectServerId']
|
||||
if server.get("ConnectServerId"):
|
||||
existing["ConnectServerId"] = server["ConnectServerId"]
|
||||
|
||||
return existing
|
||||
|
||||
|
@ -34,9 +34,13 @@ class HTTP(object):
|
||||
|
||||
self.session = requests.Session()
|
||||
|
||||
max_retries = self.config.data['http.max_retries']
|
||||
self.session.mount("http://", requests.adapters.HTTPAdapter(max_retries=max_retries))
|
||||
self.session.mount("https://", requests.adapters.HTTPAdapter(max_retries=max_retries))
|
||||
max_retries = self.config.data["http.max_retries"]
|
||||
self.session.mount(
|
||||
"http://", requests.adapters.HTTPAdapter(max_retries=max_retries)
|
||||
)
|
||||
self.session.mount(
|
||||
"https://", requests.adapters.HTTPAdapter(max_retries=max_retries)
|
||||
)
|
||||
|
||||
def stop_session(self):
|
||||
|
||||
@ -51,43 +55,44 @@ class HTTP(object):
|
||||
|
||||
def _replace_user_info(self, string):
|
||||
|
||||
if '{server}' in string:
|
||||
if self.config.data.get('auth.server', None):
|
||||
string = string.replace("{server}", self.config.data['auth.server'])
|
||||
if "{server}" in string:
|
||||
if self.config.data.get("auth.server", None):
|
||||
string = string.replace("{server}", self.config.data["auth.server"])
|
||||
else:
|
||||
LOG.debug("Server address not set")
|
||||
|
||||
if '{UserId}' in string:
|
||||
if self.config.data.get('auth.user_id', None):
|
||||
string = string.replace("{UserId}", self.config.data['auth.user_id'])
|
||||
if "{UserId}" in string:
|
||||
if self.config.data.get("auth.user_id", None):
|
||||
string = string.replace("{UserId}", self.config.data["auth.user_id"])
|
||||
else:
|
||||
LOG.debug("UserId is not set.")
|
||||
|
||||
return string
|
||||
|
||||
def request(self, data, session=None):
|
||||
|
||||
''' Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
|
||||
data dictionary can contain:
|
||||
type: GET, POST, etc.
|
||||
url: (optional)
|
||||
handler: not considered when url is provided (optional)
|
||||
params: request parameters (optional)
|
||||
json: request body (optional)
|
||||
headers: (optional),
|
||||
verify: ssl certificate, True (verify using device built-in library) or False
|
||||
'''
|
||||
"""Give a chance to retry the connection. Jellyfin sometimes can be slow to answer back
|
||||
data dictionary can contain:
|
||||
type: GET, POST, etc.
|
||||
url: (optional)
|
||||
handler: not considered when url is provided (optional)
|
||||
params: request parameters (optional)
|
||||
json: request body (optional)
|
||||
headers: (optional),
|
||||
verify: ssl certificate, True (verify using device built-in library) or False
|
||||
"""
|
||||
if not data:
|
||||
raise AttributeError("Request cannot be empty")
|
||||
|
||||
data = self._request(data)
|
||||
LOG.debug("--->[ http ] %s", JsonDebugPrinter(data))
|
||||
retry = data.pop('retry', 5)
|
||||
retry = data.pop("retry", 5)
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
r = self._requests(session or self.session or requests, data.pop('type', "GET"), **data)
|
||||
r = self._requests(
|
||||
session or self.session or requests, data.pop("type", "GET"), **data
|
||||
)
|
||||
r.content # release the connection
|
||||
|
||||
if not self.keep_alive and self.session is not None:
|
||||
@ -104,7 +109,10 @@ class HTTP(object):
|
||||
continue
|
||||
|
||||
LOG.error(error)
|
||||
self.client.callback("ServerUnreachable", {'ServerId': self.config.data['auth.server-id']})
|
||||
self.client.callback(
|
||||
"ServerUnreachable",
|
||||
{"ServerId": self.config.data["auth.server-id"]},
|
||||
)
|
||||
|
||||
raise HTTPException("ServerUnreachable", error)
|
||||
|
||||
@ -125,12 +133,18 @@ class HTTP(object):
|
||||
|
||||
if r.status_code == 401:
|
||||
|
||||
if 'X-Application-Error-Code' in r.headers:
|
||||
self.client.callback("AccessRestricted", {'ServerId': self.config.data['auth.server-id']})
|
||||
if "X-Application-Error-Code" in r.headers:
|
||||
self.client.callback(
|
||||
"AccessRestricted",
|
||||
{"ServerId": self.config.data["auth.server-id"]},
|
||||
)
|
||||
|
||||
raise HTTPException("AccessRestricted", error)
|
||||
else:
|
||||
self.client.callback("Unauthorized", {'ServerId': self.config.data['auth.server-id']})
|
||||
self.client.callback(
|
||||
"Unauthorized",
|
||||
{"ServerId": self.config.data["auth.server-id"]},
|
||||
)
|
||||
self.client.auth.revoke_token()
|
||||
|
||||
raise HTTPException("Unauthorized", error)
|
||||
@ -160,11 +174,13 @@ class HTTP(object):
|
||||
|
||||
except requests.exceptions.MissingSchema as error:
|
||||
LOG.error("Request missing Schema. " + str(error))
|
||||
raise HTTPException("MissingSchema", {'Id': self.config.data.get('auth.server', "None")})
|
||||
raise HTTPException(
|
||||
"MissingSchema", {"Id": self.config.data.get("auth.server", "None")}
|
||||
)
|
||||
|
||||
else:
|
||||
try:
|
||||
self.config.data['server-time'] = r.headers['Date']
|
||||
self.config.data["server-time"] = r.headers["Date"]
|
||||
elapsed = int(r.elapsed.total_seconds() * 1000)
|
||||
response = r.json()
|
||||
LOG.debug("---<[ http ][%s ms]", elapsed)
|
||||
@ -179,15 +195,18 @@ class HTTP(object):
|
||||
|
||||
def _request(self, data):
|
||||
|
||||
if 'url' not in data:
|
||||
data['url'] = "%s/%s" % (self.config.data.get("auth.server", ""), data.pop('handler', ""))
|
||||
if "url" not in data:
|
||||
data["url"] = "%s/%s" % (
|
||||
self.config.data.get("auth.server", ""),
|
||||
data.pop("handler", ""),
|
||||
)
|
||||
|
||||
self._get_header(data)
|
||||
data['timeout'] = data.get('timeout') or self.config.data['http.timeout']
|
||||
data['verify'] = data.get('verify') or self.config.data.get('auth.ssl', False)
|
||||
data['url'] = self._replace_user_info(data['url'])
|
||||
self._process_params(data.get('params') or {})
|
||||
self._process_params(data.get('json') or {})
|
||||
data["timeout"] = data.get("timeout") or self.config.data["http.timeout"]
|
||||
data["verify"] = data.get("verify") or self.config.data.get("auth.ssl", False)
|
||||
data["url"] = self._replace_user_info(data["url"])
|
||||
self._process_params(data.get("params") or {})
|
||||
self._process_params(data.get("json") or {})
|
||||
|
||||
return data
|
||||
|
||||
@ -204,17 +223,24 @@ class HTTP(object):
|
||||
|
||||
def _get_header(self, data):
|
||||
|
||||
data['headers'] = data.setdefault('headers', {})
|
||||
data["headers"] = data.setdefault("headers", {})
|
||||
|
||||
if not data['headers']:
|
||||
data['headers'].update({
|
||||
'Content-type': "application/json",
|
||||
'Accept-Charset': "UTF-8,*",
|
||||
'Accept-encoding': "gzip",
|
||||
'User-Agent': self.config.data['http.user_agent'] or "%s/%s" % (self.config.data.get('app.name', 'Jellyfin for Kodi'), self.config.data.get('app.version', "0.0.0"))
|
||||
})
|
||||
if not data["headers"]:
|
||||
data["headers"].update(
|
||||
{
|
||||
"Content-type": "application/json",
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-encoding": "gzip",
|
||||
"User-Agent": self.config.data["http.user_agent"]
|
||||
or "%s/%s"
|
||||
% (
|
||||
self.config.data.get("app.name", "Jellyfin for Kodi"),
|
||||
self.config.data.get("app.version", "0.0.0"),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if 'x-emby-authorization' not in data['headers']:
|
||||
if "x-emby-authorization" not in data["headers"]:
|
||||
self._authorization(data)
|
||||
|
||||
return data
|
||||
@ -222,19 +248,26 @@ class HTTP(object):
|
||||
def _authorization(self, data):
|
||||
|
||||
auth = "MediaBrowser "
|
||||
auth += "Client=%s, " % self.config.data.get('app.name', "Jellyfin for Kodi")
|
||||
auth += "Device=%s, " % self.config.data.get('app.device_name', 'Unknown Device')
|
||||
auth += "DeviceId=%s, " % self.config.data.get('app.device_id', 'Unknown Device id')
|
||||
auth += "Version=%s" % self.config.data.get('app.version', '0.0.0')
|
||||
auth += "Client=%s, " % self.config.data.get("app.name", "Jellyfin for Kodi")
|
||||
auth += "Device=%s, " % self.config.data.get(
|
||||
"app.device_name", "Unknown Device"
|
||||
)
|
||||
auth += "DeviceId=%s, " % self.config.data.get(
|
||||
"app.device_id", "Unknown Device id"
|
||||
)
|
||||
auth += "Version=%s" % self.config.data.get("app.version", "0.0.0")
|
||||
|
||||
data['headers'].update({'x-emby-authorization': ensure_str(auth, 'utf-8')})
|
||||
data["headers"].update({"x-emby-authorization": ensure_str(auth, "utf-8")})
|
||||
|
||||
if self.config.data.get('auth.token') and self.config.data.get('auth.user_id'):
|
||||
if self.config.data.get("auth.token") and self.config.data.get("auth.user_id"):
|
||||
|
||||
auth += ', UserId=%s' % self.config.data.get('auth.user_id')
|
||||
data['headers'].update({
|
||||
'x-emby-authorization': ensure_str(auth, 'utf-8'),
|
||||
'X-MediaBrowser-Token': self.config.data.get('auth.token')})
|
||||
auth += ", UserId=%s" % self.config.data.get("auth.user_id")
|
||||
data["headers"].update(
|
||||
{
|
||||
"x-emby-authorization": ensure_str(auth, "utf-8"),
|
||||
"X-MediaBrowser-Token": self.config.data.get("auth.token"),
|
||||
}
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
@ -14,7 +14,8 @@ from ..helper import LazyLogger, settings
|
||||
# If numpy is installed, the websockets library tries to use it, and then
|
||||
# kodi hard crashes for reasons I don't even want to pretend to understand
|
||||
import sys # noqa: E402,I100
|
||||
sys.modules['numpy'] = None
|
||||
|
||||
sys.modules["numpy"] = None
|
||||
import websocket # noqa: E402,I201
|
||||
|
||||
##################################################################################################
|
||||
@ -41,23 +42,29 @@ class WSClient(threading.Thread):
|
||||
if self.wsc is None:
|
||||
raise ValueError("The websocket client is not started.")
|
||||
|
||||
self.wsc.send(json.dumps({'MessageType': message, "Data": data}))
|
||||
self.wsc.send(json.dumps({"MessageType": message, "Data": data}))
|
||||
|
||||
def run(self):
|
||||
|
||||
monitor = xbmc.Monitor()
|
||||
token = self.client.config.data['auth.token']
|
||||
device_id = self.client.config.data['app.device_id']
|
||||
server = self.client.config.data['auth.server']
|
||||
server = server.replace('https://', 'wss://') if server.startswith('https') else server.replace('http://', 'ws://')
|
||||
token = self.client.config.data["auth.token"]
|
||||
device_id = self.client.config.data["app.device_id"]
|
||||
server = self.client.config.data["auth.server"]
|
||||
server = (
|
||||
server.replace("https://", "wss://")
|
||||
if server.startswith("https")
|
||||
else server.replace("http://", "ws://")
|
||||
)
|
||||
wsc_url = "%s/socket?api_key=%s&device_id=%s" % (server, token, device_id)
|
||||
|
||||
LOG.info("Websocket url: %s", wsc_url)
|
||||
|
||||
self.wsc = websocket.WebSocketApp(wsc_url,
|
||||
on_open=lambda ws: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error))
|
||||
self.wsc = websocket.WebSocketApp(
|
||||
wsc_url,
|
||||
on_open=lambda ws: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error),
|
||||
)
|
||||
|
||||
while not self.stop:
|
||||
|
||||
@ -73,41 +80,42 @@ class WSClient(threading.Thread):
|
||||
LOG.info("--->[ websocket opened ]")
|
||||
# Avoid a timing issue where the capabilities are not correctly registered
|
||||
time.sleep(5)
|
||||
if settings('remoteControl.bool'):
|
||||
self.client.jellyfin.post_capabilities({
|
||||
'PlayableMediaTypes': "Audio,Video",
|
||||
'SupportsMediaControl': True,
|
||||
'SupportedCommands': (
|
||||
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
|
||||
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
|
||||
"GoHome,PageUp,NextLetter,GoToSearch,"
|
||||
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
|
||||
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
|
||||
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
|
||||
"SetRepeatMode,Mute,Unmute,SetVolume,"
|
||||
"Play,Playstate,PlayNext,PlayMediaSource"
|
||||
),
|
||||
})
|
||||
if settings("remoteControl.bool"):
|
||||
self.client.jellyfin.post_capabilities(
|
||||
{
|
||||
"PlayableMediaTypes": "Audio,Video",
|
||||
"SupportsMediaControl": True,
|
||||
"SupportedCommands": (
|
||||
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
|
||||
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
|
||||
"GoHome,PageUp,NextLetter,GoToSearch,"
|
||||
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
|
||||
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
|
||||
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
|
||||
"SetRepeatMode,Mute,Unmute,SetVolume,"
|
||||
"Play,Playstate,PlayNext,PlayMediaSource"
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.client.jellyfin.post_capabilities({
|
||||
"PlayableMediaTypes": "Audio, Video",
|
||||
"SupportsMediaControl": False
|
||||
})
|
||||
self.client.jellyfin.post_capabilities(
|
||||
{"PlayableMediaTypes": "Audio, Video", "SupportsMediaControl": False}
|
||||
)
|
||||
|
||||
def on_message(self, ws, message):
|
||||
|
||||
message = json.loads(message)
|
||||
data = message.get('Data', {})
|
||||
data = message.get("Data", {})
|
||||
|
||||
if message['MessageType'] in ('RefreshProgress',):
|
||||
if message["MessageType"] in ("RefreshProgress",):
|
||||
LOG.debug("Ignoring %s", message)
|
||||
|
||||
return
|
||||
|
||||
if not self.client.config.data['app.default']:
|
||||
data['ServerId'] = self.client.auth.server_id
|
||||
if not self.client.config.data["app.default"]:
|
||||
data["ServerId"] = self.client.auth.server_id
|
||||
|
||||
self.client.callback(message['MessageType'], data)
|
||||
self.client.callback(message["MessageType"], data)
|
||||
|
||||
def stop_client(self):
|
||||
|
||||
|
@ -24,8 +24,8 @@ from .jellyfin import Jellyfin
|
||||
##################################################################################################
|
||||
|
||||
LOG = LazyLogger(__name__)
|
||||
LIMIT = int(settings('limitIndex') or 15)
|
||||
DTHREADS = int(settings('limitThreads') or 3)
|
||||
LIMIT = int(settings("limitIndex") or 15)
|
||||
DTHREADS = int(settings("limitThreads") or 3)
|
||||
TARGET_DB_VERSION = 1
|
||||
|
||||
##################################################################################################
|
||||
@ -43,8 +43,8 @@ class Library(threading.Thread):
|
||||
|
||||
def __init__(self, monitor):
|
||||
|
||||
self.direct_path = settings('useDirectPaths') == "1"
|
||||
self.progress_display = int(settings('syncProgress') or 50)
|
||||
self.direct_path = settings("useDirectPaths") == "1"
|
||||
self.progress_display = int(settings("syncProgress") or 50)
|
||||
self.monitor = monitor
|
||||
self.player = monitor.monitor.player
|
||||
self.server = Jellyfin().get_client()
|
||||
@ -59,7 +59,7 @@ class Library(threading.Thread):
|
||||
self.jellyfin_threads = []
|
||||
self.download_threads = []
|
||||
self.notify_threads = []
|
||||
self.writer_threads = {'updated': [], 'userdata': [], 'removed': []}
|
||||
self.writer_threads = {"updated": [], "userdata": [], "removed": []}
|
||||
self.database_lock = threading.Lock()
|
||||
self.music_database_lock = threading.Lock()
|
||||
|
||||
@ -67,16 +67,16 @@ class Library(threading.Thread):
|
||||
|
||||
def __new_queues__(self):
|
||||
return {
|
||||
'Movie': Queue.Queue(),
|
||||
'BoxSet': Queue.Queue(),
|
||||
'MusicVideo': Queue.Queue(),
|
||||
'Series': Queue.Queue(),
|
||||
'Season': Queue.Queue(),
|
||||
'Episode': Queue.Queue(),
|
||||
'MusicAlbum': Queue.Queue(),
|
||||
'MusicArtist': Queue.Queue(),
|
||||
'AlbumArtist': Queue.Queue(),
|
||||
'Audio': Queue.Queue()
|
||||
"Movie": Queue.Queue(),
|
||||
"BoxSet": Queue.Queue(),
|
||||
"MusicVideo": Queue.Queue(),
|
||||
"Series": Queue.Queue(),
|
||||
"Season": Queue.Queue(),
|
||||
"Episode": Queue.Queue(),
|
||||
"MusicAlbum": Queue.Queue(),
|
||||
"MusicArtist": Queue.Queue(),
|
||||
"AlbumArtist": Queue.Queue(),
|
||||
"Audio": Queue.Queue(),
|
||||
}
|
||||
|
||||
def run(self):
|
||||
@ -86,7 +86,7 @@ class Library(threading.Thread):
|
||||
if not self.startup():
|
||||
self.stop_client()
|
||||
|
||||
window('jellyfin_startup.bool', True)
|
||||
window("jellyfin_startup.bool", True)
|
||||
|
||||
while not self.stop_thread:
|
||||
|
||||
@ -105,17 +105,15 @@ class Library(threading.Thread):
|
||||
LOG.info("---<[ library ]")
|
||||
|
||||
def test_databases(self):
|
||||
|
||||
''' Open the databases to test if the file exists.
|
||||
'''
|
||||
with Database('video'), Database('music'):
|
||||
"""Open the databases to test if the file exists."""
|
||||
with Database("video"), Database("music"):
|
||||
pass
|
||||
|
||||
def check_version(self):
|
||||
'''
|
||||
"""
|
||||
Checks database version and triggers any required data migrations
|
||||
'''
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
"""
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
db_version = db.get_version()
|
||||
|
||||
@ -124,26 +122,37 @@ class Library(threading.Thread):
|
||||
db.add_version((TARGET_DB_VERSION))
|
||||
|
||||
# Video Database Migrations
|
||||
with Database('video') as videodb:
|
||||
with Database("video") as videodb:
|
||||
vid_db = KodiDb(videodb.cursor)
|
||||
if vid_db.migrations():
|
||||
LOG.info('changes detected, reloading skin')
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
xbmc.executebuiltin('ReloadSkin()')
|
||||
LOG.info("changes detected, reloading skin")
|
||||
xbmc.executebuiltin("UpdateLibrary(video)")
|
||||
xbmc.executebuiltin("ReloadSkin()")
|
||||
|
||||
@stop
|
||||
def service(self):
|
||||
"""If error is encountered, it will rerun this function.
|
||||
Start new "daemon threads" to process library updates.
|
||||
(actual daemon thread is not supported in Kodi)
|
||||
"""
|
||||
self.download_threads = [
|
||||
thread for thread in self.download_threads if not thread.is_done
|
||||
]
|
||||
self.writer_threads["updated"] = [
|
||||
thread for thread in self.writer_threads["updated"] if not thread.is_done
|
||||
]
|
||||
self.writer_threads["userdata"] = [
|
||||
thread for thread in self.writer_threads["userdata"] if not thread.is_done
|
||||
]
|
||||
self.writer_threads["removed"] = [
|
||||
thread for thread in self.writer_threads["removed"] if not thread.is_done
|
||||
]
|
||||
|
||||
''' If error is encountered, it will rerun this function.
|
||||
Start new "daemon threads" to process library updates.
|
||||
(actual daemon thread is not supported in Kodi)
|
||||
'''
|
||||
self.download_threads = [thread for thread in self.download_threads if not thread.is_done]
|
||||
self.writer_threads['updated'] = [thread for thread in self.writer_threads['updated'] if not thread.is_done]
|
||||
self.writer_threads['userdata'] = [thread for thread in self.writer_threads['userdata'] if not thread.is_done]
|
||||
self.writer_threads['removed'] = [thread for thread in self.writer_threads['removed'] if not thread.is_done]
|
||||
|
||||
if not self.player.isPlayingVideo() or settings('syncDuringPlay.bool') or xbmc.getCondVisibility('VideoPlayer.Content(livetv)'):
|
||||
if (
|
||||
not self.player.isPlayingVideo()
|
||||
or settings("syncDuringPlay.bool")
|
||||
or xbmc.getCondVisibility("VideoPlayer.Content(livetv)")
|
||||
):
|
||||
|
||||
self.worker_downloads()
|
||||
self.worker_sort()
|
||||
@ -154,7 +163,7 @@ class Library(threading.Thread):
|
||||
self.worker_notify()
|
||||
|
||||
if self.pending_refresh:
|
||||
window('jellyfin_sync.bool', True)
|
||||
window("jellyfin_sync.bool", True)
|
||||
|
||||
if self.total_updates > self.progress_display:
|
||||
queue_size = self.worker_queue_size()
|
||||
@ -162,58 +171,91 @@ class Library(threading.Thread):
|
||||
if self.progress_updates is None:
|
||||
|
||||
self.progress_updates = xbmcgui.DialogProgressBG()
|
||||
self.progress_updates.create(translate('addon_name'), translate(33178))
|
||||
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size))
|
||||
self.progress_updates.create(
|
||||
translate("addon_name"), translate(33178)
|
||||
)
|
||||
self.progress_updates.update(
|
||||
int(
|
||||
(
|
||||
float(self.total_updates - queue_size)
|
||||
/ float(self.total_updates)
|
||||
)
|
||||
* 100
|
||||
),
|
||||
message="%s: %s" % (translate(33178), queue_size),
|
||||
)
|
||||
elif queue_size:
|
||||
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size))
|
||||
self.progress_updates.update(
|
||||
int(
|
||||
(
|
||||
float(self.total_updates - queue_size)
|
||||
/ float(self.total_updates)
|
||||
)
|
||||
* 100
|
||||
),
|
||||
message="%s: %s" % (translate(33178), queue_size),
|
||||
)
|
||||
else:
|
||||
self.progress_updates.update(int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message=translate(33178))
|
||||
self.progress_updates.update(
|
||||
int(
|
||||
(
|
||||
float(self.total_updates - queue_size)
|
||||
/ float(self.total_updates)
|
||||
)
|
||||
* 100
|
||||
),
|
||||
message=translate(33178),
|
||||
)
|
||||
|
||||
if not settings('dbSyncScreensaver.bool') and self.screensaver is None:
|
||||
if not settings("dbSyncScreensaver.bool") and self.screensaver is None:
|
||||
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
||||
xbmc.executebuiltin("InhibitIdleShutdown(true)")
|
||||
self.screensaver = get_screensaver()
|
||||
set_screensaver(value="")
|
||||
|
||||
if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']):
|
||||
if (
|
||||
self.pending_refresh
|
||||
and not self.download_threads
|
||||
and not self.writer_threads["updated"]
|
||||
and not self.writer_threads["userdata"]
|
||||
and not self.writer_threads["removed"]
|
||||
):
|
||||
self.pending_refresh = False
|
||||
self.save_last_sync()
|
||||
self.total_updates = 0
|
||||
window('jellyfin_sync', clear=True)
|
||||
window("jellyfin_sync", clear=True)
|
||||
|
||||
if self.progress_updates:
|
||||
|
||||
self.progress_updates.close()
|
||||
self.progress_updates = None
|
||||
|
||||
if not settings('dbSyncScreensaver.bool') and self.screensaver is not None:
|
||||
if not settings("dbSyncScreensaver.bool") and self.screensaver is not None:
|
||||
|
||||
xbmc.executebuiltin('InhibitIdleShutdown(false)')
|
||||
xbmc.executebuiltin("InhibitIdleShutdown(false)")
|
||||
set_screensaver(value=self.screensaver)
|
||||
self.screensaver = None
|
||||
|
||||
if xbmc.getCondVisibility('Container.Content(musicvideos)'): # Prevent cursor from moving
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
if xbmc.getCondVisibility(
|
||||
"Container.Content(musicvideos)"
|
||||
): # Prevent cursor from moving
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
else: # Update widgets
|
||||
xbmc.executebuiltin('UpdateLibrary(video)')
|
||||
xbmc.executebuiltin("UpdateLibrary(video)")
|
||||
|
||||
if xbmc.getCondVisibility('Window.IsMedia'):
|
||||
xbmc.executebuiltin('Container.Refresh')
|
||||
if xbmc.getCondVisibility("Window.IsMedia"):
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
|
||||
def stop_client(self):
|
||||
self.stop_thread = True
|
||||
|
||||
def enable_pending_refresh(self):
|
||||
|
||||
''' When there's an active thread. Let the main thread know.
|
||||
'''
|
||||
"""When there's an active thread. Let the main thread know."""
|
||||
self.pending_refresh = True
|
||||
window('jellyfin_sync.bool', True)
|
||||
window("jellyfin_sync.bool", True)
|
||||
|
||||
def worker_queue_size(self):
|
||||
|
||||
''' Get how many items are queued up for worker threads.
|
||||
'''
|
||||
"""Get how many items are queued up for worker threads."""
|
||||
total = 0
|
||||
|
||||
for queues in self.updated_output:
|
||||
@ -228,10 +270,11 @@ class Library(threading.Thread):
|
||||
return total
|
||||
|
||||
def worker_downloads(self):
|
||||
|
||||
''' Get items from jellyfin and place them in the appropriate queues.
|
||||
'''
|
||||
for queue in ((self.updated_queue, self.updated_output), (self.userdata_queue, self.userdata_output)):
|
||||
"""Get items from jellyfin and place them in the appropriate queues."""
|
||||
for queue in (
|
||||
(self.updated_queue, self.updated_output),
|
||||
(self.userdata_queue, self.userdata_output),
|
||||
):
|
||||
if queue[0].qsize() and len(self.download_threads) < DTHREADS:
|
||||
|
||||
new_thread = GetItemWorker(self.server, queue[0], queue[1])
|
||||
@ -240,9 +283,7 @@ class Library(threading.Thread):
|
||||
self.download_threads.append(new_thread)
|
||||
|
||||
def worker_sort(self):
|
||||
|
||||
''' Get items based on the local jellyfin database and place item in appropriate queues.
|
||||
'''
|
||||
"""Get items based on the local jellyfin database and place item in appropriate queues."""
|
||||
if self.removed_queue.qsize() and len(self.jellyfin_threads) < 2:
|
||||
|
||||
new_thread = SortWorker(self.removed_queue, self.removed_output)
|
||||
@ -250,66 +291,96 @@ class Library(threading.Thread):
|
||||
LOG.info("-->[ q:sort/%s ]", id(new_thread))
|
||||
|
||||
def worker_updates(self):
|
||||
|
||||
''' Update items in the Kodi database.
|
||||
'''
|
||||
"""Update items in the Kodi database."""
|
||||
for queues in self.updated_output:
|
||||
queue = self.updated_output[queues]
|
||||
|
||||
if queue.qsize() and not len(self.writer_threads['updated']):
|
||||
if queue.qsize() and not len(self.writer_threads["updated"]):
|
||||
|
||||
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
|
||||
new_thread = UpdateWorker(queue, self.notify_output, self.music_database_lock, "music", self.server, self.direct_path)
|
||||
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
|
||||
new_thread = UpdateWorker(
|
||||
queue,
|
||||
self.notify_output,
|
||||
self.music_database_lock,
|
||||
"music",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
else:
|
||||
new_thread = UpdateWorker(queue, self.notify_output, self.database_lock, "video", self.server, self.direct_path)
|
||||
new_thread = UpdateWorker(
|
||||
queue,
|
||||
self.notify_output,
|
||||
self.database_lock,
|
||||
"video",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
|
||||
new_thread.start()
|
||||
LOG.info("-->[ q:updated/%s/%s ]", queues, id(new_thread))
|
||||
self.writer_threads['updated'].append(new_thread)
|
||||
self.writer_threads["updated"].append(new_thread)
|
||||
self.enable_pending_refresh()
|
||||
|
||||
def worker_userdata(self):
|
||||
|
||||
''' Update userdata in the Kodi database.
|
||||
'''
|
||||
"""Update userdata in the Kodi database."""
|
||||
for queues in self.userdata_output:
|
||||
queue = self.userdata_output[queues]
|
||||
|
||||
if queue.qsize() and not len(self.writer_threads['userdata']):
|
||||
if queue.qsize() and not len(self.writer_threads["userdata"]):
|
||||
|
||||
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
|
||||
new_thread = UserDataWorker(queue, self.music_database_lock, "music", self.server, self.direct_path)
|
||||
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
|
||||
new_thread = UserDataWorker(
|
||||
queue,
|
||||
self.music_database_lock,
|
||||
"music",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
else:
|
||||
new_thread = UserDataWorker(queue, self.database_lock, "video", self.server, self.direct_path)
|
||||
new_thread = UserDataWorker(
|
||||
queue,
|
||||
self.database_lock,
|
||||
"video",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
|
||||
new_thread.start()
|
||||
LOG.info("-->[ q:userdata/%s/%s ]", queues, id(new_thread))
|
||||
self.writer_threads['userdata'].append(new_thread)
|
||||
self.writer_threads["userdata"].append(new_thread)
|
||||
self.enable_pending_refresh()
|
||||
|
||||
def worker_remove(self):
|
||||
|
||||
''' Remove items from the Kodi database.
|
||||
'''
|
||||
"""Remove items from the Kodi database."""
|
||||
for queues in self.removed_output:
|
||||
queue = self.removed_output[queues]
|
||||
|
||||
if queue.qsize() and not len(self.writer_threads['removed']):
|
||||
if queue.qsize() and not len(self.writer_threads["removed"]):
|
||||
|
||||
if queues in ('Audio', 'MusicArtist', 'AlbumArtist', 'MusicAlbum'):
|
||||
new_thread = RemovedWorker(queue, self.music_database_lock, "music", self.server, self.direct_path)
|
||||
if queues in ("Audio", "MusicArtist", "AlbumArtist", "MusicAlbum"):
|
||||
new_thread = RemovedWorker(
|
||||
queue,
|
||||
self.music_database_lock,
|
||||
"music",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
else:
|
||||
new_thread = RemovedWorker(queue, self.database_lock, "video", self.server, self.direct_path)
|
||||
new_thread = RemovedWorker(
|
||||
queue,
|
||||
self.database_lock,
|
||||
"video",
|
||||
self.server,
|
||||
self.direct_path,
|
||||
)
|
||||
|
||||
new_thread.start()
|
||||
LOG.info("-->[ q:removed/%s/%s ]", queues, id(new_thread))
|
||||
self.writer_threads['removed'].append(new_thread)
|
||||
self.writer_threads["removed"].append(new_thread)
|
||||
self.enable_pending_refresh()
|
||||
|
||||
def worker_notify(self):
|
||||
|
||||
''' Notify the user of new additions.
|
||||
'''
|
||||
"""Notify the user of new additions."""
|
||||
if self.notify_output.qsize() and not len(self.notify_threads):
|
||||
|
||||
new_thread = NotifyWorker(self.notify_output, self.player)
|
||||
@ -318,11 +389,10 @@ class Library(threading.Thread):
|
||||
self.notify_threads.append(new_thread)
|
||||
|
||||
def startup(self):
|
||||
|
||||
''' Run at startup.
|
||||
Check databases.
|
||||
Check for the server plugin.
|
||||
'''
|
||||
"""Run at startup.
|
||||
Check databases.
|
||||
Check for the server plugin.
|
||||
"""
|
||||
self.test_databases()
|
||||
self.check_version()
|
||||
|
||||
@ -330,7 +400,7 @@ class Library(threading.Thread):
|
||||
Views().get_nodes()
|
||||
|
||||
try:
|
||||
if get_sync()['Libraries']:
|
||||
if get_sync()["Libraries"]:
|
||||
|
||||
try:
|
||||
with FullSync(self, self.server) as sync:
|
||||
@ -340,7 +410,7 @@ class Library(threading.Thread):
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
elif not settings('SyncInstallRunDone.bool'):
|
||||
elif not settings("SyncInstallRunDone.bool"):
|
||||
|
||||
with FullSync(self, self.server) as sync:
|
||||
sync.libraries()
|
||||
@ -349,9 +419,7 @@ class Library(threading.Thread):
|
||||
|
||||
return True
|
||||
|
||||
if settings('SyncInstallRunDone.bool') and settings(
|
||||
'kodiCompanion.bool'
|
||||
):
|
||||
if settings("SyncInstallRunDone.bool") and settings("kodiCompanion.bool"):
|
||||
# None == Unknown
|
||||
if self.server.jellyfin.check_companion_enabled() is not False:
|
||||
|
||||
@ -372,12 +440,12 @@ class Library(threading.Thread):
|
||||
except LibraryException as error:
|
||||
LOG.error(error.status)
|
||||
|
||||
if error.status in 'SyncLibraryLater':
|
||||
if error.status in "SyncLibraryLater":
|
||||
|
||||
dialog("ok", "{jellyfin}", translate(33129))
|
||||
settings('SyncInstallRunDone.bool', True)
|
||||
settings("SyncInstallRunDone.bool", True)
|
||||
sync = get_sync()
|
||||
sync['Libraries'] = []
|
||||
sync["Libraries"] = []
|
||||
save_sync(sync)
|
||||
|
||||
return True
|
||||
@ -388,33 +456,33 @@ class Library(threading.Thread):
|
||||
return False
|
||||
|
||||
def fast_sync(self):
|
||||
|
||||
''' Movie and userdata not provided by server yet.
|
||||
'''
|
||||
last_sync = settings('LastIncrementalSync')
|
||||
"""Movie and userdata not provided by server yet."""
|
||||
last_sync = settings("LastIncrementalSync")
|
||||
include = []
|
||||
filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"]
|
||||
sync = get_sync()
|
||||
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
|
||||
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
|
||||
LOG.info("--[ retrieve changes ] %s", last_sync)
|
||||
|
||||
# Get the item type of each synced library and build list of types to request
|
||||
for item_id in whitelist:
|
||||
library = self.server.jellyfin.get_item(item_id)
|
||||
library_type = library.get('CollectionType')
|
||||
library_type = library.get("CollectionType")
|
||||
if library_type in filters:
|
||||
include.append(library_type)
|
||||
|
||||
# Include boxsets if movies are synced
|
||||
if 'movies' in include:
|
||||
include.append('boxsets')
|
||||
if "movies" in include:
|
||||
include.append("boxsets")
|
||||
|
||||
# Filter down to the list of library types we want to exclude
|
||||
query_filter = list(set(filters) - set(include))
|
||||
|
||||
try:
|
||||
# Get list of updates from server for synced library types and populate work queues
|
||||
result = self.server.jellyfin.get_sync_queue(last_sync, ",".join([x for x in query_filter]))
|
||||
result = self.server.jellyfin.get_sync_queue(
|
||||
last_sync, ",".join([x for x in query_filter])
|
||||
)
|
||||
|
||||
if result is None:
|
||||
return True
|
||||
@ -423,18 +491,23 @@ class Library(threading.Thread):
|
||||
userdata = []
|
||||
removed = []
|
||||
|
||||
updated.extend(result['ItemsAdded'])
|
||||
updated.extend(result['ItemsUpdated'])
|
||||
userdata.extend(result['UserDataChanged'])
|
||||
removed.extend(result['ItemsRemoved'])
|
||||
updated.extend(result["ItemsAdded"])
|
||||
updated.extend(result["ItemsUpdated"])
|
||||
userdata.extend(result["UserDataChanged"])
|
||||
removed.extend(result["ItemsRemoved"])
|
||||
|
||||
total = len(updated) + len(userdata)
|
||||
|
||||
if total > int(settings('syncIndicator') or 99):
|
||||
if total > int(settings("syncIndicator") or 99):
|
||||
|
||||
''' Inverse yes no, in case the dialog is forced closed by Kodi.
|
||||
'''
|
||||
if dialog("yesno", "{jellyfin}", translate(33172).replace('{number}', str(total)), nolabel=translate(107), yeslabel=translate(106)):
|
||||
"""Inverse yes no, in case the dialog is forced closed by Kodi."""
|
||||
if dialog(
|
||||
"yesno",
|
||||
"{jellyfin}",
|
||||
translate(33172).replace("{number}", str(total)),
|
||||
nolabel=translate(107),
|
||||
yeslabel=translate(106),
|
||||
):
|
||||
LOG.warning("Large updates skipped.")
|
||||
|
||||
return True
|
||||
@ -453,56 +526,68 @@ class Library(threading.Thread):
|
||||
def save_last_sync(self):
|
||||
|
||||
try:
|
||||
time_now = datetime.strptime(self.server.config.data['server-time'].split(', ', 1)[1], '%d %b %Y %H:%M:%S GMT') - timedelta(minutes=2)
|
||||
time_now = datetime.strptime(
|
||||
self.server.config.data["server-time"].split(", ", 1)[1],
|
||||
"%d %b %Y %H:%M:%S GMT",
|
||||
) - timedelta(minutes=2)
|
||||
except Exception as error:
|
||||
|
||||
LOG.exception(error)
|
||||
time_now = datetime.utcnow() - timedelta(minutes=2)
|
||||
|
||||
last_sync = time_now.strftime('%Y-%m-%dT%H:%M:%Sz')
|
||||
settings('LastIncrementalSync', value=last_sync)
|
||||
last_sync = time_now.strftime("%Y-%m-%dT%H:%M:%Sz")
|
||||
settings("LastIncrementalSync", value=last_sync)
|
||||
LOG.info("--[ sync/%s ]", last_sync)
|
||||
|
||||
def select_libraries(self, mode=None):
|
||||
|
||||
''' Select from libraries synced. Either update or repair libraries.
|
||||
Send event back to service.py
|
||||
'''
|
||||
"""Select from libraries synced. Either update or repair libraries.
|
||||
Send event back to service.py
|
||||
"""
|
||||
modes = {
|
||||
'SyncLibrarySelection': 'SyncLibrary',
|
||||
'RepairLibrarySelection': 'RepairLibrary',
|
||||
'AddLibrarySelection': 'SyncLibrary',
|
||||
'RemoveLibrarySelection': 'RemoveLibrary'
|
||||
"SyncLibrarySelection": "SyncLibrary",
|
||||
"RepairLibrarySelection": "RepairLibrary",
|
||||
"AddLibrarySelection": "SyncLibrary",
|
||||
"RemoveLibrarySelection": "RemoveLibrary",
|
||||
}
|
||||
sync = get_sync()
|
||||
whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']]
|
||||
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
|
||||
libraries = []
|
||||
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
|
||||
if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'):
|
||||
for library in sync['Whitelist']:
|
||||
if mode in (
|
||||
"SyncLibrarySelection",
|
||||
"RepairLibrarySelection",
|
||||
"RemoveLibrarySelection",
|
||||
):
|
||||
for library in sync["Whitelist"]:
|
||||
|
||||
name = db.get_view_name(library.replace('Mixed:', ""))
|
||||
libraries.append({'Id': library, 'Name': name})
|
||||
name = db.get_view_name(library.replace("Mixed:", ""))
|
||||
libraries.append({"Id": library, "Name": name})
|
||||
else:
|
||||
available = [x for x in sync['SortedViews'] if x not in whitelist]
|
||||
available = [x for x in sync["SortedViews"] if x not in whitelist]
|
||||
|
||||
for library in available:
|
||||
view = db.get_view(library)
|
||||
|
||||
if view.media_type in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'):
|
||||
libraries.append({'Id': view.view_id, 'Name': view.view_name})
|
||||
if view.media_type in (
|
||||
"movies",
|
||||
"tvshows",
|
||||
"musicvideos",
|
||||
"mixed",
|
||||
"music",
|
||||
):
|
||||
libraries.append({"Id": view.view_id, "Name": view.view_name})
|
||||
|
||||
choices = [x['Name'] for x in libraries]
|
||||
choices = [x["Name"] for x in libraries]
|
||||
choices.insert(0, translate(33121))
|
||||
|
||||
titles = {
|
||||
"RepairLibrarySelection": 33199,
|
||||
"SyncLibrarySelection": 33198,
|
||||
"RemoveLibrarySelection": 33200,
|
||||
"AddLibrarySelection": 33120
|
||||
"AddLibrarySelection": 33120,
|
||||
}
|
||||
title = titles.get(mode, "Failed to get title {}".format(mode))
|
||||
|
||||
@ -519,9 +604,15 @@ class Library(threading.Thread):
|
||||
for x in selection:
|
||||
|
||||
library = libraries[x - 1]
|
||||
selected_libraries.append(library['Id'])
|
||||
selected_libraries.append(library["Id"])
|
||||
|
||||
event(modes[mode], {'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection'})
|
||||
event(
|
||||
modes[mode],
|
||||
{
|
||||
"Id": ",".join([libraries[x - 1]["Id"] for x in selection]),
|
||||
"Update": mode == "SyncLibrarySelection",
|
||||
},
|
||||
)
|
||||
|
||||
def add_library(self, library_id, update=False):
|
||||
|
||||
@ -555,13 +646,11 @@ class Library(threading.Thread):
|
||||
return True
|
||||
|
||||
def userdata(self, data):
|
||||
|
||||
''' Add item_id to userdata queue.
|
||||
'''
|
||||
"""Add item_id to userdata queue."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
items = [x['ItemId'] for x in data]
|
||||
items = [x["ItemId"] for x in data]
|
||||
|
||||
for item in split_list(items, LIMIT):
|
||||
self.userdata_queue.put(item)
|
||||
@ -570,9 +659,7 @@ class Library(threading.Thread):
|
||||
LOG.info("---[ userdata:%s ]", len(items))
|
||||
|
||||
def updated(self, data):
|
||||
|
||||
''' Add item_id to updated queue.
|
||||
'''
|
||||
"""Add item_id to updated queue."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
@ -583,9 +670,7 @@ class Library(threading.Thread):
|
||||
LOG.info("---[ updated:%s ]", len(data))
|
||||
|
||||
def removed(self, data):
|
||||
|
||||
''' Add item_id to removed queue.
|
||||
'''
|
||||
"""Add item_id to removed queue."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
@ -604,10 +689,12 @@ class UpdateWorker(threading.Thread):
|
||||
|
||||
is_done = False
|
||||
|
||||
def __init__(self, queue, notify, lock, database, server=None, direct_path=None, *args):
|
||||
def __init__(
|
||||
self, queue, notify, lock, database, server=None, direct_path=None, *args
|
||||
):
|
||||
self.queue = queue
|
||||
self.notify_output = notify
|
||||
self.notify = settings('newContent.bool')
|
||||
self.notify = settings("newContent.bool")
|
||||
self.lock = lock
|
||||
self.database = Database(database)
|
||||
self.args = args
|
||||
@ -616,7 +703,7 @@ class UpdateWorker(threading.Thread):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
|
||||
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
|
||||
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
|
||||
if kodidb.db_file == "video":
|
||||
movies = Movies(*default_args)
|
||||
@ -626,7 +713,9 @@ class UpdateWorker(threading.Thread):
|
||||
music = Music(*default_args)
|
||||
else:
|
||||
# this should not happen
|
||||
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
|
||||
LOG.error(
|
||||
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
|
||||
)
|
||||
return
|
||||
|
||||
while True:
|
||||
@ -637,39 +726,41 @@ class UpdateWorker(threading.Thread):
|
||||
break
|
||||
|
||||
try:
|
||||
LOG.debug('{} - {}'.format(item['Type'], item['Name']))
|
||||
if item['Type'] == 'Movie':
|
||||
LOG.debug("{} - {}".format(item["Type"], item["Name"]))
|
||||
if item["Type"] == "Movie":
|
||||
movies.movie(item)
|
||||
elif item['Type'] == 'BoxSet':
|
||||
elif item["Type"] == "BoxSet":
|
||||
movies.boxset(item)
|
||||
elif item['Type'] == 'Series':
|
||||
elif item["Type"] == "Series":
|
||||
tvshows.tvshow(item)
|
||||
elif item['Type'] == 'Season':
|
||||
elif item["Type"] == "Season":
|
||||
tvshows.season(item)
|
||||
elif item['Type'] == 'Episode':
|
||||
elif item["Type"] == "Episode":
|
||||
tvshows.episode(item)
|
||||
elif item['Type'] == 'MusicVideo':
|
||||
elif item["Type"] == "MusicVideo":
|
||||
musicvideos.musicvideo(item)
|
||||
elif item['Type'] == 'MusicAlbum':
|
||||
elif item["Type"] == "MusicAlbum":
|
||||
music.album(item)
|
||||
elif item['Type'] == 'MusicArtist':
|
||||
elif item["Type"] == "MusicArtist":
|
||||
music.artist(item)
|
||||
elif item['Type'] == 'AlbumArtist':
|
||||
elif item["Type"] == "AlbumArtist":
|
||||
music.albumartist(item)
|
||||
elif item['Type'] == 'Audio':
|
||||
elif item["Type"] == "Audio":
|
||||
music.song(item)
|
||||
|
||||
if self.notify:
|
||||
self.notify_output.put((item['Type'], api.API(item).get_naming()))
|
||||
self.notify_output.put(
|
||||
(item["Type"], api.API(item).get_naming())
|
||||
)
|
||||
except LibraryException as error:
|
||||
if error.status == 'StopCalled':
|
||||
if error.status == "StopCalled":
|
||||
break
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
||||
LOG.info("--<[ q:updated/%s ]", id(self))
|
||||
@ -692,7 +783,7 @@ class UserDataWorker(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
|
||||
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
|
||||
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
|
||||
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
|
||||
if kodidb.db_file == "video":
|
||||
movies = Movies(*default_args)
|
||||
@ -701,7 +792,9 @@ class UserDataWorker(threading.Thread):
|
||||
music = Music(*default_args)
|
||||
else:
|
||||
# this should not happen
|
||||
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
|
||||
LOG.error(
|
||||
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
|
||||
)
|
||||
return
|
||||
|
||||
while True:
|
||||
@ -712,27 +805,27 @@ class UserDataWorker(threading.Thread):
|
||||
break
|
||||
|
||||
try:
|
||||
if item['Type'] == 'Movie':
|
||||
if item["Type"] == "Movie":
|
||||
movies.userdata(item)
|
||||
elif item['Type'] in ['Series', 'Season', 'Episode']:
|
||||
elif item["Type"] in ["Series", "Season", "Episode"]:
|
||||
tvshows.userdata(item)
|
||||
elif item['Type'] == 'MusicAlbum':
|
||||
elif item["Type"] == "MusicAlbum":
|
||||
music.album(item)
|
||||
elif item['Type'] == 'MusicArtist':
|
||||
elif item["Type"] == "MusicArtist":
|
||||
music.artist(item)
|
||||
elif item['Type'] == 'AlbumArtist':
|
||||
elif item["Type"] == "AlbumArtist":
|
||||
music.albumartist(item)
|
||||
elif item['Type'] == 'Audio':
|
||||
elif item["Type"] == "Audio":
|
||||
music.userdata(item)
|
||||
except LibraryException as error:
|
||||
if error.status == 'StopCalled':
|
||||
if error.status == "StopCalled":
|
||||
break
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
||||
LOG.info("--<[ q:userdata/%s ]", id(self))
|
||||
@ -752,7 +845,7 @@ class SortWorker(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
|
||||
with Database('jellyfin') as jellyfindb:
|
||||
with Database("jellyfin") as jellyfindb:
|
||||
database = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
|
||||
|
||||
while True:
|
||||
@ -765,21 +858,26 @@ class SortWorker(threading.Thread):
|
||||
try:
|
||||
media = database.get_media_by_id(item_id)
|
||||
if media:
|
||||
self.output[media].put({'Id': item_id, 'Type': media})
|
||||
self.output[media].put({"Id": item_id, "Type": media})
|
||||
else:
|
||||
items = database.get_media_by_parent_id(item_id)
|
||||
|
||||
if not items:
|
||||
LOG.info("Could not find media %s in the jellyfin database.", item_id)
|
||||
LOG.info(
|
||||
"Could not find media %s in the jellyfin database.",
|
||||
item_id,
|
||||
)
|
||||
else:
|
||||
for item in items:
|
||||
self.output[item[1]].put({'Id': item[0], 'Type': item[1]})
|
||||
self.output[item[1]].put(
|
||||
{"Id": item[0], "Type": item[1]}
|
||||
)
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
||||
LOG.info("--<[ q:sort/%s ]", id(self))
|
||||
@ -801,7 +899,7 @@ class RemovedWorker(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
|
||||
with self.lock, self.database as kodidb, Database('jellyfin') as jellyfindb:
|
||||
with self.lock, self.database as kodidb, Database("jellyfin") as jellyfindb:
|
||||
default_args = (self.server, jellyfindb, kodidb, self.direct_path)
|
||||
if kodidb.db_file == "video":
|
||||
movies = Movies(*default_args)
|
||||
@ -811,7 +909,9 @@ class RemovedWorker(threading.Thread):
|
||||
music = Music(*default_args)
|
||||
else:
|
||||
# this should not happen
|
||||
LOG.error('"{}" is not a valid Kodi library type.'.format(kodidb.db_file))
|
||||
LOG.error(
|
||||
'"{}" is not a valid Kodi library type.'.format(kodidb.db_file)
|
||||
)
|
||||
return
|
||||
|
||||
while True:
|
||||
@ -821,26 +921,31 @@ class RemovedWorker(threading.Thread):
|
||||
except Queue.Empty:
|
||||
break
|
||||
|
||||
if item['Type'] == 'Movie':
|
||||
if item["Type"] == "Movie":
|
||||
obj = movies.remove
|
||||
elif item['Type'] in ['Series', 'Season', 'Episode']:
|
||||
elif item["Type"] in ["Series", "Season", "Episode"]:
|
||||
obj = tvshows.remove
|
||||
elif item['Type'] in ['MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio']:
|
||||
elif item["Type"] in [
|
||||
"MusicAlbum",
|
||||
"MusicArtist",
|
||||
"AlbumArtist",
|
||||
"Audio",
|
||||
]:
|
||||
obj = music.remove
|
||||
elif item['Type'] == 'MusicVideo':
|
||||
elif item["Type"] == "MusicVideo":
|
||||
obj = musicvideos.remove
|
||||
|
||||
try:
|
||||
obj(item['Id'])
|
||||
obj(item["Id"])
|
||||
except LibraryException as error:
|
||||
if error.status == 'StopCalled':
|
||||
if error.status == "StopCalled":
|
||||
break
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
finally:
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
||||
LOG.info("--<[ q:removed/%s ]", id(self))
|
||||
@ -854,8 +959,8 @@ class NotifyWorker(threading.Thread):
|
||||
def __init__(self, queue, player):
|
||||
|
||||
self.queue = queue
|
||||
self.video_time = int(settings('newvideotime')) * 1000
|
||||
self.music_time = int(settings('newmusictime')) * 1000
|
||||
self.video_time = int(settings("newvideotime")) * 1000
|
||||
self.music_time = int(settings("newmusictime")) * 1000
|
||||
self.player = player
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@ -868,15 +973,24 @@ class NotifyWorker(threading.Thread):
|
||||
except Queue.Empty:
|
||||
break
|
||||
|
||||
time = self.music_time if item[0] == 'Audio' else self.video_time
|
||||
time = self.music_time if item[0] == "Audio" else self.video_time
|
||||
|
||||
if time and (not self.player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')):
|
||||
dialog("notification", heading="%s %s" % (translate(33049), item[0]), message=item[1],
|
||||
icon="{jellyfin}", time=time, sound=False)
|
||||
if time and (
|
||||
not self.player.isPlayingVideo()
|
||||
or xbmc.getCondVisibility("VideoPlayer.Content(livetv)")
|
||||
):
|
||||
dialog(
|
||||
"notification",
|
||||
heading="%s %s" % (translate(33049), item[0]),
|
||||
message=item[1],
|
||||
icon="{jellyfin}",
|
||||
time=time,
|
||||
sound=False,
|
||||
)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
if window('jellyfin_should_stop.bool'):
|
||||
if window("jellyfin_should_stop.bool"):
|
||||
break
|
||||
|
||||
LOG.info("--<[ q:notify/%s ]", id(self))
|
||||
|
@ -46,23 +46,35 @@ class Monitor(xbmc.Monitor):
|
||||
|
||||
def onNotification(self, sender, method, data):
|
||||
|
||||
if sender.lower() not in ('plugin.video.jellyfin', 'xbmc', 'upnextprovider.signal'):
|
||||
if sender.lower() not in (
|
||||
"plugin.video.jellyfin",
|
||||
"xbmc",
|
||||
"upnextprovider.signal",
|
||||
):
|
||||
return
|
||||
|
||||
if sender == 'plugin.video.jellyfin':
|
||||
method = method.split('.')[1]
|
||||
if sender == "plugin.video.jellyfin":
|
||||
method = method.split(".")[1]
|
||||
|
||||
if method not in ('ReportProgressRequested', 'LoadServer', 'AddUser', 'PlayPlaylist', 'Play', 'Playstate', 'GeneralCommand'):
|
||||
if method not in (
|
||||
"ReportProgressRequested",
|
||||
"LoadServer",
|
||||
"AddUser",
|
||||
"PlayPlaylist",
|
||||
"Play",
|
||||
"Playstate",
|
||||
"GeneralCommand",
|
||||
):
|
||||
return
|
||||
|
||||
data = json.loads(data)[0]
|
||||
|
||||
elif sender.startswith('upnextprovider'):
|
||||
LOG.info('Attempting to play the next episode via upnext')
|
||||
method = method.split('.', 1)[1]
|
||||
elif sender.startswith("upnextprovider"):
|
||||
LOG.info("Attempting to play the next episode via upnext")
|
||||
method = method.split(".", 1)[1]
|
||||
|
||||
if method not in ('plugin.video.jellyfin_play_action',):
|
||||
LOG.info('Received invalid upnext method: %s', method)
|
||||
if method not in ("plugin.video.jellyfin_play_action",):
|
||||
LOG.info("Received invalid upnext method: %s", method)
|
||||
return
|
||||
|
||||
data = json.loads(data)
|
||||
@ -71,15 +83,23 @@ class Monitor(xbmc.Monitor):
|
||||
if data:
|
||||
data = json.loads(binascii.unhexlify(data[0]))
|
||||
else:
|
||||
if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'):
|
||||
if method not in (
|
||||
"Player.OnPlay",
|
||||
"VideoLibrary.OnUpdate",
|
||||
"Player.OnAVChange",
|
||||
):
|
||||
|
||||
''' We have to clear the playlist if it was stopped before it has been played completely.
|
||||
Otherwise the next played item will be added the previous queue.
|
||||
'''
|
||||
"""We have to clear the playlist if it was stopped before it has been played completely.
|
||||
Otherwise the next played item will be added the previous queue.
|
||||
"""
|
||||
if method == "Player.OnStop":
|
||||
xbmc.sleep(3000) # let's wait for the player, so we don't clear the canceled playlist by mistake.
|
||||
xbmc.sleep(
|
||||
3000
|
||||
) # let's wait for the player, so we don't clear the canceled playlist by mistake.
|
||||
|
||||
if xbmc.getCondVisibility("!Player.HasMedia + !Window.IsVisible(busydialog)"):
|
||||
if xbmc.getCondVisibility(
|
||||
"!Player.HasMedia + !Window.IsVisible(busydialog)"
|
||||
):
|
||||
|
||||
xbmc.executebuiltin("Playlist.Clear")
|
||||
LOG.info("[ playlist ] cleared")
|
||||
@ -96,14 +116,14 @@ class Monitor(xbmc.Monitor):
|
||||
return
|
||||
|
||||
try:
|
||||
if not data.get('ServerId'):
|
||||
if not data.get("ServerId"):
|
||||
server = Jellyfin()
|
||||
else:
|
||||
if method != 'LoadServer' and data['ServerId'] not in self.servers:
|
||||
if method != "LoadServer" and data["ServerId"] not in self.servers:
|
||||
|
||||
try:
|
||||
connect.Connect().register(data['ServerId'])
|
||||
self.server_instance(data['ServerId'])
|
||||
connect.Connect().register(data["ServerId"])
|
||||
self.server_instance(data["ServerId"])
|
||||
except Exception as error:
|
||||
|
||||
LOG.exception(error)
|
||||
@ -111,80 +131,90 @@ class Monitor(xbmc.Monitor):
|
||||
|
||||
return
|
||||
|
||||
server = Jellyfin(data['ServerId'])
|
||||
server = Jellyfin(data["ServerId"])
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
server = Jellyfin()
|
||||
|
||||
server = server.get_client()
|
||||
|
||||
if method == 'Play':
|
||||
if method == "Play":
|
||||
|
||||
items = server.jellyfin.get_items(data['ItemIds'])
|
||||
items = server.jellyfin.get_items(data["ItemIds"])
|
||||
|
||||
PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow',
|
||||
data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'),
|
||||
data.get('SubtitleStreamIndex')).start()
|
||||
PlaylistWorker(
|
||||
data.get("ServerId"),
|
||||
items,
|
||||
data["PlayCommand"] == "PlayNow",
|
||||
data.get("StartPositionTicks", 0),
|
||||
data.get("AudioStreamIndex"),
|
||||
data.get("SubtitleStreamIndex"),
|
||||
).start()
|
||||
|
||||
# TODO no clue if this is called by anything
|
||||
elif method == 'PlayPlaylist':
|
||||
elif method == "PlayPlaylist":
|
||||
|
||||
server.jellyfin.post_session(server.config.data['app.session'], "Playing", {
|
||||
'PlayCommand': "PlayNow",
|
||||
'ItemIds': data['Id'],
|
||||
'StartPositionTicks': 0
|
||||
})
|
||||
server.jellyfin.post_session(
|
||||
server.config.data["app.session"],
|
||||
"Playing",
|
||||
{
|
||||
"PlayCommand": "PlayNow",
|
||||
"ItemIds": data["Id"],
|
||||
"StartPositionTicks": 0,
|
||||
},
|
||||
)
|
||||
|
||||
elif method in ('ReportProgressRequested', 'Player.OnAVChange'):
|
||||
self.player.report_playback(data.get('Report', True))
|
||||
elif method in ("ReportProgressRequested", "Player.OnAVChange"):
|
||||
self.player.report_playback(data.get("Report", True))
|
||||
|
||||
elif method == 'Playstate':
|
||||
elif method == "Playstate":
|
||||
self.playstate(data)
|
||||
|
||||
elif method == 'GeneralCommand':
|
||||
elif method == "GeneralCommand":
|
||||
self.general_commands(data)
|
||||
|
||||
elif method == 'LoadServer':
|
||||
self.server_instance(data['ServerId'])
|
||||
elif method == "LoadServer":
|
||||
self.server_instance(data["ServerId"])
|
||||
|
||||
elif method == 'AddUser':
|
||||
server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add'])
|
||||
elif method == "AddUser":
|
||||
server.jellyfin.session_add_user(
|
||||
server.config.data["app.session"], data["Id"], data["Add"]
|
||||
)
|
||||
self.additional_users(server)
|
||||
|
||||
elif method == 'Player.OnPlay':
|
||||
elif method == "Player.OnPlay":
|
||||
on_play(data, server)
|
||||
|
||||
elif method == 'VideoLibrary.OnUpdate':
|
||||
elif method == "VideoLibrary.OnUpdate":
|
||||
on_update(data, server)
|
||||
|
||||
def server_instance(self, server_id=None):
|
||||
|
||||
server = Jellyfin(server_id).get_client()
|
||||
session = server.jellyfin.get_device(self.device_id)
|
||||
server.config.data['app.session'] = session[0]['Id']
|
||||
server.config.data["app.session"] = session[0]["Id"]
|
||||
|
||||
if server_id is not None:
|
||||
self.servers.append(server_id)
|
||||
elif settings('additionalUsers'):
|
||||
elif settings("additionalUsers"):
|
||||
|
||||
users = settings('additionalUsers').split(',')
|
||||
users = settings("additionalUsers").split(",")
|
||||
all_users = server.jellyfin.get_users()
|
||||
|
||||
for additional in users:
|
||||
for user in all_users:
|
||||
|
||||
if user['Name'].lower() in additional.lower():
|
||||
server.jellyfin.session_add_user(server.config.data['app.session'], user['Id'], True)
|
||||
if user["Name"].lower() in additional.lower():
|
||||
server.jellyfin.session_add_user(
|
||||
server.config.data["app.session"], user["Id"], True
|
||||
)
|
||||
|
||||
self.additional_users(server)
|
||||
|
||||
|
||||
def additional_users(self, server):
|
||||
|
||||
''' Setup additional users images.
|
||||
'''
|
||||
"""Setup additional users images."""
|
||||
for i in range(10):
|
||||
window('JellyfinAdditionalUserImage.%s' % i, clear=True)
|
||||
window("JellyfinAdditionalUserImage.%s" % i, clear=True)
|
||||
|
||||
try:
|
||||
session = server.jellyfin.get_device(self.device_id)
|
||||
@ -193,31 +223,31 @@ class Monitor(xbmc.Monitor):
|
||||
|
||||
return
|
||||
|
||||
for index, user in enumerate(session[0]['AdditionalUsers']):
|
||||
for index, user in enumerate(session[0]["AdditionalUsers"]):
|
||||
|
||||
info = server.jellyfin.get_user(user['UserId'])
|
||||
image = api.API(info, server.config.data['auth.server']).get_user_artwork(user['UserId'])
|
||||
window('JellyfinAdditionalUserImage.%s' % index, image)
|
||||
window('JellyfinAdditionalUserPosition.%s' % user['UserId'], str(index))
|
||||
info = server.jellyfin.get_user(user["UserId"])
|
||||
image = api.API(info, server.config.data["auth.server"]).get_user_artwork(
|
||||
user["UserId"]
|
||||
)
|
||||
window("JellyfinAdditionalUserImage.%s" % index, image)
|
||||
window("JellyfinAdditionalUserPosition.%s" % user["UserId"], str(index))
|
||||
|
||||
def playstate(self, data):
|
||||
|
||||
''' Jellyfin playstate updates.
|
||||
'''
|
||||
command = data['Command']
|
||||
"""Jellyfin playstate updates."""
|
||||
command = data["Command"]
|
||||
actions = {
|
||||
'Stop': self.player.stop,
|
||||
'Unpause': self.player.pause,
|
||||
'Pause': self.player.pause,
|
||||
'PlayPause': self.player.pause,
|
||||
'NextTrack': self.player.playnext,
|
||||
'PreviousTrack': self.player.playprevious
|
||||
"Stop": self.player.stop,
|
||||
"Unpause": self.player.pause,
|
||||
"Pause": self.player.pause,
|
||||
"PlayPause": self.player.pause,
|
||||
"NextTrack": self.player.playnext,
|
||||
"PreviousTrack": self.player.playprevious,
|
||||
}
|
||||
if command == 'Seek':
|
||||
if command == "Seek":
|
||||
|
||||
if self.player.isPlaying():
|
||||
|
||||
seektime = data['SeekPositionTicks'] / 10000000.0
|
||||
seektime = data["SeekPositionTicks"] / 10000000.0
|
||||
self.player.seekTime(seektime)
|
||||
LOG.info("[ seek/%s ]", seektime)
|
||||
|
||||
@ -227,69 +257,78 @@ class Monitor(xbmc.Monitor):
|
||||
LOG.info("[ command/%s ]", command)
|
||||
|
||||
def general_commands(self, data):
|
||||
"""General commands from Jellyfin to control the Kodi interface."""
|
||||
command = data["Name"]
|
||||
args = data["Arguments"]
|
||||
|
||||
''' General commands from Jellyfin to control the Kodi interface.
|
||||
'''
|
||||
command = data['Name']
|
||||
args = data['Arguments']
|
||||
if command in (
|
||||
"Mute",
|
||||
"Unmute",
|
||||
"SetVolume",
|
||||
"SetSubtitleStreamIndex",
|
||||
"SetAudioStreamIndex",
|
||||
"SetRepeatMode",
|
||||
):
|
||||
|
||||
if command in ('Mute', 'Unmute', 'SetVolume',
|
||||
'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'):
|
||||
if command in ["Mute", "Unmute"]:
|
||||
xbmc.executebuiltin("Mute")
|
||||
elif command == "SetAudioStreamIndex":
|
||||
self.player.set_audio_subs(args["Index"])
|
||||
elif command == "SetRepeatMode":
|
||||
xbmc.executebuiltin("xbmc.PlayerControl(%s)" % args["RepeatMode"])
|
||||
elif command == "SetSubtitleStreamIndex":
|
||||
self.player.set_audio_subs(None, args["Index"])
|
||||
|
||||
if command in ['Mute', 'Unmute']:
|
||||
xbmc.executebuiltin('Mute')
|
||||
elif command == 'SetAudioStreamIndex':
|
||||
self.player.set_audio_subs(args['Index'])
|
||||
elif command == 'SetRepeatMode':
|
||||
xbmc.executebuiltin('xbmc.PlayerControl(%s)' % args['RepeatMode'])
|
||||
elif command == 'SetSubtitleStreamIndex':
|
||||
self.player.set_audio_subs(None, args['Index'])
|
||||
|
||||
elif command == 'SetVolume':
|
||||
xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume'])
|
||||
elif command == "SetVolume":
|
||||
xbmc.executebuiltin("SetVolume(%s[,showvolumebar])" % args["Volume"])
|
||||
# Kodi needs a bit of time to update its current status
|
||||
xbmc.sleep(500)
|
||||
self.player.report_playback()
|
||||
|
||||
elif command == 'DisplayMessage':
|
||||
dialog("notification", heading=args['Header'], message=args['Text'],
|
||||
icon="{jellyfin}", time=int(settings('displayMessage')) * 1000)
|
||||
elif command == "DisplayMessage":
|
||||
dialog(
|
||||
"notification",
|
||||
heading=args["Header"],
|
||||
message=args["Text"],
|
||||
icon="{jellyfin}",
|
||||
time=int(settings("displayMessage")) * 1000,
|
||||
)
|
||||
|
||||
elif command == 'SendString':
|
||||
JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False})
|
||||
elif command == "SendString":
|
||||
JSONRPC("Input.SendText").execute({"text": args["String"], "done": False})
|
||||
|
||||
elif command == 'GoHome':
|
||||
JSONRPC('GUI.ActivateWindow').execute({'window': "home"})
|
||||
elif command == "GoHome":
|
||||
JSONRPC("GUI.ActivateWindow").execute({"window": "home"})
|
||||
|
||||
elif command == 'Guide':
|
||||
JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"})
|
||||
elif command == "Guide":
|
||||
JSONRPC("GUI.ActivateWindow").execute({"window": "tvguide"})
|
||||
|
||||
elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'):
|
||||
elif command in ("MoveUp", "MoveDown", "MoveRight", "MoveLeft"):
|
||||
actions = {
|
||||
'MoveUp': "Input.Up",
|
||||
'MoveDown': "Input.Down",
|
||||
'MoveRight': "Input.Right",
|
||||
'MoveLeft': "Input.Left"
|
||||
"MoveUp": "Input.Up",
|
||||
"MoveDown": "Input.Down",
|
||||
"MoveRight": "Input.Right",
|
||||
"MoveLeft": "Input.Left",
|
||||
}
|
||||
JSONRPC(actions[command]).execute()
|
||||
|
||||
else:
|
||||
builtin = {
|
||||
'ToggleFullscreen': 'Action(FullScreen)',
|
||||
'ToggleOsdMenu': 'Action(OSD)',
|
||||
'ToggleContextMenu': 'Action(ContextMenu)',
|
||||
'Select': 'Action(Select)',
|
||||
'Back': 'Action(back)',
|
||||
'PageUp': 'Action(PageUp)',
|
||||
'NextLetter': 'Action(NextLetter)',
|
||||
'GoToSearch': 'VideoLibrary.Search',
|
||||
'GoToSettings': 'ActivateWindow(Settings)',
|
||||
'PageDown': 'Action(PageDown)',
|
||||
'PreviousLetter': 'Action(PrevLetter)',
|
||||
'TakeScreenshot': 'TakeScreenshot',
|
||||
'ToggleMute': 'Mute',
|
||||
'VolumeUp': 'Action(VolumeUp)',
|
||||
'VolumeDown': 'Action(VolumeDown)',
|
||||
"ToggleFullscreen": "Action(FullScreen)",
|
||||
"ToggleOsdMenu": "Action(OSD)",
|
||||
"ToggleContextMenu": "Action(ContextMenu)",
|
||||
"Select": "Action(Select)",
|
||||
"Back": "Action(back)",
|
||||
"PageUp": "Action(PageUp)",
|
||||
"NextLetter": "Action(NextLetter)",
|
||||
"GoToSearch": "VideoLibrary.Search",
|
||||
"GoToSettings": "ActivateWindow(Settings)",
|
||||
"PageDown": "Action(PageDown)",
|
||||
"PreviousLetter": "Action(PrevLetter)",
|
||||
"TakeScreenshot": "TakeScreenshot",
|
||||
"ToggleMute": "Mute",
|
||||
"VolumeUp": "Action(VolumeUp)",
|
||||
"VolumeDown": "Action(VolumeDown)",
|
||||
}
|
||||
if command in builtin:
|
||||
xbmc.executebuiltin(builtin[command])
|
||||
@ -305,10 +344,9 @@ class Listener(threading.Thread):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
|
||||
''' Detect the resume dialog for widgets.
|
||||
Detect external players.
|
||||
'''
|
||||
"""Detect the resume dialog for widgets.
|
||||
Detect external players.
|
||||
"""
|
||||
LOG.info("--->[ listener ]")
|
||||
|
||||
while not self.stop_thread:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,15 +21,21 @@ class Artwork(object):
|
||||
self.cursor = cursor
|
||||
|
||||
def update(self, image_url, kodi_id, media, image):
|
||||
|
||||
''' Update artwork in the video database.
|
||||
Delete current entry before updating with the new one.
|
||||
'''
|
||||
if not image_url or image == 'poster' and media in ('song', 'artist', 'album'):
|
||||
"""Update artwork in the video database.
|
||||
Delete current entry before updating with the new one.
|
||||
"""
|
||||
if not image_url or image == "poster" and media in ("song", "artist", "album"):
|
||||
return
|
||||
|
||||
try:
|
||||
self.cursor.execute(QU.get_art, (kodi_id, media, image,))
|
||||
self.cursor.execute(
|
||||
QU.get_art,
|
||||
(
|
||||
kodi_id,
|
||||
media,
|
||||
image,
|
||||
),
|
||||
)
|
||||
url = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
|
||||
@ -41,43 +47,39 @@ class Artwork(object):
|
||||
self.cursor.execute(QU.update_art, (image_url, kodi_id, media, image))
|
||||
|
||||
def add(self, artwork, *args):
|
||||
|
||||
''' Add all artworks.
|
||||
'''
|
||||
"""Add all artworks."""
|
||||
KODI = {
|
||||
'Primary': ['thumb', 'poster'],
|
||||
'Banner': "banner",
|
||||
'Logo': "clearlogo",
|
||||
'Art': "clearart",
|
||||
'Thumb': "landscape",
|
||||
'Disc': "discart",
|
||||
'Backdrop': "fanart"
|
||||
"Primary": ["thumb", "poster"],
|
||||
"Banner": "banner",
|
||||
"Logo": "clearlogo",
|
||||
"Art": "clearart",
|
||||
"Thumb": "landscape",
|
||||
"Disc": "discart",
|
||||
"Backdrop": "fanart",
|
||||
}
|
||||
|
||||
for art in KODI:
|
||||
|
||||
if art == 'Backdrop':
|
||||
if art == "Backdrop":
|
||||
self.cursor.execute(QU.get_backdrops, args + ("fanart%",))
|
||||
|
||||
if len(self.cursor.fetchall()) > len(artwork['Backdrop']):
|
||||
if len(self.cursor.fetchall()) > len(artwork["Backdrop"]):
|
||||
self.cursor.execute(QU.delete_backdrops, args + ("fanart_",))
|
||||
|
||||
for index, backdrop in enumerate(artwork['Backdrop']):
|
||||
for index, backdrop in enumerate(artwork["Backdrop"]):
|
||||
|
||||
if index:
|
||||
self.update(*(backdrop,) + args + ("%s%s" % ("fanart", index),))
|
||||
else:
|
||||
self.update(*(backdrop,) + args + ("fanart",))
|
||||
|
||||
elif art == 'Primary':
|
||||
for kodi_image in KODI['Primary']:
|
||||
self.update(*(artwork['Primary'],) + args + (kodi_image,))
|
||||
elif art == "Primary":
|
||||
for kodi_image in KODI["Primary"]:
|
||||
self.update(*(artwork["Primary"],) + args + (kodi_image,))
|
||||
|
||||
elif artwork.get(art):
|
||||
self.update(*(artwork[art],) + args + (KODI[art],))
|
||||
|
||||
def delete(self, *args):
|
||||
|
||||
''' Delete artwork from kodi database
|
||||
'''
|
||||
"""Delete artwork from kodi database"""
|
||||
self.cursor.execute(QU.delete_art, args)
|
||||
|
@ -89,7 +89,13 @@ class Kodi(object):
|
||||
def add_file(self, filename, path_id):
|
||||
|
||||
try:
|
||||
self.cursor.execute(QU.get_file, (filename, path_id,))
|
||||
self.cursor.execute(
|
||||
QU.get_file,
|
||||
(
|
||||
filename,
|
||||
path_id,
|
||||
),
|
||||
)
|
||||
file_id = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
|
||||
@ -120,40 +126,49 @@ class Kodi(object):
|
||||
|
||||
def add_thumbnail(person_id, person, person_type):
|
||||
|
||||
if person['imageurl']:
|
||||
if person["imageurl"]:
|
||||
|
||||
art = person_type.lower()
|
||||
if "writing" in art:
|
||||
art = "writer"
|
||||
|
||||
self.artwork.update(person['imageurl'], person_id, art, "thumb")
|
||||
self.artwork.update(person["imageurl"], person_id, art, "thumb")
|
||||
|
||||
cast_order = 1
|
||||
|
||||
bulk_updates = {}
|
||||
|
||||
for person in people:
|
||||
person_id = self.get_person(person['Name'])
|
||||
person_id = self.get_person(person["Name"])
|
||||
|
||||
if person['Type'] == 'Actor':
|
||||
if person["Type"] == "Actor":
|
||||
sql = QU.update_actor
|
||||
role = person.get('Role')
|
||||
bulk_updates.setdefault(sql, []).append((person_id,) + args + (role, cast_order,))
|
||||
role = person.get("Role")
|
||||
bulk_updates.setdefault(sql, []).append(
|
||||
(person_id,)
|
||||
+ args
|
||||
+ (
|
||||
role,
|
||||
cast_order,
|
||||
)
|
||||
)
|
||||
cast_order += 1
|
||||
|
||||
elif person['Type'] == 'Director':
|
||||
sql = QU.update_link.replace("{LinkType}", 'director_link')
|
||||
elif person["Type"] == "Director":
|
||||
sql = QU.update_link.replace("{LinkType}", "director_link")
|
||||
bulk_updates.setdefault(sql, []).append((person_id,) + args)
|
||||
|
||||
elif person['Type'] == 'Writer':
|
||||
sql = QU.update_link.replace("{LinkType}", 'writer_link')
|
||||
elif person["Type"] == "Writer":
|
||||
sql = QU.update_link.replace("{LinkType}", "writer_link")
|
||||
bulk_updates.setdefault(sql, []).append((person_id,) + args)
|
||||
|
||||
elif person['Type'] == 'Artist':
|
||||
sql = QU.insert_link_if_not_exists.replace("{LinkType}", 'actor_link')
|
||||
bulk_updates.setdefault(sql, []).append((person_id,) + args + (person_id,) + args)
|
||||
elif person["Type"] == "Artist":
|
||||
sql = QU.insert_link_if_not_exists.replace("{LinkType}", "actor_link")
|
||||
bulk_updates.setdefault(sql, []).append(
|
||||
(person_id,) + args + (person_id,) + args
|
||||
)
|
||||
|
||||
add_thumbnail(person_id, person, person['Type'])
|
||||
add_thumbnail(person_id, person, person["Type"])
|
||||
|
||||
for sql, parameters in bulk_updates.items():
|
||||
self.cursor.executemany(sql, parameters)
|
||||
@ -163,8 +178,7 @@ class Kodi(object):
|
||||
return self.cursor.lastrowid
|
||||
|
||||
def _get_person(self, name):
|
||||
'''Retrieve person from the database, or add them if they don't exist
|
||||
'''
|
||||
"""Retrieve person from the database, or add them if they don't exist"""
|
||||
resp = self.cursor.execute(QU.get_person, (name,)).fetchone()
|
||||
if resp is not None:
|
||||
return resp[0]
|
||||
@ -172,8 +186,7 @@ class Kodi(object):
|
||||
return self.add_person(name)
|
||||
|
||||
def get_person(self, name):
|
||||
'''Retrieve person from cache, else forward to db query
|
||||
'''
|
||||
"""Retrieve person from cache, else forward to db query"""
|
||||
if name in self._people_cache:
|
||||
return self._people_cache[name]
|
||||
else:
|
||||
@ -182,9 +195,7 @@ class Kodi(object):
|
||||
return person_id
|
||||
|
||||
def add_genres(self, genres, *args):
|
||||
|
||||
''' Delete current genres first for clean slate.
|
||||
'''
|
||||
"""Delete current genres first for clean slate."""
|
||||
self.cursor.execute(QU.delete_genres, args)
|
||||
|
||||
for genre in genres:
|
||||
@ -230,29 +241,32 @@ class Kodi(object):
|
||||
return self.add_studio(*args)
|
||||
|
||||
def add_streams(self, file_id, streams, runtime):
|
||||
|
||||
''' First remove any existing entries
|
||||
Then re-add video, audio and subtitles.
|
||||
'''
|
||||
"""First remove any existing entries
|
||||
Then re-add video, audio and subtitles.
|
||||
"""
|
||||
self.cursor.execute(QU.delete_streams, (file_id,))
|
||||
|
||||
if streams:
|
||||
for track in streams['video']:
|
||||
for track in streams["video"]:
|
||||
|
||||
track['FileId'] = file_id
|
||||
track['Runtime'] = runtime
|
||||
track["FileId"] = file_id
|
||||
track["Runtime"] = runtime
|
||||
if kodi_version() < 20:
|
||||
self.add_stream_video(*values(track, QU.add_stream_video_obj_19))
|
||||
else:
|
||||
self.add_stream_video(*values(track, QU.add_stream_video_obj))
|
||||
|
||||
for track in streams['audio']:
|
||||
for track in streams["audio"]:
|
||||
|
||||
track['FileId'] = file_id
|
||||
track["FileId"] = file_id
|
||||
self.add_stream_audio(*values(track, QU.add_stream_audio_obj))
|
||||
|
||||
for track in streams['subtitle']:
|
||||
self.add_stream_sub(*values({'language': track, 'FileId': file_id}, QU.add_stream_sub_obj))
|
||||
for track in streams["subtitle"]:
|
||||
self.add_stream_sub(
|
||||
*values(
|
||||
{"language": track, "FileId": file_id}, QU.add_stream_sub_obj
|
||||
)
|
||||
)
|
||||
|
||||
def add_stream_video(self, *args):
|
||||
if kodi_version() < 20:
|
||||
@ -267,17 +281,24 @@ class Kodi(object):
|
||||
self.cursor.execute(QU.add_stream_sub, args)
|
||||
|
||||
def add_playstate(self, file_id, playcount, date_played, resume, *args):
|
||||
|
||||
''' Delete the existing resume point.
|
||||
Set the watched count.
|
||||
'''
|
||||
"""Delete the existing resume point.
|
||||
Set the watched count.
|
||||
"""
|
||||
self.cursor.execute(QU.delete_bookmark, (file_id,))
|
||||
self.set_playcount(playcount, date_played, file_id)
|
||||
|
||||
if resume:
|
||||
|
||||
bookmark_id = self.create_entry_bookmark()
|
||||
self.cursor.execute(QU.add_bookmark, (bookmark_id, file_id, resume,) + args)
|
||||
self.cursor.execute(
|
||||
QU.add_bookmark,
|
||||
(
|
||||
bookmark_id,
|
||||
file_id,
|
||||
resume,
|
||||
)
|
||||
+ args,
|
||||
)
|
||||
|
||||
def set_playcount(self, *args):
|
||||
self.cursor.execute(QU.update_playcount, args)
|
||||
|
@ -74,15 +74,11 @@ class Movies(Kodi):
|
||||
return None
|
||||
|
||||
def add_ratings(self, *args):
|
||||
|
||||
''' Add ratings, rating type and votes.
|
||||
'''
|
||||
"""Add ratings, rating type and votes."""
|
||||
self.cursor.execute(QU.add_rating, args)
|
||||
|
||||
def update_ratings(self, *args):
|
||||
|
||||
''' Update rating by rating_id.
|
||||
'''
|
||||
"""Update rating by rating_id."""
|
||||
self.cursor.execute(QU.update_rating, args)
|
||||
|
||||
def get_unique_id(self, *args):
|
||||
@ -95,15 +91,11 @@ class Movies(Kodi):
|
||||
return
|
||||
|
||||
def add_unique_id(self, *args):
|
||||
|
||||
''' Add the provider id, imdb, tvdb.
|
||||
'''
|
||||
"""Add the provider id, imdb, tvdb."""
|
||||
self.cursor.execute(QU.add_unique_id, args)
|
||||
|
||||
def update_unique_id(self, *args):
|
||||
|
||||
''' Update the provider id, imdb, tvdb.
|
||||
'''
|
||||
"""Update the provider id, imdb, tvdb."""
|
||||
self.cursor.execute(QU.update_unique_id, args)
|
||||
|
||||
def add_countries(self, countries, *args):
|
||||
@ -141,9 +133,9 @@ class Movies(Kodi):
|
||||
self.cursor.execute(QU.delete_set, args)
|
||||
|
||||
def migrations(self):
|
||||
'''
|
||||
"""
|
||||
Used to trigger required database migrations for new versions
|
||||
'''
|
||||
"""
|
||||
self.cursor.execute(QU.get_version)
|
||||
version_id = self.cursor.fetchone()[0]
|
||||
changes = False
|
||||
@ -156,10 +148,10 @@ class Movies(Kodi):
|
||||
return changes
|
||||
|
||||
def omega_migration(self):
|
||||
'''
|
||||
"""
|
||||
Adds a video version for all existing movies
|
||||
'''
|
||||
LOG.info('Starting migration for Omega database changes')
|
||||
"""
|
||||
LOG.info("Starting migration for Omega database changes")
|
||||
# Tracks if this migration made any changes
|
||||
changes = False
|
||||
self.cursor.execute(QU.get_missing_versions)
|
||||
@ -169,5 +161,5 @@ class Movies(Kodi):
|
||||
self.add_videoversion(entry[0], entry[1], "movie", "0", 40400)
|
||||
changes = True
|
||||
|
||||
LOG.info('Omega database migration is complete')
|
||||
LOG.info("Omega database migration is complete")
|
||||
return changes
|
||||
|
@ -25,10 +25,9 @@ class Music(Kodi):
|
||||
Kodi.__init__(self)
|
||||
|
||||
def create_entry(self):
|
||||
|
||||
''' Krypton has a dummy first entry
|
||||
idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
||||
'''
|
||||
"""Krypton has a dummy first entry
|
||||
idArtist: 1 strArtist: [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
||||
"""
|
||||
self.cursor.execute(QU.create_artist)
|
||||
|
||||
return self.cursor.fetchone()[0] + 1
|
||||
@ -55,9 +54,7 @@ class Music(Kodi):
|
||||
self.cursor.execute(QU.update_role, args)
|
||||
|
||||
def get(self, artist_id, name, musicbrainz):
|
||||
|
||||
''' Get artist or create the entry.
|
||||
'''
|
||||
"""Get artist or create the entry."""
|
||||
try:
|
||||
self.cursor.execute(QU.get_artist, (musicbrainz,))
|
||||
result = self.cursor.fetchone()
|
||||
@ -72,15 +69,20 @@ class Music(Kodi):
|
||||
return artist_id_res
|
||||
|
||||
def add_artist(self, artist_id, name, *args):
|
||||
|
||||
''' Safety check, when musicbrainz does not exist
|
||||
'''
|
||||
"""Safety check, when musicbrainz does not exist"""
|
||||
try:
|
||||
self.cursor.execute(QU.get_artist_by_name, (name,))
|
||||
artist_id_res = self.cursor.fetchone()[0]
|
||||
except TypeError:
|
||||
artist_id_res = artist_id or self.create_entry()
|
||||
self.cursor.execute(QU.add_artist, (artist_id, name,) + args)
|
||||
self.cursor.execute(
|
||||
QU.add_artist,
|
||||
(
|
||||
artist_id,
|
||||
name,
|
||||
)
|
||||
+ args,
|
||||
)
|
||||
|
||||
return artist_id_res
|
||||
|
||||
@ -141,7 +143,7 @@ class Music(Kodi):
|
||||
self.cursor.execute(QU.get_album_by_name72, (name,))
|
||||
album = self.cursor.fetchone()
|
||||
|
||||
if album[1] and album[1].split(' / ')[0] not in artists.split(' / '):
|
||||
if album[1] and album[1].split(" / ")[0] not in artists.split(" / "):
|
||||
LOG.info("Album found, but artist doesn't match?")
|
||||
LOG.info("Album [ %s/%s ] %s", name, album[1], artists)
|
||||
|
||||
@ -149,7 +151,14 @@ class Music(Kodi):
|
||||
|
||||
album_id = (album or self.cursor.fetchone())[0]
|
||||
except TypeError:
|
||||
album_id = self.add_album(*(album_id, name, musicbrainz,) + args)
|
||||
album_id = self.add_album(
|
||||
*(
|
||||
album_id,
|
||||
name,
|
||||
musicbrainz,
|
||||
)
|
||||
+ args
|
||||
)
|
||||
|
||||
return album_id
|
||||
|
||||
@ -225,11 +234,10 @@ class Music(Kodi):
|
||||
self.cursor.execute(QU.update_song_rating, args)
|
||||
|
||||
def add_genres(self, kodi_id, genres, media):
|
||||
|
||||
''' Add genres, but delete current genres first.
|
||||
Album_genres was removed in kodi 18
|
||||
'''
|
||||
if media == 'album' and self.version_id < 72:
|
||||
"""Add genres, but delete current genres first.
|
||||
Album_genres was removed in kodi 18
|
||||
"""
|
||||
if media == "album" and self.version_id < 72:
|
||||
self.cursor.execute(QU.delete_genres_album, (kodi_id,))
|
||||
|
||||
for genre in genres:
|
||||
@ -237,7 +245,7 @@ class Music(Kodi):
|
||||
genre_id = self.get_genre(genre)
|
||||
self.cursor.execute(QU.update_genre_album, (genre_id, kodi_id))
|
||||
|
||||
if media == 'song':
|
||||
if media == "song":
|
||||
self.cursor.execute(QU.delete_genres_song, (kodi_id,))
|
||||
|
||||
for genre in genres:
|
||||
|
@ -1,9 +1,9 @@
|
||||
from __future__ import division, absolute_import, print_function, unicode_literals
|
||||
|
||||
''' Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items.
|
||||
""" Queries for the Kodi database. obj reflect key/value to retrieve from jellyfin items.
|
||||
Some functions require additional information, therefore obj do not always reflect
|
||||
the Kodi database query values.
|
||||
'''
|
||||
"""
|
||||
create_path = """
|
||||
SELECT coalesce(max(idPath), 0)
|
||||
FROM path
|
||||
@ -254,21 +254,48 @@ add_bookmark = """
|
||||
INSERT INTO bookmark(idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_bookmark_obj = ["{FileId}", "{PlayCount}", "{DatePlayed}", "{Resume}", "{Runtime}", "DVDPlayer", 1]
|
||||
add_bookmark_obj = [
|
||||
"{FileId}",
|
||||
"{PlayCount}",
|
||||
"{DatePlayed}",
|
||||
"{Resume}",
|
||||
"{Runtime}",
|
||||
"DVDPlayer",
|
||||
1,
|
||||
]
|
||||
add_streams_obj = ["{FileId}", "{Streams}", "{Runtime}"]
|
||||
add_stream_video = """
|
||||
INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth,
|
||||
iVideoHeight, iVideoDuration, strStereoMode, strHdrType)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_stream_video_obj = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}", "{hdrtype}"]
|
||||
add_stream_video_obj = [
|
||||
"{FileId}",
|
||||
0,
|
||||
"{codec}",
|
||||
"{aspect}",
|
||||
"{width}",
|
||||
"{height}",
|
||||
"{Runtime}",
|
||||
"{3d}",
|
||||
"{hdrtype}",
|
||||
]
|
||||
# strHdrType is new to Kodi 20
|
||||
add_stream_video_19 = """
|
||||
INSERT INTO streamdetails(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth,
|
||||
iVideoHeight, iVideoDuration, strStereoMode)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_stream_video_obj_19 = ["{FileId}", 0, "{codec}", "{aspect}", "{width}", "{height}", "{Runtime}", "{3d}"]
|
||||
add_stream_video_obj_19 = [
|
||||
"{FileId}",
|
||||
0,
|
||||
"{codec}",
|
||||
"{aspect}",
|
||||
"{width}",
|
||||
"{height}",
|
||||
"{Runtime}",
|
||||
"{3d}",
|
||||
]
|
||||
add_stream_audio = """
|
||||
INSERT INTO streamdetails(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
@ -295,24 +322,82 @@ INSERT INTO movie(idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
|
||||
c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_movie_obj = ["{MovieId}", "{FileId}", "{Title}", "{Plot}", "{ShortPlot}", "{Tagline}",
|
||||
"{Votes}", "{RatingId}", "{Writers}", "{Year}", "{Unique}", "{SortTitle}",
|
||||
"{Runtime}", "{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}",
|
||||
"{Trailer}", "{Country}", "{Premiere}"]
|
||||
add_movie_obj = [
|
||||
"{MovieId}",
|
||||
"{FileId}",
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{ShortPlot}",
|
||||
"{Tagline}",
|
||||
"{Votes}",
|
||||
"{RatingId}",
|
||||
"{Writers}",
|
||||
"{Year}",
|
||||
"{Unique}",
|
||||
"{SortTitle}",
|
||||
"{Runtime}",
|
||||
"{Mpaa}",
|
||||
"{Genre}",
|
||||
"{Directors}",
|
||||
"{Title}",
|
||||
"{Studio}",
|
||||
"{Trailer}",
|
||||
"{Country}",
|
||||
"{Premiere}",
|
||||
]
|
||||
add_rating = """
|
||||
INSERT INTO rating(rating_id, media_id, media_type, rating_type, rating, votes)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_rating_movie_obj = ["{RatingId}", "{MovieId}", "movie", "default", "{Rating}", "{Votes}"]
|
||||
add_rating_tvshow_obj = ["{RatingId}", "{ShowId}", "tvshow", "default", "{Rating}", "{Votes}"]
|
||||
add_rating_episode_obj = ["{RatingId}", "{EpisodeId}", "episode", "default", "{Rating}", "{Votes}"]
|
||||
add_rating_movie_obj = [
|
||||
"{RatingId}",
|
||||
"{MovieId}",
|
||||
"movie",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
]
|
||||
add_rating_tvshow_obj = [
|
||||
"{RatingId}",
|
||||
"{ShowId}",
|
||||
"tvshow",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
]
|
||||
add_rating_episode_obj = [
|
||||
"{RatingId}",
|
||||
"{EpisodeId}",
|
||||
"episode",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
]
|
||||
add_unique_id = """
|
||||
INSERT INTO uniqueid(uniqueid_id, media_id, media_type, value, type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_unique_id_movie_obj = ["{Unique}", "{MovieId}", "movie", "{UniqueId}", "{ProviderName}"]
|
||||
add_unique_id_tvshow_obj = ["{Unique}", "{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}"]
|
||||
add_unique_id_episode_obj = ["{Unique}", "{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}"]
|
||||
add_unique_id_movie_obj = [
|
||||
"{Unique}",
|
||||
"{MovieId}",
|
||||
"movie",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
]
|
||||
add_unique_id_tvshow_obj = [
|
||||
"{Unique}",
|
||||
"{ShowId}",
|
||||
"tvshow",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
]
|
||||
add_unique_id_episode_obj = [
|
||||
"{Unique}",
|
||||
"{EpisodeId}",
|
||||
"episode",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
]
|
||||
add_country = """
|
||||
INSERT INTO country(name)
|
||||
VALUES (?)
|
||||
@ -335,14 +420,40 @@ INSERT INTO musicvideo(idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09,
|
||||
c11, c12, premiered)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_musicvideo_obj = ["{MvideoId}", "{FileId}", "{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}",
|
||||
"{Plot}", "{Album}", "{Artists}", "{Genre}", "{Index}", "{Premiere}"]
|
||||
add_musicvideo_obj = [
|
||||
"{MvideoId}",
|
||||
"{FileId}",
|
||||
"{Title}",
|
||||
"{Runtime}",
|
||||
"{Directors}",
|
||||
"{Studio}",
|
||||
"{Year}",
|
||||
"{Plot}",
|
||||
"{Album}",
|
||||
"{Artists}",
|
||||
"{Genre}",
|
||||
"{Index}",
|
||||
"{Premiere}",
|
||||
]
|
||||
add_tvshow = """
|
||||
INSERT INTO tvshow(idShow, c00, c01, c02, c04, c05, c08, c09, c10, c12, c13, c14, c15)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_tvshow_obj = ["{ShowId}", "{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}",
|
||||
"disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}"]
|
||||
add_tvshow_obj = [
|
||||
"{ShowId}",
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{Status}",
|
||||
"{RatingId}",
|
||||
"{Premiere}",
|
||||
"{Genre}",
|
||||
"{Title}",
|
||||
"disintegrate browse bug",
|
||||
"{Unique}",
|
||||
"{Mpaa}",
|
||||
"{Studio}",
|
||||
"{SortTitle}",
|
||||
]
|
||||
add_season = """
|
||||
INSERT INTO seasons(idSeason, idShow, season)
|
||||
VALUES (?, ?, ?)
|
||||
@ -352,9 +463,27 @@ INSERT INTO episode(idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c1
|
||||
idShow, c15, c16, idSeason, c18, c19, c20)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_episode_obj = ["{EpisodeId}", "{FileId}", "{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}",
|
||||
"{Directors}", "{Season}", "{Index}", "{Title}", "{ShowId}", "{AirsBeforeSeason}",
|
||||
"{AirsBeforeEpisode}", "{SeasonId}", "{FullFilePath}", "{PathId}", "{Unique}"]
|
||||
add_episode_obj = [
|
||||
"{EpisodeId}",
|
||||
"{FileId}",
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{RatingId}",
|
||||
"{Writers}",
|
||||
"{Premiere}",
|
||||
"{Runtime}",
|
||||
"{Directors}",
|
||||
"{Season}",
|
||||
"{Index}",
|
||||
"{Title}",
|
||||
"{ShowId}",
|
||||
"{AirsBeforeSeason}",
|
||||
"{AirsBeforeEpisode}",
|
||||
"{SeasonId}",
|
||||
"{FullFilePath}",
|
||||
"{PathId}",
|
||||
"{Unique}",
|
||||
]
|
||||
add_art = """
|
||||
INSERT INTO art(media_id, media_type, type, url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
@ -372,7 +501,13 @@ SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?
|
||||
WHERE idPath = ?
|
||||
"""
|
||||
update_path_movie_obj = ["{Path}", "movies", "metadata.local", 1, "{PathId}"]
|
||||
update_path_toptvshow_obj = ["{TopLevel}", "tvshows", "metadata.local", 1, "{TopPathId}"]
|
||||
update_path_toptvshow_obj = [
|
||||
"{TopLevel}",
|
||||
"tvshows",
|
||||
"metadata.local",
|
||||
1,
|
||||
"{TopPathId}",
|
||||
]
|
||||
update_path_toptvshow_addon_obj = ["{TopLevel}", None, None, 1, "{TopPathId}"]
|
||||
update_path_tvshow_obj = ["{Path}", None, None, 1, "{PathId}"]
|
||||
update_path_episode_obj = ["{Path}", None, None, 1, "{PathId}"]
|
||||
@ -431,26 +566,83 @@ SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,
|
||||
c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ?
|
||||
WHERE idMovie = ?
|
||||
"""
|
||||
update_movie_obj = ["{Title}", "{Plot}", "{ShortPlot}", "{Tagline}", "{Votes}", "{RatingId}",
|
||||
"{Writers}", "{Year}", "{Unique}", "{SortTitle}", "{Runtime}",
|
||||
"{Mpaa}", "{Genre}", "{Directors}", "{Title}", "{Studio}", "{Trailer}",
|
||||
"{Country}", "{Premiere}", "{MovieId}"]
|
||||
update_movie_obj = [
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{ShortPlot}",
|
||||
"{Tagline}",
|
||||
"{Votes}",
|
||||
"{RatingId}",
|
||||
"{Writers}",
|
||||
"{Year}",
|
||||
"{Unique}",
|
||||
"{SortTitle}",
|
||||
"{Runtime}",
|
||||
"{Mpaa}",
|
||||
"{Genre}",
|
||||
"{Directors}",
|
||||
"{Title}",
|
||||
"{Studio}",
|
||||
"{Trailer}",
|
||||
"{Country}",
|
||||
"{Premiere}",
|
||||
"{MovieId}",
|
||||
]
|
||||
update_rating = """
|
||||
UPDATE rating
|
||||
SET media_id = ?, media_type = ?, rating_type = ?, rating = ?, votes = ?
|
||||
WHERE rating_id = ?
|
||||
"""
|
||||
update_rating_movie_obj = ["{MovieId}", "movie", "default", "{Rating}", "{Votes}", "{RatingId}"]
|
||||
update_rating_tvshow_obj = ["{ShowId}", "tvshow", "default", "{Rating}", "{Votes}", "{RatingId}"]
|
||||
update_rating_episode_obj = ["{EpisodeId}", "episode", "default", "{Rating}", "{Votes}", "{RatingId}"]
|
||||
update_rating_movie_obj = [
|
||||
"{MovieId}",
|
||||
"movie",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
"{RatingId}",
|
||||
]
|
||||
update_rating_tvshow_obj = [
|
||||
"{ShowId}",
|
||||
"tvshow",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
"{RatingId}",
|
||||
]
|
||||
update_rating_episode_obj = [
|
||||
"{EpisodeId}",
|
||||
"episode",
|
||||
"default",
|
||||
"{Rating}",
|
||||
"{Votes}",
|
||||
"{RatingId}",
|
||||
]
|
||||
update_unique_id = """
|
||||
UPDATE uniqueid
|
||||
SET media_id = ?, media_type = ?, value = ?, type = ?
|
||||
WHERE uniqueid_id = ?
|
||||
"""
|
||||
update_unique_id_movie_obj = ["{MovieId}", "movie", "{UniqueId}", "{ProviderName}", "{Unique}"]
|
||||
update_unique_id_tvshow_obj = ["{ShowId}", "tvshow", "{UniqueId}", "{ProviderName}", "{Unique}"]
|
||||
update_unique_id_episode_obj = ["{EpisodeId}", "episode", "{UniqueId}", "{ProviderName}", "{Unique}"]
|
||||
update_unique_id_movie_obj = [
|
||||
"{MovieId}",
|
||||
"movie",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
"{Unique}",
|
||||
]
|
||||
update_unique_id_tvshow_obj = [
|
||||
"{ShowId}",
|
||||
"tvshow",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
"{Unique}",
|
||||
]
|
||||
update_unique_id_episode_obj = [
|
||||
"{EpisodeId}",
|
||||
"episode",
|
||||
"{UniqueId}",
|
||||
"{ProviderName}",
|
||||
"{Unique}",
|
||||
]
|
||||
update_country = """
|
||||
INSERT OR REPLACE INTO country_link(country_id, media_id, media_type)
|
||||
VALUES (?, ?, ?)
|
||||
@ -474,16 +666,41 @@ SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 =
|
||||
c11 = ?, c12 = ?, premiered = ?
|
||||
WHERE idMVideo = ?
|
||||
"""
|
||||
update_musicvideo_obj = ["{Title}", "{Runtime}", "{Directors}", "{Studio}", "{Year}", "{Plot}", "{Album}",
|
||||
"{Artists}", "{Genre}", "{Index}", "{Premiere}", "{MvideoId}"]
|
||||
update_musicvideo_obj = [
|
||||
"{Title}",
|
||||
"{Runtime}",
|
||||
"{Directors}",
|
||||
"{Studio}",
|
||||
"{Year}",
|
||||
"{Plot}",
|
||||
"{Album}",
|
||||
"{Artists}",
|
||||
"{Genre}",
|
||||
"{Index}",
|
||||
"{Premiere}",
|
||||
"{MvideoId}",
|
||||
]
|
||||
update_tvshow = """
|
||||
UPDATE tvshow
|
||||
SET c00 = ?, c01 = ?, c02 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?, c10 = ?,
|
||||
c12 = ?, c13 = ?, c14 = ?, c15 = ?
|
||||
WHERE idShow = ?
|
||||
"""
|
||||
update_tvshow_obj = ["{Title}", "{Plot}", "{Status}", "{RatingId}", "{Premiere}", "{Genre}", "{Title}",
|
||||
"disintegrate browse bug", "{Unique}", "{Mpaa}", "{Studio}", "{SortTitle}", "{ShowId}"]
|
||||
update_tvshow_obj = [
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{Status}",
|
||||
"{RatingId}",
|
||||
"{Premiere}",
|
||||
"{Genre}",
|
||||
"{Title}",
|
||||
"disintegrate browse bug",
|
||||
"{Unique}",
|
||||
"{Mpaa}",
|
||||
"{Studio}",
|
||||
"{SortTitle}",
|
||||
"{ShowId}",
|
||||
]
|
||||
update_tvshow_link = """
|
||||
INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath)
|
||||
VALUES (?, ?)
|
||||
@ -501,9 +718,26 @@ SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,
|
||||
c18 = ?, c19 = ?, c20 = ?
|
||||
WHERE idEpisode = ?
|
||||
"""
|
||||
update_episode_obj = ["{Title}", "{Plot}", "{RatingId}", "{Writers}", "{Premiere}", "{Runtime}", "{Directors}",
|
||||
"{Season}", "{Index}", "{Title}", "{AirsBeforeSeason}", "{AirsBeforeEpisode}", "{SeasonId}",
|
||||
"{ShowId}", "{FullFilePath}", "{PathId}", "{Unique}", "{EpisodeId}"]
|
||||
update_episode_obj = [
|
||||
"{Title}",
|
||||
"{Plot}",
|
||||
"{RatingId}",
|
||||
"{Writers}",
|
||||
"{Premiere}",
|
||||
"{Runtime}",
|
||||
"{Directors}",
|
||||
"{Season}",
|
||||
"{Index}",
|
||||
"{Title}",
|
||||
"{AirsBeforeSeason}",
|
||||
"{AirsBeforeEpisode}",
|
||||
"{SeasonId}",
|
||||
"{ShowId}",
|
||||
"{FullFilePath}",
|
||||
"{PathId}",
|
||||
"{Unique}",
|
||||
"{EpisodeId}",
|
||||
]
|
||||
|
||||
|
||||
delete_path = """
|
||||
|
@ -54,7 +54,14 @@ FROM album
|
||||
WHERE strMusicBrainzAlbumID = ?
|
||||
"""
|
||||
get_album_obj = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album"]
|
||||
get_album_obj82 = ["{AlbumId}", "{Title}", "{UniqueId}", "{Artists}", "album", "{DateAdded}"]
|
||||
get_album_obj82 = [
|
||||
"{AlbumId}",
|
||||
"{Title}",
|
||||
"{UniqueId}",
|
||||
"{Artists}",
|
||||
"album",
|
||||
"{DateAdded}",
|
||||
]
|
||||
get_album_by_name = """
|
||||
SELECT idAlbum, strArtists
|
||||
FROM album
|
||||
@ -132,9 +139,24 @@ INSERT INTO song(idSong, idAlbum, idPath, strArtistDisp, strGenres, strTitle
|
||||
rating, comment, dateAdded)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
add_song_obj = ["{SongId}", "{AlbumId}", "{PathId}", "{Artists}", "{Genre}", "{Title}", "{Index}",
|
||||
"{Runtime}", "{Year}", "{Filename}", "{UniqueId}", "{PlayCount}", "{DatePlayed}", "{Rating}",
|
||||
"{Comment}", "{DateAdded}"]
|
||||
add_song_obj = [
|
||||
"{SongId}",
|
||||
"{AlbumId}",
|
||||
"{PathId}",
|
||||
"{Artists}",
|
||||
"{Genre}",
|
||||
"{Title}",
|
||||
"{Index}",
|
||||
"{Runtime}",
|
||||
"{Year}",
|
||||
"{Filename}",
|
||||
"{UniqueId}",
|
||||
"{PlayCount}",
|
||||
"{DatePlayed}",
|
||||
"{Rating}",
|
||||
"{Comment}",
|
||||
"{DateAdded}",
|
||||
]
|
||||
add_genre = """
|
||||
INSERT INTO genre(idGenre, strGenre)
|
||||
VALUES (?, ?)
|
||||
@ -197,7 +219,17 @@ SET strArtistDisp = ?, strReleaseDate = ?, strGenres = ?, strReview = ?,
|
||||
iUserrating = ?, lastScraped = ?, bScrapedMBID = 1, strReleaseType = ?
|
||||
WHERE idAlbum = ?
|
||||
"""
|
||||
update_album_obj = ["{Artists}", "{Year}", "{Genre}", "{Bio}", "{Thumb}", "{Rating}", "{LastScraped}", "album", "{AlbumId}"]
|
||||
update_album_obj = [
|
||||
"{Artists}",
|
||||
"{Year}",
|
||||
"{Genre}",
|
||||
"{Bio}",
|
||||
"{Thumb}",
|
||||
"{Rating}",
|
||||
"{LastScraped}",
|
||||
"album",
|
||||
"{AlbumId}",
|
||||
]
|
||||
update_album_artist = """
|
||||
UPDATE album
|
||||
SET strArtists = ?
|
||||
@ -229,9 +261,22 @@ SET idAlbum = ?, strArtistDisp = ?, strGenres = ?, strTitle = ?, iTrack
|
||||
rating = ?, comment = ?, dateAdded = ?
|
||||
WHERE idSong = ?
|
||||
"""
|
||||
update_song_obj = ["{AlbumId}", "{Artists}", "{Genre}", "{Title}", "{Index}", "{Runtime}", "{Year}",
|
||||
"{Filename}", "{PlayCount}", "{DatePlayed}", "{Rating}", "{Comment}",
|
||||
"{DateAdded}", "{SongId}"]
|
||||
update_song_obj = [
|
||||
"{AlbumId}",
|
||||
"{Artists}",
|
||||
"{Genre}",
|
||||
"{Title}",
|
||||
"{Index}",
|
||||
"{Runtime}",
|
||||
"{Year}",
|
||||
"{Filename}",
|
||||
"{PlayCount}",
|
||||
"{DatePlayed}",
|
||||
"{Rating}",
|
||||
"{Comment}",
|
||||
"{DateAdded}",
|
||||
"{SongId}",
|
||||
]
|
||||
update_song_artist = """
|
||||
INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
|
@ -8,7 +8,16 @@ from kodi_six.utils import py2_encode
|
||||
|
||||
from .. import downloader as server
|
||||
from ..database import jellyfin_db, queries as QUEM
|
||||
from ..helper import api, stop, validate, validate_bluray_dir, validate_dvd_dir, jellyfin_item, values, Local
|
||||
from ..helper import (
|
||||
api,
|
||||
stop,
|
||||
validate,
|
||||
validate_bluray_dir,
|
||||
validate_dvd_dir,
|
||||
jellyfin_item,
|
||||
values,
|
||||
Local,
|
||||
)
|
||||
from ..helper import LazyLogger
|
||||
from ..helper.utils import find_library
|
||||
from ..helper.exceptions import PathValidationException
|
||||
@ -42,74 +51,83 @@ class Movies(KodiDb):
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def movie(self, item, e_item):
|
||||
|
||||
''' If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'Movie')
|
||||
obj = self.objects.map(item, "Movie")
|
||||
update = True
|
||||
|
||||
try:
|
||||
obj['MovieId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj['PathId'] = e_item[2]
|
||||
obj['LibraryId'] = e_item[6]
|
||||
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
|
||||
obj["MovieId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
obj["PathId"] = e_item[2]
|
||||
obj["LibraryId"] = e_item[6]
|
||||
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
|
||||
except TypeError:
|
||||
update = False
|
||||
LOG.debug("MovieId %s not found", obj['Id'])
|
||||
LOG.debug("MovieId %s not found", obj["Id"])
|
||||
|
||||
library = self.library or find_library(self.server, item)
|
||||
if not library:
|
||||
# This item doesn't belong to a whitelisted library
|
||||
return
|
||||
|
||||
obj['MovieId'] = self.create_entry()
|
||||
obj['LibraryId'] = library['Id']
|
||||
obj['LibraryName'] = library['Name']
|
||||
obj["MovieId"] = self.create_entry()
|
||||
obj["LibraryId"] = library["Id"]
|
||||
obj["LibraryName"] = library["Name"]
|
||||
else:
|
||||
if self.get(*values(obj, QU.get_movie_obj)) is None:
|
||||
|
||||
update = False
|
||||
LOG.info("MovieId %s missing from kodi. repairing the entry.", obj['MovieId'])
|
||||
LOG.info(
|
||||
"MovieId %s missing from kodi. repairing the entry.", obj["MovieId"]
|
||||
)
|
||||
|
||||
obj['Path'] = API.get_file_path(obj['Path'])
|
||||
obj['Genres'] = obj['Genres'] or []
|
||||
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])]
|
||||
obj['People'] = obj['People'] or []
|
||||
obj['Genre'] = " / ".join(obj['Genres'])
|
||||
obj['Writers'] = " / ".join(obj['Writers'] or [])
|
||||
obj['Directors'] = " / ".join(obj['Directors'] or [])
|
||||
obj['Plot'] = API.get_overview(obj['Plot'])
|
||||
obj['Mpaa'] = API.get_mpaa(obj['Mpaa'])
|
||||
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
|
||||
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
||||
obj['People'] = API.get_people_artwork(obj['People'])
|
||||
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
|
||||
obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
|
||||
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container'])
|
||||
obj['Audio'] = API.audio_streams(obj['Audio'] or [])
|
||||
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles'])
|
||||
if obj['Premiere'] is not None:
|
||||
obj['Premiere'] = str(obj['Premiere']).split('T')[0]
|
||||
obj["Path"] = API.get_file_path(obj["Path"])
|
||||
obj["Genres"] = obj["Genres"] or []
|
||||
obj["Studios"] = [
|
||||
API.validate_studio(studio) for studio in (obj["Studios"] or [])
|
||||
]
|
||||
obj["People"] = obj["People"] or []
|
||||
obj["Genre"] = " / ".join(obj["Genres"])
|
||||
obj["Writers"] = " / ".join(obj["Writers"] or [])
|
||||
obj["Directors"] = " / ".join(obj["Directors"] or [])
|
||||
obj["Plot"] = API.get_overview(obj["Plot"])
|
||||
obj["Mpaa"] = API.get_mpaa(obj["Mpaa"])
|
||||
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
|
||||
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
|
||||
obj["People"] = API.get_people_artwork(obj["People"])
|
||||
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
|
||||
obj["DatePlayed"] = (
|
||||
None
|
||||
if not obj["DatePlayed"]
|
||||
else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
)
|
||||
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
|
||||
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
|
||||
obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"])
|
||||
obj["Audio"] = API.audio_streams(obj["Audio"] or [])
|
||||
obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"])
|
||||
if obj["Premiere"] is not None:
|
||||
obj["Premiere"] = str(obj["Premiere"]).split("T")[0]
|
||||
|
||||
self.get_path_filename(obj)
|
||||
self.trailer(obj)
|
||||
|
||||
if obj['Countries']:
|
||||
if obj["Countries"]:
|
||||
self.add_countries(*values(obj, QU.update_country_obj))
|
||||
|
||||
tags = list(obj['Tags'] or [])
|
||||
tags.append(obj['LibraryName'])
|
||||
tags = list(obj["Tags"] or [])
|
||||
tags.append(obj["LibraryName"])
|
||||
|
||||
if obj['Favorite']:
|
||||
tags.append('Favorite movies')
|
||||
if obj["Favorite"]:
|
||||
tags.append("Favorite movies")
|
||||
|
||||
obj['Tags'] = tags
|
||||
obj["Tags"] = tags
|
||||
|
||||
if update:
|
||||
self.movie_update(obj)
|
||||
@ -124,239 +142,289 @@ class Movies(KodiDb):
|
||||
self.add_playstate(*values(obj, QU.add_bookmark_obj))
|
||||
self.add_people(*values(obj, QU.add_people_movie_obj))
|
||||
self.add_streams(*values(obj, QU.add_streams_obj))
|
||||
self.artwork.add(obj['Artwork'], obj['MovieId'], "movie")
|
||||
self.item_ids.append(obj['Id'])
|
||||
self.artwork.add(obj["Artwork"], obj["MovieId"], "movie")
|
||||
self.item_ids.append(obj["Id"])
|
||||
|
||||
return not update
|
||||
|
||||
def movie_add(self, obj):
|
||||
|
||||
''' Add object to kodi.
|
||||
'''
|
||||
obj['RatingId'] = self.create_entry_rating()
|
||||
"""Add object to kodi."""
|
||||
obj["RatingId"] = self.create_entry_rating()
|
||||
self.add_ratings(*values(obj, QU.add_rating_movie_obj))
|
||||
|
||||
obj['Unique'] = self.create_entry_unique_id()
|
||||
obj["Unique"] = self.create_entry_unique_id()
|
||||
self.add_unique_id(*values(obj, QU.add_unique_id_movie_obj))
|
||||
|
||||
obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj))
|
||||
obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj))
|
||||
obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj))
|
||||
obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj))
|
||||
|
||||
self.add(*values(obj, QU.add_movie_obj))
|
||||
self.add_videoversion(*values(obj, QU.add_video_version_obj))
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_movie_obj))
|
||||
LOG.debug("ADD movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"ADD movie [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["FileId"],
|
||||
obj["MovieId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def movie_update(self, obj):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
obj['RatingId'] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
|
||||
"""Update object to kodi."""
|
||||
obj["RatingId"] = self.get_rating_id(*values(obj, QU.get_rating_movie_obj))
|
||||
self.update_ratings(*values(obj, QU.update_rating_movie_obj))
|
||||
|
||||
obj['Unique'] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj))
|
||||
obj["Unique"] = self.get_unique_id(*values(obj, QU.get_unique_id_movie_obj))
|
||||
self.update_unique_id(*values(obj, QU.update_unique_id_movie_obj))
|
||||
|
||||
self.update(*values(obj, QU.update_movie_obj))
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("UPDATE movie [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"UPDATE movie [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["FileId"],
|
||||
obj["MovieId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def trailer(self, obj):
|
||||
|
||||
try:
|
||||
if obj['LocalTrailer']:
|
||||
if obj["LocalTrailer"]:
|
||||
|
||||
trailer = self.server.jellyfin.get_local_trailers(obj['Id'])
|
||||
obj['Trailer'] = "plugin://plugin.video.jellyfin/trailer?id=%s&mode=play" % trailer[0]['Id']
|
||||
trailer = self.server.jellyfin.get_local_trailers(obj["Id"])
|
||||
obj["Trailer"] = (
|
||||
"plugin://plugin.video.jellyfin/trailer?id=%s&mode=play"
|
||||
% trailer[0]["Id"]
|
||||
)
|
||||
|
||||
elif obj['Trailer']:
|
||||
obj['Trailer'] = "plugin://plugin.video.youtube/play/?video_id=%s" % obj['Trailer'].rsplit('=', 1)[1]
|
||||
elif obj["Trailer"]:
|
||||
obj["Trailer"] = (
|
||||
"plugin://plugin.video.youtube/play/?video_id=%s"
|
||||
% obj["Trailer"].rsplit("=", 1)[1]
|
||||
)
|
||||
except Exception as error:
|
||||
|
||||
LOG.exception("Failed to get trailer: %s", error)
|
||||
obj['Trailer'] = None
|
||||
obj["Trailer"] = None
|
||||
|
||||
def get_path_filename(self, obj):
|
||||
|
||||
''' Get the path and filename and build it into protocol://path
|
||||
'''
|
||||
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1]
|
||||
"""Get the path and filename and build it into protocol://path"""
|
||||
obj["Filename"] = (
|
||||
obj["Path"].rsplit("\\", 1)[1]
|
||||
if "\\" in obj["Path"]
|
||||
else obj["Path"].rsplit("/", 1)[1]
|
||||
)
|
||||
|
||||
if self.direct_path:
|
||||
|
||||
if not validate(obj['Path']):
|
||||
if not validate(obj["Path"]):
|
||||
raise PathValidationException("Failed to validate path. User stopped.")
|
||||
|
||||
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
|
||||
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
|
||||
|
||||
'''check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO'''
|
||||
if validate_dvd_dir(obj['Path'] + obj['Filename']):
|
||||
obj['Path'] = obj['Path'] + obj['Filename'] + '/VIDEO_TS/'
|
||||
obj['Filename'] = 'VIDEO_TS.IFO'
|
||||
LOG.debug("DVD directory %s", obj['Path'])
|
||||
"""check dvd directories and point it to ./VIDEO_TS/VIDEO_TS.IFO"""
|
||||
if validate_dvd_dir(obj["Path"] + obj["Filename"]):
|
||||
obj["Path"] = obj["Path"] + obj["Filename"] + "/VIDEO_TS/"
|
||||
obj["Filename"] = "VIDEO_TS.IFO"
|
||||
LOG.debug("DVD directory %s", obj["Path"])
|
||||
|
||||
'''check bluray directories and point it to ./BDMV/index.bdmv'''
|
||||
if validate_bluray_dir(obj['Path'] + obj['Filename']):
|
||||
obj['Path'] = obj['Path'] + obj['Filename'] + '/BDMV/'
|
||||
obj['Filename'] = 'index.bdmv'
|
||||
LOG.debug("Bluray directory %s", obj['Path'])
|
||||
"""check bluray directories and point it to ./BDMV/index.bdmv"""
|
||||
if validate_bluray_dir(obj["Path"] + obj["Filename"]):
|
||||
obj["Path"] = obj["Path"] + obj["Filename"] + "/BDMV/"
|
||||
obj["Filename"] = "index.bdmv"
|
||||
LOG.debug("Bluray directory %s", obj["Path"])
|
||||
|
||||
else:
|
||||
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId']
|
||||
obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
|
||||
params = {
|
||||
'filename': py2_encode(obj['Filename'], 'utf-8'),
|
||||
'id': obj['Id'],
|
||||
'dbid': obj['MovieId'],
|
||||
'mode': "play"
|
||||
"filename": py2_encode(obj["Filename"], "utf-8"),
|
||||
"id": obj["Id"],
|
||||
"dbid": obj["MovieId"],
|
||||
"mode": "play",
|
||||
}
|
||||
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params))
|
||||
obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def boxset(self, item, e_item):
|
||||
"""If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
|
||||
''' If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
|
||||
Process movies inside boxset.
|
||||
Process removals from boxset.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
Process movies inside boxset.
|
||||
Process removals from boxset.
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'Boxset')
|
||||
obj = self.objects.map(item, "Boxset")
|
||||
|
||||
obj['Overview'] = API.get_overview(obj['Overview'])
|
||||
obj["Overview"] = API.get_overview(obj["Overview"])
|
||||
|
||||
try:
|
||||
obj['SetId'] = e_item[0]
|
||||
obj["SetId"] = e_item[0]
|
||||
self.update_boxset(*values(obj, QU.update_set_obj))
|
||||
except TypeError:
|
||||
LOG.debug("SetId %s not found", obj['Id'])
|
||||
obj['SetId'] = self.add_boxset(*values(obj, QU.add_set_obj))
|
||||
LOG.debug("SetId %s not found", obj["Id"])
|
||||
obj["SetId"] = self.add_boxset(*values(obj, QU.add_set_obj))
|
||||
|
||||
self.boxset_current(obj)
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
|
||||
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
|
||||
|
||||
for movie in obj['Current']:
|
||||
for movie in obj["Current"]:
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Movie'] = movie
|
||||
temp_obj['MovieId'] = obj['Current'][temp_obj['Movie']]
|
||||
temp_obj["Movie"] = movie
|
||||
temp_obj["MovieId"] = obj["Current"][temp_obj["Movie"]]
|
||||
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj))
|
||||
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj))
|
||||
LOG.debug("DELETE from boxset [%s] %s: %s", temp_obj['SetId'], temp_obj['Title'], temp_obj['MovieId'])
|
||||
self.jellyfin_db.update_parent_id(
|
||||
*values(temp_obj, QUEM.delete_parent_boxset_obj)
|
||||
)
|
||||
LOG.debug(
|
||||
"DELETE from boxset [%s] %s: %s",
|
||||
temp_obj["SetId"],
|
||||
temp_obj["Title"],
|
||||
temp_obj["MovieId"],
|
||||
)
|
||||
|
||||
self.artwork.add(obj['Artwork'], obj['SetId'], "set")
|
||||
self.artwork.add(obj["Artwork"], obj["SetId"], "set")
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_boxset_obj))
|
||||
LOG.debug("UPDATE boxset [%s] %s", obj['SetId'], obj['Title'])
|
||||
LOG.debug("UPDATE boxset [%s] %s", obj["SetId"], obj["Title"])
|
||||
|
||||
def boxset_current(self, obj):
|
||||
|
||||
''' Add or removes movies based on the current movies found in the boxset.
|
||||
'''
|
||||
"""Add or removes movies based on the current movies found in the boxset."""
|
||||
try:
|
||||
current = self.jellyfin_db.get_item_id_by_parent_id(*values(obj, QUEM.get_item_id_by_parent_boxset_obj))
|
||||
current = self.jellyfin_db.get_item_id_by_parent_id(
|
||||
*values(obj, QUEM.get_item_id_by_parent_boxset_obj)
|
||||
)
|
||||
movies = dict(current)
|
||||
except ValueError:
|
||||
movies = {}
|
||||
|
||||
obj['Current'] = movies
|
||||
obj["Current"] = movies
|
||||
|
||||
for all_movies in server.get_movies_by_boxset(obj['Id']):
|
||||
for movie in all_movies['Items']:
|
||||
for all_movies in server.get_movies_by_boxset(obj["Id"]):
|
||||
for movie in all_movies["Items"]:
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Title'] = movie['Name']
|
||||
temp_obj['Id'] = movie['Id']
|
||||
temp_obj["Title"] = movie["Name"]
|
||||
temp_obj["Id"] = movie["Id"]
|
||||
|
||||
try:
|
||||
temp_obj['MovieId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
temp_obj["MovieId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
LOG.info("Failed to process %s to boxset.", temp_obj['Title'])
|
||||
LOG.info("Failed to process %s to boxset.", temp_obj["Title"])
|
||||
|
||||
continue
|
||||
|
||||
if temp_obj['Id'] not in obj['Current']:
|
||||
if temp_obj["Id"] not in obj["Current"]:
|
||||
|
||||
self.set_boxset(*values(temp_obj, QU.update_movie_set_obj))
|
||||
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_movie_obj))
|
||||
LOG.debug("ADD to boxset [%s/%s] %s: %s to boxset", temp_obj['SetId'], temp_obj['MovieId'], temp_obj['Title'], temp_obj['Id'])
|
||||
self.jellyfin_db.update_parent_id(
|
||||
*values(temp_obj, QUEM.update_parent_movie_obj)
|
||||
)
|
||||
LOG.debug(
|
||||
"ADD to boxset [%s/%s] %s: %s to boxset",
|
||||
temp_obj["SetId"],
|
||||
temp_obj["MovieId"],
|
||||
temp_obj["Title"],
|
||||
temp_obj["Id"],
|
||||
)
|
||||
else:
|
||||
obj['Current'].pop(temp_obj['Id'])
|
||||
obj["Current"].pop(temp_obj["Id"])
|
||||
|
||||
def boxsets_reset(self):
|
||||
|
||||
''' Special function to remove all existing boxsets.
|
||||
'''
|
||||
boxsets = self.jellyfin_db.get_items_by_media('set')
|
||||
"""Special function to remove all existing boxsets."""
|
||||
boxsets = self.jellyfin_db.get_items_by_media("set")
|
||||
for boxset in boxsets:
|
||||
self.remove(boxset[0])
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def userdata(self, item, e_item):
|
||||
|
||||
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'MovieUserData')
|
||||
obj = self.objects.map(item, "MovieUserData")
|
||||
|
||||
try:
|
||||
obj['MovieId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj["MovieId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
|
||||
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
||||
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
|
||||
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
|
||||
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
|
||||
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
|
||||
|
||||
if obj['DatePlayed']:
|
||||
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
if obj["DatePlayed"]:
|
||||
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
|
||||
if obj['Favorite']:
|
||||
if obj["Favorite"]:
|
||||
self.get_tag(*values(obj, QU.get_tag_movie_obj))
|
||||
else:
|
||||
self.remove_tag(*values(obj, QU.delete_tag_movie_obj))
|
||||
|
||||
LOG.debug("New resume point %s: %s", obj['Id'], obj['Resume'])
|
||||
LOG.debug("New resume point %s: %s", obj["Id"], obj["Resume"])
|
||||
self.add_playstate(*values(obj, QU.add_bookmark_obj))
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("USERDATA movie [%s/%s] %s: %s", obj['FileId'], obj['MovieId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"USERDATA movie [%s/%s] %s: %s",
|
||||
obj["FileId"],
|
||||
obj["MovieId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def remove(self, item_id, e_item):
|
||||
|
||||
''' Remove movieid, fileid, jellyfin reference.
|
||||
Remove artwork, boxset
|
||||
'''
|
||||
obj = {'Id': item_id}
|
||||
"""Remove movieid, fileid, jellyfin reference.
|
||||
Remove artwork, boxset
|
||||
"""
|
||||
obj = {"Id": item_id}
|
||||
|
||||
try:
|
||||
obj['KodiId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj['Media'] = e_item[4]
|
||||
obj["KodiId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
obj["Media"] = e_item[4]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
self.artwork.delete(obj['KodiId'], obj['Media'])
|
||||
self.artwork.delete(obj["KodiId"], obj["Media"])
|
||||
|
||||
if obj['Media'] == 'movie':
|
||||
if obj["Media"] == "movie":
|
||||
self.delete(*values(obj, QU.delete_movie_obj))
|
||||
elif obj['Media'] == 'set':
|
||||
elif obj["Media"] == "set":
|
||||
|
||||
for movie in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_movie_obj)):
|
||||
for movie in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(obj, QUEM.get_item_by_parent_movie_obj)
|
||||
):
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['MovieId'] = movie[1]
|
||||
temp_obj['Movie'] = movie[0]
|
||||
temp_obj["MovieId"] = movie[1]
|
||||
temp_obj["Movie"] = movie[0]
|
||||
self.remove_from_boxset(*values(temp_obj, QU.delete_movie_set_obj))
|
||||
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.delete_parent_boxset_obj))
|
||||
self.jellyfin_db.update_parent_id(
|
||||
*values(temp_obj, QUEM.delete_parent_boxset_obj)
|
||||
)
|
||||
|
||||
self.delete_boxset(*values(obj, QU.delete_set_obj))
|
||||
|
||||
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
|
||||
LOG.debug("DELETE %s [%s/%s] %s", obj['Media'], obj['FileId'], obj['KodiId'], obj['Id'])
|
||||
LOG.debug(
|
||||
"DELETE %s [%s/%s] %s",
|
||||
obj["Media"],
|
||||
obj["FileId"],
|
||||
obj["KodiId"],
|
||||
obj["Id"],
|
||||
)
|
||||
|
@ -39,19 +39,20 @@ class Music(KodiDb):
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def artist(self, item, e_item):
|
||||
|
||||
''' If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'Artist')
|
||||
obj = self.objects.map(item, "Artist")
|
||||
update = True
|
||||
|
||||
try:
|
||||
obj['ArtistId'] = e_item[0]
|
||||
obj['LibraryId'] = e_item[6]
|
||||
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
|
||||
obj["ArtistId"] = e_item[0]
|
||||
obj["LibraryId"] = e_item[6]
|
||||
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
|
||||
except TypeError:
|
||||
update = False
|
||||
|
||||
@ -60,72 +61,81 @@ class Music(KodiDb):
|
||||
# This item doesn't belong to a whitelisted library
|
||||
return
|
||||
|
||||
obj['ArtistId'] = None
|
||||
obj['LibraryId'] = library['Id']
|
||||
obj['LibraryName'] = library['Name']
|
||||
LOG.debug("ArtistId %s not found", obj['Id'])
|
||||
obj["ArtistId"] = None
|
||||
obj["LibraryId"] = library["Id"]
|
||||
obj["LibraryName"] = library["Name"]
|
||||
LOG.debug("ArtistId %s not found", obj["Id"])
|
||||
else:
|
||||
if self.validate_artist(*values(obj, QU.get_artist_by_id_obj)) is None:
|
||||
|
||||
update = False
|
||||
LOG.info("ArtistId %s missing from kodi. repairing the entry.", obj['ArtistId'])
|
||||
LOG.info(
|
||||
"ArtistId %s missing from kodi. repairing the entry.",
|
||||
obj["ArtistId"],
|
||||
)
|
||||
|
||||
obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
obj['ArtistType'] = "MusicArtist"
|
||||
obj['Genre'] = " / ".join(obj['Genres'] or [])
|
||||
obj['Bio'] = API.get_overview(obj['Bio'])
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
|
||||
obj['Thumb'] = obj['Artwork']['Primary']
|
||||
obj['Backdrops'] = obj['Artwork']['Backdrop'] or ""
|
||||
obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
obj["ArtistType"] = "MusicArtist"
|
||||
obj["Genre"] = " / ".join(obj["Genres"] or [])
|
||||
obj["Bio"] = API.get_overview(obj["Bio"])
|
||||
obj["Artwork"] = API.get_all_artwork(
|
||||
self.objects.map(item, "ArtworkMusic"), True
|
||||
)
|
||||
obj["Thumb"] = obj["Artwork"]["Primary"]
|
||||
obj["Backdrops"] = obj["Artwork"]["Backdrop"] or ""
|
||||
|
||||
if obj['Thumb']:
|
||||
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb']
|
||||
if obj["Thumb"]:
|
||||
obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
|
||||
|
||||
if obj['Backdrops']:
|
||||
obj['Backdrops'] = "<fanart>%s</fanart>" % obj['Backdrops'][0]
|
||||
if obj["Backdrops"]:
|
||||
obj["Backdrops"] = "<fanart>%s</fanart>" % obj["Backdrops"][0]
|
||||
|
||||
if update:
|
||||
self.artist_update(obj)
|
||||
else:
|
||||
self.artist_add(obj)
|
||||
|
||||
self.update(obj['Genre'], obj['Bio'], obj['Thumb'], obj['Backdrops'], obj['LastScraped'], obj['ArtistId'])
|
||||
self.artwork.add(obj['Artwork'], obj['ArtistId'], "artist")
|
||||
self.item_ids.append(obj['Id'])
|
||||
self.update(
|
||||
obj["Genre"],
|
||||
obj["Bio"],
|
||||
obj["Thumb"],
|
||||
obj["Backdrops"],
|
||||
obj["LastScraped"],
|
||||
obj["ArtistId"],
|
||||
)
|
||||
self.artwork.add(obj["Artwork"], obj["ArtistId"], "artist")
|
||||
self.item_ids.append(obj["Id"])
|
||||
|
||||
def artist_add(self, obj):
|
||||
"""Add object to kodi.
|
||||
|
||||
''' Add object to kodi.
|
||||
|
||||
safety checks: It looks like Jellyfin supports the same artist multiple times.
|
||||
Kodi doesn't allow that. In case that happens we just merge the artist entries.
|
||||
'''
|
||||
obj['ArtistId'] = self.get(*values(obj, QU.get_artist_obj))
|
||||
safety checks: It looks like Jellyfin supports the same artist multiple times.
|
||||
Kodi doesn't allow that. In case that happens we just merge the artist entries.
|
||||
"""
|
||||
obj["ArtistId"] = self.get(*values(obj, QU.get_artist_obj))
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_artist_obj))
|
||||
LOG.debug("ADD artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id'])
|
||||
LOG.debug("ADD artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"])
|
||||
|
||||
def artist_update(self, obj):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
"""Update object to kodi."""
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("UPDATE artist [%s] %s: %s", obj['ArtistId'], obj['Name'], obj['Id'])
|
||||
LOG.debug("UPDATE artist [%s] %s: %s", obj["ArtistId"], obj["Name"], obj["Id"])
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def album(self, item, e_item):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""Update object to kodi."""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'Album')
|
||||
obj = self.objects.map(item, "Album")
|
||||
update = True
|
||||
|
||||
try:
|
||||
obj['AlbumId'] = e_item[0]
|
||||
obj['LibraryId'] = e_item[6]
|
||||
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
|
||||
obj["AlbumId"] = e_item[0]
|
||||
obj["LibraryId"] = e_item[6]
|
||||
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
|
||||
except TypeError:
|
||||
update = False
|
||||
|
||||
@ -134,31 +144,35 @@ class Music(KodiDb):
|
||||
# This item doesn't belong to a whitelisted library
|
||||
return
|
||||
|
||||
obj['AlbumId'] = None
|
||||
obj['LibraryId'] = library['Id']
|
||||
obj['LibraryName'] = library['Name']
|
||||
LOG.debug("AlbumId %s not found", obj['Id'])
|
||||
obj["AlbumId"] = None
|
||||
obj["LibraryId"] = library["Id"]
|
||||
obj["LibraryName"] = library["Name"]
|
||||
LOG.debug("AlbumId %s not found", obj["Id"])
|
||||
else:
|
||||
if self.validate_album(*values(obj, QU.get_album_by_id_obj)) is None:
|
||||
|
||||
update = False
|
||||
LOG.info("AlbumId %s missing from kodi. repairing the entry.", obj['AlbumId'])
|
||||
LOG.info(
|
||||
"AlbumId %s missing from kodi. repairing the entry.", obj["AlbumId"]
|
||||
)
|
||||
|
||||
obj['Rating'] = 0
|
||||
obj['LastScraped'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
obj['Genres'] = obj['Genres'] or []
|
||||
obj['Genre'] = " / ".join(obj['Genres'])
|
||||
obj['Bio'] = API.get_overview(obj['Bio'])
|
||||
obj['Artists'] = " / ".join(obj['Artists'] or [])
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
|
||||
obj['Thumb'] = obj['Artwork']['Primary']
|
||||
obj['DateAdded'] = item.get('DateCreated')
|
||||
obj["Rating"] = 0
|
||||
obj["LastScraped"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
obj["Genres"] = obj["Genres"] or []
|
||||
obj["Genre"] = " / ".join(obj["Genres"])
|
||||
obj["Bio"] = API.get_overview(obj["Bio"])
|
||||
obj["Artists"] = " / ".join(obj["Artists"] or [])
|
||||
obj["Artwork"] = API.get_all_artwork(
|
||||
self.objects.map(item, "ArtworkMusic"), True
|
||||
)
|
||||
obj["Thumb"] = obj["Artwork"]["Primary"]
|
||||
obj["DateAdded"] = item.get("DateCreated")
|
||||
|
||||
if obj['DateAdded']:
|
||||
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
|
||||
if obj["DateAdded"]:
|
||||
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
|
||||
|
||||
if obj['Thumb']:
|
||||
obj['Thumb'] = "<thumb>%s</thumb>" % obj['Thumb']
|
||||
if obj["Thumb"]:
|
||||
obj["Thumb"] = "<thumb>%s</thumb>" % obj["Thumb"]
|
||||
|
||||
if update:
|
||||
self.album_update(obj)
|
||||
@ -169,89 +183,90 @@ class Music(KodiDb):
|
||||
self.artist_discography(obj)
|
||||
self.update_album(*values(obj, QU.update_album_obj))
|
||||
self.add_genres(*values(obj, QU.add_genres_obj))
|
||||
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album")
|
||||
self.item_ids.append(obj['Id'])
|
||||
self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
|
||||
self.item_ids.append(obj["Id"])
|
||||
|
||||
def album_add(self, obj):
|
||||
|
||||
''' Add object to kodi.
|
||||
'''
|
||||
"""Add object to kodi."""
|
||||
if self.version_id >= 82:
|
||||
obj_values = values(obj, QU.get_album_obj82)
|
||||
else:
|
||||
obj_values = values(obj, QU.get_album_obj)
|
||||
obj['AlbumId'] = self.get_album(*obj_values)
|
||||
obj["AlbumId"] = self.get_album(*obj_values)
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_album_obj))
|
||||
LOG.debug("ADD album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id'])
|
||||
LOG.debug("ADD album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"])
|
||||
|
||||
def album_update(self, obj):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
"""Update object to kodi."""
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("UPDATE album [%s] %s: %s", obj['AlbumId'], obj['Title'], obj['Id'])
|
||||
LOG.debug("UPDATE album [%s] %s: %s", obj["AlbumId"], obj["Title"], obj["Id"])
|
||||
|
||||
def artist_discography(self, obj):
|
||||
|
||||
''' Update the artist's discography.
|
||||
'''
|
||||
for artist in (obj['ArtistItems'] or []):
|
||||
"""Update the artist's discography."""
|
||||
for artist in obj["ArtistItems"] or []:
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Id'] = artist['Id']
|
||||
temp_obj['AlbumId'] = obj['Id']
|
||||
temp_obj["Id"] = artist["Id"]
|
||||
temp_obj["AlbumId"] = obj["Id"]
|
||||
|
||||
try:
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
self.add_discography(*values(temp_obj, QU.update_discography_obj))
|
||||
self.jellyfin_db.update_parent_id(*values(temp_obj, QUEM.update_parent_album_obj))
|
||||
self.jellyfin_db.update_parent_id(
|
||||
*values(temp_obj, QUEM.update_parent_album_obj)
|
||||
)
|
||||
|
||||
def artist_link(self, obj):
|
||||
|
||||
''' Assign main artists to album.
|
||||
Artist does not exist in jellyfin database, create the reference.
|
||||
'''
|
||||
for artist in (obj['AlbumArtists'] or []):
|
||||
"""Assign main artists to album.
|
||||
Artist does not exist in jellyfin database, create the reference.
|
||||
"""
|
||||
for artist in obj["AlbumArtists"] or []:
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Name'] = artist['Name']
|
||||
temp_obj['Id'] = artist['Id']
|
||||
temp_obj["Name"] = artist["Name"]
|
||||
temp_obj["Id"] = artist["Id"]
|
||||
|
||||
try:
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
|
||||
try:
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
continue
|
||||
|
||||
self.update_artist_name(*values(temp_obj, QU.update_artist_name_obj))
|
||||
self.link(*values(temp_obj, QU.update_link_obj))
|
||||
self.item_ids.append(temp_obj['Id'])
|
||||
self.item_ids.append(temp_obj["Id"])
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def song(self, item, e_item):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""Update object to kodi."""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'Song')
|
||||
obj = self.objects.map(item, "Song")
|
||||
update = True
|
||||
|
||||
try:
|
||||
obj['SongId'] = e_item[0]
|
||||
obj['PathId'] = e_item[2]
|
||||
obj['AlbumId'] = e_item[3]
|
||||
obj['LibraryId'] = e_item[6]
|
||||
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
|
||||
obj["SongId"] = e_item[0]
|
||||
obj["PathId"] = e_item[2]
|
||||
obj["AlbumId"] = e_item[3]
|
||||
obj["LibraryId"] = e_item[6]
|
||||
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
|
||||
except TypeError:
|
||||
update = False
|
||||
|
||||
@ -260,38 +275,42 @@ class Music(KodiDb):
|
||||
# This item doesn't belong to a whitelisted library
|
||||
return
|
||||
|
||||
obj['SongId'] = self.create_entry_song()
|
||||
obj['LibraryId'] = library['Id']
|
||||
obj['LibraryName'] = library['Name']
|
||||
LOG.debug("SongId %s not found", obj['Id'])
|
||||
obj["SongId"] = self.create_entry_song()
|
||||
obj["LibraryId"] = library["Id"]
|
||||
obj["LibraryName"] = library["Name"]
|
||||
LOG.debug("SongId %s not found", obj["Id"])
|
||||
else:
|
||||
if self.validate_song(*values(obj, QU.get_song_by_id_obj)) is None:
|
||||
|
||||
update = False
|
||||
LOG.info("SongId %s missing from kodi. repairing the entry.", obj['SongId'])
|
||||
LOG.info(
|
||||
"SongId %s missing from kodi. repairing the entry.", obj["SongId"]
|
||||
)
|
||||
|
||||
self.get_song_path_filename(obj, API)
|
||||
|
||||
obj['Rating'] = 0
|
||||
obj['Genres'] = obj['Genres'] or []
|
||||
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
|
||||
obj['Runtime'] = (obj['Runtime'] or 0) / 10000000.0
|
||||
obj['Genre'] = " / ".join(obj['Genres'])
|
||||
obj['Artists'] = " / ".join(obj['Artists'] or [])
|
||||
obj['AlbumArtists'] = obj['AlbumArtists'] or []
|
||||
obj['Index'] = obj['Index'] or 0
|
||||
obj['Disc'] = obj['Disc'] or 1
|
||||
obj['EmbedCover'] = False
|
||||
obj['Comment'] = API.get_overview(obj['Comment'])
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'ArtworkMusic'), True)
|
||||
obj["Rating"] = 0
|
||||
obj["Genres"] = obj["Genres"] or []
|
||||
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
|
||||
obj["Runtime"] = (obj["Runtime"] or 0) / 10000000.0
|
||||
obj["Genre"] = " / ".join(obj["Genres"])
|
||||
obj["Artists"] = " / ".join(obj["Artists"] or [])
|
||||
obj["AlbumArtists"] = obj["AlbumArtists"] or []
|
||||
obj["Index"] = obj["Index"] or 0
|
||||
obj["Disc"] = obj["Disc"] or 1
|
||||
obj["EmbedCover"] = False
|
||||
obj["Comment"] = API.get_overview(obj["Comment"])
|
||||
obj["Artwork"] = API.get_all_artwork(
|
||||
self.objects.map(item, "ArtworkMusic"), True
|
||||
)
|
||||
|
||||
if obj['DateAdded']:
|
||||
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
|
||||
if obj["DateAdded"]:
|
||||
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
|
||||
|
||||
if obj['DatePlayed']:
|
||||
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
if obj["DatePlayed"]:
|
||||
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
|
||||
obj['Index'] = obj['Disc'] * 2 ** 16 + obj['Index']
|
||||
obj["Index"] = obj["Disc"] * 2**16 + obj["Index"]
|
||||
|
||||
if update:
|
||||
self.song_update(obj)
|
||||
@ -303,227 +322,275 @@ class Music(KodiDb):
|
||||
self.song_artist_link(obj)
|
||||
self.song_artist_discography(obj)
|
||||
|
||||
obj['strAlbumArtists'] = " / ".join(obj['AlbumArtists'])
|
||||
obj["strAlbumArtists"] = " / ".join(obj["AlbumArtists"])
|
||||
self.get_album_artist(*values(obj, QU.get_album_artist_obj))
|
||||
|
||||
self.add_genres(*values(obj, QU.update_genre_song_obj))
|
||||
self.artwork.add(obj['Artwork'], obj['SongId'], "song")
|
||||
self.item_ids.append(obj['Id'])
|
||||
self.artwork.add(obj["Artwork"], obj["SongId"], "song")
|
||||
self.item_ids.append(obj["Id"])
|
||||
|
||||
if obj['SongAlbumId'] is None:
|
||||
self.artwork.add(obj['Artwork'], obj['AlbumId'], "album")
|
||||
if obj["SongAlbumId"] is None:
|
||||
self.artwork.add(obj["Artwork"], obj["AlbumId"], "album")
|
||||
|
||||
return not update
|
||||
|
||||
def song_add(self, obj):
|
||||
"""Add object to kodi.
|
||||
|
||||
''' Add object to kodi.
|
||||
|
||||
Verify if there's an album associated.
|
||||
If no album found, create a single's album
|
||||
'''
|
||||
obj['PathId'] = self.add_path(obj['Path'])
|
||||
Verify if there's an album associated.
|
||||
If no album found, create a single's album
|
||||
"""
|
||||
obj["PathId"] = self.add_path(obj["Path"])
|
||||
|
||||
try:
|
||||
obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0]
|
||||
obj["AlbumId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(obj, QUEM.get_item_song_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
|
||||
try:
|
||||
if obj['SongAlbumId'] is None:
|
||||
if obj["SongAlbumId"] is None:
|
||||
raise TypeError("No album id found associated?")
|
||||
|
||||
self.album(self.server.jellyfin.get_item(obj['SongAlbumId']))
|
||||
obj['AlbumId'] = self.jellyfin_db.get_item_by_id(*values(obj, QUEM.get_item_song_obj))[0]
|
||||
self.album(self.server.jellyfin.get_item(obj["SongAlbumId"]))
|
||||
obj["AlbumId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(obj, QUEM.get_item_song_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
self.single(obj)
|
||||
|
||||
self.add_song(*values(obj, QU.add_song_obj))
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_song_obj))
|
||||
LOG.debug("ADD song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"ADD song [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["AlbumId"],
|
||||
obj["SongId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def song_update(self, obj):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
"""Update object to kodi."""
|
||||
self.update_path(*values(obj, QU.update_path_obj))
|
||||
|
||||
self.update_song(*values(obj, QU.update_song_obj))
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("UPDATE song [%s/%s/%s] %s: %s", obj['PathId'], obj['AlbumId'], obj['SongId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"UPDATE song [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["AlbumId"],
|
||||
obj["SongId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def get_song_path_filename(self, obj, api):
|
||||
|
||||
''' Get the path and filename and build it into protocol://path
|
||||
'''
|
||||
obj['Path'] = api.get_file_path(obj['Path'])
|
||||
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1]
|
||||
"""Get the path and filename and build it into protocol://path"""
|
||||
obj["Path"] = api.get_file_path(obj["Path"])
|
||||
obj["Filename"] = (
|
||||
obj["Path"].rsplit("\\", 1)[1]
|
||||
if "\\" in obj["Path"]
|
||||
else obj["Path"].rsplit("/", 1)[1]
|
||||
)
|
||||
|
||||
if self.direct_path:
|
||||
|
||||
if not validate(obj['Path']):
|
||||
if not validate(obj["Path"]):
|
||||
raise PathValidationException("Failed to validate path. User stopped.")
|
||||
|
||||
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
|
||||
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
|
||||
|
||||
else:
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
obj['Path'] = "%s/Audio/%s/" % (server_address, obj['Id'])
|
||||
obj['Filename'] = "stream.%s?static=true" % obj['Container']
|
||||
server_address = self.server.auth.get_server_info(
|
||||
self.server.auth.server_id
|
||||
)["address"]
|
||||
obj["Path"] = "%s/Audio/%s/" % (server_address, obj["Id"])
|
||||
obj["Filename"] = "stream.%s?static=true" % obj["Container"]
|
||||
|
||||
def song_artist_discography(self, obj):
|
||||
|
||||
''' Update the artist's discography.
|
||||
'''
|
||||
"""Update the artist's discography."""
|
||||
artists = []
|
||||
for artist in (obj['AlbumArtists'] or []):
|
||||
for artist in obj["AlbumArtists"] or []:
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Name'] = artist['Name']
|
||||
temp_obj['Id'] = artist['Id']
|
||||
temp_obj["Name"] = artist["Name"]
|
||||
temp_obj["Id"] = artist["Id"]
|
||||
|
||||
artists.append(temp_obj['Name'])
|
||||
artists.append(temp_obj["Name"])
|
||||
|
||||
try:
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
|
||||
try:
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
continue
|
||||
|
||||
self.link(*values(temp_obj, QU.update_link_obj))
|
||||
self.item_ids.append(temp_obj['Id'])
|
||||
self.item_ids.append(temp_obj["Id"])
|
||||
|
||||
if obj['Album']:
|
||||
if obj["Album"]:
|
||||
|
||||
temp_obj['Title'] = obj['Album']
|
||||
temp_obj['Year'] = 0
|
||||
temp_obj["Title"] = obj["Album"]
|
||||
temp_obj["Year"] = 0
|
||||
self.add_discography(*values(temp_obj, QU.update_discography_obj))
|
||||
|
||||
obj['AlbumArtists'] = artists
|
||||
obj["AlbumArtists"] = artists
|
||||
|
||||
def song_artist_link(self, obj):
|
||||
|
||||
''' Assign main artists to song.
|
||||
Artist does not exist in jellyfin database, create the reference.
|
||||
'''
|
||||
for index, artist in enumerate(obj['ArtistItems'] or []):
|
||||
"""Assign main artists to song.
|
||||
Artist does not exist in jellyfin database, create the reference.
|
||||
"""
|
||||
for index, artist in enumerate(obj["ArtistItems"] or []):
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['Name'] = artist['Name']
|
||||
temp_obj['Id'] = artist['Id']
|
||||
temp_obj['Index'] = index
|
||||
temp_obj["Name"] = artist["Name"]
|
||||
temp_obj["Id"] = artist["Id"]
|
||||
temp_obj["Index"] = index
|
||||
|
||||
try:
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except TypeError:
|
||||
|
||||
try:
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj['Id']))
|
||||
temp_obj['ArtistId'] = self.jellyfin_db.get_item_by_id(*values(temp_obj, QUEM.get_item_obj))[0]
|
||||
self.artist(self.server.jellyfin.get_item(temp_obj["Id"]))
|
||||
temp_obj["ArtistId"] = self.jellyfin_db.get_item_by_id(
|
||||
*values(temp_obj, QUEM.get_item_obj)
|
||||
)[0]
|
||||
except Exception as error:
|
||||
LOG.exception(error)
|
||||
continue
|
||||
|
||||
self.link_song_artist(*values(temp_obj, QU.update_song_artist_obj))
|
||||
self.item_ids.append(temp_obj['Id'])
|
||||
self.item_ids.append(temp_obj["Id"])
|
||||
|
||||
def single(self, obj):
|
||||
|
||||
obj['AlbumId'] = self.create_entry_album()
|
||||
obj["AlbumId"] = self.create_entry_album()
|
||||
self.add_single(*values(obj, QU.add_single_obj))
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def userdata(self, item, e_item):
|
||||
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
"""
|
||||
|
||||
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
'''
|
||||
|
||||
obj = self.objects.map(item, 'SongUserData')
|
||||
obj = self.objects.map(item, "SongUserData")
|
||||
|
||||
try:
|
||||
obj['KodiId'] = e_item[0]
|
||||
obj['Media'] = e_item[4]
|
||||
obj["KodiId"] = e_item[0]
|
||||
obj["Media"] = e_item[4]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
obj['Rating'] = 0
|
||||
obj["Rating"] = 0
|
||||
|
||||
if obj['Media'] == 'song':
|
||||
if obj["Media"] == "song":
|
||||
|
||||
if obj['DatePlayed']:
|
||||
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
if obj["DatePlayed"]:
|
||||
obj["DatePlayed"] = (
|
||||
Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
)
|
||||
|
||||
self.rate_song(*values(obj, QU.update_song_rating_obj))
|
||||
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("USERDATA %s [%s] %s: %s", obj['Media'], obj['KodiId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"USERDATA %s [%s] %s: %s",
|
||||
obj["Media"],
|
||||
obj["KodiId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def remove(self, item_id, e_item):
|
||||
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
|
||||
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
|
||||
This should address single song scenario, where server doesn't actually
|
||||
create an album for the song.
|
||||
'''
|
||||
obj = {'Id': item_id}
|
||||
This should address single song scenario, where server doesn't actually
|
||||
create an album for the song.
|
||||
"""
|
||||
obj = {"Id": item_id}
|
||||
|
||||
try:
|
||||
obj['KodiId'] = e_item[0]
|
||||
obj['Media'] = e_item[4]
|
||||
obj["KodiId"] = e_item[0]
|
||||
obj["Media"] = e_item[4]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
if obj['Media'] == 'song':
|
||||
if obj["Media"] == "song":
|
||||
|
||||
self.remove_song(obj['KodiId'], obj['Id'])
|
||||
self.jellyfin_db.remove_wild_item(obj['Id'])
|
||||
self.remove_song(obj["KodiId"], obj["Id"])
|
||||
self.jellyfin_db.remove_wild_item(obj["Id"])
|
||||
|
||||
for item in self.jellyfin_db.get_item_by_wild_id(*values(obj, QUEM.get_item_by_wild_obj)):
|
||||
if item[1] == 'album':
|
||||
for item in self.jellyfin_db.get_item_by_wild_id(
|
||||
*values(obj, QUEM.get_item_by_wild_obj)
|
||||
):
|
||||
if item[1] == "album":
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['ParentId'] = item[0]
|
||||
temp_obj["ParentId"] = item[0]
|
||||
|
||||
if not self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
|
||||
self.remove_album(temp_obj['ParentId'], obj['Id'])
|
||||
if not self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
|
||||
):
|
||||
self.remove_album(temp_obj["ParentId"], obj["Id"])
|
||||
|
||||
elif obj['Media'] == 'album':
|
||||
obj['ParentId'] = obj['KodiId']
|
||||
elif obj["Media"] == "album":
|
||||
obj["ParentId"] = obj["KodiId"]
|
||||
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_song_obj)):
|
||||
self.remove_song(song[1], obj['Id'])
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(obj, QUEM.get_item_by_parent_song_obj)
|
||||
):
|
||||
self.remove_song(song[1], obj["Id"])
|
||||
else:
|
||||
self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_song_obj))
|
||||
self.jellyfin_db.remove_items_by_parent_id(
|
||||
*values(obj, QUEM.delete_item_by_parent_song_obj)
|
||||
)
|
||||
|
||||
self.remove_album(obj['KodiId'], obj['Id'])
|
||||
self.remove_album(obj["KodiId"], obj["Id"])
|
||||
|
||||
elif obj['Media'] == 'artist':
|
||||
obj['ParentId'] = obj['KodiId']
|
||||
elif obj["Media"] == "artist":
|
||||
obj["ParentId"] = obj["KodiId"]
|
||||
|
||||
for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)):
|
||||
for album in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(obj, QUEM.get_item_by_parent_album_obj)
|
||||
):
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['ParentId'] = album[1]
|
||||
temp_obj["ParentId"] = album[1]
|
||||
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
|
||||
self.remove_song(song[1], obj['Id'])
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
|
||||
):
|
||||
self.remove_song(song[1], obj["Id"])
|
||||
else:
|
||||
self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_song_obj))
|
||||
self.jellyfin_db.remove_items_by_parent_id(*values(temp_obj, QUEM.delete_item_by_parent_artist_obj))
|
||||
self.remove_album(temp_obj['ParentId'], obj['Id'])
|
||||
self.jellyfin_db.remove_items_by_parent_id(
|
||||
*values(temp_obj, QUEM.delete_item_by_parent_song_obj)
|
||||
)
|
||||
self.jellyfin_db.remove_items_by_parent_id(
|
||||
*values(temp_obj, QUEM.delete_item_by_parent_artist_obj)
|
||||
)
|
||||
self.remove_album(temp_obj["ParentId"], obj["Id"])
|
||||
else:
|
||||
self.jellyfin_db.remove_items_by_parent_id(*values(obj, QUEM.delete_item_by_parent_album_obj))
|
||||
self.jellyfin_db.remove_items_by_parent_id(
|
||||
*values(obj, QUEM.delete_item_by_parent_album_obj)
|
||||
)
|
||||
|
||||
self.remove_artist(obj['KodiId'], obj['Id'])
|
||||
self.remove_artist(obj["KodiId"], obj["Id"])
|
||||
|
||||
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
|
||||
|
||||
@ -547,29 +614,31 @@ class Music(KodiDb):
|
||||
|
||||
@jellyfin_item
|
||||
def get_child(self, item_id, e_item):
|
||||
|
||||
''' Get all child elements from tv show jellyfin id.
|
||||
'''
|
||||
obj = {'Id': item_id}
|
||||
"""Get all child elements from tv show jellyfin id."""
|
||||
obj = {"Id": item_id}
|
||||
child = []
|
||||
|
||||
try:
|
||||
obj['KodiId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj['ParentId'] = e_item[3]
|
||||
obj['Media'] = e_item[4]
|
||||
obj["KodiId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
obj["ParentId"] = e_item[3]
|
||||
obj["Media"] = e_item[4]
|
||||
except TypeError:
|
||||
return child
|
||||
|
||||
obj['ParentId'] = obj['KodiId']
|
||||
obj["ParentId"] = obj["KodiId"]
|
||||
|
||||
for album in self.jellyfin_db.get_item_by_parent_id(*values(obj, QUEM.get_item_by_parent_album_obj)):
|
||||
for album in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(obj, QUEM.get_item_by_parent_album_obj)
|
||||
):
|
||||
|
||||
temp_obj = dict(obj)
|
||||
temp_obj['ParentId'] = album[1]
|
||||
temp_obj["ParentId"] = album[1]
|
||||
child.append((album[0],))
|
||||
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(*values(temp_obj, QUEM.get_item_by_parent_song_obj)):
|
||||
for song in self.jellyfin_db.get_item_by_parent_id(
|
||||
*values(temp_obj, QUEM.get_item_by_parent_song_obj)
|
||||
):
|
||||
child.append((song[0],))
|
||||
|
||||
return child
|
||||
|
@ -43,24 +43,25 @@ class MusicVideos(KodiDb):
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def musicvideo(self, item, e_item):
|
||||
"""If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
|
||||
''' If item does not exist, entry will be added.
|
||||
If item exists, entry will be updated.
|
||||
|
||||
If we don't get the track number from Jellyfin, see if we can infer it
|
||||
from the sortname attribute.
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
If we don't get the track number from Jellyfin, see if we can infer it
|
||||
from the sortname attribute.
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'MusicVideo')
|
||||
obj = self.objects.map(item, "MusicVideo")
|
||||
update = True
|
||||
|
||||
try:
|
||||
obj['MvideoId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj['PathId'] = e_item[2]
|
||||
obj['LibraryId'] = e_item[6]
|
||||
obj['LibraryName'] = self.jellyfin_db.get_view_name(obj['LibraryId'])
|
||||
obj["MvideoId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
obj["PathId"] = e_item[2]
|
||||
obj["LibraryId"] = e_item[6]
|
||||
obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"])
|
||||
except TypeError:
|
||||
update = False
|
||||
|
||||
@ -69,67 +70,80 @@ class MusicVideos(KodiDb):
|
||||
# This item doesn't belong to a whitelisted library
|
||||
return
|
||||
|
||||
LOG.debug("MvideoId for %s not found", obj['Id'])
|
||||
obj['MvideoId'] = self.create_entry()
|
||||
obj['LibraryId'] = library['Id']
|
||||
obj['LibraryName'] = library['Name']
|
||||
LOG.debug("MvideoId for %s not found", obj["Id"])
|
||||
obj["MvideoId"] = self.create_entry()
|
||||
obj["LibraryId"] = library["Id"]
|
||||
obj["LibraryName"] = library["Name"]
|
||||
else:
|
||||
if self.get(*values(obj, QU.get_musicvideo_obj)) is None:
|
||||
|
||||
update = False
|
||||
LOG.info("MvideoId %s missing from kodi. repairing the entry.", obj['MvideoId'])
|
||||
LOG.info(
|
||||
"MvideoId %s missing from kodi. repairing the entry.",
|
||||
obj["MvideoId"],
|
||||
)
|
||||
|
||||
if (obj.get('ProductionYear') or 0) > 9999:
|
||||
obj['ProductionYear'] = int(str(obj['ProductionYear'])[:4])
|
||||
if (obj.get("ProductionYear") or 0) > 9999:
|
||||
obj["ProductionYear"] = int(str(obj["ProductionYear"])[:4])
|
||||
|
||||
if (obj.get('Year') or 0) > 9999:
|
||||
obj['Year'] = int(str(obj['Year'])[:4])
|
||||
if (obj.get("Year") or 0) > 9999:
|
||||
obj["Year"] = int(str(obj["Year"])[:4])
|
||||
|
||||
obj['Path'] = API.get_file_path(obj['Path'])
|
||||
obj['Genres'] = obj['Genres'] or []
|
||||
obj['ArtistItems'] = obj['ArtistItems'] or []
|
||||
obj['Studios'] = [API.validate_studio(studio) for studio in (obj['Studios'] or [])]
|
||||
obj['Plot'] = API.get_overview(obj['Plot'])
|
||||
obj['DateAdded'] = Local(obj['DateAdded']).split('.')[0].replace('T', " ")
|
||||
obj['DatePlayed'] = None if not obj['DatePlayed'] else Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
|
||||
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
|
||||
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
||||
obj['Premiere'] = Local(obj['Premiere']) if obj['Premiere'] else datetime.date(obj['Year'] or 1970, 1, 1)
|
||||
obj['Genre'] = " / ".join(obj['Genres'])
|
||||
obj['Studio'] = " / ".join(obj['Studios'])
|
||||
obj['Artists'] = " / ".join(obj['Artists'] or [])
|
||||
obj['Directors'] = " / ".join(obj['Directors'] or [])
|
||||
obj['Video'] = API.video_streams(obj['Video'] or [], obj['Container'])
|
||||
obj['Audio'] = API.audio_streams(obj['Audio'] or [])
|
||||
obj['Streams'] = API.media_streams(obj['Video'], obj['Audio'], obj['Subtitles'])
|
||||
obj['Artwork'] = API.get_all_artwork(self.objects.map(item, 'Artwork'))
|
||||
obj["Path"] = API.get_file_path(obj["Path"])
|
||||
obj["Genres"] = obj["Genres"] or []
|
||||
obj["ArtistItems"] = obj["ArtistItems"] or []
|
||||
obj["Studios"] = [
|
||||
API.validate_studio(studio) for studio in (obj["Studios"] or [])
|
||||
]
|
||||
obj["Plot"] = API.get_overview(obj["Plot"])
|
||||
obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ")
|
||||
obj["DatePlayed"] = (
|
||||
None
|
||||
if not obj["DatePlayed"]
|
||||
else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
)
|
||||
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
|
||||
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
|
||||
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
|
||||
obj["Premiere"] = (
|
||||
Local(obj["Premiere"])
|
||||
if obj["Premiere"]
|
||||
else datetime.date(obj["Year"] or 1970, 1, 1)
|
||||
)
|
||||
obj["Genre"] = " / ".join(obj["Genres"])
|
||||
obj["Studio"] = " / ".join(obj["Studios"])
|
||||
obj["Artists"] = " / ".join(obj["Artists"] or [])
|
||||
obj["Directors"] = " / ".join(obj["Directors"] or [])
|
||||
obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"])
|
||||
obj["Audio"] = API.audio_streams(obj["Audio"] or [])
|
||||
obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"])
|
||||
obj["Artwork"] = API.get_all_artwork(self.objects.map(item, "Artwork"))
|
||||
|
||||
self.get_path_filename(obj)
|
||||
|
||||
if obj['Premiere']:
|
||||
obj['Premiere'] = str(obj['Premiere']).split('.')[0].replace('T', " ")
|
||||
if obj["Premiere"]:
|
||||
obj["Premiere"] = str(obj["Premiere"]).split(".")[0].replace("T", " ")
|
||||
|
||||
for artist in obj['ArtistItems']:
|
||||
artist['Type'] = "Artist"
|
||||
for artist in obj["ArtistItems"]:
|
||||
artist["Type"] = "Artist"
|
||||
|
||||
obj['People'] = obj['People'] or [] + obj['ArtistItems']
|
||||
obj['People'] = API.get_people_artwork(obj['People'])
|
||||
obj["People"] = obj["People"] or [] + obj["ArtistItems"]
|
||||
obj["People"] = API.get_people_artwork(obj["People"])
|
||||
|
||||
if obj['Index'] is None and obj['SortTitle'] is not None:
|
||||
search = re.search(r'^\d+\s?', obj['SortTitle'])
|
||||
if obj["Index"] is None and obj["SortTitle"] is not None:
|
||||
search = re.search(r"^\d+\s?", obj["SortTitle"])
|
||||
|
||||
if search:
|
||||
obj['Index'] = search.group()
|
||||
obj["Index"] = search.group()
|
||||
|
||||
tags = []
|
||||
tags.extend(obj['Tags'] or [])
|
||||
tags.append(obj['LibraryName'])
|
||||
tags.extend(obj["Tags"] or [])
|
||||
tags.append(obj["LibraryName"])
|
||||
|
||||
if obj['Favorite']:
|
||||
tags.append('Favorite musicvideos')
|
||||
if obj["Favorite"]:
|
||||
tags.append("Favorite musicvideos")
|
||||
|
||||
obj['Tags'] = tags
|
||||
obj["Tags"] = tags
|
||||
|
||||
if update:
|
||||
self.musicvideo_update(obj)
|
||||
@ -144,106 +158,129 @@ class MusicVideos(KodiDb):
|
||||
self.add_playstate(*values(obj, QU.add_bookmark_obj))
|
||||
self.add_people(*values(obj, QU.add_people_mvideo_obj))
|
||||
self.add_streams(*values(obj, QU.add_streams_obj))
|
||||
self.artwork.add(obj['Artwork'], obj['MvideoId'], "musicvideo")
|
||||
self.item_ids.append(obj['Id'])
|
||||
self.artwork.add(obj["Artwork"], obj["MvideoId"], "musicvideo")
|
||||
self.item_ids.append(obj["Id"])
|
||||
|
||||
return not update
|
||||
|
||||
def musicvideo_add(self, obj):
|
||||
|
||||
''' Add object to kodi.
|
||||
'''
|
||||
obj['PathId'] = self.add_path(*values(obj, QU.add_path_obj))
|
||||
obj['FileId'] = self.add_file(*values(obj, QU.add_file_obj))
|
||||
"""Add object to kodi."""
|
||||
obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj))
|
||||
obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj))
|
||||
|
||||
self.add(*values(obj, QU.add_musicvideo_obj))
|
||||
self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_mvideo_obj))
|
||||
LOG.debug("ADD mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"ADD mvideo [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["FileId"],
|
||||
obj["MvideoId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def musicvideo_update(self, obj):
|
||||
|
||||
''' Update object to kodi.
|
||||
'''
|
||||
"""Update object to kodi."""
|
||||
self.update(*values(obj, QU.update_musicvideo_obj))
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("UPDATE mvideo [%s/%s/%s] %s: %s", obj['PathId'], obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"UPDATE mvideo [%s/%s/%s] %s: %s",
|
||||
obj["PathId"],
|
||||
obj["FileId"],
|
||||
obj["MvideoId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
def get_path_filename(self, obj):
|
||||
|
||||
''' Get the path and filename and build it into protocol://path
|
||||
'''
|
||||
obj['Filename'] = obj['Path'].rsplit('\\', 1)[1] if '\\' in obj['Path'] else obj['Path'].rsplit('/', 1)[1]
|
||||
"""Get the path and filename and build it into protocol://path"""
|
||||
obj["Filename"] = (
|
||||
obj["Path"].rsplit("\\", 1)[1]
|
||||
if "\\" in obj["Path"]
|
||||
else obj["Path"].rsplit("/", 1)[1]
|
||||
)
|
||||
|
||||
if self.direct_path:
|
||||
|
||||
if not validate(obj['Path']):
|
||||
if not validate(obj["Path"]):
|
||||
raise PathValidationException("Failed to validate path. User stopped.")
|
||||
|
||||
obj['Path'] = obj['Path'].replace(obj['Filename'], "")
|
||||
obj["Path"] = obj["Path"].replace(obj["Filename"], "")
|
||||
|
||||
else:
|
||||
obj['Path'] = "plugin://plugin.video.jellyfin/%s/" % obj['LibraryId']
|
||||
obj["Path"] = "plugin://plugin.video.jellyfin/%s/" % obj["LibraryId"]
|
||||
params = {
|
||||
'filename': py2_encode(obj['Filename'], 'utf-8'),
|
||||
'id': obj['Id'],
|
||||
'dbid': obj['MvideoId'],
|
||||
'mode': "play"
|
||||
"filename": py2_encode(obj["Filename"], "utf-8"),
|
||||
"id": obj["Id"],
|
||||
"dbid": obj["MvideoId"],
|
||||
"mode": "play",
|
||||
}
|
||||
obj['Filename'] = "%s?%s" % (obj['Path'], urlencode(params))
|
||||
obj["Filename"] = "%s?%s" % (obj["Path"], urlencode(params))
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def userdata(self, item, e_item):
|
||||
|
||||
''' This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
'''
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address']
|
||||
"""This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
|
||||
Poster with progress bar
|
||||
"""
|
||||
server_address = self.server.auth.get_server_info(self.server.auth.server_id)[
|
||||
"address"
|
||||
]
|
||||
API = api.API(item, server_address)
|
||||
obj = self.objects.map(item, 'MusicVideoUserData')
|
||||
obj = self.objects.map(item, "MusicVideoUserData")
|
||||
|
||||
try:
|
||||
obj['MvideoId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj["MvideoId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
obj['Resume'] = API.adjust_resume((obj['Resume'] or 0) / 10000000.0)
|
||||
obj['Runtime'] = round(float((obj['Runtime'] or 0) / 10000000.0), 6)
|
||||
obj['PlayCount'] = API.get_playcount(obj['Played'], obj['PlayCount'])
|
||||
obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0)
|
||||
obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6)
|
||||
obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"])
|
||||
|
||||
if obj['DatePlayed']:
|
||||
obj['DatePlayed'] = Local(obj['DatePlayed']).split('.')[0].replace('T', " ")
|
||||
if obj["DatePlayed"]:
|
||||
obj["DatePlayed"] = Local(obj["DatePlayed"]).split(".")[0].replace("T", " ")
|
||||
|
||||
if obj['Favorite']:
|
||||
if obj["Favorite"]:
|
||||
self.get_tag(*values(obj, QU.get_tag_mvideo_obj))
|
||||
else:
|
||||
self.remove_tag(*values(obj, QU.delete_tag_mvideo_obj))
|
||||
|
||||
self.add_playstate(*values(obj, QU.add_bookmark_obj))
|
||||
self.jellyfin_db.update_reference(*values(obj, QUEM.update_reference_obj))
|
||||
LOG.debug("USERDATA mvideo [%s/%s] %s: %s", obj['FileId'], obj['MvideoId'], obj['Id'], obj['Title'])
|
||||
LOG.debug(
|
||||
"USERDATA mvideo [%s/%s] %s: %s",
|
||||
obj["FileId"],
|
||||
obj["MvideoId"],
|
||||
obj["Id"],
|
||||
obj["Title"],
|
||||
)
|
||||
|
||||
@stop
|
||||
@jellyfin_item
|
||||
def remove(self, item_id, e_item):
|
||||
|
||||
''' Remove mvideoid, fileid, pathid, jellyfin reference.
|
||||
'''
|
||||
obj = {'Id': item_id}
|
||||
"""Remove mvideoid, fileid, pathid, jellyfin reference."""
|
||||
obj = {"Id": item_id}
|
||||
|
||||
try:
|
||||
obj['MvideoId'] = e_item[0]
|
||||
obj['FileId'] = e_item[1]
|
||||
obj['PathId'] = e_item[2]
|
||||
obj["MvideoId"] = e_item[0]
|
||||
obj["FileId"] = e_item[1]
|
||||
obj["PathId"] = e_item[2]
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
self.artwork.delete(obj['MvideoId'], "musicvideo")
|
||||
self.artwork.delete(obj["MvideoId"], "musicvideo")
|
||||
self.delete(*values(obj, QU.delete_musicvideo_obj))
|
||||
|
||||
if self.direct_path:
|
||||
self.remove_path(*values(obj, QU.delete_path_obj))
|
||||
|
||||
self.jellyfin_db.remove_item(*values(obj, QUEM.delete_item_obj))
|
||||
LOG.debug("DELETE musicvideo %s [%s/%s] %s", obj['MvideoId'], obj['PathId'], obj['FileId'], obj['Id'])
|
||||
LOG.debug(
|
||||
"DELETE musicvideo %s [%s/%s] %s",
|
||||
obj["MvideoId"],
|
||||
obj["PathId"],
|
||||
obj["FileId"],
|
||||
obj["Id"],
|
||||
)
|
||||
|
@ -23,35 +23,30 @@ class Objects(object):
|
||||
_shared_state = {}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
''' Hold all persistent data here.
|
||||
'''
|
||||
"""Hold all persistent data here."""
|
||||
|
||||
self.__dict__ = self._shared_state
|
||||
|
||||
def mapping(self):
|
||||
|
||||
''' Load objects mapping.
|
||||
'''
|
||||
"""Load objects mapping."""
|
||||
file_dir = os.path.dirname(ensure_text(__file__, get_filesystem_encoding()))
|
||||
|
||||
with open(os.path.join(file_dir, 'obj_map.json')) as infile:
|
||||
with open(os.path.join(file_dir, "obj_map.json")) as infile:
|
||||
self.objects = json.load(infile)
|
||||
|
||||
def map(self, item, mapping_name):
|
||||
"""Syntax to traverse the item dictionary.
|
||||
This of the query almost as a url.
|
||||
|
||||
''' Syntax to traverse the item dictionary.
|
||||
This of the query almost as a url.
|
||||
Item is the Jellyfin item json object structure
|
||||
|
||||
Item is the Jellyfin item json object structure
|
||||
|
||||
",": each element will be used as a fallback until a value is found.
|
||||
"?": split filters and key name from the query part, i.e. MediaSources/0?$Name
|
||||
"$": lead the key name with $. Only one key value can be requested per element.
|
||||
":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name
|
||||
MediaStreams is a list.
|
||||
"/": indicates where to go directly
|
||||
'''
|
||||
",": each element will be used as a fallback until a value is found.
|
||||
"?": split filters and key name from the query part, i.e. MediaSources/0?$Name
|
||||
"$": lead the key name with $. Only one key value can be requested per element.
|
||||
":": indicates it's a list of elements [], i.e. MediaSources/0/MediaStreams:?$Name
|
||||
MediaStreams is a list.
|
||||
"/": indicates where to go directly
|
||||
"""
|
||||
self.mapped_item = {}
|
||||
|
||||
if not mapping_name:
|
||||
@ -62,7 +57,7 @@ class Objects(object):
|
||||
for key, value in iteritems(mapping):
|
||||
|
||||
self.mapped_item[key] = None
|
||||
params = value.split(',')
|
||||
params = value.split(",")
|
||||
|
||||
for param in params:
|
||||
|
||||
@ -71,19 +66,19 @@ class Objects(object):
|
||||
obj_key = ""
|
||||
obj_filters = {}
|
||||
|
||||
if '?' in obj_param:
|
||||
if "?" in obj_param:
|
||||
|
||||
if '$' in obj_param:
|
||||
obj_param, obj_key = obj_param.rsplit('$', 1)
|
||||
if "$" in obj_param:
|
||||
obj_param, obj_key = obj_param.rsplit("$", 1)
|
||||
|
||||
obj_param, filters = obj_param.rsplit('?', 1)
|
||||
obj_param, filters = obj_param.rsplit("?", 1)
|
||||
|
||||
if filters:
|
||||
for filter in filters.split('&'):
|
||||
filter_key, filter_value = filter.split('=')
|
||||
for filter in filters.split("&"):
|
||||
filter_key, filter_value = filter.split("=")
|
||||
obj_filters[filter_key] = filter_value
|
||||
|
||||
if ':' in obj_param:
|
||||
if ":" in obj_param:
|
||||
result = []
|
||||
|
||||
for d in self.__recursiveloop__(obj, obj_param):
|
||||
@ -94,7 +89,7 @@ class Objects(object):
|
||||
obj = result
|
||||
obj_filters = {}
|
||||
|
||||
elif '/' in obj_param:
|
||||
elif "/" in obj_param:
|
||||
obj = self.__recursive__(obj, obj_param)
|
||||
|
||||
elif obj is item and obj is not None:
|
||||
@ -107,21 +102,31 @@ class Objects(object):
|
||||
continue
|
||||
|
||||
if obj_key:
|
||||
obj = [d[obj_key] for d in obj if d.get(obj_key)] if type(obj) == list else obj.get(obj_key)
|
||||
obj = (
|
||||
[d[obj_key] for d in obj if d.get(obj_key)]
|
||||
if type(obj) == list
|
||||
else obj.get(obj_key)
|
||||
)
|
||||
|
||||
self.mapped_item[key] = obj
|
||||
break
|
||||
|
||||
if not mapping_name.startswith('Browse') and not mapping_name.startswith('Artwork') and not mapping_name.startswith('UpNext'):
|
||||
if (
|
||||
not mapping_name.startswith("Browse")
|
||||
and not mapping_name.startswith("Artwork")
|
||||
and not mapping_name.startswith("UpNext")
|
||||
):
|
||||
|
||||
self.mapped_item['ProviderName'] = self.objects.get('%sProviderName' % mapping_name)
|
||||
self.mapped_item['Checksum'] = json.dumps(item['UserData'])
|
||||
self.mapped_item["ProviderName"] = self.objects.get(
|
||||
"%sProviderName" % mapping_name
|
||||
)
|
||||
self.mapped_item["Checksum"] = json.dumps(item["UserData"])
|
||||
|
||||
return self.mapped_item
|
||||
|
||||
def __recursiveloop__(self, obj, keys):
|
||||
|
||||
first, rest = keys.split(':', 1)
|
||||
first, rest = keys.split(":", 1)
|
||||
obj = self.__recursive__(obj, first)
|
||||
|
||||
if obj:
|
||||
@ -133,7 +138,7 @@ class Objects(object):
|
||||
|
||||
def __recursive__(self, obj, keys):
|
||||
|
||||
for string in keys.split('/'):
|
||||
for string in keys.split("/"):
|
||||
|
||||
if not obj:
|
||||
return
|
||||
@ -150,10 +155,10 @@ class Objects(object):
|
||||
|
||||
inverse = False
|
||||
|
||||
if value.startswith('!'):
|
||||
if value.startswith("!"):
|
||||
|
||||
inverse = True
|
||||
value = value.split('!', 1)[1]
|
||||
value = value.split("!", 1)[1]
|
||||
|
||||
if value.lower() == "null":
|
||||
value = None
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,8 +14,8 @@ LOG = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_grouped_set():
|
||||
|
||||
''' Get if boxsets should be grouped
|
||||
'''
|
||||
result = JSONRPC('Settings.GetSettingValue').execute({'setting': "videolibrary.groupmoviesets"})
|
||||
return result.get('result', {}).get('value', False)
|
||||
"""Get if boxsets should be grouped"""
|
||||
result = JSONRPC("Settings.GetSettingValue").execute(
|
||||
{"setting": "videolibrary.groupmoviesets"}
|
||||
)
|
||||
return result.get("result", {}).get("value", False)
|
||||
|
@ -44,11 +44,10 @@ class Player(xbmc.Player):
|
||||
return file in self.played
|
||||
|
||||
def onPlayBackStarted(self):
|
||||
|
||||
''' We may need to wait for info to be set in kodi monitor.
|
||||
Accounts for scenario where Kodi starts playback and exits immediately.
|
||||
First, ensure previous playback terminated correctly in Jellyfin.
|
||||
'''
|
||||
"""We may need to wait for info to be set in kodi monitor.
|
||||
Accounts for scenario where Kodi starts playback and exits immediately.
|
||||
First, ensure previous playback terminated correctly in Jellyfin.
|
||||
"""
|
||||
self.stop_playback()
|
||||
self.up_next = False
|
||||
count = 0
|
||||
@ -69,11 +68,11 @@ class Player(xbmc.Player):
|
||||
if monitor.waitForAbort(1):
|
||||
return
|
||||
else:
|
||||
LOG.info('Cancel playback report')
|
||||
LOG.info("Cancel playback report")
|
||||
|
||||
return
|
||||
|
||||
items = window('jellyfin_play.json')
|
||||
items = window("jellyfin_play.json")
|
||||
item = None
|
||||
|
||||
while not items:
|
||||
@ -81,7 +80,7 @@ class Player(xbmc.Player):
|
||||
if monitor.waitForAbort(2):
|
||||
return
|
||||
|
||||
items = window('jellyfin_play.json')
|
||||
items = window("jellyfin_play.json")
|
||||
count += 1
|
||||
|
||||
if count == 20:
|
||||
@ -90,51 +89,49 @@ class Player(xbmc.Player):
|
||||
return
|
||||
|
||||
for item in items:
|
||||
if item['Path'] == current_file:
|
||||
if item["Path"] == current_file:
|
||||
items.pop(items.index(item))
|
||||
|
||||
break
|
||||
else:
|
||||
item = items.pop(0)
|
||||
|
||||
window('jellyfin_play.json', items)
|
||||
window("jellyfin_play.json", items)
|
||||
|
||||
self.set_item(current_file, item)
|
||||
data = {
|
||||
'QueueableMediaTypes': "Video,Audio",
|
||||
'CanSeek': True,
|
||||
'ItemId': item['Id'],
|
||||
'MediaSourceId': item['MediaSourceId'],
|
||||
'PlayMethod': item['PlayMethod'],
|
||||
'VolumeLevel': item['Volume'],
|
||||
'PositionTicks': int(item['CurrentPosition'] * 10000000),
|
||||
'IsPaused': item['Paused'],
|
||||
'IsMuted': item['Muted'],
|
||||
'PlaySessionId': item['PlaySessionId'],
|
||||
'AudioStreamIndex': item['AudioStreamIndex'],
|
||||
'SubtitleStreamIndex': item['SubtitleStreamIndex']
|
||||
"QueueableMediaTypes": "Video,Audio",
|
||||
"CanSeek": True,
|
||||
"ItemId": item["Id"],
|
||||
"MediaSourceId": item["MediaSourceId"],
|
||||
"PlayMethod": item["PlayMethod"],
|
||||
"VolumeLevel": item["Volume"],
|
||||
"PositionTicks": int(item["CurrentPosition"] * 10000000),
|
||||
"IsPaused": item["Paused"],
|
||||
"IsMuted": item["Muted"],
|
||||
"PlaySessionId": item["PlaySessionId"],
|
||||
"AudioStreamIndex": item["AudioStreamIndex"],
|
||||
"SubtitleStreamIndex": item["SubtitleStreamIndex"],
|
||||
}
|
||||
item['Server'].jellyfin.session_playing(data)
|
||||
window('jellyfin.skip.%s.bool' % item['Id'], True)
|
||||
item["Server"].jellyfin.session_playing(data)
|
||||
window("jellyfin.skip.%s.bool" % item["Id"], True)
|
||||
|
||||
if monitor.waitForAbort(2):
|
||||
return
|
||||
|
||||
if item['PlayOption'] == 'Addon':
|
||||
self.set_audio_subs(item['AudioStreamIndex'], item['SubtitleStreamIndex'])
|
||||
if item["PlayOption"] == "Addon":
|
||||
self.set_audio_subs(item["AudioStreamIndex"], item["SubtitleStreamIndex"])
|
||||
|
||||
def set_item(self, file, item):
|
||||
|
||||
''' Set playback information.
|
||||
'''
|
||||
"""Set playback information."""
|
||||
try:
|
||||
item['Runtime'] = int(item['Runtime'])
|
||||
item["Runtime"] = int(item["Runtime"])
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
item['Runtime'] = int(self.getTotalTime())
|
||||
LOG.info("Runtime is missing, Kodi runtime: %s" % item['Runtime'])
|
||||
item["Runtime"] = int(self.getTotalTime())
|
||||
LOG.info("Runtime is missing, Kodi runtime: %s" % item["Runtime"])
|
||||
except Exception:
|
||||
item['Runtime'] = 0
|
||||
item["Runtime"] = 0
|
||||
LOG.info("Runtime is missing, Using Zero")
|
||||
|
||||
try:
|
||||
@ -142,22 +139,26 @@ class Player(xbmc.Player):
|
||||
except Exception: # at this point we should be playing and if not then bail out
|
||||
return
|
||||
|
||||
result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]})
|
||||
result = result.get('result', {})
|
||||
volume = result.get('volume')
|
||||
muted = result.get('muted')
|
||||
result = JSONRPC("Application.GetProperties").execute(
|
||||
{"properties": ["volume", "muted"]}
|
||||
)
|
||||
result = result.get("result", {})
|
||||
volume = result.get("volume")
|
||||
muted = result.get("muted")
|
||||
|
||||
item.update({
|
||||
'File': file,
|
||||
'CurrentPosition': item.get('CurrentPosition') or int(seektime),
|
||||
'Muted': muted,
|
||||
'Volume': volume,
|
||||
'Server': Jellyfin(item['ServerId']).get_client(),
|
||||
'Paused': False
|
||||
})
|
||||
item.update(
|
||||
{
|
||||
"File": file,
|
||||
"CurrentPosition": item.get("CurrentPosition") or int(seektime),
|
||||
"Muted": muted,
|
||||
"Volume": volume,
|
||||
"Server": Jellyfin(item["ServerId"]).get_client(),
|
||||
"Paused": False,
|
||||
}
|
||||
)
|
||||
|
||||
self.played[file] = item
|
||||
LOG.info("-->[ play/%s ] %s", item['Id'], item)
|
||||
LOG.info("-->[ play/%s ] %s", item["Id"], item)
|
||||
|
||||
def set_audio_subs(self, audio=None, subtitle=None):
|
||||
if audio:
|
||||
@ -165,15 +166,15 @@ class Player(xbmc.Player):
|
||||
if subtitle:
|
||||
subtitle = int(subtitle)
|
||||
|
||||
''' Only for after playback started
|
||||
'''
|
||||
""" Only for after playback started
|
||||
"""
|
||||
LOG.info("Setting audio: %s subs: %s", audio, subtitle)
|
||||
current_file = self.get_playing_file()
|
||||
|
||||
if self.is_playing_file(current_file):
|
||||
|
||||
item = self.get_file_info(current_file)
|
||||
mapping = item['SubsMapping']
|
||||
mapping = item["SubsMapping"]
|
||||
|
||||
if audio and len(self.getAvailableAudioStreams()) > 1:
|
||||
self.setAudioStream(audio - 1)
|
||||
@ -200,82 +201,90 @@ class Player(xbmc.Player):
|
||||
def detect_audio_subs(self, item):
|
||||
|
||||
params = {
|
||||
'playerid': 1,
|
||||
'properties': ["currentsubtitle", "currentaudiostream", "subtitleenabled"]
|
||||
"playerid": 1,
|
||||
"properties": ["currentsubtitle", "currentaudiostream", "subtitleenabled"],
|
||||
}
|
||||
result = JSONRPC('Player.GetProperties').execute(params)
|
||||
result = result.get('result')
|
||||
result = JSONRPC("Player.GetProperties").execute(params)
|
||||
result = result.get("result")
|
||||
|
||||
try: # Audio tracks
|
||||
audio = result['currentaudiostream']['index']
|
||||
audio = result["currentaudiostream"]["index"]
|
||||
except (KeyError, TypeError):
|
||||
audio = 0
|
||||
|
||||
try: # Subtitles tracks
|
||||
subs = result['currentsubtitle']['index']
|
||||
subs = result["currentsubtitle"]["index"]
|
||||
except (KeyError, TypeError):
|
||||
subs = 0
|
||||
|
||||
try: # If subtitles are enabled
|
||||
subs_enabled = result['subtitleenabled']
|
||||
subs_enabled = result["subtitleenabled"]
|
||||
except (KeyError, TypeError):
|
||||
subs_enabled = False
|
||||
|
||||
item['AudioStreamIndex'] = audio + 1
|
||||
item["AudioStreamIndex"] = audio + 1
|
||||
|
||||
if not subs_enabled or not len(self.getAvailableSubtitleStreams()):
|
||||
item['SubtitleStreamIndex'] = None
|
||||
item["SubtitleStreamIndex"] = None
|
||||
|
||||
return
|
||||
|
||||
mapping = item['SubsMapping']
|
||||
mapping = item["SubsMapping"]
|
||||
tracks = len(self.getAvailableAudioStreams())
|
||||
|
||||
if mapping:
|
||||
if str(subs) in mapping:
|
||||
item['SubtitleStreamIndex'] = mapping[str(subs)]
|
||||
item["SubtitleStreamIndex"] = mapping[str(subs)]
|
||||
else:
|
||||
item['SubtitleStreamIndex'] = subs - len(mapping) + tracks + 1
|
||||
item["SubtitleStreamIndex"] = subs - len(mapping) + tracks + 1
|
||||
else:
|
||||
item['SubtitleStreamIndex'] = subs + tracks + 1
|
||||
item["SubtitleStreamIndex"] = subs + tracks + 1
|
||||
|
||||
def next_up(self):
|
||||
|
||||
item = self.get_file_info(self.get_playing_file())
|
||||
objects = Objects()
|
||||
|
||||
if item['Type'] != 'Episode' or not item.get('CurrentEpisode'):
|
||||
if item["Type"] != "Episode" or not item.get("CurrentEpisode"):
|
||||
return
|
||||
|
||||
next_items = item['Server'].jellyfin.get_adjacent_episodes(item['CurrentEpisode']['tvshowid'], item['Id'])
|
||||
next_items = item["Server"].jellyfin.get_adjacent_episodes(
|
||||
item["CurrentEpisode"]["tvshowid"], item["Id"]
|
||||
)
|
||||
|
||||
for index, next_item in enumerate(next_items['Items']):
|
||||
if next_item['Id'] == item['Id']:
|
||||
for index, next_item in enumerate(next_items["Items"]):
|
||||
if next_item["Id"] == item["Id"]:
|
||||
|
||||
try:
|
||||
next_item = next_items['Items'][index + 1]
|
||||
next_item = next_items["Items"][index + 1]
|
||||
except IndexError:
|
||||
LOG.warning("No next up episode.")
|
||||
|
||||
return
|
||||
|
||||
break
|
||||
server_address = item['Server'].auth.get_server_info(item['Server'].auth.server_id)['address']
|
||||
server_address = item["Server"].auth.get_server_info(
|
||||
item["Server"].auth.server_id
|
||||
)["address"]
|
||||
API = api.API(next_item, server_address)
|
||||
data = objects.map(next_item, "UpNext")
|
||||
artwork = API.get_all_artwork(objects.map(next_item, 'ArtworkParent'), True)
|
||||
data['art'] = {
|
||||
'tvshow.poster': artwork.get('Series.Primary'),
|
||||
'tvshow.fanart': None,
|
||||
'thumb': artwork.get('Primary')
|
||||
artwork = API.get_all_artwork(objects.map(next_item, "ArtworkParent"), True)
|
||||
data["art"] = {
|
||||
"tvshow.poster": artwork.get("Series.Primary"),
|
||||
"tvshow.fanart": None,
|
||||
"thumb": artwork.get("Primary"),
|
||||
}
|
||||
if artwork['Backdrop']:
|
||||
data['art']['tvshow.fanart'] = artwork['Backdrop'][0]
|
||||
if artwork["Backdrop"]:
|
||||
data["art"]["tvshow.fanart"] = artwork["Backdrop"][0]
|
||||
|
||||
next_info = {
|
||||
'play_info': {'ItemIds': [data['episodeid']], 'ServerId': item['ServerId'], 'PlayCommand': 'PlayNow'},
|
||||
'current_episode': item['CurrentEpisode'],
|
||||
'next_episode': data
|
||||
"play_info": {
|
||||
"ItemIds": [data["episodeid"]],
|
||||
"ServerId": item["ServerId"],
|
||||
"PlayCommand": "PlayNow",
|
||||
},
|
||||
"current_episode": item["CurrentEpisode"],
|
||||
"next_episode": data,
|
||||
}
|
||||
|
||||
LOG.info("--[ next up ] %s", next_info)
|
||||
@ -286,7 +295,7 @@ class Player(xbmc.Player):
|
||||
|
||||
if self.is_playing_file(current_file):
|
||||
|
||||
self.get_file_info(current_file)['Paused'] = True
|
||||
self.get_file_info(current_file)["Paused"] = True
|
||||
self.report_playback()
|
||||
LOG.debug("-->[ paused ]")
|
||||
|
||||
@ -295,24 +304,21 @@ class Player(xbmc.Player):
|
||||
|
||||
if self.is_playing_file(current_file):
|
||||
|
||||
self.get_file_info(current_file)['Paused'] = False
|
||||
self.get_file_info(current_file)["Paused"] = False
|
||||
self.report_playback()
|
||||
LOG.debug("--<[ paused ]")
|
||||
|
||||
def onPlayBackSeek(self, time, seek_offset):
|
||||
|
||||
''' Does not seem to work in Leia??
|
||||
'''
|
||||
"""Does not seem to work in Leia??"""
|
||||
if self.is_playing_file(self.get_playing_file()):
|
||||
|
||||
self.report_playback()
|
||||
LOG.info("--[ seek ]")
|
||||
|
||||
def report_playback(self, report=True):
|
||||
|
||||
''' Report playback progress to jellyfin server.
|
||||
Check if the user seek.
|
||||
'''
|
||||
"""Report playback progress to jellyfin server.
|
||||
Check if the user seek.
|
||||
"""
|
||||
current_file = self.get_playing_file()
|
||||
|
||||
if not self.is_playing_file(current_file):
|
||||
@ -320,25 +326,29 @@ class Player(xbmc.Player):
|
||||
|
||||
item = self.get_file_info(current_file)
|
||||
|
||||
if window('jellyfin.external.bool'):
|
||||
if window("jellyfin.external.bool"):
|
||||
return
|
||||
|
||||
if not report:
|
||||
|
||||
previous = item['CurrentPosition']
|
||||
previous = item["CurrentPosition"]
|
||||
|
||||
try:
|
||||
item['CurrentPosition'] = int(self.getTime())
|
||||
item["CurrentPosition"] = int(self.getTime())
|
||||
except Exception as e:
|
||||
# getTime() raises RuntimeError if nothing is playing
|
||||
LOG.debug("Failed to get playback position: %s", e)
|
||||
return
|
||||
|
||||
if int(item['CurrentPosition']) == 1:
|
||||
if int(item["CurrentPosition"]) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
played = float(item['CurrentPosition'] * 10000000) / int(item['Runtime']) * 100
|
||||
played = (
|
||||
float(item["CurrentPosition"] * 10000000)
|
||||
/ int(item["Runtime"])
|
||||
* 100
|
||||
)
|
||||
except ZeroDivisionError: # Runtime is 0.
|
||||
played = 0
|
||||
|
||||
@ -347,52 +357,48 @@ class Player(xbmc.Player):
|
||||
self.up_next = True
|
||||
self.next_up()
|
||||
|
||||
if (item['CurrentPosition'] - previous) < 30:
|
||||
if (item["CurrentPosition"] - previous) < 30:
|
||||
|
||||
return
|
||||
|
||||
result = JSONRPC('Application.GetProperties').execute({'properties': ["volume", "muted"]})
|
||||
result = result.get('result', {})
|
||||
item['Volume'] = result.get('volume')
|
||||
item['Muted'] = result.get('muted')
|
||||
item['CurrentPosition'] = int(self.getTime())
|
||||
result = JSONRPC("Application.GetProperties").execute(
|
||||
{"properties": ["volume", "muted"]}
|
||||
)
|
||||
result = result.get("result", {})
|
||||
item["Volume"] = result.get("volume")
|
||||
item["Muted"] = result.get("muted")
|
||||
item["CurrentPosition"] = int(self.getTime())
|
||||
self.detect_audio_subs(item)
|
||||
|
||||
data = {
|
||||
'QueueableMediaTypes': "Video,Audio",
|
||||
'CanSeek': True,
|
||||
'ItemId': item['Id'],
|
||||
'MediaSourceId': item['MediaSourceId'],
|
||||
'PlayMethod': item['PlayMethod'],
|
||||
'VolumeLevel': item['Volume'],
|
||||
'PositionTicks': int(item['CurrentPosition'] * 10000000),
|
||||
'IsPaused': item['Paused'],
|
||||
'IsMuted': item['Muted'],
|
||||
'PlaySessionId': item['PlaySessionId'],
|
||||
'AudioStreamIndex': item['AudioStreamIndex'],
|
||||
'SubtitleStreamIndex': item['SubtitleStreamIndex']
|
||||
"QueueableMediaTypes": "Video,Audio",
|
||||
"CanSeek": True,
|
||||
"ItemId": item["Id"],
|
||||
"MediaSourceId": item["MediaSourceId"],
|
||||
"PlayMethod": item["PlayMethod"],
|
||||
"VolumeLevel": item["Volume"],
|
||||
"PositionTicks": int(item["CurrentPosition"] * 10000000),
|
||||
"IsPaused": item["Paused"],
|
||||
"IsMuted": item["Muted"],
|
||||
"PlaySessionId": item["PlaySessionId"],
|
||||
"AudioStreamIndex": item["AudioStreamIndex"],
|
||||
"SubtitleStreamIndex": item["SubtitleStreamIndex"],
|
||||
}
|
||||
item['Server'].jellyfin.session_progress(data)
|
||||
item["Server"].jellyfin.session_progress(data)
|
||||
|
||||
def onPlayBackStopped(self):
|
||||
|
||||
''' Will be called when user stops playing a file.
|
||||
'''
|
||||
window('jellyfin_play', clear=True)
|
||||
"""Will be called when user stops playing a file."""
|
||||
window("jellyfin_play", clear=True)
|
||||
self.stop_playback()
|
||||
LOG.info("--<[ playback ]")
|
||||
|
||||
def onPlayBackEnded(self):
|
||||
|
||||
''' Will be called when kodi stops playing a file.
|
||||
'''
|
||||
"""Will be called when kodi stops playing a file."""
|
||||
self.stop_playback()
|
||||
LOG.info("--<<[ playback ]")
|
||||
|
||||
def stop_playback(self):
|
||||
|
||||
''' Stop all playback. Check for external player for positionticks.
|
||||
'''
|
||||
"""Stop all playback. Check for external player for positionticks."""
|
||||
if not self.played:
|
||||
return
|
||||
|
||||
@ -401,61 +407,67 @@ class Player(xbmc.Player):
|
||||
for file in self.played:
|
||||
item = self.get_file_info(file)
|
||||
|
||||
window('jellyfin.skip.%s.bool' % item['Id'], True)
|
||||
window("jellyfin.skip.%s.bool" % item["Id"], True)
|
||||
|
||||
if window('jellyfin.external.bool'):
|
||||
window('jellyfin.external', clear=True)
|
||||
if window("jellyfin.external.bool"):
|
||||
window("jellyfin.external", clear=True)
|
||||
|
||||
if int(item['CurrentPosition']) == 1:
|
||||
item['CurrentPosition'] = int(item['Runtime'])
|
||||
if int(item["CurrentPosition"]) == 1:
|
||||
item["CurrentPosition"] = int(item["Runtime"])
|
||||
|
||||
data = {
|
||||
'ItemId': item['Id'],
|
||||
'MediaSourceId': item['MediaSourceId'],
|
||||
'PositionTicks': int(item['CurrentPosition'] * 10000000),
|
||||
'PlaySessionId': item['PlaySessionId']
|
||||
"ItemId": item["Id"],
|
||||
"MediaSourceId": item["MediaSourceId"],
|
||||
"PositionTicks": int(item["CurrentPosition"] * 10000000),
|
||||
"PlaySessionId": item["PlaySessionId"],
|
||||
}
|
||||
item['Server'].jellyfin.session_stop(data)
|
||||
item["Server"].jellyfin.session_stop(data)
|
||||
|
||||
if item.get('LiveStreamId'):
|
||||
if item.get("LiveStreamId"):
|
||||
|
||||
LOG.info("<[ livestream/%s ]", item['LiveStreamId'])
|
||||
item['Server'].jellyfin.close_live_stream(item['LiveStreamId'])
|
||||
LOG.info("<[ livestream/%s ]", item["LiveStreamId"])
|
||||
item["Server"].jellyfin.close_live_stream(item["LiveStreamId"])
|
||||
|
||||
elif item['PlayMethod'] == 'Transcode':
|
||||
elif item["PlayMethod"] == "Transcode":
|
||||
|
||||
LOG.info("<[ transcode/%s ]", item['Id'])
|
||||
item['Server'].jellyfin.close_transcode(item['DeviceId'], item['PlaySessionId'])
|
||||
LOG.info("<[ transcode/%s ]", item["Id"])
|
||||
item["Server"].jellyfin.close_transcode(
|
||||
item["DeviceId"], item["PlaySessionId"]
|
||||
)
|
||||
|
||||
path = translate_path("special://profile/addon_data/plugin.video.jellyfin/temp/")
|
||||
path = translate_path(
|
||||
"special://profile/addon_data/plugin.video.jellyfin/temp/"
|
||||
)
|
||||
|
||||
if xbmcvfs.exists(path):
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
|
||||
for file in files:
|
||||
# Only delete the cached files for the previous play session
|
||||
if item['Id'] in file:
|
||||
if item["Id"] in file:
|
||||
xbmcvfs.delete(os.path.join(path, file))
|
||||
|
||||
result = item['Server'].jellyfin.get_item(item['Id']) or {}
|
||||
result = item["Server"].jellyfin.get_item(item["Id"]) or {}
|
||||
|
||||
if 'UserData' in result and result['UserData']['Played']:
|
||||
if "UserData" in result and result["UserData"]["Played"]:
|
||||
delete = False
|
||||
|
||||
if result['Type'] == 'Episode' and settings('deleteTV.bool'):
|
||||
if result["Type"] == "Episode" and settings("deleteTV.bool"):
|
||||
delete = True
|
||||
elif result['Type'] == 'Movie' and settings('deleteMovies.bool'):
|
||||
elif result["Type"] == "Movie" and settings("deleteMovies.bool"):
|
||||
delete = True
|
||||
|
||||
if not settings('offerDelete.bool'):
|
||||
if not settings("offerDelete.bool"):
|
||||
delete = False
|
||||
|
||||
if delete:
|
||||
LOG.info("Offer delete option")
|
||||
|
||||
if dialog("yesno", translate(30091), translate(33015), autoclose=120000):
|
||||
item['Server'].jellyfin.delete_item(item['Id'])
|
||||
if dialog(
|
||||
"yesno", translate(30091), translate(33015), autoclose=120000
|
||||
):
|
||||
item["Server"].jellyfin.delete_item(item["Id"])
|
||||
|
||||
window('jellyfin.external_check', clear=True)
|
||||
window("jellyfin.external_check", clear=True)
|
||||
|
||||
self.played.clear()
|
||||
|
File diff suppressed because it is too large
Load Diff
19
service.py
19
service.py
@ -14,16 +14,16 @@ from jellyfin_kodi.helper import LazyLogger
|
||||
#################################################################################################
|
||||
|
||||
LOG = LazyLogger(__name__)
|
||||
DELAY = int(settings('startupDelay') if settings('SyncInstallRunDone.bool') else 4)
|
||||
DELAY = int(settings("startupDelay") if settings("SyncInstallRunDone.bool") else 4)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class ServiceManager(threading.Thread):
|
||||
"""Service thread.
|
||||
To allow to restart and reload modules internally.
|
||||
"""
|
||||
|
||||
''' Service thread.
|
||||
To allow to restart and reload modules internally.
|
||||
'''
|
||||
exception = None
|
||||
|
||||
def __init__(self):
|
||||
@ -44,10 +44,10 @@ class ServiceManager(threading.Thread):
|
||||
|
||||
if service is not None:
|
||||
# TODO: fix this properly as to not match on str()
|
||||
if 'ExitService' not in str(error):
|
||||
if "ExitService" not in str(error):
|
||||
service.shutdown()
|
||||
|
||||
if 'RestartService' in str(error):
|
||||
if "RestartService" in str(error):
|
||||
service.reload_objects()
|
||||
|
||||
self.exception = error
|
||||
@ -58,7 +58,7 @@ if __name__ == "__main__":
|
||||
LOG.info("Delay startup by %s seconds.", DELAY)
|
||||
|
||||
while True:
|
||||
if not settings('enableAddon.bool'):
|
||||
if not settings("enableAddon.bool"):
|
||||
LOG.warning("Jellyfin for Kodi is not enabled.")
|
||||
|
||||
break
|
||||
@ -68,12 +68,11 @@ if __name__ == "__main__":
|
||||
session.start()
|
||||
session.join() # Block until the thread exits.
|
||||
|
||||
if 'RestartService' in str(session.exception):
|
||||
if "RestartService" in str(session.exception):
|
||||
continue
|
||||
|
||||
except Exception as error:
|
||||
''' Issue initializing the service.
|
||||
'''
|
||||
"""Issue initializing the service."""
|
||||
LOG.exception(error)
|
||||
|
||||
break
|
||||
|
@ -3,45 +3,51 @@ import pytest
|
||||
from jellyfin_kodi.jellyfin.utils import clean_none_dict_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj,expected", [
|
||||
(None, None),
|
||||
([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]),
|
||||
({'foo': None, 'bar': 123}, {'bar': 123}),
|
||||
({
|
||||
'dict': {
|
||||
'empty': None,
|
||||
'string': "Hello, Woorld!",
|
||||
},
|
||||
'number': 123,
|
||||
'list': [
|
||||
None,
|
||||
123,
|
||||
"foo",
|
||||
@pytest.mark.parametrize(
|
||||
"obj,expected",
|
||||
[
|
||||
(None, None),
|
||||
([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]),
|
||||
({"foo": None, "bar": 123}, {"bar": 123}),
|
||||
(
|
||||
{
|
||||
'empty': None,
|
||||
'number': 123,
|
||||
'string': "foo",
|
||||
'list': [],
|
||||
'dict': {},
|
||||
}
|
||||
]
|
||||
}, {
|
||||
'dict': {
|
||||
'string': "Hello, Woorld!",
|
||||
},
|
||||
'number': 123,
|
||||
'list': [
|
||||
None,
|
||||
123,
|
||||
"foo",
|
||||
"dict": {
|
||||
"empty": None,
|
||||
"string": "Hello, Woorld!",
|
||||
},
|
||||
"number": 123,
|
||||
"list": [
|
||||
None,
|
||||
123,
|
||||
"foo",
|
||||
{
|
||||
"empty": None,
|
||||
"number": 123,
|
||||
"string": "foo",
|
||||
"list": [],
|
||||
"dict": {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'number': 123,
|
||||
'string': "foo",
|
||||
'list': [],
|
||||
'dict': {},
|
||||
}
|
||||
]
|
||||
}),
|
||||
])
|
||||
"dict": {
|
||||
"string": "Hello, Woorld!",
|
||||
},
|
||||
"number": 123,
|
||||
"list": [
|
||||
None,
|
||||
123,
|
||||
"foo",
|
||||
{
|
||||
"number": 123,
|
||||
"string": "foo",
|
||||
"list": [],
|
||||
"dict": {},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_clean_none_dict_values(obj, expected):
|
||||
assert clean_none_dict_values(obj) == expected
|
||||
|
@ -37,6 +37,7 @@ def test_import_downloader():
|
||||
def test_import_entrypoint():
|
||||
import jellyfin_kodi.entrypoint
|
||||
import jellyfin_kodi.entrypoint.context
|
||||
|
||||
# import jellyfin_kodi.entrypoint.default # FIXME: Messes with sys.argv
|
||||
import jellyfin_kodi.entrypoint.service # noqa: F401
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
from sqlite3 import Cursor
|
||||
from typing import Any, List, Optional, NamedTuple
|
||||
|
||||
|
||||
class ViewRow(NamedTuple):
|
||||
view_id: str
|
||||
view_name: str
|
||||
media_type: str
|
||||
|
||||
|
||||
class JellyfinDatabase:
|
||||
cursor: Cursor = ...
|
||||
def __init__(self, cursor: Cursor) -> None: ...
|
||||
|
Loading…
Reference in New Issue
Block a user