From 0fc9f21134e5942be71f77328dbb93e69dfb1f08 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 02:54:08 +0100 Subject: [PATCH 01/17] feat: add Plex authentification --- Contents/Code/default_prefs.py | 1 + Contents/Code/plex_api_helper.py | 58 +++++ Contents/Code/webapp.py | 150 ++++++++++++- Contents/DefaultPrefs.json | 7 + Contents/Resources/web/js/plexauth.js | 219 +++++++++++++++++++ Contents/Resources/web/templates/login.html | 75 +++++++ Contents/Resources/web/templates/navbar.html | 7 + 7 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 Contents/Resources/web/js/plexauth.js create mode 100644 Contents/Resources/web/templates/login.html diff --git a/Contents/Code/default_prefs.py b/Contents/Code/default_prefs.py index abc561ac..810c4f8f 100644 --- a/Contents/Code/default_prefs.py +++ b/Contents/Code/default_prefs.py @@ -23,6 +23,7 @@ enum_webapp_locale='en', str_webapp_http_host='0.0.0.0', int_webapp_http_port='9494', + bool_webapp_require_login='False', bool_webapp_log_werkzeug_messages='False', bool_migrate_locked_themes='False', bool_migrate_locked_collection_fields='False', diff --git a/Contents/Code/plex_api_helper.py b/Contents/Code/plex_api_helper.py index 640a6399..905ac835 100644 --- a/Contents/Code/plex_api_helper.py +++ b/Contents/Code/plex_api_helper.py @@ -25,6 +25,7 @@ from plexapi.base import PlexPartialObject from plexapi.exceptions import BadRequest import plexapi.server +from plexapi.myplex import MyPlexAccount # must be imported after plexapi.server otherwise it breaks from plexapi.utils import reverseSearchType # local imports @@ -765,3 +766,60 @@ def scheduled_update(): for item in all_items: if item.ratingKey not in q.queue: q.put(item=item.ratingKey) + + +def get_user_info(token): + # type: (str) -> Optional[MyPlexAccount] + """ + Get the Plex user info. + + Parameters + ---------- + token : str + The Plex token. + + Returns + ------- + Optional[MyPlexAccount] + The Plex user info. + + Examples + -------- + >>> get_user_info(token='...') + ... + """ + global plex + if not plex: + plex = setup_plexapi() + + try: + return MyPlexAccount(token=token) + except Exception: + return None + + +def is_server_owner(user): + # type: (MyPlexAccount) -> bool + """ + Check if the user is the owner of the Plex server. + + Parameters + ---------- + user : MyPlexAccount + The Plex user info. + + Returns + ------- + bool + True if the user is the owner of the Plex server, False otherwise. + + Examples + -------- + >>> is_server_owner(user=...) + ... + """ + global plex + if not plex: + plex = setup_plexapi() + + return plex.account().username in {user.email, user.username} diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index 7e5b2b46..93bd24fc 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -8,6 +8,7 @@ import logging import os from threading import Lock, Thread +import uuid # plex debugging try: @@ -23,7 +24,7 @@ # lib imports import flask -from flask import Flask, Response, render_template, send_from_directory +from flask import Flask, Response, render_template as flask_render_template, send_from_directory, session from flask_babel import Babel import polib from six.moves.urllib.parse import quote_plus @@ -32,10 +33,39 @@ # local imports from constants import contributes_to, issue_urls, plugin_directory, plugin_identifier, themerr_data_directory import general_helper -from plex_api_helper import get_database_info, setup_plexapi +from plex_api_helper import get_database_info, get_user_info, is_server_owner, setup_plexapi import themerr_db_helper import tmdb_helper + +def render_template(*args, **kwargs): + # type: (str, str) -> flask.render_template + """ + Render a template. + + This function is a wrapper for flask.render_template that adds various useful globals to the template context. + + Parameters + ---------- + *args : str + The template name. + **kwargs : str + The template context. + + Returns + ------- + flask.render_template + The rendered template. + + Examples + -------- + >>> render_template('home.html', title='Home', items=items) + """ + kwargs['Prefs'] = Prefs + kwargs['is_logged_in'] = is_logged_in + return flask_render_template(*args, **kwargs) + + # setup flask app app = Flask( import_name=__name__, @@ -106,6 +136,19 @@ database_cache_file = os.path.join(themerr_data_directory, 'database_cache.json') database_cache_lock = Lock() +secret_file = os.path.join(themerr_data_directory, 'secret.json') + +try: + app.secret_key = json.loads(Core.storage.load(filename=secret_file, binary=False))["secret"] +except Exception: + # create random secret + Log.Info('Creating random secret') + app.secret_key = uuid.uuid4().hex + Core.storage.save( + filename=secret_file, + data=json.dumps({"secret": app.secret_key}), + binary=False) + responses = { 500: Response(response='Internal Server Error', status=500, mimetype='text/plain') @@ -495,3 +538,106 @@ def translations(): return Response(response=json.dumps(data), status=200, mimetype='application/json') + + +def is_logged_in(): + # type: () -> bool + """ + Check if the user is logged in. + + Returns + ------- + bool + True if the user is logged in, otherwise False. + + Examples + -------- + >>> is_logged_in() + """ + if not Prefs['bool_webapp_require_login']: + return True + + if "token" not in session: + return False + + token = session["token"] + user = get_user_info(token) + logged_in = user and is_server_owner(user) + return logged_in + + +@app.route("/logout", methods=["GET"]) +def logout(): + # type: () -> Response + """ + Logout the user. + + Returns + ------- + Response + The logout response. + + Examples + -------- + >>> logout() + """ + session.pop("token", None) + return flask.redirect(flask.url_for('home')) + + +@app.before_request +def check_login_status(): + # type: () -> None + """ + Check if the user is logged in. + + If the user is not logged in, redirect to the login page. + + Examples + -------- + >>> check_login_status() + """ + if not Prefs['bool_webapp_require_login']: + return + + if flask.request.path.startswith('/web'): + return + + if flask.request.path in {'/login', '/logout'}: + return + + if not is_logged_in(): + # not logged in, redirect to login page + return flask.redirect(flask.url_for('login', redirect_uri=flask.request.path)) + + +@app.route("/login", methods=["GET"]) +def login(redirect_uri="/"): + # type: (str) -> render_template + """ + Serve the login page. + + Returns + ------- + render_template + The rendered page. + + Notes + ----- + The following routes trigger this function. + + - `/login` + + Examples + -------- + >>> login() + """ + return render_template('login.html', title='Login', redirect_uri=redirect_uri) + + +@app.route("/login", methods=["POST"]) +def login_post(): + session["token"] = flask.request.form["token"] + if not is_logged_in(): + return flask.Response(status=401) + return flask.Response(status=200) diff --git a/Contents/DefaultPrefs.json b/Contents/DefaultPrefs.json index 91203691..f4ffd6ff 100644 --- a/Contents/DefaultPrefs.json +++ b/Contents/DefaultPrefs.json @@ -167,6 +167,13 @@ "default": "9494", "secure": "false" }, + { + "id": "bool_webapp_require_login", + "type": "bool", + "label": "Require login to access Web UI", + "default": "False", + "secure": "false" + }, { "id": "bool_webapp_log_werkzeug_messages", "type": "bool", diff --git a/Contents/Resources/web/js/plexauth.js b/Contents/Resources/web/js/plexauth.js new file mode 100644 index 00000000..b98c9ee7 --- /dev/null +++ b/Contents/Resources/web/js/plexauth.js @@ -0,0 +1,219 @@ +// Code from https://github.com/Tautulli/Tautulli/blob/master/data/interfaces/default/js/script.js. +// Licensed under the GNU General Public License v3 (GPLv3) + +var p = { + name: 'Unknown', + version: 'Unknown', + os: 'Unknown' +}; +if (typeof platform !== 'undefined') { + p.name = platform.name; + p.version = platform.version; + p.os = platform.os.toString(); +} + +function getPlexHeaders(clientID) { + return { + 'Accept': 'application/json', + 'X-Plex-Product': 'Themerr', + 'X-Plex-Version': 'Plex OAuth', + 'X-Plex-Client-Identifier': clientID ? clientID : getLocalStorage('Tautulli_ClientID', uuidv4(), false), + 'X-Plex-Platform': p.name, + 'X-Plex-Platform-Version': p.version, + 'X-Plex-Model': 'Plex OAuth', + 'X-Plex-Device': p.os, + 'X-Plex-Device-Name': p.name + ' (Themerr)', + 'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height, + 'X-Plex-Language': 'en' + }; +} + +function encodeData(data) { + return Object.keys(data).map(function(key) { + return [key, data[key]].map(encodeURIComponent).join("="); + }).join("&"); +} + +var plex_oauth_window = null; +const plex_oauth_loader = '' + + '
' + + '' + + '
'; + +function closePlexOAuthWindow() { + if (plex_oauth_window) { + plex_oauth_window.close(); + } +} + +function setLocalStorage(key, value, path) { + var key_path = key; + if (path !== false) { + key_path = key_path + '_' + window.location.pathname; + } + localStorage.setItem(key_path, value); +} +function getLocalStorage(key, default_value, path) { + var key_path = key; + if (path !== false) { + key_path = key_path + '_' + window.location.pathname; + } + var value = localStorage.getItem(key_path); + if (value !== null) { + return value + } else if (default_value !== undefined) { + setLocalStorage(key, default_value, path); + return default_value + } +} + +function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) { + var cryptoObj = window.crypto || window.msCrypto; // for IE 11 + return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + }); +} + +getPlexOAuthPin = function (clientID) { + var x_plex_headers = getPlexHeaders(clientID); + var deferred = $.Deferred(); + + $.ajax({ + url: 'https://plex.tv/api/v2/pins?strong=true', + type: 'POST', + headers: x_plex_headers, + success: function(data) { + deferred.resolve({pin: data.id, code: data.code}); + }, + error: function() { + closePlexOAuthWindow(); + deferred.reject(); + } + }); + return deferred; +}; + +function PopupCenter(url, title, w, h) { + // Fixes dual-screen position Most browsers Firefox + var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX; + var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY; + + var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; + var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; + + var left = ((width / 2) - (w / 2)) + dualScreenLeft; + var top = ((height / 2) - (h / 2)) + dualScreenTop; + var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); + + // Puts focus on the newWindow + if (window.focus) { + newWindow.focus(); + } + + return newWindow; +} + +var polling = null; + +function PlexOAuth(success, error, pre, clientID) { + if (typeof pre === "function") { + pre() + } + closePlexOAuthWindow(); + plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700); + $(plex_oauth_window.document.body).html(plex_oauth_loader); + + getPlexOAuthPin(clientID).then(function (data) { + var x_plex_headers = getPlexHeaders(clientID); + const pin = data.pin; + const code = data.code; + + var oauth_params = { + 'clientID': x_plex_headers['X-Plex-Client-Identifier'], + 'context[device][product]': x_plex_headers['X-Plex-Product'], + 'context[device][version]': x_plex_headers['X-Plex-Version'], + 'context[device][platform]': x_plex_headers['X-Plex-Platform'], + 'context[device][platformVersion]': x_plex_headers['X-Plex-Platform-Version'], + 'context[device][device]': x_plex_headers['X-Plex-Device'], + 'context[device][deviceName]': x_plex_headers['X-Plex-Device-Name'], + 'context[device][model]': x_plex_headers['X-Plex-Model'], + 'context[device][screenResolution]': x_plex_headers['X-Plex-Device-Screen-Resolution'], + 'context[device][layout]': 'desktop', + 'code': code + } + + plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params); + polling = pin; + + (function poll() { + $.ajax({ + url: 'https://plex.tv/api/v2/pins/' + pin, + type: 'GET', + headers: x_plex_headers, + success: function (data) { + if (data.authToken){ + closePlexOAuthWindow(); + if (typeof success === "function") { + success(data.authToken) + } + } + }, + error: function (jqXHR, textStatus, errorThrown) { + if (textStatus !== "timeout") { + closePlexOAuthWindow(); + if (typeof error === "function") { + error() + } + } + }, + complete: function () { + if (!plex_oauth_window.closed && polling === pin){ + setTimeout(function() {poll()}, 1000); + } + }, + timeout: 10000 + }); + })(); + }, function () { + closePlexOAuthWindow(); + if (typeof error === "function") { + error() + } + }); +} \ No newline at end of file diff --git a/Contents/Resources/web/templates/login.html b/Contents/Resources/web/templates/login.html new file mode 100644 index 00000000..bc02fb89 --- /dev/null +++ b/Contents/Resources/web/templates/login.html @@ -0,0 +1,75 @@ +{# Code from https://github.com/Tautulli/Tautulli/blob/master/data/interfaces/default/login.html. #} +{# Licensed under the GNU General Public License v3 (GPLv3) #} + +{% extends 'base.html' %} +{% block modals %} +{% endblock modals %} + +{% block content %} +
+
+ +
+
+{% endblock content %} + +{% block scripts %} + + +{% endblock scripts %} diff --git a/Contents/Resources/web/templates/navbar.html b/Contents/Resources/web/templates/navbar.html index d29ef6a9..5a8f57ea 100644 --- a/Contents/Resources/web/templates/navbar.html +++ b/Contents/Resources/web/templates/navbar.html @@ -58,6 +58,13 @@ href="/"> {{ _('Home') }} + + {% if Prefs['bool_webapp_require_login'] and is_logged_in() %} +
  • + {{ _('Logout') }} +
  • + {% endif %} From c4bd34271d2bf9f8b9a4063378d153c4889b2483 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 02:56:50 +0100 Subject: [PATCH 02/17] doc: document auth setting --- docs/source/about/usage.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index 54e756ce..8ac95045 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -298,6 +298,19 @@ Description Default ``9494`` +Web UI Require Login +^^^^^^^^^^^^^^^^^^^^ + +Description + If set to ``True``, the Web UI will require the user to log in with their Plex account before accessing the Web UI. + Only the owner of the Plex Media Server will be able to log in. + +.. Attention:: + Consider enabling this value if you plan to expose the Web UI to the outside world. + +Default + ``False`` + Log all web server messages ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From dc6ba9918297ad7dd4d96b4bc74683f471f59dee Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 03:00:36 +0100 Subject: [PATCH 03/17] fix: fix button text --- Contents/Resources/web/templates/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contents/Resources/web/templates/login.html b/Contents/Resources/web/templates/login.html index bc02fb89..13336b1e 100644 --- a/Contents/Resources/web/templates/login.html +++ b/Contents/Resources/web/templates/login.html @@ -40,7 +40,7 @@ function signIn(token) { $('.login-container button').prop('disabled', true); - $('#sign-in-plex').html('  Sign In'); + $('#sign-in-plex').html('  Sign In with Plex'); var data = { token: token From bf071bbb4498531f4a080cc9834fa9d520a0fae8 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 03:07:46 +0100 Subject: [PATCH 04/17] style: flake8 --- Contents/Code/webapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index 93bd24fc..25f06e67 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -599,7 +599,7 @@ def check_login_status(): """ if not Prefs['bool_webapp_require_login']: return - + if flask.request.path.startswith('/web'): return From 98e814a7206d3b06b741338da93d68a1a8630dc4 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 03:31:32 +0100 Subject: [PATCH 05/17] fix: use open --- Contents/Code/webapp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index 25f06e67..90e840e9 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -139,7 +139,8 @@ def render_template(*args, **kwargs): secret_file = os.path.join(themerr_data_directory, 'secret.json') try: - app.secret_key = json.loads(Core.storage.load(filename=secret_file, binary=False))["secret"] + with open(secret_file, 'r') as f: + app.secret_key = json.load(f)['secret'] except Exception: # create random secret Log.Info('Creating random secret') From 1544a43c52ed791567515ef5de44b15c1f49e08c Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Tue, 21 Nov 2023 03:37:04 +0100 Subject: [PATCH 06/17] fix: handle error when writing secret --- Contents/Code/webapp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index 90e840e9..d2de6bae 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -145,10 +145,11 @@ def render_template(*args, **kwargs): # create random secret Log.Info('Creating random secret') app.secret_key = uuid.uuid4().hex - Core.storage.save( - filename=secret_file, - data=json.dumps({"secret": app.secret_key}), - binary=False) + try: + with open(secret_file, 'w') as f: + json.dump({'secret': app.secret_key}, f) + except Exception as e: + Log.Error('Error saving secret: {}'.format(e)) responses = { From f68eba9c0b8bd4ef5db8bc5a0bf67ff17b19254a Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Fri, 8 Dec 2023 20:08:22 +0100 Subject: [PATCH 07/17] fix: make session permanent --- Contents/Code/webapp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index d2de6bae..ee6dd374 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -639,6 +639,7 @@ def login(redirect_uri="/"): @app.route("/login", methods=["POST"]) def login_post(): + session.permanent = True session["token"] = flask.request.form["token"] if not is_logged_in(): return flask.Response(status=401) From 4bee33029545e900103572f418c42080dbc4fe9f Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Fri, 8 Dec 2023 20:17:30 +0100 Subject: [PATCH 08/17] fix: migrate code to 0.3 --- Contents/Code/plex_api_helper.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Contents/Code/plex_api_helper.py b/Contents/Code/plex_api_helper.py index 905ac835..5ff0bcd2 100644 --- a/Contents/Code/plex_api_helper.py +++ b/Contents/Code/plex_api_helper.py @@ -788,10 +788,6 @@ def get_user_info(token): >>> get_user_info(token='...') ... """ - global plex - if not plex: - plex = setup_plexapi() - try: return MyPlexAccount(token=token) except Exception: @@ -818,8 +814,6 @@ def is_server_owner(user): >>> is_server_owner(user=...) ... """ - global plex - if not plex: - plex = setup_plexapi() + plex = setup_plexapi() return plex.account().username in {user.email, user.username} From f987eb7b56a94c3a49ca7e4bb1d041e8f85624bd Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 23 Dec 2023 17:41:18 +0100 Subject: [PATCH 09/17] fix: use permalink and remove license mention Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- Contents/Resources/web/js/plexauth.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Contents/Resources/web/js/plexauth.js b/Contents/Resources/web/js/plexauth.js index b98c9ee7..a725d170 100644 --- a/Contents/Resources/web/js/plexauth.js +++ b/Contents/Resources/web/js/plexauth.js @@ -1,5 +1,4 @@ -// Code from https://github.com/Tautulli/Tautulli/blob/master/data/interfaces/default/js/script.js. -// Licensed under the GNU General Public License v3 (GPLv3) +// Reference: https://github.com/Tautulli/Tautulli/blob/d019efcf911b4806618761c2da48bab7d04031ec/data/interfaces/default/js/script.js var p = { name: 'Unknown', From 166bdf864439841ac532030498d87234b6ecbffd Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 23 Dec 2023 17:42:56 +0100 Subject: [PATCH 10/17] fix: use permalink and remove license mention again Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- Contents/Resources/web/templates/login.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Contents/Resources/web/templates/login.html b/Contents/Resources/web/templates/login.html index 13336b1e..f8b7ede8 100644 --- a/Contents/Resources/web/templates/login.html +++ b/Contents/Resources/web/templates/login.html @@ -1,5 +1,4 @@ -{# Code from https://github.com/Tautulli/Tautulli/blob/master/data/interfaces/default/login.html. #} -{# Licensed under the GNU General Public License v3 (GPLv3) #} +{# Reference: https://github.com/Tautulli/Tautulli/blob/d019efcf911b4806618761c2da48bab7d04031ec/data/interfaces/default/login.html #} {% extends 'base.html' %} {% block modals %} From 96d91bd19a8cfcfd015a988a33a4a5cd92dbfdf1 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 23 Dec 2023 18:04:23 +0100 Subject: [PATCH 11/17] style: formatting --- Contents/Resources/web/js/plexauth.js | 203 ++++++++++---------- Contents/Resources/web/templates/login.html | 45 ++--- 2 files changed, 126 insertions(+), 122 deletions(-) diff --git a/Contents/Resources/web/js/plexauth.js b/Contents/Resources/web/js/plexauth.js index a725d170..54275898 100644 --- a/Contents/Resources/web/js/plexauth.js +++ b/Contents/Resources/web/js/plexauth.js @@ -4,11 +4,11 @@ var p = { name: 'Unknown', version: 'Unknown', os: 'Unknown' -}; +} if (typeof platform !== 'undefined') { - p.name = platform.name; - p.version = platform.version; - p.os = platform.os.toString(); + p.name = platform.name + p.version = platform.version + p.os = platform.os.toString() } function getPlexHeaders(clientID) { @@ -16,7 +16,7 @@ function getPlexHeaders(clientID) { 'Accept': 'application/json', 'X-Plex-Product': 'Themerr', 'X-Plex-Version': 'Plex OAuth', - 'X-Plex-Client-Identifier': clientID ? clientID : getLocalStorage('Tautulli_ClientID', uuidv4(), false), + 'X-Plex-Client-Identifier': clientID ? clientID : getLocalStorage('Themerr_ClientID', uuidv4(), false), 'X-Plex-Platform': p.name, 'X-Plex-Platform-Version': p.version, 'X-Plex-Model': 'Plex OAuth', @@ -24,144 +24,147 @@ function getPlexHeaders(clientID) { 'X-Plex-Device-Name': p.name + ' (Themerr)', 'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height, 'X-Plex-Language': 'en' - }; + } } function encodeData(data) { - return Object.keys(data).map(function(key) { - return [key, data[key]].map(encodeURIComponent).join("="); - }).join("&"); + return Object.keys(data).map(function (key) { + return [key, data[key]].map(encodeURIComponent).join("=") + }).join("&") } -var plex_oauth_window = null; +var plex_oauth_window = null const plex_oauth_loader = '' + ''; + '' + + '' function closePlexOAuthWindow() { if (plex_oauth_window) { - plex_oauth_window.close(); + plex_oauth_window.close() } } -function setLocalStorage(key, value, path) { - var key_path = key; +function getKeyPath(key, path) { + var key_path = key if (path !== false) { - key_path = key_path + '_' + window.location.pathname; + key_path = `${key_path}_${window.location.pathname}` } - localStorage.setItem(key_path, value); + return key_path +} + +function setLocalStorage(key, value, path) { + var key_path = getKeyPath(key, path) + localStorage.setItem(key_path, value) } + function getLocalStorage(key, default_value, path) { - var key_path = key; - if (path !== false) { - key_path = key_path + '_' + window.location.pathname; - } - var value = localStorage.getItem(key_path); + var key_path = getKeyPath(key, path) + var value = localStorage.getItem(key_path) if (value !== null) { return value } else if (default_value !== undefined) { - setLocalStorage(key, default_value, path); + setLocalStorage(key, default_value, path) return default_value } } function uuidv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) { - var cryptoObj = window.crypto || window.msCrypto; // for IE 11 + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, function (c) { + var cryptoObj = window.crypto || window.msCrypto // for IE 11 return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - }); + }) } getPlexOAuthPin = function (clientID) { - var x_plex_headers = getPlexHeaders(clientID); - var deferred = $.Deferred(); + var x_plex_headers = getPlexHeaders(clientID) + var deferred = $.Deferred() $.ajax({ url: 'https://plex.tv/api/v2/pins?strong=true', type: 'POST', headers: x_plex_headers, - success: function(data) { - deferred.resolve({pin: data.id, code: data.code}); + success: function (data) { + deferred.resolve({ pin: data.id, code: data.code }) }, - error: function() { - closePlexOAuthWindow(); - deferred.reject(); + error: function () { + closePlexOAuthWindow() + deferred.reject() } - }); - return deferred; -}; + }) + return deferred +} function PopupCenter(url, title, w, h) { - // Fixes dual-screen position Most browsers Firefox - var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX; - var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY; + // Fixes dual-screen position Most browsers Firefox + var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX + var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY - var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; - var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; + var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width + var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height - var left = ((width / 2) - (w / 2)) + dualScreenLeft; - var top = ((height / 2) - (h / 2)) + dualScreenTop; - var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); + var left = ((width / 2) - (w / 2)) + dualScreenLeft + var top = ((height / 2) - (h / 2)) + dualScreenTop + var newWindow = window.open(url, title, `scrollbars=yes, width=${w}, height=${h}, top=${top}, left=${left}`) // Puts focus on the newWindow if (window.focus) { - newWindow.focus(); + newWindow.focus() } - return newWindow; + return newWindow } -var polling = null; +var polling = null function PlexOAuth(success, error, pre, clientID) { if (typeof pre === "function") { pre() } - closePlexOAuthWindow(); - plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700); - $(plex_oauth_window.document.body).html(plex_oauth_loader); + closePlexOAuthWindow() + plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700) + $(plex_oauth_window.document.body).html(plex_oauth_loader) getPlexOAuthPin(clientID).then(function (data) { - var x_plex_headers = getPlexHeaders(clientID); - const pin = data.pin; - const code = data.code; + var x_plex_headers = getPlexHeaders(clientID) + const pin = data.pin + const code = data.code var oauth_params = { 'clientID': x_plex_headers['X-Plex-Client-Identifier'], @@ -177,17 +180,17 @@ function PlexOAuth(success, error, pre, clientID) { 'code': code } - plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params); + plex_oauth_window.location = `https://app.plex.tv/auth/#!?${encodeData(oauth_params)}` polling = pin; (function poll() { $.ajax({ - url: 'https://plex.tv/api/v2/pins/' + pin, + url: `https://plex.tv/api/v2/pins/${pin}`, type: 'GET', headers: x_plex_headers, success: function (data) { - if (data.authToken){ - closePlexOAuthWindow(); + if (data.authToken) { + closePlexOAuthWindow() if (typeof success === "function") { success(data.authToken) } @@ -195,24 +198,24 @@ function PlexOAuth(success, error, pre, clientID) { }, error: function (jqXHR, textStatus, errorThrown) { if (textStatus !== "timeout") { - closePlexOAuthWindow(); + closePlexOAuthWindow() if (typeof error === "function") { error() } } }, complete: function () { - if (!plex_oauth_window.closed && polling === pin){ - setTimeout(function() {poll()}, 1000); + if (!plex_oauth_window.closed && polling === pin) { + setTimeout(function () { poll() }, 1000) } }, timeout: 10000 - }); - })(); + }) + })() }, function () { - closePlexOAuthWindow(); + closePlexOAuthWindow() if (typeof error === "function") { error() } - }); -} \ No newline at end of file + }) +} diff --git a/Contents/Resources/web/templates/login.html b/Contents/Resources/web/templates/login.html index f8b7ede8..93e9a2d0 100644 --- a/Contents/Resources/web/templates/login.html +++ b/Contents/Resources/web/templates/login.html @@ -15,7 +15,8 @@
    - +
    @@ -27,25 +28,25 @@ -{% endblock scripts %} +{% endblock scripts %} \ No newline at end of file From 224417e3c21dc9f6c6c51e0a67d6282a12076cb5 Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sat, 23 Dec 2023 18:30:17 +0100 Subject: [PATCH 12/17] style: extract function --- Contents/Code/webapp.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Contents/Code/webapp.py b/Contents/Code/webapp.py index ee6dd374..8bb1d8d7 100644 --- a/Contents/Code/webapp.py +++ b/Contents/Code/webapp.py @@ -136,20 +136,31 @@ def render_template(*args, **kwargs): database_cache_file = os.path.join(themerr_data_directory, 'database_cache.json') database_cache_lock = Lock() -secret_file = os.path.join(themerr_data_directory, 'secret.json') -try: - with open(secret_file, 'r') as f: - app.secret_key = json.load(f)['secret'] -except Exception: - # create random secret - Log.Info('Creating random secret') - app.secret_key = uuid.uuid4().hex +def create_secret(): + """ + Create secret file with random uuid. + + Examples + -------- + >>> create_secret() + """ + secret_file = os.path.join(themerr_data_directory, 'secret.json') try: - with open(secret_file, 'w') as f: - json.dump({'secret': app.secret_key}, f) - except Exception as e: - Log.Error('Error saving secret: {}'.format(e)) + with open(secret_file, 'r') as f: + app.secret_key = json.load(f)['secret'] + except Exception: + # create random secret + Log.Info('Creating random secret') + app.secret_key = uuid.uuid4().hex + try: + with open(secret_file, 'w') as f: + json.dump({'secret': app.secret_key}, f) + except Exception as e: + Log.Error('Error saving secret: {}'.format(e)) + + +create_secret() responses = { From 4558686560d4e1b31e3b8f8252a780dc69785b9e Mon Sep 17 00:00:00 2001 From: Tom Niget Date: Sun, 24 Dec 2023 01:56:52 +0100 Subject: [PATCH 13/17] style: semicolons --- Contents/Resources/web/js/plexauth.js | 120 ++++++++++---------- Contents/Resources/web/templates/login.html | 32 +++--- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/Contents/Resources/web/js/plexauth.js b/Contents/Resources/web/js/plexauth.js index 54275898..992721b9 100644 --- a/Contents/Resources/web/js/plexauth.js +++ b/Contents/Resources/web/js/plexauth.js @@ -4,11 +4,11 @@ var p = { name: 'Unknown', version: 'Unknown', os: 'Unknown' -} +}; if (typeof platform !== 'undefined') { - p.name = platform.name - p.version = platform.version - p.os = platform.os.toString() + p.name = platform.name; + p.version = platform.version; + p.os = platform.os.toString(); } function getPlexHeaders(clientID) { @@ -24,16 +24,16 @@ function getPlexHeaders(clientID) { 'X-Plex-Device-Name': p.name + ' (Themerr)', 'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height, 'X-Plex-Language': 'en' - } + }; } function encodeData(data) { return Object.keys(data).map(function (key) { - return [key, data[key]].map(encodeURIComponent).join("=") - }).join("&") + return [key, data[key]].map(encodeURIComponent).join("="); + }).join("&"); } -var plex_oauth_window = null +var plex_oauth_window = null; const plex_oauth_loader = '