`sau douyin login` fails end-to-end after the 2026 Q2 creator-center refactor

Author: DerekEXSCreated Jun 16, 2026Updated Jun 16, 2026

Summary

python sau_cli.py douyin login --account <name> (or the equivalent WebUI "login" button) fails to log any account in. Three independent breakages, all on the same refactored creator center UI, all needed to land before the flow is usable again.

This is observed in our environment on patchright 1.x / playwright 1.5x / Chromium 145+ against https://creator.douyin.com/creator-micro/content/upload as of 2026-06.

Steps to reproduce

bash
sau douyin login --account <your_account_name>

Expected: QR code is printed to the terminal, the user scans with the Douyin app, optional SMS code is entered, the cookie is written to cookies/douyin_<account_name>.json, and a follow-up sau douyin check --account <name> returns valid.

Actual: one of three things goes wrong depending on the moment of the race:

  1. _extract_douyin_qrcode_src times out at 30s with no QR extracted. Log line: Locator.wait_for: Timeout 30000ms exceeded. waiting for get_by_text("扫码登录").first to be visible
  2. After the QR is forced to extract manually, _wait_for_douyin_login times out at 120s even after a successful scan, with log line 登录失败: 等待抖音扫码登录超时. The cookie is never written.
  3. sau douyin check returns invalid for cookies that are actually still valid, because cookie_auth is racing the SPA.

Root causes

(1) QR selector no longer matches the page

Old selector chain (commit 90e01c0-ish):

python
scan_login_tab = page.get_by_text("扫码登录", exact=True).first
qrcode_img = (
    scan_login_tab
    .locator("..")
    .locator("xpath=following-sibling::div[1]")
    .locator('img[aria-label="二维码"]')
    .first
)

This worked when the login page rendered the QR inside a sibling <div> with aria-label="二维码". The current page is a single-tab layout (class single_tab-...); the QR lives inside div#animate_qrcode_container as a base64 PNG with no aria-label.

(2) _wait_for_douyin_login only waits for sessionid cookie

sessionid is set after the 2FA / SMS code step completes, not when the user scans. For accounts that trigger 2FA, the URL changes immediately on the phone's "confirm" tap, but the cookie does not appear for several seconds while the user is typing the SMS code. A 120s timeout is not enough; the loop also needs to recognise that the page is in a "verification in progress" state and not bail.

(3) cookie_auth returns a non-deterministic answer

python
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
try:
    await page.wait_for_url(".../upload", timeout=5000)
except Exception:
    return False
if await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count():
    return False
return True

page.goto returns at the load event but the SPA has not finished hydrating. wait_for_url is satisfied immediately (the URL is unchanged after a soft-redirect). The next get_by_text(...).count() fires inside the race window and sometimes returns 0 even when the login card is on the page, yielding a false-positive "valid" answer. Calling the function twice in a row gives different results; we verified this empirically (5 calls, mixed True/False) on the maintainer-built cookie from a known-valid account.

Fix (proposed)

A branch fix/douyin-cli-login-overhaul is being prepared with four atomic commits:

  1. fix(douyin): overhaul CLI auto-login to handle new creator center UI

    • New selector chain with a 4-tier fallback (id -> class partial -> container partial -> legacy aria-label) preceded by wait_for_load_state("networkidle").
    • New cookie_auth polling loop (10s) that returns invalid if neither the login-card text nor the upload-page text is observed.
    • New _wait_for_douyin_login loop that detects 2FA input fields and logs a warning so the user knows to type the SMS code, with timeout raised to 200s.
  2. fix(export_douyin_cookie): read USERNAME from os.environ in embedded python

    • The shell passed USERNAME as an env var to a python heredoc, but the python block referenced it as a local. NameError on every run. One-line fix: USERNAME = os.environ.get('USERNAME', '').
  3. feat(ks): switch default channel to chromium and add cdp-url support

    • patchright's bundled launcher does not always resolve the system "chrome" channel cleanly. Fall back to bundled "chromium" by default; conf.py:LOCAL_CHROME_PATH still works for real Chrome.
    • New cdp_url parameter on ks_setup / get_ks_cookie to drive the user's already-running real Chrome via connect_over_cdp. This is the only way to get past strict anti-bot fingerprint checks on some accounts.
  4. chore(xiaohongshu): switch default channel to chromium

    • Same as the ks uploader.

Out of scope / notes

  • sau_cli.py has an unrelated register_cli_cookie_to_webui block that we are intentionally not proposing upstream; it is a private-deployment feature and is left in the local working tree.
  • The channel="chrome" -> channel="chromium" change is a compatibility fix for patchright, not a functional regression for users on plain playwright; LOCAL_CHROME_PATH still points at real Chrome for those who want it.
  • We are not in a position to test the proposed fix against every Douyin account type, but the selector chain is verified live on https://creator.douyin.com/.

Happy to adjust scope, split into smaller PRs, or move any of the above into a separate issue if the maintainer prefers.


PR: https://github.com/dreammis/social-auto-upload/pull/229

Source: dreammis/social-auto-upload