import sys
import os
import json
import re
import shutil
import random
import stat
import gc
import threading
import time
from urllib.request import Request, urlopen
# ព្យាយាមទាញយកបណ្ណាល័យ psutil សម្រាប់វាស់ទិន្នន័យម៉ាស៊ីន
try:
import psutil
except ImportError:
psutil = None
# =========================================================
# ១. ប្រើប្រាស់តែ Flags ដើមដំបូងបង្អស់ដែលមានស្ថិរភាព ១០០%
# =========================================================
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = (
"--disable-features=FFmpegAllowLists "
"--disable-web-security "
"--allow-running-insecure-content "
"--no-sandbox "
"--ignore-certificate-errors "
"--disable-gpu "
"--disable-gpu-sandbox"
)
from PyQt6.QtCore import QUrl, Qt, QSize, QThread, pyqtSignal, QTimer, QRect, QObject
from PyQt6.QtGui import QColor, QFont, QIcon, QAction, QPainter
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QPushButton, QLineEdit
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage, QWebEngineScript
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
# ឯកសារ Database
DB_PATH = "./data/instances.json"
STORES_PATH = "./data/stores.json"
URLS_PATH = "./data/urls.json"
CONFIG_PATH = "./data/config.json"
DEFAULT_URL = "https://www.facebook.com"
PORT = 8000
DEVICES = {
"Compact Mobile (360x500)": {
"width": 360, "height": 500,
"ua": "Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
},
"Samsung Galaxy S23 (360x780)": {
"width": 360, "height": 780,
"ua": "Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
},
"Google Pixel 8 (412x892)": {
"width": 412, "height": 892,
"ua": "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
},
"iPhone 15 Pro (393x852)": {
"width": 393, "height": 852,
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
},
"iPad Pro 12.9 (1024x1366)": {
"width": 1024, "height": 1366,
"ua": "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
}
}
GLOBAL_PROFILES_CACHE = {}
running_windows = {}
instance_statuses = {}
calculated_sizes = {}
# --- Helper Functions ---
def get_dir_size(path):
total_size = 0
if not os.path.exists(path):
return 0
try:
for dirpath, dirnames, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
except Exception:
pass
return total_size
def format_size(bytes_size):
if bytes_size <= 0: return "0 KB"
kb_size = bytes_size / 1024
if kb_size < 1024: return f"{kb_size:.1f} KB"
mb_size = kb_size / 1024
if mb_size < 1024: return f"{mb_size:.1f} MB"
return f"{mb_size/1024:.1f} GB"
def load_json(path, default):
if not os.path.exists(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(default, f, ensure_ascii=False, indent=4)
return default
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def save_json(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
# ផ្ទុកទិន្នន័យដំបូង
instances = load_json(DB_PATH, [{"id": 1, "name": "គណនី #1", "device": "Compact Mobile (360x500)", "url": DEFAULT_URL, "scale": 0.50, "ip": "", "city": "", "note": "", "store": "ទូទៅ (Default)"}])
stores = load_json(STORES_PATH, ["ទូទៅ (Default)"])
urls_db = load_json(URLS_PATH, [DEFAULT_URL, "https://www.youtube.com", "https://www.google.com", "https://www.tiktok.com"])
app_config = load_json(CONFIG_PATH, {"chk_auto_scroll": True, "delay_spin": 3, "auto_close_spin": 0, "arrange_type_idx": 0, "cols_spin": 3, "preset_speed": 2, "preset_mode_idx": 1})
def get_or_create_profile(instance_id, parent_widget):
profile_name = f"instance_{instance_id}"
if profile_name in GLOBAL_PROFILES_CACHE:
return GLOBAL_PROFILES_CACHE[profile_name]
data_dir = os.path.abspath("./data")
profile = QWebEngineProfile(profile_name, parent_widget)
profile.setPersistentStoragePath(os.path.join(data_dir, profile_name, "storage"))
profile.setCachePath(os.path.join(data_dir, profile_name, "cache"))
profile.setPersistentCookiesPolicy(QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies)
GLOBAL_PROFILES_CACHE[profile_name] = profile
return profile
# =========================================================
# ២. ប្រព័ន្ធស្កេន IP និងស្វែងរកទីតាំង (Background Thread)
# =========================================================
class IpFetcher(QThread):
ip_fetched = pyqtSignal(str, str)
def run(self):
import urllib.request
opener = urllib.request.build_opener()
try:
req = Request("https://ipapi.co/json/", headers={'User-Agent': 'Mozilla/5.0'})
with opener.open(req, timeout=6) as response:
data = json.loads(response.read().decode())
self.ip_fetched.emit(data.get("ip", "Unknown IP"), data.get("city", "Unknown City"))
return
except Exception: pass
try:
req = Request("https://freeipapi.com/api/json", headers={'User-Agent': 'Mozilla/5.0'})
with opener.open(req, timeout=6) as response:
data = json.loads(response.read().decode())
self.ip_fetched.emit(data.get("ipAddress", "Unknown IP"), data.get("cityName", "Unknown City"))
return
except Exception:
self.ip_fetched.emit("127.0.0.1", "Local Connection")
class CustomWebEnginePage(QWebEnginePage):
console_message_signal = pyqtSignal(int, str, int, str)
def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
self.console_message_signal.emit(level, message, lineNumber, sourceID)
super().javaScriptConsoleMessage(level, message, lineNumber, sourceID)
# =========================================================
# ៣. ថ្នាក់បង្កើតផ្ទាំងទូរសព្ទ (Preserved PyQt6 Mobile Frame)
# =========================================================
class MobileSimulator(QMainWindow):
def __init__(self, instance_id=1, device_name=None, initial_url=DEFAULT_URL, scale_factor=0.75):
super().__init__()
self.instance_id = instance_id
self.current_device_name = device_name if device_name in DEVICES else list(DEVICES.keys())[0]
self.home_url = initial_url if initial_url else DEFAULT_URL
self.scale_factor = scale_factor if scale_factor else 0.75
self.is_landscape = False
self.is_auto_scrolling = False
self.auto_close_on_finish = False
self.is_loading_started = False
# បង្ខំឱ្យ PyQt6 កម្ទេច និងដោះលែង Memory របស់ Window នេះភ្លាមៗនៅពេលចុចបិទ (Close)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.remaining_seconds = 0
self.countdown_timer = QTimer(self)
self.countdown_timer.timeout.connect(self.update_countdown)
self.setWindowTitle(f"ទូរសព្ទតេស្ត - គណនី #{self.instance_id}")
self.setStyleSheet("background-color: #0f1115;")
self.profile = get_or_create_profile(self.instance_id, self)
self.webpage = CustomWebEnginePage(self.profile, self)
self.webpage.setBackgroundColor(QColor("white"))
self.browser = QWebEngineView()
self.browser.setPage(self.webpage)
self.browser.setStyleSheet("border: none; background-color: white; border-radius: 0px 0px 18px 18px;")
self.browser.loadStarted.connect(self.on_load_started)
self.browser.loadFinished.connect(self.on_load_finished)
self.browser.urlChanged.connect(self.update_url_bar)
main_widget = QWidget()
self.setCentralWidget(main_widget)
self.main_layout = QVBoxLayout(main_widget)
self.main_layout.setContentsMargins(10, 10, 10, 10)
self.main_layout.setSpacing(5)
self.phone_frame = QFrame()
self.phone_frame.setStyleSheet("QFrame { background-color: #020617; border: 8px solid #1e293b; border-radius: 32px; }")
self.phone_inner_layout = QVBoxLayout(self.phone_frame)
self.phone_inner_layout.setContentsMargins(6, 6, 6, 6)
self.phone_inner_layout.setSpacing(6)
self.notch_frame = QFrame()
self.notch_frame.setFixedHeight(22)
self.notch_frame.setStyleSheet("background-color: transparent; border: none;")
notch_layout = QHBoxLayout(self.notch_frame)
notch_layout.setContentsMargins(0, 0, 0, 0)
self.notch_element = QFrame()
self.notch_element.setFixedSize(90, 14)
self.notch_element.setStyleSheet("background-color: #000000; border-radius: 7px; border: none;")
notch_layout.addWidget(self.notch_element, alignment=Qt.AlignmentFlag.AlignCenter)
self.phone_inner_layout.addWidget(self.notch_frame)
browser_bar = QHBoxLayout()
browser_bar.setContentsMargins(4, 0, 4, 0)
browser_bar.setSpacing(6)
self.btn_back = QPushButton("◀")
self.btn_back.setStyleSheet(self.get_icon_style())
self.btn_back.clicked.connect(self.browser.back)
browser_bar.addWidget(self.btn_back)
self.btn_forward = QPushButton("▶")
self.btn_forward.setStyleSheet(self.get_icon_style())
self.btn_forward.clicked.connect(self.browser.forward)
browser_bar.addWidget(self.btn_forward)
self.btn_home = QPushButton("🏠")
self.btn_home.setStyleSheet(self.get_icon_style())
self.btn_home.clicked.connect(self.go_home)
browser_bar.addWidget(self.btn_home)
self.url_input = QLineEdit()
self.url_input.setText(self.home_url)
self.url_input.setStyleSheet("QLineEdit { background-color: #0f172a; color: #f8fafc; border: 1px solid #334155; border-radius: 12px; padding: 4px 10px; font-size: 11px; } QLineEdit:focus { border-color: #38bdf8; }")
self.url_input.returnPressed.connect(self.load_url)
browser_bar.addWidget(self.url_input)
self.btn_scroll = QPushButton("📜")
self.btn_scroll.setStyleSheet(self.get_icon_style())
self.btn_scroll.clicked.connect(lambda: self.toggle_auto_scroll())
browser_bar.addWidget(self.btn_scroll)
self.btn_rotate = QPushButton("🔄")
self.btn_rotate.setStyleSheet(self.get_icon_style())
self.btn_rotate.clicked.connect(self.toggle_rotation)
browser_bar.addWidget(self.btn_rotate)
self.phone_inner_layout.addLayout(browser_bar)
self.phone_inner_layout.addWidget(self.browser, 1)
self.home_bar = QFrame()
self.home_bar.setFixedHeight(12)
self.home_bar.setStyleSheet("background-color: transparent; border: none;")
home_bar_layout = QHBoxLayout(self.home_bar)
indicator = QFrame()
indicator.setFixedSize(110, 4)
indicator.setStyleSheet("background-color: #475569; border-radius: 2px;")
home_bar_layout.addWidget(indicator, alignment=Qt.AlignmentFlag.AlignCenter)
self.phone_inner_layout.addWidget(self.home_bar)
self.main_layout.addWidget(self.phone_frame, alignment=Qt.AlignmentFlag.AlignCenter)
self.scroll_timer = QTimer(self)
self.scroll_timer.timeout.connect(self.scroll_page)
self.apply_device_settings()
self.load_url()
self.ip_fetcher = IpFetcher(self)
self.ip_fetcher.ip_fetched.connect(self.on_ip_fetched)
self.ip_fetcher.start()
def get_icon_style(self):
return "QPushButton { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 13px; min-width: 26px; min-height: 26px; max-width: 26px; max-height: 26px; font-size: 10px; } QPushButton:hover { background-color: #38bdf8; color: #0f1115; }"
def on_load_started(self):
self.is_loading_started = True
self.update_status("⏳ កំពុងផ្ទុក...")
def on_load_finished(self, ok):
self.is_loading_started = False
self.update_status("🟢 Online")
def on_ip_fetched(self, ip, city):
global instances
for inst in instances:
if inst["id"] == self.instance_id:
inst["ip"] = ip
inst["city"] = city
break
save_json(DB_PATH, instances)
def update_status(self, text):
instance_statuses[self.instance_id] = text
def start_countdown(self, duration_min, auto_close=True):
if duration_min > 0:
self.remaining_seconds = duration_min * 60
self.auto_close_on_finish = auto_close
self.countdown_timer.start(1000)
def update_countdown(self):
if self.remaining_seconds > 0:
self.remaining_seconds -= 1
mins = self.remaining_seconds // 60
secs = self.remaining_seconds % 60
self.update_status(f"📜 ស្រូល ({mins:02d}:{secs:02d})")
else:
self.countdown_timer.stop()
if self.auto_close_on_finish:
self.close()
def toggle_auto_scroll(self, force_state=None, speed=2, duration_min=0, random_mode=True, auto_close=None):
if force_state is not None:
self.is_auto_scrolling = force_state
else:
self.is_auto_scrolling = not self.is_auto_scrolling
if auto_close is not None:
self.auto_close_on_finish = auto_close
if self.is_auto_scrolling:
self.scroll_speed_base = speed
self.scroll_speed = max(1, speed + random.randint(-1, 1))
self.scroll_direction = 1
self.random_mode = random_mode
self.ticks_since_dir_change = 0
self.next_dir_change_ticks = random.randint(100, 300)
self.scroll_timer.start(random.randint(35, 55))
if duration_min > 0:
self.start_countdown(duration_min, auto_close)
else:
self.update_status("📜 កំពុងស្រូល...")
else:
self.scroll_timer.stop()
self.countdown_timer.stop()
self.update_status("🟢 Online")
def scroll_page(self):
if not hasattr(self, 'browser') or self.browser is None:
self.scroll_timer.stop()
return
try:
if self.random_mode:
self.ticks_since_dir_change += 1
if self.ticks_since_dir_change >= self.next_dir_change_ticks:
self.ticks_since_dir_change = 0
self.next_dir_change_ticks = random.randint(100, 300)
self.scroll_direction = -1 if random.random() < 0.20 else 1
self.scroll_speed = max(1, self.scroll_speed_base + random.randint(-1, 1))
self.browser.page().runJavaScript(f"window.scrollBy(0, {self.scroll_direction * self.scroll_speed});")
except Exception:
self.scroll_timer.stop()
def load_url(self):
url = self.url_input.text().strip()
if not url: return
if not url.startswith("http://") and not url.startswith("https://"):
url = "https://" + url
self.browser.setUrl(QUrl(url))
def update_url_bar(self, qurl):
self.url_input.setText(qurl.toString())
def go_home(self):
self.browser.setUrl(QUrl(self.home_url))
def toggle_rotation(self):
self.is_landscape = not self.is_landscape
self.apply_device_settings()
def apply_device_settings(self):
device = DEVICES[self.current_device_name]
w, h, ua = device["width"], device["height"], device["ua"]
self.profile.setHttpUserAgent(ua)
self.profile.scripts().clear()
js_code = ""
if "Android" in ua:
js_code = "Object.defineProperty(navigator, 'platform', { get: () => 'Linux armv8l' });"
elif "iPhone" in ua:
js_code = "Object.defineProperty(navigator, 'platform', { get: () => 'iPhone' });"
if js_code:
script = QWebEngineScript()
script.setSourceCode(js_code)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
self.profile.scripts().insert(script)
if self.is_landscape:
w, h = h, w
self.notch_frame.hide()
else:
self.notch_frame.show()
w_scaled = int(w * self.scale_factor)
h_scaled = int(h * self.scale_factor)
self.phone_frame.setFixedSize(w_scaled + 24, h_scaled + (60 if self.is_landscape else 85))
self.setFixedSize(w_scaled + 44, h_scaled + (80 if self.is_landscape else 105))
self.browser.setZoomFactor(self.scale_factor)
def closeEvent(self, event):
self.scroll_timer.stop()
self.countdown_timer.stop()
# បញ្ឈប់ និងសម្អាត WebEngine ដាច់ខាត ដើម្បីដោះសោរ (Unlock) ទិន្នន័យ Profile របស់គណនី
if hasattr(self, 'browser') and self.browser is not None:
self.browser.setPage(None)
self.webpage.deleteLater()
self.browser.deleteLater()
self.browser = None
if self.instance_id in running_windows:
del running_windows[self.instance_id]
instance_statuses[self.instance_id] = "🔴 Offline"
event.accept()
# =========================================================
# ៤. Web App Control Panel (កែកែប្រែការសេឡិចគណនី - Zero Selection Loss)
# =========================================================
HTML_PANEL = """
ប្រព័ន្ធគ្រប់គ្រងគណនី - Web Control Panel
"""
# =========================================================
# ៥. ប្រព័ន្ធទំនាក់ទំនងរវាង Web UI និង PyQt6 Threading (Coordinator)
# =========================================================
class Coordinator(QObject):
start_signal = pyqtSignal(int)
stop_signal = pyqtSignal(int)
arrange_signal = pyqtSignal()
def __init__(self):
super().__init__()
self.start_signal.connect(self.on_start)
self.stop_signal.connect(self.on_stop)
self.arrange_signal.connect(self.on_arrange)
def on_start(self, inst_id):
if inst_id not in running_windows:
inst_data = next((x for x in instances if x["id"] == inst_id), None)
if inst_data:
win = MobileSimulator(
instance_id=inst_id,
device_name=inst_data["device"],
initial_url=inst_data["url"],
scale_factor=inst_data.get("scale", 0.75)
)
win.show()
running_windows[inst_id] = win
if app_config.get("chk_auto_scroll", True):
win.toggle_auto_scroll(
force_state=True,
speed=app_config.get("preset_speed", 2),
duration_min=app_config.get("auto_close_spin", 0),
random_mode=(app_config.get("preset_mode_idx", 1) == 1),
auto_close=(app_config.get("auto_close_spin", 0) > 0)
)
def on_stop(self, inst_id):
if inst_id in running_windows:
running_windows[inst_id].close()
def on_arrange(self):
if not running_windows: return
arrange_type = "Columns" if app_config.get("arrange_type_idx", 0) == 0 else "Rows"
count_limit = app_config.get("cols_spin", 3)
margin_x, margin_y, spacing = 20, 50, 15
current_col, current_row = 0, 0
sorted_keys = sorted(running_windows.keys())
for idx in sorted_keys:
win = running_windows[idx]
x = margin_x + current_col * (win.width() + spacing)
y = margin_y + current_row * (win.height() + spacing)
win.move(x, y)
if "Columns" in arrange_type:
current_col += 1
if current_col >= count_limit:
current_col = 0
current_row += 1
else:
current_row += 1
if current_row >= count_limit:
current_row = 0
current_col += 1
coordinator = None
# =========================================================
# ៦. Web App API Server Handler
# =========================================================
class WebUIRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args): return
def do_GET(self):
global instances, stores, urls_db, app_config
if self.path == "/":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML_PANEL.encode("utf-8"))
elif self.path == "/api/get_stores":
self.send_json(stores)
elif self.path == "/api/get_urls":
self.send_json(urls_db)
elif self.path == "/api/get_config":
self.send_json(app_config)
elif self.path == "/api/get_resources":
cpu, ram = 0, 0
if psutil:
try:
cpu = psutil.cpu_percent()
ram = psutil.virtual_memory().percent
except Exception: pass
self.send_json({"cpu": cpu, "ram": ram})
elif self.path == "/api/get_instances":
res = []
for inst in instances:
iid = inst["id"]
status = instance_statuses.get(iid, "🔴 Offline")
size = calculated_sizes.get(iid, "Calculating...")
ip_city = inst.get("ip", "")
if ip_city and inst.get("city", ""):
ip_city = f"{inst['ip']} ({inst['city']})"
res.append({
"id": iid,
"name": inst.get("name", ""),
"store": inst.get("store", "ទូទៅ (Default)"),
"device": inst.get("device", ""),
"scale": inst.get("scale", 0.75),
"size": size,
"ip_display": ip_city,
"url": inst.get("url", DEFAULT_URL),
"note": inst.get("note", ""),
"status": status
})
self.send_json(res)
else:
self.send_error(404)
def do_POST(self):
global instances, stores, urls_db, app_config, coordinator
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length) if content_length > 0 else b""
req = json.loads(post_data.decode("utf-8")) if post_data else {}
if self.path == "/api/update_field":
iid, field, value = req["id"], req["field"], req["value"]
for inst in instances:
if inst["id"] == iid:
if field == "ip":
inst["ip"] = value
else:
inst[field] = value
break
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/add_instance":
new_id = max([x["id"] for x in instances]) + 1 if instances else 1
instances.append({
"id": new_id, "name": f"គណនី #{new_id}", "device": "Compact Mobile (360x500)",
"url": DEFAULT_URL, "scale": 0.50, "ip": "", "city": "", "note": "", "store": "ទូទៅ (Default)"
})
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/delete_instances":
ids = req["ids"]
def handle_remove_readonly(func, path, exc):
try:
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception: pass
for iid in ids:
coordinator.stop_signal.emit(iid)
inst_dir = os.path.abspath(f"./data/instance_{iid}")
if os.path.exists(inst_dir):
shutil.rmtree(inst_dir, onerror=handle_remove_readonly)
instances = [x for x in instances if x["id"] not in ids]
save_json(DB_PATH, instances)
gc.collect()
self.send_success()
elif self.path == "/api/add_store":
stores.append(req["name"])
save_json(STORES_PATH, stores)
self.send_success()
elif self.path == "/api/delete_store":
name = req["name"]
if name in stores:
stores.remove(name)
for inst in instances:
if inst.get("store") == name: inst["store"] = "ទូទៅ (Default)"
save_json(STORES_PATH, stores)
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/add_url":
new_url = req["url"]
if not new_url.startswith("http://") and not new_url.startswith("https://"):
new_url = "https://" + new_url
urls_db.append(new_url)
save_json(URLS_PATH, urls_db)
self.send_success()
elif self.path == "/api/delete_url":
if req["url"] in urls_db:
urls_db.remove(req["url"])
save_json(URLS_PATH, urls_db)
self.send_success()
elif self.path == "/api/save_scroll_preset":
app_config["preset_speed"] = req["speed"]
app_config["preset_mode_idx"] = req["behavior"]
save_json(CONFIG_PATH, app_config)
self.send_success()
elif self.path == "/api/save_config":
app_config.update(req)
save_json(CONFIG_PATH, app_config)
self.send_success()
elif self.path == "/api/arrange_windows":
coordinator.arrange_signal.emit()
self.send_success()
elif self.path == "/api/start_profiles":
ids = req["ids"]
delay = app_config.get("delay_spin", 3)
def run_queue():
for idx, iid in enumerate(ids):
if idx > 0 and delay > 0:
time.sleep(delay)
coordinator.start_signal.emit(iid)
time.sleep(1)
coordinator.arrange_signal.emit()
threading.Thread(target=run_queue, daemon=True).start()
self.send_success()
elif self.path == "/api/stop_profiles":
for iid in req["ids"]:
coordinator.stop_signal.emit(iid)
self.send_success()
else:
self.send_error(404)
def send_json(self, data):
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
def send_success(self):
self.send_json({"status": "success"})
def run_web_server():
server = ThreadingHTTPServer(("127.0.0.1", PORT), WebUIRequestHandler)
server.serve_forever()
# =========================================================
# ៧. ថ្នាក់គណនាទំហំ Folder នៅខាងក្រោយ (Background Thread)
# =========================================================
class SizeWorker(QThread):
def run(self):
while True:
for inst in instances:
iid = inst["id"]
path = os.path.abspath(f"./data/instance_{iid}")
calculated_sizes[iid] = format_size(get_dir_size(path))
time.sleep(5)
# =========================================================
# ៨. ចំណុចចាប់ផ្ដើមកម្មវិធី (Main Application Entry)
# =========================================================
if __name__ == "__main__":
app = QApplication(sys.argv)
coordinator = Coordinator()
web_thread = threading.Thread(target=run_web_server, daemon=True)
web_thread.start()
size_worker = SizeWorker()
size_worker.start()
webbrowser = __import__("webbrowser")
webbrowser.open(f"http://127.0.0.1:{PORT}")
sys.exit(app.exec())