First Public Release!

This commit is contained in:
AshiePleb
2026-06-16 22:25:33 +01:00
parent 50c62a9765
commit 4b8c7a0220
11 changed files with 3554 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
flask>=3.0
flask-cors>=4.0
+1237
View File
File diff suppressed because it is too large Load Diff
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""
Worker API — runs alongside the dashboard on Windows by default (port 5001).
Exposes the HEVCWorker over HTTP so the dashboard can control scan/process jobs.
"""
import json
import time
from collections import deque
from flask import Flask, jsonify, request, Response, stream_with_context
from flask_cors import CORS
from worker import worker
app = Flask(__name__)
CORS(app) # Allow cross-origin requests from the dashboard
# ---------------------------------------------------------------------------
# Status & control
# ---------------------------------------------------------------------------
@app.route("/api/status")
def api_status():
return jsonify(worker.get_status())
def get_hardware_info():
import os
import subprocess
import platform
cpu_name = "Unknown CPU"
gpu_name = "Unknown GPU"
# CPU Detection
if os.name == "nt":
try:
import winreg
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
cpu_name = winreg.QueryValueEx(key, "ProcessorNameString")[0].strip()
except Exception:
out = subprocess.check_output("wmic cpu get name", shell=True, text=True)
lines = [l.strip() for l in out.splitlines() if l.strip()]
if len(lines) > 1:
cpu_name = lines[1]
except Exception:
pass
else:
# Linux
try:
if os.path.exists("/proc/cpuinfo"):
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line.casefold() or "hardware" in line.casefold():
cpu_name = line.split(":", 1)[1].strip()
break
except Exception:
pass
if not cpu_name or cpu_name == "Unknown CPU":
try:
out = subprocess.check_output("lscpu", shell=True, text=True)
for line in out.splitlines():
if "Model name:" in line:
cpu_name = line.split(":", 1)[1].strip()
break
except Exception:
pass
if not cpu_name or cpu_name == "Unknown CPU":
try:
cpu_name = platform.processor() or "Unknown CPU"
except Exception:
pass
# GPU Detection
try:
if os.name == "nt":
# Try PowerShell first since wmic is deprecated/removed in newer Windows 11
try:
out = subprocess.check_output(
["powershell", "-NoProfile", "-Command", "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name"],
text=True,
stderr=subprocess.DEVNULL
)
gpus = [l.strip() for l in out.splitlines() if l.strip()]
if gpus:
gpu_name = ", ".join(gpus)
except Exception:
# Fallback to wmic
try:
out = subprocess.check_output("wmic path win32_VideoController get name", shell=True, text=True, stderr=subprocess.DEVNULL)
lines = [l.strip() for l in out.splitlines() if l.strip()]
if len(lines) > 1:
gpus = [l for l in lines[1:] if l and l.lower() != "name"]
if gpus:
gpu_name = ", ".join(gpus)
except Exception:
pass
else:
# Linux
gpus = []
try:
out = subprocess.check_output("lspci", shell=True, text=True, stderr=subprocess.DEVNULL)
for line in out.splitlines():
if "VGA compatible controller" in line or "3D controller" in line:
parts = line.split(":", 2)
if len(parts) > 2:
gpus.append(parts[2].strip())
except Exception:
pass
if not gpus:
try:
out = subprocess.check_output("nvidia-smi --query-gpu=name --format=csv,noheader", shell=True, text=True, stderr=subprocess.DEVNULL)
for line in out.splitlines():
if line.strip():
gpus.append(line.strip())
except Exception:
pass
if not gpus:
try:
if os.path.isdir("/sys/class/drm"):
for card in os.listdir("/sys/class/drm"):
if card.startswith("card") and "-" not in card:
uevent_path = f"/sys/class/drm/{card}/device/uevent"
if os.path.exists(uevent_path):
with open(uevent_path, "r") as f:
for line in f:
if line.startswith("DRIVER="):
driver = line.split("=")[1].strip()
gpus.append(f"{driver.capitalize()} (DRM)")
except Exception:
pass
if gpus:
gpus = list(dict.fromkeys(gpus))
gpu_name = ", ".join(gpus)
except Exception:
pass
return {"cpu": cpu_name, "gpu": gpu_name}
@app.route("/api/sysinfo")
def api_sysinfo():
import os
info = get_hardware_info()
return jsonify({
"cpu_count": os.cpu_count(),
"recommended_threads": worker._recommended_workers(),
"cpu_name": info["cpu"],
"gpu_name": info["gpu"]
})
@app.route("/api/test-webhook", methods=["POST"])
def api_test_webhook():
config = request.get_json() or {}
webhook_url = str(config.get("discord_webhook") or "").strip()
if not webhook_url:
return jsonify({"error": "No webhook URL provided"}), 400
from worker import _send_discord_webhook
try:
_send_discord_webhook(webhook_url, "test_file_hello_world.mkv", 1024 * 1024 * 1024)
return jsonify({"ok": True})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/scan", methods=["POST"])
def api_scan():
config = request.get_json() or {}
return jsonify(worker.start_scan(config))
@app.route("/api/process", methods=["POST"])
def api_process():
config = request.get_json() or {}
return jsonify(worker.start_process(config))
@app.route("/api/database/clear", methods=["POST"])
def api_database_clear():
worker.clear_database()
return jsonify({"ok": True})
@app.route("/api/pause", methods=["POST"])
def api_pause():
worker.pause()
return jsonify({"ok": True})
@app.route("/api/resume", methods=["POST"])
def api_resume():
worker.resume()
return jsonify({"ok": True})
@app.route("/api/stop", methods=["POST"])
def api_stop():
worker.stop()
return jsonify({"ok": True})
# ---------------------------------------------------------------------------
# Log endpoints
# ---------------------------------------------------------------------------
@app.route("/api/browse")
def api_browse():
"""
List subdirectories of a given path on the server filesystem.
Query param: ?path=/mnt (defaults to /)
Returns: { path, parent, dirs: [{name, full_path}] }
"""
import os
req_path = request.args.get("path", "/").strip()
req_path = os.path.normpath(req_path)
if not os.path.isdir(req_path):
return jsonify({"error": f"Not a directory: {req_path}"}), 400
try:
entries = []
for name in sorted(os.listdir(req_path)):
full = os.path.join(req_path, name)
if os.path.isdir(full) and not name.startswith("."):
entries.append({"name": name, "full_path": full})
except PermissionError:
return jsonify({"error": "Permission denied"}), 403
parent = str(os.path.dirname(req_path)) if req_path != "/" else None
return jsonify({"path": req_path, "parent": parent, "dirs": entries})
@app.route("/api/drives")
def api_drives():
"""Return all active drive letters on Windows, or root on Linux/macOS."""
import os
if os.name != "nt":
return jsonify(["/"])
import string
drives = []
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
drives.append(drive.replace("\\", "/"))
return jsonify(drives)
@app.route("/api/logs")
def api_logs():
"""Return all buffered log entries as JSON."""
return jsonify(worker.get_logs())
@app.route("/api/logs/stream")
def api_logs_stream():
"""
Server-Sent Events endpoint.
Sends all existing buffered logs first, then streams new ones live.
"""
subscriber_q: deque = deque(maxlen=500)
worker.subscribe_logs(subscriber_q)
# Pre-fill with existing buffer so client sees history on connect
for entry in worker.get_logs():
subscriber_q.append(entry)
def generate():
last_keepalive = time.time()
try:
while True:
if subscriber_q:
entry = subscriber_q.popleft()
yield f"data: {json.dumps(entry)}\n\n"
last_keepalive = time.time()
else:
time.sleep(0.1)
if time.time() - last_keepalive > 15:
yield ": keepalive\n\n"
last_keepalive = time.time()
except GeneratorExit:
pass
finally:
worker.unsubscribe_logs(subscriber_q)
return Response(
stream_with_context(generate()),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
},
)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse, os as _os
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int,
default=int(_os.environ.get("WORKER_PORT", 5001)))
args = parser.parse_args()
print(f"HEVC Worker API starting on 0.0.0.0:{args.port}")
app.run(host="0.0.0.0", port=args.port, debug=False, threaded=True)