Files
douyin/douyin-login-launcher/browser_open.py
T
2026-07-20 09:57:57 +08:00

160 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""本机可见浏览器启动核心:注入托管账号登录态并打开抖音。
被命令行登录器(launcher.py)与桌面整合版(desktop_app.py)共用。
"""
from __future__ import annotations
import json
import os
import sys
import time
DOUYIN_HOME = "https://www.douyin.com"
# 打包(冻结)后让 Playwright 在程序自带目录里找 Chromium:
# 安装包内置了 chromiumPLAYWRIGHT_BROWSERS_PATH=0 表示用 playwright 包目录下的浏览器,
# 这样用户无需联网下载,点击账号即可直接打开。
if getattr(sys, "frozen", False):
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0")
def _sanitize_cookie(cookie: dict) -> dict | None:
"""规整单条 cookie,保证 Playwright 能接受(domain/path/sameSite/expires)。"""
name = cookie.get("name")
if not name:
return None
same_site = cookie.get("sameSite") or "Lax"
if same_site not in ("Strict", "Lax", "None"):
same_site = "Lax"
try:
expires = float(cookie.get("expires", -1))
except (TypeError, ValueError):
expires = -1
return {
"name": name,
"value": cookie.get("value", ""),
"domain": cookie.get("domain") or ".douyin.com",
"path": cookie.get("path") or "/",
"expires": expires,
"httpOnly": bool(cookie.get("httpOnly", False)),
"secure": bool(cookie.get("secure", True)),
"sameSite": same_site,
}
def normalize_storage_state(cookie_data) -> tuple[dict, str | None]:
"""把服务器存的 cookie_data(JSON 字符串/字典) 转成 Playwright storage_state。
返回 (storage_state, user_agent)。storage_state 只保留 cookies/origins
多余字段(如 user_agent) 单独取出,避免 Playwright 校验报错。
"""
raw = json.loads(cookie_data) if isinstance(cookie_data, str) else (cookie_data or {})
if not isinstance(raw, dict):
raise RuntimeError("账号凭证格式异常,无法解析")
cookies = [c for c in (_sanitize_cookie(c) for c in raw.get("cookies") or []) if c]
origins = raw.get("origins") or []
if not cookies:
raise RuntimeError("该账号没有可用的登录 Cookie,请先在系统里给它导入/扫码登录")
storage_state = {"cookies": cookies, "origins": origins}
user_agent = raw.get("user_agent") or None
return storage_state, user_agent
def open_logged_in_browser(
storage_state: dict,
user_agent: str | None,
log=print,
on_ready=None,
on_error=None,
) -> None:
"""在本机启动一个可见浏览器并注入登录态,打开抖音官网。阻塞至浏览器关闭。
on_ready(): 浏览器窗口已弹出时回调(用于通知前端「已打开」)。
on_error(msg): 启动失败时回调(把错误回报给前端展示)。
"""
from playwright.sync_api import sync_playwright
launch_args = ["--disable-blink-features=AutomationControlled", "--start-maximized"]
def _fail(msg: str):
log(msg)
if on_error:
try:
on_error(msg)
except Exception:
pass
with sync_playwright() as p:
browser = None
last_err = ""
try:
browser = p.chromium.launch(headless=False, args=launch_args)
log("已启动内置 Chromium")
except Exception as e1:
last_err = str(e1)
log(f"内置 Chromium 启动失败({e1}),尝试本机 Edge/Chrome…")
for channel in ("msedge", "chrome"):
try:
browser = p.chromium.launch(
headless=False, channel=channel, args=launch_args
)
log(f"已启动本机 {channel}")
break
except Exception as e2:
last_err = str(e2)
continue
if browser is None:
_fail(
"无法启动浏览器,请尝试重新安装本软件(已内置 Chromium)。"
+ (f" 详情:{last_err}" if last_err else "")
)
return
try:
context = browser.new_context(
storage_state=storage_state,
user_agent=user_agent,
no_viewport=True,
)
page = context.new_page()
except Exception as e:
_fail(f"创建浏览器会话失败:{e}")
try:
browser.close()
except Exception:
pass
return
# 浏览器窗口此刻已经弹出,先通知前端「已打开」,再去加载页面。
if on_ready:
try:
on_ready()
except Exception:
pass
log(f"正在打开 {DOUYIN_HOME} …")
try:
page.goto(DOUYIN_HOME, wait_until="domcontentloaded", timeout=60000)
except Exception as e:
log(f"页面加载提示:{e}(可在浏览器里手动刷新)")
log("浏览器已打开,登录态已注入。关闭浏览器窗口即可结束。")
# 保活等待必须用 Playwright 的 wait_for_timeout(内部持续处理浏览器事件),
# 不能用 time.sleepsleep 会卡住 Playwright 客户端线程,导致页面里
# 新开的标签页(如抖音「视频管理」)永远停在 about:blank 无法加载。
try:
while browser.is_connected() and context.pages:
pg = context.pages[0]
try:
pg.wait_for_timeout(1000)
except Exception:
# 该页面刚被用户关闭等瞬态情况,稍候重查
time.sleep(0.2)
except Exception:
pass
try:
browser.close()
except Exception:
pass
log("浏览器已关闭。")