YouTube Upload — Incremental Updates (2026-06-03)
These notes were captured after the Day 1 video was successfully uploaded to YouTube (video ID: oOhc_Gcn6KI). They supplement the original productivity_youtube-upload-via-oauth.md skill, which covers PKCE-based OAuth + raw urllib resumable upload.
What’s New vs. Original Skill
The original skill uses PKCE (code_verifier + code_challenge) for OAuth. This run used Google’s standard Desktop-app loopback redirect (handled by google-auth library) — both work, but PKCE requires manual verifier management. v4 dropped PKCE for simplicity.
v4 Flow: webbrowser.open() instead of Popen Chrome
When the user’s primary Chrome (“TZE CZIA Chrome”) is already running on Windows, attempting subprocess.Popen(["chrome.exe", "--user-data-dir=..."]) for OAuth fails silently — Chrome sees the existing User Data lock and exits with “Opening in existing browser session” without opening a new window. Killing the running Chrome first to avoid the lock kills all 7 of the user’s open tabs.
Fix: drop the Popen step entirely. After generating auth_url and starting the local HTTP server, just call:
import webbrowser
webbrowser.open(auth_url) # uses default browserOn Windows, this routes to the default browser (TZE CZIA Chrome) → opens OAuth URL in a new tab on the already-running instance → no lock conflict → uses existing Google session → user is already logged in → goes straight to consent screen (skip login step).
This was the key fix that made the OAuth flow work end-to-end without disrupting the user’s browser session.
Chrome “ERR_UNSAFE_PORT” on redirect back to port 1
Google OAuth redirect back to http://127.0.0.1:1 is blocked by Chrome as ERR_UNSAFE_PORT by default (Chrome blocks ports 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 139, 143, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 993, 995, 2049, 3659, 4045, 6000, 6665, 6666, 6667, 6668, 6669, 6697).
Fix: patch the Chrome shortcut to add the flag persistently (survives reboots, all future launches).
$shortcut = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Google Chrome.lnk"
$ws = New-Object -ComObject WScript.Shell
$sc = $ws.CreateShortcut($shortcut)
$sc.Arguments = "--explicitly-allowed-ports=1"
$sc.Save()After patching, restart Chrome via the patched shortcut. Verify with:
Get-Process chrome | Select-Object Id, ProcessName, @{n='CmdLine';e={(Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine}}CmdLine should include --explicitly-allowed-ports=1.
After restart, click “Restore” on the post-crash prompt to recover all 7 tabs from Profile 13\Sessions\Session_*.
TZE CZIA Chrome’s actual profile name
On this machine, “TZE CZIA Chrome” is Profile 13 (NOT Default):
C:\Users\IDA\AppData\Local\Google\Chrome\User Data\Profile 13\
Verify with:
Get-Content "$env:LOCALAPPDATA\Google\Chrome\User Data\Local State" | Select-String '"last_active":"Profile'YouTube videos().update() returns stale privacyStatus for 5-10s
After youtube.videos().update(part="status", body={"id": VIDEO_ID, "status": {"privacyStatus": "public"}}), immediately calling videos().list(part="status", id=VIDEO_ID) returns the OLD privacyStatus (e.g. “unlisted”) for 5-10 seconds. The write succeeded — it’s a read-side cache.
Fix: add time.sleep(5) (or longer) before re-verifying. Or trust the empty response from update() and just re-check after 10s.
Observed in the wild:
First call: Current: unlisted -> DONE. unlisted -> unlisted
After 5s: Current: public -> DONE. public -> public
ffmpeg poll script is misleading
ffmpeg_status.txt reports file size snapshots from a poll_compress.py watcher, but the snapshots are during ffmpeg write, not after success. A 33.2MB reading in the status file does NOT mean the file is finished or even valid — ffmpeg may have crashed mid-write and the file may be unplayable or missing.
Fix: always wait for subprocess.run() to return with returncode == 0 AND verify with os.path.exists(OUT). Use python -u (unbuffered) to avoid Windows MSVCRT block-buffering hiding the result.
The actual command that worked:
import subprocess, time
result = subprocess.run(cmd, capture_output=True)
if result.returncode == 0 and os.path.exists(OUT):
# true successVerified Token + Scripts (2026-06-03)
- Token:
C:\Users\IDA\.hermes\google_token_youtube.json - Verifier v4:
C:\Users\IDA\.hermes\youtube_verifier_v4.json - Working scripts:
C:\Users\IDA\Videos\publish-video\oauth_v4_default_browser.py— generates auth URL, starts HTTP server on 127.0.0.1:1, opens in default browserupload_youtube.py— uses google-api-python-client, MediaFileUpload resumable, 10MB chunksupdate_video_privacy.py— change privacyStatus post-upload
- Uploaded:
oOhc_Gcn6KI(Day 1 renovation, 33.2MB H.264 1080x1920 89.77s) - Both
youtube.uploadand fullyoutubescope were approved on first consent (user clicked “Allow” once, app got both scopes) - Unlisted → public transition took ~5s to reflect in subsequent
list()calls
Pitfalls Table Additions
| Error | Cause | Fix |
|---|---|---|
webbrowser.open opens nothing visible | Default browser Chrome is sluggish / has 50+ tabs | Wait 2-3s, check taskbar for new tab icon |
| OAuth URL opens but session in wrong Google account | Multiple Google accounts signed in Chrome | Click “Use another account” on consent → pick business |
update_video_privacy.py shows unlisted -> unlisted | YouTube API list-side cache 5-10s behind write | Add time.sleep(5) and re-verify; the write actually succeeded |
ffmpeg_status.txt reports 33.2MB but file missing | poll script snapshots size mid-write, ffmpeg later failed | Always wait for subprocess.run returncode 0 + os.path.exists(OUT) |
UnicodeEncodeError: 'charmap' codec at print after ffmpeg | Windows cp1252 stdout + Chinese in ffmpeg stderr | Use sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') or python -u |
Recommended Future Workflow (8 platforms)
For each new platform (Instagram, Facebook, TikTok, 抖音, Lemon8, 小红书, X), repeat this pattern:
- Register developer account → app credentials (client_id + client_secret)
- Define OAuth scopes
- v4-style script: HTTP server on unique port +
webbrowser.open(auth_url)(no Popen Chrome) - Save token JSON to
~/.hermes/<platform>_token.json - Upload script: read token, build API client, MediaFileUpload-style resumable upload
- Verify with first test upload (small clip), then full 9:16 video
- Capture pitfalls in skill updates file like this one
⚠️ Incident Note (2026-06-03)
Original productivity_youtube-upload-via-oauth.md was accidentally overwritten by Hermes during this update (11853 bytes → 4184 bytes). User decided to keep the new file as a separate update document and manually merge into the original (which they will restore from backup or version history).
Lesson for Hermes:
- NEVER use
write_fileto “update” a file that was loaded via partial read (offset/limit pagination) without first re-reading the whole file - Prefer
terminal cat >> file(append) when adding incremental content to existing files - Confirm with user before overwriting any file > 5KB