youtube-data-cli 0.0.1 → 1.0.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # youtube-data-cli
2
2
 
3
- YouTube Data API CLI for AI agents (and humans). Search videos, manage playlists, read and post comments, handle subscriptions, and more.
3
+ YouTube Data API CLI for AI agents (and humans). Full coverage of the YouTube Data API v3: search, channels, videos, playlists, comments, subscriptions, captions, thumbnails, and more.
4
4
 
5
5
  **Works with:** OpenClaw, Claude Code, Cursor, Codex, and any agent that can run shell commands.
6
6
 
@@ -25,16 +25,29 @@ Or run directly: `npx youtube-data-cli --help`
25
25
 
26
26
  Built on the official [YouTube Data API v3](https://developers.google.com/youtube/v3). Uses native `fetch` with no external dependencies beyond `commander`. Every command outputs structured JSON to stdout, ready for agents to parse without extra processing.
27
27
 
28
- Core endpoints covered:
28
+ All 20 YouTube Data API v3 resources covered:
29
29
 
30
30
  - **[Search](https://developers.google.com/youtube/v3/docs/search/list)** -- search for videos, channels, and playlists
31
- - **[Channels](https://developers.google.com/youtube/v3/docs/channels/list)** -- get channel details and statistics
32
- - **[Videos](https://developers.google.com/youtube/v3/docs/videos/list)** -- get video details and statistics
33
- - **[Playlists](https://developers.google.com/youtube/v3/docs/playlists)** -- list, create, update, and delete playlists
31
+ - **[Channels](https://developers.google.com/youtube/v3/docs/channels)** -- get and update channel details
32
+ - **[Videos](https://developers.google.com/youtube/v3/docs/videos)** -- get, upload, update, delete, rate videos
33
+ - **[Playlists](https://developers.google.com/youtube/v3/docs/playlists)** -- CRUD playlists
34
34
  - **[PlaylistItems](https://developers.google.com/youtube/v3/docs/playlistItems)** -- manage videos in playlists
35
+ - **[PlaylistImages](https://developers.google.com/youtube/v3/docs/playlistImages)** -- manage playlist cover images
35
36
  - **[CommentThreads](https://developers.google.com/youtube/v3/docs/commentThreads)** -- list and post top-level comments
36
- - **[Comments](https://developers.google.com/youtube/v3/docs/comments)** -- list, reply, update, and delete comments
37
+ - **[Comments](https://developers.google.com/youtube/v3/docs/comments)** -- CRUD comments, set moderation status
37
38
  - **[Subscriptions](https://developers.google.com/youtube/v3/docs/subscriptions)** -- list, subscribe, and unsubscribe
39
+ - **[Activities](https://developers.google.com/youtube/v3/docs/activities)** -- list channel activities
40
+ - **[Captions](https://developers.google.com/youtube/v3/docs/captions)** -- list, upload, download, update, delete captions
41
+ - **[ChannelBanners](https://developers.google.com/youtube/v3/docs/channelBanners)** -- upload channel banner images
42
+ - **[ChannelSections](https://developers.google.com/youtube/v3/docs/channelSections)** -- CRUD channel sections
43
+ - **[I18nLanguages](https://developers.google.com/youtube/v3/docs/i18nLanguages)** -- list supported languages
44
+ - **[I18nRegions](https://developers.google.com/youtube/v3/docs/i18nRegions)** -- list supported regions
45
+ - **[Members](https://developers.google.com/youtube/v3/docs/members)** -- list channel members
46
+ - **[MembershipsLevels](https://developers.google.com/youtube/v3/docs/membershipsLevels)** -- list membership levels
47
+ - **[Thumbnails](https://developers.google.com/youtube/v3/docs/thumbnails)** -- upload video thumbnails
48
+ - **[VideoCategories](https://developers.google.com/youtube/v3/docs/videoCategories)** -- list video categories
49
+ - **[VideoAbuseReportReasons](https://developers.google.com/youtube/v3/docs/videoAbuseReportReasons)** -- list abuse report reasons
50
+ - **[Watermarks](https://developers.google.com/youtube/v3/docs/watermarks)** -- set and unset channel watermarks
38
51
 
39
52
  ## Setup
40
53
 
@@ -44,8 +57,8 @@ This CLI supports two authentication methods:
44
57
 
45
58
  | Method | Use case | Commands |
46
59
  |--------|----------|----------|
47
- | **API key** | Public data (search, channels, videos, playlists, comments) | Read-only commands |
48
- | **OAuth 2.0** | Private data + write operations | All commands (required for `*-insert`, `*-update`, `*-delete`, `mine` queries) |
60
+ | **API key** | Public data | `search`, `channels`, `videos`, `playlists`, `playlist-items`, `comment-threads`, `comments`, `channel-sections`, `i18n-languages`, `i18n-regions`, `video-categories`, `video-abuse-report-reasons` |
61
+ | **OAuth 2.0** | Private data + write operations | All `*-insert`, `*-update`, `*-delete` commands, `--mine` queries, `captions`, `members`, `memberships-levels`, `playlist-images`, `thumbnails-set`, `watermarks-*`, `channel-banners-insert` |
49
62
 
50
63
  ### Option 1: API key only (public data)
51
64
 
@@ -63,9 +76,12 @@ For write operations and private data:
63
76
  2. Create a project and enable the **YouTube Data API v3**.
64
77
  3. Create an **OAuth 2.0 Client ID** (Desktop app type) under "Credentials".
65
78
  4. Use the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground/) or your own flow to obtain a refresh token with the required scopes:
66
- - `https://www.googleapis.com/auth/youtube` (full access)
67
- - or `https://www.googleapis.com/auth/youtube.readonly` (read-only)
68
- - or `https://www.googleapis.com/auth/youtube.force-ssl` (for comments)
79
+ - `https://www.googleapis.com/auth/youtube` -- recommended, covers all operations
80
+ - `https://www.googleapis.com/auth/youtube.upload` -- add this if using a narrower scope but need video uploads
81
+
82
+ Narrower scopes (if you don't need full access):
83
+ - `https://www.googleapis.com/auth/youtube.readonly` -- read-only access (no write operations)
84
+ - `https://www.googleapis.com/auth/youtube.force-ssl` -- videos, comments, captions, ratings (read + write)
69
85
 
70
86
  > **Note:** Service accounts do NOT work with YouTube APIs. You must use OAuth 2.0 with a refresh token.
71
87
 
@@ -100,6 +116,8 @@ Credentials are resolved in this order:
100
116
  2. `YOUTUBE_API_KEY`, `YOUTUBE_CLIENT_ID`, `YOUTUBE_CLIENT_SECRET`, `YOUTUBE_REFRESH_TOKEN` env vars
101
117
  3. `~/.config/youtube-data-cli/credentials.json` (auto-detected)
102
118
 
119
+ > **Tip:** If you also use [youtube-analytics-cli](https://github.com/Bin-Huang/youtube-analytics-cli), the environment variable names are the same, so credentials set via env vars are shared automatically between both CLIs.
120
+
103
121
  ## Usage
104
122
 
105
123
  All commands output pretty-printed JSON by default. Use `--format compact` for compact single-line JSON.
@@ -400,6 +418,303 @@ youtube-data-cli subscriptions-delete --id SUBSCRIPTION_ID
400
418
  Options:
401
419
  - `--id <id>` -- subscription ID (required)
402
420
 
421
+ ### channels-update
422
+
423
+ Update a channel's metadata (OAuth required).
424
+
425
+ ```bash
426
+ youtube-data-cli channels-update --id UCxxxxxxxxxxxxxx --description "New description"
427
+ youtube-data-cli channels-update --id UCxxxxxxxxxxxxxx --country US --keywords "tech,coding"
428
+ ```
429
+
430
+ Options:
431
+ - `--id <id>` -- channel ID (required)
432
+ - `--description <desc>` -- channel description
433
+ - `--keywords <kw>` -- channel keywords
434
+ - `--default-language <lang>` -- default language (ISO 639-1)
435
+ - `--country <code>` -- country (ISO 3166-1 alpha-2)
436
+ - `--made-for-kids <bool>` -- self-declared made for kids (true/false)
437
+
438
+ ### videos-insert
439
+
440
+ Upload a video (OAuth required).
441
+
442
+ ```bash
443
+ youtube-data-cli videos-insert --file video.mp4 --title "My Video"
444
+ youtube-data-cli videos-insert --file video.mp4 --title "My Video" --description "About this" --tags "tag1,tag2" --privacy public
445
+ ```
446
+
447
+ Options:
448
+ - `--file <path>` -- path to video file (required)
449
+ - `--title <title>` -- video title (required)
450
+ - `--description <desc>` -- video description
451
+ - `--tags <tags>` -- comma-separated tags
452
+ - `--category-id <id>` -- video category ID (default: `22`)
453
+ - `--privacy <status>` -- `public`, `private`, `unlisted` (default: `private`)
454
+ - `--content-type <type>` -- video MIME type (default: `video/mp4`)
455
+
456
+ ### videos-update
457
+
458
+ Update video metadata (OAuth required).
459
+
460
+ ```bash
461
+ youtube-data-cli videos-update --id VIDEO_ID --title "Updated Title" --category-id 22
462
+ youtube-data-cli videos-update --id VIDEO_ID --title "Title" --category-id 22 --description "Desc" --tags "a,b" --privacy public
463
+ ```
464
+
465
+ Options:
466
+ - `--id <id>` -- video ID (required)
467
+ - `--title <title>` -- video title (required)
468
+ - `--category-id <id>` -- video category ID (required, use `video-categories` to list IDs)
469
+ - `--description <desc>` -- video description
470
+ - `--tags <tags>` -- comma-separated tags
471
+ - `--privacy <status>` -- `public`, `private`, `unlisted`
472
+ - `--default-language <lang>` -- default language (ISO 639-1)
473
+
474
+ ### videos-delete
475
+
476
+ Delete a video (OAuth required).
477
+
478
+ ```bash
479
+ youtube-data-cli videos-delete --id VIDEO_ID
480
+ ```
481
+
482
+ ### videos-rate
483
+
484
+ Rate a video (OAuth required).
485
+
486
+ ```bash
487
+ youtube-data-cli videos-rate --id VIDEO_ID --rating like
488
+ youtube-data-cli videos-rate --id VIDEO_ID --rating none # remove rating
489
+ ```
490
+
491
+ Options:
492
+ - `--id <id>` -- video ID (required)
493
+ - `--rating <rating>` -- `like`, `dislike`, or `none` (required)
494
+
495
+ ### videos-get-rating
496
+
497
+ Get your rating on videos (OAuth required).
498
+
499
+ ```bash
500
+ youtube-data-cli videos-get-rating --id VIDEO_ID
501
+ youtube-data-cli videos-get-rating --id VIDEO_ID1,VIDEO_ID2
502
+ ```
503
+
504
+ ### videos-report-abuse
505
+
506
+ Report a video for abuse (OAuth required).
507
+
508
+ ```bash
509
+ youtube-data-cli videos-report-abuse --video-id VIDEO_ID --reason-id REASON_ID
510
+ ```
511
+
512
+ Options:
513
+ - `--video-id <id>` -- video ID (required)
514
+ - `--reason-id <id>` -- abuse reason ID (required)
515
+ - `--secondary-reason-id <id>` -- secondary reason ID
516
+ - `--comments <text>` -- additional comments
517
+
518
+ ### comments-set-moderation-status
519
+
520
+ Set moderation status of comments (OAuth required).
521
+
522
+ ```bash
523
+ youtube-data-cli comments-set-moderation-status --id COMMENT_ID --moderation-status published
524
+ youtube-data-cli comments-set-moderation-status --id COMMENT_ID --moderation-status rejected --ban-author
525
+ ```
526
+
527
+ Options:
528
+ - `--id <ids>` -- comment ID(s), comma-separated (required)
529
+ - `--moderation-status <status>` -- `published`, `heldForReview`, `rejected` (required)
530
+ - `--ban-author` -- ban the author from future comments
531
+
532
+ ### activities
533
+
534
+ List channel activities.
535
+
536
+ ```bash
537
+ youtube-data-cli activities --channel-id UCxxxxxxxxxxxxxx
538
+ youtube-data-cli activities --mine # your activities (OAuth required)
539
+ ```
540
+
541
+ Options:
542
+ - `--channel-id <id>` -- channel ID
543
+ - `--mine` -- list your activities (OAuth required)
544
+ - `--part <parts>` -- parts to include (default: `snippet,contentDetails`)
545
+ - `--max-results <n>` -- max results 1-50 (default: `25`)
546
+ - `--page-token <token>` -- pagination token
547
+ - `--published-after <datetime>` -- filter after date (RFC 3339)
548
+ - `--published-before <datetime>` -- filter before date (RFC 3339)
549
+
550
+ ### captions
551
+
552
+ List caption tracks for a video.
553
+
554
+ ```bash
555
+ youtube-data-cli captions --video-id VIDEO_ID
556
+ ```
557
+
558
+ ### captions-insert
559
+
560
+ Upload a caption track (OAuth required).
561
+
562
+ ```bash
563
+ youtube-data-cli captions-insert --video-id VIDEO_ID --file subs.srt --language en --name "English"
564
+ ```
565
+
566
+ Options:
567
+ - `--video-id <id>` -- video ID (required)
568
+ - `--file <path>` -- path to caption file (required)
569
+ - `--language <lang>` -- caption language BCP-47 (required)
570
+ - `--name <name>` -- caption track name (required)
571
+ - `--content-type <type>` -- MIME type (default: `application/octet-stream`)
572
+ - `--is-draft` -- mark as draft
573
+
574
+ ### captions-update
575
+
576
+ Update a caption track (OAuth required).
577
+
578
+ ```bash
579
+ youtube-data-cli captions-update --id CAPTION_ID --file new-subs.srt
580
+ youtube-data-cli captions-update --id CAPTION_ID --is-draft false
581
+ ```
582
+
583
+ ### captions-download
584
+
585
+ Download a caption track (OAuth required).
586
+
587
+ ```bash
588
+ youtube-data-cli captions-download --id CAPTION_ID
589
+ youtube-data-cli captions-download --id CAPTION_ID --tfmt srt --output subs.srt
590
+ youtube-data-cli captions-download --id CAPTION_ID --tlang fr # translated
591
+ ```
592
+
593
+ Options:
594
+ - `--id <id>` -- caption track ID (required)
595
+ - `--tfmt <format>` -- format: `sbv`, `scc`, `srt`, `ttml`, `vtt`
596
+ - `--tlang <lang>` -- translation language (BCP-47)
597
+ - `--output <path>` -- save to file (default: stdout)
598
+
599
+ ### captions-delete
600
+
601
+ Delete a caption track (OAuth required).
602
+
603
+ ```bash
604
+ youtube-data-cli captions-delete --id CAPTION_ID
605
+ ```
606
+
607
+ ### channel-banners-insert
608
+
609
+ Upload a channel banner image (OAuth required). Returns a URL to use with channels-update.
610
+
611
+ ```bash
612
+ youtube-data-cli channel-banners-insert --file banner.jpg
613
+ ```
614
+
615
+ Options:
616
+ - `--file <path>` -- path to image file (required, max 6MB)
617
+ - `--content-type <type>` -- MIME type (default: `image/jpeg`)
618
+
619
+ ### channel-sections / channel-sections-insert / channel-sections-update / channel-sections-delete
620
+
621
+ Manage channel sections.
622
+
623
+ ```bash
624
+ # List
625
+ youtube-data-cli channel-sections --channel-id UCxxxxxxxxxxxxxx
626
+ youtube-data-cli channel-sections --mine
627
+
628
+ # Create (OAuth)
629
+ youtube-data-cli channel-sections-insert --type singlePlaylist --title "Featured" --playlist-ids PLxxxxxxxxxxxxxx
630
+
631
+ # Update (OAuth)
632
+ youtube-data-cli channel-sections-update --id SECTION_ID --type singlePlaylist --title "Updated"
633
+
634
+ # Delete (OAuth)
635
+ youtube-data-cli channel-sections-delete --id SECTION_ID
636
+ ```
637
+
638
+ ### i18n-languages / i18n-regions
639
+
640
+ List supported languages and regions.
641
+
642
+ ```bash
643
+ youtube-data-cli i18n-languages
644
+ youtube-data-cli i18n-languages --hl en
645
+ youtube-data-cli i18n-regions
646
+ youtube-data-cli i18n-regions --hl en
647
+ ```
648
+
649
+ ### members / memberships-levels
650
+
651
+ List channel members and membership levels (OAuth required).
652
+
653
+ ```bash
654
+ youtube-data-cli members
655
+ youtube-data-cli members --max-results 100
656
+ youtube-data-cli memberships-levels
657
+ ```
658
+
659
+ ### playlist-images / playlist-images-insert / playlist-images-update / playlist-images-delete
660
+
661
+ Manage playlist cover images.
662
+
663
+ ```bash
664
+ # List
665
+ youtube-data-cli playlist-images --parent PLxxxxxxxxxxxxxx
666
+
667
+ # Upload (OAuth)
668
+ youtube-data-cli playlist-images-insert --playlist-id PLxxxxxxxxxxxxxx --file cover.jpg
669
+
670
+ # Update (OAuth)
671
+ youtube-data-cli playlist-images-update --id IMAGE_ID --playlist-id PLxxxxxxxxxxxxxx --file new-cover.jpg
672
+
673
+ # Delete (OAuth)
674
+ youtube-data-cli playlist-images-delete --id IMAGE_ID
675
+ ```
676
+
677
+ ### thumbnails-set
678
+
679
+ Upload a custom thumbnail for a video (OAuth required).
680
+
681
+ ```bash
682
+ youtube-data-cli thumbnails-set --video-id VIDEO_ID --file thumb.jpg
683
+ ```
684
+
685
+ Options:
686
+ - `--video-id <id>` -- video ID (required)
687
+ - `--file <path>` -- path to image file (required)
688
+ - `--content-type <type>` -- MIME type (default: `image/jpeg`)
689
+
690
+ ### video-categories
691
+
692
+ List video categories.
693
+
694
+ ```bash
695
+ youtube-data-cli video-categories
696
+ youtube-data-cli video-categories --region-code JP --hl ja
697
+ youtube-data-cli video-categories --id 1,2,10
698
+ ```
699
+
700
+ ### video-abuse-report-reasons
701
+
702
+ List video abuse report reasons.
703
+
704
+ ```bash
705
+ youtube-data-cli video-abuse-report-reasons
706
+ youtube-data-cli video-abuse-report-reasons --hl en
707
+ ```
708
+
709
+ ### watermarks-set / watermarks-unset
710
+
711
+ Manage channel watermarks (OAuth required).
712
+
713
+ ```bash
714
+ youtube-data-cli watermarks-set --channel-id UCxxxxxxxxxxxxxx --file watermark.png
715
+ youtube-data-cli watermarks-unset --channel-id UCxxxxxxxxxxxxxx
716
+ ```
717
+
403
718
  ## Error output
404
719
 
405
720
  Errors are written to stderr as JSON with an `error` field and a non-zero exit code:
package/dist/api.js CHANGED
@@ -1,12 +1,21 @@
1
+ import { readFileSync } from "fs";
1
2
  import { getAccessToken } from "./auth.js";
2
3
  const DATA_API_BASE = "https://www.googleapis.com/youtube/v3";
3
- export async function callApi(endpoint, opts) {
4
+ const UPLOAD_API_BASE = "https://www.googleapis.com/upload/youtube/v3";
5
+ function buildSearchParams(params) {
6
+ const searchParams = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(params)) {
8
+ if (value !== undefined && value !== null && value !== "") {
9
+ searchParams.set(key, value);
10
+ }
11
+ }
12
+ return searchParams;
13
+ }
14
+ async function getAuthHeaders(opts, method) {
4
15
  const params = { ...opts.params };
5
- const method = opts.method ?? "GET";
6
16
  const headers = {
7
17
  "Content-Type": "application/json",
8
18
  };
9
- // Write operations always require OAuth
10
19
  const needsOAuth = opts.requireOAuth || method !== "GET" || !opts.creds.api_key;
11
20
  if (needsOAuth) {
12
21
  const token = await getAccessToken(opts.creds);
@@ -15,21 +24,94 @@ export async function callApi(endpoint, opts) {
15
24
  else {
16
25
  params.key = opts.creds.api_key;
17
26
  }
18
- const searchParams = new URLSearchParams();
19
- for (const [key, value] of Object.entries(params)) {
20
- if (value !== undefined && value !== null && value !== "") {
21
- searchParams.set(key, value);
22
- }
23
- }
27
+ return { headers, params };
28
+ }
29
+ export async function callApi(endpoint, opts) {
30
+ const method = opts.method ?? "GET";
31
+ const { headers, params } = await getAuthHeaders(opts, method);
32
+ const searchParams = buildSearchParams(params);
24
33
  const url = `${DATA_API_BASE}${endpoint}?${searchParams.toString()}`;
25
34
  const fetchOpts = { method, headers };
26
35
  if (opts.body && method !== "GET" && method !== "DELETE") {
27
36
  fetchOpts.body = JSON.stringify(opts.body);
28
37
  }
29
38
  const res = await fetch(url, fetchOpts);
30
- // DELETE may return 204 No Content
31
- if (method === "DELETE" && res.status === 204) {
32
- return { deleted: true };
39
+ // POST with 204 (rate) or DELETE with 204
40
+ if (res.status === 204) {
41
+ return { success: true };
42
+ }
43
+ if (opts.rawResponse) {
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ throw new Error(text || `HTTP ${res.status}`);
47
+ }
48
+ return res;
49
+ }
50
+ const json = (await res.json());
51
+ if (!res.ok || json.error) {
52
+ throw new Error(json.error?.message ?? `HTTP ${res.status}`);
53
+ }
54
+ return json;
55
+ }
56
+ /** Upload a file using multipart upload (metadata + file content) */
57
+ export async function uploadFile(opts) {
58
+ const token = await getAccessToken(opts.creds);
59
+ const method = opts.method ?? "POST";
60
+ const params = { ...opts.params, uploadType: "multipart" };
61
+ const searchParams = buildSearchParams(params);
62
+ const url = `${UPLOAD_API_BASE}${opts.endpoint}?${searchParams.toString()}`;
63
+ const fileContent = readFileSync(opts.filePath);
64
+ const boundary = "----YouTubeDataCLIBoundary" + Date.now();
65
+ const metadataPart = opts.body ? JSON.stringify(opts.body) : "{}";
66
+ const bodyParts = [
67
+ `--${boundary}\r\n`,
68
+ `Content-Type: application/json; charset=UTF-8\r\n\r\n`,
69
+ `${metadataPart}\r\n`,
70
+ `--${boundary}\r\n`,
71
+ `Content-Type: ${opts.contentType}\r\n\r\n`,
72
+ ];
73
+ const textEncoder = new TextEncoder();
74
+ const headerBytes = textEncoder.encode(bodyParts.join(""));
75
+ const footerBytes = textEncoder.encode(`\r\n--${boundary}--`);
76
+ const bodyBuffer = new Uint8Array(headerBytes.length + fileContent.length + footerBytes.length);
77
+ bodyBuffer.set(headerBytes, 0);
78
+ bodyBuffer.set(fileContent, headerBytes.length);
79
+ bodyBuffer.set(footerBytes, headerBytes.length + fileContent.length);
80
+ const res = await fetch(url, {
81
+ method,
82
+ headers: {
83
+ Authorization: `Bearer ${token}`,
84
+ "Content-Type": `multipart/related; boundary=${boundary}`,
85
+ },
86
+ body: bodyBuffer,
87
+ });
88
+ if (res.status === 204) {
89
+ return { success: true };
90
+ }
91
+ const json = (await res.json());
92
+ if (!res.ok || json.error) {
93
+ throw new Error(json.error?.message ?? `HTTP ${res.status}`);
94
+ }
95
+ return json;
96
+ }
97
+ /** Upload a file using simple media upload (file content only, no metadata) */
98
+ export async function uploadFileSimple(opts) {
99
+ const token = await getAccessToken(opts.creds);
100
+ const method = opts.method ?? "POST";
101
+ const params = { ...opts.params, uploadType: "media" };
102
+ const searchParams = buildSearchParams(params);
103
+ const url = `${UPLOAD_API_BASE}${opts.endpoint}?${searchParams.toString()}`;
104
+ const fileContent = readFileSync(opts.filePath);
105
+ const res = await fetch(url, {
106
+ method,
107
+ headers: {
108
+ Authorization: `Bearer ${token}`,
109
+ "Content-Type": opts.contentType,
110
+ },
111
+ body: fileContent,
112
+ });
113
+ if (res.status === 204) {
114
+ return { success: true };
33
115
  }
34
116
  const json = (await res.json());
35
117
  if (!res.ok || json.error) {
@@ -0,0 +1,42 @@
1
+ import { loadCredentials } from "../auth.js";
2
+ import { callApi } from "../api.js";
3
+ import { output, fatal } from "../utils.js";
4
+ export function registerActivityCommands(program) {
5
+ program
6
+ .command("activities")
7
+ .description("List channel activities")
8
+ .option("--channel-id <id>", "Channel ID")
9
+ .option("--mine", "List authenticated user's activities (OAuth required)")
10
+ .option("--part <parts>", "Parts to include", "snippet,contentDetails")
11
+ .option("--max-results <n>", "Max results (1-50)", "25")
12
+ .option("--page-token <token>", "Pagination token")
13
+ .option("--published-after <datetime>", "Filter after date (RFC 3339)")
14
+ .option("--published-before <datetime>", "Filter before date (RFC 3339)")
15
+ .action(async (opts) => {
16
+ try {
17
+ const creds = loadCredentials(program.opts().credentials);
18
+ const params = {
19
+ part: opts.part,
20
+ maxResults: opts.maxResults,
21
+ };
22
+ const requireOAuth = !!opts.mine || !opts.channelId;
23
+ if (opts.channelId) {
24
+ params.channelId = opts.channelId;
25
+ }
26
+ else {
27
+ params.mine = "true";
28
+ }
29
+ if (opts.pageToken)
30
+ params.pageToken = opts.pageToken;
31
+ if (opts.publishedAfter)
32
+ params.publishedAfter = opts.publishedAfter;
33
+ if (opts.publishedBefore)
34
+ params.publishedBefore = opts.publishedBefore;
35
+ const data = await callApi("/activities", { creds, params, requireOAuth });
36
+ output(data, program.opts().format);
37
+ }
38
+ catch (err) {
39
+ fatal(err.message);
40
+ }
41
+ });
42
+ }