Skip to content

Commit 21d44e0

Browse files
authored
Merge pull request #99 from vkop007/71-YoutubeNewFeatureandBugFixed
feat: implement YouTube community post capabilities and add correspon…
2 parents 0635b20 + 5f5ae78 commit 21d44e0

9 files changed

Lines changed: 1183 additions & 62 deletions

File tree

skills/autocli/references/providers/youtube.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Interact with YouTube using an imported browser session for public lookup, engag
2626

2727
- `autocli social youtube login`
2828
- `autocli social youtube login --cookies ./cookiestest/youtube.json`
29-
- `autocli social youtube upload ./video.mp4 --title "AutoCLI upload" --visibility private`
29+
- `autocli social youtube status`
3030
- `autocli social youtube capabilities --json`
3131

3232
## Default Command
@@ -59,6 +59,19 @@ Options:
5959
- `--browser`: Open a real browser, wait for manual login, then save the extracted session (default when no cookie flags are provided)
6060
- `--browser-timeout <seconds>`: Maximum seconds to wait for manual browser login (default: 600)
6161

62+
### `status`
63+
64+
Usage:
65+
```bash
66+
autocli social youtube status [options]
67+
```
68+
69+
Show the saved YouTube session status
70+
71+
Options:
72+
73+
- `--account <name>`: Optional override for a specific saved YouTube session
74+
6275
### `upload`
6376

6477
Usage:
@@ -89,11 +102,14 @@ Usage:
89102
autocli social youtube post [options] <text>
90103
```
91104

92-
YouTube text posting is not implemented in this CLI
105+
Publish a YouTube community post, optionally with one image, through a browser-backed Community tab flow
93106

94107
Options:
95108

96109
- `--account <name>`: Optional override for a specific saved YouTube session
110+
- `--image <path>`: Attach one image to the YouTube community post
111+
- `--browser`: Force the post through the shared AutoCLI browser profile instead of the invisible browser-backed path
112+
- `--browser-timeout <seconds>`: Maximum seconds to allow the browser action to complete
97113

98114
### `search`
99115

@@ -234,6 +250,23 @@ Options:
234250

235251
- `--account <name>`: Optional override for a specific saved YouTube session
236252

253+
### `delete`
254+
255+
Usage:
256+
```bash
257+
autocli social youtube delete [options] <target>
258+
```
259+
260+
Aliases: `remove`
261+
262+
Delete your own YouTube community post by /post URL, community?lb= URL, or post ID through a browser-backed flow
263+
264+
Options:
265+
266+
- `--account <name>`: Optional override for a specific saved YouTube session
267+
- `--browser`: Force the delete through the shared AutoCLI browser profile instead of the invisible browser-backed path
268+
- `--browser-timeout <seconds>`: Maximum seconds to allow the browser action to complete
269+
237270
### `subscribe`
238271

239272
Usage:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { buildPlatformCommand } from "../../../../core/runtime/build-platform-command.js";
4+
5+
import { youtubePlatformDefinition } from "../manifest.js";
6+
7+
describe("youtube capability command surface", () => {
8+
test("exposes a status command for saved-session health checks", () => {
9+
const command = buildPlatformCommand(youtubePlatformDefinition);
10+
const byName = new Map(command.commands.map((entry) => [entry.name(), entry]));
11+
const subcommand = byName.get("status");
12+
13+
expect(subcommand).toBeDefined();
14+
expect(subcommand!.options.map((option) => option.flags)).toContain("--account <name>");
15+
});
16+
17+
test("exposes a browser-backed post command with timeout control", () => {
18+
const command = buildPlatformCommand(youtubePlatformDefinition);
19+
const byName = new Map(command.commands.map((entry) => [entry.name(), entry]));
20+
const subcommand = byName.get("post");
21+
22+
expect(subcommand).toBeDefined();
23+
expect(subcommand!.description()).toContain("community");
24+
expect(subcommand!.options.map((option) => option.flags)).toContain("--image <path>");
25+
expect(subcommand!.options.map((option) => option.flags)).toContain("--browser");
26+
expect(subcommand!.options.map((option) => option.flags)).toContain("--browser-timeout <seconds>");
27+
});
28+
29+
test("exposes a browser-backed delete command for community posts", () => {
30+
const command = buildPlatformCommand(youtubePlatformDefinition);
31+
const byName = new Map(command.commands.map((entry) => [entry.name(), entry]));
32+
const subcommand = byName.get("delete");
33+
34+
expect(subcommand).toBeDefined();
35+
expect(subcommand!.description()).toContain("community post");
36+
expect(subcommand!.aliases()).toContain("remove");
37+
expect(subcommand!.options.map((option) => option.flags)).toContain("--browser");
38+
expect(subcommand!.options.map((option) => option.flags)).toContain("--browser-timeout <seconds>");
39+
});
40+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { youtubeAdapter } from "../adapter.js";
4+
import { resolveYouTubeCommunityPostTarget } from "../service.js";
5+
6+
describe("youtube subscription params extraction", () => {
7+
test("extracts subscribe and unsubscribe params for the target channel from ytInitialData", () => {
8+
const html = `
9+
<html>
10+
<script>
11+
var ytInitialData = {
12+
"header": {
13+
"pageHeaderRenderer": {
14+
"content": {
15+
"pageHeaderViewModel": {
16+
"actions": {
17+
"flexibleActionsViewModel": {
18+
"actionsRows": [
19+
{
20+
"actions": [
21+
{
22+
"subscribeButtonViewModel": {
23+
"subscribeButtonContent": {
24+
"onTapCommand": {
25+
"innertubeCommand": {
26+
"subscribeEndpoint": {
27+
"channelIds": ["UCTARGET123"],
28+
"params": "SUBSCRIBE_TOKEN"
29+
}
30+
}
31+
}
32+
},
33+
"unsubscribeButtonContent": {
34+
"onTapCommand": {
35+
"innertubeCommand": {
36+
"signalServiceEndpoint": {
37+
"actions": [
38+
{
39+
"openPopupAction": {
40+
"popup": {
41+
"confirmDialogRenderer": {
42+
"confirmButton": {
43+
"buttonRenderer": {
44+
"serviceEndpoint": {
45+
"unsubscribeEndpoint": {
46+
"channelIds": ["UCTARGET123"],
47+
"params": "UNSUBSCRIBE_TOKEN"
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
]
57+
}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
]
64+
}
65+
]
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
};
73+
</script>
74+
</html>
75+
`;
76+
77+
const adapter = youtubeAdapter as any;
78+
expect(adapter.extractSubscriptionMutationParams(html, "UCTARGET123", true)).toBe("SUBSCRIBE_TOKEN");
79+
expect(adapter.extractSubscriptionMutationParams(html, "UCTARGET123", false)).toBe("UNSUBSCRIBE_TOKEN");
80+
expect(adapter.extractSubscriptionMutationParams(html, "UCOTHER", true)).toBeUndefined();
81+
});
82+
});
83+
84+
describe("youtube community post target parsing", () => {
85+
test("normalizes canonical and community lb URLs to a /post target", () => {
86+
expect(resolveYouTubeCommunityPostTarget("https://www.youtube.com/post/Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7")).toEqual({
87+
postId: "Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
88+
url: "https://www.youtube.com/post/Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
89+
});
90+
91+
expect(resolveYouTubeCommunityPostTarget("https://www.youtube.com/channel/UC123/community?lb=Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7")).toEqual({
92+
postId: "Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
93+
url: "https://www.youtube.com/post/Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
94+
});
95+
});
96+
97+
test("accepts a bare YouTube community post id", () => {
98+
expect(resolveYouTubeCommunityPostTarget("Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7")).toEqual({
99+
postId: "Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
100+
url: "https://www.youtube.com/post/Ugkx30z9Hvx7V3TbwDBOc8FEbcax4HDgLma7",
101+
});
102+
});
103+
});

src/platforms/social/youtube/capabilities/auth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ export const youtubeLoginCapability = createAdapterActionCapability({
1111
options: createCookieLoginOptions(),
1212
action: ({ options }) => youtubeAdapter.login(resolveCookieLoginInput(options)),
1313
});
14+
15+
export const youtubeStatusCapability = createAdapterActionCapability({
16+
id: "status",
17+
command: "status",
18+
description: "Show the saved YouTube session status",
19+
spinnerText: "Checking YouTube session...",
20+
successMessage: "YouTube session checked.",
21+
options: [{ flags: "--account <name>", description: "Optional override for a specific saved YouTube session" }],
22+
action: ({ options }) => youtubeAdapter.statusAction(options.account as string | undefined),
23+
});

src/platforms/social/youtube/capabilities/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { youtubeLoginCapability } from "./auth.js";
1+
import { youtubeLoginCapability, youtubeStatusCapability } from "./auth.js";
22
import {
33
youtubeCaptionsCapability,
44
youtubeChannelIdCapability,
@@ -11,6 +11,7 @@ import {
1111
} from "./media.js";
1212
import {
1313
youtubeCommentCapability,
14+
youtubeDeleteCapability,
1415
youtubeDislikeCapability,
1516
youtubeLikeCapability,
1617
youtubeSubscribeCapability,
@@ -20,6 +21,7 @@ import {
2021

2122
export const youtubeCapabilities = [
2223
youtubeLoginCapability,
24+
youtubeStatusCapability,
2325
youtubeUploadCapability,
2426
youtubePostCapability,
2527
youtubeSearchCapability,
@@ -32,6 +34,7 @@ export const youtubeCapabilities = [
3234
youtubeDislikeCapability,
3335
youtubeUnlikeCapability,
3436
youtubeCommentCapability,
37+
youtubeDeleteCapability,
3538
youtubeSubscribeCapability,
3639
youtubeUnsubscribeCapability,
3740
] as const;

src/platforms/social/youtube/capabilities/media.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../output.js";
1010
import { parseYouTubeLimitOption } from "../options.js";
1111
import { youtubeAdapter } from "../adapter.js";
12+
import { parseBrowserTimeoutSeconds } from "../../../shared/cookie-login.js";
1213

1314
export const youtubeUploadCapability = createAdapterActionCapability({
1415
id: "upload",
@@ -49,14 +50,26 @@ export const youtubeUploadCapability = createAdapterActionCapability({
4950
export const youtubePostCapability = createAdapterActionCapability({
5051
id: "post",
5152
command: "post <text>",
52-
description: "YouTube text posting is not implemented in this CLI",
53-
spinnerText: "Checking YouTube posting support...",
54-
successMessage: "YouTube action completed.",
55-
options: [{ flags: "--account <name>", description: "Optional override for a specific saved YouTube session" }],
53+
description: "Publish a YouTube community post, optionally with one image, through a browser-backed Community tab flow",
54+
spinnerText: "Publishing YouTube community post...",
55+
successMessage: "YouTube community post published.",
56+
options: [
57+
{ flags: "--account <name>", description: "Optional override for a specific saved YouTube session" },
58+
{ flags: "--image <path>", description: "Attach one image to the YouTube community post" },
59+
{ flags: "--browser", description: "Force the post through the shared AutoCLI browser profile instead of the invisible browser-backed path" },
60+
{
61+
flags: "--browser-timeout <seconds>",
62+
description: "Maximum seconds to allow the browser action to complete",
63+
parser: parseBrowserTimeoutSeconds,
64+
},
65+
],
5666
action: ({ args, options }) =>
5767
youtubeAdapter.postText({
5868
account: options.account as string | undefined,
5969
text: String(args[0] ?? ""),
70+
imagePath: options.image as string | undefined,
71+
browser: Boolean(options.browser),
72+
browserTimeoutSeconds: options.browserTimeout as number | undefined,
6073
}),
6174
});
6275

src/platforms/social/youtube/capabilities/write.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createAdapterActionCapability } from "../../../../core/runtime/capability-helpers.js";
2+
import { parseBrowserTimeoutSeconds } from "../../../shared/cookie-login.js";
23
import { youtubeAdapter } from "../adapter.js";
34

45
export const youtubeLikeCapability = createAdapterActionCapability({
@@ -58,6 +59,31 @@ export const youtubeCommentCapability = createAdapterActionCapability({
5859
}),
5960
});
6061

62+
export const youtubeDeleteCapability = createAdapterActionCapability({
63+
id: "delete",
64+
command: "delete <target>",
65+
aliases: ["remove"],
66+
description: "Delete your own YouTube community post by /post URL, community?lb= URL, or post ID through a browser-backed flow",
67+
spinnerText: "Deleting YouTube community post...",
68+
successMessage: "YouTube community post deleted.",
69+
options: [
70+
{ flags: "--account <name>", description: "Optional override for a specific saved YouTube session" },
71+
{ flags: "--browser", description: "Force the delete through the shared AutoCLI browser profile instead of the invisible browser-backed path" },
72+
{
73+
flags: "--browser-timeout <seconds>",
74+
description: "Maximum seconds to allow the browser action to complete",
75+
parser: parseBrowserTimeoutSeconds,
76+
},
77+
],
78+
action: ({ args, options }) =>
79+
youtubeAdapter.deletePost({
80+
account: options.account as string | undefined,
81+
target: String(args[0] ?? ""),
82+
browser: Boolean(options.browser),
83+
browserTimeoutSeconds: options.browserTimeout as number | undefined,
84+
}),
85+
});
86+
6187
export const youtubeSubscribeCapability = createAdapterActionCapability({
6288
id: "subscribe",
6389
command: "subscribe <target>",

src/platforms/social/youtube/manifest.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export const youtubePlatformDefinition: PlatformDefinition = {
1515
examples: [
1616
"autocli social youtube login",
1717
"autocli social youtube login --cookies ./cookiestest/youtube.json",
18+
"autocli social youtube status",
19+
"autocli social youtube post \"Shipping a new video soon\"",
20+
"autocli social youtube post \"Sneak peek\" --image ./cover.png",
21+
"autocli social youtube delete https://www.youtube.com/post/Ugkx1234567890",
1822
"autocli social youtube upload ./video.mp4 --title \"AutoCLI upload\" --visibility private",
1923
"autocli social youtube upload ./video.mp4 --title \"AutoCLI upload\" --description \"Uploaded from AutoCLI\" --tags cli,automation --visibility unlisted",
2024
"autocli tools download video https://www.youtube.com/watch?v=dQw4w9WgXcQ --platform youtube",

0 commit comments

Comments
 (0)