KTVManager_UI/app.py

253 lines
8.9 KiB
Python
Raw Permalink Normal View History

2025-05-09 16:31:14 +01:00
# app.py
2025-07-15 15:44:19 +01:00
from flask import (Flask, render_template, request, redirect, url_for, session,
send_file, jsonify, send_from_directory, Response)
2025-05-09 16:31:14 +01:00
from flask_caching import Cache
import requests.auth
import os
import base64
2025-07-15 15:44:19 +01:00
from typing import Dict, Any, Tuple, Union
from lib.datetime import filter_accounts_next_30_days, filter_accounts_expired
from lib.reqs import (get_urls, get_user_accounts, add_user_account,
delete_user_account, get_stream_names)
2025-07-13 16:00:52 +01:00
from config import DevelopmentConfig, ProductionConfig
2025-05-09 16:31:14 +01:00
2025-07-15 15:44:19 +01:00
try:
from paddleocr import PaddleOCR
from PIL import Image
import numpy as np
OCR_AVAILABLE = True
except ImportError:
OCR_AVAILABLE = False
2025-07-13 16:00:52 +01:00
2025-05-09 16:31:14 +01:00
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
app = Flask(__name__)
2025-07-13 16:00:52 +01:00
if os.environ.get("FLASK_ENV") == "production":
app.config.from_object(ProductionConfig)
else:
app.config.from_object(DevelopmentConfig)
2025-07-15 15:44:19 +01:00
2025-05-09 16:31:14 +01:00
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"})
2025-07-15 15:44:19 +01:00
if app.config.get("OCR_ENABLED") and OCR_AVAILABLE:
ocr = PaddleOCR(use_angle_cls=True, lang='en')
else:
app.config["OCR_ENABLED"] = False
2025-05-09 16:31:14 +01:00
2025-07-13 16:00:52 +01:00
app.config["SESSION_COOKIE_SECURE"] = not app.config["DEBUG"]
2025-07-15 15:44:19 +01:00
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = 60 * 60 * 24 * 365 # 1 year
cache.clear()
def get_version() -> str:
"""Retrieves the application version from the VERSION file.
2025-05-09 16:31:14 +01:00
2025-07-15 15:44:19 +01:00
Returns:
The version string, or 'dev' if the file is not found.
"""
2025-07-09 10:59:28 +01:00
try:
with open('VERSION', 'r') as f:
return f.read().strip()
except FileNotFoundError:
return 'dev'
@app.context_processor
2025-07-15 15:44:19 +01:00
def inject_version() -> Dict[str, str]:
"""Injects the version into all templates."""
2025-07-09 10:59:28 +01:00
return dict(version=get_version())
2025-05-09 16:31:14 +01:00
@app.before_request
2025-07-15 15:44:19 +01:00
def make_session_permanent() -> None:
"""Makes the user session permanent."""
2025-05-09 16:31:14 +01:00
session.permanent = True
2025-07-15 10:20:06 +01:00
@app.route('/site.webmanifest')
2025-07-15 15:44:19 +01:00
def serve_manifest() -> Response:
"""Serves the site manifest file."""
return send_from_directory(
os.path.join(app.root_path, 'static'),
'site.webmanifest',
mimetype='application/manifest+json'
)
2025-05-09 16:31:14 +01:00
@app.route("/favicon.ico")
2025-07-15 15:44:19 +01:00
def favicon() -> Response:
"""Serves the favicon."""
2025-05-09 16:31:14 +01:00
return send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
@app.route("/")
2025-07-15 15:44:19 +01:00
def index() -> Union[Response, str]:
"""Renders the index page or redirects to home if logged in."""
2025-05-09 16:31:14 +01:00
if session.get("logged_in"):
return redirect(url_for("home"))
return render_template("index.html")
@app.route("/home")
2025-07-15 15:44:19 +01:00
@cache.cached(timeout=60)
def home() -> str:
"""Renders the home page with account statistics."""
2025-05-09 16:31:14 +01:00
if session.get("logged_in"):
2025-07-15 15:44:19 +01:00
base_url = app.config["BASE_URL"]
2025-05-09 16:31:14 +01:00
all_accounts = get_user_accounts(base_url, session["auth_credentials"])
return render_template(
"home.html",
username=session["username"],
2025-07-15 15:44:19 +01:00
accounts=len(all_accounts),
current_month_accounts=filter_accounts_next_30_days(all_accounts),
expired_accounts=filter_accounts_expired(all_accounts),
ocr_enabled=app.config.get("OCR_ENABLED"),
2025-05-09 16:31:14 +01:00
)
return render_template("index.html")
@app.route("/login", methods=["POST"])
2025-07-15 15:44:19 +01:00
def login() -> Union[Response, str]:
"""Handles user login."""
2025-05-09 16:31:14 +01:00
username = request.form["username"]
password = request.form["password"]
credentials = f"{username}:{password}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
2025-07-15 15:44:19 +01:00
base_url = app.config["BASE_URL"]
login_url = f"{base_url}/Login"
2025-05-09 16:31:14 +01:00
2025-07-15 15:44:19 +01:00
try:
response = requests.get(
login_url, auth=requests.auth.HTTPBasicAuth(username, password)
)
response.raise_for_status()
if response.json().get("auth") == "Success":
session["logged_in"] = True
session["username"] = username
session["auth_credentials"] = encoded_credentials
return redirect(url_for("home"))
except requests.exceptions.RequestException:
pass # Fall through to error
error = "Invalid username or password. Please try again."
return render_template("index.html", error=error)
2025-05-09 16:31:14 +01:00
@app.route("/urls", methods=["GET"])
2025-07-15 15:44:19 +01:00
@cache.cached(timeout=300)
def urls() -> Union[Response, str]:
"""Renders the URLs page."""
2025-05-09 16:31:14 +01:00
if not session.get("logged_in"):
return redirect(url_for("home"))
2025-07-15 15:44:19 +01:00
base_url = app.config["BASE_URL"]
2025-05-09 16:31:14 +01:00
return render_template(
"urls.html", urls=get_urls(base_url, session["auth_credentials"])
)
@app.route("/accounts", methods=["GET"])
2025-07-15 15:44:19 +01:00
def user_accounts() -> Union[Response, str]:
"""Renders the user accounts page."""
2025-05-09 16:31:14 +01:00
if not session.get("logged_in"):
return redirect(url_for("home"))
2025-07-15 15:44:19 +01:00
base_url = app.config["BASE_URL"]
2025-07-14 20:09:17 +01:00
user_accounts_data = get_user_accounts(base_url, session["auth_credentials"])
cache.delete_memoized(user_accounts)
2025-05-09 16:31:14 +01:00
return render_template(
"user_accounts.html",
username=session["username"],
2025-07-14 20:09:17 +01:00
user_accounts=user_accounts_data,
2025-05-09 16:31:14 +01:00
auth=session["auth_credentials"],
)
@app.route("/accounts/add", methods=["GET", "POST"])
2025-07-15 15:44:19 +01:00
def add_account() -> Union[Response, str]:
"""Handles adding a new user account."""
2025-07-15 08:02:51 +01:00
base_url = app.config["BASE_URL"]
shared_text = request.args.get('shared_text')
2025-05-09 16:31:14 +01:00
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
stream = request.form["stream"]
if add_user_account(
base_url, session["auth_credentials"], username, password, stream
):
2025-07-14 20:09:17 +01:00
cache.clear()
2025-05-09 16:31:14 +01:00
return redirect(url_for("user_accounts"))
2025-07-15 15:44:19 +01:00
return render_template(
"add_account.html",
ocr_enabled=app.config.get("OCR_ENABLED"),
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED"),
shared_text=shared_text
)
2025-05-09 16:31:14 +01:00
@app.route("/accounts/delete", methods=["POST"])
2025-07-15 15:44:19 +01:00
def delete_account() -> Response:
"""Handles deleting a user account."""
2025-05-09 16:31:14 +01:00
stream = request.form.get("stream")
username = request.form.get("username")
base_url = app.config["BASE_URL"]
2025-07-15 15:44:19 +01:00
delete_user_account(base_url, session["auth_credentials"], stream, username)
2025-05-09 16:31:14 +01:00
return redirect(url_for("user_accounts"))
2025-07-15 15:13:16 +01:00
@app.route("/validateAccount", methods=["POST"])
2025-07-15 15:44:19 +01:00
def validate_account() -> Tuple[Response, int]:
"""Forwards account validation requests to the backend."""
2025-07-15 15:13:16 +01:00
base_url = app.config["BASE_URL"]
validate_url = f"{base_url}/validateAccount"
credentials = base64.b64decode(session["auth_credentials"]).decode()
username, password = credentials.split(":", 1)
2025-07-15 15:44:19 +01:00
try:
response = requests.post(
validate_url,
auth=requests.auth.HTTPBasicAuth(username, password),
json=request.get_json()
)
response.raise_for_status()
return jsonify(response.json()), response.status_code
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 500
2025-07-15 15:13:16 +01:00
2025-07-05 10:57:19 +01:00
@app.route("/get_stream_names", methods=["GET"])
2025-07-15 15:44:19 +01:00
def stream_names() -> Union[Response, str]:
"""Fetches and returns stream names as JSON."""
2025-07-05 10:57:19 +01:00
if not session.get("logged_in"):
return redirect(url_for("home"))
base_url = app.config["BASE_URL"]
2025-07-15 15:44:19 +01:00
return jsonify(get_stream_names(base_url, session["auth_credentials"]))
2025-07-05 10:57:19 +01:00
if app.config.get("OCR_ENABLED"):
@app.route('/OCRupload', methods=['POST'])
2025-07-15 15:44:19 +01:00
def ocr_upload() -> Union[Response, str, Tuple[Response, int]]:
"""Handles image uploads for OCR processing."""
if 'image' not in request.files:
return jsonify({"error": "No image file found"}), 400
file = request.files['image']
try:
image = Image.open(file.stream)
image_np = np.array(image)
result = ocr.ocr(image_np)
2025-07-15 15:44:19 +01:00
extracted_text = [line[1][0] for line in result[0]]
# Basic validation
if len(extracted_text) >= 4:
return render_template(
"add_account.html",
username=extracted_text[2],
password=extracted_text[3],
ocr_enabled=True,
text_input_enabled=app.config.get("TEXT_INPUT_ENABLED")
)
else:
return jsonify({"error": "Could not extract required fields from image"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
2025-05-09 16:31:14 +01:00
if __name__ == "__main__":
2025-07-15 15:44:19 +01:00
app.run(
debug=app.config["DEBUG"],
host=app.config["HOST"],
port=app.config["PORT"]
)