@zapier/youtube-connector 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -0
- package/NOTICE +8 -0
- package/README.md +117 -2
- package/SKILL.md +162 -0
- package/cli.js +71 -0
- package/cli.ts +5 -0
- package/connections.ts +8 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +1550 -0
- package/index.ts +79 -0
- package/package.json +59 -4
- package/preflight.sh +157 -0
- package/references/youtube-api-gotchas.md +252 -0
- package/scripts/addVideoToPlaylist.ts +75 -0
- package/scripts/createPlaylist.ts +73 -0
- package/scripts/deletePlaylist.ts +46 -0
- package/scripts/deleteVideo.ts +42 -0
- package/scripts/downloadCaption.ts +66 -0
- package/scripts/getChannel.ts +85 -0
- package/scripts/getVideo.ts +71 -0
- package/scripts/listCaptions.ts +54 -0
- package/scripts/listComments.ts +98 -0
- package/scripts/listPlaylistItems.ts +90 -0
- package/scripts/listPlaylists.ts +107 -0
- package/scripts/listSubscriptions.ts +103 -0
- package/scripts/listVideoCategories.ts +63 -0
- package/scripts/postComment.ts +65 -0
- package/scripts/rateVideo.ts +50 -0
- package/scripts/removeVideoFromPlaylist.ts +49 -0
- package/scripts/replyToComment.ts +90 -0
- package/scripts/searchVideos.ts +169 -0
- package/scripts/subscribeToChannel.ts +122 -0
- package/scripts/unsubscribeFromChannel.ts +49 -0
- package/scripts/updatePlaylist.ts +75 -0
- package/scripts/updateVideo.ts +143 -0
- package/tsup.config.ts +63 -0
package/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineConnector, toFunctions } from "@zapier/connectors-sdk";
|
|
2
|
+
|
|
3
|
+
import { connectionResolvers } from "./connections.ts";
|
|
4
|
+
import addVideoToPlaylistDefinition from "./scripts/addVideoToPlaylist.ts";
|
|
5
|
+
import createPlaylistDefinition from "./scripts/createPlaylist.ts";
|
|
6
|
+
import deletePlaylistDefinition from "./scripts/deletePlaylist.ts";
|
|
7
|
+
import deleteVideoDefinition from "./scripts/deleteVideo.ts";
|
|
8
|
+
import downloadCaptionDefinition from "./scripts/downloadCaption.ts";
|
|
9
|
+
import getChannelDefinition from "./scripts/getChannel.ts";
|
|
10
|
+
import getVideoDefinition from "./scripts/getVideo.ts";
|
|
11
|
+
import listCaptionsDefinition from "./scripts/listCaptions.ts";
|
|
12
|
+
import listCommentsDefinition from "./scripts/listComments.ts";
|
|
13
|
+
import listPlaylistItemsDefinition from "./scripts/listPlaylistItems.ts";
|
|
14
|
+
import listPlaylistsDefinition from "./scripts/listPlaylists.ts";
|
|
15
|
+
import listSubscriptionsDefinition from "./scripts/listSubscriptions.ts";
|
|
16
|
+
import listVideoCategoriesDefinition from "./scripts/listVideoCategories.ts";
|
|
17
|
+
import postCommentDefinition from "./scripts/postComment.ts";
|
|
18
|
+
import rateVideoDefinition from "./scripts/rateVideo.ts";
|
|
19
|
+
import removeVideoFromPlaylistDefinition from "./scripts/removeVideoFromPlaylist.ts";
|
|
20
|
+
import replyToCommentDefinition from "./scripts/replyToComment.ts";
|
|
21
|
+
import searchVideosDefinition from "./scripts/searchVideos.ts";
|
|
22
|
+
import subscribeToChannelDefinition from "./scripts/subscribeToChannel.ts";
|
|
23
|
+
import unsubscribeFromChannelDefinition from "./scripts/unsubscribeFromChannel.ts";
|
|
24
|
+
import updatePlaylistDefinition from "./scripts/updatePlaylist.ts";
|
|
25
|
+
import updateVideoDefinition from "./scripts/updateVideo.ts";
|
|
26
|
+
|
|
27
|
+
const connector = defineConnector({
|
|
28
|
+
scripts: {
|
|
29
|
+
addVideoToPlaylist: addVideoToPlaylistDefinition,
|
|
30
|
+
createPlaylist: createPlaylistDefinition,
|
|
31
|
+
deletePlaylist: deletePlaylistDefinition,
|
|
32
|
+
deleteVideo: deleteVideoDefinition,
|
|
33
|
+
downloadCaption: downloadCaptionDefinition,
|
|
34
|
+
getChannel: getChannelDefinition,
|
|
35
|
+
getVideo: getVideoDefinition,
|
|
36
|
+
listCaptions: listCaptionsDefinition,
|
|
37
|
+
listComments: listCommentsDefinition,
|
|
38
|
+
listPlaylistItems: listPlaylistItemsDefinition,
|
|
39
|
+
listPlaylists: listPlaylistsDefinition,
|
|
40
|
+
listSubscriptions: listSubscriptionsDefinition,
|
|
41
|
+
listVideoCategories: listVideoCategoriesDefinition,
|
|
42
|
+
postComment: postCommentDefinition,
|
|
43
|
+
rateVideo: rateVideoDefinition,
|
|
44
|
+
removeVideoFromPlaylist: removeVideoFromPlaylistDefinition,
|
|
45
|
+
replyToComment: replyToCommentDefinition,
|
|
46
|
+
searchVideos: searchVideosDefinition,
|
|
47
|
+
subscribeToChannel: subscribeToChannelDefinition,
|
|
48
|
+
unsubscribeFromChannel: unsubscribeFromChannelDefinition,
|
|
49
|
+
updatePlaylist: updatePlaylistDefinition,
|
|
50
|
+
updateVideo: updateVideoDefinition,
|
|
51
|
+
},
|
|
52
|
+
connectionResolvers,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default connector;
|
|
56
|
+
export const {
|
|
57
|
+
addVideoToPlaylist,
|
|
58
|
+
createPlaylist,
|
|
59
|
+
deletePlaylist,
|
|
60
|
+
deleteVideo,
|
|
61
|
+
downloadCaption,
|
|
62
|
+
getChannel,
|
|
63
|
+
getVideo,
|
|
64
|
+
listCaptions,
|
|
65
|
+
listComments,
|
|
66
|
+
listPlaylistItems,
|
|
67
|
+
listPlaylists,
|
|
68
|
+
listSubscriptions,
|
|
69
|
+
listVideoCategories,
|
|
70
|
+
postComment,
|
|
71
|
+
rateVideo,
|
|
72
|
+
removeVideoFromPlaylist,
|
|
73
|
+
replyToComment,
|
|
74
|
+
searchVideos,
|
|
75
|
+
subscribeToChannel,
|
|
76
|
+
unsubscribeFromChannel,
|
|
77
|
+
updatePlaylist,
|
|
78
|
+
updateVideo,
|
|
79
|
+
} = toFunctions(connector);
|
package/package.json
CHANGED
|
@@ -1,7 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zapier/youtube-connector",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Agent-callable YouTube tools — search and read videos, upload and update videos, manage playlists and playlist items, read and post comments, rate videos, manage subscriptions, and read channel and caption metadata. Use when the user mentions YouTube or wants to find, upload, comment on, or organize YouTube videos and playlists, even if they don't name YouTube explicitly.",
|
|
5
8
|
"license": "Elastic-2.0",
|
|
6
|
-
"
|
|
7
|
-
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./index.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"@zapier/youtube-connector": "./cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"cli.js",
|
|
22
|
+
"*.ts",
|
|
23
|
+
"scripts/",
|
|
24
|
+
"preflight.sh",
|
|
25
|
+
"SKILL.md",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"references/",
|
|
29
|
+
"NOTICE"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@zapier/connectors-sdk": "^0.1.0",
|
|
33
|
+
"zod": "^4.0.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@zapier/zapier-sdk": ">=0.59.0 <1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"youtube",
|
|
41
|
+
"zapier",
|
|
42
|
+
"connector",
|
|
43
|
+
"tools",
|
|
44
|
+
"skills",
|
|
45
|
+
"mcp",
|
|
46
|
+
"agent",
|
|
47
|
+
"ai",
|
|
48
|
+
"automation"
|
|
49
|
+
],
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/zapier/connectors.git",
|
|
53
|
+
"directory": "apps/youtube"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"tsup": "^8.0.0",
|
|
57
|
+
"typescript": "^5.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/preflight.sh
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Connector pre-flight readiness check.
|
|
3
|
+
#
|
|
4
|
+
# Managed by @zapier/connectors-dev — do not edit; synced byte-for-byte
|
|
5
|
+
# across every connector.
|
|
6
|
+
#
|
|
7
|
+
# Runs inside whatever agent harness installed the connector (Cursor, Claude
|
|
8
|
+
# Code, Codex, Gemini CLI, Goose, ...) — often a minimal container — and answers
|
|
9
|
+
# ONE question: how do I run the TypeScript scripts here? It picks a runtime —
|
|
10
|
+
# Node 22.18+ when it can already resolve the connector's deps, else an explicit
|
|
11
|
+
# install step (`npm install`, or `bun install` when only Bun is present) — and
|
|
12
|
+
# tells the agent the exact command to run (see EXIT CODES). When deps are
|
|
13
|
+
# missing it disambiguates the two sandbox failures that block an install: a
|
|
14
|
+
# read-only connector dir (must run unsandboxed / be granted write) vs. a
|
|
15
|
+
# blocked home dir (point the package cache inside this dir). Both surface as a
|
|
16
|
+
# misleading `EPERM`, so the recommendation names the actual fix.
|
|
17
|
+
#
|
|
18
|
+
# NEEDS_ACTION is a single self-verifying step (install a runtime / deps), not a
|
|
19
|
+
# loop: do it, then run a script. The action confirms its own success and the
|
|
20
|
+
# first `--help` run is the authoritative check, so re-running this pre-flight to
|
|
21
|
+
# reconfirm is optional, not required.
|
|
22
|
+
#
|
|
23
|
+
# THE AGENT CONTRACT IS THE STDOUT, NOT THIS HEADER. Agents don't read this file;
|
|
24
|
+
# they run it and parse the `PREFLIGHT_*` lines — each value starts with a stable
|
|
25
|
+
# token (parse as `KEY: (\w+)`), with an optional human gloss in parens, and
|
|
26
|
+
# `PREFLIGHT_RECOMMENDATION` is the one-line next step. SKILL.md "Step 0" is the
|
|
27
|
+
# agent-facing spec; this header is for maintainers of the canonical script.
|
|
28
|
+
#
|
|
29
|
+
# WHY POSIX sh (not bash)
|
|
30
|
+
# Minimal sandboxes often ship only BusyBox `sh` with no bash. This script runs
|
|
31
|
+
# unchanged under BusyBox sh, dash, and bash, and never hard-requires
|
|
32
|
+
# node/bun/npm — a missing runtime degrades to a NEEDS_ACTION instruction.
|
|
33
|
+
#
|
|
34
|
+
# EXIT CODES (the verdict; also emitted on PREFLIGHT_STATUS)
|
|
35
|
+
# 0 READY a runtime + deps are in place; run the scripts
|
|
36
|
+
# 1 NEEDS_ACTION perform the printed action (install runtime / deps), then
|
|
37
|
+
# run a script — re-running this check is optional
|
|
38
|
+
|
|
39
|
+
set -u
|
|
40
|
+
|
|
41
|
+
EXIT_READY=0
|
|
42
|
+
EXIT_NEEDS_ACTION=1
|
|
43
|
+
|
|
44
|
+
# Directory this script lives in — deps + scripts are resolved relative to it,
|
|
45
|
+
# not the caller's cwd, so `./preflight.sh` works from anywhere.
|
|
46
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
47
|
+
|
|
48
|
+
has() {
|
|
49
|
+
command -v "$1" >/dev/null 2>&1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Node >= 22.18 is the connector baseline (native .ts stripping). Anything older
|
|
53
|
+
# is treated as "no Node" so we fall back to Bun.
|
|
54
|
+
node_ge_2218() {
|
|
55
|
+
has node || return 1
|
|
56
|
+
v=$(node -v 2>/dev/null) || return 1
|
|
57
|
+
v=${v#v}
|
|
58
|
+
major=${v%%.*}
|
|
59
|
+
rest=${v#*.}
|
|
60
|
+
minor=${rest%%.*}
|
|
61
|
+
case "$major" in '' | *[!0-9]*) return 1 ;; esac
|
|
62
|
+
case "$minor" in '' | *[!0-9]*) minor=0 ;; esac
|
|
63
|
+
[ "$major" -gt 22 ] && return 0
|
|
64
|
+
[ "$major" -eq 22 ] && [ "$minor" -ge 18 ] && return 0
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Are the connector's declared deps installed where Node would find them? Node
|
|
69
|
+
# won't fetch — they must be on disk (this dir's node_modules or an ancestor's).
|
|
70
|
+
# Reads the connector's own package.json, so this stays connector-agnostic. A
|
|
71
|
+
# bare `[ -d node_modules ]` is the wrong test: under pnpm/monorepo layouts the
|
|
72
|
+
# deps can live in an ancestor (or be hoisted), and a local node_modules can
|
|
73
|
+
# exist without the package being present. We check each dep's package.json
|
|
74
|
+
# exists in one of Node's resolution paths rather than `require.resolve(name)`,
|
|
75
|
+
# because resolving the package ENTRY can fail for ESM-only / `exports`-map
|
|
76
|
+
# packages even when they're fully installed and importable.
|
|
77
|
+
node_resolves() {
|
|
78
|
+
( CDPATH= cd -- "$SCRIPT_DIR" && node -e 'const fs=require("fs"),path=require("path");const d=require("./package.json").dependencies||{};for(const k of Object.keys(d)){const ps=require.resolve.paths(k)||[];if(!ps.some(b=>fs.existsSync(path.join(b,k,"package.json"))))process.exit(1);}' ) >/dev/null 2>&1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Can we actually WRITE into this directory right now? Any dep install must land
|
|
82
|
+
# `node_modules/` here, so if the harness mounts the connector read-only (common
|
|
83
|
+
# when skills live under ~/.<agent>/skills, outside the agent's writable
|
|
84
|
+
# workspace) no install can succeed in place — the only fixes are to run it
|
|
85
|
+
# unsandboxed or grant write access. Two deliberate choices:
|
|
86
|
+
# - Probe with a real create+remove, not `[ -w ]`: a sandbox denies the write
|
|
87
|
+
# at the syscall while the permission bits still look writable.
|
|
88
|
+
# - Probe by creating a DIRECTORY (`mkdir`), not a file: that's the install's
|
|
89
|
+
# very first on-disk action (node_modules/ and the cache dirs), and at least
|
|
90
|
+
# one sandbox (Claude Code) permits creating a file here while denying
|
|
91
|
+
# `mkdir` — a file-based probe reports writable and the install then EPERMs.
|
|
92
|
+
dir_writable() {
|
|
93
|
+
_t="$SCRIPT_DIR/.preflight-write-test.$$"
|
|
94
|
+
mkdir "$_t" 2>/dev/null || return 1
|
|
95
|
+
rmdir "$_t" 2>/dev/null
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---- 1) Pick a runtime -----------------------------------------------------
|
|
100
|
+
# Node 22.18+ (native TS strip) is the baseline and the preferred runner whenever
|
|
101
|
+
# it's present — it runs the .ts scripts directly. Bun is the fallback runner
|
|
102
|
+
# only when there's no usable Node. We DON'T lean on Bun's implicit auto-install:
|
|
103
|
+
# it's silently suppressed by any ancestor node_modules (monorepo layouts) and
|
|
104
|
+
# fails the same way Node's `npm install` does under a sandbox that blocks Bun's
|
|
105
|
+
# home cache — so missing deps are always an explicit, cache-local install step.
|
|
106
|
+
nresolves=false
|
|
107
|
+
node_ge_2218 && node_resolves && nresolves=true
|
|
108
|
+
|
|
109
|
+
if [ "$nresolves" = true ]; then
|
|
110
|
+
runner=node
|
|
111
|
+
ready=true
|
|
112
|
+
elif node_ge_2218; then
|
|
113
|
+
runner=node # present but deps unresolved — install step below
|
|
114
|
+
ready=false
|
|
115
|
+
elif has bun; then
|
|
116
|
+
runner=bun # no usable Node — Bun runs the scripts; install step below
|
|
117
|
+
ready=false
|
|
118
|
+
else
|
|
119
|
+
echo "PREFLIGHT_STATUS: NEEDS_ACTION"
|
|
120
|
+
echo "PREFLIGHT_RECOMMENDATION: no Node 22.18+ or Bun found — install Node 22.18+ (ships npm) or Bun, then run \`<runner> ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
121
|
+
exit "$EXIT_NEEDS_ACTION"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ---- 2) Install deps if the chosen runtime can't run as-is -----------------
|
|
125
|
+
# Two distinct sandbox failures to disambiguate, because their fixes differ:
|
|
126
|
+
# a) This directory is READ-ONLY (skills installed outside the agent's
|
|
127
|
+
# writable workspace). No install can place node_modules here — the agent
|
|
128
|
+
# must run it unsandboxed or be granted write access. A cache flag can't
|
|
129
|
+
# help.
|
|
130
|
+
# b) The directory is writable but the HOME dir is sandboxed, so the default
|
|
131
|
+
# ~/.npm / ~/.bun cache write is what EPERMs mid-install. Pointing the cache
|
|
132
|
+
# INSIDE this directory sidesteps that without disabling the sandbox (and is
|
|
133
|
+
# harmless otherwise — just an unused cache dir).
|
|
134
|
+
# So probe writability first and recommend accordingly.
|
|
135
|
+
if [ "$ready" != true ]; then
|
|
136
|
+
echo "PREFLIGHT_STATUS: NEEDS_ACTION"
|
|
137
|
+
[ "$runner" = bun ] && install_cmd="bun install" || install_cmd="npm install"
|
|
138
|
+
if ! dir_writable; then
|
|
139
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed and ${SCRIPT_DIR} is read-only in the current sandbox (a test write there failed) — \`${install_cmd}\` can't place node_modules here. Run the install with the sandbox disabled, or grant the agent write access to ${SCRIPT_DIR} (a cache flag won't help — node_modules must land in this directory); then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
140
|
+
elif [ "$runner" = bun ]; then
|
|
141
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`BUN_INSTALL_CACHE_DIR=\"${SCRIPT_DIR}/.bun-cache\" bun install\` in ${SCRIPT_DIR} (the workspace-local cache survives a sandbox that blocks ~/.bun; plain \`bun install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
142
|
+
elif has npm; then
|
|
143
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR} (the workspace-local --cache survives a sandbox that blocks ~/.npm; plain \`npm install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
144
|
+
else
|
|
145
|
+
# node >= 22.18 ships npm, so a missing npm means it was removed from the
|
|
146
|
+
# Node install. Restore it, then install with a workspace-local cache.
|
|
147
|
+
echo "PREFLIGHT_RECOMMENDATION: npm is missing (it ships with Node 22.18+) — reinstall/repair Node 22.18+, run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR}, then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
148
|
+
fi
|
|
149
|
+
exit "$EXIT_NEEDS_ACTION"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# ---- 3) Ready --------------------------------------------------------------
|
|
153
|
+
# Runtime + deps are in place — the scripts run.
|
|
154
|
+
echo "PREFLIGHT_STATUS: READY"
|
|
155
|
+
echo "PREFLIGHT_RUNNER: ${runner}"
|
|
156
|
+
echo "PREFLIGHT_RECOMMENDATION: run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials, then run the script with the required env vars set."
|
|
157
|
+
exit "$EXIT_READY"
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# YouTube Data API — gotchas
|
|
2
|
+
|
|
3
|
+
Non-obvious behaviors of the YouTube Data API v3 that affect how tools should be
|
|
4
|
+
called and how their responses should be interpreted. Every non-trivial claim links
|
|
5
|
+
to the public source it was taken from.
|
|
6
|
+
|
|
7
|
+
## Parts & fields
|
|
8
|
+
|
|
9
|
+
- Every resource is partitioned into named **parts** (e.g. `snippet`,
|
|
10
|
+
`contentDetails`, `statistics`, `status`). The `part` parameter "is a required
|
|
11
|
+
parameter for any API request that retrieves or returns a resource" and identifies
|
|
12
|
+
which parts the response will include — request a part or its fields will be
|
|
13
|
+
absent. ([overview](https://developers.google.com/youtube/v3/getting-started),
|
|
14
|
+
[videos.list](https://developers.google.com/youtube/v3/docs/videos/list))
|
|
15
|
+
- On a **write**, `part` does double duty: it "identifies the properties that the
|
|
16
|
+
write operation will set as well as the properties that the API response will
|
|
17
|
+
include." ([playlists.insert](https://developers.google.com/youtube/v3/docs/playlists/insert))
|
|
18
|
+
|
|
19
|
+
## Quota & rate limits
|
|
20
|
+
|
|
21
|
+
- Default allocation is "10,000 units per day combined for all other endpoints,"
|
|
22
|
+
plus separate per-call allowances for the bucketed methods below.
|
|
23
|
+
([overview](https://developers.google.com/youtube/v3/getting-started),
|
|
24
|
+
[quota guide](https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits))
|
|
25
|
+
- Cost per call (verbatim "quota cost" from each method's reference page):
|
|
26
|
+
- read / list (videos, channels, playlistItems, commentThreads, videoCategories,
|
|
27
|
+
subscriptions): **1 unit**.
|
|
28
|
+
([videos.list](https://developers.google.com/youtube/v3/docs/videos/list),
|
|
29
|
+
[playlistItems.list](https://developers.google.com/youtube/v3/docs/playlistItems/list),
|
|
30
|
+
[commentThreads.list](https://developers.google.com/youtube/v3/docs/commentThreads/list),
|
|
31
|
+
[subscriptions.list](https://developers.google.com/youtube/v3/docs/subscriptions/list))
|
|
32
|
+
- write (videos.update, playlists.insert, subscriptions.insert,
|
|
33
|
+
commentThreads.insert, comments.insert, videos.rate, videos.delete): **50 units**.
|
|
34
|
+
([videos.update](https://developers.google.com/youtube/v3/docs/videos/update),
|
|
35
|
+
[playlists.insert](https://developers.google.com/youtube/v3/docs/playlists/insert),
|
|
36
|
+
[subscriptions.insert](https://developers.google.com/youtube/v3/docs/subscriptions/insert),
|
|
37
|
+
[commentThreads.insert](https://developers.google.com/youtube/v3/docs/commentThreads/insert),
|
|
38
|
+
[comments.insert](https://developers.google.com/youtube/v3/docs/comments/insert),
|
|
39
|
+
[videos.rate](https://developers.google.com/youtube/v3/docs/videos/rate))
|
|
40
|
+
- `captions.list`: **50 units**; `captions.download`: **200 units** (the
|
|
41
|
+
most expensive read in this catalog).
|
|
42
|
+
([captions.list](https://developers.google.com/youtube/v3/docs/captions/list),
|
|
43
|
+
[captions.download](https://developers.google.com/youtube/v3/docs/captions/download))
|
|
44
|
+
- `search.list` is **no longer charged against the 10,000-unit pool**. As of June 1,
|
|
45
|
+
2026 the API "is transitioning to a granular quota system … starting with
|
|
46
|
+
`videos.insert` and `search.list`," which "will be charged to their own respective
|
|
47
|
+
quota buckets." The current per-method page states `search.list` "has a quota cost
|
|
48
|
+
of 1 unit in the Search Queries quota bucket" — older guidance citing 100 units
|
|
49
|
+
against the main pool is stale.
|
|
50
|
+
([revision history](https://developers.google.com/youtube/v3/revision_history),
|
|
51
|
+
[search.list](https://developers.google.com/youtube/v3/docs/search/list))
|
|
52
|
+
- When the daily quota is exhausted the API returns `quotaExceeded` (403). The error
|
|
53
|
+
docs describe no `Retry-After` header; treat quota exhaustion as non-retryable until
|
|
54
|
+
the daily reset rather than backing off in a loop.
|
|
55
|
+
([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
56
|
+
|
|
57
|
+
## Errors & recovery
|
|
58
|
+
|
|
59
|
+
- All errors share Google's standard envelope:
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"error": {
|
|
63
|
+
"errors": [{ "domain": "...", "reason": "...", "message": "..." }],
|
|
64
|
+
"code": 400,
|
|
65
|
+
"message": "..."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
The actionable signal is `error.errors[0].reason`.
|
|
70
|
+
([error format](https://developers.google.com/youtube/v3/docs/core_errors))
|
|
71
|
+
- `quotaExceeded` (403): "The request cannot be completed because you have exceeded
|
|
72
|
+
your quota." → stop; do not retry until quota resets.
|
|
73
|
+
([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
74
|
+
- `insufficientPermissions` (403): "The OAuth 2.0 token provided for the request
|
|
75
|
+
specifies scopes that are insufficient for accessing the requested data." →
|
|
76
|
+
reconnect with the scope the operation needs.
|
|
77
|
+
([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
78
|
+
- `forbidden` (403): "Access forbidden. The request may not be properly authorized."
|
|
79
|
+
→ either a missing scope or you do not own the resource (reconnecting won't fix
|
|
80
|
+
ownership). ([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
81
|
+
- `notFound` (404): the identified resource "cannot be found" → verify the id.
|
|
82
|
+
([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
83
|
+
- `authorizationRequired` (401): e.g. "The request uses the `mine` parameter but is
|
|
84
|
+
not properly authorized." → reconnect.
|
|
85
|
+
([errors](https://developers.google.com/youtube/v3/docs/errors))
|
|
86
|
+
- `subscriptionDuplicate` (400): "The subscription that you are trying to create
|
|
87
|
+
already exists." This is a post-condition-satisfied state, not a hard failure — the
|
|
88
|
+
user is already subscribed.
|
|
89
|
+
([subscriptions.insert](https://developers.google.com/youtube/v3/docs/subscriptions/insert))
|
|
90
|
+
|
|
91
|
+
## OAuth scopes & ownership
|
|
92
|
+
|
|
93
|
+
Scope descriptions (from the consent screen):
|
|
94
|
+
|
|
95
|
+
- `youtube.readonly` — "View your YouTube account."
|
|
96
|
+
- `youtube` — "Manage your YouTube account."
|
|
97
|
+
- `youtube.force-ssl` — "See, edit, and permanently delete your YouTube videos,
|
|
98
|
+
ratings, comments and captions."
|
|
99
|
+
([scopes](https://developers.google.com/youtube/v3/guides/auth/installed-apps))
|
|
100
|
+
|
|
101
|
+
- **Comment and caption writes require `youtube.force-ssl`.** `commentThreads.insert`,
|
|
102
|
+
`comments.insert`, `captions.list`, and `captions.download` all list
|
|
103
|
+
`youtube.force-ssl` among their accepted scopes.
|
|
104
|
+
([commentThreads.insert](https://developers.google.com/youtube/v3/docs/commentThreads/insert),
|
|
105
|
+
[comments.insert](https://developers.google.com/youtube/v3/docs/comments/insert),
|
|
106
|
+
[captions.list](https://developers.google.com/youtube/v3/docs/captions/list),
|
|
107
|
+
[captions.download](https://developers.google.com/youtube/v3/docs/captions/download))
|
|
108
|
+
- **Ownership:** downloading a caption track "requires the user to have permission to
|
|
109
|
+
edit the video" (the video's owner or an editor), not merely read access.
|
|
110
|
+
([captions.download](https://developers.google.com/youtube/v3/docs/captions/download))
|
|
111
|
+
`textOriginal` for a comment "is only returned to the authenticated user if they are
|
|
112
|
+
the comment's author."
|
|
113
|
+
([comments resource](https://developers.google.com/youtube/v3/docs/comments))
|
|
114
|
+
|
|
115
|
+
## Pagination
|
|
116
|
+
|
|
117
|
+
- List methods return `nextPageToken` which "identifies the next page of the result
|
|
118
|
+
that can be retrieved"; pass it back as `pageToken`. When it is absent, there are no
|
|
119
|
+
more pages. ([commentThreads.list](https://developers.google.com/youtube/v3/docs/commentThreads/list))
|
|
120
|
+
- `maxResults` caps differ by resource:
|
|
121
|
+
- search, playlistItems, subscriptions: 0–50, default 5.
|
|
122
|
+
([search.list](https://developers.google.com/youtube/v3/docs/search/list),
|
|
123
|
+
[playlistItems.list](https://developers.google.com/youtube/v3/docs/playlistItems/list),
|
|
124
|
+
[subscriptions.list](https://developers.google.com/youtube/v3/docs/subscriptions/list))
|
|
125
|
+
- commentThreads: 1–100, default 20.
|
|
126
|
+
([commentThreads.list](https://developers.google.com/youtube/v3/docs/commentThreads/list))
|
|
127
|
+
|
|
128
|
+
## IDs
|
|
129
|
+
|
|
130
|
+
- A `playlistItem` id is distinct from the video id it points to — use the
|
|
131
|
+
playlistItem id to remove an item.
|
|
132
|
+
- A `subscription` id is distinct from the channel id — use the subscription id to
|
|
133
|
+
unsubscribe.
|
|
134
|
+
- `channels.list` `id` "specifies a comma-separated list of the YouTube channel ID(s)."
|
|
135
|
+
([channels.list](https://developers.google.com/youtube/v3/docs/channels/list))
|
|
136
|
+
|
|
137
|
+
## Statistics (counts as strings)
|
|
138
|
+
|
|
139
|
+
- Count fields are typed `unsigned long` and come back as JSON **strings**, not
|
|
140
|
+
numbers: `viewCount`, `likeCount`, `commentCount` (videos), and `viewCount`,
|
|
141
|
+
`subscriberCount`, `videoCount` (channels). Do not coerce blindly.
|
|
142
|
+
([videos resource](https://developers.google.com/youtube/v3/docs/videos),
|
|
143
|
+
[channels resource](https://developers.google.com/youtube/v3/docs/channels))
|
|
144
|
+
- `channels` `subscriberCount` "is rounded down to three significant figures," and
|
|
145
|
+
`hiddenSubscriberCount` "Indicates whether the channel's subscriber count is publicly
|
|
146
|
+
visible" — when hidden, treat `subscriberCount` as unavailable.
|
|
147
|
+
([channels resource](https://developers.google.com/youtube/v3/docs/channels))
|
|
148
|
+
- `channels` `videoCount` "reflects the count of the channel's public videos only,
|
|
149
|
+
even to owners." ([channels resource](https://developers.google.com/youtube/v3/docs/channels))
|
|
150
|
+
|
|
151
|
+
## Per-resource notes
|
|
152
|
+
|
|
153
|
+
### Videos
|
|
154
|
+
|
|
155
|
+
- `contentDetails.duration` is an ISO 8601 duration, e.g. `PT15M33S` for 15 min 33 s.
|
|
156
|
+
([videos resource](https://developers.google.com/youtube/v3/docs/videos))
|
|
157
|
+
- `contentDetails.caption` is the **string** `"true"` or `"false"`, not a boolean.
|
|
158
|
+
([videos resource](https://developers.google.com/youtube/v3/docs/videos))
|
|
159
|
+
- `status.uploadStatus` ∈ {`deleted`, `failed`, `processed`, `rejected`, `uploaded`};
|
|
160
|
+
`status.privacyStatus` ∈ {`private`, `public`, `unlisted`}.
|
|
161
|
+
([videos resource](https://developers.google.com/youtube/v3/docs/videos))
|
|
162
|
+
- `status.publishAt` (scheduled publish) "can be set only if the privacy status of the
|
|
163
|
+
video is private." ([videos resource](https://developers.google.com/youtube/v3/docs/videos))
|
|
164
|
+
- COPPA: `selfDeclaredMadeForKids` lets the channel owner "designate the video as being
|
|
165
|
+
child-directed" on insert/update; `madeForKids` is the resulting status.
|
|
166
|
+
([videos resource](https://developers.google.com/youtube/v3/docs/videos))
|
|
167
|
+
- **`videos.update` replaces, it does not merge.** "this method will override the
|
|
168
|
+
existing values for all of the mutable properties that are contained in any parts
|
|
169
|
+
that the parameter value specifies," and "if your request does not specify a value
|
|
170
|
+
for a property that already has a value, the property's existing value will be
|
|
171
|
+
deleted." Read the current resource, modify, then write back the whole part.
|
|
172
|
+
([videos.update](https://developers.google.com/youtube/v3/docs/videos/update))
|
|
173
|
+
- `videos.update` with a `snippet` part requires `snippet.title` and
|
|
174
|
+
`snippet.categoryId`.
|
|
175
|
+
([videos.update](https://developers.google.com/youtube/v3/docs/videos/update))
|
|
176
|
+
- `videos.insert` `notifySubscribers` default "is True."
|
|
177
|
+
([videos.insert](https://developers.google.com/youtube/v3/docs/videos/insert))
|
|
178
|
+
- A video title "has a character limit of 100 characters and cannot include invalid
|
|
179
|
+
characters." ([title limits, Help Center](https://support.google.com/youtube/answer/57404))
|
|
180
|
+
- `videos.rate` returns "an HTTP `204` response code (`No Content`)" — empty body on
|
|
181
|
+
success; `rating` ∈ {`like`, `dislike`, `none`}.
|
|
182
|
+
([videos.rate](https://developers.google.com/youtube/v3/docs/videos/rate))
|
|
183
|
+
|
|
184
|
+
### Search
|
|
185
|
+
|
|
186
|
+
- A `search.list` result resource contains only `kind`, `etag`, `id`, and `snippet` —
|
|
187
|
+
no `statistics` or `contentDetails`. The `snippet` "contains basic details about a
|
|
188
|
+
search result, such as its title or description"; call `getVideo` (videos.list) when
|
|
189
|
+
you need view counts, duration, or other full-resource fields.
|
|
190
|
+
([searchResult resource](https://developers.google.com/youtube/v3/docs/search))
|
|
191
|
+
- `order` ∈ {`date`, `rating`, `relevance` (default), `title`, `videoCount`,
|
|
192
|
+
`viewCount`}; `publishedAfter`/`publishedBefore` take RFC 3339 date-times;
|
|
193
|
+
`relevanceLanguage` is an ISO 639-1 two-letter code.
|
|
194
|
+
([search.list](https://developers.google.com/youtube/v3/docs/search/list))
|
|
195
|
+
|
|
196
|
+
### Playlists
|
|
197
|
+
|
|
198
|
+
- `status.privacyStatus` ∈ {`private`, `public`, `unlisted`}; no default is documented
|
|
199
|
+
in the API reference.
|
|
200
|
+
([playlists resource](https://developers.google.com/youtube/v3/docs/playlists))
|
|
201
|
+
- `playlistItems.list` `maxResults` is 0–50 (default 5).
|
|
202
|
+
([playlistItems.list](https://developers.google.com/youtube/v3/docs/playlistItems/list))
|
|
203
|
+
|
|
204
|
+
### Comments
|
|
205
|
+
|
|
206
|
+
- `textDisplay` "can be retrieved in either plain text or HTML" and "may differ from
|
|
207
|
+
the original comment text. For example, it may replace video links with video
|
|
208
|
+
titles." `textOriginal` is "the original, raw text." Control format via
|
|
209
|
+
`commentThreads.list`'s `textFormat` (`html` default, or `plainText`).
|
|
210
|
+
([comments resource](https://developers.google.com/youtube/v3/docs/comments),
|
|
211
|
+
[commentThreads.list](https://developers.google.com/youtube/v3/docs/commentThreads/list))
|
|
212
|
+
- To create a top-level comment use `commentThreads.insert`; `comments.insert` "handles
|
|
213
|
+
replies to existing comments, requiring the `snippet.parentId` property."
|
|
214
|
+
([comments.insert](https://developers.google.com/youtube/v3/docs/comments/insert))
|
|
215
|
+
|
|
216
|
+
### Captions
|
|
217
|
+
|
|
218
|
+
- `snippet.trackKind` ∈ {`standard` (a regular caption track, the default), `ASR`
|
|
219
|
+
(generated by automatic speech recognition), `forced`}; `snippet.status` ∈
|
|
220
|
+
{`serving`, `syncing`, `failed`}; `snippet.language` is a BCP-47 tag.
|
|
221
|
+
([captions resource](https://developers.google.com/youtube/v3/docs/captions))
|
|
222
|
+
- `captions.download` `tfmt` ∈ {`sbv`, `scc`, `srt`, `ttml`, `vtt`}; `tlang` requests a
|
|
223
|
+
machine translation (ISO 639-1 code). Returns a raw caption file body, not JSON.
|
|
224
|
+
([captions.download](https://developers.google.com/youtube/v3/docs/captions/download))
|
|
225
|
+
- Insufficient permission to download a track returns 403: "The permissions associated
|
|
226
|
+
with the request are not sufficient to download the caption track."
|
|
227
|
+
([captions.download](https://developers.google.com/youtube/v3/docs/captions/download))
|
|
228
|
+
|
|
229
|
+
### Channels
|
|
230
|
+
|
|
231
|
+
- Resolve the authenticated user's channel with `mine=true`; look up others with `id`
|
|
232
|
+
(comma-separated) or `forHandle` ("a YouTube handle … can be prepended with an `@`
|
|
233
|
+
symbol"). ([channels.list](https://developers.google.com/youtube/v3/docs/channels/list))
|
|
234
|
+
- A channel's uploads playlist is `contentDetails.relatedPlaylists.uploads` — "The ID
|
|
235
|
+
of the playlist that contains the channel's uploaded videos." Pass it to
|
|
236
|
+
`playlistItems.list` to enumerate a channel's videos.
|
|
237
|
+
([channels resource](https://developers.google.com/youtube/v3/docs/channels))
|
|
238
|
+
|
|
239
|
+
### Subscriptions
|
|
240
|
+
|
|
241
|
+
- `subscriptions.list` `forChannelId` "specifies a comma-separated list of channel
|
|
242
|
+
IDs" to filter by; combine with `mine=true` to check whether the user is subscribed
|
|
243
|
+
to a specific channel.
|
|
244
|
+
([subscriptions.list](https://developers.google.com/youtube/v3/docs/subscriptions/list))
|
|
245
|
+
|
|
246
|
+
### Video categories
|
|
247
|
+
|
|
248
|
+
- Only categories whose `snippet.assignable` is true can be set on a video —
|
|
249
|
+
`assignable` "Indicates whether videos can be associated with the category."
|
|
250
|
+
([videoCategories resource](https://developers.google.com/youtube/v3/docs/videoCategories))
|
|
251
|
+
- Categories are region-specific; `videoCategories.list` is queried by `regionCode`.
|
|
252
|
+
([videoCategories.list](https://developers.google.com/youtube/v3/docs/videoCategories/list))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import { PlaylistItemSchema, throwForYouTube } from "../lib/youtube.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
snippet: z
|
|
11
|
+
.object({
|
|
12
|
+
playlistId: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe(
|
|
15
|
+
"The id of the playlist to add the video to (from listPlaylists; you must own it).",
|
|
16
|
+
),
|
|
17
|
+
resourceId: z
|
|
18
|
+
.object({
|
|
19
|
+
kind: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("Always youtube#video for adding a video.")
|
|
22
|
+
.default("youtube#video"),
|
|
23
|
+
videoId: z.string().describe("The 11-char id of the video to add."),
|
|
24
|
+
})
|
|
25
|
+
.strict()
|
|
26
|
+
.describe(
|
|
27
|
+
"The video to add. Set kind to youtube#video and videoId to the 11-char video id.",
|
|
28
|
+
),
|
|
29
|
+
position: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.describe("0-based insert position. Omit to append at the end.")
|
|
33
|
+
.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict(),
|
|
36
|
+
part: z
|
|
37
|
+
.string()
|
|
38
|
+
.describe("Resource parts being written. Leave as the default.")
|
|
39
|
+
.default("snippet"),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
|
|
43
|
+
const outputSchema = PlaylistItemSchema;
|
|
44
|
+
|
|
45
|
+
const definition = defineTool({
|
|
46
|
+
name: "addVideoToPlaylist",
|
|
47
|
+
title: "Add Video To Playlist",
|
|
48
|
+
description:
|
|
49
|
+
"Add a video to a playlist owned by the authenticated user. Resolve the playlist id via listPlaylists. Returns the new playlistItem id (distinct from the video id — use it with removeVideoFromPlaylist).",
|
|
50
|
+
inputSchema,
|
|
51
|
+
outputSchema,
|
|
52
|
+
annotations: {
|
|
53
|
+
readOnlyHint: false,
|
|
54
|
+
destructiveHint: false,
|
|
55
|
+
idempotentHint: false,
|
|
56
|
+
openWorldHint: true,
|
|
57
|
+
},
|
|
58
|
+
connection: "youtube",
|
|
59
|
+
run: async (input, ctx) => {
|
|
60
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/playlistItems`);
|
|
61
|
+
url.searchParams.set("part", input.part);
|
|
62
|
+
|
|
63
|
+
const res = await ctx.fetch(url.toString(), {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ snippet: input.snippet }),
|
|
67
|
+
});
|
|
68
|
+
await throwForYouTube(res, "addVideoToPlaylist");
|
|
69
|
+
return res.json();
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export default definition;
|
|
74
|
+
|
|
75
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|