@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/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.0",
4
- "description": "Placeholder published to enable OIDC Trusted Publisher setup. The real package is published via GitLab CI.",
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
- "private": false
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 });