import json import time from pathlib import Path from urllib.parse import quote import requests from flask import Flask, Response, render_template_string, request, send_from_directory from flask_cors import CORS from config import BASE_DIR, CONFIG app = Flask(__name__) CORS(app) ALLOWED_STATIC_EXTENSIONS = { ".css", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".ico", } def safe_url(filename): return quote(str(filename or ""), safe="/") def fetch_teamspeak_data(cfg): cache_file = Path(cfg["cache_file"]) if cache_file.exists() and (time.time() - cache_file.stat().st_mtime) < cfg["cache_time"]: try: return json.loads(cache_file.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): pass base_url = f"https://{cfg['host']}:{cfg['port']}/{cfg['server_id']}" headers = {"x-api-key": cfg["api_key"]} try: channels_res = requests.get(f"{base_url}/channellist", headers=headers, timeout=2) clients_res = requests.get(f"{base_url}/clientlist?-groups", headers=headers, timeout=2) channels_json = channels_res.json() clients_json = clients_res.json() except requests.RequestException: return {"channels": [], "clients": []} except ValueError: return {"channels": [], "clients": []} data = { "channels": channels_json.get("body") or [], "clients": clients_json.get("body") or [], } try: cache_file.write_text(json.dumps(data), encoding="utf-8") except OSError: pass return data def same_id(left, right): return str(left or 0) == str(right or 0) def sort_ts_channels(channels, pid=0): ordered = [] by_order = {} for channel in channels: if same_id(channel.get("pid", 0), pid): by_order[str(channel.get("channel_order", 0))] = channel current_order = "0" max_iterations = len(channels) + 1 iterations = 0 while current_order in by_order and iterations < max_iterations: channel = by_order[current_order] ordered.append(channel) ordered.extend(sort_ts_channels(channels, channel.get("cid"))) current_order = str(channel.get("cid")) iterations += 1 return ordered def build_context(): start_time = time.perf_counter() ts_data = fetch_teamspeak_data(CONFIG["server"]) api_ping = round((time.perf_counter() - start_time) * 1000) raw_channels = ts_data.get("channels") or [] afk_count = None for channel in raw_channels: original_name = channel.get("channel_name", "") channel["is_spacer"] = "[Cspacer]" in original_name and "AFK" not in original_name if "AFK" in original_name: afk_count = channel.get("total_clients", None) channel["channel_name"] = original_name.replace("[Cspacer]", "") raw_clients = ts_data.get("clients") or [] is_online = bool(raw_channels) if afk_count is not None: afk_count = int(afk_count) - 1 channels = sort_ts_channels(raw_channels, 0) clients_by_channel = {} total_real_clients = 0 if is_online: for client in raw_clients: if int(client.get("client_type") or 0) == 1: continue clients_by_channel.setdefault(str(client.get("cid")), []).append(client) total_real_clients += 1 return { "config": CONFIG, "api_ping": api_ping, "channels": channels, "clients_by_channel": clients_by_channel, "is_online": is_online, "safe_url": safe_url, "total_real_clients": total_real_clients, "afk_count": afk_count, } FRAGMENT_TEMPLATE = """ {% if is_online %}
Users Online {{ total_real_clients }}
{% if afk_count is not none %}
AFK Users {{ afk_count }}
{% endif %}
{% for channel in channels %} {% set c_name = channel.get('channel_name', '') %} {% set cid = channel.get('cid')|string %} {% set channel_clients = clients_by_channel.get(cid, []) %} {% set bg_img = config.channel_banners.get(c_name, config.generic_images.get('background', '')) %} {% set icon_img = config.channel_icons.get(c_name, config.generic_images.get('icon', '')) %} {% if bg_img %} {% set bg_style = "background-image: linear-gradient(90deg, rgba(9, 18, 32, 0.96) 0%, rgba(9, 18, 32, 0.78) 45%, rgba(9, 18, 32, 0.94) 100%), url('" ~ safe_url(bg_img) ~ "');" %} {% else %} {% set bg_style = "" %} {% endif %} {% if not channel.is_spacer %}
{% if icon_img %} {% endif %}

{{ c_name }}

{# Only show client count for non-spacer channels #} {{ channel_clients|length }}
{# Only show client list for non-spacer channels #}
{% endif %} {% endfor %}
{% else %}
Unable to reach the TeamSpeak server.
{% endif %} """ PAGE_TEMPLATE = """ {{ config.ui.title }}
{% if is_online %}
{% endif %} {% if config.ui.banner_url %} {% endif %}
{{ html_fragment|safe }}
{% if config.external_links %} {% endif %}

{{ config.ui.title }}

{{ config.ui.subtitle }}

{% if is_online %}
SERVER ONLINE
{% else %}
SERVER OFFLINE
{% endif %}
{{ config.buttons.join }}

{{ config.buttons.download_text }} {{ config.buttons.download_link }}

""" def render_fragment(context): return render_template_string(FRAGMENT_TEMPLATE, **context) @app.route("/") def index(): context = build_context() html_fragment = render_fragment(context) if request.args.get("ajax") == "1": return Response(html_fragment, mimetype="text/html; charset=utf-8") return render_template_string(PAGE_TEMPLATE, html_fragment=html_fragment, **context) @app.route("/") def static_asset(filename): path = (BASE_DIR / filename).resolve() if ( path.is_file() and BASE_DIR in path.parents and path.suffix.lower() in ALLOWED_STATIC_EXTENSIONS ): return send_from_directory(BASE_DIR, filename) return Response("Not found", status=404) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)