143 lines
6.0 KiB
Python
143 lines
6.0 KiB
Python
# -*- 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 "<place>/.profile" --channel <brand>
|
||
- yt-studio-groupid-lookup : 同上(在线捕获套件时)
|
||
|
||
用法(cwd 任意;目录不存在会自动创建):
|
||
python login_profile.py --channel chrome --user-data-dir "<place>/.profile"
|
||
python login_profile.py --channel msedge --user-data-dir "<place>/.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="调试用户目录路径,如 <place>/.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 必填,如 <place>/.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()
|