关于 youtube
通过 curl 和 jq 调用 YouTube Data API v3 的技能,使用已保存的 OAuth Bearer 令牌。可查询频道和视频,也支持上传;发布需要相应权限并先取得用户确认。
主要功能
- 01检查已连接的频道
- 02搜索视频、频道和播放列表
- 03读取上传记录、统计数据和评论
- 04通过可恢复请求上传视频
- 05处理 OAuth 权限与 API 错误
适用场景
- 01查看频道信息和上传历史
- 02查找视频并读取互动数据
- 03确认视频详情后发布
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ acedatacloud/ skills --skill youtube安装后在 Claude Code 中输入/youtube确认已加载。
附带文件scripts/list_uploads.py
详细介绍
使用 curl + jq 调用 YouTube Data API v3。用户的 OAuth Bearer 令牌位于 $GOOGLE_YOUTUBE_TOKEN;每次调用都必须将其作为
Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN 发送。基础 URL:
https://www.googleapis.com/youtube/v3。
令牌始终包含 youtube.readonly 和身份验证作用域
(openid email profile);如果用户在安装时选择启用,还会包含
youtube.upload(发布视频)。
响应均为标准 JSON;失败时会返回
{"error": {"code": 401|403|..., "message": "..."}} — 请原样显示该错误。
401 → 令牌已过期,用户必须重新连接 YouTube 连接器。上传时出现
403 insufficientPermissions → 用户未授予 youtube.upload;请让用户重新连接,并勾选上传选项。
务必先检查频道,以确认连接有效,并了解当前操作的是哪个频道。
curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails&mine=true" \
| jq '.items[0] | {title: .snippet.title, subs: .statistics.subscriberCount, views: .statistics.viewCount, uploads: .contentDetails.relatedPlaylists.uploads}'
搜索 YouTube
# Public search (any video). type can be video|channel|playlist.
curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
--data-urlencode "q=ai video automation" \
--data-urlencode "part=snippet" \
--data-urlencode "type=video" \
--data-urlencode "maxResults=10" \
-G "https://www.googleapis.com/youtube/v3/search" \
| jq '.items[] | {videoId: .id.videoId, title: .snippet.title, channel: .snippet.channelTitle, published: .snippet.publishedAt}'
添加 --data-urlencode "order=date|viewCount|rating|relevance" 可排序,
或添加 --data-urlencode "publishedAfter=2026-01-01T00:00:00Z" 可设置时间范围。
查看我上传的视频
YouTube 没有直接“列出我的视频”的调用 — 先读取频道的 上传内容播放列表,再逐页读取其中的条目。
python3 skills/youtube/scripts/list_uploads.py --max-pages 20
此辅助程序会跟进每个 nextPageToken,直到上传历史记录全部读取完毕
(或达到明确的安全上限)。它返回精简的 JSON,其中每个条目包含
videoId、title 和 published,此外还有 complete、pages_fetched 和
next_page_token。如果 complete 为 false,切勿声称某个视频不存在。
分页时,将上一个响应中的 .nextPageToken 作为参数传入:
--data-urlencode "pageToken=$PAGE_TOKEN"。
视频统计数据(观看次数 / 点赞数 / 评论数)
# Accepts a comma-separated id list.
curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
-G "https://www.googleapis.com/youtube/v3/videos" \
--data-urlencode "part=snippet,statistics,status" \
--data-urlencode "id=VIDEO_ID_1,VIDEO_ID_2" \
| jq '.items[] | {title: .snippet.title, views: .statistics.viewCount, likes: .statistics.likeCount, comments: .statistics.commentCount, privacy: .status.privacyStatus}'
查看视频评论
curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
-G "https://www.googleapis.com/youtube/v3/commentThreads" \
--data-urlencode "part=snippet" \
--data-urlencode "videoId=VIDEO_ID" \
--data-urlencode "maxResults=20" \
--data-urlencode "order=relevance" \
| jq '.items[] | .snippet.topLevelComment.snippet | {author: .authorDisplayName, text: .textDisplay, likes: .likeCount}'
上传视频(需要 youtube.upload)
发布前先向用户确认 — 展示即将上传的视频标题、隐私设置和 文件。上传分为两步,采用可恢复流程:先使用元数据初始化 → 再 PUT 文件内容。
如果源文件是 URL,先下载
通过生成 API(Maestro、Seedance、Veo、……)制作的视频会以 CDN URL 的形式返回,而不是本地文件。先下载,再确认实际获取到的是视频,然后再开始上传:
SRC="https://example.cdn.acedata.cloud/path/video.mp4"
FILE=$(mktemp) # plain mktemp — `mktemp /tmp/x-XXXXXX.mp4` is not portable
# -f: fail on 4xx/5xx instead of saving the error page. -L: follow CDN redirects.
curl -fsSL -o "$FILE" -w 'http=%{http_code} size=%{size_download}\n' "$SRC"
# Two distinct failures, two distinct messages — a 404/DNS error is a bad URL,
# not a bad video, and saying so saves the next step from misdiagnosing it.
[ -s "$FILE" ] \
|| { echo "download failed (see http= above) — nothing to upload"; rm -f "$FILE"; exit 1; }
# A 200 can still be an HTML interstitial or an expired-link page. Reject that
# rather than allow-listing one container, so WebM/MOV/AVI still pass. `grep -a`
# is required — without it grep treats a binary header as "no match".
head -c 512 "$FILE" | grep -qai '<html\|<!doctype' \
&& { echo "got an HTML page, not a video"; rm -f "$FILE"; exit 1; }
--upload-file 会忽略文件名;YouTube 依据 Content-Type: video/* 标头判断文件类型,因此没有扩展名的临时文件也可以。
上传操作
# Falls back to the placeholder only if the fetch step above didn't set FILE.
FILE="${FILE:-/path/to/video.mp4}"
TITLE="My title"
DESC="My description"
# privacyStatus: public | unlisted | private
META=$(jq -n --arg title "$TITLE" --arg description "$DESC" '{
snippet: {title: $title, description: $description, categoryId: "22"},
status: {privacyStatus: "unlisted", selfDeclaredMadeForKids: false}
}')
# 1. Init the resumable session. Keep headers, body and status so a missing
# Location reports the real API error instead of turning into an empty PUT URL.
INIT_HEADERS=$(mktemp)
INIT_BODY=$(mktemp)
INIT_HTTP=$(curl -sS -D "$INIT_HEADERS" -o "$INIT_BODY" -w '%{http_code}' \
-H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-H "X-Upload-Content-Type: video/*" \
-X POST "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status" \
-d "$META")
UPLOAD_URL=$(tr -d '\r' < "$INIT_HEADERS" | awk 'tolower($1) == "location:" {print $2; exit}')
if [ -z "$UPLOAD_URL" ]; then
echo "upload init failed: HTTP $INIT_HTTP: $(jq -r '.error.message // .' "$INIT_BODY" 2>/dev/null || cat "$INIT_BODY")"
rm -f "$INIT_HEADERS" "$INIT_BODY"
exit 1
fi
rm -f "$INIT_HEADERS" "$INIT_BODY"
# 2. Upload the bytes -> returns the created video resource (has .id).
RESULT=$(curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
-H "Content-Type: video/*" \
-X PUT --upload-file "$FILE" "$UPLOAD_URL")
echo "$RESULT" | jq -e .id >/dev/null 2>&1 \
|| { echo "upload failed: $(echo "$RESULT" | jq -r '.error.message' 2>/dev/null || echo "$RESULT")"; exit 1; }
VIDEO_ID=$(echo "$RESULT" | jq -r '.id')
VERIFY=$(curl -sS -H "Authorization: Bearer $GOOGLE_YOUTUBE_TOKEN" \
-G "https://www.googleapis.com/youtube/v3/videos" \
--data-urlencode "part=snippet,status,processingDetails" \
--data-urlencode "id=$VIDEO_ID")
echo "$VERIFY" | jq -e '.items[0].id == "'"$VIDEO_ID"'"' >/dev/null \
|| { echo "upload readback failed for $VIDEO_ID"; exit 1; }
echo "$VERIFY" | jq '.items[0] | {
video_id: .id,
canonical_url: ("https://www.youtube.com/watch?v=" + .id),
privacy: .status.privacyStatus,
upload_status: .status.uploadStatus,
processing_status: .processingDetails.processingStatus
}'
只有在确认返回了 id 后,才删除已下载的临时文件
(echo "$RESULT" | jq -e .id >/dev/null && rm -f "$FILE")。遇到 401/403 或连接中断时,下载的文件仍然可用;重新上传比重新获取几百 MB 数据更好。
确认上传成功并返回真实的 .id 后,恰好调用一次 publish_artifact,让结果显示在我的输出中。使用 kind="video"、
channel="youtube"、status="delivered"、真实标题,以及规范的
https://www.youtube.com/watch?v=<id> URL。在摘要中注明实际的隐私设置。如果上传失败或未返回 .id,不要记录为已交付的资源,也绝不可编造 URL。
categoryId 22 = “人物与博客”(安全的默认值)。要设置自定义缩略图(需要先处理好文件),请携带图片调用
POST /upload/youtube/v3/thumbnails/set?videoId=VIDEO_ID。
注意事项
- **配额:**Data API 按配额计量(默认每天 10,000 个单位)。一次
search消耗 100 个单位;一次upload消耗约 1,600 个单位。短时间内大量搜索 可能耗尽每日配额 →403 quotaExceeded;请明确告知用户。 - 没有“我的视频”端点 — 始终通过上传内容播放列表获取。
search结果最终才会一致 — 刚上传的视频可能要过几分钟或几小时才会出现在search中;请改为通过上传内容播放列表或视频 id 查询。- 上传的视频最初处于
uploaded/processing状态;处理完成前,统计数据均为0。