{"slug":"jianshuo-wjs-uploading-video-5a716b","title":"YouTube \u89c6\u9891\u6279\u91cf\u4e0a\u4f20","description":"\u7528\u81ea\u5e26 Python \u811a\u672c\u628a\u5355\u4e2a\u6216 final/ \u76ee\u5f55\u4e0b\u7684\u591a\u4e2a MP4 \u4e0a\u4f20\u5230 YouTube\uff0c\u4ece UPLOAD_META.md \u89e3\u6790\u6807\u9898\u3001\u63cf\u8ff0\u4e0e\u6807\u7b7e\uff0c\u7ed5\u8fc7 httplib2 \u8d70 requests \u5206\u5757\u7eed\u4f20\u4ee5\u9002\u914d\u4ee3\u7406\u73af\u5883\uff0c\u5e76\u7ed9\u51fa\u914d\u989d\u4e0e\u9650\u6d41\u6392\u67e5\u3002","category":"\u53d1\u5e03\u4e0e\u8fd0\u8425","tags":["jianshuo","\u5efa\u7855","\u53d1\u5e03\u4e0e\u8fd0\u8425"],"author":"\u5efa\u7855 \u00b7 jianshuo","source_url":"https://github.com/jianshuo/claude-skills/blob/d36bc423286a34eb32adc604121e5c7714b08740/wjs-uploading-video/SKILL.md","license":"MIT","status":"published","verification":"source_reviewed","verification_notes":"\u5df2\u6838\u5bf9\u539f\u59cb\u4ed3\u5e93\u3001\u56fa\u5b9a\u63d0\u4ea4\u3001\u8bb8\u53ef\u3001\u6280\u80fd\u6b63\u6587\u4e0e\u5305\u7ed3\u6784\uff0c\u5e76\u8865\u5145\u4e2d\u6587\u6267\u884c\u9a8c\u6536\u8bf4\u660e\uff1b\u672a\u8fd0\u884c\u5305\u5185\u811a\u672c\u3002","revision":"c69c1088808644278671e933ab80279c","created_at":"2026-11-02T14:47:08+00:00","updated_at":"2026-11-02T14:47:08+00:00","source_repo":"jianshuo/claude-skills","source_path":"wjs-uploading-video/SKILL.md","source_commit":"d36bc423286a34eb32adc604121e5c7714b08740","source_updated_at":"2026-11-02T14:46:04+00:00","stars":0,"proxy_score":null,"score":null,"grade":null,"dimensions":{},"evaluation":{},"installs":0,"platforms":[],"formats":[],"category_slug":"publishing","category_name":"\u53d1\u5e03\u4e0e\u8fd0\u8425","quality":{"review_type":"text_and_package_review","readiness":"requires_setup","reviewed_at":"2026-11-02T14:46:04+00:00","review_summary":"\u4e0a\u4f20\u65b9\u6cd5\u4e0e\u5143\u6570\u636e\u89e3\u6790\u89c4\u5219\u5177\u4f53\uff0c\u811a\u672c\u4e0e\u51ed\u636e\u8bf4\u660e\u9f50\u5168\uff0c\u4f46\u9700\u7528\u6237\u81ea\u5907 Google OAuth \u51ed\u636e\u5e76\u5b8c\u6210\u9996\u6b21\u6388\u6743\uff1b\u4ee3\u7406\u4e0e\u914d\u989d\u76f8\u5173\u7ed3\u8bba\u672a\u7ecf\u672c\u673a\u590d\u73b0\u3002","cost_notes":"YouTube Data API \u6709\u6bcf\u65e5\u914d\u989d\uff08\u4e0a\u4f20\u7ea6 1600 \u5355\u4f4d/\u6b21\uff09\uff0c\u8d85\u989d\u9700\u5728 Google Cloud \u7533\u8bf7\u63d0\u989d\uff1b\u662f\u5426\u4ea7\u751f\u8d39\u7528\u9700\u7528\u6237\u81ea\u884c\u786e\u8ba4\uff0c\u672c\u5305\u672a\u62a5\u4ef7\u3002","source_sha256":"3e10c3bae5c79aeae0bc26269e08fbf5151ca2919ac300ae853e9a80f6a8489d","prerequisites":["Google Cloud \u9879\u76ee\u4e0e OAuth \u5ba2\u6237\u7aef\u51ed\u636e\uff08~/.config/youtube/credentials.json\uff09","\u9996\u6b21\u4e0a\u4f20\u9700\u6d4f\u89c8\u5668\u5b8c\u6210 Google \u6388\u6743","Python 3 \u53ca google-auth\u3001requests\uff1b\u6216 uvx \u53ef\u7528","\u80fd\u8bbf\u95ee YouTube \u4e0a\u4f20\u7aef\u70b9\u7684\u7f51\u7edc/\u4ee3\u7406"],"checks":["dry-run \u8f93\u51fa\u662f\u5426\u628a UPLOAD_META.md \u7684\u6bcf\u4e2a ## NN \u5757\u6b63\u786e\u914d\u5bf9\u5230\u5b9e\u9645\u6587\u4ef6","\u6807\u9898\u3001\u63cf\u8ff0\u662f\u5426\u9010\u5b57\u53d6\u81ea\u5143\u6570\u636e\u5757\uff0c\u6807\u7b7e\u662f\u5426\u7531 # \u8bcd\u53bb\u4e95\u53f7\u540e\u751f\u6210","\u91cd\u590d\u8fd0\u884c\u65f6\u662f\u5426\u8df3\u8fc7 results \u6587\u4ef6\u4e2d\u5df2\u5b58\u5728\u7684\u89c6\u9891","\u4e0a\u4f20\u5931\u8d25\u65f6\u662f\u5426\u4fdd\u7559\u5df2\u5b8c\u6210\u8bb0\u5f55\u3001\u53ef\u5b89\u5168\u91cd\u8dd1","\u9690\u79c1\u8bbe\u7f6e\u3001made-for-kids \u58f0\u660e\u662f\u5426\u4e0e\u547d\u4ee4\u884c\u53c2\u6570\u4e00\u81f4"],"limitations":["\u811a\u672c\u672a\u5b9e\u9645\u8fd0\u884c\uff0c\u4ee3\u7406\u7ed5\u884c\u4e0e\u5206\u5757\u7eed\u4f20\u6548\u679c\u4ec5\u636e\u6587\u6863\u63cf\u8ff0","\u4ec5\u8986\u76d6 YouTube\uff0c\u89c6\u9891\u53f7/\u6296\u97f3/\u5c0f\u7ea2\u4e66/B\u7ad9\u660e\u786e\u4e0d\u652f\u6301","\u9ed8\u8ba4 public \u7acb\u5373\u516c\u5f00\uff0c\u8bef\u7528\u4f1a\u76f4\u63a5\u5bf9\u5916\u53ef\u89c1","\u6bcf\u65e5\u4e0a\u4f20\u6570\u9650\u5236\u65e0\u6cd5\u7ed5\u8fc7\uff0c\u53ea\u80fd\u7b49\u5f85\u592a\u5e73\u6d0b\u65f6\u95f4\u91cd\u7f6e\u6216\u6539\u7528\u7f51\u9875\u7aef"],"issues":["\u6587\u6863\u4e2d\u201c\u5df2\u9a8c\u8bc1\u201d\u201c\u5b9e\u9645\u53d1\u751f\u8fc7\u201d\u7b49\u8868\u8ff0\u5c5e\u4f5c\u8005\u73af\u5883\u7ecf\u9a8c\uff0c\u672c\u5ba1\u9605\u672a\u590d\u73b0\uff0c\u7528\u6237\u4e0d\u5e94\u89c6\u4e3a\u672c\u673a\u4fdd\u8bc1","\u9ed8\u8ba4\u9891\u9053\u540d\u4e0e\u4f5c\u8005\u8eab\u4efd\u7ed1\u5b9a\uff0c\u5176\u4ed6\u7528\u6237\u9700\u81ea\u884c\u66ff\u6362 CTA \u4e0e\u6807\u7b7e\u524d\u7f00","\u51ed\u636e\u4e0e token \u8def\u5f84\u4e3a\u56fa\u5b9a\u9ed8\u8ba4\u503c\uff0c\u591a\u8d26\u53f7\u573a\u666f\u9700\u663e\u5f0f\u4f20\u53c2"],"missing_files":[]},"quality_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/SHILIU-QUALITY.md","usage_guide":{"task":"\u628a\u5df2\u5b8c\u6210\u7684\u89c6\u9891\u6587\u4ef6\u6309\u5143\u6570\u636e\u6279\u91cf\u6216\u5355\u4e2a\u4e0a\u4f20\u5230 YouTube\uff0c\u5e76\u5904\u7406\u4ee3\u7406\u3001\u914d\u989d\u4e0e\u6bcf\u65e5\u4e0a\u4f20\u6570\u9650\u5236\u3002","inputs":"\u5f85\u4e0a\u4f20\u7684 .mp4 \u6587\u4ef6\u6216\u542b\u591a\u4e2a\u5206\u6bb5\u7684 final/ \u76ee\u5f55\uff1b\u53ef\u9009\u7684 UPLOAD_META.md\uff08## NN \u00b7 \u6587\u4ef6\u540d \u5757 + **\u77ed\u6807\u9898** + **\u89c6\u9891\u63cf\u8ff0**\uff09\uff1bGoogle Cloud OAuth \u5ba2\u6237\u7aef JSON \u4e0e\u9996\u6b21\u6388\u6743\u751f\u6210\u7684 token\uff1bPython \u4f9d\u8d56 google-auth\u3001requests\uff08\u6216 uvx \u4e34\u65f6\u5e26\u4f9d\u8d56\uff09\u3002","outputs":"\u6bcf\u4e2a\u89c6\u9891\u7684 YouTube URL \u4e0e video id\uff0c\u5199\u5165 --results-file\uff08\u9ed8\u8ba4 <dir>/.youtube_upload_results.json\uff09\uff1b\u7ec8\u7aef\u8fdb\u5ea6\u8f93\u51fa\uff1b\u53ef\u9009\u7684\u64ad\u653e\u5217\u8868\u5f52\u5c5e\u4e0e\u5b9a\u65f6\u53d1\u5e03\u8bbe\u7f6e\u3002","example_input":"python3 ~/.claude/skills/wjs-uploading-video/scripts/upload_youtube.py --dir \"/path/to/final\" --meta \"/path/to/final/UPLOAD_META.md\" --privacy unlisted --dry-run","example_output":"\u793a\u610f\uff08\u975e\u771f\u5b9e\u7ed3\u679c\uff09\uff1adry-run \u5217\u51fa\u5c06\u4e0a\u4f20\u7684 3 \u4e2a\u6587\u4ef6\u53ca\u89e3\u6790\u51fa\u7684\u6807\u9898/\u6807\u7b7e\uff1b\u6b63\u5f0f\u8fd0\u884c\u540e results JSON \u542b file\u3001title\u3001video id\u3001URL\uff0c\u5e76\u4ee5\u8868\u683c\u56de\u663e\u7ed9\u7528\u6237\u3002"},"read_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b","skill_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/SKILL.md","download_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/download","install_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/install","files":[{"path":"LICENSE","bytes":1082,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/LICENSE"},{"path":"SHILIU-QUALITY.md","bytes":2076,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/SHILIU-QUALITY.md"},{"path":"SKILL.md","bytes":8867,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/SKILL.md"},{"path":"references/credentials-setup.md","bytes":2638,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/references/credentials-setup.md"},{"path":"scripts/upload_youtube.py","bytes":19167,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/scripts/upload_youtube.py"},{"path":"shiliu-quality.json","bytes":1897,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/shiliu-quality.json"},{"path":"shiliu-source.json","bytes":271,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/shiliu-source.json"},{"path":"usage.json","bytes":1041,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-uploading-video-5a716b/files/usage.json"}],"content":"---\nname: wjs-uploading-video\ndescription: Upload one or many videos to YouTube. Use when the user wants to \"\u4e0a\u4f20\u5230 YouTube\", \"\u53d1 YouTube\", \"\u6279\u91cf\u4e0a\u4f20\", \"upload to YouTube\", \"post videos to YouTube\", or to publish a finished `final/` directory of MP4s. Reads per-video metadata (title / description / tags) from a sibling `UPLOAD_META.md` file when present (the user's standard markdown format), or from command-line flags. Survives behind a SOCKS/HTTP proxy by using `requests` directly for the resumable upload (the stock `google-api-python-client` MediaFileUpload stalls under this user's proxy setup).\n---\n\n# wjs-uploading-video\n\nPush finished videos to YouTube. Defaults are tuned for this user's workflow (\u738b\u5efa\u7855 channel, China network with local proxy, 1080p horizontal recordings from Riverside / multicam edits).\n\n## When to use\n\n- User has one or more finished `.mp4` files and wants them on YouTube\n- User points to a `final/` directory with multiple segments and an `UPLOAD_META.md`\n- User wants a specific privacy / playlist / scheduled publish\n\n**Don't use** for:\n- \u5fae\u4fe1\u89c6\u9891\u53f7 upload (no public API; user uploads manually via web)\n- \u6296\u97f3 / \u5c0f\u7ea2\u4e66 / B \u7ad9 (different APIs, not yet implemented here)\n- YouTube Shorts variants from horizontal source (use `wjs-reframing-video` first to produce the 9:16 cut, then upload that via this skill)\n\n## Prerequisites (one-time per machine)\n\n1. **Google Cloud OAuth client**: `~/.config/youtube/credentials.json` must exist. See `references/credentials-setup.md` for the 5-minute setup if missing.\n2. **Python deps**: `pip3 install google-auth-oauthlib google-api-python-client requests` (only `google-auth` + `requests` are strictly needed at upload time, but the OAuth-lib pulls them).\n3. **First-ever upload** opens a browser for Google consent and writes `~/.config/youtube/token.json`. Subsequent runs reuse it silently.\n\n## How it works (and why it's not the stock youtube-uploader)\n\nYouTube's resumable upload protocol issues a `Location:` URL after the metadata POST, then accepts the bytes in chunked `PUT` requests. The stock `google-api-python-client` runs this over `httplib2`, which under this user's local SOCKS+HTTP proxy stack throws `[Errno 65] No route to host` or `socket.timeout` on those follow-up PUTs and stalls indefinitely.\n\nThis skill bypasses `httplib2`: it does OAuth via `google-auth`, then drives the resumable upload manually with `requests`, passing the proxy explicitly. 8 MB chunks (not the stock 256 KB) \u2014 fewer round-trips through the proxy. Exponential-backoff retry on `socket.timeout` / `ConnectionError` / 5xx.\n\nIf you're tempted to \"just call the YouTube API client directly,\" don't \u2014 it'll fail in this environment.\n\n**`ModuleNotFoundError: No module named 'google.oauth2'`** \u2014 Homebrew \u5347\u7ea7 Python \u5927\u7248\u672c\u540e site-packages \u4f1a\u4e22\uff082026-08 \u4ece 3.13\u21923.14 \u5b9e\u9645\u53d1\u751f\u8fc7\uff09\u3002\u522b\u91cd\u65b0 pip install\uff0c\u76f4\u63a5\u7528 uvx \u5e26\u4f9d\u8d56\u8dd1\uff08\u5df2\u9a8c\u8bc1\uff09\uff1a\n\n```bash\nuvx --with google-auth --with requests python \\\n  ~/.claude/skills/wjs-uploading-video/scripts/upload_youtube.py --dir \"/path/to/final\"\n```\n\n## Usage\n\n### Batch upload a `final/` directory\n\n```bash\npython3 ~/.claude/skills/wjs-uploading-video/scripts/upload_youtube.py \\\n  --dir \"/path/to/final\" \\\n  --meta \"/path/to/final/UPLOAD_META.md\"\n```\n\nThe script:\n1. Reads `UPLOAD_META.md` and pairs each `## NN \u00b7 filename.mp4` block to a video file in `--dir`\n2. Skips videos already in `--results-file` (default `<dir>/.youtube_upload_results.json`) \u2014 safe to re-run after failures\n3. Uploads sequentially with progress every 8 MB\n4. Writes the final URL list to `--results-file`\n\n### Single file\n\n```bash\npython3 ~/.claude/skills/wjs-uploading-video/scripts/upload_youtube.py \\\n  --video /path/to/clip.mp4 \\\n  --title \"My Title\" \\\n  --description \"Body text\" \\\n  --tags \"tag1,tag2,tag3\"\n```\n\n### Common overrides\n\n| Flag | Default | Notes |\n|---|---|---|\n| `--privacy` | `public` | `private` / `unlisted` / `public` |\n| `--category` | `28` | 28 = Science & Tech. 27 = Education. 24 = Entertainment. |\n| `--made-for-kids` | `false` | YouTube requires this declaration |\n| `--playlist <ID>` | none | Add each uploaded video to a playlist |\n| `--publish-at <ISO8601>` | none | Schedule publish (requires `--privacy private`) |\n| `--credentials` | `~/.config/youtube/credentials.json` | OAuth client JSON |\n| `--token` | `~/.config/youtube/token.json` | Cached OAuth token |\n| `--chunk-mb` | `8` | Smaller chunks if uploads keep failing mid-flight |\n| `--dry-run` | off | Parse meta + list what would upload, don't touch network |\n\n## UPLOAD_META.md format\n\nThe parser expects the user's standard structure:\n\n```\n## 01 \u00b7 segment_01_no-bugs.mp4\n\n**\u77ed\u6807\u9898**\n\u4ee3\u7801\u6ca1\u6709\u9519\u8bef,\u53ea\u6709\u610f\u56fe\u4e0d\u4e00\u81f4\n\n**\u89c6\u9891\u63cf\u8ff0**\nAI \u65f6\u4ee3\u5c4e\u5c71\u7684\u91cd\u65b0\u5b9a\u4e49...\n\n\u2014\u2014 \u738b\u5efa\u7855 \u00d7 \u4efb\u946b\u300a...\u300b\u7b2c 1 \u96c6\n\n#\u738b\u5efa\u7855 #AI\u7f16\u7a0b #ClaudeCode\n\n---\n```\n\nMapping:\n- `## NN \u00b7 <filename>` \u2192 which video this block describes\n- `**\u77ed\u6807\u9898**` (or `**Title**`) block \u2192 YouTube **title**, verbatim. Short titles work but consider that YouTube allows up to 100 chars \u2014 if you want a richer title with series name, write it that way in `**\u77ed\u6807\u9898**`\n- `**\u89c6\u9891\u63cf\u8ff0**` (or `**Description**`) block \u2192 YouTube **description**, verbatim, with the `#tag` hashtags retained at the bottom\n- All `#word` tokens in the \u89c6\u9891\u63cf\u8ff0 \u2192 comma-separated YouTube **tags** (each `#` is stripped; the user's channel name `\u738b\u5efa\u7855` is auto-prepended per global instructions)\n\nFilename in the heading must match an actual file in `--dir`. If a file exists but has no meta block, the script errors loudly \u2014 pass `--allow-missing-meta` to upload it with `--title <basename>` and empty description.\n\n## Sensible defaults this skill bakes in\n\n- **Privacy = public**: videos go live immediately and appear in subscriber feeds + search. Override per call with `--privacy unlisted` (link-only) or `--privacy private` (owner-only) when you want to review first\n- **Category = 28 (Science & Tech)**: matches \u738b\u5efa\u7855 channel's main content; override with `--category` per upload\n- **selfDeclaredMadeForKids = false**: YouTube requires this; the user's content is adult-targeted\n- **Chunk size = 8 MB**: validated working size through this user's local proxy (256 KB stalled)\n- **Skip already-uploaded**: results file is the source of truth; deleting it forces re-upload\n\n## Channel-name CTA rule\n\nIf you write description footers, signatures, or \"subscribe to me\" lines into a video's metadata, use **\u738b\u5efa\u7855** (the user's channel name). Don't put a guest's name there \u2014 guests like \u4efb\u946b belong inside the description body when they're the conversation partner, never in the channel-CTA slot.\n\n## Troubleshooting\n\n| Symptom | Fix |\n|---|---|\n| `access_denied 403` on consent screen | Add user's Google account to the OAuth client's Test users list in Google Cloud Console |\n| `[Errno 65] No route to host` mid-upload | Almost always a proxy issue \u2014 verify `curl --max-time 10 https://upload.googleapis.com/upload/youtube/v3/videos` returns a 4xx (any 4xx = proxy reachable); if `000`, the proxy is down |\n| Upload stalls with no progress lines | The proxy is silently buffering. Lower `--chunk-mb 4` or restart the proxy daemon |\n| `quotaExceeded` (API units) | YouTube Data API default quota is 10,000 units/day, each upload is 1,600 units \u2014 so ~6 uploads/day. Request a quota bump in Google Cloud Console, or split uploads across days |\n| `429 rateLimitExceeded` \u00b7 `Video Uploads per day` | **A DIFFERENT, separate limit** from API units \u2014 YouTube's per-project *daily video-upload count* (anti-abuse, tied to project verification status; can be very low). When hit, **every** upload in the batch fails `init session failed: 429`, none succeed. It is NOT a proxy/auth problem and can't be worked around. It resets at **midnight US Pacific Time** (not your local midnight). Options: wait for the PT reset and re-run (the results file was cleared so all retry); or **upload via the YouTube Studio web UI**, which does NOT consume this API limit; or get the GCP project verified to raise the cap. Earlier uploads the same PT-day (even manual API ones) eat into this count. |\n| Token refresh fails | Delete `~/.config/youtube/token.json` and re-run; OAuth browser flow restarts |\n\n## After uploading\n\n- Results saved to `--results-file` (JSON: file, title, video id, URL)\n- Echo the URL list back to the user in a markdown table\n- Videos are `public` by default \u2014 they're live the moment the upload completes. If the user wanted a review buffer, mention they can re-upload with `--privacy unlisted` (or flip back to unlisted in YouTube Studio)\n- If they mentioned a \u5408\u96c6 / series, offer to create a YouTube playlist and batch-add the new uploads (the user must give a playlist ID or let you create one)\n","usage":"\u8bfb\u53d6 SKILL.md\uff0c\u5e76\u6309\u9700\u8bfb\u53d6\u914d\u5957\u6587\u4ef6\uff1b\u4e0b\u8f7d ZIP \u83b7\u53d6\u5b8c\u6574\u5305\u3002\u6280\u80fd\u662f\u4efb\u52a1\u65b9\u6cd5\uff0c\u4e0d\u6388\u4e88\u65b0\u589e\u8d26\u6237\u3001\u4ed8\u8d39\u6216\u7cfb\u7edf\u6743\u9650\u3002"}
