@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
|
@@ -0,0 +1,107 @@
|
|
|
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 {
|
|
7
|
+
NextPageToken,
|
|
8
|
+
PlaylistSchema,
|
|
9
|
+
throwForYouTube,
|
|
10
|
+
} from "../lib/youtube.ts";
|
|
11
|
+
|
|
12
|
+
const inputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
part: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Resource parts to return. Leave as the default.")
|
|
17
|
+
.default("snippet,contentDetails,status"),
|
|
18
|
+
mine: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.describe(
|
|
21
|
+
"List the authenticated user's own playlists. The default when no channelId or id is given.",
|
|
22
|
+
)
|
|
23
|
+
.optional(),
|
|
24
|
+
channelId: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe(
|
|
27
|
+
"List playlists owned by this channel id (UC...). Mutually exclusive with mine and id.",
|
|
28
|
+
)
|
|
29
|
+
.optional(),
|
|
30
|
+
id: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe(
|
|
33
|
+
"Fetch specific playlists by id (comma-separated, max 50). Mutually exclusive with mine and channelId.",
|
|
34
|
+
)
|
|
35
|
+
.optional(),
|
|
36
|
+
maxResults: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.gte(1)
|
|
40
|
+
.lte(50)
|
|
41
|
+
.describe(
|
|
42
|
+
"Max playlists per page (1-50). Defaults to 20 when omitted; pass a value when you need a specific number of results.",
|
|
43
|
+
)
|
|
44
|
+
.optional(),
|
|
45
|
+
pageToken: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe(
|
|
48
|
+
"Page cursor from a previous response's next_page_token. Omit for the first page.",
|
|
49
|
+
)
|
|
50
|
+
.optional(),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
const outputSchema = z.object({
|
|
54
|
+
items: z.array(PlaylistSchema).describe("The playlists."),
|
|
55
|
+
next_page_token: NextPageToken,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const definition = defineTool({
|
|
59
|
+
name: "listPlaylists",
|
|
60
|
+
title: "List Playlists",
|
|
61
|
+
description:
|
|
62
|
+
"List playlists owned by the authenticated user (default) or by a channel, or fetch specific playlists by id. The resolver for any playlistId input.",
|
|
63
|
+
inputSchema,
|
|
64
|
+
outputSchema,
|
|
65
|
+
annotations: {
|
|
66
|
+
readOnlyHint: true,
|
|
67
|
+
destructiveHint: false,
|
|
68
|
+
idempotentHint: true,
|
|
69
|
+
openWorldHint: true,
|
|
70
|
+
},
|
|
71
|
+
connection: "youtube",
|
|
72
|
+
run: async (input, ctx) => {
|
|
73
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/playlists`);
|
|
74
|
+
if (input.part !== undefined) {
|
|
75
|
+
url.searchParams.set("part", String(input.part));
|
|
76
|
+
}
|
|
77
|
+
if (input.mine !== undefined) {
|
|
78
|
+
url.searchParams.set("mine", String(input.mine));
|
|
79
|
+
}
|
|
80
|
+
if (input.channelId !== undefined) {
|
|
81
|
+
url.searchParams.set("channelId", String(input.channelId));
|
|
82
|
+
}
|
|
83
|
+
if (input.id !== undefined) {
|
|
84
|
+
url.searchParams.set("id", String(input.id));
|
|
85
|
+
}
|
|
86
|
+
url.searchParams.set("maxResults", String(input.maxResults ?? 20));
|
|
87
|
+
if (input.pageToken !== undefined) {
|
|
88
|
+
url.searchParams.set("pageToken", String(input.pageToken));
|
|
89
|
+
}
|
|
90
|
+
const res = await ctx.fetch(url.toString(), {
|
|
91
|
+
method: "GET",
|
|
92
|
+
});
|
|
93
|
+
await throwForYouTube(res, "listPlaylists");
|
|
94
|
+
const payload = (await res.json()) as {
|
|
95
|
+
items?: unknown;
|
|
96
|
+
nextPageToken?: string;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
items: payload.items ?? [],
|
|
100
|
+
next_page_token: payload.nextPageToken,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export default definition;
|
|
106
|
+
|
|
107
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,103 @@
|
|
|
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 {
|
|
7
|
+
NextPageToken,
|
|
8
|
+
SubscriptionSchema,
|
|
9
|
+
throwForYouTube,
|
|
10
|
+
} from "../lib/youtube.ts";
|
|
11
|
+
|
|
12
|
+
const inputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
part: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Resource parts to return. Leave as the default.")
|
|
17
|
+
.default("snippet,contentDetails"),
|
|
18
|
+
mine: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.describe("List the authenticated user's subscriptions. The default.")
|
|
21
|
+
.default(true),
|
|
22
|
+
forChannelId: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe(
|
|
25
|
+
"Restrict to a subscription to this specific channel id — returns one item if subscribed, none otherwise. Use to check subscription state.",
|
|
26
|
+
)
|
|
27
|
+
.optional(),
|
|
28
|
+
order: z
|
|
29
|
+
.enum(["alphabetical", "relevance", "unread"])
|
|
30
|
+
.describe("Sort order.")
|
|
31
|
+
.default("relevance"),
|
|
32
|
+
maxResults: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.gte(1)
|
|
36
|
+
.lte(50)
|
|
37
|
+
.describe(
|
|
38
|
+
"Max subscriptions per page (1-50). Defaults to 20 when omitted; pass a value when you need a specific number of results.",
|
|
39
|
+
)
|
|
40
|
+
.optional(),
|
|
41
|
+
pageToken: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe(
|
|
44
|
+
"Page cursor from a previous response's next_page_token. Omit for the first page.",
|
|
45
|
+
)
|
|
46
|
+
.optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
const outputSchema = z.object({
|
|
50
|
+
items: z.array(SubscriptionSchema).describe("The subscriptions."),
|
|
51
|
+
next_page_token: NextPageToken,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const definition = defineTool({
|
|
55
|
+
name: "listSubscriptions",
|
|
56
|
+
title: "List Subscriptions",
|
|
57
|
+
description:
|
|
58
|
+
"List the channels the authenticated user is subscribed to (default), or check whether the user subscribes to a specific channel via forChannelId.",
|
|
59
|
+
inputSchema,
|
|
60
|
+
outputSchema,
|
|
61
|
+
annotations: {
|
|
62
|
+
readOnlyHint: true,
|
|
63
|
+
destructiveHint: false,
|
|
64
|
+
idempotentHint: true,
|
|
65
|
+
openWorldHint: true,
|
|
66
|
+
},
|
|
67
|
+
connection: "youtube",
|
|
68
|
+
run: async (input, ctx) => {
|
|
69
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/subscriptions`);
|
|
70
|
+
if (input.part !== undefined) {
|
|
71
|
+
url.searchParams.set("part", String(input.part));
|
|
72
|
+
}
|
|
73
|
+
if (input.mine !== undefined) {
|
|
74
|
+
url.searchParams.set("mine", String(input.mine));
|
|
75
|
+
}
|
|
76
|
+
if (input.forChannelId !== undefined) {
|
|
77
|
+
url.searchParams.set("forChannelId", String(input.forChannelId));
|
|
78
|
+
}
|
|
79
|
+
if (input.order !== undefined) {
|
|
80
|
+
url.searchParams.set("order", String(input.order));
|
|
81
|
+
}
|
|
82
|
+
url.searchParams.set("maxResults", String(input.maxResults ?? 20));
|
|
83
|
+
if (input.pageToken !== undefined) {
|
|
84
|
+
url.searchParams.set("pageToken", String(input.pageToken));
|
|
85
|
+
}
|
|
86
|
+
const res = await ctx.fetch(url.toString(), {
|
|
87
|
+
method: "GET",
|
|
88
|
+
});
|
|
89
|
+
await throwForYouTube(res, "listSubscriptions");
|
|
90
|
+
const payload = (await res.json()) as {
|
|
91
|
+
items?: unknown;
|
|
92
|
+
nextPageToken?: string;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
items: payload.items ?? [],
|
|
96
|
+
next_page_token: payload.nextPageToken,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export default definition;
|
|
102
|
+
|
|
103
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { throwForYouTube, VideoCategorySchema } from "../lib/youtube.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
part: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("Resource parts to return. Leave as the default.")
|
|
13
|
+
.default("snippet"),
|
|
14
|
+
regionCode: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe(
|
|
17
|
+
"ISO 3166-1 alpha-2 country code whose category set to return (e.g. US, GB). Categories vary by region.",
|
|
18
|
+
)
|
|
19
|
+
.default("US"),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
const outputSchema = z.object({
|
|
23
|
+
items: z
|
|
24
|
+
.array(VideoCategorySchema)
|
|
25
|
+
.describe("The categories for the region."),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const definition = defineTool({
|
|
29
|
+
name: "listVideoCategories",
|
|
30
|
+
title: "List Video Categories",
|
|
31
|
+
description:
|
|
32
|
+
"List the assignable video categories for a region (the id/title pairs that categoryId accepts on a video). Resolver for the category an upload or update should use.",
|
|
33
|
+
inputSchema,
|
|
34
|
+
outputSchema,
|
|
35
|
+
annotations: {
|
|
36
|
+
readOnlyHint: true,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: true,
|
|
39
|
+
openWorldHint: true,
|
|
40
|
+
},
|
|
41
|
+
connection: "youtube",
|
|
42
|
+
run: async (input, ctx) => {
|
|
43
|
+
const url = new URL(
|
|
44
|
+
`https://www.googleapis.com/youtube/v3/videoCategories`,
|
|
45
|
+
);
|
|
46
|
+
if (input.part !== undefined) {
|
|
47
|
+
url.searchParams.set("part", String(input.part));
|
|
48
|
+
}
|
|
49
|
+
if (input.regionCode !== undefined) {
|
|
50
|
+
url.searchParams.set("regionCode", String(input.regionCode));
|
|
51
|
+
}
|
|
52
|
+
const res = await ctx.fetch(url.toString(), {
|
|
53
|
+
method: "GET",
|
|
54
|
+
});
|
|
55
|
+
await throwForYouTube(res, "listVideoCategories");
|
|
56
|
+
const payload = (await res.json()) as { items?: unknown };
|
|
57
|
+
return { items: payload.items ?? [] };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default definition;
|
|
62
|
+
|
|
63
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,65 @@
|
|
|
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 { CommentThreadSchema, throwForYouTube } from "../lib/youtube.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
snippet: z
|
|
11
|
+
.object({
|
|
12
|
+
videoId: z.string().describe("The video to comment on."),
|
|
13
|
+
topLevelComment: z
|
|
14
|
+
.object({
|
|
15
|
+
snippet: z
|
|
16
|
+
.object({
|
|
17
|
+
textOriginal: z.string().describe("The comment text to post."),
|
|
18
|
+
})
|
|
19
|
+
.strict(),
|
|
20
|
+
})
|
|
21
|
+
.strict(),
|
|
22
|
+
})
|
|
23
|
+
.strict(),
|
|
24
|
+
part: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe("Resource parts being written. Leave as the default.")
|
|
27
|
+
.default("snippet"),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
const outputSchema = CommentThreadSchema;
|
|
31
|
+
|
|
32
|
+
const definition = defineTool({
|
|
33
|
+
name: "postComment",
|
|
34
|
+
title: "Post Comment",
|
|
35
|
+
description:
|
|
36
|
+
"Post a new top-level comment on a video. For a reply to an existing comment, use replyToComment instead. Requires the youtube.force-ssl scope.",
|
|
37
|
+
inputSchema,
|
|
38
|
+
outputSchema,
|
|
39
|
+
annotations: {
|
|
40
|
+
readOnlyHint: false,
|
|
41
|
+
destructiveHint: false,
|
|
42
|
+
idempotentHint: false,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
},
|
|
45
|
+
connection: "youtube",
|
|
46
|
+
run: async (input, ctx) => {
|
|
47
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/commentThreads`);
|
|
48
|
+
if (input.part !== undefined) {
|
|
49
|
+
url.searchParams.set("part", String(input.part));
|
|
50
|
+
}
|
|
51
|
+
const body: Record<string, unknown> = {};
|
|
52
|
+
if (input.snippet !== undefined) body["snippet"] = input.snippet;
|
|
53
|
+
const res = await ctx.fetch(url.toString(), {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
});
|
|
58
|
+
await throwForYouTube(res, "postComment");
|
|
59
|
+
return res.json();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default definition;
|
|
64
|
+
|
|
65
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { SuccessResultSchema, throwForYouTube } from "../lib/youtube.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
id: z.string().describe("The id of the video to rate."),
|
|
11
|
+
rating: z
|
|
12
|
+
.enum(["like", "dislike", "none"])
|
|
13
|
+
.describe(
|
|
14
|
+
"like or dislike to set the rating; none to remove an existing rating.",
|
|
15
|
+
),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
|
|
19
|
+
const outputSchema = SuccessResultSchema;
|
|
20
|
+
|
|
21
|
+
const definition = defineTool({
|
|
22
|
+
name: "rateVideo",
|
|
23
|
+
title: "Rate Video",
|
|
24
|
+
description:
|
|
25
|
+
"Like, dislike, or clear the authenticated user's rating on a video (the thumbs up/down action). Use rating=none to remove an existing rating. The API returns no body; the connector synthesizes a success flag.",
|
|
26
|
+
inputSchema,
|
|
27
|
+
outputSchema,
|
|
28
|
+
annotations: {
|
|
29
|
+
readOnlyHint: false,
|
|
30
|
+
destructiveHint: false,
|
|
31
|
+
// Setting the same rating twice leaves the same state.
|
|
32
|
+
idempotentHint: true,
|
|
33
|
+
openWorldHint: true,
|
|
34
|
+
},
|
|
35
|
+
connection: "youtube",
|
|
36
|
+
run: async (input, ctx) => {
|
|
37
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/videos/rate`);
|
|
38
|
+
url.searchParams.set("id", input.id);
|
|
39
|
+
url.searchParams.set("rating", input.rating);
|
|
40
|
+
|
|
41
|
+
// videos.rate returns 204 No Content on success — synthesize the success flag.
|
|
42
|
+
const res = await ctx.fetch(url.toString(), { method: "POST" });
|
|
43
|
+
await throwForYouTube(res, "rateVideo");
|
|
44
|
+
return { success: true as const };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export default definition;
|
|
49
|
+
|
|
50
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { SuccessResultSchema, throwForYouTube } from "../lib/youtube.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
id: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"The playlistItem id to remove (the item's own id from listPlaylistItems, not the video id).",
|
|
14
|
+
),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const outputSchema = SuccessResultSchema;
|
|
18
|
+
|
|
19
|
+
const definition = defineTool({
|
|
20
|
+
name: "removeVideoFromPlaylist",
|
|
21
|
+
title: "Remove Video From Playlist",
|
|
22
|
+
description:
|
|
23
|
+
"Remove an item from a playlist. The id is the playlistItem id (NOT the video id) — get it from listPlaylistItems.",
|
|
24
|
+
inputSchema,
|
|
25
|
+
outputSchema,
|
|
26
|
+
annotations: {
|
|
27
|
+
readOnlyHint: false,
|
|
28
|
+
// Reversible — the video can be re-added with addVideoToPlaylist.
|
|
29
|
+
destructiveHint: false,
|
|
30
|
+
idempotentHint: true,
|
|
31
|
+
openWorldHint: true,
|
|
32
|
+
},
|
|
33
|
+
connection: "youtube",
|
|
34
|
+
run: async (input, ctx) => {
|
|
35
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/playlistItems`);
|
|
36
|
+
if (input.id !== undefined) {
|
|
37
|
+
url.searchParams.set("id", String(input.id));
|
|
38
|
+
}
|
|
39
|
+
const res = await ctx.fetch(url.toString(), {
|
|
40
|
+
method: "DELETE",
|
|
41
|
+
});
|
|
42
|
+
await throwForYouTube(res, "removeVideoFromPlaylist");
|
|
43
|
+
return { success: true as const };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default definition;
|
|
48
|
+
|
|
49
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConnectorHttpError,
|
|
4
|
+
defineTool,
|
|
5
|
+
handleIfScriptMain,
|
|
6
|
+
} from "@zapier/connectors-sdk";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { connectionResolvers } from "../connections.ts";
|
|
10
|
+
import {
|
|
11
|
+
CommentSchema,
|
|
12
|
+
hasYouTubeReason,
|
|
13
|
+
throwForYouTube,
|
|
14
|
+
} from "../lib/youtube.ts";
|
|
15
|
+
|
|
16
|
+
const inputSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
snippet: z
|
|
19
|
+
.object({
|
|
20
|
+
parentId: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe(
|
|
23
|
+
"The TOP-LEVEL comment thread id to reply to (from listComments) — not a reply id. Replies are single-level: you cannot reply to a reply.",
|
|
24
|
+
),
|
|
25
|
+
textOriginal: z.string().describe("The reply text to post."),
|
|
26
|
+
})
|
|
27
|
+
.strict(),
|
|
28
|
+
part: z
|
|
29
|
+
.string()
|
|
30
|
+
.describe("Resource parts being written. Leave as the default.")
|
|
31
|
+
.default("snippet"),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const outputSchema = CommentSchema;
|
|
35
|
+
|
|
36
|
+
const definition = defineTool({
|
|
37
|
+
name: "replyToComment",
|
|
38
|
+
title: "Reply To Comment",
|
|
39
|
+
description:
|
|
40
|
+
"Reply to an existing top-level comment thread. The parentId must be a top-level comment thread id from listComments — replies are single-level, so you cannot reply to a reply (YouTube rejects it with operationNotSupported). Requires the youtube.force-ssl scope.",
|
|
41
|
+
inputSchema,
|
|
42
|
+
outputSchema,
|
|
43
|
+
annotations: {
|
|
44
|
+
readOnlyHint: false,
|
|
45
|
+
destructiveHint: false,
|
|
46
|
+
idempotentHint: false,
|
|
47
|
+
openWorldHint: true,
|
|
48
|
+
},
|
|
49
|
+
connection: "youtube",
|
|
50
|
+
run: async (input, ctx) => {
|
|
51
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/comments`);
|
|
52
|
+
if (input.part !== undefined) {
|
|
53
|
+
url.searchParams.set("part", String(input.part));
|
|
54
|
+
}
|
|
55
|
+
const body: Record<string, unknown> = {};
|
|
56
|
+
if (input.snippet !== undefined) body["snippet"] = input.snippet;
|
|
57
|
+
const res = await ctx.fetch(url.toString(), {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
});
|
|
62
|
+
if (res.ok) return res.json();
|
|
63
|
+
|
|
64
|
+
// Replies are single-level: passing a reply's id as parentId (instead of a
|
|
65
|
+
// top-level thread id) is rejected with operationNotSupported. Translate it
|
|
66
|
+
// into actionable guidance rather than surfacing the raw upstream reason.
|
|
67
|
+
if (res.status === 400) {
|
|
68
|
+
const errBody = (await res.json().catch(() => null)) as unknown;
|
|
69
|
+
if (hasYouTubeReason(errBody, "operationNotSupported")) {
|
|
70
|
+
throw ConnectorHttpError.fromResponseBody(res, errBody, {
|
|
71
|
+
message:
|
|
72
|
+
"YouTube replyToComment 400: operationNotSupported — parentId must be a top-level comment thread id (from listComments), not a reply. YouTube does not support nested replies.",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
throw ConnectorHttpError.fromResponseBody(res, errBody, {
|
|
76
|
+
message: `YouTube replyToComment 400: ${
|
|
77
|
+
(errBody as { error?: { message?: string } })?.error?.message ??
|
|
78
|
+
"bad request"
|
|
79
|
+
}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await throwForYouTube(res, "replyToComment");
|
|
84
|
+
return res.json();
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export default definition;
|
|
89
|
+
|
|
90
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,169 @@
|
|
|
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 {
|
|
7
|
+
NextPageToken,
|
|
8
|
+
SearchResultSchema,
|
|
9
|
+
throwForYouTube,
|
|
10
|
+
} from "../lib/youtube.ts";
|
|
11
|
+
|
|
12
|
+
const inputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
part: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Resource parts to return. Leave as the default.")
|
|
17
|
+
.default("snippet"),
|
|
18
|
+
type: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe(
|
|
21
|
+
"Resource type to search. Leave as the default to search videos.",
|
|
22
|
+
)
|
|
23
|
+
.default("video"),
|
|
24
|
+
q: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe(
|
|
27
|
+
'Free-text search query. Supports the NOT (-) and OR (|) operators, e.g. "boating|sailing -fishing".',
|
|
28
|
+
)
|
|
29
|
+
.optional(),
|
|
30
|
+
channelId: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe(
|
|
33
|
+
"Restrict results to videos uploaded by this channel id (UC...). Resolve via getChannel.",
|
|
34
|
+
)
|
|
35
|
+
.optional(),
|
|
36
|
+
forMine: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.describe(
|
|
39
|
+
"Restrict results to the authenticated user's own videos. Requires type=video. Cannot be combined with channelId.",
|
|
40
|
+
)
|
|
41
|
+
.optional(),
|
|
42
|
+
order: z
|
|
43
|
+
.enum(["date", "rating", "relevance", "title", "videoCount", "viewCount"])
|
|
44
|
+
.describe("Sort order for results.")
|
|
45
|
+
.default("relevance"),
|
|
46
|
+
publishedAfter: z
|
|
47
|
+
.string()
|
|
48
|
+
.datetime({ offset: true })
|
|
49
|
+
.describe(
|
|
50
|
+
"Only return videos published at or after this RFC3339 datetime, e.g. 2026-01-01T00:00:00Z.",
|
|
51
|
+
)
|
|
52
|
+
.optional(),
|
|
53
|
+
publishedBefore: z
|
|
54
|
+
.string()
|
|
55
|
+
.datetime({ offset: true })
|
|
56
|
+
.describe("Only return videos published before this RFC3339 datetime.")
|
|
57
|
+
.optional(),
|
|
58
|
+
videoDuration: z
|
|
59
|
+
.enum(["any", "long", "medium", "short"])
|
|
60
|
+
.describe(
|
|
61
|
+
"Filter by length. short = <4 min, medium = 4-20 min, long = >20 min.",
|
|
62
|
+
)
|
|
63
|
+
.optional(),
|
|
64
|
+
regionCode: z
|
|
65
|
+
.string()
|
|
66
|
+
.describe(
|
|
67
|
+
"ISO 3166-1 alpha-2 country code (e.g. US, GB) to return results for.",
|
|
68
|
+
)
|
|
69
|
+
.optional(),
|
|
70
|
+
relevanceLanguage: z
|
|
71
|
+
.string()
|
|
72
|
+
.describe("ISO 639-1 language code (e.g. en, es) to bias results toward.")
|
|
73
|
+
.optional(),
|
|
74
|
+
maxResults: z
|
|
75
|
+
.number()
|
|
76
|
+
.int()
|
|
77
|
+
.gte(1)
|
|
78
|
+
.lte(50)
|
|
79
|
+
.describe(
|
|
80
|
+
"Max results per page (1-50). Defaults to 10 when omitted; pass a value when you need a specific number of results.",
|
|
81
|
+
)
|
|
82
|
+
.optional(),
|
|
83
|
+
pageToken: z
|
|
84
|
+
.string()
|
|
85
|
+
.describe(
|
|
86
|
+
"Page cursor from a previous response's next_page_token. Omit for the first page.",
|
|
87
|
+
)
|
|
88
|
+
.optional(),
|
|
89
|
+
})
|
|
90
|
+
.strict();
|
|
91
|
+
const outputSchema = z.object({
|
|
92
|
+
items: z.array(SearchResultSchema).describe("The search hits."),
|
|
93
|
+
next_page_token: NextPageToken,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const definition = defineTool({
|
|
97
|
+
name: "searchVideos",
|
|
98
|
+
title: "Search Videos",
|
|
99
|
+
description:
|
|
100
|
+
"Search YouTube for videos by keyword, channel, date, or duration. Returns lightweight results (id + snippet); call getVideo for statistics and contentDetails. Metered against a separate Search Queries quota bucket, independent of the main 10,000-unit pool.",
|
|
101
|
+
inputSchema,
|
|
102
|
+
outputSchema,
|
|
103
|
+
annotations: {
|
|
104
|
+
readOnlyHint: true,
|
|
105
|
+
destructiveHint: false,
|
|
106
|
+
idempotentHint: true,
|
|
107
|
+
openWorldHint: true,
|
|
108
|
+
},
|
|
109
|
+
connection: "youtube",
|
|
110
|
+
run: async (input, ctx) => {
|
|
111
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/search`);
|
|
112
|
+
if (input.part !== undefined) {
|
|
113
|
+
url.searchParams.set("part", String(input.part));
|
|
114
|
+
}
|
|
115
|
+
if (input.type !== undefined) {
|
|
116
|
+
url.searchParams.set("type", String(input.type));
|
|
117
|
+
}
|
|
118
|
+
if (input.q !== undefined) {
|
|
119
|
+
url.searchParams.set("q", String(input.q));
|
|
120
|
+
}
|
|
121
|
+
if (input.channelId !== undefined) {
|
|
122
|
+
url.searchParams.set("channelId", String(input.channelId));
|
|
123
|
+
}
|
|
124
|
+
if (input.forMine !== undefined) {
|
|
125
|
+
url.searchParams.set("forMine", String(input.forMine));
|
|
126
|
+
}
|
|
127
|
+
if (input.order !== undefined) {
|
|
128
|
+
url.searchParams.set("order", String(input.order));
|
|
129
|
+
}
|
|
130
|
+
if (input.publishedAfter !== undefined) {
|
|
131
|
+
url.searchParams.set("publishedAfter", String(input.publishedAfter));
|
|
132
|
+
}
|
|
133
|
+
if (input.publishedBefore !== undefined) {
|
|
134
|
+
url.searchParams.set("publishedBefore", String(input.publishedBefore));
|
|
135
|
+
}
|
|
136
|
+
if (input.videoDuration !== undefined) {
|
|
137
|
+
url.searchParams.set("videoDuration", String(input.videoDuration));
|
|
138
|
+
}
|
|
139
|
+
if (input.regionCode !== undefined) {
|
|
140
|
+
url.searchParams.set("regionCode", String(input.regionCode));
|
|
141
|
+
}
|
|
142
|
+
if (input.relevanceLanguage !== undefined) {
|
|
143
|
+
url.searchParams.set(
|
|
144
|
+
"relevanceLanguage",
|
|
145
|
+
String(input.relevanceLanguage),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
url.searchParams.set("maxResults", String(input.maxResults ?? 10));
|
|
149
|
+
if (input.pageToken !== undefined) {
|
|
150
|
+
url.searchParams.set("pageToken", String(input.pageToken));
|
|
151
|
+
}
|
|
152
|
+
const res = await ctx.fetch(url.toString(), {
|
|
153
|
+
method: "GET",
|
|
154
|
+
});
|
|
155
|
+
await throwForYouTube(res, "searchVideos");
|
|
156
|
+
const payload = (await res.json()) as {
|
|
157
|
+
items?: unknown;
|
|
158
|
+
nextPageToken?: string;
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
items: payload.items ?? [],
|
|
162
|
+
next_page_token: payload.nextPageToken,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export default definition;
|
|
168
|
+
|
|
169
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|