# -*- coding: utf-8 -*- # /// script # requires-python = ">=3.10" # dependencies = [ # "playwright>=1.40", # ] # /// r""" 调试用户目录 .profile 生成 + YouTube 账号登录脚本(yt-studio-auth-prep 技能)。 职责单一:在指定目录创建(或复用)一个独立的浏览器用户数据目录,打开可见浏览器 窗口交给用户手动完成 Google/YouTube 登录,轮询确认 studio.youtube.com 不再跳转 登录页后关闭窗口退出。登录态持久化在该目录,供下游脚本复用: - youtube-studio-csv-download : --user-data-dir "/.profile" --channel - yt-studio-groupid-lookup : 同上(在线捕获套件时) 用法(cwd 任意;目录不存在会自动创建): python login_profile.py --channel chrome --user-data-dir "/.profile" python login_profile.py --channel msedge --user-data-dir "/.profile" --timeout-seconds 600 注意: - channel 一经选定,整个工作区所有下游命令必须用同一品牌(chrome/msedge); 换品牌 = 删除 .profile 后重跑本脚本,并重新捕获 bundles.json。 - 登录态失效(下游报跳 Google 登录页)时直接重跑本脚本补登录,.profile 保留原数据。 """ from __future__ import annotations import argparse import os import time STUDIO_URL = "https://studio.youtube.com/" DEFAULT_TIMEOUT_SECONDS = 600 def classify_url(url: str) -> str: """当前页面 URL 归类:google=登录页,studio=已进 Studio,other=其他。""" u = (url or "").lower() if "accounts.google" in u: return "google" if "studio.youtube.com" in u: return "studio" return "other" def selftest(): assert classify_url("https://accounts.google.com/v3/signin/identifier?x=1") == "google" assert classify_url("https://studio.youtube.com/channel/UCxx/analytics") == "studio" assert classify_url("https://www.youtube.com/") == "other" assert classify_url("") == "other" print("selftest OK:URL 分类逻辑通过") def wait_login(page, start_url: str, timeout_seconds: int) -> bool: """轮询等待用户完成登录;首次到达 studio 后回访一次,防会话弹回登录页。""" page.goto(start_url, wait_until="domcontentloaded") deadline = time.time() + timeout_seconds hinted = False last_report = time.time() while time.time() < deadline: kind = classify_url(page.url) if kind == "studio": page.wait_for_timeout(3000) page.goto(start_url, wait_until="domcontentloaded") if classify_url(page.url) == "studio": return True elif kind == "google": if not hinted: print("[登录] 请在打开的浏览器窗口中完成 Google/YouTube 登录" "(支持两步验证),完成后本脚本自动检测……") hinted = True page.wait_for_timeout(2000) else: page.wait_for_timeout(3000) page.goto(start_url, wait_until="domcontentloaded") if time.time() - last_report >= 30: print(f"[登录] 仍在等待登录……剩余 {max(0, int(deadline - time.time()))} 秒") last_report = time.time() return False def main(argv=None): ap = argparse.ArgumentParser( description="创建调试用户目录 .profile 并完成 YouTube 账号登录") ap.add_argument("--selftest", action="store_true", help="仅跑 URL 分类自测(无需浏览器)") ap.add_argument("--user-data-dir", help="调试用户目录路径,如 /.profile") ap.add_argument("--channel", choices=["chrome", "msedge"], default="chrome", help="浏览器品牌,默认 %(default)s;选定后全工作区统一") ap.add_argument("--start-url", default=STUDIO_URL, help="登录检测起始页,默认 %(default)s") ap.add_argument("--timeout-seconds", type=int, default=DEFAULT_TIMEOUT_SECONDS, help="等待登录的最长秒数,默认 %(default)s") args = ap.parse_args(argv) if args.selftest: selftest() return if not args.user_data_dir: ap.error("--user-data-dir 必填,如 /.profile") os.makedirs(args.user_data_dir, exist_ok=True) print(f"[登录] 打开 {args.channel}(profile={args.user_data_dir})," f"最长等待 {args.timeout_seconds} 秒……") try: from playwright.sync_api import sync_playwright except ImportError: raise SystemExit("[!] 缺少 playwright:在工作区(yt-download-place)内用" " uv run python 重试,或先 uv sync。") with sync_playwright() as p: try: context = p.chromium.launch_persistent_context( user_data_dir=args.user_data_dir, channel=args.channel, headless=False, args=["--disable-blink-features=AutomationControlled"], ) except Exception as e: # noqa: BLE001 raise SystemExit( f"[!] 启动浏览器失败: {e}\n" f" 多为 {args.channel} 正被占用(上次窗口未关/手动开过 .profile)。" "任务管理器彻底退出该品牌浏览器进程后重跑。") try: page = context.new_page() ok = wait_login(page, args.start_url, args.timeout_seconds) finally: context.close() if not ok: raise SystemExit( f"[!] {args.timeout_seconds} 秒内未完成登录。.profile 已保留进度," "直接重跑同一命令继续;等待不够就加大 --timeout-seconds。") print(f"[登录] 登录成功:{args.user_data_dir}(channel={args.channel})") print(f"[i] 该工作区所有下游命令统一使用 --channel {args.channel} " f"--user-data-dir {args.user_data_dir}") if __name__ == "__main__": main()