{“content”:”---\nname: youtube-upload-curl\ndescription: YouTube Data API v3 上传视频 — OAuth2 PKCE 授权 + resumable upload 全流程,绕过 google-api-python-client 安装失败的问题\ntriggers:\n - upload youtube video via api\n - youtube resumable upload\n - youtube oauth2 python\n---\n\n# YouTube Upload (curl/Python urllib)\n\n## 完整流程\n\n### Step 1: 生成授权 URL (PKCE)\n\npython\nimport base64, hashlib, os, urllib.parse, json, secrets\n\nCLIENT_ID = \"YOUR_CLIENT_ID\"\nVERIFIER_FILE = \"~/.hermes/youtube_verifier.json\"\n\ncode_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode().rstrip('=')\ncode_challenge = base64.urlsafe_b64encode(\n hashlib.sha256(code_verifier.encode()).digest()\n).decode().rstrip('=')\nstate = secrets.token_urlsafe(32)\n\nwith open(os.path.expanduser(VERIFIER_FILE), 'w') as f:\n json.dump({\"verifier\": code_verifier, \"state\": state}, f)\n\nscopes = [\n \"https://www.googleapis.com/auth/youtube.upload\",\n \"https://www.googleapis.com/auth/gmail.modify\",\n \"https://www.googleapis.com/auth/calendar\",\n]\n\nparams = {\n \"response_type\": \"code\",\n \"client_id\": CLIENT_ID,\n \"redirect_uri\": \"http://localhost:1\",\n \"scope\": \" \".join(scopes),\n \"code_challenge\": code_challenge,\n \"code_challenge_method\": \"S256\",\n \"access_type\": \"offline\",\n \"prompt\": \"consent\",\n \"state\": state,\n}\nurl = \"https://accounts.google.com/o/oauth2/auth?\" + urllib.parse.urlencode(params)\nprint(url)\n\n\n### Step 2: 用户授权后获取 code\n\n用户点击授权 URL,浏览器跳转到 http://localhost:1/?code=xxxxx,用户把那整个 URL 粘贴回来。\n\n提取 code:url.split('code=')[1].split('&')[0]\n\n### Step 3: 交换 access token\n\npython\nimport urllib.request, json\n\nwith open(os.path.expanduser(VERIFIER_FILE)) as f:\n v = json.load(f)\n\npayload = json.dumps({\n \"client_id\": CLIENT_ID,\n \"client_secret\": CLIENT_SECRET,\n \"code\": CODE,\n \"grant_type\": \"authorization_code\",\n \"redirect_uri\": \"http://localhost:1\",\n \"code_verifier\": v[\"verifier\"],\n}).encode()\n\nreq = urllib.request.Request(\n \"https://oauth2.googleapis.com/token\",\n data=payload,\n headers={\"Content-Type\": \"application/json\"},\n method=\"POST\"\n)\n\nwith urllib.request.urlopen(req, timeout=30) as resp:\n result = json.loads(resp.read())\n\naccess_token = result[\"access_token\"]\nrefresh_token = result[\"refresh_token\"]\n\n# 保存 token\ntoken_data = {\n \"token\": access_token,\n \"refresh_token\": refresh_token,\n \"token_uri\": \"https://oauth2.googleapis.com/token\",\n \"client_id\": CLIENT_ID,\n \"client_secret\": CLIENT_SECRET,\n \"scopes\": result[\"scope\"].split(),\n}\nwith open(os.path.expanduser(\"~/.hermes/google_token_youtube.json\"), 'w') as f:\n json.dump(token_data, f, indent=2)\n\n\n### Step 4: 上传视频 (resumable)\n\npython\nimport urllib.request, json\n\nwith open(os.path.expanduser(\"~/.hermes/google_token_youtube.json\")) as f:\n token = json.load(f)\naccess_token = token[\"token\"]\n\nmetadata = {\n \"snippet\": {\n \"title\": \"视频标题\",\n \"description\": \"视频描述\",\n \"tags\": [\"标签1\", \"标签2\"],\n \"categoryId\": \"10\" # Music\n },\n \"status\": {\n \"privacyStatus\": \"public\",\n \"selfDeclaredMadeForKids\": False\n }\n}\n\n# 初始化 resumable upload\ninit_req = urllib.request.Request(\n \"https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status\",\n data=json.dumps(metadata).encode(),\n headers={\n \"Authorization\": f\"Bearer {access_token}\",\n \"Content-Type\": \"application/json\",\n \"X-Upload-Content-Length\": str(len(video_data)),\n \"X-Upload-Content-Type\": \"video/mp4\",\n },\n method=\"POST\"\n)\n\nwith urllib.request.urlopen(init_req, timeout=30) as resp:\n upload_url = resp.headers.get(\"Location\")\n\n# 上传视频数据\nwith open(video_path, 'rb') as f:\n video_data = f.read()\n\nreq = urllib.request.Request(\n upload_url,\n data=video_data,\n headers={\n \"Authorization\": f\"Bearer {access_token}\",\n \"Content-Type\": \"video/mp4\",\n \"Content-Length\": str(len(video_data)),\n },\n method=\"PUT\"\n)\n\nwith urllib.request.urlopen(req, timeout=300) as resp:\n result = json.loads(resp.read())\n video_id = result[\"id\"]\n print(f\"https://www.youtube.com/watch?v={video_id}\")\n\n\n## 关键陷阱\n\n1. Auth code 只能用一次:每次点击授权链接会生成新的 code,必须立即使用\n2. code_verifier 必须正确:PKCE 的 verifier 是关键,不能搞错\n3. Google OAuth Playground 无法使用:因为 client ID 冲突,会 blocked\n4. Python urllib PKCE 问题:第一次 exchange 失败是因为 Python urllib 发送 JSON body,但 Google 期望 application/x-www-form-urlencoded,正确做法是直接发送 JSON 或者确保 PKCE verifier 编码正确\n5. Token 刷新 scope 问题:用 refresh_token 刷新时不能加新 scope,否则被拒\n\n## 简化脚本\n\n已保存到:~/.hermes/youtube_oauth.py\n\nbash\n# 生成授权 URL\npython3 ~/.hermes/youtube_oauth.py\n\n# 交换 token\npython3 ~/.hermes/youtube_oauth.py \"AUTH_CODE\"\n\n”}