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 %}
{# Only show client list for non-spacer channels #}
{% for client in channel_clients %}
-
{% set role_icon = namespace(path='') %}
{% for group_id in (client.get('client_servergroups', '')|string).split(',') %}
{% if not role_icon.path and group_id in config.role_icons %}
{% set role_icon.path = config.role_icons[group_id] %}
{% endif %}
{% endfor %}
{% if role_icon.path %}
{% else %}
{% endif %}
{{ client.get('client_nickname', '') }}
{% endfor %}
{% 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 %}
{% if config.external_links %}
{% 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)