mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2026-04-27 14:00:34 +00:00
Add Quick Connect support (#1117)
This commit is contained in:
parent
0bf363c65f
commit
1d6517d197
7 changed files with 217 additions and 31 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<centerleft>50%</centerleft>
|
||||
<centertop>50%</centertop>
|
||||
<width>920</width>
|
||||
<height>605</height>
|
||||
<height>670</height>
|
||||
<control type="group">
|
||||
<top>-30</top>
|
||||
<control type="image">
|
||||
|
|
@ -38,14 +38,14 @@
|
|||
</control>
|
||||
<control type="image">
|
||||
<width>100%</width>
|
||||
<height>605</height>
|
||||
<height>670</height>
|
||||
<texture colordiffuse="ff222326" border="10">dialogs/dialog_back.png</texture>
|
||||
</control>
|
||||
<control type="group">
|
||||
<centerleft>50%</centerleft>
|
||||
<top>10</top>
|
||||
<width>908</width>
|
||||
<height>580</height>
|
||||
<height>645</height>
|
||||
<control type="grouplist" id="100">
|
||||
<orientation>vertical</orientation>
|
||||
<itemgap>0</itemgap>
|
||||
|
|
@ -192,6 +192,24 @@
|
|||
<onup>155</onup>
|
||||
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="button" id="202">
|
||||
<label>[B]$ADDON[plugin.video.jellyfin 30618][/B]</label>
|
||||
<width>874</width>
|
||||
<height>65</height>
|
||||
<font>font13</font>
|
||||
<textcolor>ffe1e1e1</textcolor>
|
||||
<focusedcolor>white</focusedcolor>
|
||||
<selectedcolor>ffe1e1e1</selectedcolor>
|
||||
<shadowcolor>66000000</shadowcolor>
|
||||
<textoffsetx>20</textoffsetx>
|
||||
<aligny>center</aligny>
|
||||
<align>center</align>
|
||||
<texturefocus border="10" colordiffuse="FF00A4DC">buttons/shadow_smallbutton.png</texturefocus>
|
||||
<texturenofocus border="10" colordiffuse="ff464646">buttons/shadow_smallbutton.png</texturenofocus>
|
||||
<pulseonselect>no</pulseonselect>
|
||||
<onup>155</onup>
|
||||
<animation effect="slide" time="0" end="17,0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="button" id="201">
|
||||
<label>[B]$ADDON[plugin.video.jellyfin 30606][/B]</label>
|
||||
<width>874</width>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue