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
គណនីសរុប (Total Profiles): 0
00:00:00
ស្ថានភាព / សកម្មភាព ឈ្មោះគណនី ហាង / ក្រុម (Store) ឧបករណ៍ (Zoom) ទំហំផ្ទុក អាសយដ្ឋាន IP ទំព័រដើម (URL) កំណត់ចំណាំ
""" # ========================================================= # ៥. ប្រព័ន្ធទំនាក់ទំនងរវាង 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())