Title and description were typed character-by-character, so a '#'
(e.g. '#Shorts') triggered YouTube's topic-autocomplete dropdown that
follows the caret and covers the Next/Publish buttons, stalling the
upload. Switch to fill() (one-shot insert, no per-keystroke autocomplete)
with a type() fallback, and add _dismiss_autocomplete (blur, then Escape
only when a dropdown is actually visible so the upload dialog is never
closed by mistake).
Same reason as the ks uploader: bundled patchright cannot reliably resolve the system 'chrome' channel on every platform, so use the bundled 'chromium' channel by default and rely on conf.py:LOCAL_CHROME_PATH for users who want real Chrome.
Two related changes for kuaishou uploader:
- Default browser channel is switched from 'chrome' to 'chromium'. The launcher binary that ships with patchright no longer reliably resolves the system Chrome channel on every platform, so falling back to bundled chromium avoids launch-time channel-resolution errors. The user can still force a real Chrome via conf.py:LOCAL_CHROME_PATH.
- New cdp_url parameter on ks_setup / get_ks_cookie. When set, the login flow uses connect_over_cdp() to drive the user's already-running real Chrome instead of spawning a separate browser. This is the supported way around strict anti-bot detection: real Chrome has a fingerprint that passes verification, while the bundled chromium does not. should_close_context tracks ownership so we don't close a context we didn't create.
The shell passed USERNAME as an environment variable to the embedded python heredoc (USERNAME=$USERNAME python3 ...), but the python block referenced USERNAME as if it were already a local variable. The result was NameError on every run, so the export script could never produce a cookies/<platform>_<account>.json file.
Fix: read the env var explicitly with os.environ.get('USERNAME', '') at the top of the embedded block, then use the local USERNAME as before.
The Douyin creator center UI (https://creator.douyin.com/creator-micro/) has been refactored and breaks the existing login flow in three independent ways. This commit fixes all of them.
1. QR code selector no longer matches
The previous selector chain assumed the QR sat in a sibling div with an aria-label of 二维码. After the refactor, the QR is rendered as a base64 PNG inside div#animate_qrcode_container and has no aria-label. The new chain is a 4-tier fallback (id -> class partial -> container partial -> aria-label) preceded by a networkidle wait so the SPA is fully hydrated before we look.
2. cookie_auth() race condition
page.goto + wait_for_url(5000) + immediate get_by_text count() was racy: the SPA had not finished hydrating, so count() could return 0 even when the page actually shows the login card, leading to false-positive 'cookie valid' results. Replaced with a 10s polling loop that returns invalid if neither the login-card text nor the upload-page text is observed.
3. _wait_for_douyin_login() only waited for sessionid cookie (120s)
During 2FA the cookie is not yet set; the page is still on the verification screen and the URL has already changed. The new loop detects 2FA input fields, logs a warning so the user knows to type the SMS code, and keeps polling. Timeout raised to 200s to accommodate the slower human-in-the-loop flow.
Verified the selector chain against the live creator.douyin.com page: returns a 2.9 KB base64 PNG that decodes into a valid douyin scan_login URL.
- Wait until the upload progress reaches 100% before clicking publish. The
browser-based upload only progresses while the page is open, so publishing
(and closing the browser) mid-upload cut it off and left the video stuck
partway (e.g. 76%).
- Add optional YT_PROXY (conf.py): where youtube.com is blocked, direct
connections time out and the chromium does not use the system proxy, so
allow pointing it at a local proxy explicitly.
Add a YouTube uploader that follows the existing cookie-based browser
automation pattern (uploader/youtube_uploader + sau youtube CLI:
login/check/upload-video). Supports title/description/tags, thumbnail,
adding to a playlist (for series), and visibility (public/unlisted/private).
Browser automation is used instead of the official Data API on purpose:
videos uploaded by an unaudited API project are force-locked to private
and cannot be made public without Google's compliance audit, which is
impractical for personal/single-channel use. Browser automation has no
such restriction and matches how every other platform here works.
- Add tag count validation (max 10) in both CLI and uploader layers
- CLI: exit with error if tags > 10, prompt user to reduce
- Uploader: truncate to first 10 with warning as fallback
- Prevents infinite loop when XHS UI rejects excess tags
- Add check_original_declaration() to auto-check original content
declaration before publishing (video and note modes)
- Gracefully skips if the option is not present on the page
Move newline normalization out of the f-string expression so the CLI can import and run under Python 3.11+ without syntax errors.
Co-authored-by: Cursor <cursoragent@cursor.com>