diff --git a/jellyfin_kodi/connect.py b/jellyfin_kodi/connect.py
index 6dac1450..46a480dc 100644
--- a/jellyfin_kodi/connect.py
+++ b/jellyfin_kodi/connect.py
@@ -5,11 +5,12 @@ from __future__ import division, absolute_import, print_function, unicode_litera
import xbmc
import xbmcaddon
+import xbmcgui
from . import client
from .database import get_credentials, save_credentials
from .dialogs import ServerConnect, UsersConnect, LoginManual, ServerManual
-from .helper import settings, addon_id, event, api, window, LazyLogger
+from .helper import settings, addon_id, event, api, window, LazyLogger, translate
from .jellyfin import Jellyfin
from .jellyfin.connection_manager import CONNECTION_STATE
from .helper.exceptions import HTTPException
@@ -230,9 +231,19 @@ class Connect(object):
]
if not users:
- try:
- return self.login_manual()
- except RuntimeError:
+ options = [translate(30540), translate(30618)] # "Manual login", "Quick Connect"
+ idx = xbmcgui.Dialog().select(translate(30612), options)
+ if idx == 1: # Quick Connect
+ try:
+ return self.login_quick_connect()
+ except RuntimeError:
+ raise RuntimeError("No user selected")
+ elif idx == 0: # Manual login
+ try:
+ return self.login_manual()
+ except RuntimeError:
+ raise RuntimeError("No user selected")
+ else:
raise RuntimeError("No user selected")
dialog = UsersConnect("script-jellyfin-connect-users.xml", *XML_PATH)
@@ -252,6 +263,12 @@ class Connect(object):
else:
return self.connect_manager.login(server, username)
+ elif dialog.is_quick_connect():
+ try:
+ return self.login_quick_connect()
+ except RuntimeError:
+ pass
+
elif dialog.is_manual_login():
try:
return self.login_manual()
@@ -262,6 +279,53 @@ class Connect(object):
return self.login()
+ def login_quick_connect(self):
+ """Show Quick Connect code and wait for authorization via polling."""
+ server = self.connect_manager.get_server_info(self.connect_manager.server_id)[
+ "address"
+ ]
+ result = self.connect_manager.API.quick_connect_initiate(server)
+
+ if not result or "Code" not in result:
+ xbmcgui.Dialog().ok(translate(30618), translate(30621))
+ raise RuntimeError("Quick Connect not available")
+
+ code = result["Code"]
+ secret = result["Secret"]
+
+ progress = xbmcgui.DialogProgress()
+ progress.create(
+ translate(30618),
+ "%s [B]%s[/B]" % (translate(30619), code),
+ )
+
+ max_polls = 60
+ for i in range(max_polls):
+ if progress.iscanceled():
+ progress.close()
+ raise RuntimeError("Quick Connect cancelled")
+
+ xbmc.sleep(5000)
+
+ if progress.iscanceled():
+ progress.close()
+ raise RuntimeError("Quick Connect cancelled")
+
+ status = self.connect_manager.API.quick_connect_connect(server, secret)
+
+ if status.get("Authenticated"):
+ progress.close()
+ auth = self.connect_manager.login_quick_connect(server, secret)
+ if auth:
+ return auth
+ raise RuntimeError("Quick Connect authentication failed")
+
+ progress.update(int((i + 1) * 100 / max_polls))
+
+ progress.close()
+ xbmcgui.Dialog().ok(translate(30618), translate(30622))
+ raise RuntimeError("Quick Connect timed out")
+
def setup_login_manual(self):
"""Setup manual login by itself for default server."""
client = self.get_client()
diff --git a/jellyfin_kodi/dialogs/usersconnect.py b/jellyfin_kodi/dialogs/usersconnect.py
index 6df02d71..acc01a95 100644
--- a/jellyfin_kodi/dialogs/usersconnect.py
+++ b/jellyfin_kodi/dialogs/usersconnect.py
@@ -19,6 +19,7 @@ ACTION_MOUSE_LEFT_CLICK = 100
LIST = 155
MANUAL = 200
CANCEL = 201
+QUICK_CONNECT = 202
##################################################################################################
@@ -27,6 +28,7 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
_user = None
_manual_login = False
+ _quick_connect = False
def __init__(self, *args, **kwargs):
@@ -47,6 +49,9 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
def is_manual_login(self):
return self._manual_login
+ def is_quick_connect(self):
+ return self._quick_connect
+
def onInit(self):
self.list_ = self.getControl(LIST)
@@ -95,6 +100,10 @@ class UsersConnect(xbmcgui.WindowXMLDialog):
self._manual_login = True
self.close()
+ elif control == QUICK_CONNECT:
+ self._quick_connect = True
+ self.close()
+
elif control == CANCEL:
self.close()
diff --git a/jellyfin_kodi/helper/translate.py b/jellyfin_kodi/helper/translate.py
index cd6dd037..b920d97e 100644
--- a/jellyfin_kodi/helper/translate.py
+++ b/jellyfin_kodi/helper/translate.py
@@ -41,6 +41,11 @@ STRINGS = {
"cancel": 30606,
"username": 30024,
"password": 30602,
+ "quick_connect": 30618,
+ "quick_connect_code": 30619,
+ "quick_connect_waiting": 30620,
+ "quick_connect_unavailable": 30621,
+ "quick_connect_timeout": 30622,
"gathering": 33021,
"boxsets": 30185,
"movies": 30302,
diff --git a/jellyfin_kodi/jellyfin/api.py b/jellyfin_kodi/jellyfin/api.py
index 3914cc1f..88087f7a 100644
--- a/jellyfin_kodi/jellyfin/api.py
+++ b/jellyfin_kodi/jellyfin/api.py
@@ -521,3 +521,53 @@ class API(object):
except Exception as e:
LOG.warning("Error fetching media segments: %s", e)
return None
+
+ def quick_connect_initiate(self, server_url):
+ """Initiate Quick Connect and return {Secret, Code, Authenticated}."""
+ headers = self.get_default_headers()
+ headers.update({"Content-type": "application/json"})
+ try:
+ response = self.send_request(
+ server_url, "QuickConnect/Initiate", method="post",
+ timeout=10, headers=headers, data=json.dumps({})
+ )
+ if response.status_code == 200:
+ return response.json()
+ LOG.error("Quick Connect initiate failed with status %s", response.status_code)
+ return {}
+ except Exception as e:
+ LOG.error("Quick Connect initiate error: %s", e)
+ return {}
+
+ def quick_connect_connect(self, server_url, secret):
+ """Check Quick Connect authorization status."""
+ headers = self.get_default_headers()
+ try:
+ response = self.send_request(
+ server_url, "QuickConnect/Connect?Secret=%s" % quote(secret),
+ method="get", timeout=10, headers=headers
+ )
+ if response.status_code == 200:
+ return response.json()
+ LOG.error("Quick Connect check failed with status %s", response.status_code)
+ return {}
+ except Exception as e:
+ LOG.error("Quick Connect check error: %s", e)
+ return {}
+
+ def quick_connect_authenticate(self, server_url, secret):
+ """Exchange a Quick Connect secret for an auth token."""
+ headers = self.get_default_headers()
+ headers.update({"Content-type": "application/json"})
+ try:
+ response = self.send_request(
+ server_url, "Users/AuthenticateWithQuickConnect", method="post",
+ timeout=10, headers=headers, data=json.dumps({"Secret": secret})
+ )
+ if response.status_code == 200:
+ return response.json()
+ LOG.error("Quick Connect authenticate failed with status %s", response.status_code)
+ return {}
+ except Exception as e:
+ LOG.error("Quick Connect authenticate error: %s", e)
+ return {}
diff --git a/jellyfin_kodi/jellyfin/connection_manager.py b/jellyfin_kodi/jellyfin/connection_manager.py
index 304f7148..1a2992fa 100644
--- a/jellyfin_kodi/jellyfin/connection_manager.py
+++ b/jellyfin_kodi/jellyfin/connection_manager.py
@@ -100,30 +100,7 @@ class ConnectionManager(object):
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"]
-
- 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"]
-
- self.credentials.add_update_server(credentials["Servers"], found_server)
-
- info = {"Id": data["User"]["Id"], "IsSignedInOffline": True}
- self.credentials.add_update_user(server, info)
-
- self.credentials.set_credentials(credentials)
-
- return data
+ return self._save_auth_credentials(data)
def connect_to_address(self, address, options={}):
@@ -357,6 +334,49 @@ class ConnectionManager(object):
),
}
+ def login_quick_connect(self, server_url, secret):
+ """Exchange a Quick Connect secret for an authenticated session."""
+ if not server_url:
+ raise AttributeError("server url cannot be empty")
+ if not secret:
+ raise AttributeError("secret cannot be empty")
+
+ data = self.API.quick_connect_authenticate(server_url, secret)
+
+ if not data:
+ LOG.info("Quick Connect authentication failed")
+ return {}
+
+ LOG.info("Successfully logged in via Quick Connect as %s", data["User"]["Name"])
+ return self._save_auth_credentials(data)
+
+ def _save_auth_credentials(self, data):
+ """Persist auth token and user info after a successful login."""
+ credentials = self.credentials.get()
+
+ 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"]:
+ found_server = server
+ break
+ else:
+ return {}
+
+ 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)
+
+ info = {"Id": data["User"]["Id"], "IsSignedInOffline": True}
+ self.credentials.add_update_user(found_server, info)
+
+ self.credentials.set_credentials(credentials)
+
+ return data
+
def _update_server_info(self, server, system_info):
if server is None or system_info is None:
diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po
index f0212f87..62ddb511 100644
--- a/resources/language/resource.language.en_gb/strings.po
+++ b/resources/language/resource.language.en_gb/strings.po
@@ -437,6 +437,26 @@ msgctxt "#30617"
msgid "Server or port cannot be empty"
msgstr "Server or port cannot be empty"
+msgctxt "#30618"
+msgid "Quick Connect"
+msgstr "Quick Connect"
+
+msgctxt "#30619"
+msgid "Enter this code in the Jellyfin app or web interface:"
+msgstr "Enter this code in the Jellyfin app or web interface:"
+
+msgctxt "#30620"
+msgid "Waiting for authorization..."
+msgstr "Waiting for authorization..."
+
+msgctxt "#30621"
+msgid "Quick Connect is not available on this server"
+msgstr "Quick Connect is not available on this server"
+
+msgctxt "#30622"
+msgid "Quick Connect timed out. Please try again."
+msgstr "Quick Connect timed out. Please try again."
+
msgctxt "#33000"
msgid "Welcome"
msgstr "Welcome"
diff --git a/resources/skins/default/1080i/script-jellyfin-connect-users.xml b/resources/skins/default/1080i/script-jellyfin-connect-users.xml
index 7594cd5d..86574eca 100644
--- a/resources/skins/default/1080i/script-jellyfin-connect-users.xml
+++ b/resources/skins/default/1080i/script-jellyfin-connect-users.xml
@@ -25,7 +25,7 @@
50%
50%
920
- 605
+ 670
-30
@@ -38,14 +38,14 @@
100%
- 605
+ 670
dialogs/dialog_back.png
50%
10
908
- 580
+ 645
vertical
0
@@ -192,6 +192,24 @@
155
Conditional
+
+
+ 874
+ 65
+ font13
+ ffe1e1e1
+ white
+ ffe1e1e1
+ 66000000
+ 20
+ center
+ center
+ buttons/shadow_smallbutton.png
+ buttons/shadow_smallbutton.png
+ no
+ 155
+ Conditional
+
874