First Public Release!
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user